<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[올리브영 테크블로그]]></title><description><![CDATA[올리브영 테크블로그입니다. 올리브영 엔지니어들의 활동과 디지털 사업본부의 다양한 경험과 문화를 공유합니다.]]></description><link>https://oliveyoung.tech</link><generator>GatsbyJS</generator><lastBuildDate>Mon, 09 Mar 2026 10:10:55 GMT</lastBuildDate><item><title><![CDATA[배송최적화 시스템 구축기 Part 01. 올리브영이 멀티 센터 체제로 배송 시간을 14시간 단축한 과정]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2026-03-06/delivery-optimization/</link><guid isPermaLink="false">https://oliveyoung.tech/2026-03-06/delivery-optimization/</guid><pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;br&gt;
&lt;p&gt;안녕하세요. 배송최적화 스쿼드에서 백엔드 개발을 담당하고 있는 리아입니다.🐣
&lt;br&gt;&lt;br&gt;
오늘은 저희 스쿼드에서 최근까지 진행해온 &lt;strong&gt;배송최적화 시스템 구축 과정&lt;/strong&gt;을 공유하려 합니다.&lt;br&gt;
서비스의 비약적인 성장에 발맞춰 &lt;strong&gt;단일 센터 구조에서 멀티 센터 구조로 전환하는 과정&lt;/strong&gt;은 단순한 인프라 확장을 넘어, 기존의 경직된 주문 처리 아키텍처를 전면 재설계해야 하는 복잡하고도 흥미로운 과제였습니다.
&lt;br&gt;&lt;br&gt;
저희 스쿼드가 어떤 고민을 거쳐 지금의 구조를 설계하게 되었는지, 그 과정의 기록을 시작해보겠습니다.
&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-왜-새로운-시스템이-필요했을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%99%9C-%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%B4-%ED%95%84%EC%9A%94%ED%96%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;1 왜 새로운 시스템이 필요했을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 왜 새로운 시스템이 필요했을까?&lt;/h2&gt;
&lt;p&gt;올리브영은 최근 몇 년간 비약적인 성장을 이어가고 있습니다.
2024년 연간 매출과 영업이익 모두 전년 대비 20% 이상 성장하였으며, 특히 &lt;strong&gt;온라인 사업은 가파른 성장세를 보이며 전체 매출의 30% 이상을 차지&lt;/strong&gt;하게 되었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🔗 관련 자료: 소비자가 만드는 신문, &lt;a href=&quot;https://www.consumernews.co.kr/news/articleView.html?idxno=738624&quot;&gt;&quot;CJ올리브영 &apos;옴니채널 전략&apos; 통했다...온라인 매출 비중 30% 돌파, 전체 매출도 &apos;쑥쑥&apos;&quot;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div style=&quot;text-align: center; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/10717fd3503841a670a96c283fc73e32/dc3b7/sales-graph.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACCElEQVR42nWTa2/TMBSG+///Al8QEhcxtMGE4APXrTAuRRsd20C0nQK9rOtlaWzHdtwkDyfpusEkLD1y7By/5z2+NJRSJCphcYNEK373+3R7PU5PT+lJ3+11GY/P6vhELW6QUGk1kiTBqZTCeHKhSDNKk4EviE467Dc/std8x85Okw/NL0TdAd7kOJ3jdXGFVWEl6ExKex6xPdzn+ajN5qDFvV973I/eszH8zMNxi4GNyUJCx+7S0bucLN5wuHjG13hbeMp+/JiO2kNrQ8OnluOkz0a0x2b0ibvdJrejt9yJmjwYtbjVfcXP+QiXxfwwr0Vwh87FC45nWxxdbHFy8YT27BHfJy8xYq5hjCFkAUquWbdCyEv5n5FVMcXl/GAG0RRSCY4tLAxlYqm0akHnncSWtVZRluQho3Apube1RggB5xzZUubJWRLwpGRYfGllJPsui43RK0EvwRSy1GcrQv6P02tBRyhSQRKVSxEpLqsq6+9rh1YCKpdrB6WUmIuD3EjF4VJQbkFe8L9WimgtmFaCkt2FFOuV9FpMShnLSijDSyIvrq3XjPUhE3vIebpiar8xdUeM1AEq65MaS0NrLRue1QezYlkfwBort6DGaSb6O2N1dMUwbtOfH8j5tJgsemglDuuXIpd7TTX+myqhql5Goki1OBXq3gTms4TxaMbZcCqcE8cxfwCUG0HBWv00YwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/10717fd3503841a670a96c283fc73e32/263a4/sales-graph.webp 480w,
/static/10717fd3503841a670a96c283fc73e32/a6361/sales-graph.webp 960w,
/static/10717fd3503841a670a96c283fc73e32/0b34d/sales-graph.webp 1920w,
/static/10717fd3503841a670a96c283fc73e32/7201c/sales-graph.webp 2636w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/10717fd3503841a670a96c283fc73e32/9aebd/sales-graph.png 480w,
/static/10717fd3503841a670a96c283fc73e32/a91f8/sales-graph.png 960w,
/static/10717fd3503841a670a96c283fc73e32/ac7a9/sales-graph.png 1920w,
/static/10717fd3503841a670a96c283fc73e32/dc3b7/sales-graph.png 2636w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/10717fd3503841a670a96c283fc73e32/ac7a9/sales-graph.png&quot; alt=&quot;올리브영 온라인 매출 추이&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;2021~2025년 올리브영 매출 추이&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;-단일-센터-체제의-한계와-멀티-센터로의-전환&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%8B%A8%EC%9D%BC-%EC%84%BC%ED%84%B0-%EC%B2%B4%EC%A0%9C%EC%9D%98-%ED%95%9C%EA%B3%84%EC%99%80-%EB%A9%80%ED%8B%B0-%EC%84%BC%ED%84%B0%EB%A1%9C%EC%9D%98-%EC%A0%84%ED%99%98&quot; aria-label=&quot; 단일 센터 체제의 한계와 멀티 센터로의 전환 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🏢 단일 센터 체제의 한계와 멀티 센터로의 전환&lt;/h3&gt;
&lt;p&gt;올리브영은 이러한 가파른 성장세를 뒷받침하기 위해 물류 인프라를 다각화해 왔습니다.
&lt;strong&gt;양지센터 중심의 단일 센터 체제를 넘어&lt;/strong&gt;, MFC(Micro Fulfillment Center, 도심형 물류 센터) 확대와 오늘드림 서비스 고도화를 병행하며 물류망의 범위를 넓히는 데 주력했습니다. &lt;br&gt;
이러한 물류 인프라 확장의 연장선으로 2025년 1월에는 비수도권 배송 효율을 극대화할 &lt;strong&gt;경산센터를 온라인몰 배송에도 본격 투입하며 전국 단위 멀티 센터 체제를 향한 물리적 토대를 마련&lt;/strong&gt;했습니다&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🔗 관련 자료: 올리브영 뉴스룸, &lt;a href=&quot;https://corp.oliveyoung.com/ko/news/38?pg=1&amp;#x26;category=&quot;&gt;&quot;CJ올리브영, 비수도권 물류 허브 ‘경산센터’ 본격 가동&quot;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div style=&quot;text-align: center; width: 60%; max-width: 800px; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1790px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/dfc21fe17983d38c9109f41480467488/e01d3/logistics.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACgElEQVR42o1UXW/aQBD0X69U9T+0itSqvyBS1T4k7UvEIyWSBTEkQJMQiPmqA/7A9t3tdM5ODGkMyaHFhtPOze7MrQOufr+PZrMJ13XR6XTQbrfRaDRwfX2DcgkOLQMFI6p4d+xXHMcIwxDr9bqIKIoQBAGSJHmWqCV9AW740ZJVv51DJ+cmQainiGVWRGCG0Eir/UxiZCZ+lrMXULOMq/wM7ewHptrFwnT57CCSacUs0guyy18HTCXEbfobp8MjnA6OMOK7r7q4Vx7m2kNi/kJE1/bW2dfk27SFr95HvG9+gLv5jgB9MmzDNy6WMiCU2SaIlFEHKI+nJmaJbw+f8cl/h7ObL1TcxSq9xUM+QLDpIs+DHQamjH0MJU+hohUWoYfJqoFh7xx/XA/an9FjA+DCA8aTLbvdkik6YrWkWlHhJdtkkyWQcA1ZR3xu6CvGhAAegToXELcNGd3ViunYXij6yxRN3mFJD6r1AhKFMPSmGQwJRsDuJdBjLJf1gHttk8UEWgFhZJ0P3I0Ldmi1MDk5wah1zrbpt6qsC0BeF2BFUN9nub2S2a+fmB0fwyegGPMWQIE2KcSyYqkVuzbZeV0+Oyy7Vx52uORSLcV7KSovS+X9LkDH41LZSzKkGFYQyfPXe2gvuQjLUHoLZoPDohCBrCzzwqu0i/wXFaBVOVTzkqX9P8tKEDttMjLRujSuTarp23Mf0i52quSy2RrVAii1BdlZoUqxSNbYxEkx6tI0rUafZp5TDsbDA3R3xIY8eBT4mN0z5nPMGXZ22hlqSMB5mYKSnS1X65pRRGbTCcEWmE2nbO2y6l95U6SG3VPJdXtUV/GwnPt2ouePaj+J8g/gJhK2QZlKrQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dfc21fe17983d38c9109f41480467488/263a4/logistics.webp 480w,
/static/dfc21fe17983d38c9109f41480467488/a6361/logistics.webp 960w,
/static/dfc21fe17983d38c9109f41480467488/cb4b0/logistics.webp 1790w&quot; sizes=&quot;(max-width: 1790px) 100vw, 1790px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dfc21fe17983d38c9109f41480467488/9aebd/logistics.png 480w,
/static/dfc21fe17983d38c9109f41480467488/a91f8/logistics.png 960w,
/static/dfc21fe17983d38c9109f41480467488/e01d3/logistics.png 1790w&quot; sizes=&quot;(max-width: 1790px) 100vw, 1790px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dfc21fe17983d38c9109f41480467488/e01d3/logistics.png&quot; alt=&quot;올리브영 전국 물류망&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;올리브영 전국 물류망(출처: 올리브영 뉴스룸)&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;하지만 단순히 물리적인 센터를 늘리는 것만으로는 문제가 해결되지 않았습니다. 오히려 늘어난 센터를 효율적으로 활용하지 못하는 &lt;strong&gt;기존 시스템의 구조적 한계&lt;/strong&gt;가 하나둘 드러나기 시작했습니다. 🥲&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;분배 로직의 부재로 인한 양지센터 과부하&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;센터가 확장되었음에도 주문은 여전히 기존 방식대로 양지센터로 인입되었습니다. 두 개의 센터가 존재함에도 특정 센터에만 물량이 쏠리는 현상이 반복되었고, 결과적으로 인프라 확장 효과를 체감하기 어려운 구조가 지속되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;수작업 기반의 비효율적인 물량 재분배&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;양지센터의 부하를 덜기 위해 경산센터로 주문을 넘길 때마다 운영자는 매번 주문 데이터를 엑셀로 추출하여 수동으로 할당해야 했습니다. 이러한 인적 자원에 의존하는 수작업 기반의 프로세스는 물류 전반의 운영 효율을 저하시키는 핵심 요인이 되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;지속되는 배송 지연과 고객 경험 저하&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;결국 센터 확장이라는 변화에도 불구하고 이를 뒷받침할 시스템의 부재로 배송 지연은 해소되지 않았고, 고객 문의와 주문 취소 증가라는 고객 경험 저하로 이어졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결국 물류 현장의 비효율을 걷어내고 거점 확장의 효과를 극대화하기 위해서는 &lt;strong&gt;전체 주문 흐름을 실시간으로 제어할 새로운 시스템&lt;/strong&gt;이 필요했습니다. &lt;br&gt;
이를 위해 저희 스쿼드는 &lt;strong&gt;배송최적화 시스템&lt;/strong&gt;을 구축하게 되었습니다.🚀&lt;/p&gt;
&lt;h2 id=&quot;2-기존-시스템은-어떻게-되어-있었나-as-is&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EA%B8%B0%EC%A1%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%90%98%EC%96%B4-%EC%9E%88%EC%97%88%EB%82%98-as-is&quot; aria-label=&quot;2 기존 시스템은 어떻게 되어 있었나 as is permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 기존 시스템은 어떻게 되어 있었나 (As-Is)&lt;/h2&gt;
&lt;div style=&quot;text-align: center; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d69655bd17ce3b6a71e73272dcbac841/77c53/as-is-process.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 7.291666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAABCAIAAABR8BlyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAASElEQVR42gE9AML/AOrq6uvr6/Pz8+nq6fXx9M3p0ZjToKvftOrn4+u6uuSysvHg4Ojfp+DOgezis/Dw8+Hh4fLy8uvr6+jo6CrZM66M4FdhAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d69655bd17ce3b6a71e73272dcbac841/263a4/as-is-process.webp 480w,
/static/d69655bd17ce3b6a71e73272dcbac841/a6361/as-is-process.webp 960w,
/static/d69655bd17ce3b6a71e73272dcbac841/0b34d/as-is-process.webp 1920w,
/static/d69655bd17ce3b6a71e73272dcbac841/da28f/as-is-process.webp 2880w,
/static/d69655bd17ce3b6a71e73272dcbac841/d0449/as-is-process.webp 3076w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d69655bd17ce3b6a71e73272dcbac841/9aebd/as-is-process.png 480w,
/static/d69655bd17ce3b6a71e73272dcbac841/a91f8/as-is-process.png 960w,
/static/d69655bd17ce3b6a71e73272dcbac841/ac7a9/as-is-process.png 1920w,
/static/d69655bd17ce3b6a71e73272dcbac841/f9c26/as-is-process.png 2880w,
/static/d69655bd17ce3b6a71e73272dcbac841/77c53/as-is-process.png 3076w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d69655bd17ce3b6a71e73272dcbac841/ac7a9/as-is-process.png&quot; alt=&quot;기존 주문 처리 시스템 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;기존 주문 처리 시스템 구조 (As-Is)&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;기존 주문 처리 구조는 앞서 설명해 드린 대로 &lt;strong&gt;양지센터 중심의 단일 센터 체제&lt;/strong&gt;를 전제로 설계되어 있었습니다.&lt;br&gt;
고객이 주문/결제를 완료하면 출하지시 MQ(Message Queue, 메시지 큐)를 통해 주문관리시스템(OMS) → 운송관리시스템(TCS) → 창고관리시스템(WMS) 순으로 하나의 흐름을 타고 내려가며, 최종적으로
양지센터에서만 출고가 이뤄지는 구조였습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 OMS, TCS, WMS 등 올리브영 물류 시스템의 전반적인 구조가
궁금하시다면, &lt;a href=&quot;https://oliveyoung.tech/2025-08-01/logistics-system/?keyword=%EC%9E%AC%EA%B3%A0&quot;&gt;&quot;올리브영 물류 시스템의 이해&quot;&lt;/a&gt; 포스팅을
참고해주세요.☺️&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;단일 센터 체제는 출고처가 고정되어 있었기에 별도의 복잡한 로직 없이 선형적 구조만으로도 안정적인 운영이 가능했습니다. &lt;br&gt;
하지만 운영하는 센터가 늘어나는 순간, &lt;strong&gt;단일 센터 중심의 경직된 주문 처리 구조는 다음과 같은 구조적인 한계를 드러내며 명확한 페인 포인트(Pain Point)가 되었습니다.&lt;/strong&gt;
&lt;br&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;데이터 단절로 인한 판단 불가&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;주문 단계에서 실시간 재고나 센터별 가동률(CAPA) 같은 물류 데이터를 즉시 확인하고 의사결정에 반영하기 어려웠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;경직된 주문 흐름&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존 창고 시스템(WMS)은 확정된 주문을 처리하는 데 특화되어 있어, 운영 상황에 따라 출고처를 유연하게 변경하거나 재배정하는 기능이 미비했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;정책 대응의 높은 비용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새로운 배송 정책이나 센터별 특이 사항이 발생할 때마다 각 시스템의 로직을 직접 수정해야 했기에, 급변하는 비즈니스 요구에 즉각 대응하기엔 한계가 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결국 기존 센터와 신규 센터 사이에서 최적의 출고처를 실시간으로 찾아줄 새로운 &lt;strong&gt;판단 레이어(Control Tower)&lt;/strong&gt; 가 절실해졌습니다.&lt;/p&gt;
&lt;h2 id=&quot;3-무엇이-달라졌나-to-be&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EB%82%98-to-be&quot; aria-label=&quot;3 무엇이 달라졌나 to be permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 무엇이 달라졌나 (To-Be)&lt;/h2&gt;
&lt;p&gt;앞서 살펴본 구조적 한계를 극복하고 확장된 물류 인프라를 지능적으로 제어하기 위해, 기존의 선형적인 주문 흐름 사이에 &apos;지능형 판단 레이어&apos;를 추가하여 데이터 흐름의 제어권을 확보하는 설계를 진행했습니다. &lt;br&gt;
단순히 시스템 하나를 추가하는 것을 넘어, &lt;strong&gt;주문 생성부터 출고에 이르는 전체 데이터 파이프라인에서 의사결정의 주체를 재정의&lt;/strong&gt;한 것이 이번 설계의 핵심입니다. 이를 위해 다음과 같은 &lt;strong&gt;네 가지 핵심 설계 원칙&lt;/strong&gt;을 세웠습니다.&lt;br&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;로직의 파편화를 방지하는 &apos;판단 레이어의 통합&apos;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존에는 주문 시점에 단순히 실시간 재고 유무만을 확인하여 출고처를 결정하는 단편적인 방식을 사용했습니다.&lt;/li&gt;
&lt;li&gt;새로운 시스템에서는 재고뿐만 아니라 센터별 실시간 가동률(CAPA), 고객 주소지 기반의 배송 권역 정보 등 파편화된 운영 변수를 한곳에 모아 종합적으로 판단합니다.&lt;/li&gt;
&lt;li&gt;이를 통해 OMS와 WMS 사이에서 복잡한 분배 로직을 전담 처리하는 독립적인 엔진을 구축했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;운영 리스크를 최소화하는 &apos;확정 전 단계의 제어&apos;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;설비 할당 이후 수정이 어려운 WMS의 경직성을 고려하여, &lt;strong&gt;출고 지시 데이터가 넘어가기 직전 단계에서&lt;/strong&gt; 모든 검증을 마치는 구조를 택했습니다.&lt;/li&gt;
&lt;li&gt;잘못된 센터 할당이 물류 현장의 병목으로 이어지지 않도록 실시간 재고와 CAPA를 다각도로 검증하는 &lt;strong&gt;독립적인 검증 레이어&lt;/strong&gt; 역할을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;비즈니스 속도에 대응하는 &apos;정책 가변성 확보&apos;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상시로 변하는 운영 정책을 코드 수정 없이 반영할 수 있도록 &lt;strong&gt;룰 기반(Rule-based) 엔진&lt;/strong&gt; 구조를 도입했습니다.&lt;/li&gt;
&lt;li&gt;특정 센터 우선 출고나 이벤트성 정책 변경 시에도 시스템 배포 없이 DB 설정값 변경만으로 유연한 대응이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;고객 가치를 지키는 &apos;데이터 기반의 SLA 보장&apos;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;센터별 가동률과 배송 권역 정보를 실시간 데이터로 판단하여 리드타임을 최소화하는 최적의 출고처를 도출합니다.&lt;/li&gt;
&lt;li&gt;모든 시스템 판단의 기준은 &lt;strong&gt;배송 리드타임 최소화&lt;/strong&gt;라는 본질적인 가치에 집중합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;이러한 원칙을 바탕으로 주문 관리(OMS)와 창고 관리(WMS) 사이에 배송최적화 시스템(Delivery Optimization System)을 배치하여, 주문 완료 후 실제 물류 센터로 출하지시 데이터가 넘어가는 시점에 각 주문별 최적의 출고처를 실시간으로 판단하도록 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;text-align: center; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/77c53/to-be.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 27.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAzUlEQVR42pWOwWrCQBCG8+h9jSIUeupB8KC9+QA91JaqIAhtSEjdbtYY1xh11437Nd320B4K+sPMB8P8/0yU5zlKKbTWqKWi1jVPcsz19JZufI9cpKjhkKzfp0oSPuI73kZXFOmAcl2RpknwSikRQhBZa/mqpmkCTy0ru+V1GZNtBM4dMasV+/aoOxw4mgKzy1qWwWOMCXTOBX/EP9pta2wbcKn+BJ586MyVpvss6E0UcbEJM+/9z5b/VWcGvixKOg/v3DzmzGT5HXjmh59wLH/vaqd3lQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/263a4/to-be.webp 480w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/a6361/to-be.webp 960w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/0b34d/to-be.webp 1920w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/da28f/to-be.webp 2880w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/d0449/to-be.webp 3076w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/9aebd/to-be.png 480w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/a91f8/to-be.png 960w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/ac7a9/to-be.png 1920w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/f9c26/to-be.png 2880w,
/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/77c53/to-be.png 3076w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f0b2fa06f5cbcee05dfaf7eac9bcc9ee/ac7a9/to-be.png&quot; alt=&quot;개선된 주문 처리 시스템 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;개선된 주문 처리 시스템 구조 (To-Be) - 배송최적화 시스템이 최적의 센터를 동적으로 결정&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;text-align: center; width: 90%; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3b1b8a0f1dbdf522d1a86548fa5334f7/2e609/to-be-aws.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAKABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAdxWhCxP/8QAFxAAAwEAAAAAAAAAAAAAAAAAAAEhMf/aAAgBAQABBQKiKPFh/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGRAAAgMBAAAAAAAAAAAAAAAAAAERIDEh/9oACAEBAAY/AtONQbT/xAAcEAEAAgEFAAAAAAAAAAAAAAABABEhEDFxgaH/2gAIAQEAAT8hTPDsiV8CBUsXxN6AMCtP/9oADAMBAAIAAwAAABBTD//EABYRAAMAAAAAAAAAAAAAAAAAABARQf/aAAgBAwEBPxCMf//EABYRAAMAAAAAAAAAAAAAAAAAABARQf/aAAgBAgEBPxCof//EABwQAQACAgMBAAAAAAAAAAAAAAERIQAxEFFhof/aAAgBAQABPxAJAKYshmvmJpEnXjW+8ASgWmzgEGyHDQQ8I4//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3b1b8a0f1dbdf522d1a86548fa5334f7/263a4/to-be-aws.webp 480w,
/static/3b1b8a0f1dbdf522d1a86548fa5334f7/a6361/to-be-aws.webp 960w,
/static/3b1b8a0f1dbdf522d1a86548fa5334f7/0b34d/to-be-aws.webp 1920w,
/static/3b1b8a0f1dbdf522d1a86548fa5334f7/dc78f/to-be-aws.webp 2489w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3b1b8a0f1dbdf522d1a86548fa5334f7/a3e66/to-be-aws.jpg 480w,
/static/3b1b8a0f1dbdf522d1a86548fa5334f7/fb816/to-be-aws.jpg 960w,
/static/3b1b8a0f1dbdf522d1a86548fa5334f7/aaf92/to-be-aws.jpg 1920w,
/static/3b1b8a0f1dbdf522d1a86548fa5334f7/2e609/to-be-aws.jpg 2489w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3b1b8a0f1dbdf522d1a86548fa5334f7/aaf92/to-be-aws.jpg&quot; alt=&quot;고가용성 배송최적화 시스템 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;고가용성을 고려한 배송최적화 시스템 아키텍처&lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;결과적으로 OMS는 복잡한 분배 로직을 직접 인지할 필요 없이, 배송최적화 시스템과의 API 연동만으로 실시간 물류 상황이 반영된 최적의 출고처를 할당받게 되었습니다.
주문 프로세스의 중심에 위치한 시스템인 만큼, 대규모 트래픽 부하 속에서도 &lt;strong&gt;100ms 이내의 빠른 응답 속도와 고가용성&lt;/strong&gt;을 보장하기 위해 아키텍처 내부에 설계한 핵심 기술 요소들을 이어지는 섹션에서 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;4-배송최적화-시스템-delivery-optimization-system&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EB%B0%B0%EC%86%A1%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-delivery-optimization-system&quot; aria-label=&quot;4 배송최적화 시스템 delivery optimization system permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 배송최적화 시스템 (Delivery Optimization System)&lt;/h2&gt;
&lt;p&gt;배송최적화 시스템은 유연한 물류 운영을 위해 크게 &lt;strong&gt;주문분배&lt;/strong&gt;와 &lt;strong&gt;주문이관&lt;/strong&gt;이라는 두 가지 핵심 기능을 수행합니다.
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;4-1-실시간으로-최적의-출고처를-결정하는-주문분배order-distribution&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-1-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%EC%B5%9C%EC%A0%81%EC%9D%98-%EC%B6%9C%EA%B3%A0%EC%B2%98%EB%A5%BC-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%EC%A3%BC%EB%AC%B8%EB%B6%84%EB%B0%B0order-distribution&quot; aria-label=&quot;4 1 실시간으로 최적의 출고처를 결정하는 주문분배order distribution permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4-1. 실시간으로 최적의 출고처를 결정하는 &apos;주문분배(Order Distribution)&apos;&lt;/h3&gt;
&lt;p&gt;배송 정책은 비즈니스 상황과 물류 전략에 따라 &lt;strong&gt;유연한 확장성이 요구되는 영역&lt;/strong&gt;입니다. &lt;br&gt;
최적의 출고처를 판단하기 위해 각 분배 로직을 독립적인 &lt;strong&gt;전략(Strategy) 단위로 모듈화&lt;/strong&gt;하고, 이를 조합해 최종 의사결정을 내리는 &lt;strong&gt;룰 엔진(Rule Engine)&lt;/strong&gt; 구조를 채택했습니다. &lt;br&gt;
&lt;br&gt;
특히 배송최적화 시스템은 일정 주기로 인입되는 대량의 출하지시 주문들을 지연 없이 처리하는 것이 핵심입니다.&lt;br&gt;
도메인 특성상 주문 건들이 특정 시점에 한꺼번에 내려오기 때문에, 수만 건의 주문이 일시에 유입되더라도 전체 프로세스가 정체되지 않아야 합니다.
이를 위해 개별 주문에 대한 판단 로직을 &lt;strong&gt;100ms 이내로 최적화&lt;/strong&gt;하여 대규모 트래픽 환경에서도 &lt;strong&gt;성능 병목(Bottleneck) 없는 안정적인 처리&lt;/strong&gt;가 가능해졌습니다. &lt;br&gt;
&lt;br&gt;
이러한 고성능과 회복 탄력성을 뒷받침하기 위해 시스템 내부에 설계한 핵심 장치들은 다음과 같습니다.&lt;/p&gt;
&lt;br&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 격리를 위한 서킷 브레이커(Circuit Breaker) 적용&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장애 전파 방지&lt;/strong&gt;: &lt;strong&gt;Resilience4j 기반의 서킷 브레이커&lt;/strong&gt;를 적용하여, 배송최적화 시스템의 부하가 상위 시스템(OMS)으로 전이되는 현상 차단&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시스템 안정성 확보&lt;/strong&gt;: 특정 구간의 과부하가 전체 주문 프로세스의 결함으로 이어지지 않도록 회복 탄력성 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;데이터 특성에 따른 다중 캐싱 전략&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local Cache(Caffeine)&lt;/strong&gt;: 센터별 가용량(Capacity) 등 &lt;strong&gt;정적 기준 정보&lt;/strong&gt;를 서버 메모리에 적재하여 네트워크 통신 지연(Latency) 최소화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote Cache(Redis)&lt;/strong&gt;: 실시간 출고량(Throughput) 등 &lt;strong&gt;동적 데이터&lt;/strong&gt;는 Redis로 관리하여, 분산 환경에서의 데이터 정합성 유지 및 100ms 이내의 판단 성능 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;전략 패턴 기반의 필터링 파이프라인&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;런타임 전략 결정&lt;/strong&gt;: 복잡한 &lt;code class=&quot;language-text&quot;&gt;if-else&lt;/code&gt; 구조 대신, DB 설정값에 따라 런타임에 실행 전략이 결정되는 파이프라인 구축&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 정책의 유연한 변경&lt;/strong&gt;: 개별 전략(Filter/Score)의 조합을 DB 값 변경만으로 제어하여, 코드 수정이나 배포 없는 운영 정책의 유연성 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 최적의 센터를 찾아가는 프로세스&lt;/strong&gt; &lt;br&gt;
1단계. &lt;strong&gt;SKU(Stock Keeping Unit, 상품 관리 코드) 및 재고 보유 확인&lt;/strong&gt;: 해당 상품 재고를 보유한 센터 판별 (경산/양지)&lt;br&gt;
2단계. &lt;strong&gt;CAPA(Capacity, 처리 용량) 여유 확인&lt;/strong&gt;: 재고는 있지만 일일 처리 한계를 초과한 센터는 제외&lt;br&gt;
3단계. &lt;strong&gt;우편번호 기준 권역 매칭&lt;/strong&gt;: 고객 주소지 기반으로 배송 권역 확인, 최적 센터 우선순위 결정&lt;br&gt;
4단계. &lt;strong&gt;출하지시 MQ 전송&lt;/strong&gt;: 최종 선택된 센터로 출하 지시 메시지 발행&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;div style=&quot;text-align: center; width: 50%; max-width: 800px; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1207px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/be1e3820b357104b0ff3020709b5e1d3/302da/workflow.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100.62500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAD3ElEQVR42mWTXW/bZBTH/Qm4QQKJD8DLBTCxO5gYCKkXEwIkQEJIaCAhxMXEVpDoRmlRhrppaAJNQKVRWN8zipq1aZbSTOkat2mXkjQvTcISJy1JbMeOY8eOY8fxO8d11yHxXBwdH5/f8/yf478Ry7Js204kEiiKxmKxUChUqVQymUwkEmk0GlApFArFYhEatra24G0ymcxms4AAiLgwTdPVWo3neZIkO52OoihAmqYpiiLLsbIssyxbp+o4jguCoKqqdbAQ+3/LOoi6rgPQ6/UOKtbRW1HstDju8OQaI+zuUykMh1ggOFnpQefa/ubWP/GuIOfrhY1KTNd027QZsRnEwlSTphn6TjnS7HDITEq4Gq5eDuSvhsqjO3KVURPUzlOzL7/qfxdj9t4IfPik90RoLwJHDURHHp889mP6xs87409MvXBh8xLiz/E3tplf0MrYenU63SZYBRdqb4c++fTuQJNnz6HDfcH3E2QG4OuZ6RP+t/yFlVuF5RcDb46mJ5CuonTkbp1mBFHqSLJhGNAHg6EoCpJms4mVMbge1GFO2XwOctO0sliu3W4jmqaZlk3WKbnb1XXDdGGOIHkH7ihSvU0buuHAmorzpKbrQPMtoaf2kGCO9SbZ3yKlqc2qN8XhbJfg8ZO+d07d/qDGEl+gnmM3++7sobDRtfjYMzOvjBfm4tvps+eHf59fRKaT/HerpOdW6uJC5od7QoGUSBF/bem915dPY3T5DDr4vK9vsbgC8JX46NNzJ3/Kja+Fo5+d/2ZsYhYpU0KWFOMlOl3ldsECkgJ928XEeioKSb52358ISpIEOdvm/PEgwzGGYaIbmzTdeOAwilR7ypETTN3UVd1xgmFaqmkaJngGbKEpmjtRiI5JYqVmIF2/ieYDKXIpVa+xEhC+peDM3Lwq94Kl8Oeop0CXwCSwWj3JsEzXXhARb4ofCVU8vuS3C5mRVXqHkDmGOnfB0//VxXQmd/ru2Udnn/3+r+sPYNmF3YVEMXYpJ8wnyPk47kuzpToP1eVQeH4xYGkW2LD/z6H9ZsV199HJh7ArgG+14IMf6RHb7UaDhkTpKiRBwaWdO9s20xV7hvYQ1nUwiUUQBPxr0GEYuruFu4tq6A1JgFzTHYaR2rppPISDyer0vdpEpOiN1X5dK/2Nt5xpOwN25K1iC2fC/Wki5pACPhgZ/iPvPZQH8LXbyeGF3JXQ/uVlbGAuFS06al3S1rWPVj5+ZPK5r9eH4MmbnXls8vhLvlO8gB+oM5GdvUYEa23ut9FiczXfIFjxv7KjlbUv14d2ibjjBb48uDE8lZ6wD5Vb/wLw+f+y3PIilwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/be1e3820b357104b0ff3020709b5e1d3/263a4/workflow.webp 480w,
/static/be1e3820b357104b0ff3020709b5e1d3/a6361/workflow.webp 960w,
/static/be1e3820b357104b0ff3020709b5e1d3/bb469/workflow.webp 1207w&quot; sizes=&quot;(max-width: 1207px) 100vw, 1207px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/be1e3820b357104b0ff3020709b5e1d3/9aebd/workflow.png 480w,
/static/be1e3820b357104b0ff3020709b5e1d3/a91f8/workflow.png 960w,
/static/be1e3820b357104b0ff3020709b5e1d3/302da/workflow.png 1207w&quot; sizes=&quot;(max-width: 1207px) 100vw, 1207px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/be1e3820b357104b0ff3020709b5e1d3/302da/workflow.png&quot; alt=&quot;배송최적화 시스템 워크플로우&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;배송최적화 시스템의 주문 처리 워크플로우&lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;4-2-운영-변수에-유연하게-대응하는-주문이관order-transfer&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-2-%EC%9A%B4%EC%98%81-%EB%B3%80%EC%88%98%EC%97%90-%EC%9C%A0%EC%97%B0%ED%95%98%EA%B2%8C-%EB%8C%80%EC%9D%91%ED%95%98%EB%8A%94-%EC%A3%BC%EB%AC%B8%EC%9D%B4%EA%B4%80order-transfer&quot; aria-label=&quot;4 2 운영 변수에 유연하게 대응하는 주문이관order transfer permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4-2. 운영 변수에 유연하게 대응하는 &apos;주문이관(Order Transfer)&apos;&lt;/h3&gt;
&lt;p&gt;주문분배가 초기 최적화를 담당한다면, &lt;strong&gt;주문이관&lt;/strong&gt;은 출고처 배정 이후의 부하를 조절하는 &lt;strong&gt;유동성 확보&lt;/strong&gt; 의 핵심입니다. &lt;br&gt;
특정 센터에 물량이 과도하게 집중될 때, 여유가 있는 다른 센터로 물량을 이동시켜 &lt;strong&gt;물류 적체를 해소&lt;/strong&gt;하는 역할을 합니다. &lt;br&gt;
&lt;br&gt;
특히 이 기능은 &lt;strong&gt;양지센터의 부하를 경산센터로 분산할 때&lt;/strong&gt; 진가를 발휘합니다.
과거에는 양지센터의 부하가 임계치에 도달하면 운영자가 수만 건의 데이터를 엑셀로 추출해 경산센터에 수동으로 할당하는 과정을 거쳐야 했습니다.
하지만 이제는 단 한번의 클릭만으로 수만 건의 물량을 안정적으로 이관할 수 있는 환경을 구축하여 운영 효율을 개선했습니다. &lt;br&gt;
&lt;br&gt;
이러한 대량의 데이터를 안정적으로 처리하기 위해 아키텍처 내부에 설계한 기술적 장치들은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;분산 환경에서의 데이터 정합성 보장&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis 기반의 &lt;strong&gt;분산락(Distributed Lock)을 적용&lt;/strong&gt;하여 다수 인스턴스 환경에서도 중복 이관을 방지하고 작업 멱등성 및 데이터 정합성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 격리&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외부 시스템(재고, OMS) 연동 시 발생하는 응답 지연이 전체 프로세스로 전파되지 않도록 구간별 서킷 브레이커를 배치하여 &lt;strong&gt;시스템 전반의 가용성 및 서비스 연속성&lt;/strong&gt;을 유지했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;데이터 최종 정합성 확보&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;재고 처리나 OMS 상태 변경 등 외부 시스템 연동 과정에서 일부 단계가 실패할 경우, 이미 처리된 앞선 단계를 원복하는 &lt;strong&gt;보상 트랜잭션(Compensating Transaction)&lt;/strong&gt; 을 설계하였습니다.&lt;/li&gt;
&lt;li&gt;각 단계별 실패 시나리오에 맞춰 상대 시스템의 롤백 API를 즉시 호출함으로써, 분산된 환경에서도 시스템 간 데이터 불일치 리스크를 최소화하고 최종적인 정합성을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 이관 실행 프로세스&lt;/strong&gt; &lt;br&gt;
1단계. &lt;strong&gt;사전 검증&lt;/strong&gt;: 대상 센터의 실시간 재고와 기존 WMS의 작업 상태를 확인하여 이관 가능 여부 판단&lt;br&gt;
2단계. &lt;strong&gt;상태 제어&lt;/strong&gt;: 기존 센터의 주문 취소와 신규 송장 채번, 재출하 지시를 하나의 트랜잭션처럼 동기화하여 처리&lt;br&gt;
3단계. &lt;strong&gt;재고 동기화&lt;/strong&gt;: 변경 전후 센터의 재고 데이터를 정확히 증감시켜 시스템과 실재고 사이의 오차 방지&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;운영 가시성 확보를 위한 모니터링 UI&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;실시간 트래킹&lt;/strong&gt;: 대량 주문 이관 진행 시 진행률과 성공/실패 여부를 실시간으로 확인할 수 있는 &lt;strong&gt;모니터링 UI&lt;/strong&gt; 구축&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 효율 개선&lt;/strong&gt;: 운영자는 수만 건의 이관 현황을 엑셀 수작업 없이 대시보드로 실시간 파악 및 관리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;div style=&quot;text-align: center; width: 60%; max-width: 800px; margin: 30px auto;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9e0615258c788adab6a7a4132ebeffb3/42a19/shipment-transfer.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWklEQVR42oWSaXOCMBCG+f+/yg/6sa2jpESgDkcCyKGYhDMHXXGsTg+780xmk8y7+7LEWi6XhMSe7wGu5yYJzbL0OWmaVFW1Wq0sjDGreZYcEpqSiEqppv/CGAOrbduWg51ChR98EzQoELZfrz3gtHart131+hNcvuyP9jC1CL1bUCCvMuyjfezTPI6SYB95JAtZV/e67VRzQ7RSXPN25HNnZDmOYybFBW9a0fdd17Wi4cMwXr39Glrru234Csb4/c4oKcfHEwgppRDiuxjvMGzDwi1ForWChsrIoWtZUTyKwRKMNoqicRzvA5vFk9ISNGaCkiC+eb4aNxfgRCsori/J3BkhBL/KaWRBS/9wDGjhk9wrziEfD1+wMZvXg5A5APm5T+XEttuN5bo7NYmK0VOTlYyWZ8K6HO7+gAODrmEINrIteGHwbEhMCKGXIBTyKIyeEAYhSBaLxScI1mdIrioUMwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9e0615258c788adab6a7a4132ebeffb3/263a4/shipment-transfer.webp 480w,
/static/9e0615258c788adab6a7a4132ebeffb3/a6361/shipment-transfer.webp 960w,
/static/9e0615258c788adab6a7a4132ebeffb3/d71bc/shipment-transfer.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9e0615258c788adab6a7a4132ebeffb3/9aebd/shipment-transfer.png 480w,
/static/9e0615258c788adab6a7a4132ebeffb3/a91f8/shipment-transfer.png 960w,
/static/9e0615258c788adab6a7a4132ebeffb3/42a19/shipment-transfer.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9e0615258c788adab6a7a4132ebeffb3/42a19/shipment-transfer.png&quot; alt=&quot;주문이관 진행 현황 UI&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;p style=&quot;font-size: 0.9em; color: #666; margin-top: 10px;&quot;&gt;주문이관 진행 현황 UI - 실시간으로 이관 진행률, 성공/실패 건수 모니터링 가능&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;5-배송최적화-시스템-도입-효과-9월-vs-12월-세일&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%B0%B0%EC%86%A1%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%8F%84%EC%9E%85-%ED%9A%A8%EA%B3%BC-9%EC%9B%94-vs-12%EC%9B%94-%EC%84%B8%EC%9D%BC&quot; aria-label=&quot;5 배송최적화 시스템 도입 효과 9월 vs 12월 세일 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 배송최적화 시스템 도입 효과 (9월 vs 12월 세일)&lt;/h2&gt;
&lt;p&gt;배송최적화 시스템 도입 후, 실제로 어떤 변화가 있었을까요? &lt;br&gt;
객관적인 성능 검증을 위해 트래픽이 가장 집중되는 &apos;올영세일&apos; 기간을 기준으로 도입 전(9월)과 도입 후(12월)의 성과 지표를 대조해 보았습니다. &lt;br&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;참고&lt;/strong&gt;: 두 기간 모두 물류 센터의 가용량(CAPA) 한계치까지 트래픽이 몰리는 고부하 상황이라는 공통점을 기반으로 대조군을 설정했습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 30px;&quot;&gt;
&lt;div style=&quot;flex: 1; min-width: 250px; border: 1px solid #e1e4e8; border-radius: 12px; padding: 20px; background-color: #fcfcfc; border-top: 4px solid #16a34a;&quot;&gt;
&lt;div style=&quot;font-size: 14px; color: #666; margin-bottom: 8px;&quot;&gt;📦 출고 처리 성능&lt;/div&gt;
&lt;div style=&quot;font-size: 24px; font-weight: 800; color: #16a34a; margin-bottom: 5px;&quot;&gt;최대 일 출고량 43.5% ↑&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #444;&quot;&gt;총 출고 물량 &lt;b&gt;34.4%&lt;/b&gt; 증가&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1; min-width: 250px; border: 1px solid #e1e4e8; border-radius: 12px; padding: 20px; background-color: #fcfcfc; border-top: 4px solid #82DC28;&quot;&gt;
&lt;div style=&quot;font-size: 14px; color: #666; margin-bottom: 8px;&quot;&gt;⚙️ 운영 자동화&lt;/div&gt;
&lt;div style=&quot;font-size: 24px; font-weight: 800; color: #82DC28; margin-bottom: 5px;&quot;&gt;수동 개입 49.2% ↓&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #444;&quot;&gt;분배 자동화율 &lt;b&gt;100%&lt;/b&gt; 달성&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1; min-width: 250px; border: 1px solid #e1e4e8; border-radius: 12px; padding: 20px; background-color: #fcfcfc; border-top: 4px solid #FF7878;&quot;&gt;
&lt;div style=&quot;font-size: 14px; color: #666; margin-bottom: 8px;&quot;&gt;🚚 고객 배송 경험&lt;/div&gt;
&lt;div style=&quot;font-size: 24px; font-weight: 800; color: #FF7878; margin-bottom: 5px;&quot;&gt;평균 14시간 단축&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #444;&quot;&gt;리드타임 &lt;b&gt;16.5%&lt;/b&gt; 개선&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;5-1-출고-처리-성능throughput의-향상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-1-%EC%B6%9C%EA%B3%A0-%EC%B2%98%EB%A6%AC-%EC%84%B1%EB%8A%A5throughput%EC%9D%98-%ED%96%A5%EC%83%81&quot; aria-label=&quot;5 1 출고 처리 성능throughput의 향상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5-1. 출고 처리 성능(Throughput)의 향상&lt;/h3&gt;
&lt;p&gt;단일 센터 체제에서는 특정 센터의 CAPA가 차오르면 그대로 배송 지연으로 이어졌습니다. &lt;br&gt;
하지만 이제는 실시간 가동률 데이터에 기반한 동적 분배를 통해 &lt;strong&gt;양지센터의 부하를 경산센터로 즉시 분산&lt;/strong&gt;합니다. &lt;br&gt;
그 결과, 전체 센터의 가용 자원을 낭비 없이 100% 활용하며 &lt;strong&gt;최대 일 출고량을 전년 대비 43.5% 끌어올리는 성과&lt;/strong&gt;를 거두었습니다.&lt;/p&gt;
&lt;h3 id=&quot;5-2-운영-프로세스-자동화-및-안정성-확보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-2-%EC%9A%B4%EC%98%81-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%9E%90%EB%8F%99%ED%99%94-%EB%B0%8F-%EC%95%88%EC%A0%95%EC%84%B1-%ED%99%95%EB%B3%B4&quot; aria-label=&quot;5 2 운영 프로세스 자동화 및 안정성 확보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5-2. 운영 프로세스 자동화 및 안정성 확보&lt;/h3&gt;
&lt;p&gt;전체 처리 물량은 전년 대비 크게 늘었으나, 시스템 자동화를 통해 현장의 운영 부담은 오히려 대폭 감소했습니다. &lt;br&gt;
특히 주문분배 및 이관 과정에서 시스템의 자율 판단 체계를 구축함으로써 &lt;strong&gt;운영자의 수동 개입을 49.2% 줄이고, 수기 할당 업무를 사실상 &apos;제로(Zero)&apos;화&lt;/strong&gt; 했습니다.
또한, 보상 트랜잭션과 멱등성 설계를 통해 이관 프로세스의 안정성을 확보한 결과, &lt;strong&gt;휴먼 에러가 발생할 수 있는 수동 개입 포인트를 원천 차단&lt;/strong&gt;하고 &lt;strong&gt;데이터 기반의 예측 가능한 운영 환경을 구축&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;h3 id=&quot;5-3-배송-리드타임-단축을-통한-고객-경험-개선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-3-%EB%B0%B0%EC%86%A1-%EB%A6%AC%EB%93%9C%ED%83%80%EC%9E%84-%EB%8B%A8%EC%B6%95%EC%9D%84-%ED%86%B5%ED%95%9C-%EA%B3%A0%EA%B0%9D-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0&quot; aria-label=&quot;5 3 배송 리드타임 단축을 통한 고객 경험 개선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5-3. 배송 리드타임 단축을 통한 고객 경험 개선&lt;/h3&gt;
&lt;p&gt;가장 유의미한 성과는 &lt;strong&gt;고객이 체감하는 배송 속도의 향상&lt;/strong&gt;입니다. &lt;br&gt;
단순히 출고처를 나누는 것에 그치지 않고, 각 센터의 실시간 가동 상황과 권역별 최적 경로를 반영한 &apos;흐름의 최적화&apos;를 통해 출고 정체 구간을 해소했습니다. &lt;br&gt;
그 결과, 대규모 물량이 몰리는 피크 타임에도 &lt;strong&gt;배송 리드타임을 전년 대비 평균 14시간 단축&lt;/strong&gt;하는 성과를 거두었습니다.&lt;br&gt;
이는 시스템이 스스로 최적의 경로를 찾아냄으로써 물류 병목을 해결하고, 고객에게 더욱 빠르고 신뢰받는 배송 경험을 제공할 수 있게 되었음을 의미합니다.&lt;/p&gt;
&lt;h2 id=&quot;6-마치며-물류의-자율-주행을-향하여&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EB%A7%88%EC%B9%98%EB%A9%B0-%EB%AC%BC%EB%A5%98%EC%9D%98-%EC%9E%90%EC%9C%A8-%EC%A3%BC%ED%96%89%EC%9D%84-%ED%96%A5%ED%95%98%EC%97%AC&quot; aria-label=&quot;6 마치며 물류의 자율 주행을 향하여 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6. 마치며: 물류의 &apos;자율 주행&apos;을 향하여&lt;/h2&gt;
&lt;p&gt;멀티 센터 체제로의 전환은 단순히 센터 하나를 더 운영하는 수준을 넘어, 물류 시스템 전체의 관점에서 아키텍처를 전면 재설계하는 도전적인 작업이었습니다.
이번에 구축한 배송최적화 시스템은 그 변화의 첫걸음이자, 앞으로 만들어갈 유연한 물류 인프라의 기반이 될 것입니다. 그리고 이 모든 기술적 시도의 중심에는 저희 배송최적화 스쿼드가 지향하는 명확한 목표가 있었습니다.
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;-올리브영-배송최적화-스쿼드의-목표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%B0%B0%EC%86%A1%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8A%A4%EC%BF%BC%EB%93%9C%EC%9D%98-%EB%AA%A9%ED%91%9C&quot; aria-label=&quot; 올리브영 배송최적화 스쿼드의 목표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 올리브영 배송최적화 스쿼드의 목표&lt;/h3&gt;
&lt;div style=&quot;text-align: center; padding: 40px 0; border-radius: 10px; margin: 20px 0;&quot;&gt;
  &lt;p style=&quot;font-family: &apos;Noto Serif KR&apos;, &apos;Nanum Myeongjo&apos;, serif; color: #333; line-height: 1.8;&quot;&gt;
    &lt;span style=&quot;font-size: 3em; color: #e1e1e1; font-family: &apos;Georgia&apos;, serif; vertical-align: -0.4em; line-height: 0;&quot;&gt;“&lt;/span&gt;
    &lt;br&gt;
    &lt;span style=&quot;font-style: italic; font-weight: 500; font-size: 1.25em; display: inline-block; margin: 10px 0;&quot;&gt;
      고객이 주문 버튼을 누르는 순간,&lt;br&gt;
      스스로 가장 빠르고 효율적인 경로를 선택하는&lt;br&gt;
      물류 운영 시스템을 구축한다
    &lt;/span&gt;
    &lt;br&gt;
    &lt;span style=&quot;font-size: 3em; color: #e1e1e1; font-family: &apos;Georgia&apos;, serif; vertical-align: -0.6em; line-height: 0;&quot;&gt;”&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;이번 프로젝트로 그 초석을 다졌다면, 이제는 이 비전을 완성하기 위해 다음과 같은 &lt;strong&gt;기술적 로드맵&lt;/strong&gt;을 그려나가고 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;출고처 및 배송방식 자동 전환 고도화&lt;/strong&gt;: 운영 상황에 따라 택배사와 배송 방식을 실시간으로 스위칭하는 유연한 출고 구조 완성&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;분리 배송 아키텍처 도입&lt;/strong&gt;: 부분 결품이나 특정 센터 지연 시에도 가용한 상품부터 선출고하는 유연한 주문 처리 로직 고도화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정확한 배송 예정일(ETA) 산출&lt;/strong&gt;: 재고 현황, 센터별 부하, 권역별 상황을 실시간 반영하여 주문 시점에 정확한 배송 예정일을 안내하는 알고리즘 개발&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티 채널 출고 확장성 확보&lt;/strong&gt;: MFC, 매장, 외부 물류 파트너 등 신규 출고처를 코드 수정 없이 즉시 수용할 수 있는 룰 기반 확장성 강화&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;text-align: center; color: #ccc; letter-spacing: 20px; font-weight: bold; margin: 30px 0;&quot;&gt;
  •••
&lt;/div&gt;
&lt;p&gt;저희가 추구하는 가치는 명확합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;고객에게는 기다림 없는 배송 경험을, 운영자에게는 시스템이 스스로 판단하는 자율적인 환경을 제공하는 것&lt;/strong&gt; &lt;br&gt;
배송최적화 스쿼드는 이 가치를 현실로 만들기 위해 오늘도 보이지 않는 곳에서 치열하게 고민하고 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;여러분의 서비스도 물류 센터 확장을 앞두고 계신가요? &lt;br&gt;
그렇다면 센터를 늘리는 것만큼이나 중요한 &apos;어떻게 최적으로 분배할 것인가&apos;에 대한 고민을 꼭 먼저 시작해 보시길 권합니다. &lt;br&gt;
그리고 저희의 시행착오와 성취가 담긴 이 경험이 비슷한 도전을 앞둔 분들께 의미 있는 실마리가 되었기를 바랍니다. 🐥&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이번 포스팅에서는 전체적인 아키텍처와 도입 성과를 중심으로 살펴보았는데요. &lt;br&gt;
배송최적화 시스템의 상세한 엔지니어링 디테일은 이어지는 저희 스쿼드의 다음 포스팅에서 더 자세히 소개해 드릴 예정이니 많은 기대 부탁드립니다! 🔜
&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;마지막으로 올리브영에서 저희와 함께 물류의 미래를 만들고 싶으신 분들의 많은 관심과 지원 부탁드립니다.&lt;/strong&gt; 🙇‍♀️ &lt;br&gt;
👉 &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;#x26;zz_jo_num=J20260122036761&quot;&gt;올리브영 코어플랫폼유닛 Back-end 개발자 채용 공고 바로가기&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[매장을 찾는 과정을 설계한 올영매장 고도화 여정]]></title><description><![CDATA[안녕하세요! 올리브영 올영매장팀에서 PM…]]></description><link>https://oliveyoung.tech/2026-02-27/2026-02-27-oliveyoung-store-journey-renewal-ux/</link><guid isPermaLink="false">https://oliveyoung.tech/2026-02-27/2026-02-27-oliveyoung-store-journey-renewal-ux/</guid><pubDate>Fri, 27 Feb 2026 18:00:00 GMT</pubDate><content:encoded>&lt;br&gt;
&lt;p&gt;안녕하세요! 올리브영 올영매장팀에서 PM을 맡고 있는 김연경, 프로덕트 디자이너 김보라입니다.👋&lt;/p&gt;
&lt;p&gt;여러분은 올리브영 매장에 방문해 보신 적이 있으신가요? 많은 분들이 매장에서 화장품과 생활용품을 구매하고, 팝업 스토어나 체험 이벤트를 즐긴 경험이 한 번쯤은 있으실 거예요. 사실 &lt;strong&gt;올리브영 앱에도 ‘올영매장’이 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;올영매장은 전국 1,400여 개 오프라인 매장과 관련된 정보를 앱에서 제공하는 서비스인데요, 단순한 매장 안내를 넘어, 온라인에서 픽업 주문을 하든, 매장을 직접 방문하든, 고객이 어디에서 쇼핑을 시작하더라도 끊김 없이 하나의 올리브영 경험을 느낄 수 있도록 하는 것. 이것이 바로 올영매장의 핵심 가치죠.&lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;strong&gt;2025년 한 해 동안 올영매장이 어떤 방향성과 가설을 기반으로 프로덕트를 고도화해 왔는지&lt;/strong&gt;를 PM과 디자이너의 시선으로 소개해 보려 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 올영매장의 기술적 토대가 어떻게 만들어졌는지 궁금하시다면, 2025년에 발행된 &lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3&quot;&gt;[10년 된 레거시를 현대화하다 시리즈]&lt;/a&gt; 포스트에서 매장 도메인의 설계와 확장 과정을 확인할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;한-번의-실험이-바꾼-다음-경로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%9C-%EB%B2%88%EC%9D%98-%EC%8B%A4%ED%97%98%EC%9D%B4-%EB%B0%94%EA%BE%BC-%EB%8B%A4%EC%9D%8C-%EA%B2%BD%EB%A1%9C&quot; aria-label=&quot;한 번의 실험이 바꾼 다음 경로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;한 번의 실험이 바꾼 다음 경로&lt;/h2&gt;
&lt;p&gt;올영 매장 고도화의 실마리는 서비스 내 트래픽이 가장 높은 ‘구매 가능 올영매장’의 병목 현상에서 발견했습니다. 많은 고객이 매장 상품에 관심을 갖고 페이지에 진입했지만, 그 이후 행동으로 이어지지 않은 채 이탈하고 있었어요. 재고 정보는 충분했지만, &lt;strong&gt;다음에 무엇을 할 수 있는지가 명확하지 않았던 것이죠.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f599c4f8fd56e84f7def7af65986f303/5814a/store-experience-02.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAKABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAgAF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAdlOQyl//8QAFhABAQEAAAAAAAAAAAAAAAAAEQEg/9oACAEBAAEFAmNc/wD/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAg/9oACAEBAAY/Al//xAAaEAEBAAIDAAAAAAAAAAAAAAABEQAhEDFB/9oACAEBAAE/IRDFrlNXzCzbkL1z/9oADAMBAAIAAwAAABCXz//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABsQAQEAAgMBAAAAAAAAAAAAAAERACExQVGR/9oACAEBAAE/EBSgfesCoAQjuj9yQKPbJc1GKca4xDzIeZ//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f599c4f8fd56e84f7def7af65986f303/263a4/store-experience-02.webp 480w,
/static/f599c4f8fd56e84f7def7af65986f303/a6361/store-experience-02.webp 960w,
/static/f599c4f8fd56e84f7def7af65986f303/08048/store-experience-02.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f599c4f8fd56e84f7def7af65986f303/a3e66/store-experience-02.jpg 480w,
/static/f599c4f8fd56e84f7def7af65986f303/fb816/store-experience-02.jpg 960w,
/static/f599c4f8fd56e84f7def7af65986f303/5814a/store-experience-02.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f599c4f8fd56e84f7def7af65986f303/5814a/store-experience-02.jpg&quot; alt=&quot;(좌)올영매장 및 픽업 주문 화면, (우)2025년 픽업 주문 상승량을 보여주는 표&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;그래서 우리는 픽업 장바구니 담기 버튼을 전면에 배치했습니다. 재고를 보여주는 것에서, 바로 행동하게 만드는 것으로의 전환이었죠. 배포 한 달 만에 올영 매장을 경유한 픽업 주문 건수는 2배 이상 늘었습니다. 이 변화는 버튼 하나를 추가한 결과라기보다, &lt;strong&gt;구매 의향이 형성된 바로 그 순간, 다음 행동을 명확히 제시했기 때문&lt;/strong&gt;이라고 해석했습니다. 2024년이 매장안내 서비스 리뉴얼을 통해 데이터 정합성과 시스템 기반을 다지는 해였다면, 이제는 그 위에서 고객의 행동을 유도하는 설계를 해야 한다는 것을 확인했던 과정이었어요.&lt;/p&gt;
&lt;h2 id=&quot;올영매장-고도화-방향성과-대전제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EC%98%81%EB%A7%A4%EC%9E%A5-%EA%B3%A0%EB%8F%84%ED%99%94-%EB%B0%A9%ED%96%A5%EC%84%B1%EA%B3%BC-%EB%8C%80%EC%A0%84%EC%A0%9C&quot; aria-label=&quot;올영매장 고도화 방향성과 대전제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올영매장 고도화 방향성과 대전제&lt;/h2&gt;
&lt;p&gt;픽업 장바구니 개선은 올영매장이 단순한 정보 제공을 넘어 고객 행동을 이끌어내는 판으로 확장될 수 있음을 보여준 출발점이었습니다. 그다음으로는 고객에게 어떤 정보가 더 필요할지, 그리고 어떻게 행동하게 만들 수 있을지에 대해 질문할 차례였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;비즈니스-문제-발견-매장에만-존재하는-정보들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%AC%B8%EC%A0%9C-%EB%B0%9C%EA%B2%AC-%EB%A7%A4%EC%9E%A5%EC%97%90%EB%A7%8C-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EC%A0%95%EB%B3%B4%EB%93%A4&quot; aria-label=&quot;비즈니스 문제 발견 매장에만 존재하는 정보들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;비즈니스 문제 발견: 매장에만 존재하는 정보들&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;😵‍💫 문제: 매장을 직접 방문하지 않으면 진행 중인 프로모션을 알지 못했어요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/95505fdbf1814c841f9637a37bcc8198/5814a/store-experience-03.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 42.91666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAEDBAX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAAB2YbShgV//8QAGBAAAgMAAAAAAAAAAAAAAAAAERIBAyD/2gAIAQEAAQUClmFgx//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABoQAQABBQAAAAAAAAAAAAAAAAEAESAhIpH/2gAIAQEABj8CwlJqnLf/xAAaEAACAgMAAAAAAAAAAAAAAAABIQARIDFh/9oACAEBAAE/IVBwKh0O3ZY//9oADAMBAAIAAwAAABBYD//EABcRAAMBAAAAAAAAAAAAAAAAAAEQESH/2gAIAQMBAT8QGVf/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAcEAEBAAICAwAAAAAAAAAAAAABEQAhEDFhgaH/2gAIAQEAAT8QoKI2kvneCAvRWvq/cLC948//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/95505fdbf1814c841f9637a37bcc8198/263a4/store-experience-03.webp 480w,
/static/95505fdbf1814c841f9637a37bcc8198/a6361/store-experience-03.webp 960w,
/static/95505fdbf1814c841f9637a37bcc8198/08048/store-experience-03.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/95505fdbf1814c841f9637a37bcc8198/a3e66/store-experience-03.jpg 480w,
/static/95505fdbf1814c841f9637a37bcc8198/fb816/store-experience-03.jpg 960w,
/static/95505fdbf1814c841f9637a37bcc8198/5814a/store-experience-03.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/95505fdbf1814c841f9637a37bcc8198/5814a/store-experience-03.jpg&quot; alt=&quot;오프라인 매장 인쇄 연출물 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;올리브영의 각 매장은 고유한 프로모션과 혜택을 운영하고 있습니다. 특정 매장에서만 제공하는 할인 쿠폰과 증정품, 팝업 스토어, 체험 이벤트 등이 대표적이죠. 상권과 매장 특성에 맞춰 매달 수십 개의 프로모션이 기획·운영되며, 이는 고객을 매장으로 유입시키는 중요한 장치입니다.&lt;/p&gt;
&lt;p&gt;하지만 이런 정보는 대부분 매장 내 인쇄 연출물이나 구성원의 설명에 의존하고 있었어요. 온라인몰에서는 개별 매장의 혜택이나 콘셉트를 사전에 확인할 방법이 거의 없었죠. 그 결과, 고객은 매장 방문을 미리 계획하기 어려웠고, 매장이 가진 고유한 혜택은 온라인에서 충분히 전달되지 못하고 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;기회-발견-온라인에서-오프라인-경험-확장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%ED%9A%8C-%EB%B0%9C%EA%B2%AC-%EC%98%A8%EB%9D%BC%EC%9D%B8%EC%97%90%EC%84%9C-%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B2%BD%ED%97%98-%ED%99%95%EC%9E%A5&quot; aria-label=&quot;기회 발견 온라인에서 오프라인 경험 확장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기회 발견: 온라인에서 오프라인 경험 확장&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;💡기회: 매장에만 존재하던 정보를 온라인에서도 보여줄 수 있다면, 전국 1,400여 개 올영 매장이 가진 옴니채널 가치가 더욱 강해지겠죠.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;매장 프로모션이 온라인까지 확장된다면, 온라인 고객을 오프라인 매장으로 자연스럽게 유입시킬 수 있을 것으로 기대했습니다. 실제로 올리브영은 온·오프라인을 모두 이용하는 옴니채널 고객 비중이 꾸준히 증가하고 있었으며, 여전히 추가 성장을 기대할 수 있는 단계였어요.&lt;/p&gt;
&lt;p&gt;고객 관점에서도 가능성은 분명했습니다. 매장 방문 전에 프로모션과 혜택을 미리 확인할 수 있다면, 방문 계획을 세우기 훨씬 수월해집니다. 특히 2030 고객층은 오프라인 매장을 단순한 구매 공간이 아닌, 브랜드를 경험하는 장소로 인식합니다. 따라서 &lt;strong&gt;온라인에서 사전에 매력을 느낀 매장은 실제 방문으로 이어질 가능성이 높은 것이죠.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;고도화-목표와-가설-수립&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%A0%EB%8F%84%ED%99%94-%EB%AA%A9%ED%91%9C%EC%99%80-%EA%B0%80%EC%84%A4-%EC%88%98%EB%A6%BD&quot; aria-label=&quot;고도화 목표와 가설 수립 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;고도화 목표와 가설 수립&lt;/h3&gt;
&lt;p&gt;올영매장이 추구하는 가치는 분명했습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;&quot;매장에 대한 정보 접점을 넓히고, 고객이 올영 매장만의 가치를 자연스럽게 인지하고 학습하게 만들어, 실제 방문으로 이어지게 만든다.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;물론 로드맵에는 개인화된 매장 추천 기능도 포함되어 있었어요. 그러나 앞선 픽업 실험을 통해, 우리는 중요한 전제를 확인했습니다. 고객의 행동을 이끌어내려면, 그전에 먼저 인지와 맥락이 갖춰져야 한다는 것이었죠. 따라서 우리는 먼저 저변을 넓히는 단계가 선행되어야 한다고 판단했고, 이를 검증하기 위해 다음과 같은 가설을 세워 실험을 진행했습니다. 그리고 아래 가설을 바탕으로 구체적인 설계를 이어갔습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[가설 1] 접점 확대를 통한 볼륨 성장: 올영 매장으로 이어지는 유기적인 트래픽 접점을 넓히면, 전체 접속 볼륨이 증가할 것이다.&lt;/li&gt;
&lt;li&gt;[가설 2] 콘텐츠 가시화를 통한 탐색 증가: 매장별 콘텐츠와 혜택을 가시화하면, 고객의 탐색 소비량이 증가할 것이다.&lt;/li&gt;
&lt;li&gt;[가설 3] 행동 유도를 통한 액션 전환율 향상: 프로모션 정보와 함께 행동을 유도하는 요소를 제공하면, 액션 전환율이 높아질 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;그래서-어떻게-했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B7%B8%EB%9E%98%EC%84%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;그래서 어떻게 했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;그래서 어떻게 했나요?&lt;/h2&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/dd92a295fe44f66f16977299e869da2f/5814a/store-experience-04.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMBBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHpUlkUB//EABkQAAIDAQAAAAAAAAAAAAAAAAECABASEf/aAAgBAQABBQLQSA9plDTNf//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABwQAAAGAwAAAAAAAAAAAAAAAAABAhAhMREygf/aAAgBAQAGPwKrEJyTSZ8Gym//xAAbEAEBAAMAAwAAAAAAAAAAAAARAQAQITFBUf/aAAgBAQABPyGlaqwyh75oAJ9GcWEiHnX/2gAMAwEAAgADAAAAEP8A7//EABURAQEAAAAAAAAAAAAAAAAAABAh/9oACAEDAQE/EIf/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAcEAACAgMBAQAAAAAAAAAAAAABEQAhMVFhEEH/2gAIAQEAAT8QTRqKRWzcQn2QRAi++NHwERcvgMn07MGJ/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dd92a295fe44f66f16977299e869da2f/263a4/store-experience-04.webp 480w,
/static/dd92a295fe44f66f16977299e869da2f/a6361/store-experience-04.webp 960w,
/static/dd92a295fe44f66f16977299e869da2f/08048/store-experience-04.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dd92a295fe44f66f16977299e869da2f/a3e66/store-experience-04.jpg 480w,
/static/dd92a295fe44f66f16977299e869da2f/fb816/store-experience-04.jpg 960w,
/static/dd92a295fe44f66f16977299e869da2f/5814a/store-experience-04.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dd92a295fe44f66f16977299e869da2f/5814a/store-experience-04.jpg&quot; alt=&quot;올영매장 홈 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h3 id=&quot;목적-달성의-도구에서-탐색의-공간으로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%A0%81-%EB%8B%AC%EC%84%B1%EC%9D%98-%EB%8F%84%EA%B5%AC%EC%97%90%EC%84%9C-%ED%83%90%EC%83%89%EC%9D%98-%EA%B3%B5%EA%B0%84%EC%9C%BC%EB%A1%9C&quot; aria-label=&quot;목적 달성의 도구에서 탐색의 공간으로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목적 달성의 도구에서, 탐색의 공간으로&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;올영매장에서 가장 큰 변화는 탐색 구조를 다시 정의한 것&lt;/strong&gt;이었어요. 그동안 매장 관련 경험은 특정 목적을 달성하기 위한 기능 단위로 흩어져 있었습니다. 재고를 확인하거나, 픽업 주문을 하거나, 매장 정보를 찾기 위해 각각 다른 경로를 거쳐야 했죠. 그러나 매장 경험은 더 이상 부분적인 기능이 아니라, 고객에게 독립적으로 전달되어야 할 &lt;strong&gt;하나의 O2O 레이어&lt;/strong&gt;라고 보았습니다.&lt;/p&gt;
&lt;p&gt;우리는 단순히 유입을 늘리는 것이 아니라, &lt;strong&gt;‘매장 탐색’ 자체를 하나의 완결된 경험으로 제공&lt;/strong&gt;하고자 했습니다. 이를 위해 올리브영 앱 하단 Tab Bar에 ‘올영매장’을 독립적인 공간으로 배치했어요. 매장 탐색부터 혜택 확인, 구매·픽업, 실제 방문까지 이어지는 흐름을 하나의 여정으로 담아내기 위해, 파편화된 진입 구조가 아닌 &lt;strong&gt;종합적인 안내 구조 설계가 필요&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #000000;&quot;&gt; 🟡 &lt;strong&gt;연경(PM):&lt;/strong&gt; 고객의 쇼핑 여정 안에서 매장 탐색을 제공해야 한다는 점에는 전사적인 공감이 있었지만, 그 탐색을 담아낼 ‘판을 어떤 형태로 만들 것인가’는 옴니채널 스쿼드가 풀어야 할 과제였습니다. &lt;/div&gt;
&lt;br&gt;
&lt;p&gt;우리가 만들고 싶었던 건 고객이 “지금 매장에서 어떤 일이 일어나고 있을까?”를 궁금해하며 스스로 찾아오게 만드는 공간이었어요. 올영매장 홈은 매장과 관련한 모든 탐색이 출발하는 종합 안내 공간으로 기획되었습니다. 픽업이나 재고 확인처럼 뚜렷한 목적을 가진 고객만을 위한 공간이 아니라, &lt;strong&gt;올리브영을 이용하는 모든 고객이 매장을 자연스럽게 탐색할 수 있는 게이트웨이로 만든 것&lt;/strong&gt;이죠.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #000000;&quot;&gt; 🟣 &lt;strong&gt;보라(PD):&lt;/strong&gt; 올리브영 앱에서 고객의 흐름을 보면, 상품을 구경하다가 점점 ‘실제로 사야겠다’는 행동으로 이어져요. 그 흐름 어딘가에 ‘직접 매장에 가볼까?’라는 생각이 자연스럽게 드는 순간이 있어야 했고, 올영 매장 탭이 바로 그 역할을 맡았습니다. 탭 바는 단순한 메뉴가 아니라, 고객이 온라인에서 오프라인으로 발걸음을 옮기는 전환점이라고 생각했어요. &lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;오프라인-경험-사전에-설득하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B2%BD%ED%97%98-%EC%82%AC%EC%A0%84%EC%97%90-%EC%84%A4%EB%93%9D%ED%95%98%EA%B8%B0&quot; aria-label=&quot;오프라인 경험 사전에 설득하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오프라인 경험, 사전에 설득하기&lt;/h3&gt;
&lt;p&gt;정보를 잘 보여주는 것과 행동을 이끌어내는 것은 다른 문제라고 생각합니다. &lt;strong&gt;고객은 정보를 확인했을 때보다, ‘갈 이유가 생겼을 때’ 움직이기 때문&lt;/strong&gt;입니다. 매장 정보를 아무리 상세히 제공해도 ‘그래서 왜 매장에 가야 하지?’라는 질문에 답하지 못하면 방문으로 이어지기 어렵습니다. 그래서 우리는 매장 방문을 사전에 설득하는 장치가 필요하다고 판단했고 콘텐츠(가 볼 이유), 혜택·프로모션(지금 가야 하는 이유)이라는 두 가지 방향으로 접근했어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;첫 번째는 콘텐츠였습니다.&lt;/strong&gt; ‘이달의 스토어 콘텐츠’와 ‘색다른 매장’은 매장을 단순한 구매 공간이 아니라 직접 가보고 싶은 경험의 공간으로 인식시키기 위한 시도였어요. 트렌드팟, N성수처럼 올리브영만의 콘셉트를 가진 매장 이야기를 콘텐츠로 풀어내며, 매장 방문이 자연스러운 선택지가 되도록 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d72b623b81d91d24759814fa38c6e921/5814a/store-experience-05.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 61.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEA/9oADAMBAAIQAxAAAAHozdgof//EABsQAAIBBQAAAAAAAAAAAAAAAAECAAMQERIi/9oACAEBAAEFAmbENRFtoHKDqf/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABsQAAEEAwAAAAAAAAAAAAAAAAEAEBFBEiEi/9oACAEBAAY/AhzKGVttRQDf/8QAGxABAAEFAQAAAAAAAAAAAAAAASEAEBFBUTH/2gAIAQEAAT8hBhle+VCONLMJXGNpQ9YA2//aAAwDAQACAAMAAAAQ1y//xAAWEQADAAAAAAAAAAAAAAAAAAABEGH/2gAIAQMBAT8QMX//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPxAn/8QAGxABAAMAAwEAAAAAAAAAAAAAAREhMQAQUaH/2gAIAQEAAT8QVUIqhw1Idi6bXnXjII2N48i0FABhmNb+9f/Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d72b623b81d91d24759814fa38c6e921/263a4/store-experience-05.webp 480w,
/static/d72b623b81d91d24759814fa38c6e921/a6361/store-experience-05.webp 960w,
/static/d72b623b81d91d24759814fa38c6e921/08048/store-experience-05.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d72b623b81d91d24759814fa38c6e921/a3e66/store-experience-05.jpg 480w,
/static/d72b623b81d91d24759814fa38c6e921/fb816/store-experience-05.jpg 960w,
/static/d72b623b81d91d24759814fa38c6e921/5814a/store-experience-05.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d72b623b81d91d24759814fa38c6e921/5814a/store-experience-05.jpg&quot; alt=&quot;올영매장 내 &apos;이 달의 스토어 콘텐츠&apos; 및 &apos;색다른 매장 소개&apos; 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #000000;&quot;&gt; 🟡 &lt;strong&gt;연경(PM)&lt;/strong&gt; : 온라인 쇼핑이 일상이 된 지금, 오프라인 매장이 제공해야 할 가치는 결국 ‘경험’이라는 이야기를 반복해서 들어왔어요. 올리브영 역시 매장에서만 느낄 수 있는 체험적 가치를 앱 안에서 더 적극적으로 드러낼 필요가 있었습니다. 실제로 이전까지 앱에서는 온라인 상품 기획전과 이벤트 중심의 콘텐츠만 노출되고 있었는데요, 매장 기획전을 위한 프로덕트가 새롭게 마련되면서, 이를 올영매장 안에서 어떻게 보여줄지가 새로운 과제가 되었습니다. &lt;/div&gt;
&lt;br&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #000000;&quot;&gt; 🟣 &lt;strong&gt;보라(PD)&lt;/strong&gt; : 저는 고객이 탐색하는 과정에서 ‘새로운 발견’을 경험하도록 만드는 데 집중했어요. 정보를 얼마나 빠르게 전달하느냐보다, 고객이 얼마나 자주 다시 방문하게 만들 수 있는지가 더 중요하다고 봤거든요. 그래서 매장 소개를 단순한 정보 나열이 아니라, 스토리텔링 기반의 콘텐츠로 설계했습니다. 한 번 보고 끝나는 게 아니라, 다음에도 자연스럽게 다시 찾게 되는 탐색 습관을 만드는 장치로 기획한 거죠.&lt;/div&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/88f5478d1e940ebc07b4e06df6b70a60/5814a/store-experience-06.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 61.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQBAgX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAAB0kmGSpJH/8QAGRABAQEAAwAAAAAAAAAAAAAAAQMAAhES/9oACAEBAAEFAlTVbetSjNm98N//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAbEAABBQEBAAAAAAAAAAAAAAABAAIQESESI//aAAgBAQAGPwLG9LzaaqMpAx//xAAbEAEBAAMAAwAAAAAAAAAAAAABEQAhMRBBUf/aAAgBAQABPyF4v9TI22pgdw4XBEKnvGc678f/2gAMAwEAAgADAAAAEFAv/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAHRABAAICAgMAAAAAAAAAAAAAARExACEQQVFhcf/aAAgBAQABPxAGAfBrerTAu0gKjaS7rJvcgn7ndeUXp9JkYwIgrj//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/88f5478d1e940ebc07b4e06df6b70a60/263a4/store-experience-06.webp 480w,
/static/88f5478d1e940ebc07b4e06df6b70a60/a6361/store-experience-06.webp 960w,
/static/88f5478d1e940ebc07b4e06df6b70a60/08048/store-experience-06.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/88f5478d1e940ebc07b4e06df6b70a60/a3e66/store-experience-06.jpg 480w,
/static/88f5478d1e940ebc07b4e06df6b70a60/fb816/store-experience-06.jpg 960w,
/static/88f5478d1e940ebc07b4e06df6b70a60/5814a/store-experience-06.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/88f5478d1e940ebc07b4e06df6b70a60/5814a/store-experience-06.jpg&quot; alt=&quot;올영매장 내 &apos;관심 매장 소식&apos; 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;두 번째는 관심 매장의 혜택과 프로모션을 더 잘 보이게 만드는 것이었어요.&lt;/strong&gt; 고객의 생활권이나 일상 루틴과 연결된 매장을 중심으로, 매장 한정 증정품이나 지역 맞춤 할인 쿠폰 같은 혜택을 우선적으로 노출했습니다. 매장마다 다른 혜택을 눈에 띄게 보여줌으로써, 고객이 특정 매장과 자연스럽게 관계를 맺고 다시 찾을 이유가 생기는 구조를 만들고자 했습니다.&lt;/p&gt;
&lt;p&gt;이처럼 올영매장의 콘텐츠와 프로모션은 각각 분리된 기능이 아니라, 매장 방문으로 이어지기 위한 하나의 흐름으로 설계되었습니다. 온라인에서 매장 경험을 먼저 이해하고 공감한 뒤, 실제 방문이라는 행동으로 이어지게 만드는 것. 이것이 이번 개선에서 우리가 만들고자 했던 변화였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;고객의-다음-행동을-이어주는-픽업-대시보드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%A0%EA%B0%9D%EC%9D%98-%EB%8B%A4%EC%9D%8C-%ED%96%89%EB%8F%99%EC%9D%84-%EC%9D%B4%EC%96%B4%EC%A3%BC%EB%8A%94-%ED%94%BD%EC%97%85-%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C&quot; aria-label=&quot;고객의 다음 행동을 이어주는 픽업 대시보드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;고객의 다음 행동을 이어주는 픽업 대시보드&lt;/h3&gt;
&lt;p&gt;올영매장에서 직접적인 구매 전환으로 이어질 수 있는 행동은 ‘픽업’이라고 생각했어요. 그래서 고객이 올영매장에 진입했을 때 가장 먼저 인지해야 할 것은 ‘여기서 바로 매장 픽업을 할 수 있다’는 메시지였습니다. 고객이 이탈하지 않고 행동으로 이어지려면, 고민 없이 다음 단계로 넘어갈 수 있는 구조가 필요했죠.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/68b5fde86782411ea409c60a72a55f4e/5814a/store-experience-07.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 61.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAEDAgX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAAB6yzKLiD/xAAbEAACAQUAAAAAAAAAAAAAAAAAAgEDEBEiMf/aAAgBAQABBQLBrbrvUlHP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGRAAAgMBAAAAAAAAAAAAAAAAAAEQETEh/9oACAEBAAY/Ass7FNCSyP/EABsQAQACAgMAAAAAAAAAAAAAAAEAESExEGFx/9oACAEBAAE/IcjbwzB364KAiUsq3tcGwZ//2gAMAwEAAgADAAAAEGwv/8QAFhEBAQEAAAAAAAAAAAAAAAAAARAR/9oACAEDAQE/EA2f/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGxABAAMAAwEAAAAAAAAAAAAAAQARMSFxkbH/2gAIAQEAAT8QEqPHCK9jwGacC50R2B0C43ZSRpRYRLwa+RlNS5//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/68b5fde86782411ea409c60a72a55f4e/263a4/store-experience-07.webp 480w,
/static/68b5fde86782411ea409c60a72a55f4e/a6361/store-experience-07.webp 960w,
/static/68b5fde86782411ea409c60a72a55f4e/08048/store-experience-07.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/68b5fde86782411ea409c60a72a55f4e/a3e66/store-experience-07.jpg 480w,
/static/68b5fde86782411ea409c60a72a55f4e/fb816/store-experience-07.jpg 960w,
/static/68b5fde86782411ea409c60a72a55f4e/5814a/store-experience-07.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/68b5fde86782411ea409c60a72a55f4e/5814a/store-experience-07.jpg&quot; alt=&quot;올영매장 내 &apos;관심 매장 소식&apos; 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c61169d9bb8afbbd708f3040887bf4c8/5814a/store-experience-08.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 42.91666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHWiiEMP//EABYQAQEBAAAAAAAAAAAAAAAAABEgIf/aAAgBAQABBQLBn//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABcQAAMBAAAAAAAAAAAAAAAAAAABIDL/2gAIAQEABj8CMuf/xAAbEAACAgMBAAAAAAAAAAAAAAABEQAhIHGBkf/aAAgBAQABPyHoYAVegjdojeH/2gAMAwEAAgADAAAAEAsv/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAHBABAQABBQEAAAAAAAAAAAAAAREAICExQYGR/9oACAEBAAE/EGVFHgMBYI7i19uAEAekKfNH/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c61169d9bb8afbbd708f3040887bf4c8/263a4/store-experience-08.webp 480w,
/static/c61169d9bb8afbbd708f3040887bf4c8/a6361/store-experience-08.webp 960w,
/static/c61169d9bb8afbbd708f3040887bf4c8/08048/store-experience-08.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c61169d9bb8afbbd708f3040887bf4c8/a3e66/store-experience-08.jpg 480w,
/static/c61169d9bb8afbbd708f3040887bf4c8/fb816/store-experience-08.jpg 960w,
/static/c61169d9bb8afbbd708f3040887bf4c8/5814a/store-experience-08.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c61169d9bb8afbbd708f3040887bf4c8/5814a/store-experience-08.jpg&quot; alt=&quot;상태별 픽업 대시보드 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이런 맥락에서 픽업 대시보드를 홈 최상단에 배치했습니다. 단순히 픽업 주문 수를 늘리기 위한 선택이 아니었어요. 픽업은 온라인에서 시작된 구매 의도를 &lt;strong&gt;매장 방문이라는 실제 경험으로 연결하는 중요한 접점&lt;/strong&gt;이기 때문입니다.&lt;/p&gt;
&lt;p&gt;기존에는 마이페이지나 카카오 알림을 통해서만 픽업 상태를 확인할 수 있었어요. 하지만 개선 이후에는 올영매장 홈에 들어오는 순간부터 픽업 기능을 바로 확인할 수 있고, 주문 상태에 따라 다음 행동도 명확하게 이어지는 구조가 되었죠.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #000000;&quot;&gt; 🟣 &lt;strong&gt;보라(PD)&lt;/strong&gt; : 픽업 대시보드는 주문 상태에 따라 보여주는 정보의 우선순위가 달라져요. 고객의 현재 상황을 실시간으로 반영해, 지금 할 수 있는 행동이 자연스럽게 눈에 들어오도록 했고요. 정보를 나열하는 대신, 보자마자 이해하고 바로 행동할 수 있도록 최대한 단순하게 만드는 데 집중했어요. &lt;/div&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;💡 픽업 대시보드가 실시간으로 주문 상태를 반영할 수 있었던 기술적 배경이 궁금하시다면&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 
옴니채널 스쿼드 너굴님이 &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;월 중 공유할 &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;올영매장과 데이터 연동 기술 이야기&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;에서 Kafka 이벤트 기반 하이브리드 설계를 확인해 보실 수 있습니다&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;숫자로-본-변화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%AB%EC%9E%90%EB%A1%9C-%EB%B3%B8-%EB%B3%80%ED%99%94&quot; aria-label=&quot;숫자로 본 변화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숫자로 본 변화&lt;/h3&gt;
&lt;p&gt;그러자 트래픽 관점에서 아래와 같은 유의미한 움직임이 나타났습니다. 저희는 이를 올영매장이 특정 상황에서만 찾는 기능이 아니라, 고객의 탐색 흐름 안에 자연스럽게 자리 잡기 시작했다는 신호로 해석했습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; border: none; font-family: &apos;Pretendard&apos;, sans-serif;&quot;&gt;
    &lt;thead&gt;
        &lt;tr style=&quot;border-bottom: 2px solid #333;&quot;&gt;
            &lt;th style=&quot;text-align: left; padding: 12px; width: 25%; font-size: 16px;&quot;&gt;지표&lt;/th&gt;
            &lt;th style=&quot;text-align: left; padding: 12px; font-size: 16px;&quot;&gt;결과&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        &lt;tr style=&quot;border-bottom: 1px solid #eee;&quot;&gt;
            &lt;td style=&quot;padding: 15px 12px; font-weight: bold; color: #555;&quot;&gt;유입 경로&lt;/td&gt;
            &lt;td style=&quot;padding: 15px 12px;&quot;&gt;직접 유입 비중 &lt;span style=&quot;color: #00B464; font-weight: bold;&quot;&gt;약 10%p 증가&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr style=&quot;border-bottom: 1px solid #eee;&quot;&gt;
            &lt;td style=&quot;padding: 15px 12px; font-weight: bold; color: #555;&quot;&gt;트래픽&lt;/td&gt;
            &lt;td style=&quot;padding: 15px 12px;&quot;&gt;2025년 월별 PV/순 방문자 수(Unique Visitor)/MAU &lt;span style=&quot;color: #00B464; font-weight: bold;&quot;&gt;우상향 성장&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr style=&quot;border-bottom: 1px solid #eee;&quot;&gt;
            &lt;td style=&quot;padding: 15px 12px; font-weight: bold; color: #555;&quot;&gt;인지도&lt;/td&gt;
            &lt;td style=&quot;padding: 15px 12px;&quot;&gt;올리브영 전체 순 방문자 수(Unique Visitor) 대비 올영매장 UV &lt;span style=&quot;color: #00B464; font-weight: bold;&quot;&gt;비중 성장&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;이번 올영매장 확장의 핵심은 고객이 &lt;strong&gt;언제, 어디서든 가장 편한 방식으로 구매와 매장 탐색을 이어갈 수 있게 만드는 것&lt;/strong&gt;이었습니다. 특히 단기 성과를 위한 개선이 아니라, 온·오프라인을 연결하는 경험의 기반을 다지는 &lt;strong&gt;구조적 변화의 출발점&lt;/strong&gt;에 가깝습니다. 앞으로는 이 기반 위에서 온·오프라인 데이터를 유기적으로 연결하고, 고객의 구매 여정 전반에서 발생하는 니즈와 문제를 더 입체적으로 바라보며 다음 방향성을 설계해 나갈 예정입니다.&lt;/p&gt;
&lt;h2 id=&quot;아직-남은-과제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%84%EC%A7%81-%EB%82%A8%EC%9D%80-%EA%B3%BC%EC%A0%9C&quot; aria-label=&quot;아직 남은 과제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;아직 남은 과제&lt;/h2&gt;
&lt;p&gt;한편, 픽업 주문 전환율은 기대만큼 오르지 않았어요. 접근성과 전환은 다른 문제였기 때문인데요. 이는 &lt;strong&gt;고객이 왜 픽업을 선택해야 하는지에 대한 설득이 아직 충분하지 않았다는 신호&lt;/strong&gt;로 보았습니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/53def966d9a70fe09751480ecb8e8353/5814a/store-experience-09.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAEDBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHWtEkL/8QAGBAAAgMAAAAAAAAAAAAAAAAAAAEQEiH/2gAIAQEAAQUCVzY//8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQMBAT8Bh//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABcQAAMBAAAAAAAAAAAAAAAAAAABMRD/2gAIAQEABj8CcJn/xAAZEAEAAwEBAAAAAAAAAAAAAAABABEhQaH/2gAIAQEAAT8hBVS3Ng4v1BelT//aAAwDAQACAAMAAAAQAA//xAAWEQADAAAAAAAAAAAAAAAAAAABEGH/2gAIAQMBAT8QFL//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAaEAADAQADAAAAAAAAAAAAAAABESEAMVHR/9oACAEBAAE/ECrL4WEfmnCapEPEBtdN7//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/53def966d9a70fe09751480ecb8e8353/263a4/store-experience-09.webp 480w,
/static/53def966d9a70fe09751480ecb8e8353/a6361/store-experience-09.webp 960w,
/static/53def966d9a70fe09751480ecb8e8353/08048/store-experience-09.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/53def966d9a70fe09751480ecb8e8353/a3e66/store-experience-09.jpg 480w,
/static/53def966d9a70fe09751480ecb8e8353/fb816/store-experience-09.jpg 960w,
/static/53def966d9a70fe09751480ecb8e8353/5814a/store-experience-09.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/53def966d9a70fe09751480ecb8e8353/5814a/store-experience-09.jpg&quot; alt=&quot;올영매장 진입 대비 개선 전후 픽업 전환율&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;원인은 아래 두 가지로 분석할 수 있었어요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;올영매장의 인지도와 접근성은 개선되었지만 ‘픽업으로 구매해야 하는 이유’까지는 설득하지 못함&lt;/li&gt;
&lt;li&gt;온라인 구매에 익숙한 고객의 행동 패턴을 변화하기에는 시간이 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;픽업이 가능하다는 사실을 아는 것과, 지금 이 순간 픽업이 가장 합리적인 선택이라고 이해하는 것은 다른 문제&lt;/strong&gt;였어요. 앞으로는 재고가 한정된 상황이나 빠른 수령이 필요한 순간처럼, &lt;strong&gt;픽업이 자연스럽게 더 유용해지는 상황을 설계하는 실험이 필요하다고 판단&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;또한, 이미 온라인 구매에 익숙한 고객의 선택을 바꾸는 건 단기간에 이루어지기 어려워요. “왜 매장에 가야 해?”라는 근본적인 질문에 더 설득력 있는 답을 제시해야 하죠. 그래서 앞으로의 목표는 명확해요. 올영매장의 고관여 고객에 대한 이해를 바탕으로, 인지도가 아닌 재방문율을 높이고, 트래픽이 아닌 전환을 개선하며, 기능이 아닌 습관을 만드는 거예요.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
      &lt;div class=&quot;photo-item&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e863c757ecd66d05dce4469221abee82/5814a/store-experience-10.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHakSwX/8QAFhABAQEAAAAAAAAAAAAAAAAAARAh/9oACAEBAAEFAkyf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFhAAAwAAAAAAAAAAAAAAAAAAECEx/9oACAEBAAY/AlT/AP/EABsQAQAABwAAAAAAAAAAAAAAAAEAEBEhMUFx/9oACAEBAAE/IWreUC2E1mX/2gAMAwEAAgADAAAAEHfP/8QAFhEAAwAAAAAAAAAAAAAAAAAAEBFB/9oACAEDAQE/EKx//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAHBABAAIBBQAAAAAAAAAAAAAAAREhABAxQVGB/9oACAEBAAE/ECCISeV+4VcZk1360//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e863c757ecd66d05dce4469221abee82/263a4/store-experience-10.webp 480w,
/static/e863c757ecd66d05dce4469221abee82/a6361/store-experience-10.webp 960w,
/static/e863c757ecd66d05dce4469221abee82/08048/store-experience-10.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e863c757ecd66d05dce4469221abee82/a3e66/store-experience-10.jpg 480w,
/static/e863c757ecd66d05dce4469221abee82/fb816/store-experience-10.jpg 960w,
/static/e863c757ecd66d05dce4469221abee82/5814a/store-experience-10.jpg 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e863c757ecd66d05dce4469221abee82/5814a/store-experience-10.jpg&quot; alt=&quot;개선 후 올영매장 Active User 기준 관심매장 등록 비율&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
  &lt;/center&gt;
&lt;br&gt;
&lt;p&gt;또 하나의 과제는 &lt;strong&gt;개인화 경험의 시작이 쉽지 않다는 점&lt;/strong&gt;이었습니다. 관심 매장 설정을 유도하는 것만으로는 한계가 있었거든요. ‘아직 아무 설정도 하지 않은 첫 방문자에게, 어떤 즉각적인 이득을 줄 수 있는가?’에 대한 고민이 더 필요합니다. 설정을 마친 고객에게만 가치가 작동하는 구조로는, 아직 아무것도 설정하지 않은 첫 방문자를 붙잡기 어렵기 때문입니다. 따라서, 설정 이전에도 충분한 가치를 느낄 수 있는 구조를 만들어야겠다고 생각했어요.&lt;/p&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;올영매장 고도화 프로젝트를 통해 분명해진 것은 하나입니다. 고객이 원하는 것은 눈에 보이는 기능이 아니라, &lt;strong&gt;자신의 문제를 해결해 주는 경험&lt;/strong&gt;이라는 것입니다.&lt;/p&gt;
&lt;p&gt;이제 올영매장은 인지와 접근성이라는 첫 단계를 지나, &lt;strong&gt;왜 지금 이 매장을 선택해야 하는지에 대한 ‘행동의 이유’를 만들어야 하는 단계&lt;/strong&gt;에 들어섰습니다. 즉, 매장 정보를 잘 보여주는 것이 지난 단계에서의 목표였다면 고객이 별도의 탐색 없이도 ‘지금 이 선택이 가장 합리적이다’라고 느끼게 만드는 것이 다음 과제인 거죠.&lt;/p&gt;
&lt;p&gt;앞으로의 고도화 방향은 기능을 더 추가하는 데 있지 않습니다. &lt;strong&gt;고객을 움직이게 만드는 맥락과 설득을 더 정교하게 다듬고, 한 번의 선택이 자연스럽게 반복되는 습관으로 이어지도록 설계하는 것&lt;/strong&gt;입니다. 올영매장은 그렇게, 고객의 일상 안에 스며드는 경험으로 계속 진화해 나갈 예정입니다. 그럼, 앞으로도 관심 있게 지켜봐 주세요!&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;글. 김연경(올영매장 PM), 김보라(Product Designer) / 올영매장 팀&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;우리의 성장은 계속됩니다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;올리브영은 앞으로도 이러한 지식 교류의 장을 지속적으로 마련할 예정입니다. 혼자 고민하면 풀기 어려운 ‘숙제’가 되지만, 함께 나누면 세상을 놀라게 할 ‘혁신’의 실마리가 된다는 것을 믿기 때문입니다. 올리브영의 PM과 디자이너는 단순히 기능과 화면을 만드는 사람이 아닙니다. 단단한 커뮤니티 안에서 동료들과 함께 정답을 찾아가고, 그 과정에서 나만의 독보적인 커리어를 만들어갑니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 더 자세한 올리브영 프로덕트 이야기가 궁금하신가요?&lt;/strong&gt;
이 글은 올리브영 프로덕트 메이커가 직접 전하는 생생한 소식지, [올리브영 PM 블로그(Medium)]에서도 만나보실 수 있습니다. 서비스 기획의 깊은 고민과 추가 콘텐츠가 궁금하시다면 이 링크를 확인해 보세요!
👉🏻 &lt;a href=&quot;https://medium.com/@oliveyoung.olip&quot;&gt;올리브영 PM 블로그 바로가기&lt;/a&gt;   💚&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;💡 세상을 바꿔나갈 멋진 여정에 합류하세요!&lt;/strong&gt; 최고의 동료들과 함께 더 건강하게 성장할 여러분을 기다리고 있습니다.
👉🏻 &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/list.fo?zz_target_1=B&amp;#x26;zz_hot_job_yn=&amp;#x26;orderDesc=&amp;#x26;company=F97&amp;#x26;zz_title=PO&quot;&gt;올리브영 PM 채용 공고 보기&lt;/a&gt; 💚&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[Spring 트랜잭션 동기화로 레거시 알림톡 발송 시스템 한계 넘어서기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2026-02-23/from-legacy-to-modern-architecture-journey/</link><guid isPermaLink="false">https://oliveyoung.tech/2026-02-23/from-legacy-to-modern-architecture-journey/</guid><pubDate>Mon, 23 Feb 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 올리브영 배송스쿼드에서 백엔드 엔지니어로 일하고 있는 애쉬입니다. 이번 글에서는 입사 후 처음 맡게 된 프로젝트를 진행하며 느꼈던 고민과 배움을 공유해보려 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;입사 첫 프로젝트가 단순 기능 개발이 아닌 대규모 아키텍처 전환이라니... 설렘 반 걱정 반으로 시작했던 1년차의 치열한 &lt;strong&gt;레거시 탈출기&lt;/strong&gt;를 공유합니다. 🚀&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-전환의-계기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%A0%84%ED%99%98%EC%9D%98-%EA%B3%84%EA%B8%B0&quot; aria-label=&quot;1 전환의 계기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 전환의 계기&lt;/h2&gt;
&lt;p&gt;올리브영의 알림톡 발송 시스템은 주문, 결제, 배송 등 고객의 서비스 경험 속에서 핵심적인 소통 역할을 합니다.
1,600만 명 이상의 회원에게 실시간으로 소식을 전하는 매우 중요한 채널이죠.
하지만 비즈니스가 급성장하며 레거시 시스템은 점점 복잡해졌고, 변화하는 요구사항을 담아내기에는 한계에 부딪혔습니다.
특히 발송 플랫폼의 변화를 앞두고, 지속 가능한 구조를 위한 모던 아키텍처로의 전환이 필요했습니다.&lt;/p&gt;
&lt;h3 id=&quot;11-왜-바꿔야-했을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#11-%EC%99%9C-%EB%B0%94%EA%BF%94%EC%95%BC-%ED%96%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;11 왜 바꿔야 했을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1.1 왜 바꿔야 했을까?&lt;/h3&gt;
&lt;p&gt;알림톡 발송 시스템을 개편하게 된 이유는 하나의 문제가 아니라, 여러 한계가 동시에 드러났기 때문입니다. 먼저 알림톡 발송 플랫폼이 변경되면서, 기존 구조의 수정이 필요했습니다. 그런데 알림톡 발송과 관련된 로직이 여러 비즈니스 코드에 흩어져 있어, 해당 부분을 일일이 찾아 수정해야 하는 번거로움이 있었습니다. 또 데이터 조회 기준의 불일치 문제도 있었죠. 같은 내용이라도 알림 유형이나 서비스에 따라 서로 다른 테이블과 조건으로 데이터를 조회하는 경우가 있어, 일관성이 깨지고 기준이 모호했습니다. 마지막으로 올리브영은 레거시 시스템을 모던 아키텍처로 점진적 전환 중이기 때문에, 앞으로 모던 환경 중심으로 배송 알림을 확장하기 위해서라도 발송 기능을 하나의 구조로 일원화할 필요가 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;12-어떻게-바꿔야-할까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#12-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B0%94%EA%BF%94%EC%95%BC-%ED%95%A0%EA%B9%8C&quot; aria-label=&quot;12 어떻게 바꿔야 할까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1.2 어떻게 바꿔야 할까?&lt;/h3&gt;
&lt;p&gt;레거시 시스템과 모던 아키텍처 시스템 간 통신을 위해 Kafka 메시지를 사용하기로 했습니다. 기존 배송스쿼드에서 사용하던 컨슈머 서비스에서 알림톡을 발송하기 위해서죠. 여기서부터 신입 개발자의 고민이 시작됩니다...&lt;/p&gt;
&lt;p&gt;기존 레거시 코드는 주문/배송 상태 변경 로직 안에서 메시지 발송 서비스를 직접 호출하고 있었고, 이 호출은 &lt;code class=&quot;language-text&quot;&gt;try-catch&lt;/code&gt;로 감싸져 있어 알림톡 발송에 실패하더라도 기존 비즈니스 로직에는 영향을 주지 않는 구조였습니다. 그래서 처음에는 레거시의 알림톡 발송 서비스를 호출하는 시점에 Kafka 메시지를 바로 생성하려고 했습니다.&lt;/p&gt;
&lt;p&gt;하지만 코드를 작성하다 다시 살펴보니 알림톡 발송이 성공한 이후에 비즈니스 로직에서 오류가 발생하더라도, 이미 발송된 알림톡은 되돌릴 수 없다는 문제가 있었습니다. 즉, 실제 데이터 상태와 고객에게 전달된 알림이 어긋날 수 있는 구조였던 셈입니다. 오류 발생으로 같은 로직을 재실행한다면 알림이 중복 발송될 거고요.😱 그래서 알림톡 발송을 비즈니스 로직에서 분리하되, 주문/배송 상태 변경과 고객 안내의 명확한 순서가 보장되는 구조가 필요하다고 판단했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-전환-방법론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%A0%84%ED%99%98-%EB%B0%A9%EB%B2%95%EB%A1%A0&quot; aria-label=&quot;2 전환 방법론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 전환 방법론&lt;/h2&gt;
&lt;h3 id=&quot;21-트랜잭션-동기화-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#21-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%8F%99%EA%B8%B0%ED%99%94-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;21 트랜잭션 동기화 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.1 트랜잭션 동기화 구현&lt;/h3&gt;
&lt;p&gt;일반적으로 Spring 환경에서는 트랜잭션 동기화를 위해 &lt;code class=&quot;language-text&quot;&gt;@TransactionalEventListener&lt;/code&gt;를 활용한 트랜잭션 커밋 이후 이벤트를 처리하는 방식이 널리 사용됩니다.&lt;/p&gt;
&lt;p&gt;하지만 저는 해당 방식을 사용할 수 없었는데요, 간략히 제가 겪었던 과정을 설명드려보겠습니다. 처음에는 &lt;code class=&quot;language-text&quot;&gt;@TransactionalEventListener&lt;/code&gt;를 사용해서 트랜잭션 커밋 후 이벤트를 처리하려고 했는데, 이 어노테이션은 Spring 4.2부터 지원되는 기능이었습니다. 문제는 레거시에서 하위 버전을 사용하고 있어서 &lt;code class=&quot;language-text&quot;&gt;TransactionPhase.AFTER_COMMIT&lt;/code&gt; 같은 클래스 자체가 존재하지 않았다는 점이죠. 그래서 Spring 4.2.5로 버전을 올렸는데, 하필 다른 모든 모듈이 의존하는 공통 모듈을 건드렸던 게 화근이었습니다. Maven에서 기존 프레임워크와의 의존성 충돌이 발생했고, spring-context만 올렸더니 spring-tx도 함께 올려야 한다는 오류가 연쇄적으로 터졌죠. 하나씩 해결하다 보니 빌드는 성공했지만 서버 실행 시 NoClassDefFoundError가 발생했습니다. 이 오류는 클래스패스에 4.2.5와 하위 버전이 섞여 있으면서, 컴파일 타임에는 4.2.5 클래스를 참조했지만 런타임에는 하위 버전의 클래스가 로드되어 메서드 시그니처가 맞지 않아 발생한 것이었죠. 버전을 잘 맞춰서 이 문제를 해결하고 나서는 제가 수정하지 않은 부분에서도 빈 생성 오류가 발생했습니다. 🫠&lt;/p&gt;
&lt;p&gt;이렇게 하나를 해결하면 새로운 오류 메시지가 발생하는 상황의 연속을 경험하면서... 개발 경험이 부족했던 저는 운영 환경에서도 예상치 못한 오류가 발생할까 걱정되어 이벤트 리스너 구조를 고집하기 보다 차선책을 택하기로 했습니다. 레거시 시스템을 처음 크게 수정하는 프로젝트였는데, 제가 생각한 대로 흘러가지 않고 무수한 오류들을 마주하며 레거시가 얼마나 복잡한 시스템인지, 왜 전사 차원에서 모던화를 빠르게 추진하는지 체감할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;제가 선택한 방법은 &lt;code class=&quot;language-text&quot;&gt;TransactionSynchronizationManager&lt;/code&gt;를 활용한 커밋 이후 콜백 메서드 실행&lt;/strong&gt;이었습니다. 이 방식이 이벤트를 발행하거나 리스너를 등록하는 구조는 아닙니다. 하지만 트랜잭션이 성공적으로 커밋된 이후에만 특정 로직이 실행되도록 보장할 수 있기 때문에, 실행 시점 보장 측면에서는 이벤트 처리와 동일한 효과를 가집니다. 즉, 구현 방식은 단순한 콜백이지만 “데이터가 확정된 이후에만 후속 처리가 시작된다”는 중요한 조건을 만족시킬 수 있었고, 이를 통해 알림톡 발송 흐름을 비즈니스 트랜잭션으로부터 안전하게 분리할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;현재 트랜잭션 커밋 후 바로 Kafka 메시지를 발행하는 방식을 사용했는데, 메시지 발행이 실패할 경우에는 실패 로그를 DB에 저장하여 별도로 재처리할 수 있도록 설계되어 있습니다. 다행히 운영 환경에서 서버 장애로 인한 메시지 유실은 발생하지 않았지만, 이번 개발 과정을 회고해보니 더 높은 안정성이 요구되는 환경이라면 Outbox Pattern을 통해 이벤트를 DB에 먼저 저장한 후 별도 프로세스가 발행하도록 하거나, 재시도 메커니즘 같은 방어책들을 추가로 고려해볼 필요가 있겠다는 생각이 들었습니다. 당장은 필수는 아니지만 이런 부분들도 점진적으로 개선해나가면 좋을 것 같습니다.&lt;/p&gt;
&lt;h4 id=&quot;-레거시-비즈니스-로직--커밋-후-kafka-메시지-발행&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81--%EC%BB%A4%EB%B0%8B-%ED%9B%84-kafka-%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B0%9C%ED%96%89&quot; aria-label=&quot; 레거시 비즈니스 로직  커밋 후 kafka 메시지 발행 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 레거시 비즈니스 로직 + 커밋 후 Kafka 메시지 발행&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NotificationEvent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; orderNo&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; notificationType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NotificationEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; orderNo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; notificationType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;orderNo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; orderNo&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;notificationType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; notificationType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token annotation punctuation&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NotificationAfterCommitHandler&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MessagePublishService&lt;/span&gt; messagePublishService&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;// Kafka 메시지 발행 서비스&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NotificationEvent&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// 트랜잭션 커밋 이후에만 실행될 로직 등록&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;registerSynchronization&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TransactionSynchronizationAdapter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;afterCommit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;token comment&quot;&gt;// 커밋이 완료된 이후에만 후속 처리 수행&lt;/span&gt;
                    messagePublishService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                        event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getOrderNo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                        event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getNotificationType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e866f2dd55a5c85b114f0e165afcf435/63217/image1.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 84.79166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAARABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAeroKACg/8QAGBAAAwEBAAAAAAAAAAAAAAAAAAEQMUH/2gAIAQEAAQUCHeR4f//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABURAQEAAAAAAAAAAAAAAAAAABEg/9oACAECAQE/AWP/xAAUEAEAAAAAAAAAAAAAAAAAAAAw/9oACAEBAAY/Ah//xAAaEAACAwEBAAAAAAAAAAAAAAAAARExQRBR/9oACAEBAAE/IYhuCs+Co0a1zUWc/9oADAMBAAIAAwAAABCgwAD/xAAVEQEBAAAAAAAAAAAAAAAAAAABIP/aAAgBAwEBPxAI/8QAFREBAQAAAAAAAAAAAAAAAAAAASD/2gAIAQIBAT8QYf/EABsQAQEBAQEAAwAAAAAAAAAAAAERADEhEKGx/9oACAEBAAE/EKUJW8wGx7wzmRHuZA9eaDEuAhM/tvofH//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e866f2dd55a5c85b114f0e165afcf435/263a4/image1.webp 480w,
/static/e866f2dd55a5c85b114f0e165afcf435/a6361/image1.webp 960w,
/static/e866f2dd55a5c85b114f0e165afcf435/d71bc/image1.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e866f2dd55a5c85b114f0e165afcf435/a3e66/image1.jpg 480w,
/static/e866f2dd55a5c85b114f0e165afcf435/fb816/image1.jpg 960w,
/static/e866f2dd55a5c85b114f0e165afcf435/63217/image1.jpg 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e866f2dd55a5c85b114f0e165afcf435/63217/image1.jpg&quot; alt=&quot;image1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;그림 1. 레거시의 구조와 개선된 메시지 기반 아키텍처 비교&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;h3 id=&quot;22-msamicroservices-architecture-환경에서의-알림톡-처리-흐름&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#22-msamicroservices-architecture-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%95%8C%EB%A6%BC%ED%86%A1-%EC%B2%98%EB%A6%AC-%ED%9D%90%EB%A6%84&quot; aria-label=&quot;22 msamicroservices architecture 환경에서의 알림톡 처리 흐름 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.2 MSA(Microservices Architecture) 환경에서의 알림톡 처리 흐름&lt;/h3&gt;
&lt;p&gt;이제 레거시 시스템과 모던 아키텍처 시스템이 어떻게 연결되고, 어떤 시점에 실제 발송 흐름이 시작되는지 살펴보겠습니다. 레거시에서 트랜잭션 커밋 이후에 Kafka 메시지를 발행하고, MSA의 Delivery Consumer 서비스가 이를 소비(consume)합니다. 이제부터는 레거시가 아니라, Delivery Consumer가 모든 발송 처리를 전담하게 됩니다.&lt;/p&gt;
&lt;p&gt;배송에서 관리하고 있는 알림톡이 약 40개 정도로 꽤 많은 편이라, 어떻게 나눠서 기능화할지 설계와 구현에 많은 시간을 쏟았는데요. 우선 레거시에서 발행된 메시지에 알림톡 유형이 있으니, 같은 배송 유형별(ex. 오늘드림 / 일반배송)로 분리하여 처리하도록 했습니다. 배송 유형별로 안내하는 내용이 비슷하기 때문에, 같은 쿼리를 사용해 데이터를 조회하도록 한 거죠. 그런데 막상 테스트해보니 같은 배송 유형 안에서도 이 배송이 일반 주문인지, 교환 주문인지 또는 취소된 항목은 없는지에 따라서 필요한 데이터가 조금씩 상이하여 하나의 쿼리로 특정 배송 유형에 대한 모든 범위의 알림톡을 커버하지 못했습니다. 수정과 테스트를 반복하며 모든 유형을 최소 갈래로 분류하여 공통 쿼리를 수정해 나갔습니다. 이 작업을 하면서도 난항을 겪어서 그냥 알림 유형별로 조회 쿼리를 만들까도 생각했지만😅 그러면 레거시와 다른 점이 없기 때문에! 열심히 고민한 결과 배송 유형 내에 같은 조건끼리 묶어서(ex. 일반 / 교환) 데이터를 조회할 수 있도록 했습니다. 이렇게 데이터 조회까지 완성한 뒤에는 알림톡 내 링크 생성, 알림톡 발송 요청 API 등 공통 기능을 구현해서 모던 시스템을 완성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;위에서 설명한 내용을 단계별로 정리해보면 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;컨슈머가 메시지 소비&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kafka를 통해 전달된 메시지를 소비합니다.&lt;/li&gt;
&lt;li&gt;이 메시지에는 주문번호, 알림톡 유형과 같은 최소한의 필수 정보만 포함되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;알림톡 유형에 따라 배송 유형 분기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컨슈머는 전달받은 알림톡 유형을 기준으로 일반 배송, 오늘드림, 픽업, 선물하기 중 어떤 배송 유형의 알림인지 판단합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;공통 기준으로 알림톡 정보 조회&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;배송 유형이 결정되면, 해당 알림에 필요한 공통 데이터와 템플릿 정보를 DB에서 일관된 기준으로 조회합니다.&lt;/li&gt;
&lt;li&gt;과거처럼 서비스마다 서로 다른 테이블을 조회하지 않고, 하나의 기준 테이블만 조회하도록 통합했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;템플릿 구성 후 발송 요청&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;조회한 데이터를 바탕으로 알림톡 템플릿을 채우고, 최종적으로는 하나의 공통 발송 API를 통해 알림톡 발송 요청이 이루어집니다.&lt;/li&gt;
&lt;li&gt;어떤 배송 유형이든, 실제 외부 알림 플랫폼으로 나가는 통로는 항상 하나로 통일되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;즉, 레거시에서는 언제 알림을 보내야 하는지만 결정하고, 컨슈머에서는 어떤 내용을, 어떤 방식으로, 안정적으로 보낼지를 전담하는 구조로 역할이 완전히 분리되었습니다. 이렇게 공통화한 덕분에 새로운 알림톡을 추가할 때 매우 간편한 구조가 완성되었습니다. 템플릿과 발송 시점만 추가하면 끝이죠. 여담이지만 이후에 제가 알림톡 추가 작업도 진행했는데, 모던 전환 미리 해두길 정말 잘했다는 생각과 함께 보람이 느껴졌습니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/00ac2c2998761a1fcc026b994caf01e4/63217/image2.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB16AH/8QAFxAAAwEAAAAAAAAAAAAAAAAAAAEQIf/aAAgBAQABBQLRT//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABUQAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAEBAAY/Am//xAAYEAADAQEAAAAAAAAAAAAAAAAAASERwf/aAAgBAQABPyFLgaSpT//aAAwDAQACAAMAAAAQA9//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAwEBPxBX/8QAFhEAAwAAAAAAAAAAAAAAAAAAEBFB/9oACAECAQE/EIh//8QAGhABAAIDAQAAAAAAAAAAAAAAAQARMUFRgf/aAAgBAQABPxC7Zz1AUdu2HQns/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/00ac2c2998761a1fcc026b994caf01e4/263a4/image2.webp 480w,
/static/00ac2c2998761a1fcc026b994caf01e4/a6361/image2.webp 960w,
/static/00ac2c2998761a1fcc026b994caf01e4/d71bc/image2.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/00ac2c2998761a1fcc026b994caf01e4/a3e66/image2.jpg 480w,
/static/00ac2c2998761a1fcc026b994caf01e4/fb816/image2.jpg 960w,
/static/00ac2c2998761a1fcc026b994caf01e4/63217/image2.jpg 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/00ac2c2998761a1fcc026b994caf01e4/63217/image2.jpg&quot; alt=&quot;image2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;그림 2. MSA에서의 알림톡 처리 흐름&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;h2 id=&quot;3-전환-과정에서의-배움&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%A0%84%ED%99%98-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%B0%EC%9B%80&quot; aria-label=&quot;3 전환 과정에서의 배움 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 전환 과정에서의 배움&lt;/h2&gt;
&lt;p&gt;신입 개발자로서 이번 프로젝트는 단순한 기능 구현을 넘어, 시스템을 설계하는 관점에서 많은 고민을 하게 만든 경험이었는데요. 제가 느낀 내용들은 한 번 정리해보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;31-아키텍처-설계의-중요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#31-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1&quot; aria-label=&quot;31 아키텍처 설계의 중요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.1 아키텍처 설계의 중요성&lt;/h3&gt;
&lt;p&gt;코드를 작성하기 전에 시스템 전체의 흐름을 이해하는 것이 무엇보다 중요했습니다. 초반에 여러 제약을 고려하지 않고 단순한 시스템 설계 후 코드를 작성했다가, 부족한 점을 보완한 후 다시 작성하며 시간이 거의 두 배로 들었습니다. 입사 초반에는 &apos;개발자는 코드를 잘 짜야 한다&apos;는 생각이 강했다면, 이 프로젝트를 통해 &apos;설계를 잘 하는 개발자가 되고 싶다&apos;고 생각이 바뀌었습니다. 특히 단일 서비스에서 해결되던 문제도, 분산 환경에서는 여러 시스템 간 데이터 흐름을 고려해야 한다는 점을 가장 크게 느꼈습니다.&lt;/p&gt;
&lt;h3 id=&quot;32-철저한-테스트의-필요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#32-%EC%B2%A0%EC%A0%80%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1&quot; aria-label=&quot;32 철저한 테스트의 필요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.2 철저한 테스트의 필요성&lt;/h3&gt;
&lt;p&gt;아무리 설계를 잘 해놓더라도 허점이 있기 마련인데요. 이런 점은 코드만 놓고 보면 보이지 않는 경우가 많습니다. 그래서 QA팀과 함께 직접 여러 케이스를 테스트해보며 이상한 점은 없는지, 누락된 기능은 없는지 꼼꼼히 확인했습니다. 특히 알림톡은 고객에게 직접 발송되고 안내하는 기능이라 작은 실수도 치명적일 수 있어, 다양한 케이스를 반복 테스트하며 안정성을 확보하려 노력했습니다.&lt;/p&gt;
&lt;h3 id=&quot;33-msa-구조에서의-비즈니스-흐름&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#33-msa-%EA%B5%AC%EC%A1%B0%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%ED%9D%90%EB%A6%84&quot; aria-label=&quot;33 msa 구조에서의 비즈니스 흐름 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.3 MSA 구조에서의 비즈니스 흐름&lt;/h3&gt;
&lt;p&gt;입사 전에는 제대로 된 MSA 환경을 접하기 어렵기 때문에 추상적으로만 알고 있던 구조를 이번 프로젝트를 통해 좀 더 구체적으로 이해할 수 있었습니다. MSA에서는 하나의 비즈니스 흐름이 여러 시스템으로 나뉘어 처리되기 때문에, DB 트랜잭션 이후의 처리까지 포함해 전체 흐름의 일관성을 어떻게 유지할지가 중요하다는 것을 배웠고, 이후 올리브영의 다른 마이크로 서비스와 통신하며 협업해야 하는 작업에서 정말 많은 도움을 얻을 수 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-전환-후-개선된-점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EC%A0%84%ED%99%98-%ED%9B%84-%EA%B0%9C%EC%84%A0%EB%90%9C-%EC%A0%90&quot; aria-label=&quot;4 전환 후 개선된 점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 전환 후 개선된 점&lt;/h2&gt;
&lt;p&gt;저의 피땀눈물로 완성한 전환 이후 시스템은 훨씬 단순하고 안정적으로 개선되었습니다. 주요 개선 사항을 표와 함께 정리해볼까요?&lt;/p&gt;
&lt;div style=&quot;display: flex; justify-content: center; margin: 20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse: collapse; width: 80%; text-align: center; border: 1px solid black;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color: #f8f9fa; border-bottom: 2px solid black;&quot;&gt;
        &lt;th style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;구분&lt;/th&gt;
        &lt;th style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;기존 (Legacy)&lt;/th&gt;
        &lt;th style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;개선 후 (Modern)&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black; font-weight: bold; background-color: #fdfdfd;&quot;&gt; 아키텍처 구조&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;서비스 로직 내 발송 코드 결합(Monolithic)&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;Kafka 기반 이벤트 주도 아키텍처 (EDA)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black; font-weight: bold; background-color: #fdfdfd;&quot;&gt;발송 시점 보장&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;비즈니스 로직 결과와 별개로 발송&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;afterCommit 활용 트랜잭션 정합성 확보&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black; font-weight: bold; background-color: #fdfdfd;&quot;&gt;데이터 조회&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;서비스별 산재된 테이블/조건 조회&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;통합 기준으로 일관된 데이터 조회&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black; font-weight: bold; background-color: #fdfdfd;&quot;&gt;관리 주체&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;각 비즈니스 도메인 서비스&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;배송 컨슈머(Delivery Consumer)로 일원화&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black; font-weight: bold; background-color: #fdfdfd;&quot;&gt; 확장성&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;알림톡 추가 시마다 레거시 코드 수정/배포&lt;/td&gt;
        &lt;td style=&quot;padding: 12px; border: 1px solid black;&quot;&gt;템플릿 등록 및 메시지 발행으로 낮은 공수&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;h3 id=&quot;41-유지보수성-향상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#41-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%EC%84%B1-%ED%96%A5%EC%83%81&quot; aria-label=&quot;41 유지보수성 향상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4.1 유지보수성 향상&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;알림톡 관련 코드가 모던 알림톡 서비스 한 곳으로 집중되면서 관리 포인트가 크게 줄었습니다.&lt;/li&gt;
&lt;li&gt;데이터 조회 기준도 공통화되어 서비스별 테이블 불일치 문제가 해소되었습니다.&lt;/li&gt;
&lt;li&gt;수정 시 영향 범위 파악과 장애 대응 속도 모두 개선되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;42-확장성과-유연성-증가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#42-%ED%99%95%EC%9E%A5%EC%84%B1%EA%B3%BC-%EC%9C%A0%EC%97%B0%EC%84%B1-%EC%A6%9D%EA%B0%80&quot; aria-label=&quot;42 확장성과 유연성 증가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4.2 확장성과 유연성 증가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;알림 발송 시점이 이벤트 기반으로 정의되어, 이벤트 추가만으로 확장이 가능해졌습니다.&lt;/li&gt;
&lt;li&gt;알림 기능이 특정 서비스에 결합되지 않고 독립된 구조로 분리되었습니다.&lt;/li&gt;
&lt;li&gt;향후 배송 정책이나 서비스 유형이 추가되어도 최소한의 변경으로 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;43-공통-재시도-로직-구축-확장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#43-%EA%B3%B5%ED%86%B5-%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%A1%9C%EC%A7%81-%EA%B5%AC%EC%B6%95-%ED%99%95%EC%9E%A5&quot; aria-label=&quot;43 공통 재시도 로직 구축 확장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4.3 공통 재시도 로직 구축 (확장)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;레거시에서는 공통 재시도 로직을 독립적으로 구성하기가 어려웠으나, 모던 환경에서 실패 건을 별도 테이블에 저장해 비동기적으로 재처리하는 공통 재시도 구조를 새롭게 설계했습니다.&lt;/li&gt;
&lt;li&gt;이를 통해 단순한 기능 구현을 넘어 운영 관점의 안정성까지 함께 확보할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;5 마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 마무리하며&lt;/h2&gt;
&lt;p&gt;이번 프로젝트는 저에게 단순한 마이그레이션을 넘어, 시스템을 설계하고 개선하는 개발자로 성장하는 계기가 되었습니다. 트랜잭션 동기화라는 기술적 도전, 메시지 기반 구조 전환, 테스트를 통한 안정성 확보와 같은 과정을 통해 유지보수성과 확장성을 모두 갖춘 배송 알림톡 시스템을 완성할 수 있었죠. 무엇보다 이번 경험은 신입 개발자로서 시스템을 바라보는 시야를 넓혀준 좋은 시작이었습니다. 앞으로 올리브영에서 어떤 문제를 해결하게 될지, 또 어떤 기술적인 도전을 하게 될지 매우 기대되고 다음 번에 더 성장한 모습으로 돌아오겠습니다. 감사합니다!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[오프라인 매장에 코드를 배포하다 Part 2: 올리브영 전자라벨(ESL) 최적화 여정]]></title><description><![CDATA[들어가며 안녕하세요! 올리브영에서 Back-end 개발을 담당하고 있는 '코드다이버(이시훈)'입니다. 앞서 "올리브영 전자라벨(ESL) 구축기 Part…]]></description><link>https://oliveyoung.tech/2026-02-10/esl-series-2/</link><guid isPermaLink="false">https://oliveyoung.tech/2026-02-10/esl-series-2/</guid><pubDate>Tue, 10 Feb 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요! 올리브영에서 Back-end 개발을 담당하고 있는 &apos;코드다이버(이시훈)&apos;입니다. 앞서 &lt;a href=&quot;/2026-02-09/esl-series-1&quot;&gt;&quot;올리브영 전자라벨(ESL) 구축기 Part 1: 종이 없는 매장을 만드는 데이터 파이프라인&quot;&lt;/a&gt;에서는 전자라벨의 도입 배경과 아키텍처 설계 부분을 다뤘습니다. 이번 글에서는 전국 1,300여 개 매장으로 대규모 확산하는 과정에서 마주한 성능 최적화 기술들을 중점적으로 소개하겠습니다. 특히 아래와 같은 내용을 담고 있으니 참고해 주세요.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;🛠️ Engineering Highlights&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partitioning -&lt;/strong&gt; 1,300개 매장 배치 처리 시간 13일 → 5분 단축&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lock Free -&lt;/strong&gt; 데이터 범위 분할로 DB Deadlock 원천 차단&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serverless v2 -&lt;/strong&gt; 트래픽 폭주에 대응하는 오토스케일링으로 비용 40% 절감&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;🎯 특히 이런 분들께 도움이 될 거예요&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;대용량 배치 처리 속도 문제로 고민하는 백엔드 개발자&lt;/li&gt;
&lt;li&gt;예측 불가능한 트래픽에 대응하는 DB 인프라 전략이 궁금한 엔지니어&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;1편에서 이야기한 아키텍처가 설계도라면, 지금부터는 실제 데이터를 태워 가동할 차례입니다. 굳이 비유하자면, 시스템 구축 초기의 파일럿 매장은 &apos;2~4명으로 구성된 우리 가족을 위해 집에서 두쫀쿠를 몇개 만들어 제공&apos;하는 수준과 같았습니다. 하지만 전국 1,300개 이상 매장 적용은 &apos;전국에 근무 중인 수천 명의 올리브영 임직원에게 동시에 일정한 퀄리티의 두쫀쿠를 제공하는 시스템을 운영&apos;하는 것과 같습니다. 메뉴(로직)는 같을지 몰라도, 조리 도구와 조달 방식(아키텍처와 성능 최적화)은 근본적으로 달라야만 했습니다.&lt;/p&gt;
&lt;p&gt;이에 압도적인 규모로 전국 매장에 안정적으로 시스템을 도입하기 위해 저희가 치열하게 고민했던 &lt;strong&gt;대용량 트래픽 처리와 운영 최적화 이야기&lt;/strong&gt;, 지금부터 시작합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;chapter-1-배치가-끝나지-않는다-병렬-처리의-함정과-partitioning&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#chapter-1-%EB%B0%B0%EC%B9%98%EA%B0%80-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%EC%9D%98-%ED%95%A8%EC%A0%95%EA%B3%BC-partitioning&quot; aria-label=&quot;chapter 1 배치가 끝나지 않는다 병렬 처리의 함정과 partitioning permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Chapter 1. 배치가 끝나지 않는다: 병렬 처리의 함정과 Partitioning&lt;/h2&gt;
&lt;p&gt;전자라벨 시스템의 배치는 상품 데이터 중 변경된 데이터(Delta)를 찾아내는 고된 작업입니다. 초기 버전(V1) 성능 테스트 결과, &lt;strong&gt;복잡한 데이터 집계(Aggregation) 로직으로 인해 1개 매장을 처리하는 데 평균 15분이 소요&lt;/strong&gt;되었습니다. 이 속도라면 1,300개 매장을 순차적으로 처리할 경우 &lt;strong&gt;약 13일(15분 × 1,300개)이 걸린다는 충격적인 결과가 도출&lt;/strong&gt;됩니다. 매장 오픈 시간(오전 10시)까지 작업을 끝내야 하는 저희에게 이는 말그대로 &apos;불가능한 미션&apos;이었습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse; width:100%;&quot;&gt;
  &lt;caption style=&quot;caption-side: bottom; margin-top: 10px; font-size: 0.9em; color: #666;&quot;&gt;V1 성능 테스트 결과표&lt;/caption&gt;
  &lt;thead&gt;
    &lt;tr&gt;
        &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;항목&lt;/th&gt;
        &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;측정 및 계산 결과&lt;/th&gt;
        &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;비고&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;단위 매장 처리 시간&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;약 15분&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;복잡한 집계(Aggregation) 로직&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;전체 대상 매장 수&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;19,500분 (약 13.5일)&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;순차 처리 시 소요 기간&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;목표 완료 시간&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;매장 오픈 전 (오전 10시)&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong style=&quot;color: red;&quot;&gt;현실적 달성 불가능&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;h3 id=&quot;시도-단순-병렬-처리의-실패-v2&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%8F%84-%EB%8B%A8%EC%88%9C-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC%EC%9D%98-%EC%8B%A4%ED%8C%A8-v2&quot; aria-label=&quot;시도 단순 병렬 처리의 실패 v2 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시도: 단순 병렬 처리의 실패 (V2)&lt;/h3&gt;
&lt;p&gt;이러한 물리적인 시간을 단축하기 위해 저희는 가장 먼저 Scale-out(병렬 처리) 카드를 꺼냈습니다. Spring Batch의 &lt;a href=&quot;https://docs.spring.io/spring-batch/reference/scalability.html#multithreadedStep&quot;&gt;Multi-threaded Step&lt;/a&gt;을 적용하여 수십 개의 스레드가 동시에 여러 매장의 데이터를 집계하고 DB에 갱신을 하도록 설정했습니다.&lt;/p&gt;
&lt;p&gt;&quot;스레드 수만큼 속도가 선형적으로 증가하겠지&quot;라는 기대와 달리, 배치를 가동하자마자 성능 향상은커녕 터미널은&lt;code class=&quot;language-text&quot;&gt;Deadlock Loser&lt;/code&gt; 오류로 뒤덮였습니다. &lt;strong&gt;원인은 긴 트랜잭션과 자원 경합&lt;/strong&gt;에 있었습니다. 복잡한 Aggregation 로직이 실행되는 동안 트랜잭션이 길게 유지되었고, 병렬 스레드들이 동일한 테이블 내의 인접한 인덱스 범위에 대해 동시에 Write를 시도하며 락(Lock) 경쟁을 벌인 것입니다. 결국 스레드들은 서로가 점유한 자원이 해제되기만을 기다리는 교착 상태(Deadlock)에 빠졌고, 시스템은 아무런 처리도 하지 못한 채 멈춰 섰습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;해결-partitioning으로-완성한-10분의-기적-v3&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-partitioning%EC%9C%BC%EB%A1%9C-%EC%99%84%EC%84%B1%ED%95%9C-10%EB%B6%84%EC%9D%98-%EA%B8%B0%EC%A0%81-v3&quot; aria-label=&quot;해결 partitioning으로 완성한 10분의 기적 v3 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결: Partitioning으로 완성한 10분의 기적 (V3)&lt;/h3&gt;
&lt;p&gt;그래서 저희는 &lt;strong&gt;&apos;스레드 간의 데이터 간섭을 0으로 만들자&apos;는 원칙으로 구조를 전면 개편&lt;/strong&gt;했습니다. 해답은 Spring Batch Partitioning이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/scalability.html#partitioning&quot;&gt;Spring Batch Partitioning&lt;/a&gt;: 전체 데이터를 PK(Primary Key) 범위 기반으로 논리적 격리를 수행했습니다. (예: ID 1&lt;del&gt;1000, 1001&lt;/del&gt;2000...)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lock Free 구조: 각 Worker Step은 자신에게 할당된 고유한 PK 범위 내에서만 집계(Aggregation)와 갱신(Update)을 수행합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a742f12192b9dc0227fd26fa7d7fa8e4/cb544/esl_series_2_range_lock.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACYklEQVR42g2S22/SYADF+yfv0agx6sPiNPrgw6YzGTphy2LGxqV0K/cChV6/r7T0Am1pofRCuVhY6dRkkeS8nvN7+B3ErKWs1sW4mTaqPy3i0mxcjBsZs5EORCz2mcRndanZ52p9rgqZMmAqHImBbmFh1CIDRWKXXA0bLlv0AGZ3b/eZsujSorY+3PhgN4d/VpKtM5pC94UehF0BtCdaZzEm1kYJ8bTK1uefkvHM5st1tNnBiQ6OlQtUF9+E0sqDvsU8ruWl2/9+dZ3O5kpY/vLm9vgsZYEsMuJyQ1CK11oUDBweD2BlLhO+UHHVdhTKwYSXQNW3ubHaIZpou3XH5s4NuSVxmCcXkBFfsOW6pRACmpI/P3e+vZn9OHSOX3ils2ih2RqpCvX9brzUIH2vik3XZOY2Y8uVuZJHdIi5OsmVvuS+PmNO39Lpj9TJK3B0MMufrkNlG8oPSzX5rW9CRe03NallaKRv0aZY9uU84o0antkjrj7VU6+Fs0O7eKqevFTeHzjVzDqUdys1WWt/Iz1eaQNY35Mdi7FGXVPEA7WI2CI6UevhlPdgWd2Xfx7Z5+/MzIcJ8WuzVBczaKntlQsdo8d0CgyJSo1rWyNMEQvkWwS2LhQOfXq0/SmAND6U2uqgMxx0x0MqCqSp3mXJQjAFrsVydNkc0jqo2SNS79950jVightPJ5K1EvmCazHhTNhbCR24coW9Z9cgpzoZ+eK/B9Ma9QT2fgBwgcyORWw5LCKx09j/ZOt0Ypfa+VTsUbuASUKQhHDrsUkoJIt+HIBdKDwEvMqjCp2V2pnFCI8M7D+NOxdWTTox0gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a742f12192b9dc0227fd26fa7d7fa8e4/263a4/esl_series_2_range_lock.webp 480w,
/static/a742f12192b9dc0227fd26fa7d7fa8e4/a6361/esl_series_2_range_lock.webp 960w,
/static/a742f12192b9dc0227fd26fa7d7fa8e4/0b34d/esl_series_2_range_lock.webp 1920w,
/static/a742f12192b9dc0227fd26fa7d7fa8e4/c6529/esl_series_2_range_lock.webp 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a742f12192b9dc0227fd26fa7d7fa8e4/9aebd/esl_series_2_range_lock.png 480w,
/static/a742f12192b9dc0227fd26fa7d7fa8e4/a91f8/esl_series_2_range_lock.png 960w,
/static/a742f12192b9dc0227fd26fa7d7fa8e4/ac7a9/esl_series_2_range_lock.png 1920w,
/static/a742f12192b9dc0227fd26fa7d7fa8e4/cb544/esl_series_2_range_lock.png 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a742f12192b9dc0227fd26fa7d7fa8e4/ac7a9/esl_series_2_range_lock.png&quot; alt=&quot;데이터 범위 격리를 통해 병렬 스레드 간의 자원 간섭을 제거하고 Lock Free 환경을 구현한 Spring Batch Partitioning 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;PK 범위 분할(Partitioning)을 통해 Lock 경합을 제거한 아키텍처 구조(Gemini 생성)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;&lt;br&gt;
&lt;p&gt;이 방식은 &lt;strong&gt;각 워커가 서로 다른 데이터 범위를 처리&lt;/strong&gt;하므로 물리적으로 자원 침범이 발생하지 않아 &lt;strong&gt;DB Lock 경합이 원천 차단&lt;/strong&gt;되었습니다. Lock이 사라지자 CPU와 I/O 자원을 온전히 데이터 처리에만 쓸 수 있게 되었고, 13일이 걸릴 뻔했던 작업은 단 5분 만에 1,300개 매장 처리를 완료하는 기적 같은 효율을 보여주었습니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a36b6bb3d40ce9d173539b9576b30366/cb544/esl_series_2_spring_batch_time.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.58333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB7klEQVR42i1S25KjIBD1/z9mt/ZlZ5LJVRNFjYgX5KYC4nWSj9jO1FadArqL0+d0g0dZOU398zm+XuM4dmF4iWM/jgNC4kcWInQNggNkqjrN8Z0x3HWVamsuyq6l3jCotq2fT7dtw/c3wK2bhfD1mn4qTpBfVm0Nr/LkfP343P05f/xO7qemyb1l0VrzdbUAsOBcuyxm2yxwAJCEcJ77eTFu1kNHdZ3K8MgJkqryGCNK0WWGG3qa9bxoIID+jyBUNMtqJmD2tK8SYLYsVx2tKU6SwMuySIjqp7wGwAEIbwuDAhfrNkxOOYYtzRJ0OZw+rv7+etllWXg577yqerQthVFNgKkDPkwBId8Pjv51L8tk6WpnGjsqY2XXUXALK/i93Y4exhHnxTi2oDM4OY5qnPsJQllOspgHMc394BQMzBhm/q+MNvh2O3iwcU7c2FqnhlG9Z6ObmePFsnWDhjUUspbDiyB0Cfx9EHzF8bks47eyc0rK0mnew9PR3FBsZKWNUr0Qoq5oVtWYiVq1TcOKDMdJeg/jKM2iskBvMjSAUZhHIcEpJlnNm7zI85KgFF38k3+/JjjNS5w8ECmyvHj8+nv4/No3NPWsEX3Puo6ZQcFBiMJqBh0KTqLo/EgD+FVaUyULkodSEEBJ3rJSlP8Aje0rQkBwGSEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a36b6bb3d40ce9d173539b9576b30366/263a4/esl_series_2_spring_batch_time.webp 480w,
/static/a36b6bb3d40ce9d173539b9576b30366/a6361/esl_series_2_spring_batch_time.webp 960w,
/static/a36b6bb3d40ce9d173539b9576b30366/0b34d/esl_series_2_spring_batch_time.webp 1920w,
/static/a36b6bb3d40ce9d173539b9576b30366/c6529/esl_series_2_spring_batch_time.webp 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a36b6bb3d40ce9d173539b9576b30366/9aebd/esl_series_2_spring_batch_time.png 480w,
/static/a36b6bb3d40ce9d173539b9576b30366/a91f8/esl_series_2_spring_batch_time.png 960w,
/static/a36b6bb3d40ce9d173539b9576b30366/ac7a9/esl_series_2_spring_batch_time.png 1920w,
/static/a36b6bb3d40ce9d173539b9576b30366/cb544/esl_series_2_spring_batch_time.png 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a36b6bb3d40ce9d173539b9576b30366/ac7a9/esl_series_2_spring_batch_time.png&quot; alt=&quot;V1 순차 처리 방식 대비 V3 Partitioning 방식의 극적인 성능 개선: 1,300개 매장 기준 13.5일 → 10분 단축&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt; Partitioning 도입으로 1,300개 매장 배치 처리 시간이 13.5일에서 10분으로 단축된 성능 비교(Gemini 생성)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h2 id=&quot;chapter-2-예측-불가능한-폭주를-감당하라-rds-serverless-v2의-탄력성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#chapter-2-%EC%98%88%EC%B8%A1-%EB%B6%88%EA%B0%80%EB%8A%A5%ED%95%9C-%ED%8F%AD%EC%A3%BC%EB%A5%BC-%EA%B0%90%EB%8B%B9%ED%95%98%EB%9D%BC-rds-serverless-v2%EC%9D%98-%ED%83%84%EB%A0%A5%EC%84%B1&quot; aria-label=&quot;chapter 2 예측 불가능한 폭주를 감당하라 rds serverless v2의 탄력성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Chapter 2. 예측 불가능한 폭주를 감당하라: RDS Serverless v2의 탄력성&lt;/h2&gt;
&lt;p&gt;배치 성능의 문제는 해결했지만, 인프라 운영의 난관이 남았습니다. 우리 서비스의 트래픽은 단순히 &apos;배치 시간&apos;에만 몰리는 것이 아니라, 그 강도(Intensity)를 예측하기 어렵다는 것이 문제였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;평시: 적은 양의 데이터 변경만 발생&lt;/li&gt;
&lt;li&gt;프로모션/올영세일 기간: 평소 대비 수십 배, 수백 배의 변경 데이터가 예고 없이 폭주(Burst)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;기존의 RDS On-Demand(고정형 인스턴스) 방식으로는 이 변동성을 감당하기 어려웠습니다.&lt;/strong&gt; 세일 기간을 대비해 항상 최고 사양(Provisioned)을 유지하자니 비용 낭비가 심했고, 그렇다고 평시에 맞춰두자니 갑작스러운 데이터 폭증 시 장애가 발생할 위험이 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;시도-인프라-스스로-숨-쉬게-하라-rds-serverless-v2&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%8F%84-%EC%9D%B8%ED%94%84%EB%9D%BC-%EC%8A%A4%EC%8A%A4%EB%A1%9C-%EC%88%A8-%EC%89%AC%EA%B2%8C-%ED%95%98%EB%9D%BC-rds-serverless-v2&quot; aria-label=&quot;시도 인프라 스스로 숨 쉬게 하라 rds serverless v2 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시도: 인프라 스스로 숨 쉬게 하라 (RDS Serverless v2)&lt;/h3&gt;
&lt;p&gt;저희는 이 딜레마를 해결하기 위해 트래픽 양에 따라 실시간으로 DB 자원을 늘리고 줄이는 &lt;strong&gt;Aurora RDS Serverless v2 도입을 결정&lt;/strong&gt;했습니다. 저희가 주목한 핵심 기능은 데이터 폭주가 시작되는 시점에 지연 없이 대응하는 &lt;strong&gt;&apos;즉각적이고 중단 없는 확장(Instant &amp;#x26; Seamless Scaling)&apos;&lt;/strong&gt; 입니다. 이는 운영자가 수동으로 개입하지 않아도 시스템이 스스로 트래픽의 파도에 맞춰 숨을 쉬는 구조를 완성해주었습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse; width:100%;&quot;&gt;
  &lt;caption style=&quot;caption-side: bottom; margin-top: 10px; font-size: 0.9em; color: #666;&quot;&gt;기존 RDS vs Aurora RDS Serverless v2 비교표&lt;/caption&gt;
  &lt;thead&gt;
    &lt;tr&gt;
        &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;구분&lt;/th&gt;
        &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;기존 RDS (On-Demand)&lt;/th&gt;
        &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;Aurora RDS Serverless v2&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;확장 방식&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;수동 스케일업 (재부팅 필요)&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;자동 오토스케일링 (즉시 반영)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;다운타임&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;수 분 발생 (서비스 중단)&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;없음 (Seamless Scaling)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;비용&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;피크 트래픽 기준 고정 비용&lt;/td&gt;
        &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;사용량 기반 종량제 과금&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;h3 id=&quot;검증-및-결과-예상치-못한-파도타기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%80%EC%A6%9D-%EB%B0%8F-%EA%B2%B0%EA%B3%BC-%EC%98%88%EC%83%81%EC%B9%98-%EB%AA%BB%ED%95%9C-%ED%8C%8C%EB%8F%84%ED%83%80%EA%B8%B0&quot; aria-label=&quot;검증 및 결과 예상치 못한 파도타기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;검증 및 결과: 예상치 못한 파도타기&lt;/h3&gt;
&lt;p&gt;도입 후 저희는 &lt;strong&gt;Serverless v2가 보여준 야생성&lt;/strong&gt;에 놀랐습니다. &lt;strong&gt;배치가 시작되고 데이터가 급격히 유입되면, DB의 ACU(Aurora Capacity Unit) 가 수직 상승&lt;/strong&gt;하며 부하를 받아냈습니다. 반대로 작업이 끝나거나 데이터 양이 적은 날에는 즉시 최저 사양(Min ACU)으로 줄어들어 숨을 고르는 걸 볼 수 있었습니다. 운영자가 &quot;오늘 데이터가 많을까?&quot;를 고민하며 미리 스펙을 올리고 내릴 필요가 없어진 것입니다. 시스템이 데이터의 파도에 맞춰 스스로 서핑을 하는 셈입니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7c8f5eaea8d3e35e46786e79502b87b4/9eb69/esl_series_2_serverless_v2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 27.916666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAABHklEQVR42iVQ2W6DMBDk/78qFSEXoYnK6QsbsGgA3xjSx1qtNA+7M7Oz2o1mdrFLtQrgFdk0XSWyC+hxSuoE5AeGrgylvH3oKRjQv8cJqKfCvMpoQIldmsB6zfQMnSCrorvpvMJewoBdt2/bb6YL2M2wm95LEkbsXEWsOXqJftzAWUbbe5BXxd6W73bYTL9K4gTedBfaP5IHw6aZmZqFP8Pmk5NITjUlaU+zUHjTW0mtpt5yp5gVWC/QCuIUDblOtasiVgA11RHHCQIJapKxy9vm0qIzqM8tuVdl3JIHRp8QpBjeqiKmJAsSQVcMr2V+QHUczewEi48O3Tl9NHk8kAwWx1f/pSccsIxAvsILycxLr6j8DqcCOZYjuQn+/AWaYz49RtNb2AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7c8f5eaea8d3e35e46786e79502b87b4/263a4/esl_series_2_serverless_v2.webp 480w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/a6361/esl_series_2_serverless_v2.webp 960w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/0b34d/esl_series_2_serverless_v2.webp 1920w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/da28f/esl_series_2_serverless_v2.webp 2880w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/98b7d/esl_series_2_serverless_v2.webp 3840w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/27593/esl_series_2_serverless_v2.webp 3904w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7c8f5eaea8d3e35e46786e79502b87b4/9aebd/esl_series_2_serverless_v2.png 480w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/a91f8/esl_series_2_serverless_v2.png 960w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/ac7a9/esl_series_2_serverless_v2.png 1920w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/f9c26/esl_series_2_serverless_v2.png 2880w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/5da7e/esl_series_2_serverless_v2.png 3840w,
/static/7c8f5eaea8d3e35e46786e79502b87b4/9eb69/esl_series_2_serverless_v2.png 3904w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7c8f5eaea8d3e35e46786e79502b87b4/ac7a9/esl_series_2_serverless_v2.png&quot; alt=&quot; 배치 시간대 ACU 급증 후 즉시 복구되는 Serverless v2의 탄력적 확장 패턴&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;Serverless v2의 탄력적 오토스케일링: 배치 처리 중만 높은 ACU 사용, 평시에는 최소 수준 유지(Gemini 생성)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;&lt;br&gt;
&lt;p&gt;결과적으로 저희는 &lt;strong&gt;&apos;성능의 안전판&apos;과 &apos;비용 효율&apos;&lt;/strong&gt; 이라는 두 마리 토끼를 잡았습니다. 데이터가 적을 땐 확실하게 비용을 아끼고, 폭주할 땐 성능 제한 없이 확실하게 처리하는 &lt;strong&gt;&apos;클라우드 네이티브&apos; 환경을 구축함으로써, 고정형 인스턴스 대비 약 40%의 비용 절감 효과&lt;/strong&gt;를 거둘 수 있었습니다.
&lt;/br&gt;&lt;/br&gt;&lt;/p&gt;
&lt;h2 id=&quot;마치며-안정성과-효율성의-균형&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-%EC%95%88%EC%A0%95%EC%84%B1%EA%B3%BC-%ED%9A%A8%EC%9C%A8%EC%84%B1%EC%9D%98-%EA%B7%A0%ED%98%95&quot; aria-label=&quot;마치며 안정성과 효율성의 균형 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며: 안정성과 효율성의 균형&lt;/h2&gt;
&lt;p&gt;이렇게 저희는 1,300개 매장의 데이터를 안정적으로 처리하면서도 비용 효율적인 시스템을 갖추게 되었고, 전자라벨을 도입하며 두 가지 큰 교훈을 얻었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Application Level&lt;/strong&gt;: 대용량 데이터 집계(Aggregation)는 단순한 병렬화가 아니라, &lt;strong&gt;&apos;데이터의 분할과 정복(Partitioning)&apos;&lt;/strong&gt; 을 통해 Lock을 회피해야 한다는 점&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure Level&lt;/strong&gt;: 클라우드의 이점은 단순히 서버를 빌리는 것이 아니라, 상황에 맞게 &lt;strong&gt;&apos;탄력적(Elastic)으로 사용하는 것&apos;&lt;/strong&gt; 에 있다는 점&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;이제 종이 라벨이 사라진 자리에는 데이터가 흐르고, 그 데이터는 초단위의 실시간 정보로 진화하고 있습니다. 이어지는 다음 편에서는 &quot;파이프라인 위에서 고객과 직원의 경험을 실시간으로 혁신하는 이벤트 기반 재고 시스템&quot; 이야기로 찾아오겠습니다. 계속해서 많은 기대 부탁드립니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/scalability.html#partitioning&quot;&gt;Spring Batch Partitioning Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/rds/aurora/serverless/&quot;&gt;Amazon Aurora Serverless v2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[오프라인 매장에 코드를 배포하다 Part 1: 종이 없는 매장을 만드는 데이터 파이프라인 구축]]></title><description><![CDATA[들어가며 안녕하세요! 올리브영에서 Back-end 개발을 담당하고 있는 '코드다이버(이시훈)'입니다.
많은 분들에게 오프라인 매장의 가격표는 매우 익숙한 풍경이지만, 그 이면에서 온-오프라인 데이터를 실시간으로 동기화하는 전자라벨(ESL…]]></description><link>https://oliveyoung.tech/2026-02-09/esl-series-1/</link><guid isPermaLink="false">https://oliveyoung.tech/2026-02-09/esl-series-1/</guid><pubDate>Mon, 09 Feb 2026 09:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요! 올리브영에서 Back-end 개발을 담당하고 있는 &apos;코드다이버(이시훈)&apos;입니다.
많은 분들에게 오프라인 매장의 가격표는 매우 익숙한 풍경이지만, 그 이면에서 온-오프라인 데이터를 실시간으로 동기화하는 &lt;strong&gt;전자라벨(ESL, Electronic Shelf Label)의 메커니즘&lt;/strong&gt;은 다소 생소하게 느껴지실 겁니다. 그래서 최대한 쉽고 간결하게 설명해드리려고 하니 차근차근 읽어주세요.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;🎯 특히 이런 분들께 도움이 될 거예요&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오프라인 매장의 디지털 전환(DX) 사례가 궁금하신 분&lt;/li&gt;
&lt;li&gt;수백만 개의 IoT 기기를 제어하는 아키텍처가 궁금하신 개발자&lt;/li&gt;
&lt;li&gt;레거시 시스템을 단계적으로 개선한 경험이 필요하신 분&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;💡 요약하자면 이런 기술적 난제를 해결했어요&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;물리적 부채 해결: 종이라벨 교체에 드는 수동 업무 제거&lt;/li&gt;
&lt;li&gt;실시간 동기화: 온-오프라인 재고/가격 데이터 일치&lt;/li&gt;
&lt;li&gt;운영 효율화: 매장당 일 평균 2시간 업무 시간 절감&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;개발자에게 &apos;배포(Deployment)&apos;는 익숙한 일상입니다. 코드를 수정하고, 빌드하고, 서버에 배포하면 끝이죠. 하지만 &lt;strong&gt;올리브영의 오프라인 매장에는 오랫동안 &apos;사람의 손으로 직접 수행하는 수동 배포(Manual Deployment)&apos; 시스템이 존재&lt;/strong&gt;했습니다. 마치 수천 대의 서버에 SSH로 일일이 접속해 파일을 덮어쓰던 시절처럼, 이러한 종이라벨 교체는 매장 직원의 물리적인 리소스를 끝없이 소모하는 작업이었습니다. 이러한 물리적인 제약을 기술로 극복하고, 흩어진 데이터를 모아 동적인 디지털 환경으로 변화시킨 여정의 첫번째 이야기를 지금부터 본격적으로 시작해 보겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;배경-규모의-경제가-아닌-규모의-부채&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%EA%B2%BD-%EA%B7%9C%EB%AA%A8%EC%9D%98-%EA%B2%BD%EC%A0%9C%EA%B0%80-%EC%95%84%EB%8B%8C-%EA%B7%9C%EB%AA%A8%EC%9D%98-%EB%B6%80%EC%B1%84&quot; aria-label=&quot;배경 규모의 경제가 아닌 규모의 부채 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배경: 규모의 경제가 아닌 &apos;규모의 부채&apos;&lt;/h2&gt;
&lt;p&gt;본격적으로 이야기를 시작하기 전, 다뤄야 할 숫자를 먼저 이야기 해보겠습니다. 올리브영은 전국에 약 1,300여 개의 매장을 운영하고 있으며, 각 매장에는 수천 개의 상품이 진열되어 있습니다. 이를 전체 매장으로 환산하면 &lt;strong&gt;수백만 개의 가격표가 존재&lt;/strong&gt;한다는 계산이 나옵니다. 이 거대한 규모는 디지털 환경이 아닐 때, 단순한 관리의 대상을 넘어 &lt;strong&gt;감당하기 힘든 &apos;부채&apos;&lt;/strong&gt; 로 다가왔습니다. 수백만 개의 라벨을 사람의 손으로 관리해야 한다는 것, 이것이 저희가 마주한 현실이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;레거시legacy의-한계-종이라벨이라는-물리적-부채&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%EA%B1%B0%EC%8B%9Clegacy%EC%9D%98-%ED%95%9C%EA%B3%84-%EC%A2%85%EC%9D%B4%EB%9D%BC%EB%B2%A8%EC%9D%B4%EB%9D%BC%EB%8A%94-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EB%B6%80%EC%B1%84&quot; aria-label=&quot;레거시legacy의 한계 종이라벨이라는 물리적 부채 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;레거시(Legacy)의 한계: 종이라벨이라는 &apos;물리적 부채&apos;&lt;/h2&gt;
&lt;p&gt;개발자 관점에서 &lt;strong&gt;종이라벨 시스템은 &apos;확장성이 부족하고 업데이트 비용이 높은 정적 뷰(Static View)&apos;&lt;/strong&gt; 였습니다. 단순한 스티커 부착 작업처럼 보이지만, 그 이면에는 더 큰 비효율이 숨어 있었죠.&lt;/p&gt;
&lt;h3 id=&quot;프로모션-기간-내-물리적-업무-부담-과중&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EB%AA%A8%EC%85%98-%EA%B8%B0%EA%B0%84-%EB%82%B4-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EC%97%85%EB%AC%B4-%EB%B6%80%EB%8B%B4-%EA%B3%BC%EC%A4%91&quot; aria-label=&quot;프로모션 기간 내 물리적 업무 부담 과중 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;프로모션 기간 내 물리적 업무 부담 과중&lt;/h3&gt;
&lt;p&gt;올리브영의 핵심 경쟁력 중 하나가 다양하고 역동적인 프로모션이다보니 &lt;strong&gt;종이라벨 환경은 매장 직원들에게 엄청난 &apos;물리적 부채(Physical Debt)&apos;를 의미&lt;/strong&gt;했습니다. 특히 올영세일과 같은 대규모 행사가 시작되면, 하룻밤 사이 수천 개의 상품 가격이 변동됩니다. 직원들은 개점 전에 이 수많은 라벨을 모두 새로 출력해 진열대를 일일이 찾아다니며 교체해야 했는데, 이 과정은 단순 반복 노동을 넘어 &lt;strong&gt;오프라인 운영의 가장 큰 병목 구간&lt;/strong&gt;이었습니다.
&lt;/br&gt;&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1408px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/839ef6deef6fb7063be610ce90a08281/ced39/esl_series_1_efficiency_1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACpUlEQVR42gXB608SAQAA8Ptn2lq1nLP1cLWsdM6yiRaZgSIc5PGG48CDjvdD8HjDnRAHJyhggqK8MoZg+Sq1Wi9bW5Z+yPkh+xv6/YDAxJ0va4yX8/0GA9c1qaRi1szM5Lut9MmfV4cHldOT1X+nr7+XY3sJ/HC/+Pe4sVHhNvNdO3XFr4M9ADdcj7rakqHbAn4He7gH5PNZnCEB+MA3oaCnjPmMu1aNFmnzbsq1SZh+/yzmE32k/dyHpvDk+DMwG7jv0LVkIl0FC7xo1CQQ6bRO5RtHxJzBu9fOj/R3I5iOL+ShIEsMsvU2LDV175nsTKMEHh19BBaCt+aiLYvxm2mTqOI1Vu1I3W+cwTEDAjntGKe33aCDo4VMMWfNxWCHFa0XGJXExUpVsrH/DdimoYNP8q0amia9pRQx73Gs+Y0ruViKxBs07sTkuFmTTVPBkIXwaTGTBifMX3/Mrpbdb7dqwHw24Zx0IyimMWjJCC5TSQukI+O1FsrF5eWlEBmRIaoxhWIEkkKQ0KUQp+2WXCm/VKDeb9eAs20XrnR1cCGxGlarxmFm9w1cMpRHoRJhp4L41cudIqmKfB43ePwLAXjbN5Y1CpJW3jSha27WAcmlVt1Aj4cgSMwVibnjSh6lF+X8WNKptsGC1vaObkafy6BWyTl+jF2PSGaMrDg2ErbK3zQrwNJTsBGeyNOhIhWIhe25uMeHm7Sw2A5LWODoQ+5AJ6PXhMrCyOOUEyoHTVmZYBE308jY6gsamKI8Dq1ELQdRKZfQCTBYYEMVfog/rUX6BpkWD8oTcsSC4Qql30mIduOS9ZgyibDnjE92V5KAkPUI4THB3k5kmEk5dW6LJu6xZfXIIh1UjCujDovPjMnVqoTXnMWFC17RCimNmkZJlLVezfwHuNlflsceFxYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/839ef6deef6fb7063be610ce90a08281/263a4/esl_series_1_efficiency_1.webp 480w,
/static/839ef6deef6fb7063be610ce90a08281/a6361/esl_series_1_efficiency_1.webp 960w,
/static/839ef6deef6fb7063be610ce90a08281/8e4a1/esl_series_1_efficiency_1.webp 1408w&quot; sizes=&quot;(max-width: 1408px) 100vw, 1408px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/839ef6deef6fb7063be610ce90a08281/9aebd/esl_series_1_efficiency_1.png 480w,
/static/839ef6deef6fb7063be610ce90a08281/a91f8/esl_series_1_efficiency_1.png 960w,
/static/839ef6deef6fb7063be610ce90a08281/ced39/esl_series_1_efficiency_1.png 1408w&quot; sizes=&quot;(max-width: 1408px) 100vw, 1408px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/839ef6deef6fb7063be610ce90a08281/ced39/esl_series_1_efficiency_1.png&quot; alt=&quot;대규모 정기 세일 이벤트 시 매장 매대에 수많은 종이라벨을 붙이느라 분주한 직원의 모습&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;수천 장의 종이라벨을 수동으로 교체하는 매장의 모습(Gemini 생성)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h3 id=&quot;물리적-지연physical-latency과-동기화-이슈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%BC%EB%A6%AC%EC%A0%81-%EC%A7%80%EC%97%B0physical-latency%EA%B3%BC-%EB%8F%99%EA%B8%B0%ED%99%94-%EC%9D%B4%EC%8A%88&quot; aria-label=&quot;물리적 지연physical latency과 동기화 이슈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;물리적 지연(Physical Latency)과 동기화 이슈&lt;/h3&gt;
&lt;p&gt;종이라벨은 현재 상품이 품절인지 아닌지를 실시간으로 보여주지 못합니다. 그래서 고객은 진열대에 상품이 비어있으면 직원에게 &quot;이거 재고 있나요?&quot;라고 묻게 됩니다. 상품이 품절되어도 진열대의 라벨은 여전히 &apos;판매 중&apos; 상태를 유지하기 때문이죠. &lt;strong&gt;이때 발생하는 비효율은 고스란히 직원의 리소스 소모로 이어집니다.&lt;/strong&gt; 직원은 하던 일을 즉시 중단하고 시스템 조회나 창고 확인을 위해 자리를 비워야 합니다.&lt;/p&gt;
&lt;p&gt;이에 저희는 &lt;strong&gt;&apos;데이터(재고 유무)가 고객에게 도달하기 위해 반드시 직원의 물리적 개입이 필요한 시간&apos;을 물리적 지연(Physical Latency)이라 정의&lt;/strong&gt;했습니다. 시스템 관점에서는 매장 운영이라는 메인 루프를 멈추게 만드는 불필요한 I/O Blocking인거죠. 데이터가 스스로 자신을 증명하지 못해 발생하는 이 대기 시간은 결국 매장 전체의 업무 효율을 저하시켰습니다.
&lt;/br&gt;&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/73adddd788d0868c94525e4f7b63bf61/cb544/esl_series_1_efficiency_2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVR42gGfAmD9AEhJQF5fWGhpYHl6YoqKcZ2ag6OhicbCpOfhuOHctdnVu9bX1tzd29ra2dzc2uHh39vc2tzd29zc2t7e2QBbU01oZWBsaWVdXEFfXUlkZmZZXVhnY1aRh3GSiG++uq7Ly8y8vLuwsLClpqXDw8KpqaewsK/ExMTm5uMAfGdiZlhWs4+MrH91sIB2nHJsoHx2dV9SXj8sZ0g3c2JWsLi33uHf7uzp8fHw9ff28/Tz5+bl4+Pa29zPAHNSR3hiUrKNfaJ5bLSRfrqGdLuOfm9NOWBGNseliKGHb5CNg5WjnbSrhZGDa1lPR3VzbMjYzLLT0MXSywDChXi4m4i6oJWrmZSynY6yn5ykk5J4XEnXuJvpxqa7oYmjk4Smo5uIjH6rln+ggmhVTEHE0sa5uajJxrgAr6udkJGJeH55eH97mJmMbXRxbm5oV0I3n3xp362QiHBeXWlsYGtsW2Vso5mLwaCLb3Btws3CrqqTy8a5ACgxLDVDQWBobmBhaW5ycHZkVo1sYpF7YaiRS7mTZLKZQZygiISKloyamnJ3e2ZmZKmyscnQw6eej7/CtwBtNjN7VEqRl5h3YXNla2ikiXXFqmvXv2Ly3lTLsVzZu1HCtGF3b2yUk4F5eYI5N0OcnJ/a2canoY6/wbcAsWlmu52PqZ+UtJ2RgIqGjoV4sJxbsptQsJRPvZlox6thy7pWfXNnqpF/dXZ3LCw8dXZ6vr+1xMat0dHBAJmfj4CEdnFuY3twY4x0YXltYYpwZpxuc5p6VZN2MpZvOJNxSZ+MeJyNfXd6cSkyNIidnbDHxK3R08TRzQBqXFd4YlVnUEdhR0GNbl+MgnSZiHqmj4J3f3dhXleaY0uebVeRlI+eoqGNkZA9RER5jY2uz9CoxMbH0s7Pb3PoV6bUFAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/73adddd788d0868c94525e4f7b63bf61/263a4/esl_series_1_efficiency_2.webp 480w,
/static/73adddd788d0868c94525e4f7b63bf61/a6361/esl_series_1_efficiency_2.webp 960w,
/static/73adddd788d0868c94525e4f7b63bf61/0b34d/esl_series_1_efficiency_2.webp 1920w,
/static/73adddd788d0868c94525e4f7b63bf61/c6529/esl_series_1_efficiency_2.webp 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/73adddd788d0868c94525e4f7b63bf61/9aebd/esl_series_1_efficiency_2.png 480w,
/static/73adddd788d0868c94525e4f7b63bf61/a91f8/esl_series_1_efficiency_2.png 960w,
/static/73adddd788d0868c94525e4f7b63bf61/ac7a9/esl_series_1_efficiency_2.png 1920w,
/static/73adddd788d0868c94525e4f7b63bf61/cb544/esl_series_1_efficiency_2.png 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/73adddd788d0868c94525e4f7b63bf61/ac7a9/esl_series_1_efficiency_2.png&quot; alt=&quot;매장에서 고객의 재고 유무 질문에 답변하기 위해 업무를 멈춘 직원&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;실시간 재고 정보 미노출로 인해 발생하는 물리적 지연 상황(Gemini 생성)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h3 id=&quot;정보-표시-유연성-부족-및-마케팅-제약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%B3%B4-%ED%91%9C%EC%8B%9C-%EC%9C%A0%EC%97%B0%EC%84%B1-%EB%B6%80%EC%A1%B1-%EB%B0%8F-%EB%A7%88%EC%BC%80%ED%8C%85-%EC%A0%9C%EC%95%BD&quot; aria-label=&quot;정보 표시 유연성 부족 및 마케팅 제약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;정보 표시 유연성 부족 및 마케팅 제약&lt;/h3&gt;
&lt;p&gt;매장의 진열대는 한정된 공간 내에서 수십 수천 개의 상품이 저마다의 존재감을 드러내야 하는 치열한 공간입니다. 하지만 종이라벨은 정해진 규격 안에 텍스트를 인쇄하는 &apos;정적 렌더링&apos; 방식에 머물러 있었습니다.&lt;/p&gt;
&lt;p&gt;개발자 관점에서 이는 &lt;strong&gt;UI가 코드에 하드코딩되어 있어 런타임에 변경할 수 없는 상태&lt;/strong&gt;와 같습니다. 예를 들어, 특정 상품이 &apos;단독 특가&apos; 중일 때는 가격을 강조하고, &apos;증정 행사&apos; 중일 때는 사은품 정보를 부각해야 하는 등 상황에 따른 UI 다형성(Polymorphism)이 필요하지만, 종이라벨은 이러한 동적인 요구사항에 대응할 수 없는 구조적 한계를 가지고 있었습니다. 결국, 프로모션의 성격과 상관없이 천편일률적인 정보만을 제공해야 했고, 이는 곧 오프라인 매장의 마케팅 유연성까지 제약하는 요소가 되었습니다.
&lt;/br&gt;&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4eb58d69a5195732356745027b169c41/cb544/esl_series_1_polymorphism.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.58333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVR42gGfAmD9AJGNh2JiXHh4c4KBd5STfqWikbOum7+7p+bhwu7oyOTfw9TTydrUwtbV0NPT08vLyc3NzO/v7fLy8uTk4gB/eHNaVFBwbWpjYltdXDZfXlppbmtpZ198dmWDfWmuqJff1bvIo1LYzLS+wcSwr63GxcXf397X19bc3NsAsp6XcGBdhXJxuZSSs4N2q4B5mHt4LjExAAAAAAAAKScot7avzsGi5uDZ2drb4OPj7Ozt1NTPx8vFz9HLAKqBempTSJqCcrCJfqyGdraTgriSf2Rna0pWXSgoJRgYGHpnZqSusKSrlsWyjMano6ykpKu0rKmuo6y2sgDGop27fXC4hXfEe3jAioW8l47NtqnKua/ezrvEpI0+ODWTi4eyqaSdp6CUkH6PgX2HeHFtVkdlQy+GbmAAn5+eyJyZuaSfyKqlzZmSl4uHf39/vqui0J+FrJGFWVVhY212X292YWp0ZnJ9bXqJg4KBmn5oZEMxiW9eAF5hYICLiYGQj4uOl7eXicCljZSDeJyPh+Hf3bK1u0E/WFRTZ4mUn4ybp4mVm4OOk8GyoejEpbKReYBtYgCEcGt8PDqDdGmXh5Gwhoeso5za3d7u7uzs8/KanKNMSl5NSmBpbHN2eG5sd2Rxamavi3v/0q6+moNsXFMAv5ONwoN/vpGNv52ZubCutLCviYeEgYOGc2hrQTtOUlBlUE1kaGRsmYBqto56z6GapKKduqeabE04a1tQAK2una6wrLKrqJOLhpaAdqWUiIVzZaJ2aZlpZUM6S0NCV0JCWGFbYJ+Xi7CknsrCvrCso6qaYoxrM7WgaQCUjpOUhn6diX19aWCEbmSlh3qlh3+9n5imm49iZmheYmlbYGVyeXqirLC5rqK9saiqqJbUvW+fhmG8rIw8OHSG7yOEKQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4eb58d69a5195732356745027b169c41/263a4/esl_series_1_polymorphism.webp 480w,
/static/4eb58d69a5195732356745027b169c41/a6361/esl_series_1_polymorphism.webp 960w,
/static/4eb58d69a5195732356745027b169c41/0b34d/esl_series_1_polymorphism.webp 1920w,
/static/4eb58d69a5195732356745027b169c41/c6529/esl_series_1_polymorphism.webp 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4eb58d69a5195732356745027b169c41/9aebd/esl_series_1_polymorphism.png 480w,
/static/4eb58d69a5195732356745027b169c41/a91f8/esl_series_1_polymorphism.png 960w,
/static/4eb58d69a5195732356745027b169c41/ac7a9/esl_series_1_polymorphism.png 1920w,
/static/4eb58d69a5195732356745027b169c41/cb544/esl_series_1_polymorphism.png 2816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4eb58d69a5195732356745027b169c41/ac7a9/esl_series_1_polymorphism.png&quot; alt=&quot;종이라벨에 정보를 다 담지 못해 난감해하는 직원과 혼란스러운 표정의 고객&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;유연하지 못한 라벨 UI가 야기하는 매장 현장의 어려움(Gemini 생성)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;h2 id=&quot;architecture-수백만-iot-기기를-제어하는-메시지-기반-파이프라인-구축&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#architecture-%EC%88%98%EB%B0%B1%EB%A7%8C-iot-%EA%B8%B0%EA%B8%B0%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%98%EB%8A%94-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B8%B0%EB%B0%98-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95&quot; aria-label=&quot;architecture 수백만 iot 기기를 제어하는 메시지 기반 파이프라인 구축 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Architecture: 수백만 IoT 기기를 제어하는 메시지 기반 파이프라인 구축&lt;/h2&gt;
&lt;p&gt;수많은 상품 정보와 실시간 재고 데이터는 어떻게 전국 매장의 진열대까지 오차 없이 도달할까요? 수천 개의 매장, 수백만 개의 상품 데이터를 실시간으로 동기화하기에는 &lt;strong&gt;단순한 API 호출 방식만으로는 한계가 명확&lt;/strong&gt;했습니다. 매장 네트워크의 일시적인 불안정성이나, 대규모 세일 기간의 트래픽 폭주에도 시스템이 무너지지 않아야 했기 때문입니다. 그래서 저희는 대규모 데이터의 신뢰성 있는 전달을 위해 &lt;strong&gt;이벤트 기반의 메시지 파이프라인(Event-driven Message Pipeline)을 설계&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1317px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3eb0afcc9febd023d301c20fc3f1a9b7/9cafe/esl_series_1_architecture.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 50.416666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABhklEQVR42kWRDY+bMAyG+f+/6rRNt+3W226tdtO09IMWrlBKCZDEcRLDHFg5yUSOhf0+eZ0IIR4fP9+ahmhwzjsfw/vg308fwhzxo0BStavty2v+J7nWt+xcgEWu3zv5DFPMiTcYjKNO6aZptFKd7tP6dLrlyTCOV7EtDqmngdvwLu6mnNU6g+us/ZE2x0tnAQITDgM4az0mnigTIk+PdSONgYFoxuM6/+cDVczWqlajBtRaW7CVvD78+vRlu0r4RbFkEdGdy+oi9aU1vYG+Vz1zStkrRcvEEMZxHMYByTlyEfuwehabjQYQ6dvDOv3w+41HCPH369O3oixnFxbfmNkgbMvDqc4Tdlh27IXWxpyLAtABTqaGADYC8SKMdegJELu+B4BK1s+7l032mhAblqaXLOcRAJZ7FsOiX0Sthp9HudrLfRGtZrExog+MnPAjdut1ttvzxb+vZxkR81tvy8Zo65RSYGAy7OPT7nuCjpG8uwvOj1vEp517igT/c1ZFj7k8F131D5xeOh2pQkeKAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3eb0afcc9febd023d301c20fc3f1a9b7/263a4/esl_series_1_architecture.webp 480w,
/static/3eb0afcc9febd023d301c20fc3f1a9b7/a6361/esl_series_1_architecture.webp 960w,
/static/3eb0afcc9febd023d301c20fc3f1a9b7/80ebb/esl_series_1_architecture.webp 1317w&quot; sizes=&quot;(max-width: 1317px) 100vw, 1317px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3eb0afcc9febd023d301c20fc3f1a9b7/9aebd/esl_series_1_architecture.png 480w,
/static/3eb0afcc9febd023d301c20fc3f1a9b7/a91f8/esl_series_1_architecture.png 960w,
/static/3eb0afcc9febd023d301c20fc3f1a9b7/9cafe/esl_series_1_architecture.png 1317w&quot; sizes=&quot;(max-width: 1317px) 100vw, 1317px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3eb0afcc9febd023d301c20fc3f1a9b7/9cafe/esl_series_1_architecture.png&quot; alt=&quot;Conceptual Architecture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;Conceptual Architecture&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;h3 id=&quot;tech-stack&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#tech-stack&quot; aria-label=&quot;tech stack permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Tech Stack&lt;/h3&gt;
&lt;p&gt;저희는 수백만 개의 IoT 기기를 안정적으로 제어하기 위해 &lt;strong&gt;Kotlin과 Spring Boot 3.x를 선택&lt;/strong&gt;했습니다. 특히 &lt;strong&gt;파이프라인 중간에 Message Queue를 도입한 것이 핵심&lt;/strong&gt;입니다. 이는 전사적인 데이터 폭주를 완충하는 버퍼 역할을 수행할 뿐만 아니라, 네트워크 환경이 제각각인 1,300여 개 매장에 데이터를 비동기로 전달하여 시스템 전체의 가용성을 극대화하기 위한 결정이었습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;구분&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;기술 스택&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;Kotlin&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;Spring Boot 3.x&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;Messaging&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;MSK, SQS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;Oracle, ElastiCache (Redis)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;S3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;h3 id=&quot;클라우드-기반의-데이터-중앙화-cloud-centralization&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EA%B8%B0%EB%B0%98%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%95%99%ED%99%94-cloud-centralization&quot; aria-label=&quot;클라우드 기반의 데이터 중앙화 cloud centralization permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;클라우드 기반의 데이터 중앙화 (Cloud Centralization)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;파편화되어 있던 원천 데이터를 전자라벨(ESL)에 적합한 데이터로 모아 클라우드 환경으로 통합&lt;/strong&gt;했습니다. 이제는 새로운 아키텍처가 &apos;단일 진실 공급원(Single Source of Truth)&apos; 역할을 합니다. 상품 정보 변경이나 프로모션 시작과 같은 &lt;strong&gt;모든 이벤트는 거대한 메시지 파이프라인을 통과&lt;/strong&gt;합니다. 이 과정에서 DB 부하를 줄이고 속도를 높이며, 장애 복구 능력을 강화했습니다. 최종적으로 &lt;strong&gt;가공된 데이터가 각 매장의 게이트웨이로 일관성 있게 전달&lt;/strong&gt;됩니다. 어떤 매장이든 동일한 시점에 동일한 데이터를 바라볼 수 있도록 보장하는 것입니다.&lt;/p&gt;
&lt;p&gt;특히 이 과정에서 레거시 DB에 직접적인 쿼리를 날리는 대신, 이벤트 기반의 데이터 동기화를 택하여 조회 성능을 높이고 원본 시스템의 부하를 원천적으로 차단했습니다. 결과적으로 전국의 모든 매장이 물리적 위치와 상관없이 &lt;strong&gt;&apos;가장 최신의, 동일한 데이터&apos;를 바라보는 높은 데이터 일관성을 보장&lt;/strong&gt;하게 되었습니다.&lt;/p&gt;
&lt;h3 id=&quot;물리적-거리를-넘는-원격-관제-remote-monitoring--control&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%BC%EB%A6%AC%EC%A0%81-%EA%B1%B0%EB%A6%AC%EB%A5%BC-%EB%84%98%EB%8A%94-%EC%9B%90%EA%B2%A9-%EA%B4%80%EC%A0%9C-remote-monitoring--control&quot; aria-label=&quot;물리적 거리를 넘는 원격 관제 remote monitoring  control permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;물리적 거리를 넘는 원격 관제 (Remote Monitoring &amp;#x26; Control)&lt;/h3&gt;
&lt;p&gt;클라우드 아키텍처의 가장 큰 강점은 &apos;현장에 가지 않아도 된다&apos;는 것입니다. 이에 저희는 수백만 개의 IoT 디바이스를 안정적으로 운영할 수 있는 &lt;strong&gt;강력한 중앙 관제 시스템을 구축&lt;/strong&gt;했고, 시스템은 실시간으로 반영된 데이터 및 업데이트 성공 여부를 모니터링하고 있습니다. 예를 들어, 제주도 매장의 특정 라벨 업데이트가 실패했다고 가정해 볼까요? &lt;strong&gt;중앙 시스템은 문제를 즉시 감지하고 자동으로 재시도(Retry) 명령을 내려 해결합니다.&lt;/strong&gt; 엔지니어가 KTX나 비행기를 타고 현장으로 출동하지 않아도, 서울 본사 사무실에 앉아 원격으로 문제를 진단하고 해결할 수 있는 환경을 구현된 것이죠.&lt;/p&gt;
&lt;h2 id=&quot;feature-물리적-지연을-0으로-만드는-기술들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#feature-%EB%AC%BC%EB%A6%AC%EC%A0%81-%EC%A7%80%EC%97%B0%EC%9D%84-0%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EA%B8%B0%EC%88%A0%EB%93%A4&quot; aria-label=&quot;feature 물리적 지연을 0으로 만드는 기술들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Feature: 물리적 지연을 &apos;0&apos;으로 만드는 기술들&lt;/h2&gt;
&lt;p&gt;이러한 모든 기술적 노력은 결국 매장 운영의 물리적 비효율을 제거하고, 데이터를 통해 고객과 직원의 경험을 개선하기 위함입니다. 구체적으로 구현한 핵심 기능들은 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;상황에-맞는-동적-뷰dynamic-view-렌더링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%ED%99%A9%EC%97%90-%EB%A7%9E%EB%8A%94-%EB%8F%99%EC%A0%81-%EB%B7%B0dynamic-view-%EB%A0%8C%EB%8D%94%EB%A7%81&quot; aria-label=&quot;상황에 맞는 동적 뷰dynamic view 렌더링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상황에 맞는 동적 뷰(Dynamic View) 렌더링&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;종이라벨의 고질적인 &apos;정보 경직성&apos;을 서버 중심의 템플릿 렌더링 기술로 해결&lt;/strong&gt;했습니다. 전자라벨은 이제 단순한 가격표가 아니라, 서버에서 전송한 데이터를 실시간으로 시각화하는 단말기 역할을 수행합니다. 프로모션 성격에 맞춰 레이아웃을 동적으로 변경하는 로직을 통해, 한정된 디스플레이 공간 안에서 정보의 우선순위를 자유롭게 조정하며 공간 효율과 전달력을 극대화했습니다.&lt;/p&gt;
&lt;h3 id=&quot;이벤트-기반의-준실시간-품절-반영&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EA%B8%B0%EB%B0%98%EC%9D%98-%EC%A4%80%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%92%88%EC%A0%88-%EB%B0%98%EC%98%81&quot; aria-label=&quot;이벤트 기반의 준실시간 품절 반영 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;이벤트 기반의 준실시간 품절 반영&lt;/h3&gt;
&lt;p&gt;가장 큰 페인 포인트는 고객이 보는 정보와 실제 재고의 불일치였습니다. 과거에는 상품이 품절되어도 그 정보가 고객에게 도달하기까지 직원의 물리적 확인과 개입이 반드시 선행되어야 했습니다. 이것이 앞서 언급한 물리적 지연의 실체입니다. 이제는 &lt;strong&gt;시스템이 재고 변동 이벤트(Stock Event)를 실시간으로 감지&lt;/strong&gt;합니다. 그리고 특정 상품이 품절되는 즉시 관련 데이터가 메시지 파이프라인을 타고 전국의 전자라벨 단말에 도달하며, 화면은 즉시 상태를 갱신합니다. 사람이 개입하지 않아도 &lt;strong&gt;시스템이 스스로 온-오프라인의 데이터 정합성을 맞추는 자동 동기화(Auto-Sync) 환경을 구현&lt;/strong&gt;한 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;프로모션-자동화와-배치-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EB%AA%A8%EC%85%98-%EC%9E%90%EB%8F%99%ED%99%94%EC%99%80-%EB%B0%B0%EC%B9%98-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;프로모션 자동화와 배치 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;프로모션 자동화와 배치 처리&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;수작업에 의존하던 대규모 가격 변경 업무를 예약 기반의 배치(Batch) 시스템으로 전환&lt;/strong&gt;했습니다. 행사 시점에 맞춰 대량의 데이터를 안정적으로 분산 처리하여, 전국 1,300여 개 매장의 가격 정보를 오차 없이 업데이트합니다. 수동 교체 시 발생하던 오기입이나 누락 등의 휴먼 에러를 구조적으로 차단하여 운영 안정성을 비약적으로 높였습니다.&lt;/p&gt;
&lt;h2 id=&quot;outcome-기술이-선물한-90만-시간의-가치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#outcome-%EA%B8%B0%EC%88%A0%EC%9D%B4-%EC%84%A0%EB%AC%BC%ED%95%9C-90%EB%A7%8C-%EC%8B%9C%EA%B0%84%EC%9D%98-%EA%B0%80%EC%B9%98&quot; aria-label=&quot;outcome 기술이 선물한 90만 시간의 가치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Outcome: 기술이 선물한 &apos;90만+ 시간&apos;의 가치&lt;/h2&gt;
&lt;p&gt;실제 운영 데이터를 분석한 결과, 다음과 같은 유의미한 성과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📈 &lt;strong&gt;전자라벨 구축에 따른 주요 성과&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;도입 규모:&lt;/strong&gt; 전국 1,300여 개 매장&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;업무 효율화:&lt;/strong&gt; 매장당 &lt;strong&gt;평균 일 2시간&lt;/strong&gt; 절감 (라벨 관리 및 재고 응대 리소스 절감)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고객 경험 향상&lt;/strong&gt;: 상품 큐레이션과 매장 컨디션 관리 등 더 본질적이고 가치있는 고객 경험 개선 업무에 집중&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 무결성&lt;/strong&gt;: 가격 오기입으로 인한 결제 클레임 제로화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대규모 대응력&lt;/strong&gt;: 올영세일 등 피크 타임 시 수만 건의 가격 변경을 수 분 내 자동 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;결국 전자라벨 구축은 단순히 종이를 디지털로 바꾼 것이 아니라, 기술을 통해 구성원의 업무 몰입도를 높이고 고객 경험의 질을 한 단계 끌어올린 디지털 트랜스포메이션의 결실이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;마치며-디지털로-연결된-매장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-%EB%94%94%EC%A7%80%ED%84%B8%EB%A1%9C-%EC%97%B0%EA%B2%B0%EB%90%9C-%EB%A7%A4%EC%9E%A5&quot; aria-label=&quot;마치며 디지털로 연결된 매장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며: 디지털로 연결된 매장&lt;/h2&gt;
&lt;p&gt;올리브영은 계속해서 데이터와 기술로 온라인과 오프라인을 연결하고 있습니다. 그 중에서도 전자라벨 구축은 올리브영의 오프라인 매장이 더 이상 정적인 공간이 아니라, 데이터와 소프트웨어로 제어 가능한 영역으로 확장되었음을 의미합니다.
하지만 이러한 시스템 구축은 거대한 여정의 시작일 뿐이었습니다.&lt;/p&gt;
&lt;p&gt;시스템 구축 이후 올영세일 기간마다 갱신되는 트래픽 폭풍 속에서 어떻게 전국 매장을 동기화했는지 궁금하지 않나요? 이렇게 더 깊이 있는 전자라벨 구축기가 궁금한 분들을 위해 준비한 두 번째 이야기, &lt;strong&gt;&lt;a href=&quot;/2026-02-10/esl-series-2/&quot;&gt;&quot;오프라인 매장에 코드를 배포하다 Part 2: 올리브영 전자라벨(ESL) 최적화 여정&quot;&lt;/a&gt;&lt;/strong&gt; 은 아키텍처 이면에 숨겨진 치열한 성능 개선기와 실전 운영 노하우를 본격적으로 다루고 있습니다. 가장 아날로그적인 공간을 가장 디지털적인 방법으로 혁신해 나가는 올리브영의 DX 과정이 궁금한 분들께는 분명 흥미롭게 읽힐 내용이니 계속해서 많은 관심 부탁드립니다. 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영 대규모 트래픽 레거시 시스템의 무중단 OAuth2 전환기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-10-28/oliveyoung-zero-downtime-oauth2-migration/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-10-28/oliveyoung-zero-downtime-oauth2-migration/</guid><pubDate>Fri, 30 Jan 2026 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 올리브영에서 인증 시스템을 개발하는 망고입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;커머스 플랫폼에서 사용자의 진짜 여정은 로그인에서 시작됩니다. 로그인에 성공하면 사용자는 &quot;인증&quot;된 상태로 사이트를 이용할 수 있게 되며, 특히 올리브영과 같은 커머스 플랫폼에서 &lt;strong&gt;인증은 주문, 결제, 배송 조회 등 모든 핵심 기능의 전제 조건&lt;/strong&gt;입니다. 그렇다 보니 인증 시스템이 평소에는 조용히 제 역할을 하다가, 문제가 생기면 모든 서비스 담당자의 이목이 집중되는 &lt;strong&gt;가장 중요하고도 영향력있는 인프라&lt;/strong&gt;입니다. 로그인이 풀리거나 세션이 끊기면 장바구니가 비워지고, 결제에 실패합니다. 이 때 사용자는 당황스러운 경험을 하게 됩니다. 그래서 인증 시스템을 건드리는 것은 언제나 조심스러운 일입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;2025년 3월부터 10월까지, 저희 스쿼드는 올리브영 온라인몰의 레거시 세션 기반 인증을 OAuth2로 전환하는 프로젝트를 진행했습니다. 전체 배포 이후 2주 뒤에 평소 대비 10배의 대규모 트래픽을 처리해야 하는 정기 플래그십 이벤트 &apos;올영세일&apos;을 앞두고 있었기 때문에 모든 기능을 포괄하는 OAuth2 표준을 완벽히 구현하기보다, 기존 서비스의 &apos;핵심 기능&apos;을 보호하고 무중단 전환을 보장하는 &apos;안전 구조&apos;를 최우선 목표로 삼아 &apos;중단 없이, 장애 없이&apos; 배포했습니다. 이 과정에 겪은 기술적 도전과 해결 과정을 공유합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;1-들어가며-대규모-트래픽-환경에서의-인증-체계-교체&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0-%EB%8C%80%EA%B7%9C%EB%AA%A8-%ED%8A%B8%EB%9E%98%ED%94%BD-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B8%EC%A6%9D-%EC%B2%B4%EA%B3%84-%EA%B5%90%EC%B2%B4&quot; aria-label=&quot;1 들어가며 대규모 트래픽 환경에서의 인증 체계 교체 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 들어가며: 대규모 트래픽 환경에서의 인증 체계 교체&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;11-왜-인증-체계를-바꿨나&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#11-%EC%99%9C-%EC%9D%B8%EC%A6%9D-%EC%B2%B4%EA%B3%84%EB%A5%BC-%EB%B0%94%EA%BF%A8%EB%82%98&quot; aria-label=&quot;11 왜 인증 체계를 바꿨나 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1.1 왜 인증 체계를 바꿨나&lt;/h3&gt;
&lt;p&gt;기존 시스템은 Spring Session 기반이었고, 자동 로그인을 위한 별도 쿠키를 사용했습니다. 이 방식은 다음 세 가지 측면에서 명확한 한계와 비즈니스/기술적 제약을 가져왔습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;확장성 및 복잡성 문제&lt;/strong&gt;: 세션-쿠키의 혼재 구조로 인해 인증 로직이 복잡함, 새로운 마이크로서비스들이 중앙의 세션 Redis에 의존해야 했기 때문에, 마이크로서비스 환경으로의 전환과 독립적으로 인증할 표준 필요&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보안 및 제어의 한계&lt;/strong&gt;: 로그아웃 시에도 자동 로그인 쿠키가 유효하여, 도난/분실 시 보안 사고에 즉시 대응할 수 있는 메커니즘이 부재하였고, 특정 기기에서만 로그아웃하는 기기별 세션 관리가 불가능하여 짧은 수명의 토큰을 통해 노출 시간을 최소화할 필요 있었음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비즈니스 확장성 제약&lt;/strong&gt;: 복잡한 레거시 인증 로직으로 인해 외부 서비스 연동이나 신규 서비스 론칭 시 인증 연동에 큰 공수가 소요되어 비즈니스 확장 속도 저해&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;OAuth2를 선택한 이유&lt;/strong&gt;는 단순 JWT 대신 검증된 표준 프레임워크가 필요했기 때문입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refresh Token 무효화 가능 (RFC 7009)&lt;/li&gt;
&lt;li&gt;짧은 Access Token + 긴 Refresh Token 조합&lt;/li&gt;
&lt;li&gt;멀티 토큰 관리 표준 제공&lt;/li&gt;
&lt;li&gt;향후 OIDC(OpenID Connect, OAuth2 기반의 사용자 신원 확인 계층 표준)로 자연스럽게 진화 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;12-핵심-도전&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#12-%ED%95%B5%EC%8B%AC-%EB%8F%84%EC%A0%84&quot; aria-label=&quot;12 핵심 도전 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1.2 핵심 도전&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가장 큰 과제는 무중단 마이그레이션&lt;/strong&gt;이었습니다. 특히 8월 말 올영세일 기간 중에는 &lt;strong&gt;평소 대비 약 10배의 트래픽이 몰리는 상황&lt;/strong&gt;에서도 안정적으로 전환을 진행해야 했습니다. 이에 이 글에서는 &quot;왜 바꿨는지&quot;보다 &quot;어떻게 안전하게 바꿨는지&quot;에 초점을 맞춥니다. 특히 다음 기술들에 대해 상세히 다루고 있으니 참고하며 읽어주세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Feature Flag 위임 패턴&lt;/strong&gt; - 세일 중에도 점진적 전환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;점진적 롤아웃 전략&lt;/strong&gt; - 10% → 100% 단계적 확대&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jitter 도입&lt;/strong&gt; - Peak TPS 40% 감소&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilience4j 장애 격리&lt;/strong&gt; - Circuit Breaker로 안정성 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;em&gt;[참고로 OAuth2나 JWT의 기본 개념은 다루지 않습니다. 대신 대규모 트래픽 환경에서 실제로 동작하는 구현 패턴과 최적화 기법에 집중합니다.]&lt;/em&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;2-fail-safe-architecture-안전한-전환을-위한-4가지-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-fail-safe-architecture-%EC%95%88%EC%A0%84%ED%95%9C-%EC%A0%84%ED%99%98%EC%9D%84-%EC%9C%84%ED%95%9C-4%EA%B0%80%EC%A7%80-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;2 fail safe architecture 안전한 전환을 위한 4가지 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. Fail-Safe Architecture: 안전한 전환을 위한 4가지 전략&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;무중단 마이그레이션의 핵심은 &lt;strong&gt;언제든 되돌릴 수 있어야 한다는 원칙&lt;/strong&gt;이었습니다. 코드 배포 없이 런타임에 인증 방식을 전환하고, 문제 발생 시 즉시 롤백할 수 있는 구조가 필요했습니다.&lt;/p&gt;
&lt;h3 id=&quot;21-feature-flag-위임-패턴-런타임-전환&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#21-feature-flag-%EC%9C%84%EC%9E%84-%ED%8C%A8%ED%84%B4-%EB%9F%B0%ED%83%80%EC%9E%84-%EC%A0%84%ED%99%98&quot; aria-label=&quot;21 feature flag 위임 패턴 런타임 전환 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.1 Feature Flag 위임 패턴 (런타임 전환)&lt;/h3&gt;
&lt;p&gt;Feature Flag 기반 위임 패턴을 적용했습니다. 하나의 진입점(&lt;code class=&quot;language-text&quot;&gt;FeatureFlagDelegatingInterceptor&lt;/code&gt;)에서 사용자별로 다른 인증 방식으로 요청을 위임하는 구조입니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/08e9791db5ce0e65aff026e43f2168a8/42a19/featureflagdelegating.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAADBUlEQVR42o2TyWsTcRzF85dUL+LVkx7Eg4IIghcFKaInzypaaqEe26JYTVsklkrpnjZbkybNMllq9jSZptmaTDJrmshMZp/EFntR9JelW9qC8Bgm5PtJ3vfLe6q6VBU5XOIJiTsr/LSIwyfRnOcJVV2qSCwGeJE9KewCdQYktvlzTVioYWdJgUF5piTUUKBzeSCVIp6EMbHWImso8NYQy3WRUgTyAgtYBxZYHAiQAOPposIRaD6x4XNmUvF4LFgt58H0CQv4Kbj9ncCUwCayXNn/xSHIlmXNurCo87qcMl06kMsSh3eZP/znGgZuoPAkW0WQqAdeX0FiPqNuqpB0e8IhvdMRScF0tdjFH8NMpUDTaMZng573Wp49gjXqbETDJD5/dCTufdA9mLB547GGSLEMdnSjJty6EL7htui100h2s7gT02vHioVEwT+Sc70tpTyW5bFcyt8Qd9MRVwWFFYESW05VzZCw4I3AEDjst4M7yTxh1PR6tIMJ51DCOuBe7FudfPqnToSTm68M/hETRFcRsKB4DHNEOhkNBL5DLjteTFJEfsf1Mml/vWntQ1cfO72uca2+f87c0z9zddgchiP7IiW0r63wBIVnPG7I6/WazZZoJJAK6bKLtxKay2H1pcLklQg09dW8fl9ju76Qu/bJfnNontpFGgKhUoRKXaCScNDhsBsMepPRaNCvkPmgdfQ2bHqTWhtwjt8hC8Hfe5WHGnPP1Pbd0ZUX3/S7FaQOYLkFZ1OR+bk5tVr9bnBwdmaapbFswpKMQ1txKADN/j1g35uhJxNLk6bVG8NaHInvSVQnJGB1nkHDfse61bi8NBMJOPPpINjc77GENqzFfBzLR20+ZzS+USPhL2brVm5rTySPE9ZKMlUXyJ9yuSGSFLb9g8qU8dROOkTv5vBSck8gFA7jmNKBRADD4lGr2nFpRoVBOboEXIAyyK2GyzwJhsBHDpSslW2wUTuOTVgBlWz2udP+drHbmTup051tieuqJIefM3SOMImngKmuPv+vJA6sQ/wDgQPhMZgNgr8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/08e9791db5ce0e65aff026e43f2168a8/263a4/featureflagdelegating.webp 480w,
/static/08e9791db5ce0e65aff026e43f2168a8/a6361/featureflagdelegating.webp 960w,
/static/08e9791db5ce0e65aff026e43f2168a8/d71bc/featureflagdelegating.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/08e9791db5ce0e65aff026e43f2168a8/9aebd/featureflagdelegating.png 480w,
/static/08e9791db5ce0e65aff026e43f2168a8/a91f8/featureflagdelegating.png 960w,
/static/08e9791db5ce0e65aff026e43f2168a8/42a19/featureflagdelegating.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/08e9791db5ce0e65aff026e43f2168a8/42a19/featureflagdelegating.png&quot; alt=&quot;Feature Flag 위임 패턴 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 1. Feature Flag 기반 위임 패턴 - 런타임에 인증 방식을 동적으로 전환하는 구조&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;22-구현-상세-strategy-패턴으로-안전한-전환&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#22-%EA%B5%AC%ED%98%84-%EC%83%81%EC%84%B8-strategy-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EC%95%88%EC%A0%84%ED%95%9C-%EC%A0%84%ED%99%98&quot; aria-label=&quot;22 구현 상세 strategy 패턴으로 안전한 전환 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.2 구현 상세: Strategy 패턴으로 안전한 전환&lt;/h3&gt;
&lt;p&gt;Feature Flag 위임 패턴을 실제로 구현하기 위해 &lt;code class=&quot;language-text&quot;&gt;FeatureFlagDelegatingInterceptor&lt;/code&gt;를 중심으로 Strategy 패턴을 적용했습니다. 이 인터셉터는 Feature Flag 값에 따라 적절한 인터셉터 체인을 선택하는 역할을 담당합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;먼저 전체 아키텍처를 살펴보겠습니다. 아래 다이어그램은 HTTP 요청이 들어왔을 때 어떻게 처리되는지 보여줍니다.&lt;/p&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5443bfcf0ca2871279f863e1d3fd3ce1/42cbc/feature-flag-flow-diagram.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 98.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABibAAAYmwFJdYOUAAAC/UlEQVR42pWU2W8TMRDG+3+DeEHwWiFVgqpcbzxBJV5Ki4QA0fJQaLtpQrKbeNe7vvZMmqNtmutj7DYgRDlqyRpvMv555vOMV8qyBI8ipKnB38ZisXBWa4319XVsbDzG2toa9vf33e/T6dT5rLx8tYlbt+/g6bPnqLp9nJ2fO4f5fO7mErS0ZdXFu4+7eP/ps7NH9SYSlaITCeg0x0pRnRBkjMFgCKlTpFnxC8BaC57RpC8Mh6dgUqFdMbQyH8eyAU/W8E20IRQBE2XQ4QJRohHGGkXZdaA3W1u4f+8uDr5epjSZXDg7GI4Q8ASBiNGKQzRYB37C4fOYAiKgNDkBNbjM0I4Uqt6J2/jFa+H1uy/YPWSosQrHgaRIZw7I6OAwScEpCB5LsikYN7AsB7QODsiV09EOpro4DAf4cJBgr5HjwFeYTSfokzTtSCKgKaSBSTOEwhDDQJniGuBVhFJndKqEd1xH0OFOkmXKVppACDTDCH47RGz/p73XR9jrO/GFqdAJGyjlA3C+Rz72suYuwrwooI1GludQdAdGaVdOJr025csIdVoi5Aw62SKd6uAicwfZCNtUIj5L0A4FlYtEi9Z2SpX9GWg325KZziguV0LzHymzWCGhG7Uy+Cy+WpOGafFn4FUZ/tYp/QGVTZiQXw9xnKDZ8tHt0VrYslP/D1yOc2oCSZpZv97JgNaGtCzcejg6vXmEUkqsrj4A64RYkAqbm5vY3n5LNbrAeHxxc2Cv30edBWjShbUihkbgu475xkNwpW4OrE66qOkajrSHw8SDFx6jnjVwqI7QNuwmwEs7Oj2DyHLEJkOL0g5YRL0do0k2IW1/AsV/3jKlvLOzA8+rUTmBdJvgYjLFbDa7fA8tMBK2sQsH/hdQa4OH60+wRa9RkUZQcQupCqkRCrd3RdAbmJcVUttG1Dq2feywJy4f2R/vIUGHoxGy3hm0ZCgPHqP6+gi59wLNgHradkqXetfSU6ol++Jajf42zsdj52eyDLmJkeuIrKD9pavF77B21OsIX0yiAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5443bfcf0ca2871279f863e1d3fd3ce1/263a4/feature-flag-flow-diagram.webp 480w,
/static/5443bfcf0ca2871279f863e1d3fd3ce1/a6361/feature-flag-flow-diagram.webp 960w,
/static/5443bfcf0ca2871279f863e1d3fd3ce1/64296/feature-flag-flow-diagram.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5443bfcf0ca2871279f863e1d3fd3ce1/9aebd/feature-flag-flow-diagram.png 480w,
/static/5443bfcf0ca2871279f863e1d3fd3ce1/a91f8/feature-flag-flow-diagram.png 960w,
/static/5443bfcf0ca2871279f863e1d3fd3ce1/42cbc/feature-flag-flow-diagram.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5443bfcf0ca2871279f863e1d3fd3ce1/42cbc/feature-flag-flow-diagram.png&quot; alt=&quot;Feature Flag 위임 패턴 상세 플로우&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 2. Feature Flag 기반 위임 패턴의 전체 요청 처리 흐름&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;HTTP 요청이 들어오면 &lt;strong&gt;FeatureFlagDelegatingInterceptor&lt;/strong&gt;가 모든 요청을 가로챕니다. 그 다음 &lt;strong&gt;FeatureFlagService&lt;/strong&gt;를 통해 Database에서 기능플래그를 조회하고, &lt;strong&gt;기능플래그 확인&lt;/strong&gt; 분기점에서 Yes/No를 결정합니다. 이 결정에 따라 &lt;strong&gt;JWT 활성화&lt;/strong&gt; 또는 &lt;strong&gt;JWT 활성화되지 않음&lt;/strong&gt; 경로로 분기되며, 각각 &lt;strong&gt;JWT AuthenticationInterceptor&lt;/strong&gt; 또는 &lt;strong&gt;Legacy AuthenticationInterceptor&lt;/strong&gt;가 실행됩니다. 마지막으로 Request attribute에 &lt;strong&gt;Phase 정보를 저장&lt;/strong&gt;한 후 최종적으로 &lt;strong&gt;Spring Controller&lt;/strong&gt;로 전달됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이러한 흐름을 구현한 핵심 코드는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FeatureFlagDelegatingInterceptor&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HandlerInterceptorAdapter&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;preHandle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HttpServletRequest&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                            &lt;span class=&quot;token class-name&quot;&gt;HttpServletResponse&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                            &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; handler&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 1. Feature Flag 확인 (Database 조회)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; isPhase2Enabled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; loginTokenFlagService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;activatePhase2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// 2. Phase 정보 저장 (모니터링용)&lt;/span&gt;
        request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;oauth.phase.version&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isPhase2Enabled &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2.0&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// 3. Strategy 패턴: 동적으로 인터셉터 선택&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isPhase2Enabled&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;executePhase2Chain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handler&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// JWT&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;executeLegacyChain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handler&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// Legacy&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이 구현은 네 가지 핵심 설계 원칙을 따릅니다. &lt;br&gt;
첫째, &lt;strong&gt;단일 진입점&lt;/strong&gt;을 통해 모든 요청이 하나의 인터셉터를 거치도록 했습니다. &lt;br&gt;
둘째, &lt;strong&gt;런타임 분기&lt;/strong&gt;를 통해 Database 설정만으로 즉시 전환할 수 있게 했습니다. &lt;br&gt;
셋째, &lt;strong&gt;안전한 Fallback&lt;/strong&gt;으로 오류 시 자동으로 Legacy 모드로 전환됩니다. &lt;br&gt;
넷째, &lt;strong&gt;완전한 격리&lt;/strong&gt;를 통해 Phase 2와 Phase 1 로직이 독립적으로 동작하도록 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그렇다면 Feature Flag는 어떻게 Phase를 결정할까요? 우리는 임직원을 대상으로 선정하되, 비로그인 사용자는 제외하는 것으로 기준을 설정하고 단계를 나눴습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;임직원을 첫 배포 대상으로 선택한 이유&lt;/strong&gt;는 실제 운영 환경에서의 검증이 필요했기 때문입니다. 개발 서버에서 아무리 테스트해도 실제 사용자 환경에서만 발견되는 문제들이 있습니다. 특히 쿠키처럼 개인 기기와 브라우저 설정에 따라 동작이 달라지는 기능의 경우, 다양한 환경에서의 실제 검증이 필수적입니다. 임직원 대상 Beta Test를 통해 피드백을 받으면서 실 사용자에게서 발생할 수 있는 오류를 사전에 체크할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;비로그인 사용자를 제외한 이유&lt;/strong&gt;는 간단합니다. OAuth2 기반 자동 로그인은 로그인한 사용자만을 대상으로 하기 때문에, 비로그인 사용자는 애초에 전환 대상이 아닙니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이러한 배경을 바탕으로 Feature Flag는 3단계 체크를 통해 Phase를 결정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LoginTokenFlagServiceImpl&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LoginTokenFlagService&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;activatePhase2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HttpServletRequest&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                                 &lt;span class=&quot;token class-name&quot;&gt;HttpServletResponse&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token class-name&quot;&gt;Customer&lt;/span&gt; customer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCustomer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;// 1. All Open Flag 확인 (전체 공개)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isFlagEnabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;oauth.flag.code.phase2.all&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 모든 사용자에게 Phase 2 적용&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;// 2. 비로그인 사용자는 Phase 1 사용&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;customer &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;customer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isLoggedIn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;// 3. Staff Open Flag 확인 (임직원 대상)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isFlagEnabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;oauth.flag.code.phase2.staff&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;isStaff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;customer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 기본값: Phase 1&lt;/span&gt;

        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 오류 시 안전하게 Phase 1으로 Fallback&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;Feature Flag는 다음과 같은 단계로 구성됩니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;단계&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Flag&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;설명&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;대상&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;0&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;(모두 비활성)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Phase 1만 사용&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;전체&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;code&gt;phase2.staff&lt;/code&gt; = Y&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;임직원 대상 테스트&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;내부 검증&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;code&gt;phase2.all&lt;/code&gt; = Y&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;전체 공개&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;모든 사용자&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;p&gt;이러한 구조 덕분에 &lt;strong&gt;데이터베이스 설정만 변경&lt;/strong&gt;하면 코드 배포 없이 즉시 전환할 수 있었습니다. 또한 각 요청마다 Phase 정보를 로깅해 Datadog에서 &lt;strong&gt;Phase별 트래픽 분포&lt;/strong&gt;, &lt;strong&gt;에러율&lt;/strong&gt;, &lt;strong&gt;응답시간&lt;/strong&gt;을 실시간으로 추적할 수 있었습니다. 이를 통해 점진적 롤아웃 과정에서 두 Phase 간 성능을 비교하고 안정성을 검증했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;결과적으로 이 위임 패턴은 Feature Flag 값 변경만으로 인증 방식을 즉시 전환할 수 있었고, 예외가 발생하면 자동으로 Legacy로 처리되는 안전한 Fallback을 제공했습니다. Phase 2와 Phase 1 코드는 완전히 격리되어 독립적으로 동작했으며, 코드 배포 없이 DB 설정만 변경하는 무중단 배포가 가능했습니다. 임직원부터 시작해 일부 사용자를 거쳐 전체로 확대하는 세밀한 제어가 가능했고, 각 Phase별 성능 메트릭을 실시간으로 추적할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;Feature Flag 위임 패턴으로 런타임 전환 구조를 만들었다면, 이제 실제로 어떻게 사용자를 단계적으로 전환했는지 살펴보겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;23-점진적-롤아웃-전략-10--100&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#23-%EC%A0%90%EC%A7%84%EC%A0%81-%EB%A1%A4%EC%95%84%EC%9B%83-%EC%A0%84%EB%9E%B5-10--100&quot; aria-label=&quot;23 점진적 롤아웃 전략 10  100 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.3 점진적 롤아웃 전략 (10% → 100%)&lt;/h3&gt;
&lt;p&gt;Feature Flag를 통해 사용자별로 단계적으로 확대하는 전략을 수립했습니다. &lt;strong&gt;핵심은 문제 발생 시 즉시 되돌릴 수 있는 안전장치를 유지&lt;/strong&gt;하면서 점진적으로 범위를 넓혀가는 것이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;점진적 롤아웃 일정은 다음과 같았습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;날짜&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;전환율&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;단계&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;07/28 (월)&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;10%&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;첫 배포&lt;/strong&gt; - 내부 검증&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;07/29 (화)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;10%&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;안정성 모니터링&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;07/30 (수)&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;20%&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;+10% 확대&lt;/strong&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;08/01-11&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;20%&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2주간 안정화&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;08/12 (화)&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;+30% 확대&lt;/strong&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;08/13 (수)&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;75%&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;+25% 확대&lt;/strong&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;08/14-15&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;75%&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;대규모 검증&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;08/16 (토)&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;전체 배포 완료&lt;/strong&gt;&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;h4 id=&quot;shadow-mode-토큰-선배포-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#shadow-mode-%ED%86%A0%ED%81%B0-%EC%84%A0%EB%B0%B0%ED%8F%AC-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;shadow mode 토큰 선배포 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Shadow Mode: 토큰 선배포 전략&lt;/h4&gt;
&lt;p&gt;본격적인 전환 전, &lt;strong&gt;Phase 1에서는 토큰을 &quot;발급만&quot; 하고 실제 인증에는 사용하지 않았습니다.&lt;/strong&gt; 기존 세션 기반 인증은 그대로 유지하면서, 백그라운드에서 OliveToken(Access Token, Refresh Token)을 함께 발급하는 방식입니다. 즉, 기존 세션과 새로운 토큰이 공존하는 상태에서 안정성을 먼저 검증한 것입니다. 이 전략의 핵심 목적은 아래 두 가지였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;성능 검증&lt;/strong&gt;: 토큰 발급 로직이 실제 트래픽에서 안정적으로 동작하는지 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;토큰 선배포&lt;/strong&gt;: 최대한 많은 사용자에게 미리 토큰을 발급&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;토큰 선배포가 중요한 이유는, 나중에 세션에서 토큰 기반으로 전환할 때 &lt;strong&gt;토큰이 없는 사용자는 로그아웃되기 때문&lt;/strong&gt;입니다. 미리 사용자의 &quot;주머니&quot;에 토큰을 넣어두면, 실제 전환 시점에 자연스럽게 로그인이 유지됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;롤아웃 전략은 다음과 같습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;점진적 확대&lt;/strong&gt;: 10% → 20% → 50% → 75% → 100%&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 단계마다 2-14일의 안정화 기간 확보&lt;/li&gt;
&lt;li&gt;50% 이전까지는 신중하게 진행 (2주간 20% 유지)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;단계별 목적&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(임직원 대상): 내부 검증&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;10%&lt;/strong&gt;: 기본 동작 검증&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;20%&lt;/strong&gt;: 초기 사용자 피드백&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50%&lt;/strong&gt;: 대규모 트래픽 검증, 성능 이슈 확인&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;75%&lt;/strong&gt;: 올영세일 전 최종 검증&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;100%&lt;/strong&gt;: 전체 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;최종 목표: 올영세일 대비&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;100% 전환 완료 후 2주 버퍼를 두고 올영세일 진입&lt;/li&gt;
&lt;li&gt;평시 트래픽에서 100% 안정성 확인 = 올영세일 준비 완료&lt;/li&gt;
&lt;li&gt;올영세일 = 실전 대규모 트래픽 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;08/16 - 100% 배포 완료
  ↓
08/16~28 - 2주간 안정화
  ↓
08/29 - 올영세일 시작 (평소 대비 10배 트래픽)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;올영세일 기간 중 실전 트래픽에서 새로운 인증 체계를 검증했습니다. 평소 대비 10배의 트래픽 상황에서도 안정적으로 작동했습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 753px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/53ecdd6b11e0c68f7cbab8eb00142ed3/64f58/Latency.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 34.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+UlEQVR42q2RS3LEIAxEOUdiMDYIEB/bM6lU7n+yjqDs8ayyyuLRErQoCVSKCZwyjJ7/BbUuDh09mb/51IPpjevszjXUbCyIpEvO4FxOvcmnllIlrqKb0F55lpqe17ojRoYKFFFSQfQZQYiehwbHgxJ3MFWQS2j8BAfx0nnub3q9WwjKO4K1HsasmGc3uGKtLRId8GvGpI1ctA1v3zdmEY/4REc81EL1h/Q+orYDFAt8YLT2GMp5w/PrB56kgyTjlR3OJ5R6IMuIq9QVqUssE4jfuQi1yJLzPsZ5tIS9yNj1G7QG+X3GXvN44xgSrHQwfUxD3+Pe1PVpv9shxkwE8I2/AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/53ecdd6b11e0c68f7cbab8eb00142ed3/263a4/Latency.webp 480w,
/static/53ecdd6b11e0c68f7cbab8eb00142ed3/ea08d/Latency.webp 753w&quot; sizes=&quot;(max-width: 753px) 100vw, 753px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/53ecdd6b11e0c68f7cbab8eb00142ed3/9aebd/Latency.png 480w,
/static/53ecdd6b11e0c68f7cbab8eb00142ed3/64f58/Latency.png 753w&quot; sizes=&quot;(max-width: 753px) 100vw, 753px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/53ecdd6b11e0c68f7cbab8eb00142ed3/64f58/Latency.png&quot; alt=&quot;올영세일 기간 Latency 메트릭&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 3-1. 올영세일 기간 Latency 모니터링 (P50/P75/P95)&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 739px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ad1b9efb88a028e47cb9c4a607169a36/8dd81/Request_Volume.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.416666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABn0lEQVR42p2QzW7TQBSF/QYIUSe2ZzxjpxVtaYDwI9RCG2KlbQIRCjRpgpoKSiw1NBFBZQGoiA0rlrCIWIAET8CaFSxZwSt9XLtvwOJq7pxzz5m5x1k8v8TlSxXKKxe5sLyCNRGhNv9dTmRj5ksLRDYijkpEUpmpMTFWOCN9do+i+VM8tDJbyk8TnuL5jM10FkerkFCI5cpNsl75QU6qQOWCzFgrjV8ooAOdi/1iEa0znRXczXWB5xHFCziBkLe7R/RPvqDFrJm+ZXNwTChDOy8+snZ3kIs22inl6xsUz56hcqtBLGIlJmt39vKfX622qO8d42Qvt0bvePT+O6tCjj//pvvqE+vtIc++/mH7yQnbB6+ZfvtLtTMi6U0YzX5xLWnTHL4h/fCDG1tdDmc/6b6cycqy2pX1Js3Bc5KdIbXOIY2HE6rtAzb7E1r7U2oPUuq9MfVOSq37lMZgSnL/McnuEVv9MdV7+ySCrzZ6OEbyWlwqY1Ug+fn47hxGqXydQFYtGYOSWHzXlRgUnvCqWMBKht7cOek9wbX0Lll8/wBOw+FfYcQ3cQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ad1b9efb88a028e47cb9c4a607169a36/263a4/Request_Volume.webp 480w,
/static/ad1b9efb88a028e47cb9c4a607169a36/55dcb/Request_Volume.webp 739w&quot; sizes=&quot;(max-width: 739px) 100vw, 739px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ad1b9efb88a028e47cb9c4a607169a36/9aebd/Request_Volume.png 480w,
/static/ad1b9efb88a028e47cb9c4a607169a36/8dd81/Request_Volume.png 739w&quot; sizes=&quot;(max-width: 739px) 100vw, 739px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ad1b9efb88a028e47cb9c4a607169a36/8dd81/Request_Volume.png&quot; alt=&quot;올영세일 기간 Request Volume 메트릭&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 3-2. 올영세일 기간 요청량 추이&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 741px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ee19181a449d7d7e1d14a3cea0585fa9/660ed/Error.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.874999999999996%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAiklEQVR42p2RSw7CMAxEew9i59M0v7IA7n+3gfEqQhESXTxpYtljx95qaSDBR/Q2MPqJFBPcTSBO/2ajEVHx8BoM6quYYYr75YmWE9KQD3b4TljFfhqygIbqpi9PmknzKuaVrPQmogj6OUgdKLmaPvsdMSTsKeP5eFlD0mq3onJU016CHfTIxerIG87yo2XURAxMAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ee19181a449d7d7e1d14a3cea0585fa9/263a4/Error.webp 480w,
/static/ee19181a449d7d7e1d14a3cea0585fa9/d34f7/Error.webp 741w&quot; sizes=&quot;(max-width: 741px) 100vw, 741px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ee19181a449d7d7e1d14a3cea0585fa9/9aebd/Error.png 480w,
/static/ee19181a449d7d7e1d14a3cea0585fa9/660ed/Error.png 741w&quot; sizes=&quot;(max-width: 741px) 100vw, 741px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ee19181a449d7d7e1d14a3cea0585fa9/660ed/Error.png&quot; alt=&quot;올영세일 기간 Error Rate 메트릭&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 3-3. 올영세일 기간 에러율 (0% 유지)&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;실제 운영 메트릭 (2025.08.29-09.04, 올영세일 기간)입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;지표&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;측정값&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;비고&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;P50 레이턴시&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;약 5ms&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;50% 요청이 10ms 이하&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;P75 레이턴시&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;약 35ms&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;75% 요청이 40ms 이하&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;P95 레이턴시&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;약 50ms&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;95% 요청이 50ms 이하&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;리소스 사용률&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;CPU/메모리 30-35%&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;안정적 범위 유지&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;성공률&lt;/strong&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;100%&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;전 기간 무장애 운영&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;이 수치들이 말해주는 핵심 성과는 명확했습니다. 세일 기간 중에도 &lt;strong&gt;절반의 요청이 5ms 이내에 처리&lt;/strong&gt;되었고, &lt;strong&gt;95%의 요청이 50ms 이내에 완료&lt;/strong&gt;되는 매우 빠른 응답 속도를 유지했습니다. &lt;strong&gt;리소스 사용률은 30-35% 수준을 유지&lt;/strong&gt;하며 여유 있는 운영이 가능했고, &lt;strong&gt;100% 성공률&lt;/strong&gt;로 전 기간 무장애 운영을 달성했습니다. 약 5ms, 약 35ms, 약 50ms 수준의 레이턴시가 전 기간 동안 일관성을 유지하며 안정적인 성능을 보여주었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;결국 Feature Flag 위임 패턴의 진정한 가치는 이 순간에 드러났습니다. &lt;strong&gt;코드 배포 없이&lt;/strong&gt; 런타임에서 전환할 수 있었고, 문제가 생기면 &lt;strong&gt;즉시 롤백&lt;/strong&gt;할 수 있었으며, &lt;strong&gt;사용자별로 세밀하게 제어&lt;/strong&gt;할 수 있었습니다. 무엇보다 중요한 것은 평소 대비 10배의 트래픽이 발생하는 &lt;strong&gt;세일 기간에도 안전하게 전환&lt;/strong&gt;을 완료할 수 있었다는 점입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;점진적 롤아웃으로 안전하게 전환을 완료했지만, 대규모 트래픽 환경에서 또 다른 최적화 포인트를 발견했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;24-jitter-도입-peak-tps-40-감소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#24-jitter-%EB%8F%84%EC%9E%85-peak-tps-40-%EA%B0%90%EC%86%8C&quot; aria-label=&quot;24 jitter 도입 peak tps 40 감소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.4 Jitter 도입 (Peak TPS 40% 감소)&lt;/h3&gt;
&lt;p&gt;Feature Flag로 점진적 전환을 준비했지만, 또 다른 문제가 남아있었습니다. &lt;strong&gt;대규모 동시 접속 시 토큰 갱신 요청이 특정 시점에 집중되는 현상&lt;/strong&gt;이었습니다. 세일 기간 중 넷퍼널(Net Funnel) 대기 상황을 예로 들어보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;09:00:00 - 넷퍼널 오픈, 5,000명 동시 입장
         → 5,000건 로그인 요청 (초당 5,000 TPS)

09:05:00 - 정확히 5분 후, Access Token 만료
         → 5,000건 토큰 갱신 요청 (초당 5,000 TPS)

09:10:00 - 또 다시 5분 후
         → 5,000건 토큰 갱신 요청 (초당 5,000 TPS)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/98ad2f0b3adfe262b97b3be5b8c3f94b/42a19/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAELUlEQVR42m2U+1MaVxTH/TP6Q/NLZ5zkh3Y608cvff3SH9J22jqZZGrS6ZikY9pOrI8koIlEixGxKipafD8wKlQQRDRaWaIoirzLQ1BEWFjCQ54iLK/KLr2rzHQ67Zm7M989ez977zn33FOCF/5l+Pn4r+H/48FLcBwDakHEYfQ0dnU3Go3aSCSazmReLon7KY1gKGWyTDYbDoV1WkU7/XEX4+micAYLh7F8vggvLS2yBjr7f+swm/e8Xl8yiUKrqz1Uam8LdXd7G02lEMSjNqipXc2t3c3CZT6WzWFYvgSQmVx8z8VTGrgq85Qvpnzl9Qc9SGpHnpXLslsbiXVp9Digl/xxzGTGR0ei7W0BOs0rhQBIwOncyfzGgxH+Qw5EssACwB67YDer76id5h5keUcGg06H3WTyDg/CzG7v5FhgdCiu1xEwiBvsHuQjyZ3NHRwAl9vlikYjQJysSxKSFUJEoy4YBiKh2IryOEAApLhyEkXnlBbO/drOpx06xO/zILFwSGLzzjbTmffrV4z2eDTsQ2CxydFBYz774VHPkjyTThfhNaP9csP4B+3ct1s513v5erPV4oA/bOO+88vUex28j56xpRrT3oHtMwb/zZbZd3/lvfVkbFpmKMJP5uWvPRgvbZx5o2HyCuW5eEszs6m7RGZfoQlKH7MvkSd6lrfFcnUphXOZJixt5r7+cLReoLiA8d+1R9U8RQVn5w57nbqi15mtIo2V8kJ/gzp5e3yNLNZumu1K496g3EKmj9e2s2mrGh3sx3FQJET0Z5EkavQElQ4vOHQ34o6EQtlCAWb1H1ZXgeDiJzH3ecIiojnZ1U+VaiWRMwwrAQ9QoVTu2rSib9sGNOyCY7GTs0LBw+pztDQHYTgej7/yeFRqjeFuxW5v98QE2+/3F2NGgictU2ukAWEda2EKMrhg5ymKJgP+w+tlvkVhOHAcTyQO7XYhb37n6sdmKmmFz6G20gpg20S2NXauVDfAg0Qbmv5lrd5gRny+QCRqWxAg61KXZT9ymhgZGoY66XryF4aa98Xd1BXpenHlTYNDZTzQGsxWq2UOUutMllDAr1OpXsrlDEaXRMDPZdJyCFptqhEtLzSR6hrLy9BEogjLjc6+cc6SSFB5t7KBwbbYjtIpdHtHwZme+e7GN/zZ2TMMO/hT9aKtppc5ePvenbbKr7HTKH4BS3VH9L6x5+ypez9ee9Q1bLIe5s9yY6Oj1RXfV/70lVA8kcvmHY4jKekmpexzys1PFpq+LWCFIgwHYtS2zlvlt6pqy3mrkM3uTKdTMpms6udqUn2d7dCCoqjT5c7Hg7tDVRD5y7hzn2gGODiq8/ucy+X2960I4gU6GAwmk8kLZzabJYo/mQwEAkA4LRqLCvornSrCoFAwnLCL5oIRzQEnrtr5TwmBnX8v/DOn+FrA/wYgwKn24/zCtQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/98ad2f0b3adfe262b97b3be5b8c3f94b/263a4/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.webp 480w,
/static/98ad2f0b3adfe262b97b3be5b8c3f94b/a6361/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.webp 960w,
/static/98ad2f0b3adfe262b97b3be5b8c3f94b/d71bc/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/98ad2f0b3adfe262b97b3be5b8c3f94b/9aebd/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.png 480w,
/static/98ad2f0b3adfe262b97b3be5b8c3f94b/a91f8/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.png 960w,
/static/98ad2f0b3adfe262b97b3be5b8c3f94b/42a19/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/98ad2f0b3adfe262b97b3be5b8c3f94b/42a19/Gemini_Generated_Image_5ay0lx5ay0lx5ay0.png&quot; alt=&quot;토큰 갱신 요청의 시간별 집중 현상&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 4. Access Token 만료 시점에 토큰 갱신 요청이 집중되는 문제&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;Access Token의 만료 시간은 &lt;strong&gt;5분으로 고정&lt;/strong&gt;되어 있기 때문에, 갱신 요청이 5분 간격으로 스파이크를 만들었습니다. 이 문제를 해결하기 위해 Jitter(무작위 지연)를 적용했습니다. 토큰 만료 시간에 &lt;strong&gt;±30초의 랜덤 값&lt;/strong&gt;을 추가하는 방식입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;핵심 아이디어는 간단합니다. 각 토큰마다 &lt;strong&gt;독립적인 랜덤 만료 시간&lt;/strong&gt;을 부여하되, 설정을 통해 활성화/비활성화할 수 있게 했습니다. 사용자 경험에는 영향이 없습니다. ±30초는 사용자가 인지할 수 없는 수준이기 때문입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그렇다면 Jitter 적용의 효과는 어땠을까요? 아래 그래프에서 확인할 수 있습니다. 막대 그래프는 Access Token이 고정 5분일 때 Refresh API에 가해지는 부하이고, 선 그래프는 Jitter(±30초)가 적용되었을 때의 부하입니다.&lt;/p&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1394px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/36e37c4fa6a5e96b886b95c8536b6abc/02f69/jitter_tps.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACH0lEQVR42oWSXU8TQRiF+TtQ2v2Ymd22222r3Xa/WopfVUDAXggE4r2oNDT6U0wUo5Bw5b87npk1bTcpevFmZt6cffaceWdDSolGowG9CiFMuaxBnCCOY4yGQwzzHEmSmMqyDL1er9BKxbX4TrJs28GGUgppmiIIAjiOQxEFLtfaFkXLn6yt6haEY5eB2pl2EoYhHNctBGEEOb0uwA/BqJNvZhDdBMKumYTrgVYVItqF+vybH1kPAKXRyet7iPQl01QIVMvI+m4WQJvA3gjq089/A7Wry28Q8XMCt5dAl5Bk1aEB7kBd3f0f+OEHgS/KDjVID2UJrDEyHc5vFhe+PjKBV18hkmdlh51OpxxZ32E2hvryC8Ln2V0D1D1lQc6/Q4x5h9ZqZAr02zJAhwBBh/kTqI+3EIEsntE6lw2Hd8jI41c8V80TW0x54VADfcaMxwWwznNdlV3qvc+eT+B7Oswm3FtloHk2rVYRuen8nTKBgvCWT4i7AuQ+ZE8QcnlTDEUDPUa2CNSifr+PsNmErR05FbjdHHJ2B7daMS5dj2VZRXkO3AbB25ucMoHRU/Y3+WMFq1orgJ12G918CBX14TdD+KMD1Of38Fod+GEPXjyAH0Wos8y+zV5A3ewW3u4UXj2A1x/QyGNsWJZtnkE8OUZ2cMY6RcrKDs+R7L1Fun+C/PUZor0pHk0OzT7dPzW60fE7DKkbHZ1j5+iC+hP8AXwf4gNkTV/SAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/36e37c4fa6a5e96b886b95c8536b6abc/263a4/jitter_tps.webp 480w,
/static/36e37c4fa6a5e96b886b95c8536b6abc/a6361/jitter_tps.webp 960w,
/static/36e37c4fa6a5e96b886b95c8536b6abc/2e4e1/jitter_tps.webp 1394w&quot; sizes=&quot;(max-width: 1394px) 100vw, 1394px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/36e37c4fa6a5e96b886b95c8536b6abc/9aebd/jitter_tps.png 480w,
/static/36e37c4fa6a5e96b886b95c8536b6abc/a91f8/jitter_tps.png 960w,
/static/36e37c4fa6a5e96b886b95c8536b6abc/02f69/jitter_tps.png 1394w&quot; sizes=&quot;(max-width: 1394px) 100vw, 1394px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/36e37c4fa6a5e96b886b95c8536b6abc/02f69/jitter_tps.png&quot; alt=&quot;Jitter 적용 전후 TPS 비교&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 5. Jitter 도입으로 Peak TPS가 40% 감소한 모습&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;결과를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;지표&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Jitter 없음&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Jitter 적용&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;개선&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Peak TPS&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;5,000&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;3,000&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;strong&gt;-40%&lt;/strong&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;평균 TPS&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1,000&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1,000&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;동일&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;요청 분산&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;5분 간격 집중&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1분 간격 분산&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;시스템 안정성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;주기적 부하&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;평준화된 부하&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✓&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Jitter 도입의 가치는 분명했습니다. 순간 최대 부하가 5,000 TPS에서 3,000 TPS로 40% 감소했고, 급격한 스파이크가 제거되어 부하가 평준화되었습니다. Auto Scaling이 대응할 시간을 확보함으로써 시스템 안정성이 크게 향상되었으며, ±30초의 차이는 사용자가 전혀 인지할 수 없는 수준이어서 사용자 경험에는 영향을 주지 않았습니다. 이러한 Jitter 적용을 통해 세일 기간과 같은 고부하 상황에서도 안정적인 서비스를 제공할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;25-resilience4j-장애-격리-circuit-breaker&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#25-resilience4j-%EC%9E%A5%EC%95%A0-%EA%B2%A9%EB%A6%AC-circuit-breaker&quot; aria-label=&quot;25 resilience4j 장애 격리 circuit breaker permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.5 Resilience4j 장애 격리 (Circuit Breaker)&lt;/h3&gt;
&lt;p&gt;OAuth2 전환 과정에서 또 하나의 중요한 고려사항이 있었습니다. 부하 최적화와 함께, 외부 서비스 장애에 대한 대비도 필요했던 거죠. &lt;strong&gt;실제로 Authorization Server에 장애가 발생하면 어떻게 될까요?&lt;/strong&gt; 토큰 발급과 갱신이 모두 외부 서버에 의존하는 구조에서, 해당 서버의 장애가 전체 서비스 중단으로 이어질 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;OAuth2 인증 흐름은 로그인할 때 Token 발급을 요청하고, 토큰을 갱신할 때 Refresh를 요청하며, 로그아웃 시 Token 폐기를 요청하고, 로그인 기기를 조회할 때 Active Device 조회를 요청합니다. 모든 단계에서 Authorization Server를 호출합니다. 만약 이 서버가 느려지거나 응답하지 않으면 어떻게 될까요? 요청이 무한정 블로킹되어 스레드가 고갈되고, 하나의 장애가 전체 시스템으로 전파되며, 결국 로그인이 불가능해져 전체 커머스 기능이 마비됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이 문제를 해결하기 위해 &lt;strong&gt;Resilience4j의 Circuit Breaker를 도입&lt;/strong&gt;했습니다. Circuit Breaker는 전기 회로의 차단기처럼 작동합니다. 정상 상태에서는 모든 요청을 통과시키다가, 실패율이 50%를 초과하면 회로를 열어(OPEN) Authorization Server 호출을 차단하고 즉시 실패를 반환합니다. 30초 대기 후에는 반열림 상태(HALF_OPEN)로 전환되어 일부 요청만 허용해 서버 회복을 확인하고, 성공하면 다시 정상 상태(CLOSED)로 돌아가 정상 운영을 재개합니다.&lt;/p&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c6ba4b5e45638e2186f2765ef2588459/39076/circuit-breaker-diagram.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 143.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAdCAYAAACqhkzFAAAACXBIWXMAAAsTAAALEwEAmpwYAAADTElEQVR42p1V2VIcRxDk/z9HEf4H+cG2rEAICy8gGbHn3NN3z6Qra2ixSAhtsBG11VPdnVNX1pzFGNF3e6Q4wJoK47BHDB0O+zs42wCzQ1Ovcf/1GsF3GPqdnpvyqGe9a+Rcja4VjJRxJjfgzQpD8wds/x7JXWCO/yDaD8j+I/x4ga56h/bwJ9x4rvZoL2TvUnVyl0hiC+4L+DvjH5GtC/KmgK4fVbyPYp8QRaZpFpn0AvU8z0/WlLJ/Vjb4y1mArZHnRQ9Dj67r0RzOxcO3Egz38B3gIrzzLKD3XtfUxZ78BvX+vebaGKvnKCd5+BwgtXPLszUGTdNgs9lgHMcn3h4B5gfA9A3QOavPDCelKJcHqbLXVLAzeIea+4LwY8h8UUpJQnJIeRbtRU9amCzPLJy+9JSiRP8VbriUFvmE6FaY02fkcIMp3kr+bmD6K5Up3KqUvaKncI0ct4+AwW3RVlcYulvpyTt4eycvWWPOOwGXRpeiBHsvl3YCshG9wZS2qgmU41qkeRryxJAlxK6Xdhm95DBKvjz63iKEpL04v6YPS1GYfOecgAWxOenJQbW1Vm0xBi3Wq/pwaXirawLv93us12u0bfuztjkGDIuHphMPgtqW6hvV9K6EekLIvJClGHvs/nsDK5VnC9FO4OfydnIOzdhrc2setaGtNjK9fFVRCFS8oibNimbuqqp6iXo/LwovlKIol0X7B4+Zz18CjqSe0G00DHGhHoXzUXs1ndCHKdRw5l4G7BpmIFM2yp45V8ipkvVO8iisybUwpFJ5XNfCpoOc64+47D7LAP0LY8ex/69wcyUUI6dvhMsrsX/E0H6QcX+l+1lseubh7OSvlH5HnwCOesmbtEzbWfSD5MgtLeT9Mm1e/QlgqxTqGTNI+4yotm+x/fIG89RqHpcZOCvdFupxgge1vcBlq+yw0j5NtZJpdC6A5Hb8wdvTGtsYFfYjU5HSrLTsuk7PFBpSD+0nHDa/a4Fe9PCRblmnCz3hPu1cF9Z09d+4u/5N5ub9IyAPkVqcJmRAAdRZeSSFQeXOMkvZ/A8h00AACsEKEJ/pQWEN93i2hFm85ZpzsXzPz3iZuSrhlpCYK3pcwq/r+hsFC+XoLc/wmRh84f8Ep9wtFckEaQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c6ba4b5e45638e2186f2765ef2588459/263a4/circuit-breaker-diagram.webp 480w,
/static/c6ba4b5e45638e2186f2765ef2588459/a6361/circuit-breaker-diagram.webp 960w,
/static/c6ba4b5e45638e2186f2765ef2588459/0b34d/circuit-breaker-diagram.webp 1920w,
/static/c6ba4b5e45638e2186f2765ef2588459/da28f/circuit-breaker-diagram.webp 2880w,
/static/c6ba4b5e45638e2186f2765ef2588459/be3d5/circuit-breaker-diagram.webp 3165w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c6ba4b5e45638e2186f2765ef2588459/9aebd/circuit-breaker-diagram.png 480w,
/static/c6ba4b5e45638e2186f2765ef2588459/a91f8/circuit-breaker-diagram.png 960w,
/static/c6ba4b5e45638e2186f2765ef2588459/ac7a9/circuit-breaker-diagram.png 1920w,
/static/c6ba4b5e45638e2186f2765ef2588459/f9c26/circuit-breaker-diagram.png 2880w,
/static/c6ba4b5e45638e2186f2765ef2588459/39076/circuit-breaker-diagram.png 3165w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c6ba4b5e45638e2186f2765ef2588459/ac7a9/circuit-breaker-diagram.png&quot; alt=&quot;Circuit Breaker 상태 전환 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;그림 6. Circuit Breaker의 3가지 상태(CLOSED, OPEN, HALF_OPEN)와 전환 조건&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;실제 설정 예시를 살펴보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;resilience4j&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;circuitbreaker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;instances&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;OYAuthorizationApi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# 실패율 임계값&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;failureRateThreshold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;# 50% 이상 실패 시 OPEN&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;slowCallRateThreshold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;             &lt;span class=&quot;token comment&quot;&gt;# 50% 이상 느린 호출 시 OPEN&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;slowCallDurationThreshold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3000&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;# 3초 이상을 &quot;느림&quot;으로 간주&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# 측정 윈도우&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;slidingWindowType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; COUNT_BASED        &lt;span class=&quot;token comment&quot;&gt;# 호출 횟수 기반&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;slidingWindowSize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;                &lt;span class=&quot;token comment&quot;&gt;# 최근 100번 호출 기준&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;minimumNumberOfCalls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;# 최소 10번 호출 후 판단&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# 상태 전환&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;waitDurationInOpenState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30000&lt;/span&gt;            &lt;span class=&quot;token comment&quot;&gt;# OPEN 상태 30초 유지&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;permittedNumberOfCallsInHalfOpenState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# HALF_OPEN에서 5번 테스트&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# 자동 상태 전환&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;automaticTransitionFromOpenToHalfOpenEnabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;실제 코드로 구현할 때는 &lt;code class=&quot;language-text&quot;&gt;@CircuitBreaker&lt;/code&gt; 어노테이션과 함께 &lt;code class=&quot;language-text&quot;&gt;@TimeLimiter&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;@Retry&lt;/code&gt;를 조합하여 다층 보호 체계를 구축했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AuthorizationServerClient&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@CircuitBreaker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;authorizationServer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fallbackMethod &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fallbackResponse&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@TimeLimiter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;authorizationServer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Retry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;authorizationServer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ResponseEntity&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TokenResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;callAuthorizationServer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TokenRequest&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Authorization Server 호출 (Circuit Breaker 보호)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; restTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postForEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            authServerUrl &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/oauth2/token&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token class-name&quot;&gt;TokenResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Circuit Breaker OPEN 시 또는 예외 발생 시 실행&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ResponseEntity&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TokenResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fallbackResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token class-name&quot;&gt;TokenRequest&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

        log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Circuit breaker activated: {}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; exception&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// Fallback 전략에 따라 처리&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;someFallbackMethod&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이 코드가 실제로 어떻게 동작하는지 살펴보겠습니다. 정상 상황에서는 Authorization Server를 정상적으로 호출합니다. 그러다가 최근 100번의 호출 중 50번 이상이 실패하면 Circuit이 OPEN 상태로 전환되고, 이후 들어오는 모든 요청은 Authorization Server를 호출하지 않고 즉시 fallback 메서드로 처리됩니다. 30초가 지나면 자동으로 일부 요청만 허용해 서버 상태를 확인하고, 정상이면 다시 모든 트래픽을 통과시킵니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;하지만 Circuit Breaker만으로는 충분하지 않았습니다. &lt;strong&gt;Timeout&lt;/strong&gt;과 &lt;strong&gt;Retry&lt;/strong&gt;를 함께 적용해야 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;resilience4j&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;timelimiter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;instances&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;OYAuthorizationApi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;timeoutDuration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 3s        &lt;span class=&quot;token comment&quot;&gt;# 3초 타임아웃&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;retry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;instances&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;OYAuthorizationApi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;maxAttempts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;             &lt;span class=&quot;token comment&quot;&gt;# 최대 2회 시도 (원본 1회 + 재시도 1회)&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;waitDuration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;# 재시도 간격 100ms&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;retryExceptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; java.net.SocketTimeoutException
          &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; java.net.ConnectException&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이 세 가지 보호 장치가 어떻게 협력하는지 살펴보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Request
  ↓
[Timeout: 3초] ─────→ 3초 초과 시 실패
  ↓
[Retry: 최대 2회] ───→ 네트워크 오류 시 재시도
  ↓
[Circuit Breaker] ───→ 반복 실패 시 차단
  ↓
Authorization Server&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;요청이 들어오면 먼저 Timeout이 3초 제한을 걸고, 네트워크 오류가 발생하면 Retry가 최대 2회까지 재시도하며, 그래도 계속 실패하면 Circuit Breaker가 회로를 차단합니다. 이렇게 3단계 보호 체계를 구축함으로써 단일 실패 지점을 제거하고 시스템 안정성을 확보했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이 시스템이 어떻게 작동했는지는 2025년 9월에 발생한 실제 사례를 통해 확인할 수 있었습니다. 당시 Authorization Server의 응답 시간이 정상 상태인 3초에서 10초 이상으로 급증하는 장애가 발생했습니다. Circuit Breaker는 실패율이 50%에 도달하자 즉시 OPEN 상태로 전환되었고, 이후 들어오는 모든 요청은 Authorization Server를 호출하지 않고 Fallback 전략으로 처리되었습니다. Fallback 전략은 Graceful Degradation 모드로 동작해 기존 세션 인증 방식으로 자동 대체되었고, 사용자는 서비스 중단 없이 계속 이용할 수 있었습니다. 30초 후 Circuit Breaker는 HALF_OPEN 상태로 전환되어 서버 정상화를 확인했고, 정상 응답을 받자 다시 CLOSED 상태로 돌아가 정상 운영을 재개했습니다. 전체 영향은 약 2분간 일부 요청이 Fallback으로 처리된 것뿐이었고, 실제 서비스 중단은 없었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이 사례가 보여주는 핵심 가치는 명확합니다. 외부 서버 장애가 전체 서비스로 전파되지 않았고, 자동 복구 덕분에 수동 개입이 필요하지 않았으며, 사용자는 어떤 영향도 느끼지 못했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;결국 Resilience4j를 도입한 &lt;strong&gt;가장 큰 성과는 장애 격리&lt;/strong&gt;였습니다. Authorization Server에 문제가 생겨도 전체 서비스로 확산되지 않았습니다. &lt;strong&gt;빠른 실패 전략&lt;/strong&gt;으로 타임아웃을 기다리지 않고 즉시 fallback으로 처리했고, &lt;strong&gt;자동 회복 메커니즘&lt;/strong&gt; 덕분에 서버가 정상화되면 자동으로 트래픽을 재개했습니다. 무엇보다 &lt;strong&gt;스레드 보호&lt;/strong&gt;가 중요했습니다. 블로킹으로 인한 스레드 고갈을 방지함으로써 시스템 전체가 마비되는 최악의 상황을 막았습니다. 그리고 &lt;strong&gt;실시간 모니터링&lt;/strong&gt;을 통해 장애 상황을 즉시 감지하고 대응할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;3-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;3 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 마치며&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;6개월간의 OAuth2 전환 프로젝트를 되돌아보면, 성공의 핵심은 완벽한 기술이 아니라 안전한 접근 방식이었습니다. Feature Flag로 언제든 되돌릴 수 있는 구조를 만들고, 10%부터 시작해 점진적으로 확대하며, Circuit Breaker로 장애를 격리했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;가장 중요했던 것은 &lt;strong&gt;완벽보다 안정성&lt;/strong&gt;을 우선했다는 점입니다. 이론적으로 괜찮을 것이라는 가정이 아닌, 실제 트래픽으로 검증하고 작은 실패를 빠르게 경험하며 큰 실패를 예방했습니다. Authorization Server는 언제든 장애날 수 있다는 전제로 모든 안전장치를 설계했고, 그 결과 올영세일이라는 가장 중요한 순간에도 안전하게 전환을 완료할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;레거시 시스템 전환을 고민 중이라면, 안전하게 되돌릴 수 있는 구조를 먼저 만들고, 테스트 환경이 아닌 실제 사용자로 검증하며, 장애를 가정한 설계로 시작하시길 권장합니다. 올리브영의 인증 체계는 이제 OAuth2를 기반으로 OIDC 도입을 준비하며 계속 진화하고 있습니다. 이 글이 같은 고민을 하는 개발자분들께 도움이 되길 바라며 글을 마치겠습니다. 감사합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;참고자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;참고자료&lt;/h2&gt;
&lt;h3 id=&quot;oauth2-및-인증-표준&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#oauth2-%EB%B0%8F-%EC%9D%B8%EC%A6%9D-%ED%91%9C%EC%A4%80&quot; aria-label=&quot;oauth2 및 인증 표준 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OAuth2 및 인증 표준&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot;&gt;RFC 6749 - The OAuth 2.0 Authorization Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7009&quot;&gt;RFC 7009 - OAuth 2.0 Token Revocation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/projects/spring-authorization-server&quot;&gt;Spring Authorization Server 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;장애-격리-패턴&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%95%A0-%EA%B2%A9%EB%A6%AC-%ED%8C%A8%ED%84%B4&quot; aria-label=&quot;장애 격리 패턴 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;장애 격리 패턴&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/CircuitBreaker.html&quot;&gt;Circuit Breaker Pattern - Martin Fowler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://resilience4j.readme.io/&quot;&gt;Resilience4j 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[T4 GPU 1장으로 일궈낸 올리브영의 Gemma 3 기반 sLLM 구축기]]></title><description><![CDATA[안녕하세요! 😆 지난 포스팅인 'ItemLM…]]></description><link>https://oliveyoung.tech/2026-01-21/oy_sllm/</link><guid isPermaLink="false">https://oliveyoung.tech/2026-01-21/oy_sllm/</guid><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 😆 지난 포스팅인 &apos;&lt;a href=&quot;https://oliveyoung.tech/2025-06-02/itemlm/&quot;&gt;ItemLM: 상품 정보 기반의 임베딩 생성기&lt;/a&gt;&apos;에 이어 두 번째로 인사드립니다.&lt;br&gt;
저는 올리브영에서 머신러닝으로 다양한 문제를 풀고 있는 🪄 데이터 사이언티스트 입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;지난 글에서 상품 데이터를 효율적으로 다루는 기초 체력을 다뤘다면, 이번에는 최신 오픈소스 모델을 기반으로 상용 모델 대비 95%의 정확도를 유지하면서도 운영 비용은 획기적으로 낮춘, 올리브영의 sLLM(Small LLM) 구축 경험을 공유하려 합니다. 🥂&lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;올리브영의-첫-sllm-학습기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%EC%B2%AB-sllm-%ED%95%99%EC%8A%B5%EA%B8%B0&quot; aria-label=&quot;올리브영의 첫 sllm 학습기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영의 첫 sLLM 학습기&lt;/h2&gt;
&lt;p&gt;그 중에서도 자체 학습시킨 sLLM을 서비스에 도입한 첫 번째 사례, &apos;리뷰 테마 추천&apos; 시스템을 소개합니다.&lt;/p&gt;
&lt;p&gt;아래 이미지와 같이 올리브영 앱을 켜면 보이는 &apos;인기 리뷰 추천 테마&apos; 영역은 단순히 상품을 보여주는 일반적인 추천이 아닙니다. 사용자들이 남긴 방대한 리뷰 데이터를 sLLM이 분석하여, &quot;복합성 피부에 딱 맞는 클렌징 워터&quot;나 &quot;합리적인 가격에 품질 좋은 헤어 트리트먼트&quot;처럼 사용자의 고민과 니즈에 딱 맞는 &apos;추천 테마&apos;를 매력적인 문구와 이미지로 자동 생성해 제안하는 공간입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1446px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/1739d5584d555ef2dafe90c64867fd1a/93a6a/oy_sllm_intro.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 58.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAACJElEQVR42mNgIAD+///P/u7d69jnz583M5ABGIEGgDAziNM9c4v5sROX7jx/8e7/u3dvb4SGhjLDFNbX1zMBbWMkylSggUwguqh5lc2ajYf+Hzpy+v+VK5efXblyRQJuGE7NUMlnmzaZ5vt57axOC91YmRi0vSMpauGqRQcV2/sXPt+1e9//rdt2fl+1aqsuTF9RUpFzWkWj5X90w/+vWgX2xsHJk4PrI/z/75jf82fdlMb/HQnh788u2yU1Ye7Gg4tWbfs/Ydby/81dc11Aaud0dvJ2lzRcn9XR+n/RnOnXm3t6rOAuXwU1cEVXbfD8moL/K/rqf22c3v5/aX35h5lpofz59fPzApM6H/omdv0PSOpMAmusaQ1Z21v9f2ZuzP+i3p43hW0VKkgGQgJ6WmF28Mqmmv9za7J/reiq/L+qvfldVVq0JEhOSyuUTde21MjKr1QdxC/NL62ZWF/+vzg7879fSnYPSAweYTAXTqwsDtm+YPb/2T0NfxbP6v6/YHr/h6qCNEloJKHHJrOxlZdjeFTywqTQaHWUiIIxSkoKrNva6va2ttdvq6kt211dW7o6NNRNCDniHl+8KHNn/aKNT/btXvvs0DlRQjHOAsQ8QMwLxVwgl4B9AfXOnt5GxwvN5f+3piW/ytOylgMbyMDARHJqhxk4OzExb9vEyf/npme86LWwkCaYJnEBmKZyf3+HxpSUlgZf/9x6e3seWO5CVgwA/dEJwgFhyMYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1739d5584d555ef2dafe90c64867fd1a/263a4/oy_sllm_intro.webp 480w,
/static/1739d5584d555ef2dafe90c64867fd1a/a6361/oy_sllm_intro.webp 960w,
/static/1739d5584d555ef2dafe90c64867fd1a/60900/oy_sllm_intro.webp 1446w&quot;
              sizes=&quot;(max-width: 1446px) 100vw, 1446px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1739d5584d555ef2dafe90c64867fd1a/9aebd/oy_sllm_intro.png 480w,
/static/1739d5584d555ef2dafe90c64867fd1a/a91f8/oy_sllm_intro.png 960w,
/static/1739d5584d555ef2dafe90c64867fd1a/93a6a/oy_sllm_intro.png 1446w&quot;
            sizes=&quot;(max-width: 1446px) 100vw, 1446px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1739d5584d555ef2dafe90c64867fd1a/93a6a/oy_sllm_intro.png&quot;
            alt=&quot;intro&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;그림 1. Intro&lt;/figcaption&gt;
&lt;p&gt;실제로 존재하는 생생하고 다양한 상품에 대한 고객의 표현을 녹여 추천 카드로 탄생시킨 건데요, 이 과정은 단순한 모델 개발 그 이상의 도전이었습니다. &quot;고가의 상용 API 없이 Tesla T4 GPU 1장이라는 제한된 환경에서 과연 상용 모델급의 고성능을 낼 수 있는가?&quot;라는 질문에 대한 기술적 해답을 찾아가는 여정이었기 때문입니다. 저사양 GPU 환경이라는 제약을 딛고 상용 모델 못지않은 고품질 sLLM을 완성하기까지 치열하게 고민했던 해결 과정을 &apos;올리브영 리뷰테마 추천 시스템&apos;을 예로 들어 상세히 소개해 보겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1526px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6462f0176729609f7dd4fba9a4e31a9f/2fa46/oy_sllm1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 82.70833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAAD4klEQVR42o2UXUybVRjHix93xAtvvDXReKFXJnphZoIX08R4OYnfiR8zUQJZdlHqdA4pMDIlhonAwBakfExktPI1XEEK7UYZKyKUjrbwlpav8vKu0K+37ftyTv+e8yLZsphsT/LPc/Gc/PKc83+eo9M9IAA8ynM0Gv0knU7vgNKVeDwuhNbWBP/KqiBKkkAJCUq74i47U6pLJpMWWZYt2WzWQumBhQE0UUp/VVW1l6jqtxy4GIkaYqzwz/ou+am5DYuemwj7vei2dKBrcIwIacC5vGXU7cfj2BF3IayFoagqjiKfzxOeKYWTA8e9q+W+uIrGwesHX5bq86O/d+Unrd35ltpz+Tff/ujAtnQHPTOCUTc4L5DzdReJubGe6GsaiHNFIkOuOfJVVZ1y4oPPUNvcOcmBrqWgISTn0dBzlVh7++Gw9cLnuobNhevoqD9PbJ4whn2iUVfTZsWFqhp4p0bxxcnPcck2gY5hB8pPlZJ3ThTjqaef1zp0M+C2ApypM5GW9t8wamqBb/IqPONW6MtKyIBnHXafZNQ19gxhtPcyFlnRabOg58oAzEMOlJ78lDj7OvHsM89pwKVQxJDMs8cdcZLvfjTh75turM9PwdJUh1Nna8lSjGLCu2HU6S+0oL25Fb6JQVz6vhIN5i6MzPrwwouvEMPH7+IHY4UGjES3DPxREwohwpaEYHgTYngZIWaMIMZJgtV8G+zKbdZraLb0YazPjHNnv0b3Xx6MuT14+dhr5MP33ke/1a4B2XgYuEmptEySqTT29vagZGRQNQtVVTQDpf2EUTdgn6DmX0z0YqWe1tecoea2dtpkbqPHX39DLS8pwbGXXp3iwOBq0DB76xZcLhcJhULY3t4GYSPAI5fLacBEggFv3JiGe3wEzj+HMNJ/GTOOcfTbbKiuriV9XZ2oqq497FCMGthcQpIkwgAaiBACRVHAZvgQmEoZ+Sb0/acrOUW1szzENMw0yDTOVMeBm5uRbwTW2fzSbcK7C/gDHA62NVx3gfesWEFZWdkTxUVFhcXFxYVFLHOdPl3xZEXFz4WiKFbtMYAQidBkOoVsUkaOdXfvEiQShx0WHOn/dtnhcDzGczq2X75BsrgdXidKdB+pbAYZWUYmk0EyldKAqZRsvP8jKLhfHo/ncV7LxOKGPyJBNHmmibx9B+GgH/5AQHObmXHXlAfFUYexmKR3r0ZgX/YraXGH0HCEZLMqB3Fpd99PJCp1D/F9HQKTOTaHUTZ7EqEkr7nLXeeZ6fDKsmx8GOAjPAcCkePuGXvXjGu6dXkhYJpbXDDNzc+ZZmdnTV6ftzUqRruFcPitfwFfHY1e6nl2QgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6462f0176729609f7dd4fba9a4e31a9f/263a4/oy_sllm1.webp 480w,
/static/6462f0176729609f7dd4fba9a4e31a9f/a6361/oy_sllm1.webp 960w,
/static/6462f0176729609f7dd4fba9a4e31a9f/88408/oy_sllm1.webp 1526w&quot; sizes=&quot;(max-width: 1526px) 100vw, 1526px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6462f0176729609f7dd4fba9a4e31a9f/9aebd/oy_sllm1.png 480w,
/static/6462f0176729609f7dd4fba9a4e31a9f/a91f8/oy_sllm1.png 960w,
/static/6462f0176729609f7dd4fba9a4e31a9f/2fa46/oy_sllm1.png 1526w&quot; sizes=&quot;(max-width: 1526px) 100vw, 1526px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6462f0176729609f7dd4fba9a4e31a9f/2fa46/oy_sllm1.png&quot; alt=&quot;리뷰 테마 추천 화면 UX&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;그림 2. 리뷰 테마 추천 화면 UX&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h2 id=&quot;리뷰테마-추천-시스템의-컴포넌트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A6%AC%EB%B7%B0%ED%85%8C%EB%A7%88-%EC%B6%94%EC%B2%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8&quot; aria-label=&quot;리뷰테마 추천 시스템의 컴포넌트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;리뷰테마 추천 시스템의 컴포넌트&lt;/h2&gt;
&lt;p&gt;리뷰 테마 추천 시스템은 리뷰 분류부터 표현 추출, 검증, MAB(Multi-Armed Bandit) 기반 전시 최적화까지 다양한 컴포넌트로 연결되어 있습니다. 본문에서는 전체 구조 중 sLLM이 활약하는 영역에만 집중합니다. 특히 사용자 리뷰를 매력적인 추천 문구로 탄생시키는 &apos;표현 생성 모델&apos;의 학습 과정을 중심으로 심층적인 기술 노하우를 공유합니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 1600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/15937b8cb110e63c36a5674fbbdcb97a/8a084/oy_sllm2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.333333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABVElEQVR42iVQTUsDMRDd/3+ziCcPHqxHQTx6EFGE2gpadMWPpa2VWsta2243mclM9jnZBh6TyXsvk7wsf30/dM7tCUuHSDoisQVbv3XcVmbe7SWajvcXi8XBaDLZs9pyEtGJBgCdDI0ixgai0SCITUTTWG8qtbMg2tbY7DSJC6oIQXc6O9OYvIq0spPeUh+LX4WQDqdOj65/dDT7082W9HxY61l/rkxeB5+ip/1S11WtREF7D7n28ye1gVqMp3rTf1BPpFn3aozBUwFEQVFGdC8LjKff8CHi8mWDs6tn/K3W6I0Ixxc5vmZzsL36pnePu7cBQlR8jKa4HQzhPSHzdYWtI1S1hwrDWU8csFxv7bvWe8ZqU2O5LFGWJVbrxEtrJh9QObbq4Z2zaBQZByOJzSwt0mXEDA4BnIxENkjwOf9FPp5hVdVtrkDTZpdyjClD06QM/wH9SsA4NPaANAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/15937b8cb110e63c36a5674fbbdcb97a/263a4/oy_sllm2.webp 480w,
/static/15937b8cb110e63c36a5674fbbdcb97a/a6361/oy_sllm2.webp 960w,
/static/15937b8cb110e63c36a5674fbbdcb97a/0b34d/oy_sllm2.webp 1920w,
/static/15937b8cb110e63c36a5674fbbdcb97a/da28f/oy_sllm2.webp 2880w,
/static/15937b8cb110e63c36a5674fbbdcb97a/6f814/oy_sllm2.webp 3816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/15937b8cb110e63c36a5674fbbdcb97a/9aebd/oy_sllm2.png 480w,
/static/15937b8cb110e63c36a5674fbbdcb97a/a91f8/oy_sllm2.png 960w,
/static/15937b8cb110e63c36a5674fbbdcb97a/ac7a9/oy_sllm2.png 1920w,
/static/15937b8cb110e63c36a5674fbbdcb97a/f9c26/oy_sllm2.png 2880w,
/static/15937b8cb110e63c36a5674fbbdcb97a/8a084/oy_sllm2.png 3816w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/15937b8cb110e63c36a5674fbbdcb97a/ac7a9/oy_sllm2.png&quot; alt=&quot;리뷰 테마 추천을 위한 모델 추론 플로우&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;그림 3. 리뷰 테마 추천을 위한 모델 추론 플로우&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h2 id=&quot;상용-모델-대신-자체-sllm을-선택한-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%EC%9A%A9-%EB%AA%A8%EB%8D%B8-%EB%8C%80%EC%8B%A0-%EC%9E%90%EC%B2%B4-sllm%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;상용 모델 대신 자체 sllm을 선택한 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상용 모델 대신 자체 sLLM을 선택한 이유&lt;/h2&gt;
&lt;p&gt;이 프로젝트는 &quot;사용자들의 생생한 리뷰로부터 최적의 추천 문구를 자동으로 생성하자&quot;라는 비즈니스 요구에서 출발했습니다.&lt;/p&gt;
&lt;p&gt;추천 문구는 &quot;합리적인 가격에 품질 좋은 헤어 트리트먼트&quot;처럼 자연스러우면서도 다양한 조합이 필요했기 때문에, 고정된 패턴만 다루는 규칙 기반(Rule-based) 시스템으로는 품질과 확장성 측면에서 명확한 한계가 있었습니다.&lt;/p&gt;
&lt;p&gt;이에 따라 자연어 생성이 가능한 LLM을 사용하기로 결정했고, 특히 상용 LLM API를 직접 호출하는 방식과 오픈소스 sLLM을 자체 학습(SFT)하는 방식을 면밀히 비교 검토했습니다. 최종적으로는 sLLM SFT 방식을 선택했으며, 결정적 이유는 다음과 같습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;재현성과 버전 통제&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;상용 LLM API는 제공사의 모델 업데이트에 따라 동일한 프롬프트라도 응답 품질이나 스타일이 예측 불가능하게 변할 수 있습니다. 리뷰테마 추천처럼 안정적인 품질의 결과를 지속적으로 제공해야 하는 서비스에서는, 학습 데이터, 코드, 체크포인트를 직접 관리하고 언제든 동일한 결과를 재현할 수 있는 구조가 필수적이었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 엔지니어링의 한계 극복&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;상용 모델을 운영 수준의 품질로 사용하려면, 프롬프트에 조건·예시·제약을 반복해서 추가해야 합니다. 그러나 프롬프트가 길어질수록 토큰 비용이 급증하고 응답 시간이 늘어납니다. 또한 모델이 모든 지시사항을 완벽히 따르기 어려워집니다. 반면 SFT를 통해 지시사항과 출력 형식을 모델에 직접 학습시키면, 훨씬 짧은 프롬프트만으로도 안정적이고 정확한 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;운영 비용의 예측 가능성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;API 방식은 호출량에 비례해 비용이 급증하는 구조이기 때문에, 트래픽이 급증하는 상황에서 비용 통제가 어렵습니다. 반면 자체 GPU 환경에서 운영하는 sLLM은 시간당 고정된 서버 비용 내에서 명확히 예측·통제 가능하며, 서비스 규모가 커져도 비용 상한선을 관리하기 용이합니다.&lt;/p&gt;
&lt;h3 id=&quot;비용과-품질을-동시에-잡는-자체-sllm의-이점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EC%9A%A9%EA%B3%BC-%ED%92%88%EC%A7%88%EC%9D%84-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%9E%A1%EB%8A%94-%EC%9E%90%EC%B2%B4-sllm%EC%9D%98-%EC%9D%B4%EC%A0%90&quot; aria-label=&quot;비용과 품질을 동시에 잡는 자체 sllm의 이점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;비용과 품질을 동시에 잡는 자체 sLLM의 이점&lt;/h3&gt;
&lt;p&gt;초기에는 상용 LLM API를 그대로 사용하는 방식과 오픈소스 sLLM을 직접 SFT하는 방식을 모두 검토했습니다.
결과적으로 아래와 같은 이유로 소형 오픈소스 LLM을 직접 학습(sLLM SFT) 하는 방향을 선택했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SFT (Supervised Fine-Tuning)&lt;/strong&gt; &lt;br&gt;
기초 교육을 마친 신입사원(Base Model)에게 올리브영만의 업무 매뉴얼(데이터)을 집중적으로 가르쳐 우리 부서에 딱 맞는 인재로 만드는 과정입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;figcaption style=&quot;margin-bottom:0px;&quot;&gt;표 1. 상용 LLM API와 오픈소스 sLLM SFT 방식 비교&lt;/figcaption&gt;
&lt;div style=&quot;font-family: &apos;Pretendard&apos;, -apple-system, BlinkMacSystemFont, &apos;Malgun Gothic&apos;, &apos;Segoe UI&apos;, sans-serif; border-radius: 12px; overflow: hidden; border: 1px solid #e0e0e0; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 900px; margin: 0 auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; table-layout: fixed;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color: #E4F5FF;&quot;&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6); width: 18%;&quot;&gt;
          구분
        &lt;/th&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6);&quot;&gt;
          상용 LLM API
        &lt;/th&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center;&quot;&gt;
          오픈소스 sLLM SFT
        &lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          성능&lt;br&gt;&lt;span style=&quot;font-size: 12px; color: #888; font-weight: normal;&quot;&gt;(파라미터 규모)&lt;/span&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          20B 이상 대형 모델 중심,&lt;br&gt;전반적으로 높은 성능
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          9B 이하 소형 모델 중심,&lt;br&gt;절대 성능은 상대적으로 낮음
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          재현성
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          동일 프롬프트라도 응답 변화 가능,&lt;br&gt;내부 업데이트에 영향 받음
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          학습·체크포인트 고정으로&lt;br&gt;재현성 확보 가능
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          프롬프트&lt;br&gt;복잡성
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          운영 수준의 완성도를 위해&lt;br&gt;매우 긴 프롬프트·다단계 호출 필요
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          원하는 형태의 Ground Truth를 학습해&lt;br&gt;프롬프트를 단순화 가능
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          버전 통제
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          모델 버전·동작 변경을&lt;br&gt;제어하기 어려움
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          모델·데이터·코드를 모두&lt;br&gt;내부에서 버전 관리 가능
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          속도
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          클라우드 인프라 기반으로&lt;br&gt;대체로 빠름
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          디바이스 성능에 의존.&lt;br&gt;단, 적절한 서버/GPU 구성 시 충분히 빠름
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          비용
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          호출량 증가 시 비용 급증
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; line-height: 1.5; font-size: 14px; vertical-align: middle;&quot;&gt;
          자체 서버 기준,&lt;br&gt;시간당 수 USD 수준으로 예측 가능·통제 가능
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;이 중 가장 중요한 의사결정 기준은 “재현성”과 “버전 통제 가능성”이었습니다.&lt;/p&gt;
&lt;p&gt;리뷰테마 추천 시스템은 항상 일정한 품질의 표현을 안정적으로 제공해야 하기 때문에, &lt;strong&gt;언제든 다시 학습/재현할 수 있고 모델, 데이터, 코드를 함께 버전 관리할 수 있는 구조&lt;/strong&gt;가 필요했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;프롬프트-엔지니어링의-한계-복잡도와-재현성-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81%EC%9D%98-%ED%95%9C%EA%B3%84-%EB%B3%B5%EC%9E%A1%EB%8F%84%EC%99%80-%EC%9E%AC%ED%98%84%EC%84%B1-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;프롬프트 엔지니어링의 한계 복잡도와 재현성 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;프롬프트 엔지니어링의 한계: 복잡도와 재현성 문제&lt;/h3&gt;
&lt;p&gt;상용 LLM을 사용할 때는, 원하는 품질을 얻기 위해 &lt;strong&gt;프롬프트에 조건·예시·제약을 계속 추가&lt;/strong&gt;하게 됩니다. 이 과정이 반복되면, 결국 다음과 같은 문제가 생깁니다.&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;프롬프트가 지나치게 길어져 토큰 비용과 응답 지연이 커짐&lt;/li&gt;
&lt;li&gt;여러 단계의 호출을 조합하면서 사실상 if 문이 많은 프로그램을 짜는 것과 비슷한 복잡도로 변함&lt;/li&gt;
&lt;li&gt;프롬프트가 길고 복잡할수록 작은 변경에도 동작이 예측하기 어려워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;아무리 정교하게 프롬프트를 다듬어도 Task의 복잡도가 임계치를 넘으면 LLM이 모든 지시사항을 완벽히 이행하지 못합니다. 일관된 품질 유지에 명확한 한계가 존재하는 것입니다.&lt;/p&gt;
&lt;p&gt;반면 sLLM을 직접 SFT하는 방식은 우리가 원하는 출력 형식과 품질 기준을 데이터에 직접 내재화할 수 있습니다. 덕분에 프롬프트 구성을 단순하게 유지하면서도 결과의 재현성과 버전 통제권을 완벽히 확보하게 됩니다. 결과적으로 리뷰 테마 생성과 같은 도메인 특화 Task에서 sLLM이 상용 모델보다 더 적합한 선택지라고 판단했습니다.&lt;/p&gt;
&lt;h3 id=&quot;최적의-파운데이션-모델-찾기-gemma-3와-후보군-비교&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%A0%81%EC%9D%98-%ED%8C%8C%EC%9A%B4%EB%8D%B0%EC%9D%B4%EC%85%98-%EB%AA%A8%EB%8D%B8-%EC%B0%BE%EA%B8%B0-gemma-3%EC%99%80-%ED%9B%84%EB%B3%B4%EA%B5%B0-%EB%B9%84%EA%B5%90&quot; aria-label=&quot;최적의 파운데이션 모델 찾기 gemma 3와 후보군 비교 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;최적의 파운데이션 모델 찾기: Gemma 3와 후보군 비교&lt;/h3&gt;
&lt;p&gt;프로젝트 초기에는 Gemma2-2B을 기반으로 시작했습니다.
이후 개발 기간 중 Gemma3가 출시되면서 성능과 다국어 지원 면에서 더 적합하다고 판단해 Gemma3-4B로 변경했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;후보 모델군 검토 및 한국어 벤치마크 비교&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아래 한국어 벤치마크 성능 비교표와 같이 네이버의 HyperCLOVA X SEED, Qwen 시리즈 등 한국어 성능이 준수한 여러 오픈소스 모델을 함께 검토했습니다. 하지만 서비스 적합성을 고려하여 최종 후보군에서는 제외했는데요, 사유는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HyperCLOVA X SEED 3B&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;한국어 성능은 가장 우수했으나, MAU 기반 라이선스 제약으로 인해 서비스 적용이 어렵다고 판단.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Qwen 2.5/3 시리즈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;벤치마크 성능은 우수했으나, 실제 테스트 시 한국어 오타 정정(Typo Robustness) 성능이 상대적으로 낮아 리뷰 기반 생성 Task에는 부적합하다고 판단.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;figcaption style=&quot;margin-bottom:0px;&quot;&gt;표 2. 후보 모델별 한국어 벤치마크 성능 비교 (Zero-shot 기준)&lt;/figcaption&gt;
&lt;div style=&quot;font-family: &apos;Pretendard&apos;, -apple-system, BlinkMacSystemFont, &apos;Malgun Gothic&apos;, &apos;Segoe UI&apos;, sans-serif; border-radius: 12px; overflow: hidden; border: 1px solid #e0e0e0; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 1000px; margin: 0 auto;&quot;&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; table-layout: fixed;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color: #E4F5FF;&quot;&gt;
        &lt;th style=&quot;padding: 12px 16px; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6); width: 35%;&quot;&gt;
          모델
        &lt;/th&gt;
        &lt;th style=&quot;padding: 12px 8px; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6);&quot;&gt;
          파라미터
        &lt;/th&gt;
        &lt;th style=&quot;padding: 12px 8px; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6);&quot;&gt;
          KMMLU&lt;sup style=&quot;font-size: 11px;&quot;&gt;1&lt;/sup&gt;
        &lt;/th&gt;
        &lt;th style=&quot;padding: 12px 8px; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6);&quot;&gt;
          HAE-RAE&lt;sup style=&quot;font-size: 11px;&quot;&gt;2&lt;/sup&gt;
        &lt;/th&gt;
        &lt;th style=&quot;padding: 12px 8px; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6);&quot;&gt;
          CLiCK&lt;sup style=&quot;font-size: 11px;&quot;&gt;3&lt;/sup&gt;
        &lt;/th&gt;
        &lt;th style=&quot;padding: 12px 8px; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center;&quot;&gt;
          KoBEST&lt;sup style=&quot;font-size: 11px;&quot;&gt;4&lt;/sup&gt;
        &lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: left; background-color: #fafafa; vertical-align: middle; word-break: break-all;&quot;&gt;
          HyperCLOVAX-SEED-Vision-Instruct-3B
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          3B
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.4422
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.6499
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.5599
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.7180
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: left; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          Qwen2.5-3B-Instruct
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          3B
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.4451
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.6031
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.5649
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.7053
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr style=&quot;background-color: #FFF8E1;&quot;&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #333; font-weight: 700; text-align: left; background-color: #FFF8E1; vertical-align: middle;&quot;&gt;
          gemma-3-4b-it &lt;span style=&quot;color: #0056b3; font-size: 13px; font-weight: 600;&quot;&gt; (최종 선택)&lt;/span&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; font-size: 14px; text-align: center; background-color: #FFF8E1; vertical-align: middle;&quot;&gt;
          4B
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; font-size: 14px; text-align: center; background-color: #FFF8E1; vertical-align: middle;&quot;&gt;
          0.3895
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; font-size: 14px; text-align: center; background-color: #FFF8E1; vertical-align: middle;&quot;&gt;
          0.6059
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; font-size: 14px; text-align: center; background-color: #FFF8E1; vertical-align: middle;&quot;&gt;
          0.5303
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #0056b3; font-weight: 700; font-size: 14px; text-align: center; background-color: #FFF8E1; vertical-align: middle;&quot;&gt;
          0.7262
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;div style=&quot;max-width: 1000px; margin: 16px auto 0; padding-top: 10px; font-size: 12px; color: #888; line-height: 1.5;&quot;&gt;
  &lt;div style=&quot;display: inline-block; border-top: 1px solid #e8e8e8; padding-top: 8px;&quot;&gt;
    &lt;sup&gt;1&lt;/sup&gt; KMMLU: 한국어 일반 상식 지식, &amp;nbsp; &lt;sup&gt;2&lt;/sup&gt; HAE-RAE: 한국 문화·역사 지식, &amp;nbsp; &lt;sup&gt;3&lt;/sup&gt; CLiCK: 언어 이해 및 문화 상식, &amp;nbsp; &lt;sup&gt;4&lt;/sup&gt; KoBEST: 추론 및 이해 성능
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;실무 적용을 위한 두 가지 핵심 허들&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;컴퓨팅 효율성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;개발 및 운영 환경이 Tesla T4 16GB 기준이기 때문에, 이 조건에서 안정적으로 동작할 만큼 메모리 사용이 가벼우면서도 충분한 성능을 제공하는 모델이어야 했습니다. Gemma는 효율적인 추론을 위해 설계된 아키텍처적 강점을 가지고 있습니다. Sliding Window Attention과 Local-Global Attention 혼합 구조를 통해 메모리 사용량을 최소화하면서도 긴 컨텍스트를 효과적으로 처리할 수 있으며, Multi-Query Attention(MQA)과 Grouped-Query Attention(GQA) 기법을 적용해 추론 속도를 향상시켰습니다. 이러한 최적화 덕분에 T4와 같은 제한된 GPU 환경에서도 안정적으로 동작하면서 실시간 서비스에 필요한 응답 속도를 확보할 수 있었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;한국어 처리 능력&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;실제 사용자 리뷰는 오타, 비문, 구어체가 많기 때문에 한국어 인식 및 오타 복원 능력을 필수 조건으로 설정했습니다. Gemma 3는 140개 이상의 언어를 지원하는 멀티링구얼 모델로 설계되었으며, Gemini 2.0 계열의 SentencePiece 토크나이저(262k vocab)를 사용해 한국어를 포함한 CJK(중국어·일본어·한국어) 언어의 인코딩 효율이 크게 개선되었습니다. 동일한 한국어 텍스트를 처리할 때 이전 세대 대비 토큰 수가 줄어들어 메모리 사용량과 추론 속도가 향상되었으며, 리뷰 데이터처럼 구어체와 오타가 혼재된 텍스트에서도 안정적인 토큰화 성능을 보여주어 실무 적용에 적합했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;왜 Gemma 3-4B 인가&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Gemma는 경량화된 실용적인 크기에서 효과적인 성능을 제공하도록 설계된 Google의 오픈소스 LLM입니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 250px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 432px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/53e79eb0852debec0c5ca9fe80e5256c/dc3b1/oy_sllm_gemma.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 90.04629629629629%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAsTAAALEwEAmpwYAAAC+ElEQVR42p2U2U9TQRjFi7hEFAUsWEBQBMFUQWppQVQqoGiMxC3F6IMPGhGMEVpQkGhu3JUognu1GgTx4SLQNqaoNWlkiWJMfDEx8Y+5P2doqxhBxUm+3LmznDnn+86MTveHpijMkF81iMEXRC/7QJRu+k1uIiqyWR3D0D/GotCUGJsOaLWLWSefkyL7NweIcwlmnjEKvEPkSVD1C7O7XjNPHSLx74yFRLvK3BoPOQ4vWZdF3AwSp74jQ4bvE3pvkCzPKKZXH0kbZ68SPSWYyU3Kli5W732mOQ73ktfkI722h3j3O851DbErIBkOk+79QJl3mKoXoyyNMA2laZJmvY+xohPrHjdLdeL0BiHvip9A+2vORnIoijTfM0JB3yhJk4OFq5l/icS822QX3CWt2kfM/h70R1Uu1fWy6bqfxquD2h65TgL5BFP/CJn+b9qcSXIZ+rFe1Zbkd5C4SgDq7ERXPKZ1Xzd142d2awsE0w4JOvKVWO97Fku5vxXHLjbanOg3nCax8Dzmooukrm1FMd2hZ/0jNoUEhBS4hAPa3lDTEcDrDrDX/4lM3ygrXw4TP/CZuB+AZSdYbGvEsK4Zi/UCGeZrmsN0g3u5HeycmBLhgPl1A1r9tUHcD95SKcE8HzDK6vcHwj6dKLnMQeq6BpIKLoQssaadUzJkv+gRCVU9dNb2UuES+W1UMTwWcl3hG/Sr2cMMNjQTLyK7sIllkan8dlry2qgsfkjL5ids3yHAarpIOtWnLWn1k6kEmTmFwYmSd7e0AXOJE9M4KwdzzQoxpjaG82+hyLHdAqyqG8sRVbO1DJI89TUUYFvrSS6tI0fEQVsDq9crpBefJNZygxOWdsqNQrbxDoZtney2P+NAfR/JTl9Y8lTNrjC73En2RidpG4+TIQGLz5MjHLDC7GJhbhvLS56wYnP4vv/1obApzLQdwyD7xYeIlXYqOUOuVcFovo5eVDpaAMdYnkaqOq3nLLRYWqq8eQLA/72JP+VIKxUpJPwLo+/WgstYY0MsZgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/53e79eb0852debec0c5ca9fe80e5256c/e3a5e/oy_sllm_gemma.webp 432w&quot; sizes=&quot;(max-width: 432px) 100vw, 432px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/53e79eb0852debec0c5ca9fe80e5256c/dc3b1/oy_sllm_gemma.png 432w&quot; sizes=&quot;(max-width: 432px) 100vw, 432px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/53e79eb0852debec0c5ca9fe80e5256c/dc3b1/oy_sllm_gemma.png&quot; alt=&quot;Google의 오픈소스 LLM Gemma&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;그림 4. Google의 오픈소스 LLM Gemma&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gemma2&lt;/strong&gt;: 2B, 9B, 27B 세 가지 모델로 제공되며, 가장 작은 2B 모델조차 GPT-3.5를 능가하는 성능을 보여주었습니다. (출처: &lt;a href=&quot;https://huggingface.co/spaces/lmarena-ai/chatbot-arena&quot;&gt;Chatbot Arena&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemma3&lt;/strong&gt;: 멀티모달 입력과 더 확장된 다국어 지원을 제공하며, 특히 4B 모델이 Chatbot Arena에서 GPT-4와 유사한 수준의 성능을 기록하여 소형 모델 중 매우 우수한 결과를 보였습니다. (참고: &lt;a href=&quot;https://ai.google.dev/gemma/docs/core/model_card_3?hl=ko&quot;&gt;Gemma 3 Model Card&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;학습-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%99%EC%8A%B5-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;학습 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;학습 방법&lt;/h2&gt;
&lt;h3 id=&quot;학습-목표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%99%EC%8A%B5-%EB%AA%A9%ED%91%9C&quot; aria-label=&quot;학습 목표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;학습 목표&lt;/h3&gt;
&lt;p&gt;리뷰 테마 추천 모델의 핵심 목적은 사용자 리뷰에서 상품 추천 카드에 사용할 &apos;테마 표현&apos;을 안정적으로 추출하는 것입니다. 구체적으로는 입력된 리뷰에서 &lt;code class=&quot;language-text&quot;&gt;entity&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;expression&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;name&lt;/code&gt;을 식별하여 구조화된 JSON 리스트 형태로 출력하도록 학습합니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 900px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/02f9f0b1a40b6660e9b36691c7848daf/3f8e8/oy_sllm3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 22.499999999999996%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA1klEQVR42p2PT0+EMBTE+/0/FgfiQS4SLJeCKOCG3XZdodD/Y9tEYzae/CWT13Qm72WIVofzITgATgjhvPf5/ZdCzCWfc+4Sd76NCuSNPuGbaZpAKcUwDOj7Hl3X5ckYQzz2kxvFiPFyBmUDntkLXk8XvM8ieB9AHh9KLMuCfd/zLIoCZVmiqio0TYO2bVHXNeZ5hlIK1lpIdUAZjc9Vgn/coI3Buu4hHSPG2BxMDWKlPJNiNdyT/MT5esURl67bAXGT+W858bBtEgT/QEqZG/1Gax2i8AXVFYEUcZYIPgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/02f9f0b1a40b6660e9b36691c7848daf/263a4/oy_sllm3.webp 480w,
/static/02f9f0b1a40b6660e9b36691c7848daf/a6361/oy_sllm3.webp 960w,
/static/02f9f0b1a40b6660e9b36691c7848daf/0b34d/oy_sllm3.webp 1920w,
/static/02f9f0b1a40b6660e9b36691c7848daf/4c6e1/oy_sllm3.webp 1976w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/02f9f0b1a40b6660e9b36691c7848daf/9aebd/oy_sllm3.png 480w,
/static/02f9f0b1a40b6660e9b36691c7848daf/a91f8/oy_sllm3.png 960w,
/static/02f9f0b1a40b6660e9b36691c7848daf/ac7a9/oy_sllm3.png 1920w,
/static/02f9f0b1a40b6660e9b36691c7848daf/3f8e8/oy_sllm3.png 1976w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/02f9f0b1a40b6660e9b36691c7848daf/ac7a9/oy_sllm3.png&quot; alt=&quot;표현 생성 모델의 input과 output&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;그림 5. 표현 생성 모델의 input과 output&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;학습-데이터-생성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%99%EC%8A%B5-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1&quot; aria-label=&quot;학습 데이터 생성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;학습 데이터 생성&lt;/h3&gt;
&lt;p&gt;리뷰 기반 추천 표현을 학습시키기 위해서는 대규모의 입력–출력 쌍이 필요합니다.
그러나 이를 사람이 모두 직접 제작하는 것은 현실적으로 시간과 비용 측면에서 매우 비효율적입니다.&lt;/p&gt;
&lt;p&gt;따라서 기본적인 지침과 출력 형식을 정의한 뒤, 오픈소스 LLM으로 생성하고 사람의 검토 및 수정을 거쳐 SFT용 데이터셋을 구축했습니다.
이 방식은 품질을 일정 수준으로 통제하면서도, 필요한 데이터 규모를 빠르게 확보할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;supervised-fine-tuning-전략-post-training&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#supervised-fine-tuning-%EC%A0%84%EB%9E%B5-post-training&quot; aria-label=&quot;supervised fine tuning 전략 post training permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Supervised Fine-Tuning 전략 (Post-Training)&lt;/h3&gt;
&lt;p&gt;오픈소스 LLM을 특정 도메인(Task)으로 적응시키는 가장 일반적인 방법은 Post-training 단계의 첫 과정인 Supervised Fine-tuning(SFT) 입니다.
대부분의 오픈소스 모델은 다음과 같은 형태로 공개됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~pt&lt;/strong&gt; : Pre-training만 완료된 Base 모델&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~it&lt;/strong&gt; : Instruction finetuning까지 완료된 Chat 모델 (SFT 1차 완료 상태)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이번 프로젝트에서는 Gemma 3-4B-IT 모델을 기반으로, 그 위에 올리브영 리뷰 데이터에 특화된 도메인 적응(Domain Adaptation) SFT를 수행했습니다. (아래 표의 D단계)&lt;br&gt;
즉, 기존 Chat 모델 위에 리뷰테마에 적합한 추가 지식·스타일·출력 규칙을 학습시키는 단계입니다.&lt;/p&gt;
&lt;br&gt;
&lt;figcaption style=&quot;margin-bottom:0px;&quot;&gt;표 3. LLM 학습 파이프라인 단계별 설명&lt;/figcaption&gt;
&lt;div style=&quot;font-family: &apos;Pretendard&apos;, -apple-system, BlinkMacSystemFont, &apos;Malgun Gothic&apos;, &apos;Segoe UI&apos;, sans-serif; border-radius: 12px; overflow: hidden; border: 1px solid #e0e0e0; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 900px; margin: 0 auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; table-layout: fixed;&quot;&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 20px 16px; background-color: #E4F5FF; border-bottom: 1px solid #d1e9ff; border-right: 1px solid #d1e9ff; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; vertical-align: middle; width: 25%;&quot;&gt;
          1) Pre-training&lt;br&gt;&lt;span style=&quot;font-size:12px; font-weight:500; opacity:0.8;&quot;&gt;(사전 학습)&lt;/span&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 20px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.6; font-size: 15px; text-align: left; vertical-align: middle;&quot;&gt;
          &lt;div style=&quot;margin-bottom: 4px;&quot;&gt;• &lt;strong&gt;데이터:&lt;/strong&gt; 대규모 웹, 문서, 코드, 멀티모달 데이터&lt;/div&gt;
          &lt;div&gt;• &lt;strong&gt;결과물:&lt;/strong&gt; Base Model (기반 모델)&lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 20px 16px; background-color: #E4F5FF; border-bottom: 1px solid #d1e9ff; border-right: 1px solid #d1e9ff; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; vertical-align: middle;&quot;&gt;
          2) Post-training&lt;br&gt;&lt;span style=&quot;font-size:12px; font-weight:500; opacity:0.8;&quot;&gt;(후학습)&lt;/span&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 20px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.8; font-size: 15px; text-align: left; vertical-align: middle;&quot;&gt;
          &lt;div style=&quot;margin-bottom: 8px;&quot;&gt;(A) &lt;strong&gt;SFT:&lt;/strong&gt; Supervised Fine-tuning (Instruction/Chat)&lt;/div&gt;
          &lt;div style=&quot;margin-bottom: 8px;&quot;&gt;(B) &lt;strong&gt;Preference Optimization:&lt;/strong&gt; RLHF, DPO, ORPO&lt;/div&gt;
          &lt;div style=&quot;margin-bottom: 8px;&quot;&gt;(C) &lt;strong&gt;Safety Tuning:&lt;/strong&gt; 안전 가이드라인 및 거부 응답 강화&lt;/div&gt;
          &lt;div style=&quot;background-color: #F0F9FF; border: 1px solid #B3E0FF; border-radius: 8px; padding: 10px 14px; margin-top: 8px;&quot;&gt;
            &lt;span style=&quot;color: #0056b3; font-weight: 700;&quot;&gt;(D) Domain Adaptation (우리가 수행한 과정)&lt;/span&gt;&lt;br&gt;
            &lt;span style=&quot;font-size: 14px; color: #555;&quot;&gt;   └ 도메인 특화 데이터 기반 SFT 수행&lt;/span&gt;
          &lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 20px 16px; background-color: #E4F5FF; border-right: 1px solid #d1e9ff; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; vertical-align: middle;&quot;&gt;
          3) Deployment&lt;br&gt;&lt;span style=&quot;font-size:12px; font-weight:500; opacity:0.8;&quot;&gt;(서빙 및 배포)&lt;/span&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 20px; border-bottom: none; color: #444; line-height: 1.6; font-size: 15px; text-align: left; vertical-align: middle;&quot;&gt;
          &lt;div style=&quot;margin-bottom: 4px;&quot;&gt;• &lt;strong&gt;Quantization:&lt;/strong&gt; 양자화를 통한 경량화&lt;/div&gt;
          &lt;div&gt;• &lt;strong&gt;Distillation:&lt;/strong&gt; 지식 증류를 통한 모델 압축&lt;/div&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;우리가 베이스 모델(Base Model)로 선택한 Gemma 3-4B-IT는 이미지와 텍스트를 동시에 처리하는 멀티모달 모델입니다. 따라서 내부적으로 이미지 인코더인 비전 타워(Vision Tower)와 전용 프로세서를 포함하고 있습니다. 하지만 이번 프로젝트는 오직 텍스트 데이터만을 활용하므로 제한된 인프라 자원을 효율적으로 쓰기 위해 모델과 전처리 체인을 텍스트 전용으로 경량화하는 과정이 반드시 필요했습니다.&lt;/p&gt;
&lt;p&gt;허깅페이스(Hugging Face)의 가이드에 따르면 멀티모달 기능을 모두 활용할 때는 &lt;code class=&quot;language-text&quot;&gt;Gemma3ForConditionalGeneration&lt;/code&gt; 클래스를 사용해야 합니다. 하지만 우리는 비전 타워 로딩을 과감히 생략하고 일반적인 LLM처럼 활용하기 위해 &lt;code class=&quot;language-text&quot;&gt;Gemma3ForCausalLM&lt;/code&gt; 클래스로 모델을 호출했습니다. 이러한 Text-only 최적화 로드 방식을 통해 불필요한 파라미터를 제거하고 오직 텍스트 학습에만 집중할 수 있는 환경을 구축했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고 &lt;br&gt;
Hugging Face 공식 블로그: &lt;strong&gt;Transformers를 활용한 Gemma 3의 세부 추론 가이드&lt;/strong&gt; &lt;br&gt;
&lt;a href=&quot;https://huggingface.co/blog/gemma3#detailed-inference-with-transformers&quot;&gt;https://huggingface.co/blog/gemma3#detailed-inference-with-transformers&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;학습 시에는 어차피 LoRA를 사용해 전체 파라미터가 아니라 일부만 업데이트하기 때문에, 학습 속도나 GPU 메모리 사용량이 극적으로 줄어들지는 않습니다.
다만 운영 관점에서는 비전 관련 가중치를 제외함으로써 모델 파일 크기와 로드 시 메모리 사용량이 약 15% 정도 감소하는 효과가 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;16gb-vram-환경을-위한-학습-최적화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#16gb-vram-%ED%99%98%EA%B2%BD%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%95%99%EC%8A%B5-%EC%B5%9C%EC%A0%81%ED%99%94&quot; aria-label=&quot;16gb vram 환경을 위한 학습 최적화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;16GB VRAM 환경을 위한 학습 최적화&lt;/h3&gt;
&lt;p&gt;16GB GPU(Nvidia Tesla T4) 작은 VRAM 환경에서는 파라미터 규모가 4B 수준인 sLLM이라도 학습 시에 VRAM이 부족하기에 메모리 최적화가 필수적입니다. 그래서 다음과 같은 방법들을 적용했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;모델 연산 효율화를 위한 Precision 설정&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;보통 VRAM이 작은 환경에서 LLM은 FP16(BF16) 기반의 half-precision 연산을 사용하도록 가이딩되어 있으며, 이는 연산 속도와 VRAM 사용량을 균형 있게 줄여주는 가장 기본적인 설정입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;배치 처리 최적화로 메모리 사용량 제어&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;두 가지의 최적화 방법을 통해서 작은 GPU 메모리에서도 안정적으로 학습이 가능하도록 최적화했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Gradient Checkpointing&lt;/p&gt;
&lt;p&gt;중간 활성화를 모두 저장하지 않고 일부 지점만 저장한 뒤 역전파 때 다시 계산해서, 연산량을 늘리는 대신 학습 시 필요한 GPU 메모리를 대략 절반 수준까지 줄이는 방법&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gradient Accumulation&lt;/p&gt;
&lt;p&gt;GPU 메모리가 작아 한 번에 큰 배치를 올리기 어려울 때, 여러 번의 작은 배치(micro batch)에서 그래디언트를 누적했다가 한 번에 업데이트하여 메모리는 작은 배치 수준으로 유지하면서도 큰 batch size로 학습한 것과 비슷한 효과를 내는 방법&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;LoRA 및 파라미터 Quantization 학습&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 중요한 부분으로 SFT를 진행할 때는 전체 파라미터를 직접 학습하지 않고, 특정 모듈에 저차원 어댑터(LoRA) 를 추가한 뒤 이 파라미터만 업데이트하는 방식을 사용했습니다. 이 방식은 전체 모델을 미세 조정하는 것보다 학습 시간과 GPU 메모리 사용량을 크게 줄일 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;이번 프로젝트에서는 여기에 더해, 기저 모델 가중치를 4bit로 양자화한 상태에서 LoRA 어댑터를 학습하는 QLoRA 방식을 적용해 학습 시 필요한 VRAM을 추가로 절감했습니다. (연산은 FP16/BF16으로 수행하고, Base 모델 가중치 저장에만 4bit quantization을 적용)&lt;/p&gt;
&lt;p&gt;LoRA 기반으로 학습하면 추론 단계에서도 이점이 있습니다.
기존 base model에 학습된 LoRA 모듈만 어댑터처럼 결합해 사용할 수 있기 때문에, 여러 도메인·목적에 맞춰 학습된 LoRA를 빠르게 교체할 수 있고, 전체 모델을 여러 개 따로 관리하는 것보다 저장 공간과 로딩 시간 모두를 크게 줄일 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LoRA &amp;#x26; Quantization&lt;/strong&gt; &lt;br&gt;
거대한 모델 전체를 수정하려면 엄청난 메모리가 필요합니다. Quantization(양자화)으로 모델의 무게를 가볍게 줄이고, LoRA를 통해 모델의 핵심 지식은 건드리지 않은 채 &apos;얇은 노트&apos; 한 권 분량의 지식만 추가로 학습시키는 효율적인 방법입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 1200px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e3cdf681f53678f8a3ad6625a91912d7/b8a05/oy_sllm4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABSElEQVR42o2Ry04cMRBF+8ejPBT+gmWUL0AEBbEnUXZEURbJAgHDDPPodo/t8qt9Uu0BKUgssFRyqVS+dY+rG4aB7XaLMYZaK687h77RDPTre8Tp2ylTSqFLKRFj1EjkMmnUJjxpOElYHxnMntEJfx8iv+8D130lFtgaz9X1wK8bw5/FSAxCt1wu2WzWjDawWFvuNPpRyLngQ2r3arVCJHB8mXh/uufj18DaVn7eCW9PHR/OHJ9/SHPeee+1WSg546xj1xv2ekuITfBl3ANyrZkpC1PymqdG1v3fOiPPToMOsCo657PoPGBGX/SJ211k4yp5AsmVnYdezRk5DHkmWFTQSySliPOiLnNDnr/Fe0X+Vjg6jxxdZB4U+epGeHNieffF8um7PyA/g1HLQZcj4lVYmuOnOm1RkLT0WNZhGbN3DKNtS52R/wHbbxpqxP8QyQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e3cdf681f53678f8a3ad6625a91912d7/263a4/oy_sllm4.webp 480w,
/static/e3cdf681f53678f8a3ad6625a91912d7/a6361/oy_sllm4.webp 960w,
/static/e3cdf681f53678f8a3ad6625a91912d7/0b34d/oy_sllm4.webp 1920w,
/static/e3cdf681f53678f8a3ad6625a91912d7/da28f/oy_sllm4.webp 2880w,
/static/e3cdf681f53678f8a3ad6625a91912d7/946eb/oy_sllm4.webp 3034w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e3cdf681f53678f8a3ad6625a91912d7/9aebd/oy_sllm4.png 480w,
/static/e3cdf681f53678f8a3ad6625a91912d7/a91f8/oy_sllm4.png 960w,
/static/e3cdf681f53678f8a3ad6625a91912d7/ac7a9/oy_sllm4.png 1920w,
/static/e3cdf681f53678f8a3ad6625a91912d7/f9c26/oy_sllm4.png 2880w,
/static/e3cdf681f53678f8a3ad6625a91912d7/b8a05/oy_sllm4.png 3034w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e3cdf681f53678f8a3ad6625a91912d7/ac7a9/oy_sllm4.png&quot; alt=&quot;QLoRA SFT개념과 adapter방식의 이점 설명&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;그림 6. QLoRA SFT개념과 adapter 방식의 이점 설명&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 단축&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SFT 과정에서 프롬프트를 단축하는 이유는 추론 시 토큰 비용을 줄이고 응답 속도를 개선하기 위해서입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 단축의 비용 절감 효과&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;프롬프트를 단축 전 528자에서 98자로 단축(약 81% 감소)하면, 대규모 배치 추론 시 상당한 인프라 비용을 절감할 수 있습니다. 이를 구체적인 수치로 환산해보면 다음과 같습니다. 프롬프트 단축은 자체 GPU 서버(AWS g4dn.xlarge 등)에서 sLLM을 운영할 때 처리량(throughput) 향상에 직접적인 영향을 미칩니다.&lt;/p&gt;
&lt;br&gt;
&lt;figcaption style=&quot;margin-bottom:0px;&quot;&gt;표 4. 자체 GPU 서버 운영 시 프롬프트 단축 효과 (g4dn.xlarge 기준)&lt;/figcaption&gt;
&lt;div style=&quot;font-family: &apos;Pretendard&apos;, -apple-system, BlinkMacSystemFont, &apos;Malgun Gothic&apos;, &apos;Segoe UI&apos;, sans-serif; border-radius: 12px; overflow: hidden; border: 1px solid #e0e0e0; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 900px; margin: 0 auto;&quot;&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; table-layout: fixed;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color: #E4F5FF;&quot;&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6); width: 25%;&quot;&gt;
          구분
        &lt;/th&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6);&quot;&gt;
          원본 프롬프트&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(528자, ~350 토큰)&lt;/span&gt;
        &lt;/th&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center;&quot;&gt;
          짧은 프롬프트&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(98자, ~65 토큰)&lt;/span&gt;
        &lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          배치당 처리 시간&lt;sup style=&quot;font-size: 11px;&quot;&gt;*&lt;/sup&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 3.2초
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 0.6초&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(약 81% 단축)&lt;/span&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          시간당 처리 가능량
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 18,000 건
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 96,000 건&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(약 5.3배 향상)&lt;/span&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          100만 건 처리&lt;br&gt;소요 시간
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 55.6 시간
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 10.4 시간&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(약 81% 단축)&lt;/span&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          100만 건 추론&lt;br&gt;인프라 비용&lt;sup style=&quot;font-size: 11px;&quot;&gt;**&lt;/sup&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 $38.9
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          약 $7.3&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(약 81% 절감)&lt;/span&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;div style=&quot;max-width: 900px; margin: 16px auto 0; padding-top: 10px; font-size: 12px; color: #888; line-height: 1.5;&quot;&gt;
  &lt;div style=&quot;display: inline-block; border-top: 1px solid #e8e8e8; padding-top: 8px;&quot;&gt;
    &lt;sup&gt;*&lt;/sup&gt; batch_size=16 기준, 실제 측정값 (짧은 프롬프트 기준 0.6초/배치)&lt;br&gt;
    &lt;sup&gt;**&lt;/sup&gt; AWS g4dn.xlarge 온디맨드 가격 $0.70/hour 기준
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;LLM으로 학습 데이터를 생성할 때는 제약조건·지시사항을 모두 포함한 긴 프롬프트가 필요하지만, 자체 학습한 sLLM에서는 지시문 자체를 모델이 내재적으로 학습할 수 있으므로 훨씬 짧은 형태의 프롬프트로도 동일한 품질을 유지할 수 있습니다.&lt;/p&gt;
&lt;p&gt;프롬프트 길이가 모델 성능에 어떤 영향을 주는지 검증하기 위해, 다양한 형식을 구성하여 1 epoch씩 학습한 후 비교한 결과 다음과 같은 인사이트를 얻었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;실험 인사이트&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;설명(Rich Desc)을 함께 제공하는 형식이 가장 안정적으로 동작&lt;/li&gt;
&lt;li&gt;예시(Example based)만 제공하면 품질이 떨어짐&lt;/li&gt;
&lt;li&gt;설명 없이 한 블록으로 줄인 extreme 단축 버전(TaskTagOnly)은 성능이 가장 낮았음&lt;/li&gt;
&lt;li&gt;설명과 input 사이의 구조적 구분(ShortDesc, NoInputHeader)을 제거하면 품질이 저하됨&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;즉, 지나치게 축약하는 것보다 짧더라도 구조, 규칙, 역할을 명확히 보여주는 형태가 가장 효율적이라는 결론을 얻었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 800px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/869bcfadc22340ae01b188f0621f9451/a1895/oy_sllm5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAB7CAAAewgFu0HU+AAABp0lEQVR42nVS2W7jMAz0//9jHzbxpnEs2bqo256SWjRIg60AgrJkjmaGnI7e0TlO/FvGGKzrCmsttm2D1hpKqbGXkL3cafXAcr+N+5QSaq04jgNTKQXOO+xk0c7zCSzrPF+/fp5RIVxvV8zXGZfLBfM8w3uPqQpgCFh3BWMX+OiehfKi5NeQM1k5JyitBkPNWZSJ0inngpoDtrChtA4fNHKro+g3wO/9uwrZT9Y6tFqQk8KDmbaaYdwDxGfvYK/F/3tsAIrJjZnhKCBaBujBYORXRAYf8nod0RlE2H/Dyn1/A56cc5DGDMqNGPSBO4PG2rGbBfUQG3ZuHPvEdjj22ZIZCpSeETM9GY8uE9Fo+zgUST2jsnzlNRaj8fH5B5YBMz9KMTEjEZNgzR1HKzD8UOkNQvs5NjJDPwwewAG9GmT/FyEaZrXD0MahYVKAT8SZI3gEll5YiSzucoaEgEpurY3c+4kU4xiFmj0TdxwGib0tkS1ghplWkL1xIytsJDgKmISVFEUulhBgGdDOwOKv3G27we3zzj4S+0kDwFkzcuB/xbLMIYP9BQOzWMPqFlWKAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/869bcfadc22340ae01b188f0621f9451/263a4/oy_sllm5.webp 480w,
/static/869bcfadc22340ae01b188f0621f9451/a6361/oy_sllm5.webp 960w,
/static/869bcfadc22340ae01b188f0621f9451/0b34d/oy_sllm5.webp 1920w,
/static/869bcfadc22340ae01b188f0621f9451/18c0b/oy_sllm5.webp 2179w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/869bcfadc22340ae01b188f0621f9451/9aebd/oy_sllm5.png 480w,
/static/869bcfadc22340ae01b188f0621f9451/a91f8/oy_sllm5.png 960w,
/static/869bcfadc22340ae01b188f0621f9451/ac7a9/oy_sllm5.png 1920w,
/static/869bcfadc22340ae01b188f0621f9451/a1895/oy_sllm5.png 2179w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/869bcfadc22340ae01b188f0621f9451/ac7a9/oy_sllm5.png&quot; alt=&quot;각 프롬프트 구성 조합별 1epoch eval loss 비교&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;그림 7. 각 프롬프트 구성 조합별 1epoch eval loss 비교&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;프롬프트 단축 실험 예시&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;준비된 학습 데이터를 기반으로 SFT를 진행하면서, 다양한 단축 형식을 실험했습니다. 아래는 실제 테스트한 프롬프트 형식 예시입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;예시 1: 최소 축약형&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;#OY-TASK: 올리브영 리뷰 테마 추천용 표현을 추출
상품: {goods_nm}
리뷰: {review}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예시 2: 구조적 명시형&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;올리브영 리뷰 테마 추천용 표현을 추출&gt;
수식하는 표현은 2 단어보단 많아야해
적절한 표현이 없다면 공백 목록만 반환 [&quot;&quot;]

&amp;lt;input review&gt;
상품: {goods_nm}
리뷰: {review}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예시 3: 예시 기반 형식&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 올리브영 리뷰 표현 추출
example : [&quot;사용감 : 순하고 자극 없는 &apos;미스트&apos;&quot;]
input : {goods_nm}
&quot;{review}&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;최종 프롬프트 형식&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;실험 결과, 다음 구성의 템플릿이 성능 유지 + 토큰 단축 관점에서 가장 우수했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;#OY-TASK: 올리브영 리뷰 테마 추천용 표현을 추출&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#FORMAT: 긍정적 표현만, 카테고리+표현, JSON 리스트&lt;/span&gt;
상품&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;goods_nm&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
리뷰&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;review&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이 템플릿은 아래와 같은 구성을 통해 최소한의 토큰으로도 안정적인 결과를 제공합니다.&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;짧고 명확한 task 명명(#OY-TASK)&lt;/li&gt;
&lt;li&gt;출력 형태를 강제하는 FORMAT 규칙&lt;/li&gt;
&lt;li&gt;불필요한 설명 없이 input만 제시&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;성능-비교&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90&quot; aria-label=&quot;성능 비교 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;성능 비교&lt;/h3&gt;
&lt;p&gt;표현 추출 모델 기준으로 정규식의 형식/어미 alignment 검증과 LLM as a judge 방법론으로 LLM을 통해서 문맥을 검증했습니다. 평가/벤치마크를 위한 비교 대상은 모델 개발 시점 Gemini flash 버전인 2.0 기준으로 &lt;strong&gt;압축하지 않은 프롬프트&lt;/strong&gt;로 질의하여 비교했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figcaption style=&quot;margin-bottom:0px;&quot;&gt;표 5. Gemini 2.0 flash와 Gemma3-4b-it 모델 성능 비교&lt;/figcaption&gt;
&lt;div style=&quot;font-family: &apos;Pretendard&apos;, -apple-system, BlinkMacSystemFont, &apos;Malgun Gothic&apos;, &apos;Segoe UI&apos;, sans-serif; border-radius: 12px; overflow: hidden; border: 1px solid #e0e0e0; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 900px; margin: 0 auto;&quot;&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; table-layout: fixed;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background-color: #E4F5FF;&quot;&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6); width: 20%;&quot;&gt;
          구분
        &lt;/th&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; border-right: 1px solid rgba(255,255,255,0.6); width: 40%;&quot;&gt;
          Gemini 2.0 flash&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(비교 대상)&lt;/span&gt;
        &lt;/th&gt;
        &lt;th style=&quot;padding: 16px; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; width: 40%;&quot;&gt;
          Gemma3-4b-it&lt;br&gt;&lt;span style=&quot;font-size: 13px; font-weight: normal; color: #4a7aa6;&quot;&gt;(SFT 후)&lt;/span&gt;
        &lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          프롬프트 길이
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          528 자
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          98 자
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          정확도 상대 평가
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #0056b3; font-weight: 700; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          1.0 (기준)
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; font-size: 15px; text-align: center; vertical-align: middle;&quot;&gt;
          0.95
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #333; font-weight: 600; text-align: center; background-color: #fafafa; vertical-align: middle;&quot;&gt;
          속도
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; line-height: 1.5; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.8초 / 16개 동시 요청&lt;br&gt;&lt;span style=&quot;color:#888; font-size: 12px;&quot;&gt;(GCP API)&lt;/span&gt;
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #0056b3; font-weight: 700; line-height: 1.5; font-size: 14px; text-align: center; vertical-align: middle;&quot;&gt;
          0.6 초 / 배치&lt;br&gt;&lt;span style=&quot;color:#4a7aa6; font-weight: normal; font-size: 12px;&quot;&gt;(GPU 16GB, batch_size=16)&lt;/span&gt;
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;평가 설정 &lt;br&gt;
위의 정확도 평가는 리뷰 샘플에 대해 내부에서 정의한 기준으로 레이블과 모델 출력을 비교해 산출한 상대 지표입니다. &lt;br&gt;
Gemini 2.0 flash 기반 결과를 기준값 1.0으로 두고, 같은 검증 셋에서 Gemma3-4B의 SFT 모델의 상대적인 정확도를 측정한 값이 0.95입니다. &lt;br&gt;
Gemini는 생성 결과 비교 및 벤치마크(LLM as a judge) 용도로만 사용했고 SFT 학습/레이블 생성에는 사용하지 않았습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;실제로 두 모델의 출력을 비교해보면, 유사한 품질의 결과를 생성합니다. 아래는 실제 리뷰에 대한 두 모델의 출력 예시입니다:&lt;/p&gt;
&lt;p&gt;예시 1&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;리뷰: 블랙프라이데이 때 특가로 득템했는데 무난하게 수분 채워주기 좋아요! 안개분사~

Gemini: [&quot;무난하게 수분 채워주는 &apos;미스트&apos; : 효과&quot;]
Ours: [&quot;수분 채워주기 좋은 &apos;미스트&apos; : 보습력&quot;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예시 2&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;리뷰: 이젠 집에 없으면 안될 제품이에여 몇통째 먹고있는제품인데
      젤 괜찮은제품같아요 속쓰림 속부대낌 그런거 없어요 굿굿

Gemini: [&quot;속쓰림 속부대낌 없는 &apos;기능식품&apos; : 효과&quot;]
Ours: [&quot;속쓰림 없이 편안한 &apos;영양제&apos; : 효과&quot;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;결론적으로 도메인 특화 SFT를 통해 상용 LLM 대비 약간의 정확도 손실(5%p 이내)이 있었지만, 서비스에 필요한 수준의 품질을 충분히 확보할 수 있었습니다.&lt;/p&gt;
&lt;figcaption style=&quot;margin-bottom:0px;&quot;&gt;표 6. sLLM 도입의 주요 성과 요약&lt;/figcaption&gt;
&lt;div style=&quot;font-family: &apos;Pretendard&apos;, -apple-system, BlinkMacSystemFont, &apos;Malgun Gothic&apos;, &apos;Segoe UI&apos;, sans-serif; border-radius: 12px; overflow: hidden; border: 1px solid #e0e0e0; box-shadow: 0 4px 12px rgba(0,0,0,0.05); max-width: 900px; margin: 0 auto;&quot;&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; border-spacing: 0; background-color: #ffffff; table-layout: fixed;&quot;&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; background-color: #E4F5FF; border-bottom: 1px solid #d1e9ff; border-right: 1px solid #d1e9ff; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; vertical-align: middle; width: 25%;&quot;&gt;
          비용 효율
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 15px; text-align: left; vertical-align: middle;&quot;&gt;
          외부 API 호출 대비 &lt;span style=&quot;font-weight: 700; color: #333;&quot;&gt;비용 예측 가능성&lt;/span&gt; 확보
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; background-color: #E4F5FF; border-bottom: 1px solid #d1e9ff; border-right: 1px solid #d1e9ff; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; vertical-align: middle;&quot;&gt;
          속도
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: 1px solid #f0f0f0; color: #444; line-height: 1.5; font-size: 15px; text-align: left; vertical-align: middle;&quot;&gt;
          배치 처리 최적화로 &lt;span style=&quot;font-weight: 700; color: #0056b3;&quot;&gt;0.6초 대&lt;/span&gt; 응답 속도 달성
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 16px; background-color: #E4F5FF; border-right: 1px solid #d1e9ff; color: #0056b3; font-weight: 700; font-size: 16px; text-align: center; vertical-align: middle;&quot;&gt;
          품질
        &lt;/td&gt;
        &lt;td style=&quot;padding: 16px; border-bottom: none; color: #444; line-height: 1.5; font-size: 15px; text-align: left; vertical-align: middle;&quot;&gt;
          상용 모델 대비 &lt;span style=&quot;font-weight: 700; color: #333;&quot;&gt;95% 수준&lt;/span&gt;의 정확도 유지하며 도메인 특화 완료
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;속도 및 운영 관점 해석&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;속도는 각 환경에서 측정한 평균 응답 시간입니다.&lt;/p&gt;
&lt;p&gt;Gemini 2.0 flash는 GCP API 기준 16개 동시요청 시 약 0.8초이며, Gemma3-4B SFT 모델은 Tesla T4 16GB 환경에서 batch size 16 기준 한 배치를 처리하는 데 약 0.6초가 소요되었습니다. GCP Gemini API의 동시 호출의 한계가 있으나, 자체 LLM은 Scale-Out으로 Throughput을 충분히 늘려 원하는 만큼의 처리가 가능하다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;자체 sLLM은 배치 처리 전제가 있기 때문에 실제 단일 요청 단위 처리 시간과 전체 처리량(throughput) 관점에서는 상용 LLM 대비 더 유리하게 동작할 수 있으며, 호출량이 증가해도 GPU 인스턴스 단가 범위 내에서 비용 상한을 예측 가능하게 유지할 수 있다는 장점이 있습니다&lt;/p&gt;
&lt;h2 id=&quot;자체-sllm을-sft하며-얻은-것&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EC%B2%B4-sllm%EC%9D%84-sft%ED%95%98%EB%A9%B0-%EC%96%BB%EC%9D%80-%EA%B2%83&quot; aria-label=&quot;자체 sllm을 sft하며 얻은 것 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;자체 sLLM을 SFT하며 얻은 것&lt;/h2&gt;
&lt;br&gt;
&lt;h3 id=&quot;학습-목표-품질-기준을-먼저-고정하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%99%EC%8A%B5-%EB%AA%A9%ED%91%9C-%ED%92%88%EC%A7%88-%EA%B8%B0%EC%A4%80%EC%9D%84-%EB%A8%BC%EC%A0%80-%EA%B3%A0%EC%A0%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;학습 목표 품질 기준을 먼저 고정하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;학습 목표, 품질 기준을 먼저 고정하기&lt;/h3&gt;
&lt;p&gt;LLM에게 “좋은 품질의, 내가 원하는 느낌의 결과”를 막연히 기대하는 것은 올바른 접근 방식이 아닙니다.&lt;/p&gt;
&lt;p&gt;모델이 제대로 학습하려면 어떤 출력이 좋은 결과인지를 구체적인 기준으로 정의해야 합니다. 이를 검증 가능하게 형식화하는 것이 필수적입니다. 그래야만 해당 Task를 안정적으로 학습시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;실제 리뷰 테마 프로젝트 과정에서는 초기 기획 단계의 콘셉트와 서비스 UI가 변경되면서 학습 목표가 여러 차례 수정되었습니다. 그 결과 이미 학습을 마친 모델의 출력을 서비스 직전에 다시 필터링해야 하는 비효율이 발생하기도 했습니다.&lt;/p&gt;
&lt;p&gt;이 경험을 통해, 학습 전에 목표와 품질 기준을 충분히 합의·고정해 두는 것이 sLLM 프로젝트의 성패를 좌우하는 핵심 요소라는 점을 다시 한 번 확인했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;human-in-the-loop-검수의-필요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#human-in-the-loop-%EA%B2%80%EC%88%98%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1&quot; aria-label=&quot;human in the loop 검수의 필요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Human-in-the-loop 검수의 필요성&lt;/h3&gt;
&lt;p&gt;리뷰테마 추천은 사용자 화면에 직접 노출되는 콘텐츠이기 때문에 자동 검증 모델만으로는 충분하지 않습니다. 1차적으로는 검증 모델을 통해 필터링을 수행하지만, 최종적으로는 사람이 직접 확인하는 단계가 반드시 필요합니다.&lt;/p&gt;
&lt;p&gt;이를 위해서는 정량적·정성적으로 명확한 검증 기준이 정의되어 있어야 하며, 사람이 그 기준에 따라 일관되게 검수할 수 있어야 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;올리브영의-첫-번째-자체-sllm의-의미&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EC%9E%90%EC%B2%B4-sllm%EC%9D%98-%EC%9D%98%EB%AF%B8&quot; aria-label=&quot;올리브영의 첫 번째 자체 sllm의 의미 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영의 첫 번째 자체 sLLM의 의미&lt;/h3&gt;
&lt;p&gt;이번 프로젝트는 NVIDIA Tesla T4 16GB라는 매우 한정된 자원 환경에서 sLLM을 학습하고 서비스에 성공적으로 적용할 수 있음을 검증한 올리브영의 첫 사례라는 점에서 큰 의미가 있습니다.&lt;/p&gt;
&lt;p&gt;비록 완벽한 결과는 아닐지라도 범용 LLM에 의존하지 않고 비용 효율적이면서도 빠른 개선이 가능한, 우리만의 도메인 특화 sLLM을 구축할 수 있다는 가능성을 명확히 확인했습니다. 이는 향후 올리브영이 도메인 특화 AI를 전방위로 확장해 나가는 데 있어 견고한 기술적 기반이 될 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;앞으로의-방향성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EB%B0%A9%ED%96%A5%EC%84%B1&quot; aria-label=&quot;앞으로의 방향성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;앞으로의 방향성&lt;/h2&gt;
&lt;p&gt;리뷰 테마 추천은 올리브영 내부에서 sLLM 역량을 쌓는 파일럿 프로젝트이자, 전자신문 등 외부 매체에 &apos;고객 리뷰 기반 생성형 추천&apos; 사례로 소개된 의미 있는 레퍼런스입니다. 우리는 이 첫 번째 버전을 출발점 삼아 기술적 고도화를 진행 중입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고 &lt;br&gt;
&lt;a href=&quot;https://www.etnews.com/20250915000260&quot;&gt;CJ올리브영, 자체 &apos;sLLM&apos; 개발... AI가 &apos;K뷰티&apos; 골라준다 (전자신문, 2025년 9월 16일)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;표현 품질 개선을 위한 개선 작업&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;리뷰 텍스트를 기반으로 생성된 추천 표현 중에는 문맥이나 문법적으로 UI에 노출하기 적합하지 않은 문장도 일부 포함됩니다. 이를 개선하기 위해, 생성된 표현을 임베딩 공간에서 클러스터링하고 군집 중심에서 벗어난 outlier 표현을 자동으로 제거하는 방식을 적용하고 있습니다.&lt;/p&gt;
&lt;p&gt;이를 통해 더 자연스럽고 안정적인 품질의 표현만 최종 추천에 반영되도록 개선하고 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;카드 이미지 자동 생성 개발&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;현재는 UI/UX 팀이 제작·리터칭한 소수의 이미지를 사용하고 있어, 추천 내용이 달라도 유사한 카드가 반복 노출되는 한계가 있습니다.
이에 따라 이미지 생성 파이프라인을 자동화하여, 아래와 같은 목표로 개발을 진행 중입니다. 자동화로 더 많은 이미지를 생성해서 추천 카드를 더 신선하고 자연스럽게 확장 가능한 형태로 전환하려고 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다양한 스타일과 구성의 이미지 생성&lt;/li&gt;
&lt;li&gt;리뷰·추천 내용과 연동된 맞춤형 시각 요소 생성&lt;/li&gt;
&lt;li&gt;운영 비용 절감 및 이미지 다양성 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;더 크고 복잡한 Task를 위한 LLM 개발&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;시작은 4B 규모의 작은 모델과 간단한 생성 과업이었지만, 이번 경험은 우리에게 큰 자신감을 주었습니다. 앞으로는 더 크고 복잡한 태스크를 수행할 수 있는 대형 모델 학습에 도전하며, 올리브영의 다양한 도메인 문제를 AI로 해결해 나가는 여정을 멈추지 않을 것입니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영 타입스크립트로 알아보는 제네릭과 매개변수 다형성]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-12-31/generics-and-parametric-polymorphism/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-31/generics-and-parametric-polymorphism/</guid><pubDate>Wed, 31 Dec 2025 17:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#&quot; aria-label=&quot; permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;지난 아티클 &lt;a href=&quot;https://oliveyoung.tech/2024-08-11/type-and-type-system-with-typescript/&quot;&gt;타입스크립트로 알아보는 타입과 타입 시스템&lt;/a&gt;에서 우리는 타입 시스템의 기초를 탄탄히 다졌습니다. 타입 검사기가 어떻게 프로그램의 모순을 찾아내는지, 그리고 서브타입에 의한 다형성이 딱딱한 타입에 유연성을 어떻게 부여하는지 살펴보았죠.&lt;/p&gt;
&lt;p&gt;서브타입에 의한 다형성 덕분에 &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt; 타입의 값을 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt; 타입이 필요한 곳에 전달할 수 있게 되었습니다. &quot;(올리브영의 당일 배송 서비스인) 오늘드림 상품도 상품이다&quot;라는 직관적인 관계가 타입 시스템에서도 인정받게 된 것이죠.&lt;/p&gt;
&lt;p&gt;그런데 한 가지 의문이 듭니다.&lt;/p&gt;
&lt;p&gt;서브타입 관계가 없는, 완전히 독립적인 타입들에 동일한 로직을 적용하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p&gt;이번 글에서도 올리브영의 실무 코드를 바탕으로, 타입의 한계를 뛰어넘어 유연성을 극대화하는 방법을 함께 파헤쳐 보겠습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;h1 id=&quot;같은-로직-다른-타입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%99%EC%9D%80-%EB%A1%9C%EC%A7%81-%EB%8B%A4%EB%A5%B8-%ED%83%80%EC%9E%85&quot; aria-label=&quot;같은 로직 다른 타입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;같은 로직, 다른 타입&lt;/h1&gt;
&lt;p&gt;올리브영 서비스에는 수많은 데이터 모델이 존재합니다. 상품, 리뷰, 주문 내역은 각각 그 목적에 맞게 서로 다른 속성들을 가지고 있죠.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 상품은 가격이 중요합니다.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Review&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  rating&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 리뷰는 별점이 핵심이죠.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Order&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  orderDate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 주문은 언제 했는지가 중요합니다.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;이렇게 데이터의 모양이 제각각인 상황에서, 목록의 첫 번째 요소를 가져오는 함수가 필요하다고 가정해봅시다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirstProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;잘 작동합니다. 그런데 이번엔 리뷰 목록의 첫 번째 요소도 가져오고 싶습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirstReview&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Review&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Review &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;주문 내역의 첫 번째 요소도요.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirstOrder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Order&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Order &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;세 함수의 본문을 보세요. &lt;code class=&quot;language-text&quot;&gt;return items[0];&lt;/code&gt;로 모두 동일합니다. 로직은 하나인데, 타입이 다르다는 이유만으로 함수를 세 개나 만들어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;Review&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;Order&lt;/code&gt; 사이에는 서브타입 관계가 없습니다. &quot;상품은 리뷰이다&quot;라거나 &quot;주문은 상품이다&quot;라는 명제는 성립하지 않으니까요. 서브타입에 의한 다형성으로는 이 문제를 해결할 수 없습니다.&lt;/p&gt;
&lt;p&gt;우리에겐 새로운 도구가 필요합니다. 타입 자체를 추상화하는 도구, 바로 &lt;strong&gt;제네릭&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;제네릭이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%9D%B4%EB%9E%80&quot; aria-label=&quot;제네릭이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;제네릭이란?&lt;/h1&gt;
&lt;p&gt;제네릭(Generics)은 &lt;strong&gt;타입 변수(Type Variable)를 사용하여 타입을 추상화하는 방법&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;함수가 값을 매개변수로 받아 다양한 값에 대해 같은 로직을 수행하듯이, 제네릭은 타입을 매개변수로 받아 다양한 타입에 대해 같은 로직을 수행합니다.&lt;/p&gt;
&lt;p&gt;앞서 만든 세 개의 함수를 제네릭으로 합쳐봅시다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;T&gt;&lt;/code&gt;가 바로 타입 변수입니다. 이 함수는 &quot;어떤 타입 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;의 배열을 받아서, 그 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt; 타입의 값을 반환한다&quot;고 선언합니다.&lt;/p&gt;
&lt;p&gt;이제 하나의 함수로 모든 경우를 처리할 수 있습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firstProduct &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// Product | undefined&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firstReview &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Review&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;reviews&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;// Review | undefined&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firstOrder &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Order&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;orders&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;// Order | undefined&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;함수를 호출할 때 &lt;code class=&quot;language-text&quot;&gt;&amp;lt;Product&gt;&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;Review&gt;&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;Order&gt;&lt;/code&gt;처럼 타입 인자를 전달합니다. 마치 함수에 값을 전달하듯이요.&lt;/p&gt;
&lt;p&gt;여기서 중요한 점이 있습니다. 제네릭 없이 &lt;code class=&quot;language-text&quot;&gt;any&lt;/code&gt;를 사용해도 비슷해 보이지만, 결정적인 차이가 있습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// any 사용 - 타입 정보 손실&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirstAny&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; first &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirstAny&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// any - 타입 정보가 사라짐&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 제네릭 사용 - 타입 정보 보존&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;undefined&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; first &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Product | undefined - 타입 정보 유지&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;any&lt;/code&gt;를 사용하면 함수를 통과하는 순간 타입 정보가 증발해 버립니다. 반환값이 &lt;code class=&quot;language-text&quot;&gt;any&lt;/code&gt;이니 이후 어떤 속성에 접근해도 타입 검사기는 아무런 도움을 주지 못합니다.&lt;/p&gt;
&lt;p&gt;반면 제네릭을 사용하면 타입 정보가 함수를 관통하여 보존됩니다. &lt;code class=&quot;language-text&quot;&gt;Product[]&lt;/code&gt;를 넣으면 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;가 나오고, &lt;code class=&quot;language-text&quot;&gt;Review[]&lt;/code&gt;를 넣으면 &lt;code class=&quot;language-text&quot;&gt;Review&lt;/code&gt;가 나옵니다. 타입 검사기의 보호를 계속 받을 수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a56e4a2564c224022ae6b391243f2ad8/d55ca/generic1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.95833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42l1Ti3IbIRDz/39jxnaTdJrU9vE+4A5QJBy3SfDsrA+0Yh/isG0bUkx4eX7G6+vrtPP5jKenJ9yWG0ot6L2jtYZ1XZGSsC84Ho84/zrjdDrBOTfPSik4xBiRthU+B/g1IOc8D/KasW87aqlzTxaysPQlIteM3njR3qbXEtch5YTUSbgT1ArMxttaRmwJoUUs2SAXfq8JcSO2rdPs5meM/vstwLUAE82dcO0Zbg/zsPaKNjr20cC7kZmRCBMJVYndHYnCvFS4Jly/Y0MMOKwsZYyBS76h7htGH7NnnV5r3/fZgpjiLM19kn1ditdSfw+Bda9ZAWq4MsnzO8nURxKqf+pPrA22giUOOPqHmTKYIbBOwhCYaoL1AdfF4HJb8Pe6wM+9+IUwwOaO3xF49gMnN3C0Ay8BeEsDvIODJKHzng3n5JjhYj2lYnEzbu75kL4RutJxK8A7Cd4S8Id2WYFrfhCuko2CGoMDat1QJRX6QrnIS39TMjxvbbDczMFxCCTYZVRMbfde5kxCNbKzqT7ey/u5RKihiHBrG97LBY6SkQp+Ll18COzdfC2fA1DGmnwmibys1goX3NRh2GVxak+adfRhi5QT25WWe4aVhCIQYaM0JANJR157IpxqoA4TNSu9Fuq160eMdEg0laGSrwaWk12WBdZaGGPmu3ysB6HAi7/jjDX/sEroW8meD1v9sc4SZO4BBOrBS3vyCtJrcd7Nb+HM8j8JxQvjvccHPus++RRywboAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a56e4a2564c224022ae6b391243f2ad8/263a4/generic1.webp 480w,
/static/a56e4a2564c224022ae6b391243f2ad8/a6361/generic1.webp 960w,
/static/a56e4a2564c224022ae6b391243f2ad8/0b34d/generic1.webp 1920w,
/static/a56e4a2564c224022ae6b391243f2ad8/4d98c/generic1.webp 2212w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a56e4a2564c224022ae6b391243f2ad8/9aebd/generic1.png 480w,
/static/a56e4a2564c224022ae6b391243f2ad8/a91f8/generic1.png 960w,
/static/a56e4a2564c224022ae6b391243f2ad8/ac7a9/generic1.png 1920w,
/static/a56e4a2564c224022ae6b391243f2ad8/d55ca/generic1.png 2212w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a56e4a2564c224022ae6b391243f2ad8/ac7a9/generic1.png&quot; alt=&quot;일반 함수에서는 타입 정보가 손실되고, 제네릭 함수에서는 타입 정보가 보존되는 흐름을 보여주는 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;[그림 1: 일반 함수 vs 제네릭 함수 - 타입 정보의 흐름]&lt;/figcaption&gt;
&lt;br&gt;
&lt;h2 id=&quot;타입-인자-추론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85-%EC%9D%B8%EC%9E%90-%EC%B6%94%EB%A1%A0&quot; aria-label=&quot;타입 인자 추론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;타입 인자 추론&lt;/h2&gt;
&lt;p&gt;사실 대부분의 경우, 타입 인자를 명시적으로 적을 필요가 없습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; firstProduct &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFirst&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// T가 Product로 자동 추론됨&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;타입스크립트 컴파일러는 &lt;code class=&quot;language-text&quot;&gt;products&lt;/code&gt;의 타입이 &lt;code class=&quot;language-text&quot;&gt;Product[]&lt;/code&gt;임을 알고 있습니다. 이 정보를 바탕으로 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;임을 스스로 추론합니다. 이를 &lt;strong&gt;타입 인자 추론(Type Argument Inference)&lt;/strong&gt;이라고 합니다.&lt;/p&gt;
&lt;p&gt;타입 인자 추론 덕분에 제네릭 코드도 일반 코드처럼 간결하게 작성할 수 있습니다. 컴파일러가 추론에 실패하는 복잡한 경우에만 명시적으로 타입 인자를 적어주면 됩니다. (더 자세한 내용은 &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/2/generics.html&quot;&gt;타입스크립트 공식 핸드북&lt;/a&gt;을 참고하세요.)&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;제네릭의-필요성---올리브영-api-응답&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%84%A4%EB%A6%AD%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1---%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-api-%EC%9D%91%EB%8B%B5&quot; aria-label=&quot;제네릭의 필요성   올리브영 api 응답 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;제네릭의 필요성 - 올리브영 API 응답&lt;/h1&gt;
&lt;p&gt;제네릭의 진가는 실무에서 더욱 빛납니다. 올리브영의 API 응답 구조를 생각해봅시다.&lt;/p&gt;
&lt;p&gt;상품 목록 API, 사용자 정보 API, 주문 내역 API 모두 비슷한 응답 구조를 가집니다. 실제 데이터(&lt;code class=&quot;language-text&quot;&gt;data&lt;/code&gt;)와 함께 상태 코드(&lt;code class=&quot;language-text&quot;&gt;status&lt;/code&gt;), 메시지(&lt;code class=&quot;language-text&quot;&gt;message&lt;/code&gt;)를 반환하죠.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 제네릭 없이 - 타입마다 별도 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductResponse&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;UserResponse&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; User&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OrderResponse&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Order&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReviewListResponse&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Review&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ... API가 늘어날 때마다 새로운 타입 정의&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;status&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;message&lt;/code&gt; 부분이 계속 반복됩니다. API가 수십 개라면 수십 번 같은 코드를 작성해야 합니다.&lt;/p&gt;
&lt;p&gt;제네릭으로 이 반복을 제거해봅시다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 제네릭으로 - 하나의 타입으로 모든 응답 처리&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductResponse&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;UserResponse&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;User&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OrderResponse&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Order&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReviewListResponse&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Review&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ApiResponse&amp;lt;T&gt;&lt;/code&gt;라는 하나의 제네릭 인터페이스가 모든 API 응답 타입을 생성합니다. &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt; 자리에 실제 데이터 타입을 넣으면 완전한 응답 타입이 만들어집니다.&lt;/p&gt;
&lt;p&gt;이제 API 응답 구조가 변경되어도 &lt;code class=&quot;language-text&quot;&gt;ApiResponse&amp;lt;T&gt;&lt;/code&gt; 한 곳만 수정하면 됩니다. 모든 응답 타입에 일괄 적용되니까요.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1886px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4e48ecb883fae0627949f66d2f06549c/1fa94/generic2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.20833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB8UlEQVR42nWTa7OiMAyG+f+/zw86jjqO15VLofRCoSWbN1jXw5yNE1tC8yRNQhFjJOg0TaLjOIqGEGTtuo72+z0dj0dSSv04A81+YKSUqICDc476vhfntm2paRrZQ7XWVNc1VVUl77Id56HGGPEfhkHsBQxrmef5A7/dbnQ6nWS93+8SACCsgAAG9d6LvcDfWnCFsiwlU4C22y3tdjt6PB5iQ8aXy0UCvl4vej6fYhcgIv0GRIYQRD+fzwKBEwT1wh7ncmbYCzA7fgscUDNrLV2vV9psNnQ4HCRTgNAcBFoLkiuQ9m+COubOYUW9YMsdxf777AeIaN9GjIv3A43h32jka+Vxyoog3yJXXgPhHOMkz8gulyA7wyZ2fo+z+QwEE1PkQqfE1wlEryfPpeWrxnc9+ad9T2VTEo/xEohjD9NMf2pFfkzyLECu+SfDcZhZuasmkdULPM6RTLQU0kh2cmRYE9s6PtsHnlXPe5/IhIVofwDZyDelNJGAAeTY5KKnMI8CdnFgS6TWzwKpbKLGrYB5bCYm9ZqL3w1keHWOQVy33vF1Dc+gVdSZjrvMjYmc4ZAkuz4kmuIXMA82MlGdkulH1m2rlpW118un1vBwoxH/k0+G6I7utDgBolQr4JZX9QYvQZZgVVWL4uvBs34z8P4vnmiMuBDIh7sAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4e48ecb883fae0627949f66d2f06549c/263a4/generic2.webp 480w,
/static/4e48ecb883fae0627949f66d2f06549c/a6361/generic2.webp 960w,
/static/4e48ecb883fae0627949f66d2f06549c/62b89/generic2.webp 1886w&quot; sizes=&quot;(max-width: 1886px) 100vw, 1886px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4e48ecb883fae0627949f66d2f06549c/9aebd/generic2.png 480w,
/static/4e48ecb883fae0627949f66d2f06549c/a91f8/generic2.png 960w,
/static/4e48ecb883fae0627949f66d2f06549c/1fa94/generic2.png 1886w&quot; sizes=&quot;(max-width: 1886px) 100vw, 1886px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4e48ecb883fae0627949f66d2f06549c/1fa94/generic2.png&quot; alt=&quot;제네릭 없이 여러 타입을 반복 정의하던 코드가 제네릭으로 하나로 통합되는 과정&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;[그림 2: 제네릭으로 코드 중복 제거]&lt;/figcaption&gt;
&lt;br&gt;
&lt;h1 id=&quot;제네릭-함수와-타입-추론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%84%A4%EB%A6%AD-%ED%95%A8%EC%88%98%EC%99%80-%ED%83%80%EC%9E%85-%EC%B6%94%EB%A1%A0&quot; aria-label=&quot;제네릭 함수와 타입 추론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;제네릭 함수와 타입 추론&lt;/h1&gt;
&lt;p&gt;제네릭 함수를 좀 더 살펴봅시다. 다양한 목록을 필터링하는 상황을 생각해보세요.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 설명을 위해 단순화한 예시입니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;filterItems&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;predicate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; items&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;predicate&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;이 함수는 &quot;어떤 타입 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;의 배열과, &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;를 받아 불리언을 반환하는 함수를 받아서, &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;의 배열을 반환한다&quot;고 선언합니다.&lt;/p&gt;
&lt;p&gt;올리브영에서 사용해볼까요?
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Product에는 price, Review에는 rating, Order에는 date 속성이 있다고 가정&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 5만원 이상 상품 필터링&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; expensiveProducts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;filterItems&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;price &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 4점 이상 리뷰 필터링&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; goodReviews &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;filterItems&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;reviews&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;r&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; r&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rating &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 이번 달 주문 필터링&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; thisMonthOrders &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;filterItems&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;orders&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;o&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; o&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMonth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMonth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;세 번의 호출 모두 타입 인자를 명시하지 않았습니다. 그런데 콜백 함수 안에서 &lt;code class=&quot;language-text&quot;&gt;p.price&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;r.rating&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;o.date&lt;/code&gt;에 접근할 때 자동완성이 작동합니다. 타입 검사도 정상적으로 이루어집니다.&lt;/p&gt;
&lt;p&gt;어떻게 가능할까요? 타입스크립트가 &lt;code class=&quot;language-text&quot;&gt;products&lt;/code&gt;의 타입에서 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;를 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;로 추론했기 때문입니다. 그 추론이 콜백 함수의 매개변수 &lt;code class=&quot;language-text&quot;&gt;p&lt;/code&gt;에까지 전파되어, &lt;code class=&quot;language-text&quot;&gt;p&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt; 타입임을 알게 됩니다.&lt;/p&gt;
&lt;p&gt;이것이 제네릭과 타입 추론의 시너지입니다. 타입을 명시하지 않아도 타입 안전성을 누릴 수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;제네릭-제약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%84%A4%EB%A6%AD-%EC%A0%9C%EC%95%BD&quot; aria-label=&quot;제네릭 제약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;제네릭 제약&lt;/h1&gt;
&lt;p&gt;제네릭의 강력함에는 대가가 따릅니다. &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;가 무엇이든 될 수 있기 때문에, &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;에 특정 속성이 있다고 가정할 수 없습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;arg&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; arg&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Error: Property &apos;length&apos; does not exist on type &apos;T&apos;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;number&lt;/code&gt;일 수도 있고, &lt;code class=&quot;language-text&quot;&gt;boolean&lt;/code&gt;일 수도 있습니다. 이들에게는 &lt;code class=&quot;language-text&quot;&gt;length&lt;/code&gt; 속성이 없죠. 타입 검사기는 가능한 모든 경우를 고려하므로, 이 코드를 허용하지 않습니다.&lt;/p&gt;
&lt;p&gt;하지만 우리는 &apos;길이&apos; 속성이 있는 타입에 대해서만 이 함수를 사용하고 싶습니다. 이때 &lt;strong&gt;제네릭 제약(Generic Constraints)&lt;/strong&gt;을 사용합니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HasLength&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  length&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; HasLength&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;arg&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; arg&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;T extends HasLength&lt;/code&gt;는 &quot;&lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;HasLength&lt;/code&gt;의 서브타입이어야 한다&quot;는 제약입니다. 이제 &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;는 최소한 &lt;code class=&quot;language-text&quot;&gt;length&lt;/code&gt; 속성을 가진 타입만 될 수 있습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;token comment&quot;&gt;// OK - string은 length가 있음&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;// OK - 배열도 length가 있음&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; length&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK - length 속성이 있는 객체&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;123&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// Error - number는 length가 없음&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;getLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;// Error - boolean도 length가 없음&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;여기서 서브타입에 의한 다형성과 매개변수에 의한 다형성이 만납니다. &lt;code class=&quot;language-text&quot;&gt;extends&lt;/code&gt; 키워드가 그 접점입니다.&lt;/p&gt;
&lt;p&gt;올리브영 예제로 적용해봅시다. 가격이 있는 모든 것에 할인을 적용하는 함수를 만들어보겠습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 설명을 위해 단순화한 예시입니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HasPrice&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; HasPrice&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;price &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; rate &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;이 함수는 &lt;code class=&quot;language-text&quot;&gt;price&lt;/code&gt; 속성을 가진 어떤 타입이든 받을 수 있습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; discountedProduct &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// Product&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; discountedTodayProduct &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;todayProduct&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// TodayProduct&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; discountedGiftSet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;giftSet&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;     &lt;span class=&quot;token comment&quot;&gt;// GiftSet&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;GiftSet&lt;/code&gt; 모두 &lt;code class=&quot;language-text&quot;&gt;price&lt;/code&gt; 속성을 가지고 있다면 이 함수를 사용할 수 있습니다. 그리고 반환 타입은 입력 타입과 동일하게 유지됩니다. &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;를 넣으면 &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;가 나옵니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1886px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/148c667853efe4035468022995a91d88/1fa94/generic3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 78.95833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAACTElEQVR42o1UCW7jMAzM/39YFGibOLV86LTkU7ZmSaYuWiCLXQJjMjI1GpJyLsMwIKUE5xystTDGQGstvu97vLy84PX1FW9vb+LrupZ3nMt7Qgg4Obz3uMzzjG3bZHGimG3fdyzLguM4sK6rxNM0CUop+JuN44gLP2baoOnUGOP3Zl5nz+RMzOCYVZzqWSUr5IoYHF/OTUzESrechcg6K4rTmBBTxBAHLOsiuZJHyJSb9ywxr7OgC9fOxgrYuCQuM2+UnHeJnXXwzsvaMzvbIIRyclmR8gS/DYQAR2hjDxVaQoeb+cTdKTSxE3RJS27YImJOsncmDjc4XFxwQsin7GUX5CMjjhGqrdH2RNrU1OMeTd9AdQrWW1BXKZeeJ1DQ+Z4IiZW68aQMYKbllTqx7MBEyGtBWSDYx4Jj/j1xPRhcbLBPCTMR+fmg0ncqk6a7HAgm41gK5paGQO/2iQ7I5T8ISR63IJG8QESWNkdStzLJSHfT0bDCLvFPM9E+IdwmqjN9+Uhl0T2lcE4FC/mFVTFP+cIPs/FrKELIqjhn9ihDS2iAQeGgUxfq1RSLkG4rX7HnX4pNROgH/1thXlBGA6SOJuFQ1gl0nzHTEObpEPLj4MMfbfnGScj3cKKxhUT3ygdYYwW662C0ETj6xOIQ8S97EDLREaGjQaXuuDUV3j+vqHWDqr3jqm641lfcVIW6V2hdB2Ua9F7L7871qOo7Rur5o+Tg0QeNxrS4tRU+1BXv6gMfDRG1D0jcsL8K3s8c8lX3CUWH60B/e1bjDz/X1qUHeBI7AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/148c667853efe4035468022995a91d88/263a4/generic3.webp 480w,
/static/148c667853efe4035468022995a91d88/a6361/generic3.webp 960w,
/static/148c667853efe4035468022995a91d88/62b89/generic3.webp 1886w&quot; sizes=&quot;(max-width: 1886px) 100vw, 1886px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/148c667853efe4035468022995a91d88/9aebd/generic3.png 480w,
/static/148c667853efe4035468022995a91d88/a91f8/generic3.png 960w,
/static/148c667853efe4035468022995a91d88/1fa94/generic3.png 1886w&quot; sizes=&quot;(max-width: 1886px) 100vw, 1886px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/148c667853efe4035468022995a91d88/1fa94/generic3.png&quot; alt=&quot;extends 키워드로 제네릭 타입 T의 범위를 특정 인터페이스의 서브타입으로 제한하는 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;[그림 3: 제네릭 제약으로 타입 범위 한정]&lt;/figcaption&gt;
&lt;br&gt;
&lt;h1 id=&quot;제네릭과-서브타입의-만남---변성-입문&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%84%A4%EB%A6%AD%EA%B3%BC-%EC%84%9C%EB%B8%8C%ED%83%80%EC%9E%85%EC%9D%98-%EB%A7%8C%EB%82%A8---%EB%B3%80%EC%84%B1-%EC%9E%85%EB%AC%B8&quot; aria-label=&quot;제네릭과 서브타입의 만남   변성 입문 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;제네릭과 서브타입의 만남 - 변성 입문&lt;/h1&gt;
&lt;p&gt;지금까지 제네릭의 기초를 살펴보았습니다. 이제 조금 더 깊이 들어가봅시다.&lt;/p&gt;
&lt;p&gt;지난 글에서 우리는 서브타입 관계를 배웠습니다. &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;의 서브타입입니다. &quot;오늘드림 상품은 상품이다&quot;가 성립하니까요.&lt;/p&gt;
&lt;p&gt;그렇다면 질문을 던져봅시다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;의 서브타입일 때, &lt;code class=&quot;language-text&quot;&gt;Array&amp;lt;TodayProduct&gt;&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;Array&amp;lt;Product&gt;&lt;/code&gt;의 서브타입일까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;직관적으로는 &quot;그렇다&quot;고 답하고 싶습니다. 오늘드림 상품의 배열은 상품의 배열이니까요.&lt;/p&gt;
&lt;p&gt;하지만 이 질문의 답은 생각보다 복잡합니다. 그리고 이 복잡함을 설명하는 개념이 바로 &lt;strong&gt;변성(Variance)&lt;/strong&gt;입니다.
&lt;br/&gt;&lt;/p&gt;
&lt;h2 id=&quot;변성의-세-가지-종류&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%80%EC%84%B1%EC%9D%98-%EC%84%B8-%EA%B0%80%EC%A7%80-%EC%A2%85%EB%A5%98&quot; aria-label=&quot;변성의 세 가지 종류 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;변성의 세 가지 종류&lt;/h2&gt;
&lt;p&gt;타입 &lt;code class=&quot;language-text&quot;&gt;A&lt;/code&gt;가 타입 &lt;code class=&quot;language-text&quot;&gt;B&lt;/code&gt;의 서브타입일 때, 제네릭 타입 &lt;code class=&quot;language-text&quot;&gt;F&amp;lt;A&gt;&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;F&amp;lt;B&gt;&lt;/code&gt;의 관계는 크게 세 가지로 나뉩니다. (타입스크립트에는 이변성이라는 네 번째 종류도 있는데, 뒤에서 다룹니다.)
&lt;br/&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-공변성-covariance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%B3%B5%EB%B3%80%EC%84%B1-covariance&quot; aria-label=&quot;1 공변성 covariance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 공변성 (Covariance)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A가 B의 서브타입이면, F&amp;#x3C;A&gt;도 F&amp;#x3C;B&gt;의 서브타입이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;타입 인자의 서브타입 관계가 &lt;strong&gt;그대로 유지&lt;/strong&gt;됩니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;TodayProduct &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product
     ↓ 관계 유지
&lt;span class=&quot;token builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;TodayProduct&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;직관과 일치하죠. 오늘드림 상품이 상품의 서브타입이니, 오늘드림 상품 배열도 상품 배열의 서브타입입니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1551px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4f04e02fd61dcd400ca015be9ae18623/71647/generic4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.70833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACL0lEQVR42nVUiXabMBD0/39hm7hJXBtjY0AXIEBC01nhq88Jz+sF7TU7WmmDH54QAoqiwOFwyHq/36Nt22xLKf0Uho21Fl3nYK2B1hqKQRJYVRXe39/x9vaG7XabZbfbrT5KwRgDS3HOoe/7rL332CzLAtf1sFzQxlIMFGUYBsQYr7LkIClu6GNdB0PpBs9E1F2XOxIgG4HZXk6oDp+oyz31F85890z43J44l8cC5+KLPh/ZX1dHXKoTmqaBABP0OWEayE3ogMlR94BXwOyfmLlyNtMWWKivKBd+S4y9e90RLu6C5CokW65aZHRPydL637dI5ohU/Uaq/yC1O6SuvpXLSFeEI6t4jejO6/vEynH+BiHR5cInRApMCQzqFWFi/yQBhgimWTZjRkwRS1r+nwnhk785TKgFGe0zfQN9pYdHwivxjhzZ2eFEjvZszQrSb555CWh8C0P+Du6IsqsyAK2umxIj8wfg43KEG0bQltElvA6w+FlOwEGfGbjG3p77Lo99ypva6B5DHzGSqhheUiFMCRNtXceZMwPCSOr7h4fM6ZpwSHkPeo0cIMkqo1G4EgXJL7kR+V1dMvpO8TA07CIiF79RfU/omXCexEiSx7QWYFLh8lezxaf6Cx98RijdDC7B6YXvBDA+EMrxW+cwSjskm0bRt3bHOMFwjIbg79MjNhFBJ/r5nsgIJet6RnnoNS8G1eYBres6a9VwjSLf69q6LiOitcpxkkMuC7k0/gH4oI1K9teFtwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4f04e02fd61dcd400ca015be9ae18623/263a4/generic4.webp 480w,
/static/4f04e02fd61dcd400ca015be9ae18623/a6361/generic4.webp 960w,
/static/4f04e02fd61dcd400ca015be9ae18623/c929e/generic4.webp 1551w&quot; sizes=&quot;(max-width: 1551px) 100vw, 1551px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4f04e02fd61dcd400ca015be9ae18623/9aebd/generic4.png 480w,
/static/4f04e02fd61dcd400ca015be9ae18623/a91f8/generic4.png 960w,
/static/4f04e02fd61dcd400ca015be9ae18623/71647/generic4.png 1551w&quot; sizes=&quot;(max-width: 1551px) 100vw, 1551px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4f04e02fd61dcd400ca015be9ae18623/71647/generic4.png&quot; alt=&quot;공변성: TodayProduct가 Product의 서브타입일 때 Array&lt;TodayProduct&gt;도 Array&lt;Product&gt;의 서브타입이 되는 관계&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;[그림 4: 공변성 - 서브타입 관계가 유지된다]&lt;/figcaption&gt;
&lt;br&gt;
&lt;h3 id=&quot;2-반공변성-contravariance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%B0%98%EA%B3%B5%EB%B3%80%EC%84%B1-contravariance&quot; aria-label=&quot;2 반공변성 contravariance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 반공변성 (Contravariance)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A가 B의 서브타입이면, F&amp;#x3C;B&gt;가 F&amp;#x3C;A&gt;의 서브타입이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;타입 인자의 서브타입 관계가 &lt;strong&gt;뒤집힙니다&lt;/strong&gt;.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;TodayProduct &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product
     ↓ 관계 역전
Consumer&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Consumer&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;TodayProduct&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Consumer&amp;lt;T&gt;는 T를 &quot;소비&quot;하는 타입, 즉 T를 매개변수로 받는 함수 타입입니다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// type Consumer&amp;lt;T&gt; = (item: T) =&gt; void&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;이건 좀 낯섭니다. 하지만 지난 글에서 이미 이 개념을 만났습니다. 기억하시나요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;함수 타입은 매개변수 타입의 서브타입 관계를 뒤집는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;바로 이것이 반공변성입니다. 상품을 처리하는 함수는 오늘드림 상품도 처리할 수 있기에, 상품 함수는 오늘드림 상품 함수의 서브타입이 됩니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1551px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9e927670d4e15fbe0551cbdd6137aed5/71647/generic5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACEklEQVR42m1UDZOiMAz1//9Cdxz3nLlDQb7aUooU2rcvQV3X2ziZGJo+kryEHd4k56x2mibs93t8fHzgcDioHo9H9eXsNfZVdtZaOOeoFn3fo+s6tG2Luq5RVRXKslQrftM0atu2YayBMUbvDoOH9179nTxIKeN8vujFy6VEURT6XzJ4ZCEvEjBjehhrCNhBklGfQCEEBWeGTi/MtsYSLOJI9T1m1/woRS4IwLU0qEtHHdBUA+rK6nM5H4YBO2MswxPyUAK2oP4F3Bnw1//6k1YgzgRnDj2PwwCsi+hWhQDvOvZNAceOaY5kwyLHQL9luekNMOMWMrzJMFdamzCFhCWmF8Cu3aIJsgwVgmQpwGnBb7Iww/kGtM5jnTeffH8DCqMPiSnCzZ46ovAVrmODRep8Hy3+6tDCxxEl4wq2K+Zl66GMwkNu64Ru6tiAFWEJqhnpnkF+ZiKAlbyMLZnWqMAiPwAjZ9X5GUXHwNujlF/k5pBvFmV/+u75fbRkFp8lTyMbTnUNLcHnsJIcErWQtNVQSe1Caj2nwV3YxM/NCnmSAWUUQBlQfTHZE/rFbmyu5IUlT3+oB4JfkBN98w+5OyE1n8jtSX2wbAUcR5lDo84aM+K9VDl/cpGYYWJWOdx9BqRps+y5UP0oWQFlpWTZ/ejZ1G2nZbVkpWS3+1523Onuqs8ERA1HxMo3gERI72RT5PwLZVI+gF7FKjwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9e927670d4e15fbe0551cbdd6137aed5/263a4/generic5.webp 480w,
/static/9e927670d4e15fbe0551cbdd6137aed5/a6361/generic5.webp 960w,
/static/9e927670d4e15fbe0551cbdd6137aed5/c929e/generic5.webp 1551w&quot; sizes=&quot;(max-width: 1551px) 100vw, 1551px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9e927670d4e15fbe0551cbdd6137aed5/9aebd/generic5.png 480w,
/static/9e927670d4e15fbe0551cbdd6137aed5/a91f8/generic5.png 960w,
/static/9e927670d4e15fbe0551cbdd6137aed5/71647/generic5.png 1551w&quot; sizes=&quot;(max-width: 1551px) 100vw, 1551px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9e927670d4e15fbe0551cbdd6137aed5/71647/generic5.png&quot; alt=&quot;반공변성: TodayProduct가 Product의 서브타입일 때 Consumer&lt;Product&gt;가 Consumer&lt;TodayProduct&gt;의 서브타입이 되어 관계가 역전됨&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;[그림 5: 반공변성 - 서브타입 관계가 뒤집힌다]&lt;/figcaption&gt;
&lt;br&gt;
&lt;h3 id=&quot;3-불변성-invariance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%B6%88%EB%B3%80%EC%84%B1-invariance&quot; aria-label=&quot;3 불변성 invariance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 불변성 (Invariance)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A와 B가 다른 타입이면, F&amp;#x3C;A&gt;와 F&amp;#x3C;B&gt; 사이에 서브타입 관계가 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;정확히 같은 타입만 허용됩니다. 서브타입도 안 되고, 슈퍼타입도 안 됩니다. 오늘드림 상품과 상품은 엄연히 다른 타입이기에, 둘 사이에는 어떤 서브타입 관계도 인정하지 않습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1551px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/71647/generic6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB/ElEQVR42m1UaXPrIAzM//+FmU6mH5o6dQ4fYBvMuV0pR19ewowGDNKyWglv8GbUWnX23mO73eLj4wO73U5n59yTz/9jM44jrLVq4zig73t0Xad2Op3Qtq2arM/nMy6Xi/oMwwCJNcZgmiY1+d4IUCkVTXPQwKZpsN/v8fPTPjEJIei5XCRgAi4AdxLiJ6AbY+w1wJ6R1/lq3iLN/YsMEtAx+EyWx+OJQIPaPM/qo4DjaLgsqBMZmQYY95wPPD2+6FNKwboGhJgwMC7EiJXMU856LulvetJXwIWMAm8iO0SH6jqyKi+AhhL1g0HPzAR05FyeAKmJDoIk28IJS67p9baKmcGipyeRkAtilkvrH2B3BxTMkmDJUqyhBMflhMS9t8Df36gsSmZxUityGVjRUNrgPnzyuLgemam65OCiv1b5bo/UK+LhgLKuqLTCfgX1nP4FZCxvCGj6jpUG0vqWmBbFE8QvDiuL40N8tJZU+5HyulQEz9ZgbWLgmoCV+hRrUJhaubWGgImOxk5IKWuVY0p/gNKkounqKnKqBC4M4EzGeV4Qv74Q+Ozi56em6AmwkF3fj1icf2qbZVmuT09FjlXTjpJuvBVZHBkg+jBX3cxsnZzk0qJAMt9TfgBKGzjvdGOep9vbtFo1yzQeJt+3dy8+kqLEyA9j5YXSNr+CEUBTUZvXmQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/263a4/generic6.webp 480w,
/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/a6361/generic6.webp 960w,
/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/c929e/generic6.webp 1551w&quot; sizes=&quot;(max-width: 1551px) 100vw, 1551px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/9aebd/generic6.png 480w,
/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/a91f8/generic6.png 960w,
/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/71647/generic6.png 1551w&quot; sizes=&quot;(max-width: 1551px) 100vw, 1551px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c2167cbe7f1ff80c2dc2ab4e183e2b4a/71647/generic6.png&quot; alt=&quot;불변성: A와 B가 다른 타입이면 F&lt;A&gt;와 F&lt;B&gt; 사이에 어떤 서브타입 관계도 성립하지 않음&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;[그림 6: 불변성 - 서브타입 관계가 없다]&lt;/figcaption&gt;
&lt;br&gt;
&lt;h2 id=&quot;왜-변성이-중요한가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-%EB%B3%80%EC%84%B1%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%9C%EA%B0%80&quot; aria-label=&quot;왜 변성이 중요한가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 변성이 중요한가?&lt;/h2&gt;
&lt;p&gt;변성이 왜 필요한지, 배열을 예로 들어 살펴봅시다.&lt;/p&gt;
&lt;p&gt;배열이 공변적이라고 가정해봅시다. 즉, &lt;code class=&quot;language-text&quot;&gt;Array&amp;lt;TodayProduct&gt;&lt;/code&gt;가 &lt;code class=&quot;language-text&quot;&gt;Array&amp;lt;Product&gt;&lt;/code&gt;의 서브타입입니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; todayProducts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TodayProduct&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;탈모 샴푸&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;25000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isTodayDelivery&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;마스크팩&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isTodayDelivery&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 공변성에 의해 이 할당이 허용된다고 가정&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; products&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; todayProducts&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;여기까지는 문제없어 보입니다. 읽기만 한다면요.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 읽기 - 안전함&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;하지만 쓰기를 하면 어떨까요?
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 쓰기 - 위험!&lt;/span&gt;
products&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;일반 상품&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// isTodayDelivery 속성이 없는 일반 Product&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// todayProducts 배열에 TodayProduct가 아닌 값이 들어감!&lt;/span&gt;
todayProducts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isTodayDelivery&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// undefined - 런타임 에러 가능!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;products&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;todayProducts&lt;/code&gt;는 같은 배열을 참조합니다. &lt;code class=&quot;language-text&quot;&gt;products&lt;/code&gt;를 통해 일반 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;를 추가하면, &lt;code class=&quot;language-text&quot;&gt;todayProducts&lt;/code&gt;에도 그 값이 들어갑니다. 하지만 &lt;code class=&quot;language-text&quot;&gt;todayProducts&lt;/code&gt;는 모든 요소가 &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;라고 믿고 있죠. 타입 시스템이 깨지는 순간입니다.&lt;/p&gt;
&lt;p&gt;이것이 변성이 중요한 이유입니다. &lt;strong&gt;읽기만 하는 컨테이너&lt;/strong&gt;는 공변적이어도 안전하지만, &lt;strong&gt;쓰기도 하는 컨테이너&lt;/strong&gt;는 공변적이면 위험합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;타입스크립트의-변성-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%9D%98-%EB%B3%80%EC%84%B1-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;타입스크립트의 변성 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;타입스크립트의 변성 처리&lt;/h1&gt;
&lt;p&gt;타입스크립트는 이 복잡한 변성을 어떻게 처리할까요? 타입 안전성과 실용성 사이에서 고민한 타입스크립트만의 현실적인 절충안을 여기서 엿볼 수 있습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;h2 id=&quot;배열은-공변적으로-처리된다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%EC%97%B4%EC%9D%80-%EA%B3%B5%EB%B3%80%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%B2%98%EB%A6%AC%EB%90%9C%EB%8B%A4&quot; aria-label=&quot;배열은 공변적으로 처리된다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배열은 공변적으로 처리된다&lt;/h2&gt;
&lt;p&gt;앞서 살펴본 것처럼, 쓰기가 가능한 배열이 공변적인 것은 타입 안전하지 않습니다(unsound). &lt;code class=&quot;language-text&quot;&gt;todayProducts&lt;/code&gt; 배열을 &lt;code class=&quot;language-text&quot;&gt;products&lt;/code&gt;로 참조한 뒤 일반 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;를 추가하면, 타입 시스템이 보장하던 안전성이 깨지죠.&lt;/p&gt;
&lt;p&gt;그럼에도 타입스크립트는 배열을 공변적으로 처리합니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printProductTitles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;products&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  products&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; todayProducts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TodayProduct&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printProductTitles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;todayProducts&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK - 허용됨&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;왜일까요? &lt;strong&gt;실용성&lt;/strong&gt; 때문입니다.&lt;/p&gt;
&lt;p&gt;위 코드처럼 배열을 읽기 전용으로 사용하는 패턴은 자바스크립트에서 매우 흔합니다. 이 패턴을 에러로 처리하면 수많은 정상적인 코드가 컴파일되지 않습니다.&lt;/p&gt;
&lt;p&gt;타입스크립트는 완벽한 타입 안전성보다 실용성을 선택했습니다. 대신 개발자가 위험한 쓰기를 하지 않을 것이라고 신뢰합니다.
&lt;br/&gt;&lt;/p&gt;
&lt;h2 id=&quot;함수-타입의-변성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%A8%EC%88%98-%ED%83%80%EC%9E%85%EC%9D%98-%EB%B3%80%EC%84%B1&quot; aria-label=&quot;함수 타입의 변성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;함수 타입의 변성&lt;/h2&gt;
&lt;p&gt;배열에서는 실용성을 위해 공변성을 허용했습니다. 그렇다면 함수 타입은 어떨까요? 함수 타입은 좀 더 엄격합니다. &lt;code class=&quot;language-text&quot;&gt;strictFunctionTypes&lt;/code&gt; 옵션이 켜져 있다면 타입스크립트는 다음과 같이 동작합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;반환 타입&lt;/strong&gt;: 공변적 (서브타입 관계 유지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;매개변수 타입&lt;/strong&gt;: 반공변적 (서브타입 관계 역전)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;매개변수 반공변성이 낯설게 느껴진다면, 지난 글의 예제를 떠올려보세요. &quot;함수 타입은 매개변수 타입의 서브타입 관계를 뒤집는다&quot;고 했었죠. 왜 그래야 하는지, 만약 공변적이라면 어떤 일이 벌어지는지 살펴봅시다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TodayProductHandler&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TodayProduct&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// TodayProduct의 isTodayDelivery 속성에 접근하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; processTodayProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;TodayProductHandler&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;오늘 배송 여부: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;isTodayDelivery&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 만약 매개변수가 공변적이라면, 이 할당이 허용될 것입니다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// (TodayProduct &amp;lt;: Product 이므로 TodayProductHandler &amp;lt;: ProductHandler?)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; processProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProductHandler &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; processTodayProduct&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 실제로는 에러!&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 일반 Product를 전달하면?&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;processProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;일반 상품&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// isTodayDelivery 속성이 없음 - 런타임 에러!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;processTodayProduct&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;isTodayDelivery&lt;/code&gt; 속성이 있다고 가정하고 동작합니다. 그런데 일반 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;에는 이 속성이 없죠. 매개변수가 공변적이면 타입 시스템을 통과하지만 런타임에 실패합니다.&lt;/p&gt;
&lt;p&gt;반대 방향은 안전합니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductHandler&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TodayProductHandler&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TodayProduct&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Product의 공통 속성만 사용하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;ProductHandler&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;상품명: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, 가격: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;price&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 반공변성: ProductHandler를 TodayProductHandler에 할당 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; handleTodayProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TodayProductHandler &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; handleProduct&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK!&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// TodayProduct를 전달해도 안전&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;handleTodayProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;샴푸&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isTodayDelivery&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// title과 price는 확실히 존재함&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;handleProduct&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;의 속성만 사용합니다. &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;의 모든 속성을 가지고 있으므로, &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;를 전달해도 문제없이 동작합니다. 이것이 매개변수 반공변성이 타입 안전한 이유입니다.&lt;/p&gt;
&lt;p&gt;반환 타입은 직관대로 공변적입니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;GetProduct&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;GetTodayProduct&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; TodayProduct&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getTodayProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;GetTodayProduct&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;샴푸&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; isTodayDelivery&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 공변성: GetTodayProduct를 GetProduct에 할당 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GetProduct &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; getTodayProduct&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK!&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; product &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// TodayProduct도 title이 있으므로 안전&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;getTodayProduct&lt;/code&gt;가 반환하는 &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;의 모든 속성을 포함합니다. &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;를 기대하는 곳에서 사용해도 안전하죠.
&lt;br/&gt;&lt;/p&gt;
&lt;h2 id=&quot;메서드-vs-함수-프로퍼티&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%94%EC%84%9C%EB%93%9C-vs-%ED%95%A8%EC%88%98-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0&quot; aria-label=&quot;메서드 vs 함수 프로퍼티 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;메서드 vs 함수 프로퍼티&lt;/h2&gt;
&lt;p&gt;함수 타입에서 매개변수는 반공변적이라고 했습니다. 그런데 타입스크립트에서는 같은 함수라도 어떻게 정의하느냐에 따라 이 규칙이 다르게 적용됩니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 메서드 문법 - 이변성(bivariant)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 함수 프로퍼티 문법 - strictFunctionTypes에서 반공변성&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Product&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;메서드는 &lt;strong&gt;이변성(bivariance)&lt;/strong&gt;으로 처리됩니다. 양방향 할당이 모두 허용된다는 뜻입니다. 이것도 실용성을 위한 선택입니다. 기존 자바스크립트 코드와의 호환성, 그리고 일반적인 객체 지향 패턴을 지원하기 위해서입니다.&lt;/p&gt;
&lt;p&gt;더 엄격한 타입 검사를 원한다면 메서드 대신 함수 프로퍼티 문법을 사용하세요.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;실전-활용---유틸리티-타입-이해하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9---%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%ED%83%80%EC%9E%85-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0&quot; aria-label=&quot;실전 활용   유틸리티 타입 이해하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실전 활용 - 유틸리티 타입 이해하기&lt;/h1&gt;
&lt;p&gt;제네릭의 강력함을 가장 잘 보여주는 것이 타입스크립트의 내장 유틸리티 타입들입니다. 이들이 어떻게 구현되어 있는지 살펴보면 제네릭에 대한 이해가 깊어집니다. (더 자세한 내용은 &lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/utility-types.html&quot;&gt;타입스크립트 공식 핸드북 - Utility Types&lt;/a&gt;를 참고하세요.)
&lt;br/&gt;&lt;/p&gt;
&lt;h2 id=&quot;partialt&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#partialt&quot; aria-label=&quot;partialt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Partial&amp;#x3C;T&gt;&lt;/h2&gt;
&lt;p&gt;모든 속성을 선택적으로 만드는 유틸리티입니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 구현 원리 - &quot;객체의 모든 키를 순회하며 새 타입을 만든다&quot; 정도로 이해하면 됩니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MyPartial&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;K&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용 예시 - 올리브영 상품 수정 폼&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// (설명을 위해 간략화된 Product 타입)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 수정 시에는 일부 필드만 보내도 됨&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductUpdateInput&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Partial&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// { title?: string; price?: number; description?: string; }&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;updateProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; updates&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Partial&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 전달된 필드만 업데이트&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;updateProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;123&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;25000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK - price만 업데이트&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;h2 id=&quot;pickt-k와-omitt-k&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pickt-k%EC%99%80-omitt-k&quot; aria-label=&quot;pickt k와 omitt k permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pick&amp;#x3C;T, K&gt;와 Omit&amp;#x3C;T, K&gt;&lt;/h2&gt;
&lt;p&gt;특정 속성만 선택하거나 제외합니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 상품 목록에서는 일부 정보만 보여줌&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductListItem&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Pick&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;price&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// { title: string; price: number; }&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 상품 생성 시에는 id를 제외&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductCreateInput&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Omit&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;h2 id=&quot;올리브영-맞춤-유틸리티-타입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%A7%9E%EC%B6%A4-%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%ED%83%80%EC%9E%85&quot; aria-label=&quot;올리브영 맞춤 유틸리티 타입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영 맞춤 유틸리티 타입&lt;/h2&gt;
&lt;p&gt;내장 유틸리티를 참고하여 올리브영에 필요한 유틸리티 타입도 만들 수 있습니다.
&lt;br/&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 할인 적용 결과 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Discounted&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; HasPrice&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  originalPrice&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  discountRate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; discountedProduct&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Discounted&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;product&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  originalPrice&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; product&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;price&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  discountRate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;


&lt;span class=&quot;token comment&quot;&gt;// API 응답에서 데이터만 추출&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ExtractData&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;infer&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;U&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;U&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;never&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductData&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ExtractData&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ApiResponse&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Product&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Product&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;위 &lt;code class=&quot;language-text&quot;&gt;ExtractData&lt;/code&gt; 타입에서 사용된 &lt;code class=&quot;language-text&quot;&gt;infer&lt;/code&gt; 키워드는 조건부 타입에서 타입을 추론하는 데 사용됩니다. 지금은 &quot;이런 것도 가능하구나&quot; 정도로 넘어가도 괜찮습니다. 조건부 타입과 &lt;code class=&quot;language-text&quot;&gt;infer&lt;/code&gt;는 다음 글에서 더 자세히 다루겠습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h1&gt;
&lt;p&gt;이번 글에서 우리는 제네릭과 매개변수에 의한 다형성을 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;서브타입에 의한 다형성&lt;/strong&gt;은 &quot;A는 B이다&quot;라는 관계로 타입 간 호환성을 만들었습니다. &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;이므로 &lt;code class=&quot;language-text&quot;&gt;Product&lt;/code&gt;가 필요한 곳에 &lt;code class=&quot;language-text&quot;&gt;TodayProduct&lt;/code&gt;를 쓸 수 있었죠.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;매개변수에 의한 다형성&lt;/strong&gt;은 타입 자체를 추상화합니다. &lt;code class=&quot;language-text&quot;&gt;T&lt;/code&gt;라는 타입 변수를 사용하여, 어떤 타입이든 받을 수 있는 범용적인 코드를 작성합니다. 서브타입 관계 없이도 같은 로직을 여러 타입에 적용할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;strong&gt;변성(Variance)&lt;/strong&gt;을 통해 두 다형성이 만났을 때 어떤 일이 일어나는지 이해했습니다. 타입 인자의 서브타입 관계가 제네릭 타입에 어떻게 전파되는지, 그리고 타입스크립트가 실용성과 타입 안전성 사이에서 어떤 선택을 했는지 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;올리브영에서도 제네릭은 코드 품질 향상에 크게 기여하고 있습니다. &lt;code class=&quot;language-text&quot;&gt;ApiResponse&amp;lt;T&gt;&lt;/code&gt;로 수십 개의 API 응답 타입을 하나로 통합했고, 상품·리뷰·주문 등 서로 다른 도메인의 목록을 처리하는 공통 유틸리티 함수들도 제네릭으로 구현되어 있습니다. 덕분에 새로운 도메인이 추가되어도 기존 유틸리티를 그대로 재사용할 수 있고, 타입 안전성은 그대로 유지됩니다.&lt;/p&gt;
&lt;p&gt;다음 글에서는 &lt;strong&gt;조건부 타입(Conditional Types)&lt;/strong&gt;과 타입 추론의 심화 내용을 다루어볼 예정입니다. 타입 수준에서 조건을 분기하고, 복잡한 타입을 변환하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;reference&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#reference&quot; aria-label=&quot;reference permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/2/generics.html&quot;&gt;https://www.typescriptlang.org/docs/handbook/2/generics.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/docs/handbook/type-compatibility.html&quot;&gt;https://www.typescriptlang.org/docs/handbook/type-compatibility.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/microsoft/TypeScript/wiki/FAQ&quot;&gt;https://github.com/microsoft/TypeScript/wiki/FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://toss.tech/article/typescript-type-compatibility&quot;&gt;https://toss.tech/article/typescript-type-compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.hjaem.info/46&quot;&gt;https://blog.hjaem.info/46&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[SQS 기반 알림톡 처리에서 발생한 DB 커넥션 데드락 분석기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-12-30/alimtalk_improve_event_driven_architecture/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-30/alimtalk_improve_event_driven_architecture/</guid><pubDate>Tue, 30 Dec 2025 17:30:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;들어가며&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요. 올리브영 클레임 스쿼드에서 백엔드 담당하고 있는 바로바로입니다.&lt;/p&gt;
&lt;p&gt;올리브영에서는 고객분들에게 주문 완료, 배송 완료, 반품 완료 등 다양한 알림톡이 발송되고 있습니다.&lt;/p&gt;
&lt;p&gt;저희 클레임 스쿼드는 이번에 여러 서비스에 흩어져 있던 알림톡 발송 로직을 정리하고, 이를 &lt;strong&gt;이벤트 기반 구조로 사내에 새롭게 구축된 Messaging-API 호출로 전환하는 작업을 진행&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;하지만 진정한 개선은 기능을 완성했을 때가 아니라, 배포 후 마주한 데드락 문제를 해결하며 시스템의 한계치를 정확히 이해했을 때 이루어졌습니다. 30,000ms의 타임아웃 에러를 0으로 만들기까지, SQS와 트랜잭션 설정을 꼼꼼하게 튜닝했던 기록을 정리했습니다.&lt;/p&gt;
&lt;h2 id=&quot;기존-알림톡-구조의-문제점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A1%B4-%EC%95%8C%EB%A6%BC%ED%86%A1-%EA%B5%AC%EC%A1%B0%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90&quot; aria-label=&quot;기존 알림톡 구조의 문제점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기존 알림톡 구조의 문제점&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;기존 알림톡 발송 구조에서는 반품 완료, 스마트반품 접수 등 각 비즈니스 로직마다 알림톡 전송 로직이 개별적으로 구현되어 있어, &lt;strong&gt;공통 로직을 수정하거나 정책을 변경할 때 여러 코드를 동시에 수정해야 하는 문제&lt;/strong&gt;가 있었습니다.&lt;/p&gt;
&lt;p&gt;이로 인해 유지보수가 점점 어려워지고, 변경에 따른 사이드 이펙트 발생 가능성도 높아졌습니다.&lt;/p&gt;
&lt;p&gt;또한 알림톡 전송이 실패했을 경우 이를 자동으로 재처리할 수 있는 &lt;strong&gt;별도의 재전송 프로세스가 마련되어 있지 않아, 장애 상황에서는 운영자가 직접 개입해야 하는 한계&lt;/strong&gt;가 있었습니다.&lt;/p&gt;
&lt;p&gt;기술적으로는 알림톡 발송 로직이 비즈니스 트랜잭션 내에 강하게 결합되어 있다는 점이 가장 큰 문제였습니다. &lt;strong&gt;외부 API 호출 지연이나 네트워크 이슈가 발생할 경우 비즈니스 트랜잭션 전체가 대기 상태에 빠지게 되며&lt;/strong&gt;, 이는 곧 시스템 전체의 처리량 저하로 이어질 수 있었습니다.&lt;/p&gt;
&lt;p&gt;특히 대량의 알림톡을 배치로 발송할 때는 &lt;strong&gt;모놀리식 애플리케이션에 부하가 집중되어 시스템 전반의 안정성을 위협&lt;/strong&gt;하는 구조였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;sqs-기반-이벤트-알림-구조-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#sqs-%EA%B8%B0%EB%B0%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%95%8C%EB%A6%BC-%EA%B5%AC%EC%A1%B0-%EB%8F%84%EC%9E%85&quot; aria-label=&quot;sqs 기반 이벤트 알림 구조 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;SQS 기반 이벤트 알림 구조 도입&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;저희는 기존 알림톡 구조를 개선하기 위해 &lt;strong&gt;Amazon SQS&lt;/strong&gt; 기반의 이벤트 알림 구조 도입을 검토했습니다.&lt;br&gt;이에 따라 SQS 중심으로 알림 발송 흐름을 재설계했고, 아래와 같이 구성도를 설계했습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-구성도-설계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%B5%AC%EC%84%B1%EB%8F%84-%EC%84%A4%EA%B3%84&quot; aria-label=&quot;1 구성도 설계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;1. 구성도 설계&lt;/strong&gt;&lt;/h3&gt;
&lt;br&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1622px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/79015df9c046e269690b0244cb8d58db/79f3a/image.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 43.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABxElEQVR42jWSSXPTQBCF9b85c+EXcOeUAgo4kCJULpwoUgGDs+EEY0WbF9mStdiytX280YQqdU1P9/Tr16/l5EdIKkhlxp/vINpDqDM92JjJZ/K38iPFQ+UXsrKGfQdVZ/2VYs6VD+++w4eRHmUCUdE4bJisWo4tfL6Gt5c2XyrnZR0//CNrNWiPOY9XZ4RBSNc2ZAJ1/AQuXRh5AlOHtdj8nhe4yYGDAG8juJjBLzWuGvC3DTfBdnjXdw27bEFVVfR9P0zk1KKrbzBD24zsFzBNxVi+YbIxJoClGbfUGzWOdEZFi5sas+OamLNMOm7cmjuvoax6FXX89Cse4qMt2tkmg7a6h4UB7QfgR3W51zQzAS4FGBjA2w18/AufNFas4KpsuYsypvFuYLzIYbbW462dwNd9mlhpCmn6J4bJUrVqtDUapu+/Mnv2Cu/5CfuHiI1mn4vBQgV7afZlAqdjOLu2S3GTmm/TXBsXSzE7uYDXWtrYt9t28vMR0Ys3LF+ecvBiYoEEeSf6PfkTQ1eMQjEsatvMyxrip9/MVzwQcKLpEt0dTYjkGcysfdCptGdQWG2M4Ibx/7jR1ehl/lWTNzbECvgHs8arZtclOy0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/79015df9c046e269690b0244cb8d58db/263a4/image.webp 480w,
/static/79015df9c046e269690b0244cb8d58db/a6361/image.webp 960w,
/static/79015df9c046e269690b0244cb8d58db/a713d/image.webp 1622w&quot;
              sizes=&quot;(max-width: 1622px) 100vw, 1622px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/79015df9c046e269690b0244cb8d58db/9aebd/image.png 480w,
/static/79015df9c046e269690b0244cb8d58db/a91f8/image.png 960w,
/static/79015df9c046e269690b0244cb8d58db/79f3a/image.png 1622w&quot;
            sizes=&quot;(max-width: 1622px) 100vw, 1622px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/79015df9c046e269690b0244cb8d58db/79f3a/image.png&quot;
            alt=&quot;테스트&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;이벤트 기반 알림 발송 시스템 구성도&lt;/figcaption&gt;
&lt;p&gt;위 구성도를 바탕으로 한 알림톡 발송 프로세스는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트리거: 각 비즈니스 로직(반품 완료 등)에서 알림 발송 API를 호출합니다.&lt;/li&gt;
&lt;li&gt;메시지 발행: 발송 API는 알림에 필요한 최소한의 데이터를 담아 SQS 메시지를 발행합니다.&lt;/li&gt;
&lt;li&gt;이벤트 소비: 컨슈머(Consumer)에서 발행된 메시지를 획득합니다.&lt;/li&gt;
&lt;li&gt;최종 발송: 컨슈머는 알림톡 종류에 따라 필요한 데이터를 가공한 뒤, Messaging API를 호출하여 최종 발송 처리를 완료합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;발송 실패 시에는 실패 로그를 DB에 적재하며, Amazon EventBridge를 통해 주기적으로 재시도 API를 호출하여 위 프로세스를 반복하게 됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Amazon EventBridge를 활용한 주기적 재시도 처리&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;알림톡 발송은 외부 API 호출을 동반하므로 네트워크 이슈 등에 의한 일시적 실패가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이런 경우에도 메시지가 유실되지 않도록 &lt;strong&gt;실패 이력을 테이블에 저장하고&lt;/strong&gt;, EventBridge가 주기적으로 재시도 API를 호출해 해당 건을 &lt;strong&gt;재발송&lt;/strong&gt;하도록 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DLQ(Dead Letter Queue)&lt;/strong&gt; 방식도 고려했지만, 실패 원인 추적과 개별 재처리 제어가 어렵고 운영 가시성이 낮아, &lt;br&gt;대신 DB 기반 이력 관리 + EventBridge 재시도 구조를 선택했습니다. 이를 통해 운영자가 직접 실패 건을 조회·분석하고 유연하게 대응할 수 있는 운영 가시성을 확보했습니다.&lt;/p&gt;
&lt;aside&gt;
💡
DLQ: SQS에서 설정된 최대 재시도 횟수를 초과한 메시지를 별도 큐에 보관하는 기능으로, 메시지 유실 방지에는 유용하지만 세부 제어(원인 분석, 개별 재처리)는 직접 구현이 필요합니다.
&lt;/aside&gt;
&lt;br&gt;&lt;br&gt;&lt;br&gt;
&lt;h3 id=&quot;2-알림톡-발송-로직-공통화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%95%8C%EB%A6%BC%ED%86%A1-%EB%B0%9C%EC%86%A1-%EB%A1%9C%EC%A7%81-%EA%B3%B5%ED%86%B5%ED%99%94&quot; aria-label=&quot;2 알림톡 발송 로직 공통화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;2. 알림톡 발송 로직 공통화&lt;/strong&gt;&lt;/h3&gt;
&lt;br&gt;
&lt;p&gt;기존에 각 업무 로직 내부에 흩어져 있던 알림톡 전송 코드를 아래와 같이 &lt;strong&gt;공통 인터페이스 기반 구조&lt;/strong&gt; (&lt;strong&gt;NoticeSender&lt;/strong&gt;)로 표준화했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//알림톡 SQS 메시지 Listener&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Listener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; noticeService&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoticeService
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@SqsListener&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;consumeMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; queueMessage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; mapper&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; QueueMessage&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            noticeService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;handleNotice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//알림 핸들러 (다양한 알림톡 분기)&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NoticeHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; noticeSenders&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; NoticeSender&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleNotice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueMessage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; QueueMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sender &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueMessage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;noticeType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 반품 완료 알림톡&lt;/span&gt;
            RETURN_COMPLETE &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; noticeSenders&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;returnComplete&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 교환 철회 알림톡&lt;/span&gt;
            EXCHANGE_CANCEL &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; noticeSenders&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;exchangeCancel&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// ...그 밖의 알림톡 발송&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IllegalArgumentException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;지원하지 않는 알림톡 타입이거나 Sender가 등록되지 않았습니다. noticeType=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;queueMessage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;noticeType&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//실제 알림톡 전송 프로세스 Sender&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; NoticeSender &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//알림톡 전송 시작&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueMessage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; QueueMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//공통 DTO&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; noticeSenderDto &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NoticeSenderDto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueMessage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; queueMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//검증 및 관련 데이터 셋&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;validateAndDataPut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//템플릿 생성&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;createTemplate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
       &lt;span class=&quot;token comment&quot;&gt;//Messaging API Request 생성&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;createMessagingRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//Messaging API&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;callMessageApi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//알림 내역 저장&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;saveNoticeMessageInfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//검증 및 데이터 PUT&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;validateAndDataPut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//템플릿 생성&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createTemplate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; String&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//messaging API Request 생성&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createMessagingRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageApiRequest
    &lt;span class=&quot;token comment&quot;&gt;//Messaging API 호출&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;callMessageApi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//알림 내역 저장&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;saveNoticeMessageInfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;noticeSenderDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoticeSenderDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;//...공통 구현체&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;알림톡 발송 로직 공통 인터페이스&lt;/figcaption&gt;
&lt;p&gt;이를 통해 알림 발송 흐름(&lt;strong&gt;검증 → 템플릿 생성 → 메시지 생성 → 발송 → 저장&lt;/strong&gt;)을 하나의 파이프라인으로 일원화하고, 각 알림 유형은 별도의 Sender 구현체만 추가하면 동작하도록 설계했습니다.&lt;/p&gt;
&lt;p&gt;이렇게 개선함으로써 알림 발송 로직이 &lt;strong&gt;트랜잭션 처리와 분리&lt;/strong&gt;되어 성능 저하나 커넥션 점유의 위험을 줄일 수 있었으며, 새로운 알림이 추가될 때도 기존 코드를 수정하거나 의존성을 변경할 필요 없이, 각 알림이 독립적으로 &lt;strong&gt;확장·관리&lt;/strong&gt;될 수 있는 구조를 만들었습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-트랜잭션-결합-해소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%B0%ED%95%A9-%ED%95%B4%EC%86%8C&quot; aria-label=&quot;3 트랜잭션 결합 해소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;3. 트랜잭션 결합 해소&lt;/strong&gt;&lt;/h3&gt;
&lt;br&gt;
&lt;p&gt;기존 구조에서는 알림톡 발송이 비즈니스 트랜잭션 내부에서 함께 처리되면서, 알림 전송 성공 여부와 관계없이 트랜잭션이 알림 발송 완료 시점까지 유지되는 구조였습니다.
이로 인해 &lt;strong&gt;외부 Messaging API 호출 지연이나 실패가 발생할 경우에도 트랜잭션이 계속 유지되며, 커넥션 점유 시간이 불필요하게 길어지는 문제가 발생할 수 있었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;SQS 기반 이벤트 구조로 전환한 이후에는, 비즈니스 로직이 알림톡 발송 요청을 SQS에 발행하는 시점까지만 책임지고 즉시 다음 프로세스로 진행하도록 변경되었습니다.&lt;/p&gt;
&lt;p&gt;실제 알림톡 발송 여부나 처리 결과는 Consume 서버에서 비동기적으로 처리되기 때문에, &lt;strong&gt;발송 성공·실패와 관계없이 비즈니스 트랜잭션은 빠르게 종료됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이를 통해 트랜잭션 경합과 커넥션 점유를 줄이고, 알림 발송 처리량이 증가하더라도 비즈니스 로직에 미치는 영향을 최소화할 수 있는 구조를 만들 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;배포-후-db-커넥션-데드락&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%ED%8F%AC-%ED%9B%84-db-%EC%BB%A4%EB%84%A5%EC%85%98-%EB%8D%B0%EB%93%9C%EB%9D%BD&quot; aria-label=&quot;배포 후 db 커넥션 데드락 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배포 후 DB 커넥션 데드락&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;배포 후 안정적으로 알림톡들이 발행되고 한숨을 돌리려던 찰나… 아래의 오류가 순간 대량 발생한 것을 모니터링을 통해 확인했습니다.&lt;/p&gt;
&lt;aside&gt;
💡
`java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30008ms.`
&lt;/aside&gt;
&lt;p&gt;&lt;br&gt;이는 &lt;strong&gt;트랜잭션이 커넥션을 점유한 상태에서 추가 커넥션을 확보하지 못하여&lt;/strong&gt;, 대기 시간을 초과했기 때문에 타임아웃이 발생한 에러였습니다.&lt;/p&gt;
&lt;p&gt;우리는 해당 오류에 즉시 대응했습니다. 오류와 상황을 분석해 보니 자원 경합에 의한 데드락이었어요. 단일 원인이 아닌, 아래 세 가지 요소가 복합적으로 맞물리며 발생했습니다.&lt;/p&gt;
&lt;aside&gt;
&lt;ul&gt;
&lt;li&gt;SQS 폴링(Polling) 방식의 병렬 처리 특성&lt;/li&gt;
&lt;li&gt;HikariCP &lt;code class=&quot;language-text&quot;&gt;maximumPoolSize&lt;/code&gt; 설정의 한계&lt;/li&gt;
&lt;li&gt;트랜잭션 전파 옵션 &lt;code class=&quot;language-text&quot;&gt;REQUIRES_NEW&lt;/code&gt;에 의한 커넥션 중첩 점유&lt;/li&gt;
&lt;/ul&gt;
&lt;/aside&gt;
&lt;br&gt;
&lt;p&gt;그렇다면 위의 시스템 구성 요소가 어떤 메커니즘을 통해 &lt;strong&gt;데드락을 유발&lt;/strong&gt;했을까요? 내부 라이브러리 코드와 예제를 통해 상세히 살펴보겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-sqs-폴링-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-sqs-%ED%8F%B4%EB%A7%81-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot;1 sqs 폴링 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;1. SQS 폴링 방식&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Spring에서 &lt;code class=&quot;language-text&quot;&gt;@SqsListener&lt;/code&gt;를 사용하여 메시지를 소비할 경우 기본적으로 &lt;strong&gt;SQS는 폴링(Polling) 방식으로 메시지를 가져옵니다.&lt;/strong&gt;&lt;/p&gt;
&lt;aside&gt;
&lt;p&gt;💡*Polling: 서버가 주기적으로 새로운 데이터를 확인하기 위해 &lt;strong&gt;반복적으로 요청을 보내는 방식&lt;/strong&gt;&lt;/p&gt;
&lt;/aside&gt;
&lt;br&gt;
&lt;p&gt;아래는 실제 메시지를 폴링하는 &lt;strong&gt;Spring AWS 라이브러리 코드&lt;/strong&gt;의 일부입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;org&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;springframework&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cloud&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;aws&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;messaging&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;listener&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;//SimpleMessageListenerContainer 라이브러리 일부&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SimpleMessageListenerContainer&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AbstractMessageListenerContainer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;startQueue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; queueName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;QueueAttributes&lt;/span&gt; queueAttributes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//...생략&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//**큐별로 비동기적 호출&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;Future&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; future &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTaskExecutor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;submit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SimpleMessageListenerContainer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;AsynchronousMessageListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; queueAttributes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scheduledFutureByQueue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queueName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; future&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AsynchronousMessageListener&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Runnable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;//**주기적 polling 시작점&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isQueueRunning&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;token comment&quot;&gt;//**SQS에 쌓인 메시지 가져오기&lt;/span&gt;
                    &lt;span class=&quot;token class-name&quot;&gt;ReceiveMessageResult&lt;/span&gt; receiveMessageResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getAmazonSqs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;receiveMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                                    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;queueAttributes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getReceiveMessageRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    
                    &lt;span class=&quot;token comment&quot;&gt;//**가져온 메시지의 프로세스가 모두 종료됐는지 확인하기 위한 CountDownLatch 생성                &lt;/span&gt;
                    &lt;span class=&quot;token class-name&quot;&gt;CountDownLatch&lt;/span&gt; messageBatchLatch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CountDownLatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                            receiveMessageResult&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                            
                    &lt;span class=&quot;token comment&quot;&gt;//**각 메시지 처리&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Message&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; receiveMessageResult&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isQueueRunning&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		                        &lt;span class=&quot;token comment&quot;&gt;//**메시지 처리 Executor (메시지를 @SqsListener로 전달 및 처리된 SQS 메시지 삭제 처리)&lt;/span&gt;
                            &lt;span class=&quot;token class-name&quot;&gt;SimpleMessageListenerContainer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;MessageExecutor&lt;/span&gt; messageExecutor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SimpleMessageListenerContainer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;MessageExecutor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                                    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;logicalQueueName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;queueAttributes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

                            &lt;span class=&quot;token comment&quot;&gt;//**메시지를 처리할 태스크 Executor(메시지 처리 Executor 종료시 CountDownLatch countDown 처리)&lt;/span&gt;
                            &lt;span class=&quot;token comment&quot;&gt;//**각 태스크는 별도의 스레드&lt;/span&gt;
                            &lt;span class=&quot;token function&quot;&gt;getTaskExecutor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SimpleMessageListenerContainer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SignalExecutingRunnable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                                    messageBatchLatch&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; messageExecutor&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                            messageBatchLatch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;countDown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		                    &lt;span class=&quot;token comment&quot;&gt;//모든 메시지가 처리가 끝날때까지 대기&lt;/span&gt;
                        messageBatchLatch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;InterruptedException&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                        &lt;span class=&quot;token class-name&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;currentThread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;interrupt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                   &lt;span class=&quot;token comment&quot;&gt;//...생략&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;//...생략&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//...생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;SQS 라이브러리 코드 일부&lt;/figcaption&gt;
&lt;p&gt;코드의 실행 프로세스를 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;큐 이름별로 메시지 수신을 위한 폴링 스레드 실행&lt;/li&gt;
&lt;li&gt;폴링을 통해 SQS로부터 메시지 뭉치(Batch)를 가져옴&lt;/li&gt;
&lt;li&gt;가져온 각 메시지를 처리하기 위해 &lt;strong&gt;워커 스레드(Worker Thread)를 병렬로 생성&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;CountDownLatch&lt;/code&gt;를 사용하여 해당 배치의 &lt;strong&gt;모든 메시지 처리가 끝날 때까지 대기&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;메시지 처리가 완료되면 다시 1번으로 돌아가 폴링 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;여기서 중요한 점은, SQS는 수신한 메시지 개수만큼 스레드를 생성하여 &lt;strong&gt;병렬로 처리&lt;/strong&gt;한다는 것입니다.&lt;/p&gt;
&lt;p&gt;SQS는 폴링할 때 최대 처리 메시지를 &lt;code class=&quot;language-text&quot;&gt;MaxNumberOfMessages&lt;/code&gt; 옵션을 통해 설정할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;messageListenerFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; SimpleMessageListenerContainerFactory &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; factory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SimpleMessageListenerContainerFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAmazonSqs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;amazonSQSAsync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//**폴링을 통해 가져오는 최대 메시지 개수 5개로 설정&lt;/span&gt;
    factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setMaxNumberOfMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; factory
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;SQS 설정 클래스&lt;/figcaption&gt;
&lt;p&gt;&lt;br&gt;처리 방식을 좀 더 자세히 분석해보면 &lt;code class=&quot;language-text&quot;&gt;MaxNumberOfMessages&lt;/code&gt; 옵션을 통해 아래처럼 병렬로 처리할 &lt;code class=&quot;language-text&quot;&gt;TaskExecutor&lt;/code&gt;의 스레드 개수가 설정됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt; org&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;springframework&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cloud&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;aws&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;messaging&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;listener

&lt;span class=&quot;token comment&quot;&gt;//SimpleMessageListenerContainer 라이브러리 일부&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; SimpleMessageListenerContainer extends AbstractMessageListenerContainer &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//...생략&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; AsyncTaskExecutor &lt;span class=&quot;token function&quot;&gt;createDefaultTaskExecutor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		String beanName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getBeanName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		ThreadPoolTaskExecutor threadPoolTaskExecutor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; new &lt;span class=&quot;token function&quot;&gt;ThreadPoolTaskExecutor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		threadPoolTaskExecutor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setThreadNamePrefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
				beanName &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; beanName &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DEFAULT_THREAD_NAME_PREFIX&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		int spinningThreads &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getRegisteredQueues&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;spinningThreads &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			threadPoolTaskExecutor
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setCorePoolSize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;spinningThreads &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; DEFAULT_WORKER_THREADS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

			&lt;span class=&quot;token comment&quot;&gt;//**MaxNumberOfMessages 설정값을 통해 Executor Thread 개수 결정&lt;/span&gt;
			int maxNumberOfMessagePerBatch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMaxNumberOfMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
					&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMaxNumberOfMessages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DEFAULT_MAX_NUMBER_OF_MESSAGES&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
			&lt;span class=&quot;token comment&quot;&gt;//**스레드 최대 개수 설정		&lt;/span&gt;
			threadPoolTaskExecutor
					&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setMaxPoolSize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;spinningThreads &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;maxNumberOfMessagePerBatch &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

		&lt;span class=&quot;token comment&quot;&gt;// No use of a thread pool executor queue to avoid retaining message to long in&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;// memory&lt;/span&gt;
		threadPoolTaskExecutor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setQueueCapacity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		threadPoolTaskExecutor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;afterPropertiesSet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; threadPoolTaskExecutor&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//...생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;Spring SQS 라이브러리 SimpleMessageListenerContainer 코드 일부&lt;/figcaption&gt;
&lt;p&gt;또한 SQS 폴링시 메시지 개수 파라미터로 보낸다는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 786px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/8939d1887bcbf16437efdc24df944df5/d703b/image6.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 37.708333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6klEQVR42i3RWVMaQRSG4fknuYgLMAsDMwPOvgEzwwgYqfLCaLRSQsUyS0WJEDWA0Sy3+cVv2piLp74+XdWnq/tIjrVFu1VF12Q0uUpdqaHKFeryJrqyLWqxp6n/aWj1OooqUlAVhZosI8uKOFNFUypI7cGU9OA94eEc/3hFePId9/Ab0dGC3dNLRkcTvFdvsffe4Y3PifePCYcHeKNTvN0xXj7AKcbCHnFZItUaO5hmC70VoLk5+hO/RHdSwtGE6OQPxuAGY7ikMbinUS4xdsW6XGEOVljDlahX6OUac7hG0rVtVFU8Td1Erm5Q3XpJrbJJZeMFmhnROZiTv16RvVkyezjj+ueE619TkVNmP6ZcPT7lhMvHMy5uz5GCHZkoMIgSB893ads+lmXhtesErknk2YRJRpCP6JQ9in5Akbv0C0dwKfsuWeaT5zZp2kJ6+lC9YdBomjQNSzAxDAPLbKA3Pc4mNnf3LhfXOR8XBR8WOZ9mAZczh89XrkiX5doi6eYE4mJJFZPrZMU/SaeHHyaE3ZK4V9B2upyeWDz8dvmyzpnfd0XziMVti/lCF5p8vWlye6fh+CFuED03DOKUKE5EJvhRihfnuFEX23WIwpQkG4gJjon7+6RZRqfXJRU6WY8oTbC9GC989hf3RCV8ekVVQAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8939d1887bcbf16437efdc24df944df5/263a4/image6.webp 480w,
/static/8939d1887bcbf16437efdc24df944df5/f6aa4/image6.webp 786w&quot;
              sizes=&quot;(max-width: 786px) 100vw, 786px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8939d1887bcbf16437efdc24df944df5/9aebd/image6.png 480w,
/static/8939d1887bcbf16437efdc24df944df5/d703b/image6.png 786w&quot;
            sizes=&quot;(max-width: 786px) 100vw, 786px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8939d1887bcbf16437efdc24df944df5/d703b/image6.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;SQS 메시지 폴링 요청시 maxNumberOfMessages 값 확인&lt;/figcaption&gt;
&lt;p&gt;하지만 이러한 SQS 메시지 처리 동작 방식이 어떻게 데드락을 유발했을까요? 다음 이슈도 같이 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;2-hikari-db-maximum-pool-size&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-hikari-db-maximum-pool-size&quot; aria-label=&quot;2 hikari db maximum pool size permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. &lt;strong&gt;Hikari DB Maximum-pool-size&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hikari maximum-pool-size&lt;/strong&gt;는 스프링 개발자에게 많이 익숙한 옵션으로, 동시에 사용 가능한 &lt;strong&gt;DB 커넥션의 최대 개수를 설정합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아래 코드는 병렬로 fooTransaction() 2번 호출하여 DB에 데이터를 저장하는 코드 예제입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;foo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//Thread Pool 2개 SET&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; exec &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Executors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newFixedThreadPool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//총 2번 병렬 호출&lt;/span&gt;
        exec&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fooService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fooTransaction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    
&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooService&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooRepository&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FooRepository&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fooTransaction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;num: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Transaction Start&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token comment&quot;&gt;//저장&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooEntity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        fooRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fooEntity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;num: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Transaction End&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;트랜잭션 예제 코드&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;그리고 아래와 같이 &lt;code class=&quot;language-text&quot;&gt;maximum-pool-size=2&lt;/code&gt; 설정후에 예제를 수행했습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 746px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/9f624128efdb8d4c72aebcc8f1c849d7/7e81d/image1.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 48.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABHUlEQVR42qVSzXqEMAj0bbomhKjR6Pq/bffQr+//PFNI3HYv7aEeJoCGgYEUS7DYo4NjxktpYYyFtf9HcesI956gdmsJfcOnSIvgCasQjYFRs4VnOtehkWMOhNhkQnYnJeuhcl8FH1fC3HloEXqCtZRBiicCOr5/3xFCTdiE7Baz9Fjn5ZSCZGWepiwlFlwuyTdGYbJfPvxLsgVJxV0I32QxU0PJf0++xedIGGQU4TqhE8RpQT+vCLFH6AcM6452GNCNE/plQx263GGSHPOGtYCXOarVmaoUcizPyoGrCs779MRYLHuJ078ck3MyQ9IuMuHSSjc+E7WVRSXEnViWBOLqR+ovOJZCUiXLfkBnqRhqmwq1jXZWwx5Jf235CyPsM0VOQPcTAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/9f624128efdb8d4c72aebcc8f1c849d7/263a4/image1.webp 480w,
/static/9f624128efdb8d4c72aebcc8f1c849d7/dbf53/image1.webp 746w&quot;
              sizes=&quot;(max-width: 746px) 100vw, 746px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/9f624128efdb8d4c72aebcc8f1c849d7/9aebd/image1.png 480w,
/static/9f624128efdb8d4c72aebcc8f1c849d7/7e81d/image1.png 746w&quot;
            sizes=&quot;(max-width: 746px) 100vw, 746px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/9f624128efdb8d4c72aebcc8f1c849d7/7e81d/image1.png&quot;
            alt=&quot;image1.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;maximum-pool-size=2 설정&lt;/figcaption&gt;
&lt;p&gt;수행 후 실행 로그를 보면 각 스레드1, 스레드2가 커넥션 1개씩 점유하여 &lt;strong&gt;병렬로 트랜잭션이 실행&lt;/strong&gt;됩니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 974px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/10b0ea9e94461717b7a44fabc327855b/1ed31/image2.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 27.500000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAr0lEQVR42pWQWw6EIAxFXY9KgpiA+ADR/a+pk9MExknmY+ajuRTa+6Dbtk2ccwKmlLSWZZF932VdVxmG4a/qjuNQQvC+b8k5KxnEiPxC0vf9m/C6LiXDTYxRzvNUhIw73FZyEDEMMBdCaEm89zLPs3SlFH1kgQF6kB7iKgIZSAIWSYMYPW+NEAUOuGSBQpGhaZo0xjiOrYwxH/2zNDKELOIIdzU+99ba9j9P/Fb1D1/WwsF1kduXnAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/10b0ea9e94461717b7a44fabc327855b/263a4/image2.webp 480w,
/static/10b0ea9e94461717b7a44fabc327855b/a6361/image2.webp 960w,
/static/10b0ea9e94461717b7a44fabc327855b/c1156/image2.webp 974w&quot;
              sizes=&quot;(max-width: 974px) 100vw, 974px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/10b0ea9e94461717b7a44fabc327855b/9aebd/image2.png 480w,
/static/10b0ea9e94461717b7a44fabc327855b/a91f8/image2.png 960w,
/static/10b0ea9e94461717b7a44fabc327855b/1ed31/image2.png 974w&quot;
            sizes=&quot;(max-width: 974px) 100vw, 974px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/10b0ea9e94461717b7a44fabc327855b/1ed31/image2.png&quot;
            alt=&quot;image2.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;maximum-pool-size=2 설정 후 트랜잭션 실행 로그&lt;/figcaption&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;maximum-pool-size=1&lt;/code&gt;로 설정후 수행시 다음과 같이 &lt;strong&gt;스레드1 트랜잭션 종료 -&gt; 스레드2의 트랜잭션이 수행&lt;/strong&gt;되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 934px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/0ddef8647dbffe28af820981d92436ae/e7956/image3.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 28.750000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAn0lEQVR42oVQWwqEMBDrffqAgn1ptY/732mWDIx0xWU/QuokTSeqbdsoxkjXdVFrjXnfd+q9EzStNRljbuD7DaIrCUEozrVWKqXQeZ6UUuIz9OM4mHPO7MWDYPiwgPeePQpDmBAGk1wGrLX8qvAK59ytAbIlB8p2Y4y/laX2GyOYKwOot1aWmVTGXCpj/rNyCIEACGvgnPOryhPP3yCVP7FRwXtbEsWhAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0ddef8647dbffe28af820981d92436ae/263a4/image3.webp 480w,
/static/0ddef8647dbffe28af820981d92436ae/51e77/image3.webp 934w&quot;
              sizes=&quot;(max-width: 934px) 100vw, 934px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0ddef8647dbffe28af820981d92436ae/9aebd/image3.png 480w,
/static/0ddef8647dbffe28af820981d92436ae/e7956/image3.png 934w&quot;
            sizes=&quot;(max-width: 934px) 100vw, 934px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0ddef8647dbffe28af820981d92436ae/e7956/image3.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;maximum-pool-size=1 설정 후 트랜잭션 실행 로그&lt;/figcaption&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;maximum-pool-size=1&lt;/code&gt; 이므로 총 사용 가능 커넥션은 &lt;strong&gt;1개&lt;/strong&gt;, 스레드 2개가 병렬로 수행되어도,&lt;br&gt;&lt;strong&gt;커넥션 점유한 스레드1은 트랜잭션을 수행하고, 스레드2는 스레드1 트랜잭션이 끝난후 수행됩니다.&lt;/strong&gt;
&lt;br&gt;&lt;br&gt;
여기서! &lt;code class=&quot;language-text&quot;&gt;sleep&lt;/code&gt;을 추가하여 커넥션 점유 시간이 &lt;code class=&quot;language-text&quot;&gt;connection-timeout&lt;/code&gt;을 초과하도록 수정해봤습니다.&lt;/p&gt;
&lt;aside&gt;
💡
Connection-timeout : 커넥션 점유를 대기하는 최대 시간
&lt;/aside&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooService&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooRepository&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FooRepository&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fooTransaction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;num: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Transaction Start&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token comment&quot;&gt;//저장&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooEntity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        fooRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fooEntity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token comment&quot;&gt;//오래 점유&lt;/span&gt;
        Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;num: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Transaction End&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;sleep 추가한 예제 코드&lt;/figcaption&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 452px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/d2a78907b344d608e3ea1be932e359c4/80d88/image4.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 30.973451327433626%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABCElEQVR42l2Q7U7DMAxF+zIwmtj5aNOuS1fGhDTUbUzAQOL9X+TiJIICP059nTruUSvSGo3VaI2GF1rJ1jC0nCfoF/pf/pkhkkq5Vkwax0h4u+fM00CY1gGrmqBkuFYLqb/7zoJWtaD+LK/SIw1a1giuWHZSO1tyIwRbzB0v2RoCGwdiAy1SOlvKQpKmFt1dIHw+iOGGcBXT9x3jsSecIuN1Yuz7MnMZGc9bxiY42C7CtR3q2xuoepVts2Hangw7R5lkkCwHT9nacenXPhmW973MMcu9pkM7TGiEZJsNlRjGRuMiXz6PhJeJMIvpWWxOsZgdhuU/p/M5GjTeI4x7TMcPbOcrjPP4AtnhvM0c2y+5AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d2a78907b344d608e3ea1be932e359c4/a0edd/image4.webp 452w&quot;
              sizes=&quot;(max-width: 452px) 100vw, 452px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d2a78907b344d608e3ea1be932e359c4/80d88/image4.png 452w&quot;
            sizes=&quot;(max-width: 452px) 100vw, 452px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d2a78907b344d608e3ea1be932e359c4/80d88/image4.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;maximum-pool-size=1 설정&lt;/figcaption&gt;
&lt;p&gt;실행 후 로그를 확인해보니 &lt;strong&gt;이번 운영 이슈와 동일한 에러로그가 발생&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/2451a3db9ff60b3b40555e9bd1c2a7c5/b2b72/image5.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 8.124999999999998%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAXklEQVR42l2NSQ7AIAwDeU5BYguEtOL/73Idbu1hDrascdiP4VaFToWZHeoYiK0dUu+IIoi1ojE7nZ2wK6Ug5/whbKOoDwhRitdayBxflDppTiSeuHgwO5Odb1385wXJuj8MRdcbSAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2451a3db9ff60b3b40555e9bd1c2a7c5/263a4/image5.webp 480w,
/static/2451a3db9ff60b3b40555e9bd1c2a7c5/a6361/image5.webp 960w,
/static/2451a3db9ff60b3b40555e9bd1c2a7c5/0b34d/image5.webp 1920w,
/static/2451a3db9ff60b3b40555e9bd1c2a7c5/40fc5/image5.webp 2766w&quot;
              sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2451a3db9ff60b3b40555e9bd1c2a7c5/9aebd/image5.png 480w,
/static/2451a3db9ff60b3b40555e9bd1c2a7c5/a91f8/image5.png 960w,
/static/2451a3db9ff60b3b40555e9bd1c2a7c5/ac7a9/image5.png 1920w,
/static/2451a3db9ff60b3b40555e9bd1c2a7c5/b2b72/image5.png 2766w&quot;
            sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2451a3db9ff60b3b40555e9bd1c2a7c5/ac7a9/image5.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;커넥션 타임아웃 에러 로그&lt;/figcaption&gt;
&lt;p&gt;위 에러 발생 상황을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;스레드1 트랜잭션 커넥션 점유&lt;/li&gt;
&lt;li&gt;스레드2 트랜잭션 커넥션 점유 대기 (&lt;code class=&quot;language-text&quot;&gt;maximum-pool-size=1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;스레드1 6초(Sleep)동안 트랜잭션 커넥션 점유&lt;/li&gt;
&lt;li&gt;스레드2 트랜잭션 커넥션 점유 대기 5초 초과로 인한 에러 발생 (&lt;code class=&quot;language-text&quot;&gt;Connection-timeout=5000ms&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h3 id=&quot;3-트랜잭션-requires_new&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-requires_new&quot; aria-label=&quot;3 트랜잭션 requires_new permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;3. 트랜잭션 REQUIRES_NEW&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;트랜잭션 &lt;code class=&quot;language-text&quot;&gt;REQUIRES_NEW&lt;/code&gt;는 &lt;strong&gt;트랜잭션 전파 옵션 중 하나입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;propagation &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Propagation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;REQUIRES_NEW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;특징은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이전에 수행된 트랜잭션과 별개로 항상 새로운 트랜잭션을 생성&lt;/li&gt;
&lt;li&gt;별개의 트랜잭션을 생성하므로 커넥션도 새로 점유
→ &lt;code class=&quot;language-text&quot;&gt;A 트랜잭션&lt;/code&gt;에서 &lt;code class=&quot;language-text&quot;&gt;REQUIRES_NEW&lt;/code&gt;  &lt;code class=&quot;language-text&quot;&gt;B 트랜잭션&lt;/code&gt;이 실행되면 &lt;strong&gt;A, B 커넥션 총 2개의 커넥션을 점유&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;별개의 트랜잭션으로 동작하므로 &lt;code class=&quot;language-text&quot;&gt;B 트랜잭션&lt;/code&gt;이 정상처리되어 커밋되고 &lt;code class=&quot;language-text&quot;&gt;A 트랜잭션&lt;/code&gt;이 롤백되어도 &lt;code class=&quot;language-text&quot;&gt;B 트랜잭션&lt;/code&gt;은 롤백 X&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 주의해야할 점은 &lt;code class=&quot;language-text&quot;&gt;REQUIRES_NEW&lt;/code&gt;를 사용할 시 데드락 이슈가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;예제로 살펴보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fooRequiresNew&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//요청 Thread 2&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; exec &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Executors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newFixedThreadPool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//두번 병렬 호출&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
		        &lt;span class=&quot;token comment&quot;&gt;//FooParentService 호출&lt;/span&gt;
            exec&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fooParentService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fooTransactionParent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooParentService&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooChildService&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FooChildService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooRepository&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FooRepository
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Transactional&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fooTransactionParent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Parent Transaction Start&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooEntity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        fooRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fooEntity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
				
				&lt;span class=&quot;token comment&quot;&gt;//Child 트랜잭션 시작전 Sleep&lt;/span&gt;
        Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token comment&quot;&gt;//Child 트랜잭션 호출&lt;/span&gt;
        fooChildService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fooTransactionChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Parent Transaction End&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooChildService&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooRepository&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FooRepository
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;REQUIRES_NEW&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fooTransactionChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Child Transaction Start&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fooEntity &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FooEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        fooRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fooEntity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;num&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Child Transaction End&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;REQUIRES_NEW 트랜잭션 예제 코드&lt;/figcaption&gt;
&lt;p&gt;해당 예제를 &lt;code class=&quot;language-text&quot;&gt;maximum-pool-size=2&lt;/code&gt;로 설정하고 수행시 다음과 같은 결과가 나오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/e7f8ed94aa30110a15177417b603feeb/447f2/image7.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 13.541666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAkElEQVR42mWPXQ7DIAyDe5mppYAElJ8RYL3/qTxHfZm0h09xArFhW2thzolcCkIMCOHBOwdrDOx5PpWc1IqhVqy1f2xzqqFg9YreBGMMLAb4nHFcF4xSKw72MUaklFAYntl77+EY/MvWRTDljQ9fd8UCGTSVAdMaXjzbe4e5b+wMqjQWznRHf6bGjfd0rlrrF3FGX0BUWmbpAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/e7f8ed94aa30110a15177417b603feeb/263a4/image7.webp 480w,
/static/e7f8ed94aa30110a15177417b603feeb/a6361/image7.webp 960w,
/static/e7f8ed94aa30110a15177417b603feeb/0b34d/image7.webp 1920w,
/static/e7f8ed94aa30110a15177417b603feeb/807ed/image7.webp 2738w&quot;
              sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/e7f8ed94aa30110a15177417b603feeb/9aebd/image7.png 480w,
/static/e7f8ed94aa30110a15177417b603feeb/a91f8/image7.png 960w,
/static/e7f8ed94aa30110a15177417b603feeb/ac7a9/image7.png 1920w,
/static/e7f8ed94aa30110a15177417b603feeb/447f2/image7.png 2738w&quot;
            sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/e7f8ed94aa30110a15177417b603feeb/ac7a9/image7.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;maximum-pool-size=2 설정 후 예제 실행 로그&lt;/figcaption&gt;
&lt;p&gt;로그를 확인해보면 1번, 2번 트랜잭션 모두 &lt;code class=&quot;language-text&quot;&gt;Child 트랜잭션&lt;/code&gt;이 수행되지 못하고 데드락이 발생하여 타임아웃이 발생한 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;데드락 발생 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;병렬 &lt;code class=&quot;language-text&quot;&gt;fooTransactionParent()&lt;/code&gt; 2번 호출&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;1번 Parent 트랜잭션&lt;/code&gt; 커넥션 1개 점유&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;2번 Parent 트랜잭션&lt;/code&gt; 커넥션 1개 점유&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;1번 Parent 트랜잭션&lt;/code&gt; Save 및 1초후 &lt;code class=&quot;language-text&quot;&gt;1번 Child 트랜잭션&lt;/code&gt; 점유를 위해 대기 (이미 커넥션은 모두 점유된 상태)&lt;/li&gt;
&lt;li&gt;1번 트랜잭션과 동일하게 &lt;code class=&quot;language-text&quot;&gt;2번 Child 트랜잭션&lt;/code&gt; 점유 대기&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;1, 2번 Parent 트랜잭션&lt;/code&gt;은 &lt;code class=&quot;language-text&quot;&gt;Child 트랜잭션&lt;/code&gt;이 끝날때까지 계속 점유하기에 &lt;strong&gt;데드락 발생&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그림으로 표현하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 757px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/3267b3ef6682d6368f748720d967c7fd/70668/image10.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 110.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAYAAADAQbwGAAAACXBIWXMAAAsTAAALEwEAmpwYAAADfklEQVR42lVV2W7bVhDVTxX9hrTf0f5RFziAizZ57QJ0D5CtaJPWiNfGsixTSSVxs0RJXERSIimenrlDmvLDMcf3Dg9n5syMem4KOGtg2iIB7LVCzl15pq1dQ/z7doijwRQnIx/HIw/Hloc3QxvXfoyeR8dFDsRlh2QHBBvAy2AInKQh5tPj/26Yww5W8FYx3GUIl893/grj5Qa9uAB2JLi5Bb+gGNIuKyAtleg2rUlemwySLVDRfxSo33AGWHPlSMjVyyQiRvjlS+DbY4XYK0ayodO7EHjLj5w6wDgC8krvxOfrV8RfwBcvmBmzEa7emn82ZP7lAnjaB55dqZ1u1UFq2XdiXLKIdipR1+but387iH9eaEa9INcX1zyQ9OON2nIpdRQx7LiCQwj5PNe7tNAUBca/qjFnlD0petCIEpVKGhWdKELoZwqjdOMvPvtCzjesM+/vRBnuiSIQUaQc/7Fug7nWcRppNHInPn0XuPLUrniWSMpppaI8/AP4/kRxQDtMVYAJW+Wc8p5NM5OylCfM1P/Ra8XBy0aUak8UKe7TKxXlV9rZniiTsGAdy0YUvfv9LfCkgbwroqz3RRH2FtIucrbYKKHUTWBEYT9mVHq9rTtBiLzUAenZzWhZlG/gxaxJhEt7aUbQbSdlb/xEKIGfNmdpM0UNenLgk9m6jXB0aeFi5BC2mVunUdZuXpyGJY7fezji/T83NqzZWjsh2SNcMS1mgEWmCAhzxrSjrfRgGxE/wA9PrscYW8TFNaarrUaZdFkYUVISPP67xg+nNX48q6lcjShTlSX1a7bN6bSGRTuY+Cg//gjx4TfgNMKNd3eLw0Roup6RPKb8P50DP19oK8R5R/h+WeKNFWDCD9+yJMWDB4gODg2hR0JvvRchNw4KWVcyEbEiIMmWZOGmrU3N1tnClenxuK4WEXwngMs956a4H6GoRV/TKpwexU5Hy4xb0zKe1JB+s+EUs/MBZid9Etb3ayiESTNKZ5P72JbaqJKyTMs44j5kGXxnherDDxA+/Ao2U3aiyhDaTZSmhlKvz1/oLvyOo/fZc2Apo7fTn4XBrMZrq8CNNHZ/hOyTTxEePoJdCMkOthC2KctAF8SfN8ArSyH2pllh0oNTrqG+vdBG546yOfzuPKHCddf4LeEs19Fpd5yMUtakK7uvdb570TSy1K7ufsDSrob/A1PTgBzAzrsMAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3267b3ef6682d6368f748720d967c7fd/263a4/image10.webp 480w,
/static/3267b3ef6682d6368f748720d967c7fd/06aef/image10.webp 757w&quot;
              sizes=&quot;(max-width: 757px) 100vw, 757px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3267b3ef6682d6368f748720d967c7fd/9aebd/image10.png 480w,
/static/3267b3ef6682d6368f748720d967c7fd/70668/image10.png 757w&quot;
            sizes=&quot;(max-width: 757px) 100vw, 757px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3267b3ef6682d6368f748720d967c7fd/70668/image10.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;트랜잭션 데드락 발생 과정&lt;/figcaption&gt;
&lt;br&gt;
&lt;h2 id=&quot;해결-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;해결 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결 과정&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;이제 위에서 봐온 내용을 토대로 이번 이슈가 실제로 &lt;strong&gt;어떻게 발생했는지 간략하게 정리하고 해결방법에 대해 말씀드리겠습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-장애-발생-메커니즘-root-cause&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9E%A5%EC%95%A0-%EB%B0%9C%EC%83%9D-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98-root-cause&quot; aria-label=&quot;1 장애 발생 메커니즘 root cause permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. &lt;strong&gt;장애 발생 메커니즘 (Root Cause)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;이번 장애는 &lt;strong&gt;병렬 처리 환경과 트랜잭션 전파 속성이 충돌&lt;/strong&gt;하며 발생한 전형적인 자원 경합 문제였습니다.
&lt;br&gt;아래 두 가지 설정이 맞물리면서 시스템은 데드락에 빠질 수 있는 임계 상태에 놓여 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;커넥션 점유의 불균형: SQS의 &lt;strong&gt;MaxNumberOfMessages&lt;/strong&gt; 가 DB 커넥션 풀 크기보다 크게 설정되어 있었습니다. (&lt;strong&gt;커넥션 개수 &amp;#x3C; 최대 폴링 메시지 개수&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;중첩 트랜잭션의 함정: 이력 저장 로직에 &lt;strong&gt;REQUIRES_NEW&lt;/strong&gt; 옵션을 사용하여, 알림 발송 1건당 &lt;strong&gt;최소 2개의&lt;/strong&gt; 커넥션이 필요한 구조였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;정리하면 병렬로 처리되던 각 메시지가 &lt;strong&gt;커넥션을 1개씩 점유한 상태&lt;/strong&gt;에서, 이력 저장을 위해 &lt;strong&gt;1개의 커넥션이 더 필요&lt;/strong&gt;했지만 가용 커넥션이 남아 있지 않아 모든 메시지 처리가 동시에 대기 상태에 빠지며 &lt;strong&gt;데드락&lt;/strong&gt;이 발생했습니다&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 883px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/06c4efbd148089e2ba536dac68a0f78e/31459/image9.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 75.20833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACi0lEQVR42m1UXW/TQBD0//8bIJBAAvEEDxQEErQotLRpmqRJEyfxV+zYcePvpI6H2TubthIP65NvfbMzs3s27sICg1Wo4nadYLmr4RWAmwFW0mDsxBi5MebbPbwcKhzmXK521qgzN1YIc1uqPWOTEyCusAgKBLs9guIIK5OPgfs9DzJnESzi/qbUYHauAR0CLpgztxVWSQ1HAPMH4MsIePULOJsBxMfFKsN4XSIh4PsL4DVzYw9IHxpcLFP0Wc0tGgUujNeiqNBFDGFB1jAjIOAHLKYqWrsDSBirWOdiHvBZ7dL0cLMM4NAOt5PPEHZOB+jcA9OA1QgcC0grKzkAsxCYMBdyL5LiEpTutbYIM7/Ue/KuJJ9Q8stT4PROS/5t7jBi6YRs350/Sk4ONX4MfZyzip03Sk2QagVuS8aQR0WQjGxKrmEpXrBR+VExlFxR65zHJtwFCRZRxibo/d4ceNsDPt/wneQMAbqyga9jcAQ08NCvMdvWyFjslI36PuEIbQHaivHmiGl4VH5FZLjhuqJlPpneVwJI1J4JfBwA1wSW94GdkEmuuiwT8Im5RSRjVONsbOHPzCNgoz1suyyzK74ryfujpi+rSF5XOpL9c8l+Ox4ewZ521k01sNs15dst8KanmUpTrt09ppsHpAT8cKlzE7GDc3hlFRi6Jdb549g4+RPAlL6YHI2+Q8qx9mnI+ZnTHOUnu9u3dTcjUj2fOugvfHVLnBbEzR/D4M1CTJk+E7tKD7ZNWU7rS3TQ47ApNBu/0uH+B8xTV48HTobAi5+6oymN781jXC136qPuXrtt2IpZ8w/sKUspaAijNeUsYj0CUdnwL1PDTmvtT2d+9vyadQWkId3fx0mBv6ZddFPiL6NoAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/06c4efbd148089e2ba536dac68a0f78e/263a4/image9.webp 480w,
/static/06c4efbd148089e2ba536dac68a0f78e/8c45b/image9.webp 883w&quot;
              sizes=&quot;(max-width: 883px) 100vw, 883px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/06c4efbd148089e2ba536dac68a0f78e/9aebd/image9.png 480w,
/static/06c4efbd148089e2ba536dac68a0f78e/31459/image9.png 883w&quot;
            sizes=&quot;(max-width: 883px) 100vw, 883px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/06c4efbd148089e2ba536dac68a0f78e/31459/image9.png&quot;
            alt=&quot;image.png&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;SQS 메시지 폴링 요청시 maxNumberOfMessages 값 확인&lt;/figcaption&gt;
&lt;h3 id=&quot;2-병렬-처리-환경에서의-최소-커넥션-계산&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%B5%9C%EC%86%8C-%EC%BB%A4%EB%84%A5%EC%85%98-%EA%B3%84%EC%82%B0&quot; aria-label=&quot;2 병렬 처리 환경에서의 최소 커넥션 계산 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. &lt;strong&gt;병렬 처리 환경에서의 최소 커넥션 계산&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;해당 상황 해결을 위해 HikariCP에서 제시하는 풀 사이징 가이드를 기준으로 필요 커넥션 수를 계산해보면, 다음과 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;p&gt;(참고: &lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; style=&quot;color:#6bbf59;&quot;&gt;Hikari CP 공식 문서&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;데드락을 피하기 위한 최소 필요 커넥션 수 = &lt;strong&gt;Tn × (Cm − 1) + 1&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;• Tn: 동시에 처리되는 최대 스레드 개수
• Cm: 스레드 1개 처리 시 필요한 최대 커넥션 수&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이번 사례에서는 알림톡 1건 처리 시 부모 트랜잭션 1개, 이력 저장을 위한 REQUIRES_NEW 트랜잭션 1개로, &lt;strong&gt;Cm = 2&lt;/strong&gt;인 구조였습니다.&lt;/p&gt;
&lt;p&gt;즉, &lt;strong&gt;SQS 폴링으로 동시에 처리되는 메시지 수(Tn)가 증가할수록, 필요한 커넥션 수 역시 증가하는 구조&lt;/strong&gt;라 볼 수 있습니다. (Cm = 1 인 경우 필요 커넥션 개수는 1개로 고정)&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;3-커넥션-요구량-제어를-통한-문제-해결&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%BB%A4%EB%84%A5%EC%85%98-%EC%9A%94%EA%B5%AC%EB%9F%89-%EC%A0%9C%EC%96%B4%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0&quot; aria-label=&quot;3 커넥션 요구량 제어를 통한 문제 해결 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. &lt;strong&gt;커넥션 요구량 제어를 통한 문제 해결&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;이번 이슈의 해결 방법은 필요 커넥션 수를 조절하면 되기에 아래처럼 간단하게 수정하여 해결했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REQUIRES_NEW 옵션이 필요없는 로직이었기에 &lt;strong&gt;REQUIRES_NEW 옵션 제거&lt;/strong&gt; (&lt;strong&gt;Cm = 1&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;SQS 최대 폴링 메시지 개수를 커넥션 개수와 같거나 적게 변경 (&lt;strong&gt;메시지 개수 &amp;#x3C;= 커넥션 개수&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이러한 설정을 조정함으로써, 알림 처리 과정에서 동시에 요구되던 커넥션 수를 효과적으로 줄일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;REQUIRES_NEW 옵션 제거를 통해 알림 한 건당(스레드) 추가 트랜잭션 생성을 방지했고, SQS 폴링 시 한 번에 처리되는 메시지 개수를 제한함으로써 병렬로 실행되는 트랜잭션 수 또한 제어할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;그 결과, 메시지 처리량이 증가하는 상황에서도 커넥션 경합과 데드락이 발생하던 구조를 해소할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;이번 사례는 단순한 설정 오류라기보다는, 서로 다른 구성 요소들이 병렬 처리 환경에서 어떻게 맞물려 동작하는지에 대한 이해가 충분히 연결되지 않았던 지점에서 비롯된 이슈였습니다. 이를 통해 기능 자체의 구현뿐만 아니라, 해당 기능이 어떤 실행 환경과 처리 흐름 속에서 동작하는지를 함께 고려하는 것이 얼마나 중요한지 다시 한 번 되돌아보게 되었습니다.&lt;/p&gt;
&lt;p&gt;이 글을 읽고 계신 분들 역시 &lt;strong&gt;비동기 처리 구조를 설계할 때, 각 설정과 옵션이 병렬 환경에서 어떤 영향을 미칠 수 있는지 한 번쯤 점검&lt;/strong&gt;해보시면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;저희는 앞으로도 이러한 경험들을 바탕으로, 시스템의 동작 방식과 구조적인 특성을 함께 고려하는 개발을 이어가고자 합니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영의 실시간 캠페인 타겟팅을 위한 CDC 전환기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-12-29/campaign-worekr_review/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-29/campaign-worekr_review/</guid><pubDate>Mon, 29 Dec 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;캠페인과-데이터-정합성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BA%A0%ED%8E%98%EC%9D%B8%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1&quot; aria-label=&quot;캠페인과 데이터 정합성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;캠페인과 데이터 정합성&lt;/h2&gt;
&lt;p&gt;캠페인 시스템은 올리브영 고객들에게 필요한 혜택과 상품을 적절한 타이밍에 제공하여 완성된 쇼핑 경험을 제공하고 구매 전환을 극대화하는 역할을 담당합니다.
주요 임무는 고객의 최신 행동이나 구매 패턴에 기반해 가장 적절하다고 생각되는 알림을 선별해 발송하는 것입니다.&lt;/p&gt;
&lt;p&gt;예를 들자면 쿠폰 등의 혜택을 미사용한 사용자에게 혜택을 사용하도록 유도하거나 사용자가 놓치기 쉬운 프로모션을 강조함으로써 올리브영의 구매 전환율을 끌어올리는데 크게 기여하고 있습니다.&lt;/p&gt;
&lt;p&gt;그런데 만약 유저 패턴이 실시간으로 시스템에 반영되지 않아서 유저가 필요한 알림을 받지 못한다면 어떨까요? 쿠폰 발행 대상자가 알림을 받지 못한다든지,
혹은 마케팅 정보 수신을 거부했음에도 알림받는 경우가 있겠는데요. 전자는 잠재적인 구매 전환율의 하락으로 이어질 것이고, 후자는 고객의 신뢰를 잃을 수 있겠죠.&lt;/p&gt;
&lt;p&gt;이런 이슈는 데이터 정합성(Data Consistency) 측면에서 발생합니다. 다르게 말하면 고객의 최신 상태가 캠페인 시스템에 실시간성으로 반영되지 못했기 때문입니다.&lt;/p&gt;
&lt;p&gt;올리브영의 캠페인 시스템은 ODI를 이용한 배치성 스케줄러로 고객 데이터를 소스 데이터베이스에서 싱크하고 있었습니다.
주기는 20분에서 1시간까지 다양하지만 실시간 데이터 변경을 반영하지 못 한다는 명백한 한계를 가지고 있습니다. 배치 주기를 아무리 짧게 해도 짧게는 몇 분, 길게는 몇십 분의 공백이 발생하기 때문입니다.&lt;/p&gt;
&lt;p&gt;이는 실시간으로 변화하는 고객 행동에 기민하게 대응하는 것을 어렵게 만들었습니다. 따라서 저희는 이 근본적인 문제를 해결하고자 OGG 기반의 CDC와 Kafka 기반 실시간 스트리밍 시스템으로 데이터 파이프라인을 구축하기로 했습니다.&lt;/p&gt;
&lt;p&gt;그럼 이제부터 캠페인 도메인에서 ODI 기반의 배치 시스템을 OGG와 Kafka 기반의 CDC 아키텍처로 성공적으로 전환한 여정과 그 과정에서 마주한 기술적 도전, 그리고 해결책에 대해 소개드리고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;odi의-기반-캠페인-시스템의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#odi%EC%9D%98-%EA%B8%B0%EB%B0%98-%EC%BA%A0%ED%8E%98%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;odi의 기반 캠페인 시스템의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;ODI의 기반 캠페인 시스템의 한계&lt;/h2&gt;
&lt;p&gt;기존 올리브영 캠페인 시스템은 ODI(Oracle Data Integrator)를 활용한 배치(Batch) 방식으로 고객 데이터를 동기화했습니다.
이 구조는 데이터 웨어하우징 통합에는 최적화되어 있지만, 실시간 대응이 필요한 캠페인 서비스에는 근본적인 한계를 가집니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ODI 핵심 구조 및 작동 원리
&lt;ul&gt;
&lt;li&gt;데이터 추출 (Extract): 소스 데이터베이스에서 데이터를 추출합니다.&lt;/li&gt;
&lt;li&gt;타겟 적재 (Load): 추출한 데이터를 타겟 데이터베이스에 적재합니다.&lt;/li&gt;
&lt;li&gt;대규모 변환 (Transform): 데이터를 적재한 후, 타겟 데이터베이스의 성능을 이용하여 대규모 데이터 변환(조인, 계산 등)을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동기화 방식: 캠페인 시스템이 바라보는 원천 테이블에 대응하는 ODI 스케줄러를 통해 동기화가 이루어집니다.&lt;/li&gt;
&lt;li&gt;동기화 주기: 최소 20분에서 최대 1시간까지 주기적인 간격으로 데이터를 싱크합니다.&lt;/li&gt;
&lt;li&gt;데이터 매핑 구조: 1:1 단순 매핑부터 여러 소스 테이블을 조인하여 하나의 타겟에 싱크하는 복잡한 방식까지 혼재되어 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;width: 70%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 749px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9075637bfcbe8f92d48dc1ae00ca44ff/2ab73/1-campaign-odi-based-architecture.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 95.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB8klEQVR42q2U626bQBCF8/6PVDVRq+ZP5crNxXFiiIOBNTE2pJiruSynM2sg0Bo1bTOSZS+z/vbMzFnO0ItKStT4PSQ959ypqOt6kDtrfzg7D/eGiY3/wrvUprKqkBcFnsQzFqaNKEmPOXrOhyRpCs0SKpfl+RHIJ3DcPq5w/u0a1mar1odDjiIvEEYxJnMNl9d3WG89lcuyA6qyRBBGuJhc4cvVDAWtBwq3no87fYlgH6p1SRtYJaux1w7M9Rpxkqgcq2YhknKPKwsaVSabsjvgfr/HyjQRRdEAKCsJ2xYwDANxHKtc0QD5MNd11Yfbcyy5AQoq54bKdl+CV2Cz6ddogaei6+HsycSn6S0sd4ealGVZNgD2AX0gf/dzTck1Nu4Ws4UOj6fMCgnWlsHT5YEkNAzVQ5roqMKuh0EAx3GQkhVaIDeflU4fdHwm9feGpWzDDjiN6wFj+qPY+YjTrOvhgb1FgA15VCe/eUHQ1DMeXQ9ZxYevUyyFA1lLghXKW5wXgsy7WMD3PfwpOoUu+fBGW+JH40MGtT1sb40cuX4ngc/UP13TSIXfJcsR27wJWDeA/vD+C8h3eE5edGgwTK/ohrT386+AnbGXK3ycfIdNxlZ9I3XVG3o2qnBHQ5nzy6Gxxr/GwNhC0DsvDN8H+F7xE9vEzYEWBDtFAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9075637bfcbe8f92d48dc1ae00ca44ff/263a4/1-campaign-odi-based-architecture.webp 480w,
/static/9075637bfcbe8f92d48dc1ae00ca44ff/9c16c/1-campaign-odi-based-architecture.webp 749w&quot; sizes=&quot;(max-width: 749px) 100vw, 749px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9075637bfcbe8f92d48dc1ae00ca44ff/9aebd/1-campaign-odi-based-architecture.png 480w,
/static/9075637bfcbe8f92d48dc1ae00ca44ff/2ab73/1-campaign-odi-based-architecture.png 749w&quot; sizes=&quot;(max-width: 749px) 100vw, 749px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9075637bfcbe8f92d48dc1ae00ca44ff/2ab73/1-campaign-odi-based-architecture.png&quot; alt=&quot;1 campaign odi based architecture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
ODI를 통한 소스와 타겟 데이터베이스의 싱크&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;이러한 배치 중심의 구조는 구체적으로 다음과 같은 성능 및 비즈니스적 문제로 이어졌습니다.&lt;/p&gt;
&lt;h4 id=&quot;데이터-지연-data-latency&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A7%80%EC%97%B0-data-latency&quot; aria-label=&quot;데이터 지연 data latency permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 지연 (Data Latency)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;실시간 대응 불가: 데이터가 정해진 배치 주기에만 업데이트되므로, 고객의 행동 변화(예: 마케팅 수신 동의/철회, 쿠폰 발급)에 대한 최신 정보를 즉각적으로 활용할 수 없었습니다.&lt;/li&gt;
&lt;li&gt;고객 타겟팅 오류: ODI 수행 시간대에 따라 데이터 최신화가 지연되어, 마케팅 수신 동의를 했음에도 타겟팅에서 누락되거나(기회 손실), 반대로 알림을 껐음에도 알림을 받는(고객 경험 저하) 이슈가 발생했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;성능-이슈-performance-bottleneck&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5-%EC%9D%B4%EC%8A%88-performance-bottleneck&quot; aria-label=&quot;성능 이슈 performance bottleneck permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;성능 이슈 (Performance Bottleneck)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;소스 DB 부하 집중: 특정 시간대에 쿠폰 대량 발급이나 회원 멤버십 승급과 같이 대규모 데이터 변경이 발생하면, ODI가 이를 한꺼번에 처리하려 하면서 소스 데이터베이스에 과도한 부하를 주었습니다.&lt;/li&gt;
&lt;li&gt;연쇄적인 싱크 지연: 처리해야 할 데이터량이 많아지면 ODI 작업 자체가 지연되고, 이 지연된 ODI Task가 전체 시스템에 영향을 미쳐 모든 데이터 싱크 프로세스가 함께 지연되는 결과를 야기했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;oggoracle-golden-gate&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#oggoracle-golden-gate&quot; aria-label=&quot;oggoracle golden gate permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OGG(Oracle Golden Gate)&lt;/h2&gt;
&lt;p&gt;올리브영이 실시간 데이터 동기화(CDC)를 위해 채택한 OGG는 엔터프라이즈급 안정성과 확장성을 갖춘 툴로,
기존 배치 시스템의 한계를 극복하는 핵심 기능을 제공합니다.&lt;/p&gt;
&lt;p&gt;OGG의 주요 기능은 Oracle DB의 트랜잭션 로그를 활용하여 데이터 변경 사항을 실시간으로 캡처하고, 이를 Kafka 스트리밍 생태계로 효율적으로 전송하는 데 집중되어 있습니다.&lt;/p&gt;
&lt;h4 id=&quot;실시간-변경-데이터-캡처-cdc-기능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%B3%80%EA%B2%BD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%BA%A1%EC%B2%98-cdc-%EA%B8%B0%EB%8A%A5&quot; aria-label=&quot;실시간 변경 데이터 캡처 cdc 기능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실시간 변경 데이터 캡처 (CDC 기능)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;OGG는 Oracle 데이터베이스의 트랜잭션 로그(Redo Log)를 직접 읽어 INSERT, UPDATE, DELETE와 같은 데이터 변경 사항을 실시간으로 캡처합니다. 이는 데이터 변경이 발생하자마자 즉시 감지하여 데이터 지연 시간을 극소화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;kafka-연동-및-데이터-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kafka-%EC%97%B0%EB%8F%99-%EB%B0%8F-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;kafka 연동 및 데이터 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Kafka 연동 및 데이터 처리&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;캡처된 변경 사항을 Kafka 환경에서 사용하기 적합하도록 JSON, Avro, Parquet 등의 형식으로 변환하고 포맷팅합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;kafka로-전송&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kafka%EB%A1%9C-%EC%A0%84%EC%86%A1&quot; aria-label=&quot;kafka로 전송 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Kafka로 전송&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;변환된 데이터를 Kafka Connect 프레임워크를 통해 지정된 Kafka 메시지로 안정적으로 전송합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;데이터-파이프라인-구축&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95&quot; aria-label=&quot;데이터 파이프라인 구축 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 파이프라인 구축&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;OGG 커넥터를 사용하여 데이터베이스를 Kafka의 데이터 스트리밍 생태계에 연결함으로써, 복잡한 실시간 데이터 파이프라인을 효율적으로 구축할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;안정성-신뢰성-및-확장성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%88%EC%A0%95%EC%84%B1-%EC%8B%A0%EB%A2%B0%EC%84%B1-%EB%B0%8F-%ED%99%95%EC%9E%A5%EC%84%B1&quot; aria-label=&quot;안정성 신뢰성 및 확장성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;안정성, 신뢰성 및 확장성&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;OGG는 엔터프라이즈급 솔루션으로서, 데이터 손실 없이 안정적인 데이터 복제 및 전송을 보장합니다.&lt;/li&gt;
&lt;li&gt;Kafka Connect 프레임워크를 사용하기 때문에 수평 확장(Scale-out)이 용이하며, 여러 데이터베이스의 변경 사항을 동시에 처리할 수 있는 능력을 갖춥니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;ogg-커넥터의-활용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ogg-%EC%BB%A4%EB%84%A5%ED%84%B0%EC%9D%98-%ED%99%9C%EC%9A%A9&quot; aria-label=&quot;ogg 커넥터의 활용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OGG 커넥터의 활용&lt;/h3&gt;
&lt;p&gt;OGG 커넥터를 활용하면 데이터베이스의 변경 사항이 발생하는 즉시 Kafka로 전송되어, 실시간 분석, 데이터 웨어하우징, 마이크로서비스 간 데이터 동기화 등 다양한 실시간 시나리오에 즉각적으로 활용할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;width: 70%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1620px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/07a5fb0bce7c6905d2f1480707a660ba/ddd99/goldengate_configs.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.041666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAKABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAEDBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAHZBUKhX//EABkQAAIDAQAAAAAAAAAAAAAAAAABAjEyQv/aAAgBAQABBQLpFD3Cp6//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEv/aAAgBAwEBPwFL/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BV//EABgQAQADAQAAAAAAAAAAAAAAAAABEDFB/9oACAEBAAY/Ata4ipf/xAAbEAEAAwADAQAAAAAAAAAAAAABABEhMUFRcf/aAAgBAQABPyEK9d6NhxS2dksxY+whV4wibDiA4p//2gAMAwEAAgADAAAAEJvf/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQARQf/aAAgBAwEBPxDkFNv/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/EB//xAAdEAEAAwABBQAAAAAAAAAAAAABABEhMVFhcbHw/9oACAEBAAE/EKAXhI/CoqjCQppeuQvms8HfcAwG+HiPaFI2QQADODsT/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/07a5fb0bce7c6905d2f1480707a660ba/263a4/goldengate_configs.webp 480w,
/static/07a5fb0bce7c6905d2f1480707a660ba/a6361/goldengate_configs.webp 960w,
/static/07a5fb0bce7c6905d2f1480707a660ba/77fde/goldengate_configs.webp 1620w&quot; sizes=&quot;(max-width: 1620px) 100vw, 1620px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/07a5fb0bce7c6905d2f1480707a660ba/a3e66/goldengate_configs.jpg 480w,
/static/07a5fb0bce7c6905d2f1480707a660ba/fb816/goldengate_configs.jpg 960w,
/static/07a5fb0bce7c6905d2f1480707a660ba/ddd99/goldengate_configs.jpg 1620w&quot; sizes=&quot;(max-width: 1620px) 100vw, 1620px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/07a5fb0bce7c6905d2f1480707a660ba/ddd99/goldengate_configs.jpg&quot; alt=&quot;goldengate configs&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
OGG가 지원하는 주요 데이터 복제 토폴로지
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h2 id=&quot;ogg를-활용한-하이브리드-cdc-적용-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ogg%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-cdc-%EC%A0%81%EC%9A%A9-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;ogg를 활용한 하이브리드 cdc 적용 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OGG를 활용한 하이브리드 CDC 적용 전략&lt;/h2&gt;
&lt;p&gt;OGG 커넥터를 그대로 도입하지 못한 주요 원인은 기존 배치 시스템의 복잡한 데이터 통합 로직 때문이었습니다.
이 문제를 해결하고 커넥터의 장점을 유지하기 위해 &lt;strong&gt;OGG 커넥터와 워커를 혼합한 하이브리드 아키텍처를 절충안으로 채택&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OGG 커넥터 단독으로는 기존 ODI 시스템의 복잡한 요구사항을 처리할 수 없었습니다.
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;1:1 매핑 제약&lt;/strong&gt; : OGG 커넥터는 기본적으로 소스 테이블의 Row와 타겟 테이블의 Row가 1:1로 대응하는 구조만 지원합니다. 즉, 타겟 테이블의 컬럼이 모두 소스 테이블에 존재해야 합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;복잡한 비즈니스 로직 미지원&lt;/strong&gt; : 기존 캠페인 도메인은 ODI를 통해 &lt;strong&gt;다수의 원천 테이블을 조인(Join)&lt;/strong&gt; 하고 데이터를 가공하여 하나의 &lt;strong&gt;통합 테이블(Consolidated Table)&lt;/strong&gt; 에 데이터를 싱크하는 복잡한 통합 로직을 가지고 있었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt; : 순수한 OGG 커넥터만으로는 이처럼 예외적이고 복잡한 데이터 변환 및 통합 비즈니스 로직을 처리하는 것이 불가능했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;하이브리드-구조-구상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%EA%B5%AC%EC%A1%B0-%EA%B5%AC%EC%83%81&quot; aria-label=&quot;하이브리드 구조 구상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;하이브리드 구조 구상&lt;/h3&gt;
&lt;p&gt;OGG는 CDC의 안정성을, Campaign-Worker는 데이터의 통합 및 가공을 담당하는 방식으로 역할을 분리했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 70%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 651px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c80e4eb1bf37e934a11f213c3e92e0b8/b7e05/3-ogg-campaign-worker-based-architecture.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 90.41666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGklEQVR42qWUy27TQBSGs2bNe7HiCeAZWPIgrJBYdMkGAYuKS4sC9JJUDdQkbSiRHaeJ67sTX2YmP2cmGcdOrIqKI41sz+U75//nyC2so2AMtuPCjxI0RbJIwYXYmXeDCBPXK9day+VSvZhTB2++fEd3MISgxaJgYIyD0bP76xKvP7dxaY7V3pzmOBfw/ACfOj18PD1HmMxXQJ0pponDzjlG9kR9S6hMxjlH1+jj4OQMVyOzce30ok8FFFvAKII5GsFxHLVRhzzIyA5Bg9Oohpy3x2NYpoksy+pA6eGQJLlhhPtETpXleV5+1zx8//VEecild5RAr90V2usSuPEwwYejDq6tlfGMZOd5sQtd0m0Kki54KbtqUQn0fR/TyQS35CFbeyXNb4Ru+dgIFGKJMI6R5UXtgG4hDeU3FygG+2BWdw0slJodD+1bFwfdHn4Mr0mVUCA9pEfZYo45qfVfPYH37AHCF48gWznL0nqFGujHCd62j3Fl2uvMXDWvHFJWKoF0zt97ivD5QyQvH0NiWJHXLCkle54Hoz/AiHqqmnEleePjjfENfw73YHb20eRsq2qubO5qT0mI/NYwQTdrzzxYswDW1C0vrxEo6JAXxojpJ7CmqaYVWz+ELE0xTygxPRsr3DT2DO/aRzgb/FaSm2D/EiUwoYztnkFSHF1gY8j9euAuD4MgwE/DgGnVL+XeFVYzLxaL/4JJ4F9aMnZG0hzjbwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c80e4eb1bf37e934a11f213c3e92e0b8/263a4/3-ogg-campaign-worker-based-architecture.webp 480w,
/static/c80e4eb1bf37e934a11f213c3e92e0b8/60c7d/3-ogg-campaign-worker-based-architecture.webp 651w&quot; sizes=&quot;(max-width: 651px) 100vw, 651px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c80e4eb1bf37e934a11f213c3e92e0b8/9aebd/3-ogg-campaign-worker-based-architecture.png 480w,
/static/c80e4eb1bf37e934a11f213c3e92e0b8/b7e05/3-ogg-campaign-worker-based-architecture.png 651w&quot; sizes=&quot;(max-width: 651px) 100vw, 651px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c80e4eb1bf37e934a11f213c3e92e0b8/b7e05/3-ogg-campaign-worker-based-architecture.png&quot; alt=&quot;3 ogg campaign worker based architecture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
OGG 커넥터와 Campaign-Worker의 하이브리드 아키텍처
&lt;/figcaption&gt;
&lt;/div&gt;
제시된 하이브리드 아키텍처는 OGG 커넥터의 안정성과 Campaign-Worker의 비즈니스 로직 처리 능력을 결합하여, 기존 ODI 시스템의 복잡한 요구사항과 CDC의 실시간성 목표를 동시에 달성했습니다.
&lt;h4 id=&quot;1-ogg-커넥터-담당-11-단순-매핑&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-ogg-%EC%BB%A4%EB%84%A5%ED%84%B0-%EB%8B%B4%EB%8B%B9-11-%EB%8B%A8%EC%88%9C-%EB%A7%A4%ED%95%91&quot; aria-label=&quot;1 ogg 커넥터 담당 11 단순 매핑 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;1. OGG 커넥터 담당 (1:1 단순 매핑)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;메시지 발행량이 많고, 별도의 비즈니스 로직이 필요 없는 테이블은 OGG 커넥터를 통해 ADW(타겟 DB)로 자동 싱크됩니다. 애플리케이션 레벨의 관리 없이 안정성과 효율성을 확보했습니다.&lt;/p&gt;
&lt;h4 id=&quot;2-campaign-worker-담당-복합-처리-로직&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-campaign-worker-%EB%8B%B4%EB%8B%B9-%EB%B3%B5%ED%95%A9-%EC%B2%98%EB%A6%AC-%EB%A1%9C%EC%A7%81&quot; aria-label=&quot;2 campaign worker 담당 복합 처리 로직 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;2. Campaign-Worker 담당 (복합 처리 로직)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;복잡한 비즈니스 로직이 필요한 테이블 (예: 여러 테이블 조인, 조건부 변환)의 변경 메시지는 Kafka를 통해 워커가 직접 소비하여 자체 로직으로 처리합니다. 커넥터의 안정성을 유지하면서 복잡한 통합 요구사항을 만족시킵니다.&lt;/p&gt;
&lt;h2 id=&quot;메시지-순서-문제와-해결-전략-요약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%88%9C%EC%84%9C-%EB%AC%B8%EC%A0%9C%EC%99%80-%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5-%EC%9A%94%EC%95%BD&quot; aria-label=&quot;메시지 순서 문제와 해결 전략 요약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;메시지 순서 문제와 해결 전략 요약&lt;/h2&gt;
&lt;h3 id=&quot;문제-상황-테이블-간의-의존성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B0%84%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1&quot; aria-label=&quot;문제 상황 테이블 간의 의존성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 상황: 테이블 간의 의존성&lt;/h3&gt;
&lt;p&gt;하지만 위 구조 역시 완전한 솔루션은 아니었습니다. 가장 큰 문제를 하나 꼽자면 시스템 자체가 여러 토픽에서 발행된 메시지들의 순서에 서로 의존하는 구조라는 점 입니다. 하지만 카프카의 단일 파티션이 아닌 서로 다른 토픽들간의 메시지 순서 제어는 카프카 자체의 메커니즘으로만으론 불가능합니다.
이러한 이슈로 인해 핵심 문제는 서로 연관된 두 테이블이 바라보는 토픽의 메시지가 타겟에서 원하는 순서대로 소비되지 않을 때 발생합니다. &lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이해를 돕기 위해 테이블 A와 테이블 B를 다음과 같이 정의하겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테이블 A (베이스 정보): 회원 기본 정보(회원번호, 회원ID 등)를 담고 있으며, 변경량이 많습니다.&lt;/li&gt;
&lt;li&gt;테이블 B (부가 정보): 마케팅 수신 정보 등 부가 데이터를 담고 있으며, 테이블 A와 조인되어 타겟에 통합됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;문제-발생-원인-및-시나리오&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EB%B0%9C%EC%83%9D-%EC%9B%90%EC%9D%B8-%EB%B0%8F-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4&quot; aria-label=&quot;문제 발생 원인 및 시나리오 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 발생 원인 및 시나리오&lt;/h3&gt;
&lt;p&gt;문제는 테이블 B의 변경 메시지가 먼저 도착했는데, 테이블 A에 해당하는 베이스 정보(Key)가 타겟 테이블에 아직 없는 경우에 발생합니다. 일반적으로는 A 테이블의 변경이 먼저 완료되지만, 드물게 A와 B 테이블이 거의 동시에 업데이트될 경우 Kafka 토픽에서 메시지 순서가 보장되지 않아, B에 해당하는 A의 베이스(Key) 정보를 찾을 수 없어 B의 변경 메시지가 저장에 실패하는 문제가 발생했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 70%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1105px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/332479cc410439915b40d8eb895eede9/0b602/4-message-accept-failed-case.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABfUlEQVR42p1Ty0rDQBTtZ/oNois3/kURF7rwgRt3amtFcOWiIIJLF4VQgxh8kEyaluadzBznJCRNHyLthcncDDNnzj3nTgs60jRFHMf1yLIMm0Yrz3OMRiNEUYQgCIrheR6UUv8eVo0xB0gwhpSymMMwrHMhBJIkWY8hD3A2TbNY5AVpWpZ90+nAtu2SUYM1M1fl+EaOYaglk2oGSN0Y0+m0Blwsmf95LlEtD0SCh6cAz/0J2gMHH1FaArI06kaWNIPgLHlVSM1CQcKfZOgd+TjfHeNyX+B0x8H9mSgB+SEQWRGIc6XfkgmandRlem6E3sEYh1s/uNizcbzt4O5EzEp2XbcAa7q8KrLPr0JrxqPlo30t8KrLfvEieJlcdpk6cTRdZs4KuE9qWSplrTDG1fANfXOId9upz9cu85BlWTVI1Srd2y4Mw5hrq8rnLNIPwQ90RyTzgDSCm9ngiy7zor80XdZYlS77vl88P+YEp46bRAHIhCyrllmH0SrAX5f08oydATu2AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/332479cc410439915b40d8eb895eede9/263a4/4-message-accept-failed-case.webp 480w,
/static/332479cc410439915b40d8eb895eede9/a6361/4-message-accept-failed-case.webp 960w,
/static/332479cc410439915b40d8eb895eede9/b495b/4-message-accept-failed-case.webp 1105w&quot; sizes=&quot;(max-width: 1105px) 100vw, 1105px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/332479cc410439915b40d8eb895eede9/9aebd/4-message-accept-failed-case.png 480w,
/static/332479cc410439915b40d8eb895eede9/a91f8/4-message-accept-failed-case.png 960w,
/static/332479cc410439915b40d8eb895eede9/0b602/4-message-accept-failed-case.png 1105w&quot; sizes=&quot;(max-width: 1105px) 100vw, 1105px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/332479cc410439915b40d8eb895eede9/0b602/4-message-accept-failed-case.png&quot; alt=&quot;4 message accept failed case&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
메시지 순서 불일치로 인한 데이터 누락 발생 시나리오
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;해결-전략---retry-dlt-복구-배치를-활용한-3중-안전장치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5---retry-dlt-%EB%B3%B5%EA%B5%AC-%EB%B0%B0%EC%B9%98%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-3%EC%A4%91-%EC%95%88%EC%A0%84%EC%9E%A5%EC%B9%98&quot; aria-label=&quot;해결 전략   retry dlt 복구 배치를 활용한 3중 안전장치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결 전략 - Retry, DLT, 복구 배치를 활용한 3중 안전장치&lt;/h3&gt;
&lt;p&gt;이러한 이슈를 보완하기 위해 메시지의 소비 순서를 강제로 보장할 수 없다는 가정을 바탕으로, 손실을 최소화하고 복구 능력을 확보하는 3가지 전략들을 보완했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Retry (재시도) 메커니즘&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;목적: 일시적인 메시지 도착 지연으로 인한 데이터 불일치 문제를 해결합니다.&lt;/li&gt;
&lt;li&gt;작동: 베이스 테이블(A)의 레코드(ROW)가 없을 경우, 최대 3회까지 짧은 간격으로 즉시 재시도를 수행하여 대부분의 요청을 성공시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. DLT(Dead Letter Topic) 발행&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;목적: 컨슈머 처리량 부족이나 장기간의 순서 문제로 인한 메시지 유실을 방지하고 격리합니다.&lt;/li&gt;
&lt;li&gt;작동: 3회 재시도 후에도 실패한 메시지는 별도의 DLT 토픽으로 발행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. DLT 복구 배치 (DLT Recovery Batch)를 통한 최종 정합성 보장&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;목적: 영구적인 데이터 누락을 해결하여 최종 데이터 정합성을 보장합니다.&lt;/li&gt;
&lt;li&gt;작동: DLT 리스너가 해당 메시지를 받아 별도의 테이블에 저장하고, 5분/1시간 주기로 동작하는 복구 배치를 통해 저장되지 않은 건들을 주기적으로 재처리합니다. 이는 베이스 테이블 정보가 뒤늦게 도착했을 경우의 누락을 해결합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;retry와-dlt-사용-예시&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#retry%EC%99%80-dlt-%EC%82%AC%EC%9A%A9-%EC%98%88%EC%8B%9C&quot; aria-label=&quot;retry와 dlt 사용 예시 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Retry와 DLT 사용 예시&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Retry와 DLT를 사용한 이중 백업&lt;/strong&gt; 전략을 수립할 때, 다음과 같은 가정을 바탕에 두었습니다.&lt;/br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;가정 1&lt;/strong&gt; : 연관되어 있는 두 토픽에서 발행하는 메시지는 기본적으로 짧은 텀의 간격을 두고 동시에 발행됩니다.&lt;/br&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;가정 2&lt;/strong&gt; : 메시지 발행량에 따라 이 텀은 커질 수 있으며, 기본적으로 테이블 A와 B 중의 순서는 보장 불가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위 가정을 기반으로 Retry 로직을 적용했습니다. 전체 처리 속도가 Retry 덕분에 감소할 수 있지만, 데이터 정합성이 훨씬 중요했기에 짧은 간격으로 Retry 시도하는 것만으로도 대부분의 요청이 성공하는 것을 확인했습니다.
하지만 Retry만으로도 부족한 케이스가 존재했습니다. 특정 토픽에 대해 메시지 발행량이 컨슈머의 처리량보다 커져서 offset lag이 많이 발생한다면, 몇 분의 간격으로 토픽의 메시지 소비 텀이 길어지기도 했기 때문입니다.
이처럼 Retry만으로 해결되지 않는 장기적인 지연 케이스를 수정하기 위해 실패 케이스를 저장하는 DLT 테이블과, 해당 테이블에서 데이터를 복구시키는 복구 배치가 필요했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 70%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1021px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8623d63d4d5a95d5334f8b3ae31f4310/d8785/5-DLT-process.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.083333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABuklEQVR42pVTy24UMRDcn+UI4hs4cMs/cOOAUMIphwghbkiAEChCERLKk82+x/O0x3a7qPZmkxkERGlNa6Zsd7n6MRPQqhXgHbKdXAtefbL4eBoz3jTAwRePo+8BzgMpAe9/CPa/RswMAZ+3J4I33yIWZcJEGPfzs4dZpkxweCx49LLDiw8h4zOuP3nt8OzQo7KAxIS9d4LH+xHHU8ZIwvMjwdODSDEk1CDnHEQkE0QG9F4Q4vYCVeT8di1tl+BDgnURNyHcUywZT7YLgYdvTtPqzo2wfg+x2k7AnzgTXs4L3hpvN8+vN1Qq/yTUzzgg1J04JLyaG/S9z7cIa3JBQu896rpmsGBdtqhal5lSjPcrvFoYhIFCVawK+77PeGksFvRkO0jb5pKsTPP/lJ3rUZYmv8+n65GCdelISLKuQyJhUbeYUsQu3b8q9GyMMB1hWhezYlSjFQnnVAhHJ2lFhbNV+cCUB4RLqiuaHoEpByosW4vpsrztiO1DLsMgZTPqsjbFcjabpqHX+DXfYF1UuUnqprpLWQmrRmva3hF2/KdkMBat9bnbkSVQVwU6qxqt46MNC2Hc5d1Y/QbC2KOvGoToiQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8623d63d4d5a95d5334f8b3ae31f4310/263a4/5-DLT-process.webp 480w,
/static/8623d63d4d5a95d5334f8b3ae31f4310/a6361/5-DLT-process.webp 960w,
/static/8623d63d4d5a95d5334f8b3ae31f4310/c76c1/5-DLT-process.webp 1021w&quot; sizes=&quot;(max-width: 1021px) 100vw, 1021px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8623d63d4d5a95d5334f8b3ae31f4310/9aebd/5-DLT-process.png 480w,
/static/8623d63d4d5a95d5334f8b3ae31f4310/a91f8/5-DLT-process.png 960w,
/static/8623d63d4d5a95d5334f8b3ae31f4310/d8785/5-DLT-process.png 1021w&quot; sizes=&quot;(max-width: 1021px) 100vw, 1021px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8623d63d4d5a95d5334f8b3ae31f4310/d8785/5-DLT-process.png&quot; alt=&quot;5 DLT process&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
    Retry &amp;amp; DLT 동작 flow
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;백업 로직은 다음과 같이 구성됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Base 테이블 (A)에 해당하는 레코드가 없을 경우 예외 발생 &lt;br&gt;&lt;/li&gt;
&lt;li&gt;예외 발생시 최대 3회 Retry&lt;/li&gt;
&lt;li&gt;Retry 후에도 Base 테이블이 없을 경우 DLT 로 발행&lt;/li&gt;
&lt;li&gt;DLT 리스너에서는 해당 건을 받아 따로 테이블에 저장&lt;/li&gt;
&lt;li&gt;이후 복구 배치가 돌면서 저장되지 않은 건들을 BASE 테이블에 저장 시도&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;    &lt;span class=&quot;token comment&quot;&gt;//카프카 에러 핸들러에서의 DLQ 설정&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Bean&lt;/span&gt;
    fun &lt;span class=&quot;token function&quot;&gt;kafkaListenerMultiConcurrencyContainerFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kafkaTemplate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;KafkaTemplate&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ConcurrentKafkaListenerContainerFactory&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        val factory&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ConcurrentKafkaListenerContainerFactory&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ConcurrentKafkaListenerContainerFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;consumerFactory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;memberConsumerFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;containerProperties&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ackMode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ackMode
        factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;containerProperties&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pollTimeout &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POLL_TIMEOUT&lt;/span&gt; 
        factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;containerProperties&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;idleBetweenPolls &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; idleBetweenPolls
        factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setConcurrency&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        val errorHandler &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getDlqErrorHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kafkaTemplate&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        factory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setCommonErrorHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorHandler&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; factory
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; fun &lt;span class=&quot;token function&quot;&gt;getDlqErrorHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kafkaTemplate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;KafkaTemplate&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DefaultErrorHandler&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DefaultErrorHandler&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token class-name&quot;&gt;DeadLetterPublishingRecoverer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kafkaTemplate&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ex &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
                &lt;span class=&quot;token class-name&quot;&gt;TopicPartition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;${record.topic()}.dlq&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;partition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FixedBackOff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BACK_OFF_INTERVAL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;KAFKA_CONSUMER_MAX_RETRY_ATTEMPT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;검증과-모니터링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%80%EC%A6%9D%EA%B3%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&quot; aria-label=&quot;검증과 모니터링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;검증과 모니터링&lt;/h2&gt;
&lt;p&gt;CDC 기반 데이터 파이프라인의 핵심은 실시간 데이터의 정합성과 안정성을 보장하는 것입니다. 이를 위해 Datadog 대시보드와 주기적 배치 작업을 결합한 다각적인 검증 및 모니터링 체계를 구축하였습니다.&lt;/p&gt;
&lt;h4 id=&quot;1-datadog-기반-실시간-모니터링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-datadog-%EA%B8%B0%EB%B0%98-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&quot; aria-label=&quot;1 datadog 기반 실시간 모니터링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. Datadog 기반 실시간 모니터링&lt;/h4&gt;
&lt;div style=&quot;width: 80%; margin: 0 auto; text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1023px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/907c3ae0445321c1151b783e580673ee/bb4c7/6-datadog-dashboard.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6ElEQVR42m2S62vUQBTF899b/S74raBS3CpqpGrRgmhBtEgXW3a7u4mbzb4f2e1kJjOZPI93ZnfFQgd+3MuQnHvPSZyj/g8c+ud42iGC73gV/sLk80/MX3/F2P2GpXuO5ekFDk8GODge4OEL3/LYHeDJSYhHDQ8PjjwcmPuGDydgC/hEyCMMRIQwXmEzWoCPlpCzNVJCEC1vgWs/Qm/E0RnG+DNNCGnpz5QlmEs4RVagyktLoTNopZEXJcypiJooqhqbzRoJZ6irEmWZo66re3F4IiBEbEkIzmMUO8G6xlaYBBeLCNF6gywvkNJgM1TnW0y/x4m5QEKiSiVQMoEZcDZp403YxLt+E+6wiU/TFqbRCuyWQWkNSS6MkCJ3mgbke1HqnSRRJEhCwiAsV1GIy3WI3+uhrR0+x9VNF62OZ19UKqUtc+hsK1aUFFlpoqANBQnIhEOnCmkqobVCpjKkQu2ypWyKCjIlF1pQpjmqOrPp1ruM/z8OY9xmo5SySCkR05AougVfxbQJ2csytK9nuLwYI/AEejcxweB1GbptZvseVb/D4JiH8zy/A4sFfVVGUUioNIWJ5cv7Kd4+C+E+D6gGOH05xpk7w4fG5B8fj8dwzAZGJCPhPYmkXIW0We3vzC+Be03etfwXgq3tnYXXAvEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/907c3ae0445321c1151b783e580673ee/263a4/6-datadog-dashboard.webp 480w,
/static/907c3ae0445321c1151b783e580673ee/a6361/6-datadog-dashboard.webp 960w,
/static/907c3ae0445321c1151b783e580673ee/8aada/6-datadog-dashboard.webp 1023w&quot; sizes=&quot;(max-width: 1023px) 100vw, 1023px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/907c3ae0445321c1151b783e580673ee/9aebd/6-datadog-dashboard.png 480w,
/static/907c3ae0445321c1151b783e580673ee/a91f8/6-datadog-dashboard.png 960w,
/static/907c3ae0445321c1151b783e580673ee/bb4c7/6-datadog-dashboard.png 1023w&quot; sizes=&quot;(max-width: 1023px) 100vw, 1023px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/907c3ae0445321c1151b783e580673ee/bb4c7/6-datadog-dashboard.png&quot; alt=&quot;6 datadog dashboard&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
CDC 시스템을 모니터링하는 Datadog 대시보드
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;Datadog 대시보드는 CDC 시스템의 운영 현황과 성능 지표를 실시간으로 시각화하여 데이터 흐름의 건강 상태를 파악하는 핵심 도구입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;토픽 메시지 유입 현황: 각 토픽에 얼마나 많은 메시지가 들어오고 있는지를 파악하여 실시간 데이터 로드 상태를 확인합니다.&lt;/li&gt;
&lt;li&gt;컨슈머 지연 (Kafka Lag): 토픽별로 컨슈머가 메시지를 처리하지 못하고 얼마나 뒤처지고 있는지(지연 정도)를 모니터링하여, 시스템의 처리 속도 및 병목 현상을 즉시 감지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-dlq-후처리-배치를-이용한-정합성-검증-및-복구&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-dlq-%ED%9B%84%EC%B2%98%EB%A6%AC-%EB%B0%B0%EC%B9%98%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%ED%95%A9%EC%84%B1-%EA%B2%80%EC%A6%9D-%EB%B0%8F-%EB%B3%B5%EA%B5%AC&quot; aria-label=&quot;2 dlq 후처리 배치를 이용한 정합성 검증 및 복구 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. DLQ 후처리 배치를 이용한 정합성 검증 및 복구&lt;/h4&gt;
&lt;div style=&quot;width: 80%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1390px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/59fbc801516067c952f08286ea36c983/55991/12-dlq-batch.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 42.29166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABSUlEQVR42lWSW3LDIAxFvf99tk3q2mAQYBBwK4k0M/3QeIiVw314G2Og1I67TfQ+0bih6tQq09AaQ3eYO0Ik5JLlt4bzPO3prwul3AghIqWMbc4JTwP7xQatXBHvhPuWZ4y22BTMA4/nDudOuYhxXUEu4fVewCkl5FywjTER8zBo6wB3Rmm3LZdSbHrvAuz4/HrAO/9S6Ey1996c/FMYBOii/gkCraCazDIR2bBEwBLH/nMaQIEhBHQBqgvdXQoFOAR4pY4jiKI6Lb9U83tpAVlm4CmWFajnS7JT5QrWC3RP3RiQyhDoQCoClJdqWcvQsM0yd8vwQywfx2H5ns7Zjl6gZ7VMlFaGnrqVUv9KKVKKNmelBFPLPHFIbkTxXQabZXqVkk3ANgVIuRt0ZSgK621L2lrKqxS2lr+lZbc+F78sq3U9R9JPquAX5eJuHuH1wKQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/59fbc801516067c952f08286ea36c983/263a4/12-dlq-batch.webp 480w,
/static/59fbc801516067c952f08286ea36c983/a6361/12-dlq-batch.webp 960w,
/static/59fbc801516067c952f08286ea36c983/6b515/12-dlq-batch.webp 1390w&quot; sizes=&quot;(max-width: 1390px) 100vw, 1390px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/59fbc801516067c952f08286ea36c983/9aebd/12-dlq-batch.png 480w,
/static/59fbc801516067c952f08286ea36c983/a91f8/12-dlq-batch.png 960w,
/static/59fbc801516067c952f08286ea36c983/55991/12-dlq-batch.png 1390w&quot; sizes=&quot;(max-width: 1390px) 100vw, 1390px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/59fbc801516067c952f08286ea36c983/55991/12-dlq-batch.png&quot; alt=&quot;12 dlq batch&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
DLT 복구 배치
&lt;/figcaption&gt;
&lt;/div&gt;
데이터 유실이나 정합성 문제를 방지하기 위해 복구 메커니즘과 검증 프로세스를 주기적인 Batch로 처리했습니다.
&lt;ul&gt;
&lt;li&gt;검증 배치: 실제 데이터가 소스와 타겟 간에 정확하게 동기화되었는지를 확인하는 배치 작업입니다.&lt;/li&gt;
&lt;li&gt;DLT 복구 배치: 실패 메시지가 격리된 DLT(Dead Letter Topic) 테이블에 쌓이면, 이 배치 작업이 주기적으로 돌면서 저장되지 않은 메시지들을 재처리하여 누락된 데이터가 없도록 복구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 검증 배치 및 복구 배치의 수행 내역과 복구 현황 또한 모니터링 대시보드에서 확인할 수 있도록 구성하여, 데이터 무결성을 이중으로 보장합니다.&lt;/p&gt;
&lt;h2 id=&quot;최종-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%A2%85-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;최종 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;최종 구현&lt;/h2&gt;
&lt;p&gt;최종적으로 구축된 CDC 아키텍처는 다음과 같습니다.&lt;/p&gt;
&lt;div style=&quot;width: 70%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1809px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0457607b6aec0bc7962a4baced69a78e/39979/final-architecture.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 58.958333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABgUlEQVR42oVT2W7CMBDM//9Rn9uXPlRtRW9AFASCHJDTdnxONw4hBiG6ysrreHcys+tEzjrAkaNbxtUY4+OPlwme7u4xWHd2yrNjTDsfR+WsQr1m0LqF2O6g6gqcSyQrCWMtrNTQB07nBlopiDiGzHNoq1EuKlTLGpSFdDtFliwQ8YOAKASspYK6hmlbih02qwSz6QzWWSo29M561prRx0Xb8UGZVmA59/w4yyF4iaij2SWHkjrL8j1+Hl9hKmJXC8hdAaV1IBFeQdgmL7kHHJMGUyTJbiu4UsA1EnZTQhPDM8Bgn8ZTHLLfI6C7AmgU9aomiS1JlFA5STXnDG3AUJDk9iT5RLtPTEqF5wVDwXRfeJRz6SFDP8BLyQNgVkm8rWiCXPvrcAsQzj/43n9hns/HoYzADmkpMVk2R8C+4JqFkhvVgGl2HTAjwPdV/T/DAFC2CkrpHtBd9LBgCvMdA2sNbpkN/pRdnCBOUkThHRp6uc44Hj4L38uw8JoPJqX0/gdQ76yqc6WzKQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0457607b6aec0bc7962a4baced69a78e/263a4/final-architecture.webp 480w,
/static/0457607b6aec0bc7962a4baced69a78e/a6361/final-architecture.webp 960w,
/static/0457607b6aec0bc7962a4baced69a78e/56825/final-architecture.webp 1809w&quot; sizes=&quot;(max-width: 1809px) 100vw, 1809px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0457607b6aec0bc7962a4baced69a78e/9aebd/final-architecture.png 480w,
/static/0457607b6aec0bc7962a4baced69a78e/a91f8/final-architecture.png 960w,
/static/0457607b6aec0bc7962a4baced69a78e/39979/final-architecture.png 1809w&quot; sizes=&quot;(max-width: 1809px) 100vw, 1809px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0457607b6aec0bc7962a4baced69a78e/39979/final-architecture.png&quot; alt=&quot;final architecture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
최종 아키텍쳐&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;OGG-kafka 기반의 CDC 최종 아키텍처는 데이터의 종류에 따라 처리 경로를 이원화하고, 재시도(Retry)와 DLT(Dead Letter Topic)를 활용한
복합적인 복구 전략을 통해 데이터 정합성과 안정성을 극대화했습니다.&lt;/p&gt;
&lt;h3 id=&quot;️데이터-처리-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot;️데이터 처리 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;️데이터 처리 방식&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;이원화된 경로: 데이터를 단순 베이스 정보와 복잡한 부가 정보로 나누어, 각 데이터의 특성에 맞는 최적의 경로로 처리했습니다.&lt;/li&gt;
&lt;li&gt;커넥터(OGG) 담당: 가장 변경량이 많은 핵심 데이터는 커넥터를 활용해 신속하고 안정적인 ADW(Analytical Data Warehouse) 적재를 담당합니다.&lt;/li&gt;
&lt;li&gt;워커(Campaign-Worker) 담당: 약관 동의, 마케팅 정보 등 여러 테이블의 변경 사항을 받아 복잡한 조인 및 변환 로직을 수행하여 타겟 테이블에 적재합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
🛡️ 복합적인 비즈니스 로직 환경에서 메시지 순서 불일치로 인한 데이터 손실을 방지하기 위해 3단계의 방어 메커니즘을 구축했습니다. 회원 기본 정보(Base) 메시지가 부가 정보 메시지보다 늦게 도착하여 발생하는 일시적인 데이터 불일치 문제를 해결하기 위해, Campaign-Worker는 매칭되는 회원 번호(MBR_NO)가 없으면 최대 3회까지 메시지 처리를 재시도(Retry)하여 순서 지연을 해소합니다. 이후 3회 재시도 후에도 처리에 실패한 메시지는 DLT(Dead Letter Topic)로 이동시켜 별도의 테이블에 안전하게 저장합니다. 이는 메시지 유실을 방지하는 1차적인 격리 조치입니다. 마지막으로 복구 배치가 DLT 테이블에 쌓인 실패 메시지들을 대상으로 주기적인 실행(5분 및 1시간 주기)을 통해 누락된 기본 정보가 타겟에 반영된 후 실패 메시지를 재처리하여 최종적인 데이터 정합성을 보장합니다.&lt;/blockquote&gt;
&lt;h2 id=&quot;성과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EA%B3%BC&quot; aria-label=&quot;성과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;성과&lt;/h2&gt;
&lt;p&gt;ODI 기반(Batch)에서 OGG/Kafka 기반(CDC)으로 전환하며 다음과 같은 개선 사항을 달성했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터 처리 속도: 20분 ~ 1시간 주기 (배치)에서 준실시간 (이벤트 기반)으로 개선&lt;/li&gt;
&lt;li&gt;고객 대응 능력: 실시간 고객 행동에 대한 지연 대응에서 벗어나, 변경 발생 즉시 타겟팅 가능 (예: 마케팅 동의 즉각 반영)&lt;/li&gt;
&lt;li&gt;소스 DB 부하: 특정 시간대에 부하가 집중되던 문제에서 트랜잭션 로그 기반으로 전환되어 부하 분산 및 최소화&lt;/li&gt;
&lt;li&gt;데이터 정합성 보장: 순서 이슈 발생 시 데이터 누락 위험이 있었으나, Retry, DLT, 복구 배치를 통한 이중 안정성 확보로 극복&lt;/li&gt;
&lt;li&gt;모니터링: Datadog과 Slack 등을 활용하여 메시지 누락 발생이나 소비 이상 시 즉시 탐지 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이번 프로젝트는 올리브영 캠페인 시스템의 데이터 파이프라인 핵심 문화를 &lt;strong&gt;&apos;주기적 동기화&apos;&lt;/strong&gt; 에서 &lt;strong&gt;&apos;이벤트 기반의 실시간 반응&apos;&lt;/strong&gt; 으로 변화시켰다는 점에 큰 의의가 있습니다.&lt;/p&gt;
&lt;p&gt;특히, ODI의 1:N 조인 변환 로직을 OGG 커넥터와 Campaign-Worker의 하이브리드 구조로 분리하고, 메시지 순서 문제 해결을 위해
DLT와 복구 배치를 적용한 것은 실시간 스트리밍 환경에서 복잡한 비즈니스 로직과 엔터프라이즈급 안정성을 동시에 확보한 전략이었습니다.&lt;/p&gt;
&lt;p&gt;이러한 준실시간 데이터 환경을 통해 올리브영은 고객의 행동 변화에 즉각적으로 대응하는 개인화된 캠페인을 제공할 수 있는 견고하고 확장 가능한 시스템을 구축하며, 고객 경험 측면에서 한 단계 더 성장할 수 있었습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[권한마다 보안 솔루션이 다르다면? 하이브리드 환경에서 우아하게 파일 보호하기]]></title><description><![CDATA[목차 들어가며: 변화의 지침과 현실적인 제약 해결책 탐색: 왜 전략 패턴인가? 기술적 설계: 전략 패턴을 통한 추상화 AOP를 활용한 자동 적용 삽질기: 실제 적용하면서 마주한 문제들 마치며…]]></description><link>https://oliveyoung.tech/2025-12-26/protection-switch/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-26/protection-switch/</guid><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0-%EB%B3%80%ED%99%94%EC%9D%98-%EC%A7%80%EC%B9%A8%EA%B3%BC-%ED%98%84%EC%8B%A4%EC%A0%81%EC%9D%B8-%EC%A0%9C%EC%95%BD&quot;&gt;들어가며: 변화의 지침과 현실적인 제약&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-%ED%95%B4%EA%B2%B0%EC%B1%85-%ED%83%90%EC%83%89-%EC%99%9C-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4%EC%9D%B8%EA%B0%80&quot;&gt;해결책 탐색: 왜 전략 패턴인가?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%84%A4%EA%B3%84-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%B6%94%EC%83%81%ED%99%94&quot;&gt;기술적 설계: 전략 패턴을 통한 추상화&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-aop%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9E%90%EB%8F%99-%EC%A0%81%EC%9A%A9&quot;&gt;AOP를 활용한 자동 적용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-%EC%82%BD%EC%A7%88%EA%B8%B0-%EC%8B%A4%EC%A0%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EB%A9%B4%EC%84%9C-%EB%A7%88%EC%A3%BC%ED%95%9C-%EB%AC%B8%EC%A0%9C%EB%93%A4&quot;&gt;삽질기: 실제 적용하면서 마주한 문제들&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-%EB%A7%88%EC%B9%98%EB%A9%B0&quot;&gt;마치며&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;1-들어가며-변화의-지침과-현실적인-제약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0-%EB%B3%80%ED%99%94%EC%9D%98-%EC%A7%80%EC%B9%A8%EA%B3%BC-%ED%98%84%EC%8B%A4%EC%A0%81%EC%9D%B8-%EC%A0%9C%EC%95%BD&quot; aria-label=&quot;1 들어가며 변화의 지침과 현실적인 제약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 들어가며: 변화의 지침과 현실적인 제약&lt;/h1&gt;
&lt;p&gt;올리브영의 오프라인 매장 운영을 책임지는 백오피스 시스템은 매일 수많은 엑셀과 PDF 문서를 생성하고 처리합니다. 그동안 우리는 이 문서들을 보호하기 위해 표준화된 &lt;strong&gt;설치형 에이전트(Agent) 보안 솔루션&lt;/strong&gt;을 사용하여 파일을 암호화해 왔습니다.&lt;/p&gt;
&lt;p&gt;최근 전사 보안 정책이 고도화됨에 따라, 문서 보안 체계를 &lt;strong&gt;신규 클라우드 기반 보안 시스템&lt;/strong&gt;으로 전환하게 되었습니다. 이에 따라 백오피스 시스템 내 파일 암호화가 적용된 모든 영역을 새로운 방식으로 전환해야 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;하지만 분석 단계에서 중요한 현실적 제약에 직면했습니다. 신규 도입된 클라우드 보안 시스템은 라이선스 및 인프라 구조상 &apos;본사 구성원&apos;에게만 지원된다는 점이었습니다.&lt;/p&gt;
&lt;p&gt;백오피스 시스템은 본사 구성원뿐만 아니라, 매장에서 근무하는 &apos;매장 구성원&apos;이나 외부 &apos;협력사&apos;들도 빈번하게 사용합니다. 결국, 우리는 두 가지 보안 체계를 동시에 운영해야 하는 상황이 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;본사 임직원: 신규 클라우드 기반 보안 (Type A)&lt;/li&gt;
&lt;li&gt;그 외 (매장/협력사): 기존 에이전트 기반 보안 (Type B)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;즉, &lt;strong&gt;&quot;로그인한 사용자의 권한(Role)에 따라 적절한 암호화 솔루션을 동적으로 선택하여 적용&quot;&lt;/strong&gt;해야 하는 과제가 주어진 것입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;2-해결책-탐색-왜-전략-패턴인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%ED%95%B4%EA%B2%B0%EC%B1%85-%ED%83%90%EC%83%89-%EC%99%9C-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;2 해결책 탐색 왜 전략 패턴인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 해결책 탐색: 왜 전략 패턴인가?&lt;/h1&gt;
&lt;p&gt;처음부터 전략 패턴을 떠올린 건 아니었습니다. 가장 먼저 떠오른 건 역시 단순한 if-else 분기였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;21-첫-번째-시도-단순-if-else-분기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#21-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EC%8B%9C%EB%8F%84-%EB%8B%A8%EC%88%9C-if-else-%EB%B6%84%EA%B8%B0&quot; aria-label=&quot;21 첫 번째 시도 단순 if else 분기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.1. 첫 번째 시도: 단순 if-else 분기&lt;/h2&gt;
&lt;p&gt;&quot;일단 돌아가게 만들자&quot;는 생각으로 빠르게 프로토타입을 작성해봤습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 첫 번째 시도: 단순 if-else&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;downloadSalesReport&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; excelData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; excelService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createExcel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isHeadquarters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 클라우드 API 호출&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cloudSecurityApi&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;encrypt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;excelData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 에이전트 라이브러리 호출&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; agentSecurityLib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;excelData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;간단해 보이지만, 실제로 적용하려니 문제가 보이기 시작했습니다.&lt;/p&gt;
&lt;p&gt;백오피스 시스템에는 파일을 다운로드하는 API가 수십 개에 달했습니다. 매출 리포트, 재고 현황, 발주서, 정산 내역... 이 모든 곳에 동일한 if-else 로직을 복붙해야 했습니다. 그리고 만약 향후 제3의 보안 솔루션이 추가된다면? 수십 개의 파일을 다시 찾아다니며 else if를 추가해야 하는 악몽이 그려졌습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;22-두-번째-시도-별도-서비스-클래스로-분리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#22-%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%8B%9C%EB%8F%84-%EB%B3%84%EB%8F%84-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A1%9C-%EB%B6%84%EB%A6%AC&quot; aria-label=&quot;22 두 번째 시도 별도 서비스 클래스로 분리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.2. 두 번째 시도: 별도 서비스 클래스로 분리&lt;/h2&gt;
&lt;p&gt;그렇다면 암호화 로직을 별도 서비스로 분리하면 어떨까요?&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 두 번째 시도: 서비스 클래스 분리&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileEncryptionService&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;encrypt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isHeadquarters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cloudSecurityApi&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;encrypt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; agentSecurityLib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용하는 쪽&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;exportExccel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; excelData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; excelService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createExcel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fileEncryptionService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;encrypt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;excelData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 호출 필요&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;if-else가 한 곳으로 모이긴 했지만, 여전히 두 가지 문제가 남았습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;호출 누락 위험&lt;/strong&gt;: 모든 다운로드 메서드에서 &lt;code class=&quot;language-text&quot;&gt;fileEncryptionService.encrypt()&lt;/code&gt;를 명시적으로 호출해야 합니다. 신규 API를 만들 때 깜빡하면? 암호화되지 않은 파일이 그대로 내려갑니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OCP 위반&lt;/strong&gt;: 새로운 보안 솔루션이 추가되면 &lt;code class=&quot;language-text&quot;&gt;FileEncryptionService&lt;/code&gt; 내부를 수정해야 합니다. &quot;확장에는 열려 있고, 수정에는 닫혀 있어야 한다&quot;는 개방-폐쇄 원칙(OCP)에 어긋납니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h2 id=&quot;23-세-번째-시도-팩토리-패턴&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#23-%EC%84%B8-%EB%B2%88%EC%A7%B8-%EC%8B%9C%EB%8F%84-%ED%8C%A9%ED%86%A0%EB%A6%AC-%ED%8C%A8%ED%84%B4&quot; aria-label=&quot;23 세 번째 시도 팩토리 패턴 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.3. 세 번째 시도: 팩토리 패턴?&lt;/h2&gt;
&lt;p&gt;잠깐, 팩토리 패턴은 어떨까요? 사용자 타입에 따라 적절한 암호화 객체를 생성해서 반환하면 되지 않을까요?&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 팩토리 패턴 검토&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;EncryptorFactory&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Encryptor&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isHeadquarters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CloudEncryptor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AgentEncryptor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;팩토리 패턴은 &lt;strong&gt;객체 생성&lt;/strong&gt;에 초점을 맞춘 패턴입니다. 반면 우리가 필요한 건 이미 존재하는 암호화 구현체 중 하나를 &lt;strong&gt;선택&lt;/strong&gt;하는 것이었습니다.&lt;/p&gt;
&lt;p&gt;매번 &lt;code class=&quot;language-text&quot;&gt;new&lt;/code&gt;로 객체를 생성하는 방식은 여러 측면에서 적합하지 않았습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Spring 컨테이너 외부의 객체&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;new&lt;/code&gt;로 생성한 객체는 Spring 컨테이너가 관리하지 않습니다. 이 말은 &lt;code class=&quot;language-text&quot;&gt;@Autowired&lt;/code&gt;를 통한 의존성 주입, AOP 프록시, 트랜잭션 관리 등 Spring이 제공하는 이점을 전혀 활용할 수 없다는 의미입니다. 실제로 우리의 암호화 구현체들은 내부적으로 다른 Spring 빈(API 클라이언트, 설정 정보 등)에 의존하고 있었기에 이는 치명적인 단점이었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;싱글톤 빈의 설계 철학과 불일치&lt;/strong&gt;: Spring의 기본 빈 스코프는 싱글톤입니다. 애플리케이션 전체에서 하나의 인스턴스만 생성하여 재사용함으로써 메모리 효율성과 일관성을 보장합니다. 팩토리 패턴으로 매번 새 인스턴스를 생성하는 것은 이러한 Spring의 설계 철학에 역행하는 방식이었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결론적으로, 우리에게 필요한 것은 &lt;strong&gt;&quot;객체를 생성&quot;&lt;/strong&gt;하는 것이 아니라 &lt;strong&gt;&quot;이미 Spring 컨테이너가 관리하고 있는 싱글톤 빈 중 적절한 것을 선택&quot;&lt;/strong&gt;하는 것이었습니다. 이 요구사항에 가장 부합하는 패턴이 바로 전략 패턴이었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;24-결론-전략-패턴--aop-조합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#24-%EA%B2%B0%EB%A1%A0-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4--aop-%EC%A1%B0%ED%95%A9&quot; aria-label=&quot;24 결론 전략 패턴  aop 조합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.4. 결론: 전략 패턴 + AOP 조합&lt;/h2&gt;
&lt;p&gt;고민 끝에 내린 결론은 &lt;strong&gt;전략 패턴&lt;/strong&gt;과 &lt;strong&gt;AOP&lt;/strong&gt;의 조합이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;전략 패턴&lt;/strong&gt;: 암호화 방식을 인터페이스로 추상화하고, 각 구현체를 독립적으로 관리합니다. 새로운 보안 솔루션이 추가되면 기존 코드 수정 없이 구현체만 추가하면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AOP&lt;/strong&gt;: 개발자가 명시적으로 암호화 메서드를 호출할 필요 없이, 어노테이션 하나로 자동 적용됩니다. 호출 누락의 위험이 사라집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방식&lt;/th&gt;
&lt;th&gt;중복 제거&lt;/th&gt;
&lt;th&gt;확장성&lt;/th&gt;
&lt;th&gt;누락 방지&lt;/th&gt;
&lt;th&gt;채택&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;if-else 분기&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서비스 클래스 분리&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;팩토리 패턴&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;전략 패턴 + AOP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;채택&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;3-기술적-설계-전략-패턴을-통한-추상화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%84%A4%EA%B3%84-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%B6%94%EC%83%81%ED%99%94&quot; aria-label=&quot;3 기술적 설계 전략 패턴을 통한 추상화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 기술적 설계: 전략 패턴을 통한 추상화&lt;/h1&gt;
&lt;p&gt;전략 패턴이 낯선 분들을 위해 설명드리자면, 전략 패턴은 &apos;게임기&apos;와 &apos;게임 팩&apos;의 관계로 비유할 수 있습니다. 게임기(비즈니스 로직)는 꽂는 팩(전략: 클라우드 보안 또는 에이전트 보안)의 종류에 따라 플레이할 게임(암호화 방식)이 달라지죠. 이와 유사하게 우리는 사용자가 누구냐에 따라 알맞은 전략을 자동으로 반영하는 시스템을 설계했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;31-전략-인터페이스와-구현체&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#31-%EC%A0%84%EB%9E%B5-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%99%80-%EA%B5%AC%ED%98%84%EC%B2%B4&quot; aria-label=&quot;31 전략 인터페이스와 구현체 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.1. 전략 인터페이스와 구현체&lt;/h2&gt;
&lt;p&gt;먼저 &lt;code class=&quot;language-text&quot;&gt;FileProtectionStrategy&lt;/code&gt;라는 인터페이스(규격)를 정의하고, 각각의 보안 방식을 따르는 구현체(게임 팩)를 만들었습니다. 이렇게 하면 향후 또 다른 보안 솔루션이 추가되더라도 기존 코드를 수정할 필요 없이 새로운 구현체만 추가하면 됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/** 전략 식별자 (예: &quot;CLOUD&quot;, &quot;AGENT&quot;) */&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/** 파일 보호(암호화) 처리 */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/** 파일 보호 해제(복호화) 처리 */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;unprotect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; protectedBytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;각각의 구현체는 전략에 맞는 API 혹은 라이브러리를 호출하여 실제 암호화를 수행합니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * 전략 A: 신규 클라우드 보안 구현체
 */&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CloudFileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CLOUD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// GW API를 호출하여 클라우드 방식 암호화 수행&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; 
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ... unprotect 구현 생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * 전략 B: 기존 에이전트 보안 구현체
 */&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AgentFileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AGENT&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 기존 에이전트 방식 암호화 수행&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ... unprotect 구현 생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;32-전략-선택기-resolver&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#32-%EC%A0%84%EB%9E%B5-%EC%84%A0%ED%83%9D%EA%B8%B0-resolver&quot; aria-label=&quot;32 전략 선택기 resolver permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.2. 전략 선택기 (Resolver)&lt;/h2&gt;
&lt;p&gt;다음으로, 런타임 시점에 필요한 전략을 빠르게 찾을 수 있도록 Resolver를 구성했습니다.&lt;/p&gt;
&lt;p&gt;여기서 주목할 점은 생성자에서 &lt;code class=&quot;language-text&quot;&gt;List&amp;lt;FileProtectionStrategy&gt;&lt;/code&gt;를 주입받는 부분입니다. 이는 Spring의 &lt;strong&gt;다중 빈 자동 주입(Autowiring Collections)&lt;/strong&gt; 기능을 활용한 것으로, 동일한 인터페이스를 구현한 모든 빈을 한 번에 주입받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;전략의 개수가 적을 때는 리스트를 순회해도 문제가 없지만, 호출 빈도가 매우 높은 보안 모듈 특성상 확장 시에도 성능 저하가 없는 구조를 목표로 했습니다. 따라서 주입받은 전략 리스트를 &lt;strong&gt;애플리케이션 시작 시점에 단 한 번만 Map으로 변환하여 캐싱&lt;/strong&gt;해둠으로써, 런타임에는 별도의 조건문(if-else)이나 리스트 순회 없이 &lt;strong&gt;O(1)&lt;/strong&gt; 시간 복잡도로 전략을 즉시 조회할 수 있도록 구현했습니다.&lt;/p&gt;
&lt;p&gt;이 접근법의 장점은 &lt;strong&gt;새로운 전략 구현체를 추가할 때 Resolver 코드를 전혀 수정할 필요가 없다&lt;/strong&gt;는 것입니다. &lt;code class=&quot;language-text&quot;&gt;@Component&lt;/code&gt;로 등록된 새 전략 빈은 애플리케이션 시작 시점에 Spring이 자동으로 감지하여 리스트에 포함시킵니다. 이는 OCP(개방-폐쇄 원칙)를 자연스럽게 만족하는 구조입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고문서: &lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/annotation-config/autowired.html&quot;&gt;Spring 공식 문서 - Using @Autowired&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategyResolver&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; registry&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 생성자 주입 시점에 모든 전략 구현체를 Map으로 변환하여 캐싱&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategyResolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; strategies&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;registry &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; strategies&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Collectors&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;identity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;/**
     * 지정한 전략 식별자(Type)로 구현체를 O(1)로 조회
     */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; strategy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; registry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;strategy &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IllegalArgumentException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;지원하지 않는 보안 타입입니다: &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; strategy&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;4-aop를-활용한-자동-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-aop%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9E%90%EB%8F%99-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;4 aop를 활용한 자동 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. AOP를 활용한 자동 적용&lt;/h1&gt;
&lt;p&gt;마지막 퍼즐은 &lt;strong&gt;AOP(Aspect Oriented Programming)&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;앞서 전략 패턴을 게임과 게임 팩에 비유했다면, AOP는 공항 보안 검색대에 가깝습니다. 파일을 생성하는 모든 코드마다 &lt;code class=&quot;language-text&quot;&gt;resolve()&lt;/code&gt;를 호출하고 &lt;code class=&quot;language-text&quot;&gt;protect()&lt;/code&gt;를 실행하는 방식은 번거로울 뿐 아니라, 호출을 누락할 위험도 있습니다. 이런 반복적이고 빠뜨리기 쉬운 보안 처리를 AOP에 맡기면 훨씬 안정적으로 관리할 수 있습니다.
마치 승객이 비행기를 타기 전에 반드시 보안 검색대를 지나가야 하듯, AOP는 비즈니스 로직으로 진입하기 전에 데이터를 자동으로 검사하고 통과시키는 관문 역할을 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;우리는 @ProtectFile이라는 커스텀 어노테이션만 붙이면, AOP가 자동으로 개입하여 보안 처리를 수행하도록 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Aspect&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProtectFileAspect&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategyResolver&lt;/span&gt; resolver&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProtectFileAspect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategyResolver&lt;/span&gt; resolver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;resolver &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; resolver&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Around&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@annotation(protectFile)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;aroundProtect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProceedingJoinPoint&lt;/span&gt; pjp&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; protectFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Throwable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 1. 비즈니스 로직 실행 (파일 생성/다운로드)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// (예시를 위해 반환 타입이 byte[]라고 가정)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; raw &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; pjp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;proceed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; 

        &lt;span class=&quot;token comment&quot;&gt;// 2. 보안 전략 결정&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 실제 운영 코드에서는 사용자 세션 정보(SessionUser)를 기반으로&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// &quot;CLOUD&quot; 또는 &quot;AGENT&quot; 타입을 동적으로 결정합니다.&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; securityType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCurrentUserSecurityType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; 

        &lt;span class=&quot;token comment&quot;&gt;// 3. 전략 조회 및 암호화 수행&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; strategy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; resolver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;securityType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        
        &lt;span class=&quot;token comment&quot;&gt;// 4. 암호화된 결과 반환&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// (전략 구현체 내부에서 처리)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; strategy&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sample_report.pdf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 사용자 권한에 따른 보안 타입 결정 로직 (예시)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCurrentUserSecurityType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 실제로는 SecurityContextHolder 등에서 사용자 권한을 확인합니다.&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// return userContext.isHeadquarters() ? &quot;CLOUD&quot; : &quot;AGENT&quot;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CLOUD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 데모를 위해 고정값 사용&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이제 비즈니스 로직 개발자는 파일 생성 코드에 집중하고, 아래처럼 @ProtectFile 어노테이션 하나만 붙여주면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@ProtectFile&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이 어노테이션 하나로 암호화 로직이 자동 적용됩니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;downloadSalesReport&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 순수 비즈니스 로직 (파일 생성)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; reportService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createPdf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;5-삽질기-실제-적용하면서-마주한-문제들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EC%82%BD%EC%A7%88%EA%B8%B0-%EC%8B%A4%EC%A0%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EB%A9%B4%EC%84%9C-%EB%A7%88%EC%A3%BC%ED%95%9C-%EB%AC%B8%EC%A0%9C%EB%93%A4&quot; aria-label=&quot;5 삽질기 실제 적용하면서 마주한 문제들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 삽질기: 실제 적용하면서 마주한 문제들&lt;/h1&gt;
&lt;p&gt;설계는 깔끔했지만, 실제 적용 과정은 순탄치 않았습니다. 여기서는 개발 중 마주했던 문제들과 해결 과정을 공유합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;51-로컬-개발-환경에서-에이전트-솔루션이-동작하지-않는-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#51-%EB%A1%9C%EC%BB%AC-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%86%94%EB%A3%A8%EC%85%98%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;51 로컬 개발 환경에서 에이전트 솔루션이 동작하지 않는 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5.1. 로컬 개발 환경에서 에이전트 솔루션이 동작하지 않는 문제&lt;/h2&gt;
&lt;p&gt;가장 먼저 부딪힌 벽은 로컬 개발 환경이었습니다.&lt;/p&gt;
&lt;p&gt;클라우드 보안 API는 개발 환경에서도 문제없이 동작했지만, 기존 에이전트 기반 솔루션은 상황이 달랐습니다. 에이전트 솔루션은 &lt;strong&gt;네이티브 라이브러리&lt;/strong&gt;에 의존하고 있었는데, 이 라이브러리가 macOS 아키텍처를 지원하지 않았습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 문제: macOS 로컬 환경에서 에이전트 솔루션 호출 시&lt;/span&gt;
agentSecurityLib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// -&gt; UnsatisfiedLinkError: 네이티브 라이브러리 로드 실패&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;대부분의 개발자가 macOS 환경에서 개발하고 있었기 때문에, 에이전트 방식 암호화를 테스트하려면 매번 개발 서버에 배포하거나 Docker 환경에서 실행해야 했습니다. 로컬에서 코드 수정 후 즉시 테스트하면 피드백 루프가 1~2분이면 충분하지만, 개발 서버 배포를 거치면 커밋 → CI/CD 파이프라인 → 서버 재시작까지 적지 않은 시간이 소요되어 개발 생산성이 크게 저하되는 문제가 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;해결책: 로컬 환경용 Mock 전략 구현체&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 로컬 개발 환경에서만 활성화되는 Mock 전략을 추가하여, 로컬 환경에서는 Mock 전략이 에이전트 전략을 대체하도록 설정했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Profile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;local&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 로컬 환경에서만 활성화&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MockFileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FileProtectionStrategy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AGENT&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 에이전트 타입을 대체&lt;/span&gt;

    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;protect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 실제 암호화 없이 원본 그대로 반환&lt;/span&gt;
        log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;[Mock] 에이전트 암호화 스킵: {}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; inputBytes&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이제 우리는 실제 암호화는 수행하지 않지만, 전략 패턴의 흐름은 그대로 테스트할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;실제 암호화 동작 검증이 필요할 때만 개발 서버에 배포하면 되니, 암호화 에이전트를 사용할 수 없는 macOS 환경에서 코드와 흐름에 대한 불확실성을 제거할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이는 인프라의 종속성에서 개발 환경을 완전히 독립시킨 의미 있는 리팩터링었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;52-기존-코드에-어노테이션-일괄-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#52-%EA%B8%B0%EC%A1%B4-%EC%BD%94%EB%93%9C%EC%97%90-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EC%9D%BC%EA%B4%84-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;52 기존 코드에 어노테이션 일괄 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5.2. 기존 코드에 어노테이션 일괄 적용&lt;/h2&gt;
&lt;p&gt;마지막으로, 기존에 존재하는 수십 개의 다운로드 API에 &lt;code class=&quot;language-text&quot;&gt;@ProtectFile&lt;/code&gt; 어노테이션을 붙이는 작업이 남아있었습니다.&lt;/p&gt;
&lt;p&gt;단순 작업이라 생각했지만, 막상 시작하니 까다로운 케이스들이 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파일이 아닌 JSON을 반환하는 API에 실수로 어노테이션을 붙이면?&lt;/li&gt;
&lt;li&gt;이미 다른 방식으로 암호화가 적용된 레거시 코드는?&lt;/li&gt;
&lt;li&gt;암호화가 필요 없는 공개 문서(약관 등)는?&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;해결책: API 목록화 + 꼼꼼한 QA 테스트&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;무작정 적용하기보다, 먼저 모든 파일 다운로드 API를 목록화하고 각각의 특성을 파악했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[파일 다운로드 API 체크리스트]
✅ /api/report/sales - 매출 리포트 (암호화 필요)
✅ /api/report/inventory - 재고 현황 (암호화 필요)
⬜ /api/template - 양식받기 (공개 문서, 암호화 불필요)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;목록화 이후에는 QA 엔지니어분과 함께 꼼꼼한 테스트 과정을 거쳤습니다. 사용자 권한별로 파일을 다운로드하고, 암호화가 정상 적용되는지 하나하나 검증했습니다.&lt;/p&gt;
&lt;p&gt;이 자리를 빌려 늦은 시간까지 수십 개의 API를 빠짐없이 테스트해주신 QA팀 여러분께 깊은 감사를 전합니다.🙇‍♀️&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;6-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;6 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6. 마치며&lt;/h1&gt;
&lt;p&gt;이번 프로젝트는 전사적인 보안 체계 전환이라는 미션을 수행함과 동시에, 현장의 다양한 사용자 환경을 모두 고려해야 하는 과제였습니다.&lt;/p&gt;
&lt;p&gt;전략 패턴과 AOP의 조합으로 본사 구성원에게는 최신 클라우드 보안 환경을, 매장 구성원에게는 익숙한 기존 환경을 제공하면서도 코드의 복잡도는 최소화할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;단순히 &quot;돌아가는 코드&quot;를 넘어, 변화에 유연하게 대처할 수 있는 &quot;좋은 구조&quot;를 고민했던 이번 경험이, 복잡한 레거시 환경과 새로운 요구사항 사이에서 고민하는 개발자분들에게 작은 힌트가 되기를 바랍니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[한 기기에 개발·운영 앱을 동시에 설치하는 방법: 올리브영 DEV/PROD 환경 분리]]></title><description><![CDATA[안녕하세요. 올리브영의 앱 개발을 맡고 있는 Hz, 송편입니다. 이번 글에서는 개발용 앱(이하 ‘개발 앱’) 과 실제 운영용 앱(이하 ‘운영 앱’)을 완전히 분리해 관리하게 된 과정을 공유합니다.
특히 Android(Product Flavor…]]></description><link>https://oliveyoung.tech/2025-12-24/dev-prod-app-environment-separation/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-24/dev-prod-app-environment-separation/</guid><pubDate>Wed, 24 Dec 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영의 앱 개발을 맡고 있는 Hz, 송편입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 개발용 앱(이하 ‘개발 앱’) 과 실제 운영용 앱(이하 ‘운영 앱’)을 완전히 분리해 관리하게 된 과정을 공유합니다.
특히 Android(Product Flavor)와 iOS(xcconfig)에서 &lt;strong&gt;환경을 코드 밖 설정으로 고정&lt;/strong&gt;하고, CI/CD까지 연결해 &lt;strong&gt;운영 배포 리스크를 줄인 방법&lt;/strong&gt;을 정리했습니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;왜-환경을-분리해야-했는가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-%ED%99%98%EA%B2%BD%EC%9D%84-%EB%B6%84%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%96%88%EB%8A%94%EA%B0%80&quot; aria-label=&quot;왜 환경을 분리해야 했는가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 환경을 분리해야 했는가&lt;/h1&gt;
&lt;h2 id=&quot;-커뮤니케이션-비용을-줄이고-싶어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%BB%A4%EB%AE%A4%EB%8B%88%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B9%84%EC%9A%A9%EC%9D%84-%EC%A4%84%EC%9D%B4%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 커뮤니케이션 비용을 줄이고 싶어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;① 커뮤니케이션 비용을 줄이고 싶어요&lt;/h2&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;프로덕트팀 🍊&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 2:34&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;안녕하세요! 이번 업데이트 화면 확인하려고 하는데, 지금 제 폰에 깔려있는 앱이 개발용인가요 아니면 운영용인가요?&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;디자인팀 🐰&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 3:15&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;새로운 이벤트 페이지 확인하고 싶은데 개발 서버로 바꾸려면 어떻게 해야 하나요? 앱을 삭제하고 다시 깔아야 하나요?&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;QA팀 🦫&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 4:22&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;지금 운영앱으로 보고 있는데 개발중인 기능이 안보이네요. 혹시 제가 잘못 보고 있는 건가요?&lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d; margin-top: 8px;&quot;&gt;아, 개발앱을 봐야 하는군요. 운영앱 지우고 개발앱 다시 설치할게요...&lt;/div&gt;
&lt;/div&gt;
&lt;figcaption style=&quot;text-align: center;&quot;&gt;환경 분리 전, 반복되던 문의들&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;이런 상황은 왜 발생했을까요?&lt;/p&gt;
&lt;p&gt;올리브영 앱은 개발용 앱과 운영용 앱이 &lt;strong&gt;같은 앱 식별자(설치 단위)&lt;/strong&gt; 를 사용하고 있었습니다. (iOS는 &lt;strong&gt;Bundle Identifier&lt;/strong&gt;, Android는 &lt;strong&gt;applicationId&lt;/strong&gt;가 설치 단위를 구분합니다.)&lt;/p&gt;
&lt;p&gt;모바일 OS의 샌드박스 정책상 식별자가 같으면 동일한 앱으로 구분하기 때문에, 한 기기에 두 가지 버전을 동시에 설치하는 것은 불가능했습니다.
이로 인해 환경을 전환하려면 반드시 기존 앱을 삭제하거나 덮어씌워야 했습니다.
이 과정에서 로그인 세션, 로컬 DB, 캐시 데이터가 초기화되다 보니, QA나 개발 과정에서 연속적인 테스트를 수행하는 데 큰 걸림돌이 되었습니다.
또한, 시각적으로 두 앱이 구분되지 않아 비개발 직군의 동료들은 지금 보고 있는 앱이 어떤 환경인지 몰라 혼란을 겪는 일도 빈번했습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;-휴먼-에러를-방지하고-싶어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%9C%B4%EB%A8%BC-%EC%97%90%EB%9F%AC%EB%A5%BC-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 휴먼 에러를 방지하고 싶어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;② 휴먼 에러를 방지하고 싶어요&lt;/h2&gt;
&lt;p&gt;개발 앱과 운영 앱이 같은 앱 식별자를 공유하면, 실수로 개발 앱을 스토어에 배포하는 사고가 발생할 수 있습니다.
특히 QA·개발·운영이 동시에 진행되는 바쁜 시기에는 사람이 수동으로 설정을 바꾸는 과정에서 실수로 테스트용 푸시 알림이 실제 고객에게 발송되거나, 운영 DB에 테스트용 데이터가 쌓이는 등의 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 이유로 많은 테크 기업들은 배포 파이프라인에서 환경 변수가 맞지 않으면 빌드 단계에서 실패하도록 가드레일을 둡니다.
또한 실리콘밸리의 여러 기업들은 dogfooding 문화의 일환으로, 내부 테스트 전용 빌드나 사내 배포용 앱을 운영하며 운영용 앱과 명확히 구분된 환경에서 검증을 진행합니다.&lt;/p&gt;
&lt;p&gt;저희 역시 사람이 조심하는 것에는 한계가 있다고 판단했고, 이를 앱 레벨(xcconfig / Product Flavor)에서 강제함으로써 배포 담당자가 실수하고 싶어도 구조적으로 환경이 섞일 수 없는 상태를 목표로 환경 분리를 설계했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;환경-분리-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;환경 분리 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;환경 분리 전략&lt;/h1&gt;
&lt;p&gt;그래서 올리브영 앱을 두 가지로 분리했습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 15px; color: #111827;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; width: 16%; color: #111827;&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; width: 42%; color: #111827;&quot;&gt;운영 앱&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; width: 42%; color: #111827;&quot;&gt;개발 앱&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; color: #111827;&quot;&gt;대상&lt;/th&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;실제 고객&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;내부 구성원(QA·개발·기획·디자인·마케팅 등)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; color: #111827;&quot;&gt;목적&lt;/th&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;검증이 완료된 기능을 스토어를 통해 안정적으로 제공&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;QA 검증, 개발 및 테스트 수행&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; color: #111827;&quot;&gt;서버 환경&lt;/th&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;운영 서버 고정&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;운영 / STG / QA 등 서버 전환 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th style=&quot;border: 1px solid #e5e7eb; background: #f9fafb; padding: 10px; text-align: left; color: #111827;&quot;&gt;특징&lt;/th&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;운영 키·설정 고정, 스토어 배포 기준에 따른 품질·안정성 검증, 불필요한 로그 제거&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #e5e7eb; background: #ffffff; padding: 10px; vertical-align: top; color: #111827;&quot;&gt;디버깅에 도움이 되는 개발자 모드 제공&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;이러한 분리를 위해 iOS와 Android는 각각의 플랫폼 특성이 있지만, &lt;strong&gt;완전히 별도의 앱으로 구분&lt;/strong&gt;하는 것을 원칙으로 개발환경 분리 작업을 진행했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;android-프로젝트에서-환경-분리하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#android-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;android 프로젝트에서 환경 분리하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Android 프로젝트에서 환경 분리하기&lt;/h2&gt;
&lt;h3 id=&quot;1-build-variant로-환경-분리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-build-variant%EB%A1%9C-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC&quot; aria-label=&quot;1 build variant로 환경 분리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. Build Variant로 환경 분리&lt;/h3&gt;
&lt;p&gt;Android는 Gradle의 빌드 구성 시스템인 Build Variant를 통해 다른 버전의 앱을 만들어낼 수 있습니다.
하나의 코드베이스에서 BuildType과 ProductFlavor를 조합하여 다양한 앱 버전을 만들 수 있게 해줍니다.
올리브영 프로젝트에서의 BuildType은 debug/release로, ProductFlavor는 production/develop으로 구성되어 있습니다.
앱 버전은 아래처럼 총 4가지입니다.
이 중에서 협업 동료들에게 배포되는 버전은 developRelease(개발 앱)와 productionRelease(운영 앱)입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;developDebug - 개발 환경 디버그
&lt;ul&gt;
&lt;li&gt;Android 개발자들이 로컬 환경에서 개발할 때 사용하는 앱&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;developRelease&lt;/strong&gt; - 개발 환경 릴리즈
&lt;ul&gt;
&lt;li&gt;QA 기간 동안 협업 동료들이 설치하여 확인할 때 사용하는 앱&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;productionDebug - 운영 환경 디버그
&lt;ul&gt;
&lt;li&gt;운영 환경과 동일한 설정을 로컬에서 빠르게 확인할 때 사용하는 디버그용 앱(개발자용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;productionRelease&lt;/strong&gt; - 운영 환경 릴리즈
&lt;ul&gt;
&lt;li&gt;협업 동료들이 설치하여 스토어 배포 전 확인할 때 사용하는 앱&lt;/li&gt;
&lt;li&gt;스토어 배포용(운영) 빌드. 사내 배포 채널을 통해 검증 후 스토어에 게시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;개발 앱과 운영 앱이 별도로 설치되게 하기 위해서는 추가적으로 해줘야 하는 작업이 있습니다.
각 앱에게 별도의 패키지명을 지정하는 일입니다.
개발과 운영을 구분하기 위해 기존 운영 패키지명 뒤에 dev를 추가적으로 붙여주었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;운영(production)
&lt;ul&gt;
&lt;li&gt;applicationId : com.oliveyoung&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발(develop)
&lt;ul&gt;
&lt;li&gt;applicationId: com.oliveyoung.&lt;strong&gt;dev&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;패키지명이 달라지면 OS가 두 앱을 &lt;strong&gt;서로 다른 앱으로 인식&lt;/strong&gt;하기 때문에, 한 기기에 개발 앱과 운영 앱을 동시에 설치할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;// build.gradle
flavorDimensions += &quot;version&quot;

  productFlavors {
      create(&quot;develop&quot;) {
          dimension = &quot;version&quot;
          applicationIdSuffix = &quot;.dev&quot;
      }

      create(&quot;production&quot;) {
          dimension = &quot;version&quot;
      }
  }&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;2-운영과-개발-환경으로-소스셋-나누기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%9A%B4%EC%98%81%EA%B3%BC-%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EC%9C%BC%EB%A1%9C-%EC%86%8C%EC%8A%A4%EC%85%8B-%EB%82%98%EB%88%84%EA%B8%B0&quot; aria-label=&quot;2 운영과 개발 환경으로 소스셋 나누기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 운영과 개발 환경으로 소스셋 나누기&lt;/h3&gt;
&lt;p&gt;ProductFlavor를 통해 flavor를 나누어놓으면 지정한 이름 별로 소스셋을 관리할 수 있습니다. 아직 개발 중인 코드나 디버깅용으로 확인하기 위한 기능들은 개발(develop) 디렉토리에, 상용에 출시할 기능들은 운영(production) 디렉토리에 두어 환경별로 소스 분리를 했습니다.
예를 들어 앱 아이콘과 앱 이름을 각각 리소스에 분리하여 처리해주고 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;   app/src/
    ├── main/                # 공통 소스
    ├── develop/             # 개발 환경 전용
    │  ├── java/
    │  ├── res/
    │  │   ├── mipmap/      # 개발용 앱 아이콘
    │  │   │   └── ic_launcher.png
    │  │   └── values/
    │  │       └── strings.xml  # 앱 이름: &quot;올디브영&quot;
    │  └── AndroidManifest.xml
    └── production/           # 운영 환경 전용 (production)
       ├── java/
       ├── res/
       │   ├── mipmap/      # 운영용 앱 아이콘
       │   │   └── ic_launcher.xml
       │   └── values/
       │       └── strings.xml  # 앱 이름: &quot;올리브영&quot;
       └── AndroidManifest.xml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;각 모듈들을 운영/개발 flavor로 나눈 결과의 프로젝트 구조는 아래와 같습니다.
&lt;br&gt;&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/09d237a670559411f6750262f87fc1c3/db9fb/img-android-01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.541666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACcUlEQVR42k1TaW+rMBDk//+q9utr07ShaZWQA0g4bUwAcx/zdo1a1dLIo8W7O3tgVVWFtm3RNI1BkiQoimK1tc1CB1mWOUopG3TIPvM3skFKad79+A/DAKssS+R5DnJA13XmQ9/3KPIK0T1dxmGC1tqhJHY3tgjEfdaNRtd2v8GEEEYEJ7c4UBzHiKLIZPg5kZti82wvTdlC19opH6WdVDGev55mVavfdyzC932jeJ5nWHVdYxxH84ExTZMJzArGYVzY6Uch82Ea5q5rjQ+DFfL7vu9WhVxeEATg0jkDZ+O+rtwzPaSkDjnaTd0gjuKZA4tUmMr4pEmKTGaGmx66rmcazGpd14XKFKvC8XhY+GaFJSnMqxxe4JmhxCJCkASmqqN3xPl2WgMKKZCmqelBGIbIVIZUpMZGzV4ejwd0pR16Zx+SA17OL3OWZ3i7brB1t/ReYnN+xc7boaOyLZlm2L8eEXoJRCzxtSHux8hShX9P74sIFU/eoeB2XubwQ3/OHzmC+I6QFPJQg/BO5a9DtZTMcdxdkAYSmVA4fJxpXRKURYX7JVp00VArtCOVtLm3cRjND1qRJE5oZ1NwBbx2fJuAbdPyNNF3vcE4TnQPhlOvfqesdbkudtfMbd+gG2gretpD4nXLqNceJjeJqx3C/STsQxzfrrjYd8MvH/dF3gpUZeUkKrZP+Qm7YDd/Rp/Y+u+ELZjbhF1oox0aWIWskPklstuK8ETD8B7EK76XLp9Q69rJS2WLScArvfmmbzirswFzv/LZjgkjrNPpBM/zzLowPJ+4t/Lr9brwH7Tf753v72+b945ss3lHPn/9GDffx383iNF/F7neQQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/09d237a670559411f6750262f87fc1c3/263a4/img-android-01.webp 480w,
/static/09d237a670559411f6750262f87fc1c3/a6361/img-android-01.webp 960w,
/static/09d237a670559411f6750262f87fc1c3/0b34d/img-android-01.webp 1920w,
/static/09d237a670559411f6750262f87fc1c3/d21c8/img-android-01.webp 2772w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/09d237a670559411f6750262f87fc1c3/9aebd/img-android-01.png 480w,
/static/09d237a670559411f6750262f87fc1c3/a91f8/img-android-01.png 960w,
/static/09d237a670559411f6750262f87fc1c3/ac7a9/img-android-01.png 1920w,
/static/09d237a670559411f6750262f87fc1c3/db9fb/img-android-01.png 2772w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/09d237a670559411f6750262f87fc1c3/ac7a9/img-android-01.png&quot; alt=&quot;올리브영 Android 앱 구조에서의 환경별(PROD/DEV) 소스셋 분리 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/p&gt;&lt;figcaption&gt;올리브영 Android 앱 구조&lt;/figcaption&gt;
&lt;p&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;ios-프로젝트에서-환경-분리하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ios-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;ios 프로젝트에서 환경 분리하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;iOS 프로젝트에서 환경 분리하기&lt;/h2&gt;
&lt;p&gt;iOS에서는 빌드 설정과 설정 파일(xcconfig, Info.plist)을 중심으로 환경을 분리했습니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;용어를 간단히 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scheme: “어떤 앱을 어떻게 빌드/실행할지”를 고르는 레시피&lt;/li&gt;
&lt;li&gt;Build Configuration: DEV/PROD처럼 &lt;strong&gt;빌드 설정 묶음&lt;/strong&gt;(값의 집합)&lt;/li&gt;
&lt;li&gt;xcconfig: 위 설정값을 코드 밖 파일로 분리해 관리하는 설정 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-build-configuration과-scheme로-환경-정의하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-build-configuration%EA%B3%BC-scheme%EB%A1%9C-%ED%99%98%EA%B2%BD-%EC%A0%95%EC%9D%98%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1 build configuration과 scheme로 환경 정의하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. Build Configuration과 Scheme로 환경 정의하기&lt;/h3&gt;
&lt;p&gt;기존에는 Debug / Release 두 가지 설정만 두고, 코드 안에서 #if DEBUG 전처리로 개발/운영 동작을 나누는 방식이 섞여 있었습니다.
이 구조에서는 어느 빌드가 어떤 키와 엔드포인트를 쓰는지 한눈에 파악하기 어려웠습니다.
환경을 명확히 나누기 위해 아래와 같이 빌드 설정과 Scheme을 재구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build Configuration 예시&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Debug-DEV
Release-PROD&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;각 Configuration에 대응하는 Scheme 구성&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;OliveYoung-DEV
OliveYoung-PROD&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 해 두면 Xcode에서 Scheme만 바꿔도 &quot;지금 이 빌드는 어느 환경용인지&quot;를 바로 알 수 있고, CI에서도 동일한 Scheme 이름을 사용해 환경별 빌드를 자동화할 수 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-xcconfig로-환경별-설정-분리하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-xcconfig%EB%A1%9C-%ED%99%98%EA%B2%BD%EB%B3%84-%EC%84%A4%EC%A0%95-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;2 xcconfig로 환경별 설정 분리하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. xcconfig로 환경별 설정 분리하기&lt;/h3&gt;
&lt;p&gt;다음 단계는 환경별 설정을 코드에서 빼서 xcconfig로 옮기는 작업이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;환경별 설정&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dev.xcconfig, Prod.xcconfig를 분리&lt;/li&gt;
&lt;li&gt;각 파일에서만 달라지는 값들만 관리&lt;/li&gt;
&lt;li&gt;(예: 서버 베이스 URL, 서드파티 ‘앱 등록 ID’, API 키 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예를 들어 아래와 같은 값들을 xcconfig에서 관리하도록 변경했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 서버 주소: API_BASE_URL&lt;/li&gt;
&lt;li&gt;서드파티 API Key&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;3-bundle-identifier-app-group-딥링크-설정까지-분리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-bundle-identifier-app-group-%EB%94%A5%EB%A7%81%ED%81%AC-%EC%84%A4%EC%A0%95%EA%B9%8C%EC%A7%80-%EB%B6%84%EB%A6%AC&quot; aria-label=&quot;3 bundle identifier app group 딥링크 설정까지 분리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. Bundle Identifier, App Group, 딥링크 설정까지 분리&lt;/h3&gt;
&lt;p&gt;환경을 완전히 분리하려면 UI만 다르게 보는 것만으로는 부족하고, 시스템 레벨 식별자들도 분리해야 합니다. iOS에서는 다음 항목들을 환경별로 각각 갖도록 정리했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bundle Identifier
&lt;ul&gt;
&lt;li&gt;앱을 구분하기 위해 사용하는 고유 식별자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;App Group / Keychain Group
&lt;ul&gt;
&lt;li&gt;Extension과 데이터를 공유할 때 환경이 섞이지 않도록 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Associated Domains (Universal Links)
&lt;ul&gt;
&lt;li&gt;운영/개발용 도메인을 나누고, 각 환경 앱에서 바라볼 수 있도록 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이렇게 정리하여, 식별자들도 서로 다른 환경별로 겹치지 않도록 설정했습니다. 각 패키지별로 설정파일을 바라보게 되며 분리되는 프로젝트 구조는 다음과 같습니다.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2b0b0cf6cda0ab7e56c33df0ef220375/f69aa/img-ios-01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 50.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB+UlEQVR42j1T2W7bMBDU//9RX/rQAq1TF7ALtG4VX7GUyJYoUTx1U9NdAokAgkuROzs7QybGGAzDgGma0DQNsixDlr9CGY9hCivok1KmSqk9x8u8BGcd8izH7XbjPYzjGPOFEEistei6Dlpr2mzQyBZKvMG+/YG/H1bMlsBt6q3dD2HA+XEOx+KISlaQjaTzTczt+x51XSPhhfc+LozRmBYi1Vdwz5+hD59WdHc0yqRWm307SXz5+zV8P22gB4UwBzAh7oxJVVWFhANG5+GcA7UGoxXGzmDqDaGvnJRS0diyH3xwnYWm4nyWczh3mqfYfsL9z/McNeDvcrkgz/MYW+uihgSWUlIEHIcx8PxKOl8v13iOSS3LEltPuIIk3WTbxmotzWyUcxZlWa5cndYpsdz70aGoi+C8Q60EqpakofguClSqQt2QhpxcE1VF6KJuCFjF0UTB5drRDaCC0eVTfcLmuAkPecfuZYen41ME3Z632N12KEVJDK2GednS+AmnSujrD5hsB9s+II/f1rl+Jq3alJjv+7GDaEXg2yCkiE5zR1VTQmrJHTGgISMP8AUNXaMrfqN7/IPTAjb/ta4mi4DMkO4gtNJBtWyc4Zg6U7SvSSL/7jI7FDBObMwc55ESJzJqGKdoinXuw2XSNLCJ/BjeHwTP/I/Z/gcQW/XzH71vhAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2b0b0cf6cda0ab7e56c33df0ef220375/263a4/img-ios-01.webp 480w,
/static/2b0b0cf6cda0ab7e56c33df0ef220375/a6361/img-ios-01.webp 960w,
/static/2b0b0cf6cda0ab7e56c33df0ef220375/0b34d/img-ios-01.webp 1920w,
/static/2b0b0cf6cda0ab7e56c33df0ef220375/20394/img-ios-01.webp 2776w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2b0b0cf6cda0ab7e56c33df0ef220375/9aebd/img-ios-01.png 480w,
/static/2b0b0cf6cda0ab7e56c33df0ef220375/a91f8/img-ios-01.png 960w,
/static/2b0b0cf6cda0ab7e56c33df0ef220375/ac7a9/img-ios-01.png 1920w,
/static/2b0b0cf6cda0ab7e56c33df0ef220375/f69aa/img-ios-01.png 2776w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2b0b0cf6cda0ab7e56c33df0ef220375/ac7a9/img-ios-01.png&quot; alt=&quot;올리브영 iOS 앱 구조(환경별 xcconfig/식별자 분리)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/p&gt;&lt;figcaption&gt;iOS에서의 Build Configuration 및 식별자(Identifier) 격리 구조&lt;/figcaption&gt;
&lt;p&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;서드파티-sdk와-분석-도구-설정-통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%93%9C%ED%8C%8C%ED%8B%B0-sdk%EC%99%80-%EB%B6%84%EC%84%9D-%EB%8F%84%EA%B5%AC-%EC%84%A4%EC%A0%95-%ED%86%B5%ED%95%A9&quot; aria-label=&quot;서드파티 sdk와 분석 도구 설정 통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;서드파티 SDK와 분석 도구 설정 통합&lt;/h1&gt;
&lt;p&gt;AppsFlyer, Firebase, Braze 같은 서드파티 SDK는 환경별로 다른 앱 ID나 프로젝트를 바라봐야 합니다.
&lt;br&gt;
환경 분리 이후에는 아래의 작업을 했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 SDK에서 참조하는 &lt;strong&gt;앱 등록 ID / API Key&lt;/strong&gt;를 전부 환경 설정 파일(xcconfig 등)로 이관&lt;/li&gt;
&lt;li&gt;운영/개발용 앱 ID를 명확히 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;작업 후에는 특정 빌드가 어떤 환경의 데이터 소스로 연결되는지가 설정 파일만 봐도 명확해졌고, 마케팅/데이터 팀과의 커뮤니케이션도 수월해졌습니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;cicd-구축&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cicd-%EA%B5%AC%EC%B6%95&quot; aria-label=&quot;cicd 구축 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CI/CD 구축&lt;/h1&gt;
&lt;p&gt;마지막으로, GitHub Actions 기반 CI/CD에서 환경별로 다른 빌드를 생성하고 다른 채널로 배포하도록 워크플로우를 정리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DEV 빌드&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발용 Scheme을 사용해 빌드&lt;/li&gt;
&lt;li&gt;Firebase App Distribution 등 내부 테스트 채널로 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;PROD 빌드&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;운영 Scheme으로 빌드&lt;/li&gt;
&lt;li&gt;TestFlight 및 App Store용 아카이브 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;추가로, 환경 혼선을 줄이기 위해 CI에서 다음 가드레일을 적용했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DEV 워크플로우는 DEV Scheme만 허용&lt;/li&gt;
&lt;li&gt;PROD 워크플로우는 PROD Scheme만 허용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;워크플로우 분리를 통해 환경 설정이 잘못된 상태로 운영 빌드가 스토어에 배포되는 리스크를 줄였습니다.
&lt;br&gt;
하지만 자동화만으로는 충분하지 않았습니다.
정말로 개발 환경과 운영 환경이 섞이지 않는지를 &lt;strong&gt;반복적으로 확인할 수 있는 기준&lt;/strong&gt;이 필요했습니다.&lt;/p&gt;
&lt;h1 id=&quot;환경이-섞이지-않도록-확인하는-기준&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%99%98%EA%B2%BD%EC%9D%B4-%EC%84%9E%EC%9D%B4%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%ED%99%95%EC%9D%B8%ED%95%98%EB%8A%94-%EA%B8%B0%EC%A4%80&quot; aria-label=&quot;환경이 섞이지 않도록 확인하는 기준 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;환경이 섞이지 않도록 확인하는 기준&lt;/h1&gt;
&lt;p&gt;그래서 우리는 다음 기준으로 확인했습니다. 환경 분리는 “분리했다”에서 끝나지 않고, &lt;strong&gt;정말로 섞이지 않는지&lt;/strong&gt;를 반복 검증할 수 있어야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;설치 확인&lt;/strong&gt;: 한 기기에 DEV/PROD 앱이 동시에 설치되는지(아이콘/앱명으로 즉시 구분)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;엔드포인트 확인&lt;/strong&gt;: 앱 실행 후 네트워크 로그에서 DEV 빌드가 DEV 서버(API_BASE_URL)를 바라보는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서드파티 프로젝트 확인&lt;/strong&gt;: AppsFlyer/Firebase/Braze에서 DEV 이벤트가 DEV 프로젝트로만 수집되는지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;딥링크 확인&lt;/strong&gt;: DEV/PROD 도메인이 각 앱으로 올바르게 라우팅되는지(Universal Links/App Links)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위 항목 중 하나라도 어긋나면, 설정 파일(xcconfig)과 식별자(Bundle Identifier/App Group/Associated Domains)를 우선 점검하도록 팀 내의 컨벤션을 정리했습니다.&lt;/p&gt;
&lt;h1 id=&quot;마무리-개발환경분리를-통해-얻은-구조적-안정성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD%EB%B6%84%EB%A6%AC%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%96%BB%EC%9D%80-%EA%B5%AC%EC%A1%B0%EC%A0%81-%EC%95%88%EC%A0%95%EC%84%B1&quot; aria-label=&quot;마무리 개발환경분리를 통해 얻은 구조적 안정성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리: 개발환경분리를 통해 얻은 구조적 안정성&lt;/h1&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 676px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/37f7c831fd85a23d3937edf5f17f529e/ff08e/img-appicon.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.45833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC60lEQVR42iXOW0yTdxjH8ffCLHrB4i52sWQ3LluxQMXCmGAFo85swxNuWcR18bCDejGjbEvM1GUyCWxMBYkHRtywAS0EQY7vkUNf2gHNVDIUI5jMRS0tRfhXaCVmmd+9cxef/J7nufjlke7Xa/xPZfqKSaJjiNnWAIn2AR639DNxSSF0SSZ0WSbsUZgzRvn77hTz1+8xZ95m2hikaJ/CyrwG1q6uQ3ro1Znwajy4rHCocBd5dgcF2bmsczj57L0CxmqbiTSpRBu6GVJP4hlzUz+2G88tN95RN0G9ns0b2slZdZFc1wWkUINKrM3EPFWDJEls3u7mq++K2VT40fO9+uAhErKfcKNKxR8uTj5ci2/mLF1TP1AedbC/pQCXq5nslTXkrDiPNNGkEZcDtJZUkmVbwvivJdR+ks/Aia/5MH8d3+/cyz/6EKFOhZrxrdyJ99IW+QYt8hPDTy+yp3ELmW95WJF9lqw3q5AiLRpPtABXSytZk76UgcNuPlggYRQVsn+3m2M790BvkOjgb3ju7WBkTubo+BLO/fk+fz3rYW/DRpZn1JCZVUmG8xTS5FWdhFXYW1HNCwsXsu3jHeS+vZ6Kc9UsSkrizBdFPBu8wezwCKfvrOaXiJu68Of0z1ygaX4bn3rXsCztPM6McpanlyM9ajcQnT2EWmQ25azi5aQXcTkzeGXxS2S+nsyox8uT368zHxzFvPYjZSNOqm9voOrmeg6YTt7d9yUp9gqWpZfhSCtDmunqQVimO3Qm1T7u633cbe16nhHVulviwWFmA91EtSYe9A4SNg38jTr5W+tIST1BqqOU1JQSq/g4Ulz3kTB8xGSDCUu4z09I6ybcYxJSdKatfBq0PgxoTBm1hP1DRH0mBw9cIe+dn0lLL8Wechy7vRh7cjFSTDHEY9UQMUUXU52KiLTLYrKjS0TaOkRUVkXM7Bdx0xRzPr94pFtztyo6q5rFq/YS8VryMWFb+q2w2Y4I2xv/OSz+BU4UJRsR14zQAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/37f7c831fd85a23d3937edf5f17f529e/263a4/img-appicon.webp 480w,
/static/37f7c831fd85a23d3937edf5f17f529e/c7990/img-appicon.webp 676w&quot; sizes=&quot;(max-width: 676px) 100vw, 676px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/37f7c831fd85a23d3937edf5f17f529e/9aebd/img-appicon.png 480w,
/static/37f7c831fd85a23d3937edf5f17f529e/ff08e/img-appicon.png 676w&quot; sizes=&quot;(max-width: 676px) 100vw, 676px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/37f7c831fd85a23d3937edf5f17f529e/ff08e/img-appicon.png&quot; alt=&quot;개발용 앱(올디브영)과 운영용 앱(올리브영) 아이콘 비교&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/p&gt;&lt;figcaption&gt;시각적 인지 편의성을 극대화한 개발용(올디브영) 및 운영용(올리브영) 앱 아이콘/명칭 분리&lt;/figcaption&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;개발 환경과 운영 환경을 완전히 분리한 후, 다음과 같은 긍정적인 변화를 경험했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;협업 부서 간 커뮤니케이션 비용 감소&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발 앱과 운영 앱이 동시에 설치 가능해지면서, &quot;지금 보고 있는 앱이 어떤 환경인가요?&quot;라는 질문이 사라졌습니다.&lt;/li&gt;
&lt;li&gt;앱 아이콘과 이름만으로도 환경을 구분할 수 있게 되어, 비개발부서와의 소통이 훨씬 명확해졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;배포 과정에서의 안정성 향상&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CI/CD 파이프라인에서도 환경별로 명확히 분리된 워크플로우가 각각의 빌드를 처리하기 때문에, 배포 과정에서의 안정성이 크게 향상되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;개발 생산성 향상&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발자들은 더 이상 환경 전환을 위해 설정을 바꾸거나 앱을 재설치할 필요가 없어졌습니다.&lt;/li&gt;
&lt;li&gt;두 앱을 동시에 띄워놓고 비교 테스트를 진행할 수 있게 되어, 디버깅과 검증 작업이 훨씬 수월해졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;안정적인 앱 운영&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서드파티 SDK와 분석 도구들이 환경별로 명확히 분리되어, 개발 중인 기능이 운영 데이터에 영향을 주는 일이 없어졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;동료 A 👱‍♀️&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 12:25&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;올디브영 있어서 너무 좋아요🥰 스토어버전 앱과 개발중인 앱 따로 볼 수 있어서 너무 편합니다!!&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;동료 B 👨‍🍳&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 2:28&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;올디브영 없을때 어떻게 개발 했나 싶네요. 상용앱 깔았다가 개발 앱 깔았다가… 이제 개발 앱을 따로 깔 수 있어서 헷갈리는 일이 없어요! 너무 좋습니다.&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;동료 C 👩‍🦱&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 3:35&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;예전처럼 서버 설정 바꿀 필요 없이 앱만 전환하면 되니까 확인이 너무 수월합니다. 가이드도 꼼꼼해서 좋네요. 잘 쓰겠습니다!&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f8f9fa; padding: 16px; border-radius: 8px; margin: 16px 0; border-left: 4px solid #611f69;&quot;&gt;
  &lt;div style=&quot;margin-bottom: 12px;&quot;&gt;
    &lt;strong style=&quot;color: #1d1c1d;&quot;&gt;동료 D 🧑‍🎨&lt;/strong&gt;
    &lt;span style=&quot;color: #616061; font-size: 13px; margin-left: 8px;&quot;&gt;오후 5:30&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;color: #1d1c1d;&quot;&gt;올디브영이 생겨서 개발, 상용 돌아가면서 설치하지 않아도 되서 너무 좋아요&lt;/div&gt;
&lt;/div&gt;
&lt;figcaption style=&quot;text-align: center;&quot;&gt;올디브영(개발앱)에 대한 동료들의 긍정적인 피드백&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;환경 분리는 단순히 기술적인 개선이 아니라, 개발조직 전체의 협업 방식과 개발 문화를 개선하는 계기가 되었습니다.&lt;/p&gt;
&lt;p&gt;기술적인 개선을 통해, 전체적인 커뮤니케이션 비용을 줄이고, 배포 안정성을 높일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;올리브영의 개발자들은 앞으로도 더 나은 개발 경험과 안정적인 서비스 제공을 위해 지속적으로 개선해 나갈 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;올리브영-네이티브-앱-개발-더-알아보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EB%8D%94-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;올리브영 네이티브 앱 개발 더 알아보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영 네이티브 앱 개발 더 알아보기&lt;/h1&gt;
&lt;p&gt;DEV/PROD 환경 분리부터 앱 구조 개선, 생산성 도구까지—
아래 글들을 함께 보시면 &lt;strong&gt;올리브영 앱 개발자들이 실제로 어떤 문제를 풀고 있는지&lt;/strong&gt;를 더 입체적으로 살펴볼 수 있습니다. ☺️&lt;/p&gt;
&lt;h2 id=&quot;앱-구조운영-안정성성능-개선기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%B1-%EA%B5%AC%EC%A1%B0%EC%9A%B4%EC%98%81-%EC%95%88%EC%A0%95%EC%84%B1%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0&quot; aria-label=&quot;앱 구조운영 안정성성능 개선기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;앱 구조·운영 안정성·성능 개선기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-11-06/swiftui-developer-mode&quot;&gt;하이브리드 앱에 구축하는 iOS 개발자 모드: 운영 환경에서 안전하게 디버깅하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-11-04/server-driven-technical-exploration&quot;&gt;SDUI로 네이티브 앱 운영 민첩성 높이기: 서버 주도 UI 도입 사례&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-04-02/app-improve-camera-scan&quot;&gt;올리브영 앱 스마트 스캐너 개선기: 사용자 경험과 성능을 함께 개선한 사례&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;개발-생산성문서화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C-%EC%83%9D%EC%82%B0%EC%84%B1%EB%AC%B8%EC%84%9C%ED%99%94&quot; aria-label=&quot;개발 생산성문서화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발 생산성·문서화&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-03-28/swift-docc&quot;&gt;iOS 개발자를 위한 DocC 실무 튜토리얼: 문서로 협업 품질 높이기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-11-15/swift-macro-part1&quot;&gt;Swift 매크로 1탄: Swift 매크로란 무엇이고 왜 필요한가&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-01-15/swift-macro-part2&quot;&gt;Swift 매크로 2탄: 실무에서 Swift 매크로 활용하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;개발자-행사커뮤니티&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C%EC%9E%90-%ED%96%89%EC%82%AC%EC%BB%A4%EB%AE%A4%EB%8B%88%ED%8B%B0&quot; aria-label=&quot;개발자 행사커뮤니티 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발자 행사·커뮤니티&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-02-26/letswift2024-review&quot;&gt;Let’s Swift 2024 × 올리브영: 기술과 경험을 나눈 개발자 행사 후기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[운영 비용을 95% 절감한 서버리스 온콜 시스템 구축기]]></title><description><![CDATA[안녕하세요. 고기를 좋아하는 올리브영 SRE 태극기입니다! 올리브영은 기존에 사용하던 외부 솔루션 기반 온콜 시스템의 한계를 해결하기 위해, 2025년 7월 Amazon SES + Amazon Connect…]]></description><link>https://oliveyoung.tech/2025-12-24/amazon-connect/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-24/amazon-connect/</guid><pubDate>Wed, 24 Dec 2025 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 고기를 좋아하는 올리브영 SRE 태극기입니다!&lt;br&gt;
올리브영은 기존에 사용하던 &lt;strong&gt;외부 솔루션 기반 온콜 시스템&lt;/strong&gt;의 한계를 해결하기 위해, 2025년 7월 Amazon SES + Amazon Connect 기반의 서버리스 온콜 시스템으로 전환했습니다.&lt;br&gt;
이 글에서는 그 과정에서의 설계 의도, 시행착오, 그리고 최종적으로 얻어진 안정성과 결과를 공유합니다.&lt;/p&gt;
&lt;h2 id=&quot;1-온콜on-call은-무엇이며-왜-중요한가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%98%A8%EC%BD%9Con-call%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EB%A9%B0-%EC%99%9C-%EC%A4%91%EC%9A%94%ED%95%9C%EA%B0%80&quot; aria-label=&quot;1 온콜on call은 무엇이며 왜 중요한가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;1. 온콜(On-call)은 무엇이며 왜 중요한가&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;온콜은 장애가 발생했을 때 담당자에게 즉시 연락해 문제가 더 커지기 전에 빠르게 조치할 수 있도록 하는 &lt;strong&gt;비상 호출 체계&lt;/strong&gt;입니다.&lt;br&gt;
특히 대규모 트래픽을 처리하는 플랫폼이나 이벤트 발생 시에는 1분의 지연이 수만 명의 고객 경험에 영향을 주기 때문에, 이 비상 호출의 무게감이 남다릅니다. 이에 장애 발생 시 온콜이 정상적으로 작동해야만 비로소 다음과 같은 가치들을 지켜낼 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장애 조기 감지: 시스템 모니터링이 포착한 신호를 엔지니어의 인지로 즉각 연결합니다.&lt;/li&gt;
&lt;li&gt;SLA 준수 및 고객 영향 최소화: 대응 골든타임을 확보하여 서비스 가용성을 유지합니다.&lt;/li&gt;
&lt;li&gt;장애 확산 억제: 초기 대응을 통해 국지적 결함이 전체 시스템 마비로 번지는 것을 막습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;온콜이 울리지 않는 순간, SRE의 어떤 대응 프로세스도 시작할 수 없습니다. 따라서, &lt;strong&gt;온콜의 신뢰성 자체가 서비스 안정성의 가장 중요한 요소&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h2 id=&quot;2-기존-온콜-솔루션의-구조적-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EA%B8%B0%EC%A1%B4-%EC%98%A8%EC%BD%9C-%EC%86%94%EB%A3%A8%EC%85%98%EC%9D%98-%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;2 기존 온콜 솔루션의 구조적 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;2. 기존 온콜 솔루션의 구조적 한계&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;올리브영은 그동안 &lt;strong&gt;외부 온콜 솔루션&lt;/strong&gt;을 이용해 장애 알림을 운영했습니다. 하지만 다음과 같은 한계가 존재했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;해외 번호 발신&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;온콜 발신 번호가 해외번호로 표시되면서, 국내 통신사 스팸정책에 의해 전화가 차단되는 사례가 반복되었습니다.&lt;br&gt;
개발자가 직접 통신사 고객센터에 연락해 국제전화 수신을 요청해야 하는 불편함도 있었습니다.&lt;/p&gt;
&lt;center&gt;
 &lt;div style=&quot;width: 80%; display:block;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 958px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b5b2621a2d96ff520eaa9b3f38b73f78/abb65/overseas_call.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.791666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABp0lEQVR42oVSy04CQRDkQ/ViCERPGgxcOIDGEH+BXzB68uofIG+BEFwQWPYJu8uyENhHOd0KChrtpLazMz01XTUdg4goiqDrOkajETRNg6qqmEwmsG0b8/kcQRDgt6Bz3zNFjD5hGGI8Gu+ILMsWaxF8398jo4NbHP5v15hws1njbThEq9VCvVZDo17Ha68HRVHQ7/e5a4IsyxiKOrpYFXuKuJxqBoOBaML6IqSotnvI3xRwXbhFNneFu/sHJpAkCaZhsPTlcrnrJtxCqCMVlJmwqct4eqng8vwCJ/E4EskkTs/OkEgkcHR8jGKxCGc6hWmaWK1WWK/X2PgBAt9DFHg/fI09vTbx2HqGOpZZcrPR4K7qQna73WZPDdEhPRph0JegGRascQkrvfxpbvhFWKuWUGmUkc/lkUqlkE6nkclkOGezWXS7XeHxZifpv4hJmgJJlaEJo93FAp7nYTab8ciQ0eQdYSH2GK4Lx3HEvgXXnbMNe4Tbsel0OvxispBuGiaTWTOLycm/DxKbMRWeknyygi7ZIwzCAOLdeDyomIr+GuLDtUO8A3Zw5svsdBqEAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b5b2621a2d96ff520eaa9b3f38b73f78/263a4/overseas_call.webp 480w,
/static/b5b2621a2d96ff520eaa9b3f38b73f78/8e3cb/overseas_call.webp 958w&quot; sizes=&quot;(max-width: 958px) 100vw, 958px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b5b2621a2d96ff520eaa9b3f38b73f78/9aebd/overseas_call.png 480w,
/static/b5b2621a2d96ff520eaa9b3f38b73f78/abb65/overseas_call.png 958w&quot; sizes=&quot;(max-width: 958px) 100vw, 958px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b5b2621a2d96ff520eaa9b3f38b73f78/abb65/overseas_call.png&quot; alt=&quot;국제전화 수신거부 서비스로 온콜 수신 실패 사례&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;국제전화 수신거부 서비스로 온콜 수신 실패 사례&lt;/figcaption&gt;
 &lt;/div&gt;
&lt;/center&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;발신 실패 발생&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;외부 솔루션 이슈로 간헐적으로 발신 실패하는 사례가 발생했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;높은 고정 비용&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;발신 실패 여부와 관계없이 월 약 200만 원 이상의 솔루션 사용료가 고정적으로 청구되었습니다.
사용량 대비 비용 효율이 떨어지는 구조였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;기존 외부 온콜 솔루션이 가진 제약을 해결하기 위해,
&lt;b&gt;&apos;어떻게 하면 더 단순하면서도 확실하게 전화가 울리게 만들 수 있을까?&apos;&lt;/b&gt;라는 관점에서 전체 구조를 다시 검토했습니다.&lt;br&gt;
Amazon Connect는 주로 AICC(AI Contact Center, 인공지능 컨택센터) 구축에 활용되는 서비스입니다.
&lt;a href=&quot;https://youtu.be/kZj08CmmvcA&quot; target=&quot;_blank&quot;&gt;아메리칸 항공에서 고객센터를 Amazon Connect로 전환한 사례&lt;/a&gt;처럼, API 기반의 발신 제어와 안정적인 통화 품질을 강점으로 하는 서비스이기도 합니다.
올리브영에서는 이 서비스를 ‘장애 대응 온콜 시스템’이라는 특수한 목적에 맞게 재해석해 적용했습니다.
콜센터 서비스 특유의 안정적인 발신 품질, 고가용성, TTS 연동 능력이 온콜 발신 시스템으로 활용하기에 매우 적합했기 때문입니다.&lt;br&gt;
이제 필요한 것은 &apos;이 발신 엔진을 어떤 방식으로 트리거할 것인가&apos;였습니다.
다양한 장애 알림을 하나의 입력으로 통합하려면 표준화된 단일 채널이 필요했습니다.
이 기준에서 검토한 결과, 가장 신뢰성이 높고 구현이 단순한 방식이 바로 &lt;b&gt;&apos;이메일을 온콜 트리거로 사용하는 구조&apos;&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;h2 id=&quot;3-온콜-트리거를-이메일로-통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%98%A8%EC%BD%9C-%ED%8A%B8%EB%A6%AC%EA%B1%B0%EB%A5%BC-%EC%9D%B4%EB%A9%94%EC%9D%BC%EB%A1%9C-%ED%86%B5%ED%95%A9&quot; aria-label=&quot;3 온콜 트리거를 이메일로 통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;3. 온콜 트리거를 이메일로 통합&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;새로운 온콜 시스템 설계의 핵심 원칙은 &lt;strong&gt;가능한 빠르고 확실하게 전화를 울리게 하는 것&lt;/strong&gt;이었습니다.&lt;br&gt;
이를 위해 이메일(Amazon SES)을 온콜 트리거로 선택했습니다.&lt;/p&gt;
&lt;p&gt;Slack, Datadog 등 올리브영이 사용하는 대부분의 모니터링 도구는 &apos;이메일 발신&apos;을 기본 인터페이스로 지원합니다.
따라서 별도의 복잡한 API 연동 없이도 이메일이라는 표준화된 통로를 통해 모든 장애 신호를 하나로 통합할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;실제 운영을 통해 확인한 이메일 기반 구조의 강점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;단일 파이프라인: 모든 장애 트리거가 이메일이라는 하나의 채널로 수렴되어 관리가 용이함&lt;/li&gt;
&lt;li&gt;구조적 단순함: 아키텍처가 직관적이라 유지보수가 쉽고 장애 포인트가 적음&lt;/li&gt;
&lt;li&gt;검증된 신뢰성: 운영 이후 트리거 누락 없이 안정적으로 동작 중&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;4-이메일-기반-아키텍처-설계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EA%B8%B0%EB%B0%98-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%84%A4%EA%B3%84&quot; aria-label=&quot;4 이메일 기반 아키텍처 설계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;4. 이메일 기반 아키텍처 설계&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이메일을 트리거로 삼기로 결정한 후, 이 신호를 단 한 번의 유실도 없이 Amazon Connect까지 전달할 파이프라인을 설계했습니다.
이 설계의 핵심은 &apos;유연성&apos;과 &apos;안정성&apos;입니다.&lt;/p&gt;
&lt;h3 id=&quot;핵심-설계-포인트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B5%EC%8B%AC-%EC%84%A4%EA%B3%84-%ED%8F%AC%EC%9D%B8%ED%8A%B8&quot; aria-label=&quot;핵심 설계 포인트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;핵심 설계 포인트&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;표준화된 통합: Slack 인시던트나 Datadog Alert 등 파편화된 장애 신호를 이메일이라는 공통 표준 인터페이스로 수렴하여 연동 복잡도를 제거&lt;/li&gt;
&lt;li&gt;관리의 유연성: Google Groups를 브릿지로 활용해, 인프라 코드(IaC) 수정 없이도 팀·스쿼드 단위의 온콜 그룹을 유연하게 운영&lt;/li&gt;
&lt;li&gt;안정적인 메시지 릴레이: SES(수신) → SNS(팬아웃) → SQS(버퍼링) 구조를 채택하여, 대량 메일이 쏟아지는 상황에서도 메시지 유실을 방지하고 발신 속도를 제어할 수 있는 기반을 마련&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;이메일에서-온콜-발신까지의-데이터-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EB%A9%94%EC%9D%BC%EC%97%90%EC%84%9C-%EC%98%A8%EC%BD%9C-%EB%B0%9C%EC%8B%A0%EA%B9%8C%EC%A7%80%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;이메일에서 온콜 발신까지의 데이터 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;이메일에서 온콜 발신까지의 데이터 여정&lt;/strong&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 트리거 발송: 모니터링 도구가 미리 정의된 온콜 전용 이메일 주소로 장애 알림을 발송  
2. 신호 수집(SES): Amazon SES가 이메일을 수신하면, Receipt Rule을 통해 메일 원문을 SNS를 거쳐 SQS에 적재  
3. 데이터 파싱(Lambda): 온콜 발신 Lambda가 SQS에서 메시지를 꺼내 수신 대상자(전화번호)와 안내 음성(TTS) 내용을 정교하게 파싱  
4. 최종 발신(Amazon Connect): 파싱된 정보를 바탕으로 Amazon Connect API를 호출. 이때 Lambda는 SQS 메시지를 초당 4건의 속도로 소비하여 발신량(Rate Limit)을 제어  &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;즉, 온콜 프로세스는 이메일(Amazon SES) 수신을 출발점으로 삼아, 이후 모든 단계가 서버리스 기반으로 자동 처리됩니다.&lt;/p&gt;
&lt;p&gt;이렇게 아키텍처가 정리되면 다음 과제는 &apos;누구에게 전화를 발신해야 하는가&apos;를 정확하게 식별하는 일입니다.&lt;br&gt;
온콜 수신 대상자는 조직 개편, 담당자 변경 등 다양한 이유로 변경이 잦을 수 있기 때문에, 구조적으로 단순하면서도 변경이 쉬운 방식이 필요했습니다.
이에 전화번호를 DB에 저장하고 관리하는 일반적인 방식 대신, &apos;기존 조직 관리 체계(Google Groups)를 그대로 활용하면서도, 별도 DB 없이 동적으로 발신 대상을 식별할 수 없을까?&apos;를 고민했습니다.&lt;br&gt;
그 해답은 Google Groups와 RFC 5233 표준인 &apos;플러스 어드레싱&apos;의 조합에 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;5-google-groups-기반-온콜-대상자-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-google-groups-%EA%B8%B0%EB%B0%98-%EC%98%A8%EC%BD%9C-%EB%8C%80%EC%83%81%EC%9E%90-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;5 google groups 기반 온콜 대상자 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;5. Google Groups 기반 온콜 대상자 관리&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Google Groups를 활용하면 온콜 그룹을 이메일 주소 하나로 관리할 수 있고, 담당자 변경 시 온콜 시스템을 수정하지 않아도 됩니다.&lt;/p&gt;
&lt;p&gt;예시는 다음과 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;sre-oncall@oliveyoung.co.kr
security-oncall@oliveyoung.co.kr
payment-oncall@oliveyoung.co.kr&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;시스템의 작동 원리는 간단합니다.
장애 트리거 메일이 Google Groups로 발송되면, 해당 메일은 등록된 멤버들의 온콜 전용 주소로 즉시 전달됩니다.&lt;/p&gt;
&lt;p&gt;이때 Amazon SES는 유입되는 메일을 Receipt Rule에 의해 SNS를 거쳐 SQS로 전달합니다.
이후 파이프라인의 핵심인 온콜 발신 Lambda가 동작하며, SQS 메시지에서 다음 두 가지 핵심 데이터를 추출합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수신 주소: 누가 전화를 받을 것인가? (플러스 어드레싱 기반 식별)&lt;/li&gt;
&lt;li&gt;이메일 제목: 어떤 내용으로 전화를 걸 것인가? (TTS 음성 변환)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, 이메일에 &apos;누구에게, 무엇을&apos; 전달할지에 대한 정보가 모두 담겨 있어, 별도의 DB 조회 없이도 즉각적인 온콜 발신이 가능해집니다.&lt;/p&gt;
&lt;h3 id=&quot;플러스-어드레싱-기반-대상자-식별--rfc-5233-subaddressing-활용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%8C%EB%9F%AC%EC%8A%A4-%EC%96%B4%EB%93%9C%EB%A0%88%EC%8B%B1-%EA%B8%B0%EB%B0%98-%EB%8C%80%EC%83%81%EC%9E%90-%EC%8B%9D%EB%B3%84--rfc-5233-subaddressing-%ED%99%9C%EC%9A%A9&quot; aria-label=&quot;플러스 어드레싱 기반 대상자 식별  rfc 5233 subaddressing 활용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;플러스 어드레싱 기반 대상자 식별 — RFC 5233 Subaddressing 활용&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;온콜 대상자 식별을 위해 이메일 주소에 플러스 어드레싱(+ Addressing) 방식을 적용했습니다.&lt;br&gt;
이는 &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5233.html&quot; target=&quot;_blank&quot;&gt;RFC 5233에서 정의된 공식 표준(Subaddressing)&lt;/a&gt;으로,
이메일 로컬 파트(local-part)에 + 식별자를 추가하는 방식입니다.&lt;/p&gt;
&lt;p&gt;실제로 Gmail 등 대부분의 메일 시스템은 &apos;ID+식별자@domain.com&apos; 형태에서 &apos;+&apos; 뒤의 문자열을 무시하고 원래의 &apos;ID 주소&apos;로 메일을 전달합니다.
하지만 이메일 원문에는 이 플러스 어드레스가 그대로 남아있다는 점에 주목했습니다.
Amazon SES가 수신한 메일 원문을 SQS로 넘겨주면, Lambda는 복잡한 DB 조회 대신 아주 단순한 정규식만으로 &apos;누구에게 전화를 걸지&apos; 판단합니다.&lt;/p&gt;
&lt;p&gt;[데이터 파싱 예시]&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;- 수신 주소: call+821012345678@oliveyoung.co.kr → 추출 번호: 821012345678
- 메일 제목: [장애] ALB 응답시간 지연 발생 → TTS 내용: ALB 응답시간 지연 발생&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 방식의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;무상태성(Stateless) 유지: 시스템이 전화번호 데이터를 들고 있을 필요가 없어 DB 관리 공수가 0에 수렴&lt;/li&gt;
&lt;li&gt;즉각적인 확장: 새로운 담당자가 추가되어도 Google Groups에 플러스 어드레싱이 적용된 메일 주소만 등록하면 즉시 온콜 대상에 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;6-실제-운영-중-겪은-문제--야간-스팸메일로-인한-오발신&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EC%8B%A4%EC%A0%9C-%EC%9A%B4%EC%98%81-%EC%A4%91-%EA%B2%AA%EC%9D%80-%EB%AC%B8%EC%A0%9C--%EC%95%BC%EA%B0%84-%EC%8A%A4%ED%8C%B8%EB%A9%94%EC%9D%BC%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%98%A4%EB%B0%9C%EC%8B%A0&quot; aria-label=&quot;6 실제 운영 중 겪은 문제  야간 스팸메일로 인한 오발신 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;6. 실제 운영 중 겪은 문제 — 야간 스팸메일로 인한 오발신&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;완벽하다고 믿었던 시스템도 실제 프로덕션의 변수 앞에서는 예외가 발생했습니다.&lt;br&gt;
내부 테스트 기간 중, 모두가 깊이 잠든 자정 즈음에 느닷없이 온콜 전화가 울렸습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 656px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/521a14cf2faebe01b334859bc523b100/5ad6b/night_call.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 15.833333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAArUlEQVR42pWMuwqCYACFfTTBRcTnaVWwscnFxXcoaOtCoNDFli5DUA1FSwRp+ftrGPJlDu0d+OCcw+EoSXLjOAvo93pomoZpmj90XW8wDANVVfF9n+FgxDwa83xcyfMXaZqSZRlxHNc5R5lOuvidFstogeO0axwsy8K2bVzXxfO8hq8Pw5DNesv5dECIO1LmyPpMSllnQVEUKJf9jlUw4qt3WTalSEUzqKqKf/UBEv3HHPrBia0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/521a14cf2faebe01b334859bc523b100/263a4/night_call.webp 480w,
/static/521a14cf2faebe01b334859bc523b100/0ccf2/night_call.webp 656w&quot; sizes=&quot;(max-width: 656px) 100vw, 656px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/521a14cf2faebe01b334859bc523b100/9aebd/night_call.png 480w,
/static/521a14cf2faebe01b334859bc523b100/5ad6b/night_call.png 656w&quot; sizes=&quot;(max-width: 656px) 100vw, 656px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/521a14cf2faebe01b334859bc523b100/5ad6b/night_call.png&quot; alt=&quot;스팸 온콜로 잠에서 깬 직후 슬랙 대화&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;스팸 온콜로 잠에서 깬 직후 슬랙 대화&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;원인은 황당하게도 &apos;광고성 스팸 메일&apos;이었습니다.
수신된 이메일을 모두 온콜로 처리하는 트리거의 치명적인 허점이었습니다.
온콜 전용 주소가 외부로 노출되지 않았더라도, 무작위로 발송되는 스팸 메일은 온콜 트리거를 작동시켰고, Lambda는 스팸메일 제목을 정성스럽게 TTS로 읽어주면서 담당자를 깨웠던 것입니다.&lt;/p&gt;
&lt;p&gt;이 사건 이후, 지정한 도메인에서 발신한 이메일만 온콜 트리거로 허용하는 필터링 규칙을 Lambda에 적용하여 문제를 해결했습니다.&lt;/p&gt;
&lt;p&gt;최종 해결책: 이메일 화이트리스트(Allow-List) 도입&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;보완 전: 수신된 모든 메일을 온콜 처리&lt;/li&gt;
&lt;li&gt;보완 후: 이메일의 발신도메인을 체크하여, 허가된 시스템(Datadog, Slack, 올리브영 사내메일)에서 보낸 메일만 온콜 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;전사 적용 후에 이런 일이 발생했다면, 새벽에 모든 개발자를 온콜로 깨우는 상황이 될 뻔 했습니다.
전사 확산 전, 내부 테스트 기간에 이렇게 매운맛 경험을 한 것이 오히려 천운이었습니다. 덕분에 현재는 단 한 건의 오발신 없는 철벽 방어 시스템을 유지하고 있습니다.
&lt;del&gt;(어? 사내메일로 예약메일을 보내면 모닝콜 대신 쓸 수 있는 건 비밀입니다.)&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;이메일 기반 온콜 흐름에서 발생할 수 있는 문제들을 해결한 이후, 실제 대규모 발신 테스트를 통해 Amazon Connect의 제약 사항도 확인할 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;7-amazon-connect-도입-직후-발견된-rate-limit-제약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#7-amazon-connect-%EB%8F%84%EC%9E%85-%EC%A7%81%ED%9B%84-%EB%B0%9C%EA%B2%AC%EB%90%9C-rate-limit-%EC%A0%9C%EC%95%BD&quot; aria-label=&quot;7 amazon connect 도입 직후 발견된 rate limit 제약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;7. Amazon Connect 도입 직후 발견된 Rate Limit 제약&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;숨겨진-제약-amazon-connect의-동시-발신-제한&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%A8%EA%B2%A8%EC%A7%84-%EC%A0%9C%EC%95%BD-amazon-connect%EC%9D%98-%EB%8F%99%EC%8B%9C-%EB%B0%9C%EC%8B%A0-%EC%A0%9C%ED%95%9C&quot; aria-label=&quot;숨겨진 제약 amazon connect의 동시 발신 제한 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;숨겨진 제약: Amazon Connect의 동시 발신 제한&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;위와 같이 이메일 기반 온콜 흐름에서 발생할 수 있는 문제들을 해결한 이후, 실제 대규모 발신 테스트를 통해 Amazon Connect의 제약 사항도 확인할 수 있었습니다.
온콜 테스트를 위해 약 50여 명에게 동시에 발신을 시도하자, 예상치 못한 상황이 발생한 것인데요, 일부 인원에게 전화가 가지 않는 &apos;발신 누락&apos;이 확인되었습니다.
원인은 Amazon Connect의 &lt;b&gt;초당 호출 제한(Rate Limit)&lt;/b&gt;이었습니다. 확인해보니 &lt;b&gt;StartOutboundVoiceContact&lt;/b&gt; API를 짧은 시간에 집중적으로 호출할 경우, Quota 초과로 호출이 거절된 것이었습니다.
수십 명에게 즉시 전화를 걸어야 하는 온콜 시스템에는 턱없이 부족한 수치였습니다.&lt;/p&gt;
&lt;h3 id=&quot;해결-과정-quota-증설과-안정성-확보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95-quota-%EC%A6%9D%EC%84%A4%EA%B3%BC-%EC%95%88%EC%A0%95%EC%84%B1-%ED%99%95%EB%B3%B4&quot; aria-label=&quot;해결 과정 quota 증설과 안정성 확보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;해결 과정: Quota 증설과 안정성 확보&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;올리브영 담당 AWS TAM(Technical Account Manager)에 서비스 할당량 증설을 요청했고, 약 2주간의 검토를 거쳐 초당 5회까지 발신 가능하도록 증설을 마쳤습니다.
하지만 여기서 그치지 않았습니다. 인프라의 한계치까지 사용하는 것은 위험하다고 판단하여, 실제 운영 시에는 안전 마진을 둔 초당 4회로 발신 속도를 제어하기로 결정했습니다.&lt;/p&gt;
&lt;h2 id=&quot;8-sqs-기반-발신-속도-제어-구조로-문제-해결&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#8-sqs-%EA%B8%B0%EB%B0%98-%EB%B0%9C%EC%8B%A0-%EC%86%8D%EB%8F%84-%EC%A0%9C%EC%96%B4-%EA%B5%AC%EC%A1%B0%EB%A1%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0&quot; aria-label=&quot;8 sqs 기반 발신 속도 제어 구조로 문제 해결 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;8. SQS 기반 발신 속도 제어 구조로 문제 해결&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;발신 속도를 안정적으로 제어하기 위해 SQS를 도입하여 아키텍처를 재설계했습니다.&lt;/p&gt;
&lt;h3 id=&quot;개선된-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0%EB%90%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;개선된 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;개선된 아키텍처&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SQS 지연 처리: 대량의 메일이 쏟아지더라도 SQS가 이를 안전하게 보관하며 버퍼 역할을 수행합니다.&lt;/li&gt;
&lt;li&gt;Lambda 동시성 제어: Lambda의 Reserved concurrency 값을 4로 설정하여, Amazon Connect API 호출이 초당 4건을 넘지 않도록 정교하게 제어했습니다.&lt;/li&gt;
&lt;li&gt;관측성 확보: 모든 발신 성공/실패 로그를 Datadog으로 전송하여, 실제 전화가 몇 초 만에 도달했는지 대시보드에서 실시간으로 확인할 수 있게 구성했습니다.&lt;br&gt;
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 888px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/018b3ffbfa39fcafb25bac38e17c80de/8dc7d/review_1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.37499999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACnklEQVR42pVTW2vUUBDO/xNR8Q+oYFt88knwQQpe3goVVFAQRHwRqoL4VtTSiq21xaXbvSWbTTaXTTa3TbLJNpvN5XOSdKvig+vAlzkz5+SbmTNzGME20NIl3F1dxbWrV7CyvISVpeu4sbJc2g/u34PvuZhMQgRBgDAMkec5Cqm+fwpz2GOxyzdx2ODxef8IO4cNbH0/xubeEek69uscOqoD0fAQz2ZIkgQZERacc10hR5blYKpQOUR7AtaYgCO09RBHio+mFqCjB2goLtoDD/xwjA5pTvfAah66Qx9t1a18WuVjSmaCZAVo0WaLNgvdVEbo6j4d8hHFKWZphmmS/sLsb8TkZ+a1G4aJoWFgMNBK9HoCNE2DKPYh9ftwRyMsImeElmVCVVX4vo/hcFgiOjlBHE8RRVF5dwsTFmUXZM1mkzI1YJomWJalIBb+V84ydBwXKpUqyxLGlGVBOqIyXdeFbdtl5otkycynyTYkaHKvvDdJkuB5HsLJBB4RFpkW9mKEeUpdplWsIwtkpPSTZRr08+x0dOfjWwxb9k8weUxljYY4+vEN+3tb2PuyiT5fx3ikwLclpFMHWeygOJdNC1TrvPT9jsrHbH16h9drd3D71k1cvnQeFy+cw/NnjzDxNVg6D1NlMXZk5IlHLyGg5xECyajCjEgSl9ZuaedkM/UfO6h9fANFPAbfPoAqNsC1DiByNUjdGnrdOra3N/HyxVO83XiFem0XoUvj5UgIqYqA4Fr9Mmg01ukOszGNzYQawaPV5SCpXQhCA7zAosN3aNgV7HzdxpPH61h/uIYP7zegy+0S5oCDrnQgC8elXZAyWWQhiWyoukXv14Vg9CFLNQiqDM4JIIktykahpmQUmLqcnQCpVyE5ReqXOp+5+Am8FV7qbflqdQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/018b3ffbfa39fcafb25bac38e17c80de/263a4/review_1.webp 480w,
/static/018b3ffbfa39fcafb25bac38e17c80de/d8a71/review_1.webp 888w&quot; sizes=&quot;(max-width: 888px) 100vw, 888px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/018b3ffbfa39fcafb25bac38e17c80de/9aebd/review_1.png 480w,
/static/018b3ffbfa39fcafb25bac38e17c80de/8dc7d/review_1.png 888w&quot; sizes=&quot;(max-width: 888px) 100vw, 888px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/018b3ffbfa39fcafb25bac38e17c80de/8dc7d/review_1.png&quot; alt=&quot;Amazon Connect 기반 온콜 발신 테스트 후기&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Amazon Connect 기반 온콜 발신 테스트 후기&lt;/figcaption&gt;
	&lt;/div&gt;
	&lt;br&gt;
&lt;/center&gt;
&lt;h3 id=&quot;테스트-결과-46명에게-17초-만에-도달&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC-46%EB%AA%85%EC%97%90%EA%B2%8C-17%EC%B4%88-%EB%A7%8C%EC%97%90-%EB%8F%84%EB%8B%AC&quot; aria-label=&quot;테스트 결과 46명에게 17초 만에 도달 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;테스트 결과: 46명에게 17초 만에 도달&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;재설계된 구조로 대규모 발신 테스트를 진행한 결과, 단 한 건의 누락도 발생하지 않았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;성능: 46명 전원 발신 완료까지 약 17초 소요 (초당 4건 목표치 준수)&lt;/li&gt;
&lt;li&gt;안정성: API Throttling 에러 발생률 0%&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 624px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/098af04411acd6bd1705e6ecbb8ba57b/08d47/review_2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABDklEQVR42o2Pz0qEYBTFfZFeodcJ2wr+CxzdtWojOG1cVJtp39b1gNCmWvQCwYQkjsL4d2ZkbHRS8+R3IapdFw7f4fKd372Xq3Y7PD/MEb29YDa7hSzLMAwDk8kEuq6TmGfSNA2qqsJ1Xey3W9TVO5qmQd/3+C7u6fEex0cczs9OcHV9g1OehyRJJFEUIQgCFEUhEAMq4+s4zgirwJbZbDY4HA4/wMXrAvYFj/ndJbpPoG0/kCQJ8jxHURQIw5BCbJP/FDcMAxkWmE6nsG0b2/EcBsqyjGBswGq1IqVpivV6jbIsqR/HMfmu6/4C93UN0zRhWRZ98jyPwr+By+USURRRnykIAvi+T4PbtiXgF0cRUccTF257AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/098af04411acd6bd1705e6ecbb8ba57b/263a4/review_2.webp 480w,
/static/098af04411acd6bd1705e6ecbb8ba57b/5f6bc/review_2.webp 624w&quot; sizes=&quot;(max-width: 624px) 100vw, 624px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/098af04411acd6bd1705e6ecbb8ba57b/9aebd/review_2.png 480w,
/static/098af04411acd6bd1705e6ecbb8ba57b/08d47/review_2.png 624w&quot; sizes=&quot;(max-width: 624px) 100vw, 624px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/098af04411acd6bd1705e6ecbb8ba57b/08d47/review_2.png&quot; alt=&quot;온콜 품질 개선 이후 개발팀 실제 후기&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;온콜 품질 개선 이후 개발팀 실제 후기&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h2 id=&quot;9-전화--sms-이중화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#9-%EC%A0%84%ED%99%94--sms-%EC%9D%B4%EC%A4%91%ED%99%94&quot; aria-label=&quot;9 전화  sms 이중화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;9. 전화 + SMS 이중화&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;온콜이 안정적으로 작동하자, 실제 온콜을 받는 개발팀으로부터 실무적인 피드백이 도착했습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 820px; margin: 35px auto; font-family: &apos;Inter&apos;, sans-serif; display: flex; flex-direction: column; gap: 16px;&quot;&gt;
  &lt;div style=&quot;padding: 14px 18px; background: #f7f4ff; border-radius: 10px; border: 1px solid #e3ddff; color: #4a4a4a;&quot;&gt;
    &lt;i&gt;“회의 중이거나 이동 중이라 전화를 놓치는 경우가 있어요. 전화를 못 받더라도 어떤 이슈인지 SMS로 바로 확인할 수 있으면 좋겠습니다!”&lt;/i&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;이에 Amazon SNS를 활용한 SMS 발신 기능을 추가하여 알림 이중화를 구현했습니다.&lt;/p&gt;
&lt;h3 id=&quot;알림-레이어의-확장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EB%A6%BC-%EB%A0%88%EC%9D%B4%EC%96%B4%EC%9D%98-%ED%99%95%EC%9E%A5&quot; aria-label=&quot;알림 레이어의 확장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;알림 레이어의 확장&lt;/h3&gt;
&lt;p&gt;기존 Lambda에 Amazon SNS 연동을 추가하여, 단일 트리거로 두 가지 경로의 알림이 동시에 발생하도록 개선했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1단계 Voice: Amazon Connect를 통해 긴급 상황임을 알리는 전화 발신 (즉각적 대응 유도)&lt;/li&gt;
&lt;li&gt;2단계 Text: 동일한 TTS 안내 내용을 SMS로 즉시 전송 (기록성 및 가독성 확보)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;이제 담당자가 전화를 받지 못하더라도, 뒤이어 도착한 SMS를 통해 장애의 맥락을 즉시 파악하고 조치에 들어갈 수 있게 되었습니다.&lt;br&gt;
&apos;전화&apos;가 대응의 시작을 알린다면, &apos;SMS&apos;는 대응의 정확도를 높이는 역할을 수행하게 된 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;10-고정-비용-최적화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#10-%EA%B3%A0%EC%A0%95-%EB%B9%84%EC%9A%A9-%EC%B5%9C%EC%A0%81%ED%99%94&quot; aria-label=&quot;10 고정 비용 최적화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;10. 고정 비용 최적화&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;기존 외부 솔루션은 발신 건수와 관계없이 고정비용으로 월 200만 원 이상을 지출하고 있었습니다.&lt;br&gt;
하지만 Amazon SES + Amazon Connect 기반 서버리스 구조로, 전환하며 운영 비용을 월 10만원 이하(약 95% 이상 절감)로 조정할 수 있었습니다.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;&quot;&gt;
  &lt;thead&gt;
	&lt;tr style=&quot;background:#f7f4ff;&quot;&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;구분&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;기존 외부 온콜 솔루션&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;Olive Connect (신규 서비스)&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;비고&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;&lt;strong&gt;과금 방식&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;구독형 고정 비용&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;사용량 기반 과금&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;&lt;strong&gt;월 예상 비용&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;약 2,000,000원 + α&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;약 100,000원 미만&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;&lt;strong&gt;약 95% 절감&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;&lt;strong&gt;주요 과금 항목&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;유저당 라이선스, 플랫폼 사용료&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;Amazon Connect 발신 비용, Lambda 호출 비용 등&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;&lt;strong&gt;발신 안정성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;해외 번호 발신 (스팸 차단 위험)&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;국내 번호 발신 및 서버리스로 안정성 높음&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:center;&quot;&gt;안정성 향상&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;h2 id=&quot;11-최종-온콜-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#11-%EC%B5%9C%EC%A2%85-%EC%98%A8%EC%BD%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;11 최종 온콜 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;11. 최종 온콜 아키텍처&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;아래는 현재 운영 중인 온콜 시스템 구성도입니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 100%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1874px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ba6c0c5f145785b48ba461f2c56a8a44/bb65e/diagram.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 29.583333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAGABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAd0ECf/EABcQAAMBAAAAAAAAAAAAAAAAAAABESH/2gAIAQEAAQUChok5/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFhAAAwAAAAAAAAAAAAAAAAAAABAx/9oACAEBAAY/Air/xAAaEAACAgMAAAAAAAAAAAAAAAAAARExUbHR/9oACAEBAAE/IaYIlyIIns//2gAMAwEAAgADAAAAEAQ//8QAFxEBAAMAAAAAAAAAAAAAAAAAAAERMf/aAAgBAwEBPxC06//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABoQAQACAwEAAAAAAAAAAAAAAAEAESExUbH/2gAIAQEAAT8QFoiFpq6gSVJynYzGQBzfZ//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ba6c0c5f145785b48ba461f2c56a8a44/263a4/diagram.webp 480w,
/static/ba6c0c5f145785b48ba461f2c56a8a44/a6361/diagram.webp 960w,
/static/ba6c0c5f145785b48ba461f2c56a8a44/9ec20/diagram.webp 1874w&quot; sizes=&quot;(max-width: 1874px) 100vw, 1874px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ba6c0c5f145785b48ba461f2c56a8a44/a3e66/diagram.jpg 480w,
/static/ba6c0c5f145785b48ba461f2c56a8a44/fb816/diagram.jpg 960w,
/static/ba6c0c5f145785b48ba461f2c56a8a44/bb65e/diagram.jpg 1874w&quot; sizes=&quot;(max-width: 1874px) 100vw, 1874px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ba6c0c5f145785b48ba461f2c56a8a44/bb65e/diagram.jpg&quot; alt=&quot;Google Groups → SES → SNS → SQS → Lambda → Amazon Connect 기반 온콜 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Google Groups와 Amazon SES, Amazon Connect를 활용한 Olive Connect 전체 구성도&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;h2 id=&quot;12-사내-전파와-안착--개발자-포털idp을-통한-운영-가이드-제공&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#12-%EC%82%AC%EB%82%B4-%EC%A0%84%ED%8C%8C%EC%99%80-%EC%95%88%EC%B0%A9--%EA%B0%9C%EB%B0%9C%EC%9E%90-%ED%8F%AC%ED%84%B8idp%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%9A%B4%EC%98%81-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%A0%9C%EA%B3%B5&quot; aria-label=&quot;12 사내 전파와 안착  개발자 포털idp을 통한 운영 가이드 제공 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;12. 사내 전파와 안착 — 개발자 포털(IDP)을 통한 운영 가이드 제공&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;온콜 시스템이 안정적으로 운영되기 시작하면서, 다음 과제는 &lt;b&gt;&apos;이 시스템을 어떻게 조직 전체에 안전하게 확산시키고, 오래 유지할 것인가&apos;&lt;/b&gt;였습니다.
아무리 잘 만든 시스템이라도 사용 방법이 명확하지 않거나, 담당자 변경 시 지식이 단절되는 구조라면 문제가 됩니다.
시간이 지나면 결국 또 다른 운영 리스크로 되돌아오기 마련입니다.&lt;/p&gt;
&lt;p&gt;이를 방지하기 위해, 온콜 시스템의 &lt;strong&gt;설계 배경, 사용 방법&lt;/strong&gt;을 정리한 가이드 문서를 작성했고,
이를 사내 IDP(Internal Developer Portal)에 배포했습니다.&lt;/p&gt;
&lt;p&gt;가이드에는 다음과 같은 내용을 포함했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;온콜 시스템 전체 아키텍처 개요&lt;/li&gt;
&lt;li&gt;신규 서비스 온콜 연동 방법 (이메일 트리거 설정 가이드)&lt;/li&gt;
&lt;li&gt;Google Groups 기반 온콜 그룹 관리 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;이제 새로운 서비스에 온콜을 도입할 때,
별도의 설명이나 인프라 지원 없이도 &lt;strong&gt;가이드 문서만으로 셀프 적용이 가능한 구조&lt;/strong&gt;가 되었습니다.
기술적인 완성도뿐 아니라,
운영과 전파까지 고려한 문서화는 이 온콜 시스템을 &apos;특정 팀의 도구&apos;가 아닌 &lt;strong&gt;조직의 공통 인프라 자산&lt;/strong&gt;으로 만드는 데 중요한 역할을 했습니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;마무리&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;현재 Amazon Connect는 Amazon Polly TTS 서비스로 장애 정보를 온콜로 전달하고 있습니다. 하지만 긴박한 장애 상황에서 기계적인 한국어 발음은 때때로 정보 전달력을 떨어뜨리기도 합니다.&lt;br&gt;
이에 다음 단계로 &lt;strong&gt;음성 사용자 경험(VUX)을 고도화할 계획&lt;/strong&gt;입니다. 네이버 클로바 더빙이나 Typecast 같은 외부 AI TTS 솔루션을 연동하여, 마치 동료가 직접 전화를 걸어 상황을 설명해 주는 듯한 자연스러운 음성 안내를 구현하고자 합니다.
단순히 &apos;듣기 좋은 목소리&apos;를 넘어, 장애 대응 중인 엔지니어의 인지 부하를 줄이고, 상황 파악 속도를 높이는 데 기여할 것입니다.&lt;/p&gt;
&lt;p&gt;그리고, 이메일 기반 온콜이라는 단순한 접근 방식은 예상보다 훨씬 강력했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;신뢰성: 복잡한 연동 없이 이메일 표준을 활용해 발신 실패율 0% 달성&lt;/li&gt;
&lt;li&gt;완결성: 전화와 SMS 이중화를 통한 장애 전파 누락 방지&lt;/li&gt;
&lt;li&gt;유연성: 별도 DB 없이 Google Groups만으로 온콜 그룹 관리 자동화&lt;/li&gt;
&lt;li&gt;효율성: 운영 리소스와 비용(95% 절감)의 획기적 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;무엇보다, 가장 단순한 구조가 가장 안정적인 구조가 될 수 있다는 경험을 얻었습니다.&lt;/p&gt;
&lt;p&gt;온콜은 단순한 전화 발신 기능을 넘어, &lt;b&gt;장애 상황과 엔지니어의 대응 사이의 간극을 메우는 &apos;서비스 신뢰성의 마지막 1cm&apos;&lt;/b&gt;입니다. 이 짧은 간극을 어떻게 채우느냐에 따라 고객이 경험하는 서비스의 안정성이 결정됩니다.&lt;br&gt;
올리브영 SRE팀은 앞으로도 복잡하게 얽힌 문제들을 가장 명쾌하고 단순한 기술로 풀어내며, 고객이 믿고 머무를 수 있는 탄탄한 플랫폼을 만들어 나갈 것입니다. &apos;단순함으로 구현한 견고한 신뢰&apos;에 대한 저희의 또 다른 도전과 해결 사례들도 계속해서 기대해 주세요.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[QA Built on Trust - 팀워크가 품질을 만든다]]></title><description><![CDATA[안녕하세요. 주문결제, 클레임, 고객센터 스쿼드의 QA 엔지니어 유갱입니다! 
2025년의 끝자락, 저희 QA…]]></description><link>https://oliveyoung.tech/2025-12-17/QA-Conference-2025/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-17/QA-Conference-2025/</guid><pubDate>Wed, 17 Dec 2025 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 주문결제, 클레임, 고객센터 스쿼드의 QA 엔지니어 유갱입니다! &lt;br&gt;
2025년의 끝자락, 저희 QA 조직은 한 해를 마무리하며 올리브영의 품질을 지켜냈던 치열했던 시간들을 반추해보고 있습니다.
그 중 가장 뜨거운 에너지와 팀워크를 발휘했던 순간은 바로 7월 5일에 참여했던 &lt;b&gt;&lt;a href=&quot;https://www.qa-korea.com/&quot;&gt;2025 QA Korea Conference&lt;/a&gt;&lt;/b&gt; 였습니다.
이 글에서는 올리브영 QA 조직이 복잡한 옴니채널 환경에서 어떤 비전을 가지고 외부와 소통했는지, 그리고 팀워크가 어떻게 품질의 기반이 되었는지를 회고록 형식으로 공유하려 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;QA Korea Conference는 국내 QA 직무 또는 소프트웨어 품질 관련 분야에 종사하는 실무자들이 모이는 큰 규모의 컨퍼런스입니다.
주로 각자의 업무 프로세스와 전략을 공유하며 함께 성장을 도모하고 있는데요!
2022년을 시작으로 다양한 업계의 QA 전문가들이 서로를 알아가며 네트워크를 넓혀 왔습니다.
그동안 COVID-19 등의 이유로 온라인으로만 이어오던 행사를 올해 최초! 오프라인으로 진행한다는 소식을 듣고,
올리브영은 국내 SW 품질 생태계 확장에 기여하고자 공식 후원사로 참여를 결정했습니다.
저희 QA팀은 이 뜻깊은 자리를 주도적으로 준비하며, 외부와 소통하는 소중한 기회로 삼았습니다. &lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-2025-qa-korea-conference-우리가-왜-거기-있었냐면요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-2025-qa-korea-conference-%EC%9A%B0%EB%A6%AC%EA%B0%80-%EC%99%9C-%EA%B1%B0%EA%B8%B0-%EC%9E%88%EC%97%88%EB%83%90%EB%A9%B4%EC%9A%94&quot; aria-label=&quot; 2025 qa korea conference 우리가 왜 거기 있었냐면요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍭 2025 QA Korea Conference, 우리가 왜 거기 있었냐면요.&lt;/h2&gt;
&lt;p&gt;드디어 오프라인으로 문을 열게 된 QA Korea Conference! &lt;br&gt;
2025년의 QA Conference는 ‘국내 SW 품질 엔지니어링 생태계 확장’을 모토로 약 500여 명의 업계 전문가가 한 자리에 모인 행사였습니다.
첫 오프라인 개최였던 만큼, QA 종사자들이 직접 경험을 공유하며 교류할 수 있었고 현장은 그야말로 뜨거운 열기로 가득했습니다.
그동안 화면에서만 보던 발표자와 참가자들이 한 공간에 모였고 발표 세션과 부스, 네트워킹 공간까지 모든 것이 알차게 준비되어 있었습니다.&lt;br&gt;&lt;br&gt;
공식 후원사로서 올리브영 QA팀은 이 순간을 놓칠 수 없었습니다.
회사의 지원을 통해 팀장님 포함 팀원 다섯 명이 직접 부스를 기획/운영하고 연사로 참여하며 올리브영의 품질 문화를 전파하기 위해 현장에 나섰습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;☀️ 우리 팀의 참여 목적은 단순하고 명확했습니다️.&lt;/p&gt;
&lt;div style=&quot;
  margin: auto;
  padding: 22px;
  border-left: 4px solid #d5aeef;
  border-right: 4px solid #d5aeef;
  background: rgb(248,243,255);
  font-family: &apos;Arial&apos;, sans-serif;
  line-height: 1.6;
&quot;&gt;
  &lt;div style=&quot;padding-left: 4px; display: flex; flex-direction: column; gap: 14px;&quot;&gt;
      1. 올리브영 QA팀은 어떤 일을 하고 있는가?&lt;br&gt;
      2. 우리 팀은 어떤 목표를 지향하고, 어떻게 나아가고 있는가?&lt;br&gt;
      3. 우리 팀의 존재감과 팀워크를 보여주며 향후 인재 확보 가능성과 올리브영 서비스의 홍보 효과 높이기
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
이 세 가지를 진솔하게 전하고, 나아가 저희 팀에 관심이 있거나 미래의 지원자가 될 지도 모르는 분들과 더 가깝게 소통하기 위해서였어요!&lt;br&gt;&lt;br&gt;&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/af731238d4161ef2cc82a371634a23cf/3a3ec/qa-techblog-conference.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 24.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABfUlEQVR42g2Q30saAQCA7x8J6qmHtn4gjga97GEjItgqWmVlu8o79fTUM0/Tm95QWnQGLi+1X7aGbYJlVGwEQRA1Bnv1Jdb7/o0vH77n7+MTXFMB5lwhxt99YOztErMzMXxyCtW/iCKLaEEvle0a9lYZcW6CeMTP/Owk4bBEfjNB0daxSzFKlSTVQxNhXjTwKBaKWkANbNA8eeDu7j+Nep39nT22izZnF48c1/9ydLjL1c8m6bSJx6PhV3TCKxESSRUl0JavLCOk1i8x838of2kRi35DW21wcNwiGoqQMRJkzQRrORvLOiAZC7XxYWZ323W3FLZuyK1/xyNJyN4FVg0NIZiqEs+cU8z/Jps+wzB/8fFzk77uDl44njHoeM7QywHkZZH+3h6cfV0Y6RJ75X9Ujx6xCpdIkhf3wjRK0I8g+zLoegFro8HX/Xt+1FrsVK4ZffMKt+s9I8OvcTodfDLjDA700NnZgbiosmmdks3VCIWTRDUvuuZufxV5Ajrz8y8vh7EOAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/af731238d4161ef2cc82a371634a23cf/263a4/qa-techblog-conference.webp 480w,
/static/af731238d4161ef2cc82a371634a23cf/a6361/qa-techblog-conference.webp 960w,
/static/af731238d4161ef2cc82a371634a23cf/0b34d/qa-techblog-conference.webp 1920w,
/static/af731238d4161ef2cc82a371634a23cf/da28f/qa-techblog-conference.webp 2880w,
/static/af731238d4161ef2cc82a371634a23cf/82e36/qa-techblog-conference.webp 3577w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/af731238d4161ef2cc82a371634a23cf/9aebd/qa-techblog-conference.png 480w,
/static/af731238d4161ef2cc82a371634a23cf/a91f8/qa-techblog-conference.png 960w,
/static/af731238d4161ef2cc82a371634a23cf/ac7a9/qa-techblog-conference.png 1920w,
/static/af731238d4161ef2cc82a371634a23cf/f9c26/qa-techblog-conference.png 2880w,
/static/af731238d4161ef2cc82a371634a23cf/3a3ec/qa-techblog-conference.png 3577w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/af731238d4161ef2cc82a371634a23cf/ac7a9/qa-techblog-conference.png&quot; alt=&quot;qa techblog conference&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;2025 QA 코리아 컨퍼런스 현장&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;오전 9시부터 시작된 행사는 약 500여 명의 참여자와 14명의 연사자가 함께하며 더 뜨거운 열기를 더했습니다.&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;- 방문객 수 : 약 500명 (QA 리더십부터 주니어 실무자까지, 폭넓은 경력층 참여)
- 연사자 수 : Open Talk 포함 14명
- 스폰서 수 : 올리브영 포함 14곳&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-이-부스-그냥-지나칠-수-없게-만들었습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4-%EB%B6%80%EC%8A%A4-%EA%B7%B8%EB%83%A5-%EC%A7%80%EB%82%98%EC%B9%A0-%EC%88%98-%EC%97%86%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; 이 부스 그냥 지나칠 수 없게 만들었습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍭 이 부스, 그냥 지나칠 수 없게 만들었습니다.&lt;/h2&gt;
&lt;p&gt;| 🐾 방문자_발목_부여잡은_우리_부스의_매력.zip &lt;br&gt;&lt;/p&gt;
&lt;p&gt;저희는 올리브영 QA팀의 색깔을 그대로 담은 공간으로 부스를 꾸몄습니다. 눈에 띄면서도 방문객들의 참여도를 높이고, 올리브영에 대한 잔상이 오래도록 남게 하기 위한 의도였어요!
부스 앞 판넬에는 참여를 유도하면서도 오늘드림 문구를 넣어 올리브영 서비스의 홍보 효과까지 높였답니다. &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;img src=&quot;/9340194c7dfdf14930c39462cc8356eb/qa-techblog-booth.jpg&quot; width=&quot;100%&quot; /&gt;
    &lt;figcaption&gt;우리 부스, 올리브영의 색깔을 참 잘 담아내지 않았나요?&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이 때문이었을까요? 올리브영 QA팀의 부스는 방문객들의 참여가 끊이지 않았습니다. 우리도 그 기대에 부응하고자 최선을 다했습니다.
특히 팀의 업무와 올리브영 서비스를 전파하기 위해 온몸을 불사르고 왔답니다. &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7c35b132abaddd755129b8128fc43bc5/7318a/qa-techblog-pplr.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 22.916666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABiUlEQVR42g3M30saAQDA8fsvemgR7CHCPVSsBUUiLVuSBWK70erIKWa6nOLA7oTc+bNtzUWRM45gVpLYkpZb9BAUvjSOFaMeKkYwqP/j+K73Dx8hv35BZVdno3RAcX2b6s4Oa1+WSckB0rEgatTL8kIKbTXP3l6Vev2ESvWI8+t/3Nzdop+ecXhwTG2rbPz6vo3Q71ZxeHK4oxvMJgpom/dpbZ/wtIQc8VGYT5DNfGUmVsQnb+GVd7FNapRqdU5+6+z/+EnUNUk66DEWIlMIyUwci9VGIZdg1D5IS/NDXgf8vP9c4JXbz1w8yTv1E0sfs4hjIm9lF5IkYrZYCYXe3LsV8ivzxKI+Y2SgDyGhKPR2dvEhqVIqbRKacNBrtmDue8aDhkY62ntIKjECnnGcdisz7jHaOh7T1W1mcPgl3lAQTzhCsaoZ38rTCE8emWhpamQxG+fvpc7Vn2PO9UPUuEybqRV7/1PKWh7ROcpzhw0lPIFjaAibc4QXcwO4vFOIkkRq1W9UjhT+A/cQ8to5LTX6AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7c35b132abaddd755129b8128fc43bc5/263a4/qa-techblog-pplr.webp 480w,
/static/7c35b132abaddd755129b8128fc43bc5/a6361/qa-techblog-pplr.webp 960w,
/static/7c35b132abaddd755129b8128fc43bc5/0b34d/qa-techblog-pplr.webp 1920w,
/static/7c35b132abaddd755129b8128fc43bc5/da28f/qa-techblog-pplr.webp 2880w,
/static/7c35b132abaddd755129b8128fc43bc5/f1bfd/qa-techblog-pplr.webp 3359w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7c35b132abaddd755129b8128fc43bc5/9aebd/qa-techblog-pplr.png 480w,
/static/7c35b132abaddd755129b8128fc43bc5/a91f8/qa-techblog-pplr.png 960w,
/static/7c35b132abaddd755129b8128fc43bc5/ac7a9/qa-techblog-pplr.png 1920w,
/static/7c35b132abaddd755129b8128fc43bc5/f9c26/qa-techblog-pplr.png 2880w,
/static/7c35b132abaddd755129b8128fc43bc5/7318a/qa-techblog-pplr.png 3359w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7c35b132abaddd755129b8128fc43bc5/ac7a9/qa-techblog-pplr.png&quot; alt=&quot;qa techblog pplr&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;문전성시를 이뤘던 우리 부스의 모습&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;h4 id=&quot;qa-misson--qa-직무와-직결된-그런데-이제-지루하지-않은-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#qa-misson--qa-%EC%A7%81%EB%AC%B4%EC%99%80-%EC%A7%81%EA%B2%B0%EB%90%9C-%EA%B7%B8%EB%9F%B0%EB%8D%B0-%EC%9D%B4%EC%A0%9C-%EC%A7%80%EB%A3%A8%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%80-&quot; aria-label=&quot;qa misson  qa 직무와 직결된 그런데 이제 지루하지 않은  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;QA Misson : QA 직무와 직결된, 그런데 이제 지루하지 않은 !&lt;/h4&gt;
&lt;br&gt;
&lt;p&gt;부스 프로그램은 세 가지의 간단한 미션으로 진행했어요. 미션 카드를 들고 게임에 참여하여 두 개 이상의 PASS 스탬프를 받으면 미션 성공!
마지막에는 자유롭게 뽑기 게임을 한 후 당첨된 상품을 수령하면 됩니다. 카드 뒷면에는 우리 팀의 업무 방식을 소개하며 팀의 홍보 효과를 높였어요! &lt;br&gt;&lt;br&gt;
업무에 대한 간접적인 설명과 참여자들의 흥미를 동시에 잡았던 세 가지 게임을 간단하게 소개할게요. &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5579fdd0492583ed1cf29da58e9f9c50/9ac23/qa-techblog-brochure.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABlUlEQVR42g3O20tTcQDA8fOfFF0gdHpSU9cKCWdb245zk5UoBqkUpqxWpDCcTstdnM0LTm1qTOhhEBEWSBNRTKeRoL5IYKz1knYQvEQFvfj7dh4+8H38Sg73BDfbRrDen8TeMoXrYQxDXVxY3IM8jntoHfbSPNDBgzEP5pYx8quSlDXGcMc8NITbNO3ciz4STZEu9DVJVXr1foXs3irb2TSZvTSz6TmKXOPiojOErASxNk1ivhOnpHqAcxVRLigJim8FMTZ0YWoMaHo1PaLQ6efs9VFVSiTX+ff3D+rPIw73D1n5vEOOY1rkKk+QLV7M9cMYa6MYqkOcMnSiq5wm1/YU2dxNka0P+UaAfFNQnLni0zqiSq9Ti/w+PuD41wFwwsaXXa7VvBCWu2Fkqx/T7RGMdUPonf3oyn1cdo2SZ39GgcVHoa2XUnuAnAq/OK1vp1gJqVL45Tu+ZnZJpdZZWtpk/M0aM28XxNb2D2aXMyx8yjK/luXD8jc+bn5nKDHHVZsXnXZXoDznUtWgJirOG/vIc0TU/8cz95TKidJYAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5579fdd0492583ed1cf29da58e9f9c50/263a4/qa-techblog-brochure.webp 480w,
/static/5579fdd0492583ed1cf29da58e9f9c50/a6361/qa-techblog-brochure.webp 960w,
/static/5579fdd0492583ed1cf29da58e9f9c50/0b34d/qa-techblog-brochure.webp 1920w,
/static/5579fdd0492583ed1cf29da58e9f9c50/da4f1/qa-techblog-brochure.webp 2494w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5579fdd0492583ed1cf29da58e9f9c50/9aebd/qa-techblog-brochure.png 480w,
/static/5579fdd0492583ed1cf29da58e9f9c50/a91f8/qa-techblog-brochure.png 960w,
/static/5579fdd0492583ed1cf29da58e9f9c50/ac7a9/qa-techblog-brochure.png 1920w,
/static/5579fdd0492583ed1cf29da58e9f9c50/9ac23/qa-techblog-brochure.png 2494w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5579fdd0492583ed1cf29da58e9f9c50/ac7a9/qa-techblog-brochure.png&quot; alt=&quot;qa techblog brochure&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;프로그램 소개 브로셔와 QA 미션카드&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;h4 id=&quot;1-초간단-ox-퀴즈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%B4%88%EA%B0%84%EB%8B%A8-ox-%ED%80%B4%EC%A6%88&quot; aria-label=&quot;1 초간단 ox 퀴즈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 초간단 OX 퀴즈&lt;/h4&gt;
&lt;p&gt;OX 퀴즈는 간단하지만 올리브영 QA팀 업무의 핵심 내용을 담아 구성했습니다!
특히 저희 올리브영 테크 블로그의 QA 포스트나 외부 발표 세션 참여 경험이 있으셨던 독자분들이
&apos;오, 이거 아는 내용이다!&apos;라고 반가워하며 정답을 척척 맞히는 모습에 보람을 느꼈습니다.
이는 컨퍼런스 현장을 넘어, 우리가 온라인으로 공유했던 기술적 메시지와 노하우가 실제 커뮤니티와 QA 생태계에 잘 전달되고 있음을 확인하는 소중한 경험이었습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 30%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3391a715e18ab3d45ec78ce4c34c7aad/2a0c7/qa-techblog-ox.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 70.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAADqUlEQVR42kWU+1NUVRzA959IzR/T8lGRKEOGYI5ZPAQDFBAUtZiBmeyBjhCOawYOIG8WkDciiKACMzDIK3BhgUVxixlnLCBYeZRIRBdi14WF3fvpcFntzJw5534fn+/jnHNV7hHXcAnJ4uCRZLz8EzkSEI/fsRT2h1ez43AZe0I0uJ0oY1dgBU7+N/AKL8H3ZD47vYtw/zKFfRFJ7A0uk499m8TB03GSyjk4G+egXN7xq+Q93yLe/TSbQ6dukVT8CLWmh70ht/kuSUtWpYGYtC4i4zu5kNaDS1Ap7wdo+OComJ+XyGdi6giLLhTAo8V4RlYQGnODz6KK2CaycgqowCuyFp+oWj754g7B5xo5fbFZWYOiG/COrMMluBS38FxcQ6+z+3iq7HMuUVQigE6BGq4UVjE69ZyHT0bpMUygezzFg/5xtI8mePBwgrZeIx36ce5rR2ntMdIl5N0Dk0I3JuwnGZ+elhNu5uMWcVFSbfTQkFDQwdpo0Y3T1DVGs84oIM9o63vGvbZh7neP0S2C3G76lSax1w/+KfTj3G0dYfC3mTVXOaumkR2BCZLqo+NVZJYbeDUmns9zp2WYuvYRARuioXOUioanAmpEZ5gir3qQe61DSqZ/zZlfuclXcrS86Z4tqXYF3iSldGBdKsvYbLBgMmN6ucTSsozZssKi2aqslqUV5heWMFllLFY7NruwX11VXC9l6djsniWpNu3T8GNWhyMOGP9pp3PsPFpjHLPmJ45AdmSb4sjq03aWWtOwzYxgqolhxbygmMRl9rLBI0dSbTmQyVXNOnDOPErL8FlMyy+YXvyF1pFvRBbLis5ut63DLf+yUBjOnNoZS3cJdkcql3O72eKdIqk278/l+wydIjXOddE7nqjsbXYrbSPRorwX/wMdUHNzKrNnN2D7e/I1ML5Ay9u+1yTVnrAEEkvaFal19SU//R6D4Y9CAU5GP5HmgMmCtV6ydUSPlOGHqf4H5vNPsLpqX+9hto433ETJh76OJf1Wk6NXokf2BYzzd5labBaSldeHJdsducwOwYyYoq8Y9dgsi4pJbLqOjR65kurD8DT8o/Mor++nptFAcqGeuPSfuZT5mMvZPSSJ74TrfSQX9ZNS0o86bwB1fpe4u82oC/qIzejmQmqn7BNVv/bWJdVWz1K2+xYSdr4KdXoT/l9Vi9PKZZNHjvJzOHCmBtfgSj4+VSOeXC3bvEvZ6p3G7tBEXE/G4xJ2lbc8s+Xth8vFsy2W/gOmYUfpDr09cgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3391a715e18ab3d45ec78ce4c34c7aad/263a4/qa-techblog-ox.webp 480w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/a6361/qa-techblog-ox.webp 960w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/0b34d/qa-techblog-ox.webp 1920w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/da28f/qa-techblog-ox.webp 2880w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/6f6cd/qa-techblog-ox.webp 3374w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3391a715e18ab3d45ec78ce4c34c7aad/9aebd/qa-techblog-ox.png 480w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/a91f8/qa-techblog-ox.png 960w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/ac7a9/qa-techblog-ox.png 1920w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/f9c26/qa-techblog-ox.png 2880w,
/static/3391a715e18ab3d45ec78ce4c34c7aad/2a0c7/qa-techblog-ox.png 3374w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3391a715e18ab3d45ec78ce4c34c7aad/ac7a9/qa-techblog-ox.png&quot; alt=&quot;qa techblog ox&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;QA 검증 범위에 대한 OX 퀴즈! 정답이 뭘까요?&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;h4 id=&quot;2-qa-defect-challenge-버그-헌터즈---바이브-코딩으로-다듬고-다듬은-ux&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-qa-defect-challenge-%EB%B2%84%EA%B7%B8-%ED%97%8C%ED%84%B0%EC%A6%88---%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9%EC%9C%BC%EB%A1%9C-%EB%8B%A4%EB%93%AC%EA%B3%A0-%EB%8B%A4%EB%93%AC%EC%9D%80-ux&quot; aria-label=&quot;2 qa defect challenge 버그 헌터즈   바이브 코딩으로 다듬고 다듬은 ux permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. QA Defect Challenge 버그 헌터즈 - 바이브 코딩으로 다듬고 다듬은 UX&lt;/h4&gt;
&lt;p&gt;&apos;버그 헌터즈&apos; 게임은 QA팀원들이 이번 행사를 위해 직접 기획하고 구현한 웹 기반 미니 게임입니다.
특히 Netlify를 활용해 빠르게 배포함으로써 현장에서 방문객들에게 직접 체험 기회를 제공했습니다.
팀원들이 바이브 코딩을 통해 즐겁게 만들었으며, 올리브영 QA팀이 단순 테스트를 넘어 직접 코드를 구현하고 배포할 수 있는 역량을 갖췄음을 보여주고자 했습니다. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;🕹️ 버그 헌터즈 게임을 직접 체험해 보세요! : &lt;b&gt;&lt;i&gt;&lt;a href=&quot;https://qa-conference-2025.netlify.app/&quot;&gt;QA Defect Challenge 버그 헌터즈 체험하기&lt;/a&gt;&lt;/i&gt;&lt;/b&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;본 게임을 개발하는 과정에서 가장 집중했던 부분은 코드를 논리적으로 우선 작성한 뒤, 사용자가 체감하기에 자연스럽고 편안한 타이밍을 맞추는 과정이었습니다.
그리고 단순히 기능 구현을 넘어 사용자 경험을 극대화하기 위해 애니메이션과 깨알 같은 효과음의 타이밍까지 QA 관점에서 섬세하게 다듬었습니다. &lt;br&gt;
사용자에게 직관적이며 불편한 점이 없어야 한다는 원칙(feat. QA Engineer 직업병)이 반영된 게임인 만큼 실제 현장에서의 반응도 좋았답니다!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/593c8fd18d0d100b40f3a872fbac6318/b774d/qa-techblog-game.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 27.708333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZUlEQVR42i2R247TMBCG+7rccc8l78AFT7GiEuy2FeIgQFtpQVpgUdqqTZpN4sRN4pwPJP5wwtr6NePfnm808qIfeyZ1Y0unm6e8px4asj4jjEO8yMP1XfzI5+yfScqE7f2W1fsbltdL3q3fmviGzWbNArOyouDhYHH36weeEJPFoEfKv4q8kAh5JohsROQQJz5FnWCdfhMELp53xjNNXM8mNrUzMA0F+7tb7r9+43FvGUczak3eZYyjpmxLZCaJ88vs112DE5zmxtrs3kwz6mE+z8CuykjFASWP1Cp8egiytFGF4BILROgSGE15kkvc0GYYtIkCOzrxKIO56D9wTPnivOTmz3PC+nYGjmbkpPZp24K6KUnSC6mKqZoEVUVYh5949pHTboe933PcWUSOwyLvClSnkJVLWDiknWTy0lYhqtB8zHSfEzcpysC3/itWxxd8+L5h9XHN+vPKaMP1pyWvr57xD3tpvSajRRjgAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/593c8fd18d0d100b40f3a872fbac6318/263a4/qa-techblog-game.webp 480w,
/static/593c8fd18d0d100b40f3a872fbac6318/a6361/qa-techblog-game.webp 960w,
/static/593c8fd18d0d100b40f3a872fbac6318/0b34d/qa-techblog-game.webp 1920w,
/static/593c8fd18d0d100b40f3a872fbac6318/a0e7e/qa-techblog-game.webp 2568w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/593c8fd18d0d100b40f3a872fbac6318/9aebd/qa-techblog-game.png 480w,
/static/593c8fd18d0d100b40f3a872fbac6318/a91f8/qa-techblog-game.png 960w,
/static/593c8fd18d0d100b40f3a872fbac6318/ac7a9/qa-techblog-game.png 1920w,
/static/593c8fd18d0d100b40f3a872fbac6318/b774d/qa-techblog-game.png 2568w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/593c8fd18d0d100b40f3a872fbac6318/ac7a9/qa-techblog-game.png&quot; alt=&quot;qa techblog game&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;진행하는 입장에서도 재미있었던 버그헌터즈&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;게임은 올리브영 QA 업무와 직접적으로 관련된 질문과 답변으로 구성했습니다. 정답을 바로 맞히기 보다는 한 번 더 생각할 수 있도록 유도했어요!
참여율을 높이고 긍정적인 경험을 제공하기 위해 틀렸더라도 재도전의 기회가 있었다는 건 비밀이에요😉 &lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;3-카드-선택-게임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%B9%B4%EB%93%9C-%EC%84%A0%ED%83%9D-%EA%B2%8C%EC%9E%84&quot; aria-label=&quot;3 카드 선택 게임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 카드 선택 게임&lt;/h4&gt;
&lt;p&gt;카드 선택 게임은 본인이 풀 문제 카드를 직접 골라 답을 맞히는 게임이었어요.
서로 다른 종류의 문제 카드 중 하나를 고르면, 그 문제에 맞는 답안지 세 장이 오픈되는 3지 선다형의 방식이었답니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 30%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/600dcdc6dbc47c6a21b89050bc3aa779/81f49/qa-techblog-cardtable.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAAF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAgAB/9oADAMBAAIQAxAAAAETyPyPZ0V//8QAGRABAQADAQAAAAAAAAAAAAAAAgEAAwQS/9oACAEBAAEFAuQ1O8/rFKbrGV3w9tS//8QAFhEBAQEAAAAAAAAAAAAAAAAAABES/9oACAEDAQE/Aa0//8QAFxEBAAMAAAAAAAAAAAAAAAAAAAECIf/aAAgBAgEBPwHE0f/EABwQAAICAgMAAAAAAAAAAAAAAAARARICMVFhgf/aAAgBAQAGPwIajoWWxwVp6zg//8QAGhABAAMBAQEAAAAAAAAAAAAAAQARIUFRMf/aAAgBAQABPyFwCleMo4kwXkMAFONxzLU+XOAsGCNPCf/aAAwDAQACAAMAAAAQxC//xAAXEQEBAQEAAAAAAAAAAAAAAAABABFR/9oACAEDAQE/EOCFl//EABcRAQADAAAAAAAAAAAAAAAAAAEAESH/2gAIAQIBAT8QaOsC8n//xAAcEAEAAgMBAQEAAAAAAAAAAAABABEhMUFRYXH/2gAIAQEAAT8QURhcArWm+RsmlvG30hA94z9mxJFNMPYwqgBY8/C41bUeE//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/600dcdc6dbc47c6a21b89050bc3aa779/263a4/qa-techblog-cardtable.webp 480w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/a6361/qa-techblog-cardtable.webp 960w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/0b34d/qa-techblog-cardtable.webp 1920w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/da28f/qa-techblog-cardtable.webp 2880w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/98b7d/qa-techblog-cardtable.webp 3840w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/d182c/qa-techblog-cardtable.webp 4032w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/600dcdc6dbc47c6a21b89050bc3aa779/a3e66/qa-techblog-cardtable.jpg 480w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/fb816/qa-techblog-cardtable.jpg 960w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/aaf92/qa-techblog-cardtable.jpg 1920w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/1d134/qa-techblog-cardtable.jpg 2880w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/9ad4e/qa-techblog-cardtable.jpg 3840w,
/static/600dcdc6dbc47c6a21b89050bc3aa779/81f49/qa-techblog-cardtable.jpg 4032w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/600dcdc6dbc47c6a21b89050bc3aa779/aaf92/qa-techblog-cardtable.jpg&quot; alt=&quot;qa techblog cardtable&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;정답을 빨리 선택해야 해요. 3,2,1..!&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
  &lt;br&gt;
&lt;p&gt;답안지가 공개된 후 빠르게 답을 골라야하는 방식에 다들 긴장해서였을까요?
약간의 위트를 더해 힌트를 드렸지만, 생각보다 오답이 많아서 더 긴장감 넘치고 재미있었습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-발-디딜-틈-없고-활력이-넘쳤던-발표-세션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%B0%9C-%EB%94%94%EB%94%9C-%ED%8B%88-%EC%97%86%EA%B3%A0-%ED%99%9C%EB%A0%A5%EC%9D%B4-%EB%84%98%EC%B3%A4%EB%8D%98-%EB%B0%9C%ED%91%9C-%EC%84%B8%EC%85%98&quot; aria-label=&quot; 발 디딜 틈 없고 활력이 넘쳤던 발표 세션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍭 발 디딜 틈 없고 활력이 넘쳤던 발표 세션&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; energy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; crowd&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getEnergy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;energy &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;Infinity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  energy&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;⚠️ EnergyOverflow: 현장의 열기가 메모리 한도를 초과했습니다!!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;올리브영 QA팀에서는 두 가지 발표를 준비했는데요! 첫 번째 발표의 주제는 올리브영이 옴니 서비스로서 갖고 있는 특성 및 구조와 그 안에서 이뤄지는 QA의 업무에 대한 설명이었습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;1-복잡한-옴니채널의-소프트웨어-품질을-높이기-위한-qa-engineer의-역할feat-qa-팀장님&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%B3%B5%EC%9E%A1%ED%95%9C-%EC%98%B4%EB%8B%88%EC%B1%84%EB%84%90%EC%9D%98-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%ED%92%88%EC%A7%88%EC%9D%84-%EB%86%92%EC%9D%B4%EA%B8%B0-%EC%9C%84%ED%95%9C-qa-engineer%EC%9D%98-%EC%97%AD%ED%95%A0feat-qa-%ED%8C%80%EC%9E%A5%EB%8B%98&quot; aria-label=&quot;1 복잡한 옴니채널의 소프트웨어 품질을 높이기 위한 qa engineer의 역할feat qa 팀장님 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 복잡한 옴니채널의 소프트웨어 품질을 높이기 위한 QA Engineer의 역할(feat. QA 팀장님)&lt;/h4&gt;
&lt;p&gt;저희 팀의 첫 번째 발표에서는 복잡하게 얽힌 옴니 서비스의 기술 및 구조와 함께 올리브영의 QA 업무 중 당면하게 되는 과제들을 소개했습니다.
특히 올리브영 QA팀의 기술 베이스와 전략에 대한 설명은 현재 실무자인 저희 팀원들에게도 다시금 되뇌게 하는 내용이었습니다.&lt;br&gt;
당장 눈 앞에 놓인 업무를 단기적인 목표로 수행하지 않고 우리가 궁극적으로 나아가야하는 방향에 대해서도 상기시킬 수 있었습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/28f6ff1a8c4c9911cbfba707dac7a6d7/d54c6/qa-techblog-ppt1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACOElEQVR42n2RaU8TURSG+wc0IQqiEQpFUKR0oVCg0EVAf6ef/MQf6DIlGhODJn5zSRQQCqH7NrSd7c7j6SAKCfEkb+7cc3Oe980c35R/gumpSaLRMCvLS0QjYYLP5wmHgoQWRcFLRSMRVmIx6S8SCYe8t3A4yGp8mbXVuJwrBBfm8fknHxOY9pNJJclkkuxsb5FKbvIikyaxESeRWmYjFSe9nWBrJ8lmeo1UZp1UKkE6meTVyx021teYfRLwgvkejo8x/mCU+/dGmAlMecmWoiFiS1EWI89YiAWYW5jkacjPXNDPzPwjJkN3CQTHmJ4dZ2ziDqOjIx5jyPLl8zmGKhTy7O0VKWgamiZnUZNegVxWI5stoBU0isN+XiOXy5OVmVw+i1bMe7NXHB/Xynagb1jUmh1anR6W3NsWtMzLd0vBmQ6HTSh1oNYHxc3yKaW4kikTHX1Apdqg0dQxTIdqx6YiVOU4DAyHUt2mVLM5qdqUWwrbFqRy/zJuJHTE78Ia0Oi20M0+tnTA9d7Kjs43pyo3xYFbZ1+d8Mk54TPn/HAb/xJeBxquQ8vocXhWoqy3GQjyqs6dLl/sBrbwvzst3pmnvDdLfHBKfHVrtwOH7rY78GSJlBiYqoGh6vKtGKiKmJ5jUcWgLHbdP/m5HegIpN2pcXR0IP/xVJbSo6H2qTsfxeSCkr3Lz/5rfhlvOHN3qfMW/gd0JZFhdRmYumy7LQamgHTZriSRhAYVeurYU989lqTNa7Oup98C1LrluE75uwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/28f6ff1a8c4c9911cbfba707dac7a6d7/263a4/qa-techblog-ppt1.webp 480w,
/static/28f6ff1a8c4c9911cbfba707dac7a6d7/a6361/qa-techblog-ppt1.webp 960w,
/static/28f6ff1a8c4c9911cbfba707dac7a6d7/0b34d/qa-techblog-ppt1.webp 1920w,
/static/28f6ff1a8c4c9911cbfba707dac7a6d7/1e46e/qa-techblog-ppt1.webp 2652w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/28f6ff1a8c4c9911cbfba707dac7a6d7/9aebd/qa-techblog-ppt1.png 480w,
/static/28f6ff1a8c4c9911cbfba707dac7a6d7/a91f8/qa-techblog-ppt1.png 960w,
/static/28f6ff1a8c4c9911cbfba707dac7a6d7/ac7a9/qa-techblog-ppt1.png 1920w,
/static/28f6ff1a8c4c9911cbfba707dac7a6d7/d54c6/qa-techblog-ppt1.png 2652w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/28f6ff1a8c4c9911cbfba707dac7a6d7/ac7a9/qa-techblog-ppt1.png&quot; alt=&quot;qa techblog ppt1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;[발표 자료] 옴니채널 환경에 대한 소개와 QA 전략 슬라이드&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;&lt;br&gt;
&lt;h4 id=&quot;2-다양한-스쿼드-조직에서의-qa-협업을-말하다--실무자의-진짜-이야기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%8A%A4%EC%BF%BC%EB%93%9C-%EC%A1%B0%EC%A7%81%EC%97%90%EC%84%9C%EC%9D%98-qa-%ED%98%91%EC%97%85%EC%9D%84-%EB%A7%90%ED%95%98%EB%8B%A4--%EC%8B%A4%EB%AC%B4%EC%9E%90%EC%9D%98-%EC%A7%84%EC%A7%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0&quot; aria-label=&quot;2 다양한 스쿼드 조직에서의 qa 협업을 말하다  실무자의 진짜 이야기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 다양한 스쿼드 조직에서의 QA 협업을 말하다 – 실무자의 진짜 이야기&lt;/h4&gt;
&lt;p&gt;여러 기업들의 멋진 발표 후, 저희 팀의 두 번째 발표는 마지막 순서였습니다. 바로 옴니채널의 협업에 대해서도 공유하는 실무자의 인사이트 토크 시간도 있었는데요.
여기서 저희 팀에 대한 소개와 올리브영에서 일하는 방식을 간접적으로 체험할 수 있도록 자세한 설명을 준비했습니다.&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e343bc167b9e949af9cfd0408c894d99/5e85f/qa-techblog-session2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 37.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAACVUlEQVR42h2P20uTARiHvxmk89TU5UznnOf0wswLbRR5Sg1ClKZ5CDKxDMnIpDISKrPUEs/pmo7SlttyLi0PzTmXjmkewzQvosugf+Ppo4sXHl74Pe/7E4xv+jHoXmK1GNDrerjbeIuG2zfp7h1AP2KkrbOPUd1TVq0d2MdamNI/wNx/D6uumbGeJl613qG9uY7ezmZMo0MIbtccLscnlp3zTE5+wGgxYXg3xpxjHZNtmlZLHSbjQ3ame9ia6WF9uhuPrQuH8TmzpgFWt7dZcLtZ9LjZ3N9H+Pndw8HeBlvryzgcs9jmZpmYn8UyMU/3ThWGmQbc5kF+rQzhMj3D/vYx+069yO0sfhxmY8ODc+kLa5urHPz5i9Df8YSD3TX2dr+xvvaVpSU7w/pBxt+PM/S7EL2xj66OBlbMLbxoquZ1WwM/FnS4zG3Yp0b4PGP7X9U0acW5uomgOZVORZmW7KyzlJYUU1N9hZjoKPJycqm9X0RFzQXSM5PJ06Rxo/I8xfkaEtXHqNKew9LdyImEaCqLc6m5lI9CFoCgUkbgK/XhkJeEsEApMepIgoJkSCQCmpMZRIeFkRytJlatIlalIMDPB0EQUIYGUV+aI0r8kflLuVaSS3VRFkKwLJCYCAUymYwIRSiJCXGiMAhVZCTxcbHiPoS8MymEy4/gJx6OD5cTFhLI6bR4CrPTiBKzflIpteUFPKovQ/DxliIPluPrJ0OpVKIQpRIvL5HDiVIp8T7sjSb1OKmqo1zOSuJ6Xgpy8YmUJDXaggxqSrO4qhXnYibl59P5B541ZPpPTlxAAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e343bc167b9e949af9cfd0408c894d99/263a4/qa-techblog-session2.webp 480w,
/static/e343bc167b9e949af9cfd0408c894d99/a6361/qa-techblog-session2.webp 960w,
/static/e343bc167b9e949af9cfd0408c894d99/0b34d/qa-techblog-session2.webp 1920w,
/static/e343bc167b9e949af9cfd0408c894d99/094b8/qa-techblog-session2.webp 2120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e343bc167b9e949af9cfd0408c894d99/9aebd/qa-techblog-session2.png 480w,
/static/e343bc167b9e949af9cfd0408c894d99/a91f8/qa-techblog-session2.png 960w,
/static/e343bc167b9e949af9cfd0408c894d99/ac7a9/qa-techblog-session2.png 1920w,
/static/e343bc167b9e949af9cfd0408c894d99/5e85f/qa-techblog-session2.png 2120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e343bc167b9e949af9cfd0408c894d99/ac7a9/qa-techblog-session2.png&quot; alt=&quot;qa techblog session2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;[발표 현장] (좌) 올리브영의 옴니채널 QA 전략 발표 / (우) 올리브영의 스쿼드 협업에 관한 인사이트 토크&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;마지막 순서였지만, 많은 분들이 끝까지 집중해주어 기쁜 마음으로 발표를 마칠 수 있었습니다. 좋은 반응을 얻어 준비하느라 고생했던 일련의 과정에 보람을 느꼈습니다. &lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;3-우리-발표에-대한-목소리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%9A%B0%EB%A6%AC-%EB%B0%9C%ED%91%9C%EC%97%90-%EB%8C%80%ED%95%9C-%EB%AA%A9%EC%86%8C%EB%A6%AC&quot; aria-label=&quot;3 우리 발표에 대한 목소리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 우리 발표에 대한 목소리&lt;/h4&gt;
&lt;p&gt;발표가 끝난 후 공유받은 따뜻한 후기를 공유합니다! 같은 QA엔지니어로서 느끼는 공감이 올리브영 QA팀에 대한 관심까지 확장 되었다는 점을 확연하게 느꼈습니다.
이번 발표에 쏟은 노력이 결실을 맺는 순간이었습니다. 많은 후기를 받았지만, 그 중 기억에 남는 다섯 가지를 뽑아봤어요!&lt;/p&gt;
&lt;div style=&quot;max-width: 820px; margin: 35px auto; font-family: &apos;Inter&apos;, sans-serif; display: flex; flex-direction: column; gap: 16px;&quot;&gt;
  &lt;div style=&quot;padding: 14px 18px; background: #f7f4ff; border-radius: 10px; border: 1px solid #e3ddff; color: #4a4a4a;&quot;&gt;
    &lt;i&gt;&quot;체계적인 시스템을 구축하여 사용자의 불편함을 빠르게 해소하려는 프로세스가 정말 잘 잡혀있다고 느꼈습니다.&quot;&lt;/i&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: 14px 18px; background: #f7f4ff; border-radius: 10px; border: 1px solid #e3ddff; color: #4a4a4a;&quot;&gt;
    &lt;i&gt;&quot;올리브영의 QA 업무와 커뮤니케이션의 중요성을 배워 의미 있는 시간이었습니다.&quot;&lt;/i&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: 14px 18px; background: #f7f4ff; border-radius: 10px; border: 1px solid #e3ddff; color: #4a4a4a;&quot;&gt;
    &lt;i&gt;&quot;올리브영에서 어떤 인재를 원하는지 궁금한 부분이 있었는데, 오늘 발표를 듣고 제대로 알게 되었습니다.&quot;&lt;/i&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: 14px 18px; background: #f7f4ff; border-radius: 10px; border: 1px solid #e3ddff; color: #4a4a4a;&quot;&gt;
    &lt;i&gt;&quot;기술과 협업의 중심에서 QA가 어떤 역할을 하는지 실감할 수 있었어요.&quot;&lt;/i&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding: 14px 18px; background: #f7f4ff; border-radius: 10px; border: 1px solid #e3ddff; color: #4a4a4a;&quot;&gt;
    &lt;i&gt;&quot;보안, 성능, 인프라까지 QA가 고려해야 할 넓은 범위를 알게 됐고, ‘작은 소통이 큰 결함을 막는다’는 말이 깊이 와닿았습니다.&quot;&lt;/i&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h4 id=&quot;4-막간-세션-듣기-다른-팀의-발표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EB%A7%89%EA%B0%84-%EC%84%B8%EC%85%98-%EB%93%A3%EA%B8%B0-%EB%8B%A4%EB%A5%B8-%ED%8C%80%EC%9D%98-%EB%B0%9C%ED%91%9C&quot; aria-label=&quot;4 막간 세션 듣기 다른 팀의 발표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 막간 세션 듣기! 다른 팀의 발표&lt;/h4&gt;
&lt;p&gt;부스를 운영하느라 모든 발표를 자세히 듣지는 못했지만, 예상보다 더 다양한 QA 사례를 들을 수 있어 좋았습니다.
테스트 자동화와 AI 관련 내용은 QA 업계에서 빠지지 않고 발의 되는 주제인데요, AI 기술을 실무에 적용시키는 건 이제 거의 모든 직무의 공통 분모가 된 것 같습니다.
다른 발표 중 테스트 자동화 전략에 대한 내용이 있었는데요.
모든 것을 무조건 자동화시키기 보다는 ROI(투자 대비 효과)가 높은 영역을 찾아 점진적으로 확대하는 것이 효율적인 전략이라는 점에 깊이 공감했고,
이 것은 올리브영 QA팀에서도 자주 고민하고 논의하고 있는 지점이라고 느꼈습니다. &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-teamwork-log---우리-팀을-알리기-위한-치열한-기록&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-teamwork-log---%EC%9A%B0%EB%A6%AC-%ED%8C%80%EC%9D%84-%EC%95%8C%EB%A6%AC%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%B9%98%EC%97%B4%ED%95%9C-%EA%B8%B0%EB%A1%9D&quot; aria-label=&quot; teamwork log   우리 팀을 알리기 위한 치열한 기록 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍭 Teamwork Log - 우리 팀을 알리기 위한 치열한 기록&lt;/h2&gt;
&lt;p&gt;| 우리_팀워크_실화냐_?_땀방울로_맺어진_준비_과정_그_비하인드.zip &lt;br&gt;&lt;/p&gt;
&lt;p&gt;저희는 컨퍼런스에 더 좋은 모습으로 참여하기 위해 약 한 달전부터 주기적으로 회의를 열었고, 어떤 프로그램을 진행할지 아이디어를 공유했습니다.
테크전략지원팀의 도움을 받아 이전 행사들에 대한 정보를 참고했지만, 제한된 시간 안에 예상했던 것보다 훨씬 더 많은 것들에 대해 논의해야 했어요.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;우리가 논의했던 사항들&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: rgb(208,192,248); color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;&lt;i&gt;No.&lt;/i&gt;&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;&lt;i&gt;Main Topic&lt;/i&gt;&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;1&lt;/i&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;발표 주제는 무엇이 적절하고, 팀 소개 및 우리의 업무는 어디까지 공유할 수 있는가?&lt;/i&gt;&lt;/td&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;2&lt;/i&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;우리 팀이 타사의 QA팀과 차별화 되는 부분을 어필하기 위해 어떤 것을 강조해야 하는가?&lt;/i&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;3&lt;/i&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;제한된 공간과 시간 안에서 진행할 수 있는 QA직무와 관련된 프로그램은 무엇이 있는가?&lt;/i&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;4&lt;/i&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;참여자들이 만족하면서 올리브영에 대해 좋은 기억을 간직할만한 경품은 어떤 것인가?&lt;/i&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;5&lt;/i&gt;&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;&lt;i&gt;커피챗 타임에서 예상되는 질문은 무엇이고, 우리 팀이 추구하는 방향과 일치하는 모범 답변은 무엇인가?&lt;/i&gt;&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;p&gt;뿐만 아니라, 당일에 발생할 수 있는 여러 변수들에 대한 체크도 필요했어요.
경품은 사람들의 관심을 끌면서 동시에 회사의 정체성을 드러낼 수 있어야했고, 향후 지원자가 될 지도 모르는 분들을 위해 어필할 수 있는 포인트를 통찰력있게 그려내야했어요!&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1012px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/89d7f05766d7324704862127414e99a8/9d874/qa-techblog-teamwork.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC4UlEQVR42h2Q3U9TBwDF7x+wxcwlxgejyaprEZxEEBsVES61lRZarG0vtvRSbIvSQiqFiW3pQFpWxQJSISBolQ+dk3RFRYkfWRaNMSYaY9Soccanve1piS5m8Fu383KefifnHMFh05PojbApT8Ho8AlGB2M0ym7U4jW01nPs96YpqbxDiXgXmzdD8uwVUpOP6Tn5HI35FlZXms+fP7L0zxLLy8sIe8sL6Gw2IG4rxGE30z8g0xlqQPL0I5qTqCsvoChaRLX9Hnb/BJPXnUwt2sj8FkGsnaE9tMB/Wlpa/t8Fv1XNcFjCKct4vRaORY18f9xFW1eGKv0MyuJZzGWnOag9Rf7OFFpTkPb4IMG+KGXVCRYWn/H3p794+/p1Li7XMJUI47buwldfRZ+/BnGHAl+LxK83L3HU68dzeBqrKUl5US+qTR4qi4vJKz5BhTl3hTZKcmiKQw12CjZ8w4f37xFmR+LEAxbkSiUhWU11hYLq0vVkh2Qu9tp5crmL8wPtrM33sUNtxrR3FwadEXN9c25yPetUGiJ2E3MxNzfm5xHq9tXwY7yHQMshUv0h/IcdOWgn6R4r8yM+FieCTHUacJj2cLKrlZe/9JBqM1K2RUm+Ko+i7zbT5HIRC3XQ5GlFCDTbuJodZ2wkyexkgkaPA+X6dWjUBWgKlaSDEgn3HoYD1bzNRMik2ug8qGesxYBNLKFw47fsN+6jdLcO2elFWLtmJT+MBUlfzVJvt7B6xddsVimwmHWYqrbR0VTLrelunt4e5VJMImivQLtVQcRWymCrRIPTwwFJxmnU4bDaEL78ahUWl4HsXJRAUzkbVn+BTr2d4bEMP9+YY3R2nqdv5vhp4T7v/pjiz99PEXXpsFnq6IrGefDgIdMzWcJtx9BrRIRmSUcsKnJxqJYGfT51pnKMuhriiQ4kezfhHBTtHuBoeJxHr/p48WGA472NSJIbv6+dibPnOXNmEtl1BEOVnn8BMPrUuKycmhsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/89d7f05766d7324704862127414e99a8/263a4/qa-techblog-teamwork.webp 480w,
/static/89d7f05766d7324704862127414e99a8/a6361/qa-techblog-teamwork.webp 960w,
/static/89d7f05766d7324704862127414e99a8/ca72c/qa-techblog-teamwork.webp 1012w&quot; sizes=&quot;(max-width: 1012px) 100vw, 1012px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/89d7f05766d7324704862127414e99a8/9aebd/qa-techblog-teamwork.png 480w,
/static/89d7f05766d7324704862127414e99a8/a91f8/qa-techblog-teamwork.png 960w,
/static/89d7f05766d7324704862127414e99a8/9d874/qa-techblog-teamwork.png 1012w&quot; sizes=&quot;(max-width: 1012px) 100vw, 1012px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/89d7f05766d7324704862127414e99a8/9d874/qa-techblog-teamwork.png&quot; alt=&quot;qa techblog teamwork&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;딱딱한 바닥 작업도 재미있게 느껴졌던 환상 그 잡채 팀워크&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;사실 이번 포스팅을 통해 가장 강조하고 싶은 것은 팀워크의 효과입니다.
팀워크가 실제 업무 효율성에 얼마나 중대한 영향을 끼치는 지는 많은 분들이, 단 n개월의 경력만으로도 느끼실거라 생각하는데요.
팀워크 수준이 높을수록 조직의 성과와 생산성이 향상 된다는 점은 ‘협업 계수(collaborative coefficient)’라는 개념을 사용한 연구(MDPI)를 통해 증명된 바 있습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 70%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1628px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e5b32b5bcc066c20feb1766daa12a9da/134d7/qa-techblog-cc.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.04166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABx0lEQVR42j2R3U4TURSF+yRcCD6Gia9E0CAafQHxGbxRK8Q7vPBSDaCEEKOWTsX5Oz8zc+av7bRIS6FZ7r3bMsmXs9fea+asnWk1zRguL1FWNfKiJCqBNfe5ruq+zFZaKCsU5MnyQuaT6RTXsxlabLrwI0TKIIgUwkhLrW2KWFnRYWwQays6Eoz0A4L7fhCTR6MZXaI1GDZIs+IuQeYKYVUXZU1p++TJJRnrlPoM65WPN5KEBUXnm22SLVLRjUon0IZJBUMzRZqTio9qpZfv0JwTF/Tx+XyOFu/PL3CCJHVIMidGu+wxNnEwNqM0i+Q8Y69NM0m38o7GtDKvaoyFycjoKkFTvaBC4mrEli7La/LwjH5EOYBxrLkeQqUVBqMr3NxSwnL4D6Ycw6oQyveg/R5M8EdQf727niIi7ydU5wTR7xMkvTMY7xSqewpNqM53TAaO/vKPT/APXiF8+QDd7TWc76zDf7EB//kGvKfr6O7cI30f3pM1BLsPEX94hou3Wwjbj4lH6L3Zgt/eRvxuE82vj5Tw/Av059dQh+9hj/ehj/aQfNuHoVMdLuvjPdijNrLOV7j+WFa+ur7BZHaLCZ1TOnldfv4DoEmELNA4mSYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e5b32b5bcc066c20feb1766daa12a9da/263a4/qa-techblog-cc.webp 480w,
/static/e5b32b5bcc066c20feb1766daa12a9da/a6361/qa-techblog-cc.webp 960w,
/static/e5b32b5bcc066c20feb1766daa12a9da/3a180/qa-techblog-cc.webp 1628w&quot; sizes=&quot;(max-width: 1628px) 100vw, 1628px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e5b32b5bcc066c20feb1766daa12a9da/9aebd/qa-techblog-cc.png 480w,
/static/e5b32b5bcc066c20feb1766daa12a9da/a91f8/qa-techblog-cc.png 960w,
/static/e5b32b5bcc066c20feb1766daa12a9da/134d7/qa-techblog-cc.png 1628w&quot; sizes=&quot;(max-width: 1628px) 100vw, 1628px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e5b32b5bcc066c20feb1766daa12a9da/134d7/qa-techblog-cc.png&quot; alt=&quot;qa techblog cc&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;“The Impact of Teamwork on an Organization’s Performance” 논문 중&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이미 여러 연구를 통해 데이터적으로 입증되었던 사실이지만, 이번 컨퍼런스를 준비하며 팀원들과 협업을 도모하는 과정에서 일이 빠르게 진행되는 것을 체험했습니다.
작은 상품 하나를 준비하는 것 마저도 서로가 잘하는 부분을 찾아주며 기분 좋은 분업을 했고, 이는 마치 누군가가 알맞는 인재를 적재적소에 배치한듯한 느낌이었습니다.
실제로 &apos;우리 팀워크 진짜 좋다!&apos;라는 말을 여러 번 외쳤고, 그 때문인지 컨퍼런스 준비는 체력적으로 힘든 것이 느껴지지 않았을 정도로 즐거웠습니다.
의견을 조율하고 문제를 함께 정의하는 과정에서 흩어져 있던 퍼즐이 자연스럽게 맞춰졌고, 그만큼 우리의 일도 효율적으로 흘러갔습니다.
서로의 강점을 믿고 맡길 수 있었기에 가능한 경험이었고, 이 힘이 앞으로의 업무에서도 강력한 동력이 될 것이라는 확신이 들었습니다. &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7f877fdc5c31e95fbec4b82fed86a66f/9a278/qa-techblog-team.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB+ElEQVR42g2QXU9SAQBA70/pJ9Sma9awUOEiKAlcrqRcL1dAQPGafGggXTS42vxgNnWizkpsNrXpxmxlUWv20ltPjdVLa/0IH0+8n3MejnD17Rfp/AVS5B1yuE5o7IRe2xlqtE5g9C19Wo1kqUrja5PizEsCsY+MTFyiaHUc4gWDkc/0D7/HGdri6eYRwvb+FXZ/idRcllQ2SW47TaGaYXdfoZLS6bAfk5yvcXjyA13Pk4jrZJZneLxrsFwOsfU8iHPoFW1dqzzbOUVIJYKk9AClJxqLhQEWytMUF8Yom2PMjse43VUmY5xTWT/G5gpgGhPk0iOsLUVYMYcpZiUG1CXae3bY2GsgNFdFro+8bGbDWCx9TA36ifgkcvEkuqYS8Hh5s7FC4yDKpNZDUJ3Gdc/B/bYOomoCWQ7R0fLmcmucHx0ifJ+/RdO8wYfFUTwOibuWdixWkZASYjIaR/XLrGfcXH+6yc9jG/1uCbfNhdh5B2unFSM/i1v0kYn42FsvIiS8IntGjINCkprZeliYQpIUxN4HeEM1uj0vWvE8tZxCtaiwsj3OpjlJbFTloexBCc/jDp7idD3CKJgIdms3cr8bp2OAaiGOHlUw5mZbgJdef53XZ/+oX/xGHdKQfQEqixFWcxrqiEZ6OoG9r0Sl+ofLL3/xhhv8B7jiSOtwkYQyAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7f877fdc5c31e95fbec4b82fed86a66f/263a4/qa-techblog-team.webp 480w,
/static/7f877fdc5c31e95fbec4b82fed86a66f/a6361/qa-techblog-team.webp 960w,
/static/7f877fdc5c31e95fbec4b82fed86a66f/0b34d/qa-techblog-team.webp 1920w,
/static/7f877fdc5c31e95fbec4b82fed86a66f/aef83/qa-techblog-team.webp 2771w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7f877fdc5c31e95fbec4b82fed86a66f/9aebd/qa-techblog-team.png 480w,
/static/7f877fdc5c31e95fbec4b82fed86a66f/a91f8/qa-techblog-team.png 960w,
/static/7f877fdc5c31e95fbec4b82fed86a66f/ac7a9/qa-techblog-team.png 1920w,
/static/7f877fdc5c31e95fbec4b82fed86a66f/9a278/qa-techblog-team.png 2771w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7f877fdc5c31e95fbec4b82fed86a66f/ac7a9/qa-techblog-team.png&quot; alt=&quot;qa techblog team&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;사진만 봐도 느껴지는 현장의 밝은 분위기&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-부드러운-coffee에-깊이를-더했던-chat&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%B6%80%EB%93%9C%EB%9F%AC%EC%9A%B4-coffee%EC%97%90-%EA%B9%8A%EC%9D%B4%EB%A5%BC-%EB%8D%94%ED%96%88%EB%8D%98-chat&quot; aria-label=&quot; 부드러운 coffee에 깊이를 더했던 chat permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍭 부드러운 &quot;Coffee&quot;에 깊이를 더했던 &quot;Chat&quot;&lt;/h2&gt;
&lt;p&gt;| QA 업무와 현실적인 진로 이야기 &lt;br&gt;&lt;/p&gt;
&lt;p&gt;마지막 프로그램은 커피챗을 신청해주신 분들과 진솔한 이야기를 나누는 시간이었어요. 커피챗은 단순한 질문과 답변의 시간이 아니라, 서로의 경험을 나누고 상황에 맞는 해법을 함께 찾아가는 시간이었습니다.
그리고 현실적인 진로의 방향성을 같이 고심해보는 시간도 가지게 되어 큰 의미가 있었습니다. 나아가 신청자분들의 고민을 통해 우리 팀원들 또한 진취적인 관점을 얻게 되었습니다.&lt;br&gt;&lt;br&gt;
커피챗에서 만났던 여러 지원자들 중 특히 인상 깊었던 분이 있습니다.
Front-End 개발자로 입사했지만, 스타트업에서 여러 역할을 맡게 되었고 ‘QA 프로세스도 직접 만들어보는 게 어떻겠냐’는 제안을 받은 상황이었습니다.
이를 계기로 QA 직무에 본격적으로 관심을 갖게 되었고, 정해진 프로세스 없이 업무를 수행하는 상황에서 ‘어떻게 QA 프로세스를 구축하면 좋을까?’ 하는 고민이 많았다고 합니다.
이에 그 분의 상황에 맞춰 실제로 QA 프로세스를 어떻게 시작하고 개선할 수 있는지, 또 커리어를 확장하는 과정에서 어떤 접근을 할 수 있는지를 공유했습니다.
단순한 서비스 검증을 넘어 프로세스를 정립하고 품질 문화를 만들어 가는 어려움과 보람에 대해 이야기를 나누며 깊이 공감할 수 있었습니다.&lt;br&gt;&lt;br&gt;
커피챗을 진행하며 올리브영 QA팀에서는 보다 다양한 업무 기회가 있다는 점을 알리고, 온오프라인 서비스를 동시에 경험할 수 있다는 장점도 전달했습니다.
비록 긴 시간은 아니었지만, QA라는 직무의 가치를 커피챗을 통해 서로 공유하고 대화를 나눴던 분들의 커리어에도 작은 도움이 되었길 바라고, 올리브영 QA팀에 대한 관심이 더욱 커졌길 바랍니다.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-컨퍼런스-참여-후기--우리의-이야기를-나누다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%BB%A8%ED%8D%BC%EB%9F%B0%EC%8A%A4-%EC%B0%B8%EC%97%AC-%ED%9B%84%EA%B8%B0--%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%9D%B4%EC%95%BC%EA%B8%B0%EB%A5%BC-%EB%82%98%EB%88%84%EB%8B%A4&quot; aria-label=&quot; 컨퍼런스 참여 후기  우리의 이야기를 나누다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍭 컨퍼런스 참여 후기 : 우리의 이야기를 나누다&lt;/h2&gt;
&lt;p&gt;| 이 경험은 앞으로 우리에게 어떤 의미가 되고, 팀에는 어떤 영향을 가져올까? &lt;br&gt;&lt;/p&gt;
&lt;p&gt;부스 운영자로 참여함으로써 올리브영이라는 브랜드의 긍정적인 이미지와 저희 QA팀에 대한 인지도를 모두 얻었다고 생각합니다.
이번 활동이 팀과 조직에 대한 홍보 효과뿐만 아니라, 고객 관점에서 서비스에 대한 경험 확대에도 영향을 미쳤다고 느꼈어요.
컨퍼런스에서의 경험은 앞서 언급한 대로, 단순한 참여 또는 개인적인 성장을 뛰어넘어 팀 전체가 한 단계 성장하는 계기가 되었습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ae4a6325e20ab628e20d47b0af6a6bd4/8b6bd/qa-techblog-team2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.20833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEO0lEQVR42h2U2VPTBxDH89TO9KnMdNpqrVWHw6NeyFSllkEtg9aTQlGuCEhBDpmqyCVHQFBIQEICAuEISThiIEBiCPdhECEiFQ9EBcUSheJoO/4Fn/7sw87uPux3Z7+7+xVVKQrR1ZZRV1HEyJCVPksLPSY9g9Y2BjqN/8fmFh3SvGykl7No0lZToyqnsVHN3bFbDHTfxDbQha3fyqhQL7qYEEnmxQTys5KokF9Bq5Kjq1bQ0ViNWV9Di6YMvVZFq76J/OxLKEtLKC0pRl1Xya3BHkxGvQBqYbivk9HhXkQ114upURZgqJfToJKhqSqi/rqMNl05LWolDZVFlFyVYDK1k5+TwdU8CfLiQtTV5XSajOh19bTdaMKob0BbW4Ho5cI7Xjnes7T0jtdv3jL3cpEFxyIOh4MXs/MsLLyhp8tKWFAgQf7HEZ8MID4mgqz0C8iuZFNckEuVQoZKoC4rORHRVp8S9hxT4Beh4reoKoLPqPD2L2Ovn5J9/kq8jioprNUwOmJhqO+jmQW+TNhtncKorYz0tzM+bOZWr5GOGxpE7b0PaOmcxHBznAbjCM0dY2gMt9Eax1ELXt0wRI6+jd8lXcRkWYnPsZBe1M1lRTfSygHSpFZSrpj5I7udOsMAomcLy8w5lnmx+AHH8gdev/2Xhbf/ML/0nskn8ywuLVNlu028pJnsYhPhiaWkXdZQrOomMVWJRGogV9lDpsyKtKkd0eTjWaZmXtGuFTZXUcy9yfuM2Gw8nv2LFsUZbIM2uuafck0h51xsJGGBfsSEB6O4VkBsVLhwHan0W1tprK1k5OkEIq3OQnJGAXF+O0gNP8TVjAxCDnii1hg4cWQ/oSdDabJ24nssiM+dVrNqrSuu329jw6btuLht5pNPPxP8JjasXU96fhaiX8IuEh0ZiPtuXxJCD5Ofug9Pzx+oyIlmv7cP3rv3kHU+mb3e+1mz5Tu8/D1w2fkFHhu2s37jFr5auQLXVetw+mIN4pBgRHEXTiA+dZCz4lOkxARxNvE4Z8S/UhT1M+fEgeTFnSbvbCTum1342vkbNv90AN8jx/E7dBgnpy9xdnXG5+A63JzXExvgiyg3IYLMdDnJMXF4eOwiNlhMyukw3Fy34HPgEDkpiUjOReG+yRk3l2/Z6OlNYUkZ7boa0gQujwWFEZ8QQnlSFJkJAYhsXUbmnk/j5fUjK1auJjTAj6G6UkIP+rBzlzchEWLS0hMJ8vMlWmh0IkxMXZ2KsQEL9/rNSPJllMvLMFwvolCWIwAO9/NwaoJLKeeRFebivm0bSZGn2LHVhYjoozQLomEfsnDXfhv72AhWSxtDvWbuCH/b22USchOGZh03BNH4e+4hokdTY0zfH+VOfwfWVg1GQRTqlVLk+UkM9lVj721l+u4gs7MzzDx5wPSjP5l+YGdmapxnDyeE2jvMPZ7g+Ucc+zD/AVfaFxs4glt8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ae4a6325e20ab628e20d47b0af6a6bd4/263a4/qa-techblog-team2.webp 480w,
/static/ae4a6325e20ab628e20d47b0af6a6bd4/a6361/qa-techblog-team2.webp 960w,
/static/ae4a6325e20ab628e20d47b0af6a6bd4/0b34d/qa-techblog-team2.webp 1920w,
/static/ae4a6325e20ab628e20d47b0af6a6bd4/57a8e/qa-techblog-team2.webp 2354w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ae4a6325e20ab628e20d47b0af6a6bd4/9aebd/qa-techblog-team2.png 480w,
/static/ae4a6325e20ab628e20d47b0af6a6bd4/a91f8/qa-techblog-team2.png 960w,
/static/ae4a6325e20ab628e20d47b0af6a6bd4/ac7a9/qa-techblog-team2.png 1920w,
/static/ae4a6325e20ab628e20d47b0af6a6bd4/8b6bd/qa-techblog-team2.png 2354w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ae4a6325e20ab628e20d47b0af6a6bd4/ac7a9/qa-techblog-team2.png&quot; alt=&quot;qa techblog team2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;컨퍼런스를 마치며 팀원들과 함께! ^.^&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;제한된 조건 속에서도 우리가 목표를 완수할 수 있었던 이유는 팀워크였습니다.
누군가 무거운 부분을 짊어지면 다른 누군가가 자연스럽게 빈 자리를 채웠고, 그 경험은 업무 효율을 넘어 ‘함께 일하는 이유’를 다시 일깨워주었습니다.
QA로서 확신하건대, 팀워크는 품질을 향상시키는 힘입니다.
우리가 지향하는 품질 기준을 더 높이기 위해 앞으로도 밀도 높은 팀워크와 유기적인 협업을 그려나가봅니다!☺️&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;-우리의-참여-소회-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%B0%B8%EC%97%AC-%EC%86%8C%ED%9A%8C-&quot; aria-label=&quot; 우리의 참여 소회  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💌 우리의 참여 소회 💌&lt;/h4&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;&quot;&gt;
  &lt;div style=&quot;background:rgb(238,222,251); padding:20px; border-radius:15px; box-shadow:0 7px 7px rgba(0,0,0,0.1);&quot;&gt;
    &lt;p&gt;&lt;i&gt;이번 기회에 &apos;진짜 옴니채널의 협업 이야기&apos;를 주제로 발표하며, 현장에서의 우리의 고민과 노력을 공유할 수 있어 뜻깊었습니다.&lt;/i&gt;&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#f8f0d2; padding:20px; border-radius:15px; box-shadow:0 7px 7px rgba(0,0,0,0.1);&quot;&gt;
    &lt;p&gt;&lt;i&gt;10분이라는 시간의 제약 때문에 우리가 매일 마주하는 스쿼드 기반의 품질 확보와 협력사와의 상생 등 더 구체적인 이야기들을 모두 담지 못해 아쉬움이 남습니다.&lt;/i&gt;&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#e9f3fd; padding:20px; border-radius:15px; box-shadow:0 7px 7px rgba(0,0,0,0.1);&quot;&gt;
    &lt;p&gt;&lt;i&gt;올리브영의 복잡한 옴니채널 환경은 QA를 넘어 모두의 유기적인 협력이 필요하다는 점과 이는 우리 팀의 가장 큰 강점이자 올리브영의 성장을 이끄는 동력이라고 다시 느꼈어요.&lt;/i&gt;&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#f8d9d0; padding:20px; border-radius:15px; box-shadow:0 7px 7px rgba(0,0,0,0.1);&quot;&gt;
    &lt;p&gt;&lt;i&gt;앞으로 더 많은 내부 소통 채널을 통해 우리의 &apos;진짜&apos; 협업 사례들을 공유하고 싶어요.&lt;/i&gt;&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#dff8df; padding:20px; border-radius:15px; box-shadow:0 7px 7px rgba(0,0,0,0.1);&quot;&gt;
    &lt;p&gt;&lt;i&gt;컨퍼런스를 준비하며 체력적으로는 힘들었지만, 업무 외적으로도 더 가까워진 계기가 되었습니다.&lt;/i&gt;&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#fbd9ea; padding:20px; border-radius:15px; box-shadow:0 7px 7px rgba(0,0,0,0.1);&quot;&gt;
    &lt;p&gt;&lt;i&gt;서로 다른 스쿼드를 맡고 있지만, 협업할 기회가 많은 업무 특성상 팀에도 시너지를 낼 것 같아요. 좋아서 웃었고, 힘들어서 성장했습니다.&lt;/i&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h4 id=&quot;-올리브영-qa팀에-전하는-방문객들의-후기-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-qa%ED%8C%80%EC%97%90-%EC%A0%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%AC%B8%EA%B0%9D%EB%93%A4%EC%9D%98-%ED%9B%84%EA%B8%B0-&quot; aria-label=&quot; 올리브영 qa팀에 전하는 방문객들의 후기  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💌 올리브영 QA팀에 전하는 방문객들의 후기 💌&lt;/h4&gt;
&lt;p&gt;끝으로, 이 모든 과정을 뿌듯함으로 마무리할 수 있게 해줬던 부스 참여자 분들의 후기를 나눠봅니다.
한 문장 한 문장이 컨퍼런스 준비부터 마지막까지 모든 일련의 과정을 떠올리게 했고 성취감을 느끼게 해줬습니다.
이 소중함을 그대로 간직한 채로, 오늘 터득한 팀워크를 내일의 효율성에 보태는 팀이 되겠습니다. &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1857px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bee03219177bb27c5228ff6d0b660147/530b1/qa-techblog-prize.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAENElEQVR42hWPi0+TBxTFv79gL93cK0qWzTmnc3EuE3UEUxwKOHUgDhUFEXBFUigiyhARqriyTiuWUa0IlVqgtVIFlRZREByKFLCU56DykDeb4LZA3PLb501+Ocm5uSfnCpnqBu7UuRkaecaLFzPAjKjTzMy+5DkTkyO0PH6Ew9GA293L8+fTvJyamgcEfR/HWws2M8cjFI+lMXgsiUZYH1rGnGVW/MPNyI7aOW9oobnVLQaNMTs7JZ7OivzLzMw0A4NuXG1OOrs7eTb1J1PTzyi9VsHW7Ym8/1Ewb8zfgbBfasBrdQHeYVq+DCzk0PFGdJdbyClo4fa9HoqtTrqetDM4MszA6CiDE3/QPzpGb/8g3e5B0Z+g/+kQ1jIboXuOICxeYWKfb7aIliBfPcFRpYQn2NgSXUpA2E0+8dGgMVTQ83SU3uFJ+sfGMZhL0RvN9PQP8bitS6ST7t4norYjeO9JRxKkIm77ccIDcljzXQGrtxXiv09EeoY9mTHoy8ro7BtjeOov0jNyCPRdi+eKRVTcrsY9IIa6OmhqdlFb/wjB3O7BpeZPOXvPk7N3vfhBGcznK0NY4nmUr4Py2RR/AKPdTId7lL7JvynSZjFct4neGz7cMupwi61b2zpwtLj47aEDodDxLpb2eZR2zcXqfpvrJj9O7vRDsnwp67wu8IGnEq2xkr6RSTqH/2N8oIN/yr+gMnkhJvUJ2nt6cHX+TpOzjfqGJgTJ1hS8dhwjWJ5KjCIWh/4QtxJDiAySIY8+w4a9P5GUVYS1qlFscYG7ddUUG43kpURgM2RQYc3F2Vwt7py0icHCws90eAYYiIxVsTlQhjY2GOWBBGRxStKOnSVRVUtcmpVsbTr2a2uJilzPFUsRA60WrEVZtDXfYKCjHMeDKgzFFoQFX2kIDE/F19+PDz9ezJYNEqqz5VTkZlCdp8FmLqGqtg3t+UxM5jMkHIhkqLcSu0WF476VyaEGRtz3uHJJzeKlKxDiU06x/6ACVXYOsfEyynRp4rsSlLK92LQxPKq10eN0kRkfTYHmOFeVCdy5eBhT/gkmRl0M991netxFYa6CV1+fg7AtYhtpiiy6nKU0lsRhtRaxaeMG8tMjqGnNpb3PRU9jE/cVURhTIzAm7yAvOYoqw2n664oYfVLP095GTLqTvDl3HkJE+EbUp9IoSvShat0rFEZKyJBuJl8t55zlV/L05ShVeuRJSpKSUzi2aw2pcXspM2qwnJRxU5dJvd2A/WoOq5b7IEhDQpCHStgXMB/5qkVIw3az1tubxCNZaAo1KLU/EyFPQbLeH+lOfw5LA1EkxzI80EDjw5tcv6zGolNwq0RFeKAfQpL8W3aHrOS9d14jaOcujv5yELXuR7T60xRfLeHcxWKyVCrK7ScwlR7E32sZRr2a8REn3a4aOlvv0PG4khrbJWRh3/A//KNGqQwcUhwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bee03219177bb27c5228ff6d0b660147/263a4/qa-techblog-prize.webp 480w,
/static/bee03219177bb27c5228ff6d0b660147/a6361/qa-techblog-prize.webp 960w,
/static/bee03219177bb27c5228ff6d0b660147/43d24/qa-techblog-prize.webp 1857w&quot; sizes=&quot;(max-width: 1857px) 100vw, 1857px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bee03219177bb27c5228ff6d0b660147/9aebd/qa-techblog-prize.png 480w,
/static/bee03219177bb27c5228ff6d0b660147/a91f8/qa-techblog-prize.png 960w,
/static/bee03219177bb27c5228ff6d0b660147/530b1/qa-techblog-prize.png 1857w&quot; sizes=&quot;(max-width: 1857px) 100vw, 1857px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bee03219177bb27c5228ff6d0b660147/530b1/qa-techblog-prize.png&quot; alt=&quot;qa techblog prize&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;1등 상품 당첨 기념 사진&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;div style=&quot; margin: 20px auto; font-family: &apos;Inter&apos;, Arial, sans-serif; display: flex; flex-direction: column; gap: 25px;&quot;&gt;
  &lt;div style=&quot;padding-left: 14px; border-left: 3px solid #8FB4FF;&quot;&gt;
    &lt;p style=&quot;margin: 0; color: #333; font-size: 15.5px; line-height: 1.2;&quot;&gt;
      👩🏻‍🔬 : &lt;i&gt;“QA 업무 중에 발생하는   문제를 예시를 통해 쉽게 이해할 수 있었던 것 같아요.”&lt;/i&gt;
    &lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding-left: 14px; border-left: 3px solid #8FB4FF;&quot;&gt;
    &lt;p style=&quot;margin: 0; color: #333; font-size: 15.5px; line-height: 1.2;&quot;&gt;
      🧙🏻‍♀️ : &lt;i&gt;“현장에서 QA 사례를 직접 체험할 수 있어 실무 인사이트가 풍부하다고 느꼈습니다.”&lt;/i&gt;
    &lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding-left: 14px; border-left: 3px solid #8FB4FF;&quot;&gt;
    &lt;p style=&quot;margin: 0; color: #333; font-size: 15.5px; line-height: 1.2;&quot;&gt;
      👨🏼‍🌾 : &lt;i&gt;“프로그램도 직관적이고 설명도 친절해서 품질 관리에 대한 실무자 분들의 진정성을 느낄 수 있었어요.”&lt;/i&gt;
    &lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding-left: 14px; border-left: 3px solid #8FB4FF;&quot;&gt;
    &lt;p style=&quot;margin: 0; color: #333; font-size: 15.5px; line-height: 1.2;&quot;&gt;
      👨‍🚀 : &lt;i&gt;“퀴즈의 내용이 알차다고 느꼈고, 너무 재미있었는데 실무자로서 유익하기까지 했습니다!”&lt;/i&gt;
    &lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding-left: 14px; border-left: 3px solid #8FB4FF;&quot;&gt;
    &lt;p style=&quot;margin: 0; color: #333; font-size: 15.5px; line-height: 1.2;&quot;&gt;
      👩🏻‍🏫 : &lt;i&gt;“참여형 부스라 재미있었고, 퀴즈를 풀던 중 긴장하고 집중도 할 수 있어서 좋은 추억이 될 것 같아요. 자연스럽게 올리브영 QA팀에도 관심이 생겼습니다.”&lt;/i&gt;
    &lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;padding-left: 14px; border-left: 3px solid #8FB4FF;&quot;&gt;
    &lt;p style=&quot;margin: 0; color: #333; font-size: 15.5px; line-height: 1.2;&quot;&gt;
      🧑‍🎄 : &lt;i&gt;“올리브영이 너무 잘 준비해주셔서 컨퍼런스가 잘 진행된 것 같습니다.&quot; from. QA 컨퍼런스 운영진&lt;/i&gt;
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;&quot;이번 경험을 발판 삼아, ‘모두가 함께 만들어가는 진짜 품질’을 향해 나아갑시다. 고생한 모든 팀원들에게 감사합니다.&quot; - QA Team Leader&lt;/b&gt;&lt;/i&gt;&lt;br&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올영세일 선착순 쿠폰, 미발급 0%를 향한 여정]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-12-15/fcfs-coupon/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-15/fcfs-coupon/</guid><pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영 쿠폰 시스템 백엔드 개발자 &apos;라이트&apos;입니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;올리브영에서 행사와 쿠폰을 빼면 고객들의 관심도가 떨어질 만큼 쿠폰은 아주 중요한 요소 중 하나입니다.
특히 선착순 쿠폰은 올영세일 같은 대규모 행사 때마다 핵심 이벤트로 빛을 발하기 때문에, 오픈 시점에 스파이크성의 동시다발적인 대량 트래픽이 발생합니다.
올리브영에서는 여러 차례에 걸쳐 쿠폰 관련 포스트를 게시했습니다. 그 중에서도 &lt;a href=&quot;https://oliveyoung.tech/2023-09-18/oliveyoung-coupon-rabbit/&quot;&gt;쿠폰 발급 RabbitMQ 도입기&lt;/a&gt;는 대량 트래픽에도 안정적인 발급 시스템을 구축한 이야기를 다루고 있죠.&lt;br&gt;
이렇게 지속적인 개선으로 시스템 자체의 안정화에 도달했지만, 여전히 비동기 시스템의 단점이 남아 있었습니다.
이런 상황에 쿠폰 미발급 문제를 직면하니 제대로 해결해보자는 강한 의지가 생겼습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;문제-상황---올영세일에서-마주한-과제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9---%EC%98%AC%EC%98%81%EC%84%B8%EC%9D%BC%EC%97%90%EC%84%9C-%EB%A7%88%EC%A3%BC%ED%95%9C-%EA%B3%BC%EC%A0%9C&quot; aria-label=&quot;문제 상황   올영세일에서 마주한 과제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 상황 - 올영세일에서 마주한 과제&lt;/h2&gt;
&lt;p&gt;2025년 6월 올영세일, 7일간의 대규모 선착순 쿠폰 행사를 운영하면서 지속적으로 모니터링한 결과 예상치 못한 문제를 발견했습니다.
세일 기간에는 온라인몰에서 동시다발적으로 쿠폰 발급 요청이 발생합니다. 모든 요청의 제한 수량 유효성 검사를 통과하면 Message Queue로 메시지가 발행되고, 발급 워커가 유효성 검사를 또 한 번 진행한 뒤 쿠폰을 최종 발급합니다.
그런데 6월 올영세일에서 발급 워커의 2차 유효성 검사가 실패해, 쿠폰이 발급되지 않은 문제가 발생했습니다.
이로 인해 사용자에게는 발급 성공으로 응답했지만, 실제 워커에서는 발급에 실패했습니다. 이는 곧바로 CS 문의로 이어지는 치명적인 문제였습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2025년 6월 올영세일 선착순 쿠폰 현황&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;📅 &lt;strong&gt;행사 기간&lt;/strong&gt;: 7일간 (05.31 ~ 06.06)&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;평균 미발급률&lt;/strong&gt;: &lt;strong&gt;약 0.014% (10,000건당 약 1.4건 수준)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;일차별 미발급률
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1일차 ███     0.021%
2일차 ██      0.008%
3일차 ██████  0.037% 
4일차 ██      0.013%
5일차 ██      0.013%
6일차 █       0.004%
7일차 ████    0.025%&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;미발급률이 평균 0.014%라는 수치는 통계적으로는 낮아 보일 수 있습니다. 하지만 이는 사용자에게 발급 성공 응답을 보냈음에도 실제 처리가 실패하여 고객 경험의 불일치를 야기하는 심각한 문제였습니다.
특히, 선착순 쿠폰을 기다린 고객에게 브랜드 신뢰도 하락과 CS 처리 비용 증가로 이어지는 치명적인 비즈니스 리스크로 인식되었습니다.
이 미발급 문제를 해결하기 위해 다방면으로 고민하고 개선한 과정을 지금부터 자세히 소개해 드리겠습니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;비동기-시스템의-단점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EB%8B%A8%EC%A0%90&quot; aria-label=&quot;비동기 시스템의 단점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;비동기 시스템의 단점&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;비동기 처리는 요청처에 빠르게 응답하여 트래픽 지연을 해소할 수 있는 장점이 있지만, 실제 데이터 처리 결과는 비동기 로직 수행이 완료된 후 알 수 있기 때문에 데이터 일관성을 보장하기 어렵습니다.
성능과 안정성이 중요한 선착순 쿠폰 시스템에서 대량의 트래픽을 감당하기에 MQ 기반 비동기 시스템이 탁월한 방안이었지만, 여전히 개선이 필요한 점들이 남아있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot; style=&quot;width: 80%;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7a884e49bfe56adb0252ee64c504053e/49142/issueCoupon_seqDiagram_ASIS.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 101.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAC4jAAAuIwF4pT92AAACl0lEQVR42p1U2XLaQBD0/39bquIjBizQDbrvlQTYQKdnJUGwwVXOg0qj0W5vz3TvPMyTBk+bAo/rHGa1h9MBdnuC25302+LjMXY6iY86r2P1TzzmZd2D258QtzukaquTQdUhrBTBjtgwThlbzQfWzQ5Z3cJm7Dd7nXfGOGtaxu/cDzzYPGldNPDTAkaxhZfV8LMSjjDLGwRZAUcdCLhHXNawGXv1DmHONUoAd0jK5gI40V6WO8ziBkZCdtxg99AsV/U7ljxI/ptk5DAnoBIPe48w+X0puRv65fKHvwNPZ+lJSaYsjwxWBHv0EsyiGibBpXyL4FZS4by3PZzjgSETNinrBhcdnCCF44R4CUu8ZQr+lgf1w7rp8NsxAWXxIqrw7ApABTNt+FSwRGWWYo2luSzVUTdArvIE9Ngro+gh9pnFVI5CrDaJZuvznzNa4j6rTwwFUPy3Yl+0OFGGhRfAEBGYt9XhZ4BT0qz3eMt72OyhR/8tsg6GfLNkOXTo9fFrmZ9Lnhaaoz1svkUgrTiFkB7OaSWpwOahg0D3WnFlm6FnV0zOwnCDuIB2+s1r+rLOYObt94ADyPG2mjxoQ8ZuzPs+N/EW11jSDZe9dwFvNFwNgBYZGRRrGeR4pT+fydSdhsVPAPXd1V4t8Wr7mPEGLcstDd+e9/wX4JI+XVguxdlq8cS3Z0B1R5Sv4IMtpGS7ogOCBH94mybVZfRNB18BWnp6HPS0EEVX9eA9GaISD5PkCCMq8EpBnqjyLzvCisY3qf4i314AhU1Y94jrDjIbQ74jGbBKBmyPtJ4G7DhIO85GtdczUPzq6Xx3mYfCKqxaRKXS1AO+w6LWjNaFQjwO0kDJxOYaNUzvlMPWbz+wIXjOSe6pd+3jv3a+7ELuk3HaAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7a884e49bfe56adb0252ee64c504053e/263a4/issueCoupon_seqDiagram_ASIS.webp 480w,
/static/7a884e49bfe56adb0252ee64c504053e/a6361/issueCoupon_seqDiagram_ASIS.webp 960w,
/static/7a884e49bfe56adb0252ee64c504053e/0b34d/issueCoupon_seqDiagram_ASIS.webp 1920w,
/static/7a884e49bfe56adb0252ee64c504053e/da28f/issueCoupon_seqDiagram_ASIS.webp 2880w,
/static/7a884e49bfe56adb0252ee64c504053e/42ca8/issueCoupon_seqDiagram_ASIS.webp 3000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7a884e49bfe56adb0252ee64c504053e/9aebd/issueCoupon_seqDiagram_ASIS.png 480w,
/static/7a884e49bfe56adb0252ee64c504053e/a91f8/issueCoupon_seqDiagram_ASIS.png 960w,
/static/7a884e49bfe56adb0252ee64c504053e/ac7a9/issueCoupon_seqDiagram_ASIS.png 1920w,
/static/7a884e49bfe56adb0252ee64c504053e/f9c26/issueCoupon_seqDiagram_ASIS.png 2880w,
/static/7a884e49bfe56adb0252ee64c504053e/49142/issueCoupon_seqDiagram_ASIS.png 3000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7a884e49bfe56adb0252ee64c504053e/ac7a9/issueCoupon_seqDiagram_ASIS.png&quot; alt=&quot;선착순 쿠폰 발급 로직 (기존)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;선착순 쿠폰 발급 로직 (기존)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;기존의 쿠폰 발급 로직을 보면, 발급 수량 유효성 검사에 대한 빠른 처리를 위해 데이터 저장소를 DB 외에 인메모리 데이터 저장소인 Redis를 하나 더 두어 수량을 관리하고 있습니다.
매번 DB를 조회하는 비용이 크기 때문에, 수량 관리 용도로 Redis에 쿠폰 유효 기간만큼의 TTL(Time To Live)을 설정한 쿠폰별/유저별 키를 세팅하고, 쿠폰 발급 요청 시 Redis를 통해 수량을 조회하여 유효성 검사를 수행합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;다만, 앞서 말씀드린 바와 같이 실제 데이터 처리는 워커에서 비동기로 수행하기 때문에 1차 유효성 검사 시점과 실제 DB에 반영되어 수량을 처리하는 시점 사이에 필연적으로 시간 간극(Time Gap)이 존재할 수밖에 없는 구조였습니다.
특히, 밀리초(ms) 단위로 다량의 발급 요청이 동시에 들어온다면, 1차 유효성 검사를 통과한 n개의 요청이 모두 발급 성공 응답을 했지만, 일부 요청은 2차 유효성 검사에서 실패하게 되는 문제가 발생했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;문제의-근본-원인&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C%EC%9D%98-%EA%B7%BC%EB%B3%B8-%EC%9B%90%EC%9D%B8&quot; aria-label=&quot;문제의 근본 원인 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제의 근본 원인&lt;/h2&gt;
&lt;p&gt;이 문제를 더 깊이 분석해보니 세 가지 핵심 원인이 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-time-gap-시간-간극&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-time-gap-%EC%8B%9C%EA%B0%84-%EA%B0%84%EA%B7%B9&quot; aria-label=&quot;1 time gap 시간 간극 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. Time Gap (시간 간극)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;T0: Redis GET (count=99) → 통과 ✅
T1: MQ 발행 (10ms)
T2: Worker 수신 (50ms)
T3: Redis GET (count=101) → 실패 ❌  &amp;lt;- T0와 T3 사이의 간극!&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 번째 검증(T0)과 워커의 재검증(T3) 사이에는 MQ 지연 시간을 포함한 시간 간극이 발생합니다. 이처럼 짧은 순간(예: 100ms)에 수백 건의 요청이 동시에 몰리면서 경합 상태(Race Condition)가 발생하여, T0 검증이 무의미해지는 결과를 낳았습니다.&lt;/p&gt;
&lt;h3 id=&quot;2-double-validation-overhead-이중-검증-오버헤드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-double-validation-overhead-%EC%9D%B4%EC%A4%91-%EA%B2%80%EC%A6%9D-%EC%98%A4%EB%B2%84%ED%97%A4%EB%93%9C&quot; aria-label=&quot;2 double validation overhead 이중 검증 오버헤드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. Double Validation Overhead (이중 검증 오버헤드)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[Online Mall]        [Worker]
유효성 검사 1차  ---→  유효성 검사 2차
    (통과)            (실패 가능)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;비동기 시스템의 신뢰성을 위해 워커에서 한 번 더 검증하는 것은 필수였지만, 이 시간 간극으로 인해 앞단의 1차 검증 결과가 무의미해지는 상황이 발생했습니다.&lt;/p&gt;
&lt;h3 id=&quot;3-redis-getincr-원자성atomicity-부재&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-redis-getincr-%EC%9B%90%EC%9E%90%EC%84%B1atomicity-%EB%B6%80%EC%9E%AC&quot; aria-label=&quot;3 redis getincr 원자성atomicity 부재 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. Redis GET/INCR 원자성(Atomicity) 부재&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 문제가 되는 코드 패턴&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; currentCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;opsForValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// GET&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentCount &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; maxCount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    redisTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;opsForValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// INCR (별도 명령)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ⚠️ GET과 INCR 사이에 다른 요청이 끼어들 수 있음! (이 짧은 간극 사이에 경합 발생)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Redis의 개별 명령(&apos;GET&apos;, &apos;INCR&apos;)은 원자적으로 동작하지만, 이들을 조합한 &apos;GET&apos;과 &apos;INCR&apos; 로직이 분리되어 수행되면서 문제가 발생했습니다.
즉, 두 명령 사이에 다른 요청이 개입할 수 있게 되어 결과적으로 원자성이 깨지고 수량 오버플로우를 유발했습니다.
이를 해결하기 위해 일부 로직을 변경하며 겪은 시행착오는 다음과 같습니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;개선-방안&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0-%EB%B0%A9%EC%95%88&quot; aria-label=&quot;개선 방안 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선 방안&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1️⃣-시도-1-발급-수량-선차감-시간-간극-줄이기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F%E2%83%A3-%EC%8B%9C%EB%8F%84-1-%EB%B0%9C%EA%B8%89-%EC%88%98%EB%9F%89-%EC%84%A0%EC%B0%A8%EA%B0%90-%EC%8B%9C%EA%B0%84-%EA%B0%84%EA%B7%B9-%EC%A4%84%EC%9D%B4%EA%B8%B0&quot; aria-label=&quot;1️⃣ 시도 1 발급 수량 선차감 시간 간극 줄이기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1️⃣ 시도 1: 발급 수량 선차감 (시간 간극 줄이기)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;문제점: 기존에는 워커가 수량을 증가(INCR) 처리했기 때문에, 1차 검증(GET)과 실제 수량 반영(INCR) 사이에 MQ 전송 시간까지 포함된 긴 시간 간극이 발생했습니다.&lt;/li&gt;
&lt;li&gt;개선 방안: 이 시간 간극을 줄이기 위해 &apos;Redis 발급 수량 증가&apos; 처리를 워커가 아닌, 온라인몰 앞단에서 선차감하도록 로직을 변경했습니다.&lt;/li&gt;
&lt;li&gt;실패 처리: 당연히 발급 실패 시 처리도 고려했습니다. 워커에서 실제 DB 처리 중 이슈가 발생하면 선차감했던 수량을 다시 원상복구(Rollback) 하도록 구현했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot; style=&quot;width: 80%;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/147a9408706757120aac7b89d6d9b28b/49142/issueCoupon_seqDiagram_TOBE.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 114.58333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAAC4jAAAuIwF4pT92AAAC00lEQVR42p1VWXviMAzs//9pfdm2WyhXyEFCE5z7INyzIwUC7G77tX3wZ0WxxyNpLD88Lwx+LWI8Wku8rUq4LeCsAbc5cT5d7eZqu3/ZdnO1HzxuCustomYLl3ZQrLHMG9j1ET7tsKhhl3ssKq4p6K8O8ModwryGU+3hVTtEZQOHPgWUBcskh29STLMN5wz+KlHAhcnxvjIE2SMoNzBp1oEXLSITK4hHW/xOsSEg8HAJZ5q2GLznmJgSdr7pQqwPsIpdP+YEc3iQkBBb9/J7Xu2vIV+crjDaMBdZAzetlaH4Z2T9ywkxCHPMCSps5/kWc1OcQboD7gDF6dAprOyk4uIco2SN30GCkanOTA4abs+w2PYM7wA9VnUUZhj4K7yZGjOePHlPYHGzMLIkT+t7Jrcg/wKy/MLidZnihcMlw1kYK1tPJXSEd5GOpOYrgFLdKUO0ygMmAZk6PhyCeQzLjmstQJe/LzC8cxLckaKQ5ZgArh3AG1l4jQoMWRRLDqilwkwFCyNk/gsoQxiMyMZJCZjVvDUEJWvVo7BlHr/F8Kq1Tm8iIQERiQzJcJI0ZNXqWlVE/Qng5YeGINW8WSyb5XshvlWGJ977V8rJisvPAI93FXRuwhEmUvGg5Tpey6GkgqqYrgrd97GwPwDUZsAcWrySIzdgbmuV2DOZXvb8CHAc5RjYCwyiUq/jLFv3efwR4IQhjm1XwaRok+QK6HwXUPQpHcgKIpXSG6v+5BlVhXabYvcTwBZDhjyOG2X3IpWmpOS/dW4UVx1KHupT3+rt6qjdW+y52h3DAa/k48zHix9rtfsn4ExGAR0CLaq9DmEks7R48Xu0/aoLS2y5knoBshZuvtYe6srectvZ3RNwRJhkCONU2SxN0rV9eVP4LJhVpKHJW5OlKds+D2HbT4xh29/qc5Bn7FK3T4Ar7fzMSma1xd+zlY4uj9O2t3Vd7991vuaEP8BGzkaR75qgAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/147a9408706757120aac7b89d6d9b28b/263a4/issueCoupon_seqDiagram_TOBE.webp 480w,
/static/147a9408706757120aac7b89d6d9b28b/a6361/issueCoupon_seqDiagram_TOBE.webp 960w,
/static/147a9408706757120aac7b89d6d9b28b/0b34d/issueCoupon_seqDiagram_TOBE.webp 1920w,
/static/147a9408706757120aac7b89d6d9b28b/da28f/issueCoupon_seqDiagram_TOBE.webp 2880w,
/static/147a9408706757120aac7b89d6d9b28b/42ca8/issueCoupon_seqDiagram_TOBE.webp 3000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/147a9408706757120aac7b89d6d9b28b/9aebd/issueCoupon_seqDiagram_TOBE.png 480w,
/static/147a9408706757120aac7b89d6d9b28b/a91f8/issueCoupon_seqDiagram_TOBE.png 960w,
/static/147a9408706757120aac7b89d6d9b28b/ac7a9/issueCoupon_seqDiagram_TOBE.png 1920w,
/static/147a9408706757120aac7b89d6d9b28b/f9c26/issueCoupon_seqDiagram_TOBE.png 2880w,
/static/147a9408706757120aac7b89d6d9b28b/49142/issueCoupon_seqDiagram_TOBE.png 3000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/147a9408706757120aac7b89d6d9b28b/ac7a9/issueCoupon_seqDiagram_TOBE.png&quot; alt=&quot;선착순 쿠폰 발급 로직 (개선)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;선착순 쿠폰 발급 로직 (개선)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;ul&gt;
&lt;li&gt;결과 및 한계: 이처럼 수량 선차감 로직을 적용한 후 부하 테스트를 진행한 결과, 미발급 건수가 줄기는 했지만 여전히 잔존하는 문제를 발견했습니다. 이는 Time Gap을 줄여도 원자성이 보장되지 않았기 때문입니다. Redis GET과 INCR이 여전히 분리되어 수행되는 한, 경합 상태는 원천적으로 차단될 수 없었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;2️⃣-시도-2-lua-스크립트-적용-원자성-확보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EF%B8%8F%E2%83%A3-%EC%8B%9C%EB%8F%84-2-lua-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A0%81%EC%9A%A9-%EC%9B%90%EC%9E%90%EC%84%B1-%ED%99%95%EB%B3%B4&quot; aria-label=&quot;2️⃣ 시도 2 lua 스크립트 적용 원자성 확보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2️⃣ 시도 2: Lua 스크립트 적용 (원자성 확보)&lt;/h2&gt;
&lt;p&gt;첫 번째 시도에서 확인했듯이, 근본적인 문제는 Time Gap이 아니라 원자성 부재였습니다. 이 문제를 완전히 해결하려면 Redis의 GET/INCR 명령 수행 간 원자성이 보장되어야 합니다. 그래야 모든 요청이 순차 처리되어 정확한 유효성 검사가 가능해집니다.
이러한 원자성 보장을 위해 저희가 적용을 검토한 방법이 바로 &lt;strong&gt;Lua 스크립트&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;-여기서-lua-스크립트란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%97%AC%EA%B8%B0%EC%84%9C-lua-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%9E%80&quot; aria-label=&quot; 여기서 lua 스크립트란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💡 여기서 Lua 스크립트란?&lt;/h3&gt;
&lt;p&gt;루아(Lua) 스크립트는 가볍고 빠른 스크립트 프로그래밍 언어로, 다른 시스템(예: 게임 엔진, 서버, 데이터베이스 등)에 내장해서 동작을 확장하거나 자동화할 때 자주 사용됩니다.
Redis는 서버 내부에서 &lt;strong&gt;Lua 스크립트&lt;/strong&gt;를 실행할 수 있게 지원하며, 여러 명령을 &lt;strong&gt;원자적(atomic)&lt;/strong&gt; 으로 실행하거나, 복잡한 로직을 네트워크 왕복 없이 한 번에 처리할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;lua&quot;&gt;&lt;pre class=&quot;language-lua&quot;&gt;&lt;code class=&quot;language-lua&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;GET&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; KEYS&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; current &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;local&lt;/span&gt; num &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tonumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; num &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; num &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tonumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ARGV&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;INCR&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; KEYS&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
    redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;SET&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; KEYS&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    redis&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;EXPIREAT&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; KEYS&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tonumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ARGV&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;참고 문서) &lt;a href=&quot;https://redis.io/docs/latest/develop/programmability/&quot;&gt;Redis Programmability - Scripting with Lua&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
위의 예시처럼 작성된 스크립트를 하나의 트랜잭션처럼 묶어서 처리하므로 원자성을 보장하지만, Redis는 단일 스레드 구조이기 때문에 스크립트를 수행하는 동안 다른 요청은 모두 대기하게 됩니다. 따라서, 스크립트가 길고 복잡해질수록 성능은 낮아질 수밖에 없습니다.
실제로 스크립트를 적용하여 부하 테스트를 수행한 결과, 미발급/과발급 건은 모두 0건으로 해소되었지만, &lt;strong&gt;기존 대비 약 21% 정도의 성능 저하가 발생&lt;/strong&gt;하였습니다.
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 주의사항: Lua 스크립트와 성능&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;고성능을 요구하는 시스템에서는 Lua 스크립트의 수행 시간을 최소화하고, 반드시 필요한 로직만 원자적으로 처리하도록 구현해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
처음부터 원자성을 고려한 스크립트 적용으로 설계가 되었으면 모르겠으나, 이미 선착순 쿠폰 발급에 대한 우수한 성능을 자랑하던 시스템에 성능 저하가 되는 로직을 넣기에는 무리였습니다.
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;3️⃣-시도-3-발급-요청-수량-체크-용도의-별도-redis-key-추가-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EF%B8%8F%E2%83%A3-%EC%8B%9C%EB%8F%84-3-%EB%B0%9C%EA%B8%89-%EC%9A%94%EC%B2%AD-%EC%88%98%EB%9F%89-%EC%B2%B4%ED%81%AC-%EC%9A%A9%EB%8F%84%EC%9D%98-%EB%B3%84%EB%8F%84-redis-key-%EC%B6%94%EA%B0%80-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;3️⃣ 시도 3 발급 요청 수량 체크 용도의 별도 redis key 추가 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3️⃣ 시도 3: 발급 요청 수량 체크 용도의 별도 Redis Key 추가 관리&lt;/h2&gt;
&lt;h3 id=&quot;이중-카운터-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EC%A4%91-%EC%B9%B4%EC%9A%B4%ED%84%B0-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;이중 카운터 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;이중 카운터 전략&lt;/h3&gt;
&lt;p&gt;원자성 보장과 성능 유지라는 두 마리 토끼를 잡기 위해 고안한 방법은 바로 이중 카운터(Double Counter) 전략입니다.
이 전략은 실제 쿠폰 발급 제한 수량을 관리하는 키 외에, 발급 요청 수량 관리를 위한 별도의 Redis 키를 추가하여 유효성 검사를 이중화하는 방식입니다.&lt;/p&gt;
&lt;br&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;Redis Key&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;용도&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;C001-count&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;최종 쿠폰 발급 제한 수량 제어 (DB 정합성에 사용)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;C001-countReq&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;쿠폰 발급 요청 수량 제어 (경합 상태 방지)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;기존 방식의 문제&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot; style=&quot;width: 50%;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1429px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/95ce722d3137d6e9c6f374be04c109f6/eb228/Redis_countUp_ASIS.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 37.291666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAC4jAAAuIwF4pT92AAABOElEQVR42p1RW26EMAzk/hfoTfrTI/QC1Yrl/UyABAiPQNip4+3uARrJsTSxx+NJgH+dB9+XMzjzEC4MgW1jLCirBuE9RlW3MMuKlR6EEJBSQmtNdRuO48C+72jbFnVdYxgGmHnGcZ5YlcQ2aViqWZYFQSMk4iJD20mM0wxFJFVVIYru3Kz1CGMMkzdNQ3iMLMvQ9wNmqp/GCZpipOi6HoGtD6ivEdO3YcnucrDWEvmE67rgnHtnH4NSOEkZY+dB2Mn46wRbvKP6EOg+FXa7Y11XVpTnORP7NdI05dUtrR1FERSt7EmLoiBVHdvxsibw7NNtxt7bp8LT4fH4M51UeFKlNOeXEv/um/U4YqbhnvytME4y/IQ3pDRN0jTvoQ/vnW/wKrxnRVk+ccKE7Fh1kiSc+QMJb4XALyInGQHnWgwsAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/95ce722d3137d6e9c6f374be04c109f6/263a4/Redis_countUp_ASIS.webp 480w,
/static/95ce722d3137d6e9c6f374be04c109f6/a6361/Redis_countUp_ASIS.webp 960w,
/static/95ce722d3137d6e9c6f374be04c109f6/0143d/Redis_countUp_ASIS.webp 1429w&quot; sizes=&quot;(max-width: 1429px) 100vw, 1429px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/95ce722d3137d6e9c6f374be04c109f6/9aebd/Redis_countUp_ASIS.png 480w,
/static/95ce722d3137d6e9c6f374be04c109f6/a91f8/Redis_countUp_ASIS.png 960w,
/static/95ce722d3137d6e9c6f374be04c109f6/eb228/Redis_countUp_ASIS.png 1429w&quot; sizes=&quot;(max-width: 1429px) 100vw, 1429px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/95ce722d3137d6e9c6f374be04c109f6/eb228/Redis_countUp_ASIS.png&quot; alt=&quot;Redis 수량 차감 흐름도 (기존)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;Redis 수량 차감 흐름도 (기존)&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;개선된 방식&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot; style=&quot;width: 60%;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1916px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d1cabe32144928356aed64ce0601a86b/db5ee/Redis_countUp_TOBE.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.541666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAC4jAAAuIwF4pT92AAABEklEQVR42m1Qi26EMAzj/z9hPzbdpAMx2EFpoS1XoPSBl0a6aZqWKnIcRXaa6roulPwdf/krhBsw7ZLrdKV/Z6rms4UxFtYYdF2Hvu+xriu2bYdSCm3bomkaCDHBOAtpZohR4FbfMAwDnNt4tmTRqdRtwbkFhBhI6AlrLWKM7Oa9Z3FDZtu+IaaIlDN2qpWaCXeklJCpVzLGhEq8KXjhkegZbVgwhAApJQ8XrpREvjK01liJ55TRPzo8vWHzSHORFsqElb0/SSDyNv7wjMdxYJ4XxhcvWL6kF/pR4bTleR5Ifkd0hs3LItX7/QPzon/uUHIYR77dNE0kdHLv6/HgXl3XkJNkA0snyptD1AMC3desDt8vps4hroOIlQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d1cabe32144928356aed64ce0601a86b/263a4/Redis_countUp_TOBE.webp 480w,
/static/d1cabe32144928356aed64ce0601a86b/a6361/Redis_countUp_TOBE.webp 960w,
/static/d1cabe32144928356aed64ce0601a86b/a9c0c/Redis_countUp_TOBE.webp 1916w&quot; sizes=&quot;(max-width: 1916px) 100vw, 1916px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d1cabe32144928356aed64ce0601a86b/9aebd/Redis_countUp_TOBE.png 480w,
/static/d1cabe32144928356aed64ce0601a86b/a91f8/Redis_countUp_TOBE.png 960w,
/static/d1cabe32144928356aed64ce0601a86b/db5ee/Redis_countUp_TOBE.png 1916w&quot; sizes=&quot;(max-width: 1916px) 100vw, 1916px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d1cabe32144928356aed64ce0601a86b/db5ee/Redis_countUp_TOBE.png&quot; alt=&quot;Redis 수량 차감 흐름도 (개선)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;Redis 수량 차감 흐름도 (개선)&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;왜 이 방법이 효과적인가?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 요청 카운터가 먼저 증가하므로 동시 요청 차단&lt;/li&gt;
&lt;li&gt;✅ 실제 카운터는 통과된 요청만 증가 → 정확성 보장&lt;/li&gt;
&lt;li&gt;✅ Lua 스크립트 없이 2개 명령으로 해결 → 성능 영향 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;참고 문서) &lt;a href=&quot;https://redis.io/docs/latest/commands/incr//#pattern-counter&quot;&gt;Redis Docs - Pattern:Counter&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;결론적으로, 실제 수량이 초과될 상황이 원천 차단되었으며, 워커로 넘어가는 초과 발급 건수는 모두 0건으로 완벽히 해소되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;최종-의사결정-1번--3번-조합-선택&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%A2%85-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-1%EB%B2%88--3%EB%B2%88-%EC%A1%B0%ED%95%A9-%EC%84%A0%ED%83%9D&quot; aria-label=&quot;최종 의사결정 1번  3번 조합 선택 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;최종 의사결정: 1번 + 3번 조합 선택&lt;/h2&gt;
&lt;p&gt;다양한 시도 끝에 &lt;strong&gt;발급 수량 선차감(1번)과 이중 카운터 전략(3번)을 조합한 방식을 최종 선택&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;h4 id=&quot;의사결정-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;의사결정 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;의사결정 과정&lt;/h4&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;방안&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;정확성&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;성능&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;선택 여부&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;1️⃣ 수량 선차감&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;△ (개선됨)&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;✅ 채택&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;2️⃣ Lua 스크립트&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐⭐⭐ (21%↓)&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;❌ 기각&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;3️⃣ 이중 카운터&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐⭐⭐⭐ (8%↓)&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;✅ 채택&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;기각 이유 (Lua 스크립트):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;성능 저하&lt;/strong&gt;: 21%의 처리량 감소는 대규모 트래픽 환경에서 치명적&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Redis 부하&lt;/strong&gt;: 단일 스레드 특성상 전체 시스템 병목 가능성&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;운영/관리 복잡도 증가&lt;/strong&gt;: 스크립트 디버깅과 모니터링의 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;채택 이유 (이중 카운터):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;허용 가능한 성능 영향&lt;/strong&gt;: 8%의 성능 저하가 발생했지만, 운영 환경 스펙 및 실제 요청량 대비 TPS 및 분당 처리량을 계산했을 때 충분히 감당 가능한 수준이라고 판단 (정확성 100% 확보와 동시에 성능 트레이드오프 최소화)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;개선-결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0-%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;개선 결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선 결과&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-성능-지표-비교-및-성과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%84%B1%EB%8A%A5-%EC%A7%80%ED%91%9C-%EB%B9%84%EA%B5%90-%EB%B0%8F-%EC%84%B1%EA%B3%BC&quot; aria-label=&quot; 성능 지표 비교 및 성과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📊 성능 지표 비교 및 성과&lt;/h2&gt;
&lt;p&gt;개발 환경에서 수행한 부하테스트 결과입니다. 실제 운영 환경보다 높은 부하 조건에서 측정한 수치이므로 미발급률에 다소 차이가 있을 수 있으나, 이를 감안해도 개선 효과는 매우 분명하게 나타났습니다.&lt;/p&gt;
&lt;h3 id=&quot;부하-테스트-결과-쿠폰-제한-수량-10000건&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC-%EC%BF%A0%ED%8F%B0-%EC%A0%9C%ED%95%9C-%EC%88%98%EB%9F%89-10000%EA%B1%B4&quot; aria-label=&quot;부하 테스트 결과 쿠폰 제한 수량 10000건 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;부하 테스트 결과 (쿠폰 제한 수량 10,000건)&lt;/h3&gt;
&lt;p&gt;선착순 쿠폰 제한 수량 10,000건을 기준으로 비교한 결과, &lt;strong&gt;발급 정확도는 100%로 향상&lt;/strong&gt;되었고, &lt;strong&gt;미발급 건수는 0건&lt;/strong&gt;으로 완전히 감소했습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;구분&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;개선 전&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;개선 후&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;개선율&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;발급 성공 응답&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;10,162건&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;10,000건&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐️&lt;strong&gt;100% 정확&lt;/strong&gt;⭐️&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;미발급 건수&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;162건&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;0건&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐️&lt;strong&gt;100% 감소&lt;/strong&gt;⭐️&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;미발급률&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;1.62%&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;0%&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;⭐️&lt;strong&gt;완전 해소&lt;/strong&gt;⭐️&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;실제 운영에서도 동일한 결과가 확인되었습니다.
정확한 비교를 위해 9월 올영 세일 3일차까지는 위에서 개선한 기능 플래그를 OFF로 해두고, 4일차부터 ON하여 미발급 건의 차이가 있는지 확인해보았습니다.
그 결과 3일차까지 기존처럼 미발급 건이 발생하였고, 4일차부터는 &lt;strong&gt;미발급 건수는 0건&lt;/strong&gt;, 그리고 &lt;strong&gt;분당/초당 최대 처리량&lt;/strong&gt; 역시 6월 대비 &lt;strong&gt;저하 없이 안정적&lt;/strong&gt;으로 유지되었습니다! 🥳🎉👏🏻👏🏻
(8% 성능 저하는 실전에서 영향이 미미했습니다.)
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;개선 방안을 실무에 적용 시 고려했던 사항들은 아래와 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-redis-키-관리-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-redis-%ED%82%A4-%EA%B4%80%EB%A6%AC-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;1 redis 키 관리 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. Redis 키 관리 전략&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;키 네이밍 명확히 구분 및 적절한 TTL 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 쿠폰별 키 네이밍 규칙&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; countKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;coupon:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;couponId&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:count&quot;&lt;/span&gt;&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;// 실제 발급 수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; countReqKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;coupon:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;couponId&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:countReq&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 발급 요청 수&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// TTL 설정 (쿠폰 종료 시각 + 버퍼)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; ttl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; couponEndTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;plusHours&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;2-모니터링-포인트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%ED%8F%AC%EC%9D%B8%ED%8A%B8&quot; aria-label=&quot;2 모니터링 포인트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 모니터링 포인트&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;정합성 체크&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;count&lt;/code&gt; vs &lt;code class=&quot;language-text&quot;&gt;countReq&lt;/code&gt; 간의 지속적인 차이 모니터링 (경합 상태 완화 후에도 발생 가능한 비정상적 흐름 감지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;알람 설정&lt;/strong&gt;: 두 키의 카운트 차이가 임계치를 벗어날 경우 즉시 알람 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis 자원 추적&lt;/strong&gt;: Redis 메모리 사용량 및 명령 처리 지연(Latency) 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-장애-대응-및-안정화-시나리오&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91-%EB%B0%8F-%EC%95%88%EC%A0%95%ED%99%94-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4&quot; aria-label=&quot;3 장애 대응 및 안정화 시나리오 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 장애 대응 및 안정화 시나리오&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fallback 전략&lt;/strong&gt;: Redis 장애 발생 시 DB 기반 Fallback 로직을 통한 서비스 연속성 확보 필요 (단, 성능 저하 위험 인지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정합성 보정&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;count&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;countReq&lt;/code&gt; 수량 불일치 감지 시 자동 또는 수동 정합성 체크 및 수량 조정 프로세스 마련&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;이 프로젝트를 통해 배운 것은 크게 세 가지입니다.&lt;br&gt;
&lt;strong&gt;첫째, 완벽한 시스템은 없다, 그러나 개선은 계속된다.&lt;/strong&gt;
어떠한 예외 상황에서도 완벽한 시스템을 만들어내는 것은 거의 불가능에 가까운 일 아닌가 싶습니다. 99.99%의 고가용성 시스템이라도 단 0.01% 가 실패하면 그건 완벽한 시스템은 아니니까요. 그래서 문제가 발생하면 빠르게 캐치하고, 더 나은 해결책을 찾아 적용하는 과정이 무엇보다 중요하다는걸 느꼈습니다.&lt;br&gt;
&lt;strong&gt;둘째, 성능과 정확성의 균형이 필요하다.&lt;/strong&gt;
Lua 스크립트가 기술적으로 가장 완벽한 해결책처럼 보였지만, 실제 프로덕션 환경에서는 21%의 성능 저하가 더 큰 문제였습니다. 반면 이중 카운터 전략을 통해 &lt;strong&gt;시스템의 응답 속도를 유지하면서도 데이터 정합성을 100% 보장하는 실질적인 균형점&lt;/strong&gt;을 찾아냈습니다.&lt;br&gt;
&lt;strong&gt;셋째, 데이터 정합성과 응답 속도는 트레이드오프다.&lt;/strong&gt;
비동기 시스템은 빠른 응답 속도를 얻는 대신 데이터 정합성 보장이 어렵다는 근본적인 트레이드오프를 가지고 있습니다. 다행히 이번 개선에서는 응답 속도를 유지하면서도 정합성을 보장하는 방법을 찾았습니다.
&lt;br&gt;
&lt;br&gt;
저의 해결책 역시 올리브영 쿠폰 시스템 환경에 최적화된 결과일 뿐, 완벽하다고 말할 수는 없습니다. 하지만 각 시스템마다 최우선시해야 하는 요소가 무엇인지에 따라 해결책은 다양하게 적용될 수 있습니다.
&lt;strong&gt;올리브영 쿠폰 시스템에서는 고객 신뢰가 가장 중요한 가치&lt;/strong&gt;였기에, 저희는 정확성을 최우선으로 두면서도 성능을 포기하지 않는 합리적인 방법을 찾아냈습니다. 이 과정에서 얻은 경험과 노하우가 비슷한 고민을 하는 분들께 조금이나마 도움이 되길 바랍니다.
&lt;br&gt;
저의 첫 포스팅을 읽어 주셔서 감사합니다. 다음에는 더 좋은 내용으로 찾아뵙겠습니다. 🙏&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Kafka Streams 기반 EDA 구축 사례: 올리브영 품절 시스템 현대화 프로젝트]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-12-15/kafka-streams-for-out-of-stock/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-15/kafka-streams-for-out-of-stock/</guid><pubDate>Mon, 15 Dec 2025 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.
올리브영의 상품 운영 프로세스 개발 업무를 담당하고 있는 벙개맨⚡️ 입니다.&lt;br&gt;
제가 소속된 상품 탐색 스쿼드는 보다 안정적이고 확장성 있는 상품 정보 관리를 위해 상품 통합 프로젝트를 진행하고 있습니다.&lt;br&gt;
오늘은 위 프로젝트의 한 부분인 상품의 품절 여부 관리 프로세스를 어떻게 개선했는지, 그리고 개선 과정에서 실시간 데이터 처리를 위해 활용한 Kafka Streams에 대하여 소개해 드리겠습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 450px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2862e3ae532d390cddda5f26f3e7ebde/42a19/image1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEk0lEQVR42hWSaVMaBwCG91f1UzuZzjS9MtM0TWI0Y2xMk8bEMSY6SSSo4EUUUCMEBDlFQO5zF4HlWoFdQM7dZTnCDZEVY5r0F9T+gOd555l5ARdR5dnDP07wfnnKH11TboIRTTS14QzjH3uFXtdBUEyjT+ePclTg1SnB8LxSYI1GiBbRHniTJUAWzb8D45Pr2nGW4tGmTuiLqaMJnhvBP3Yz3c4lvGCG4URs58A5tqKH03Wqe0G2BzGqU+pdAKIoPqM6+unZu9sr0jGehu+K6ND0JUz2e1S/h1QrLKMfyhX4eu/fW8ajajlR6RHNQbZOI3gLUMSLI+v67yf5C5Bz5sDGc4XVseSaM+ymquFqbceLPhQ6FHBq15PgBPwCMijPxYOVmiNWmuQ7ALTUEZiOr88IxrZV97gHHLPflkQvFSBVVWH5WV1gQuySeFK6BKXLFVQEJioj3NTRhMB6f8kMiEDUHChw3dknm4Y/GIoXIovJH1wywtFGK1Jr2PKULkPuowXpccGFV9r5TKpObKGhZ0LnwzUroPWFTH74pSLkRBIy17EkmJ2UuCbeO9RJ3F2oWFKUvVyzUzUL+QFt04PBaatdOOk3+Ibo+IoZaPTPnIHj7/5cneTrTDEySNS2XOE36sBeKLcH56TenCKQA0utwqfzCt0nTv8p0jRary4q4PurZqDQOvflmpZI4dGGfnT1kGdLQAiexZtI7WOC7if6/Ui1h5FdpE5Hm11Pox2sdtRBnKkNzZsiANU5J1pnl4r3dmxsWcPRhw0x6qQ3SH9uquLgPgZlvnQS7bP9EC6Ds4Y0BdYajkpNncKnxQ6AaNKGED4r8vzFta8dRtXBvCyYx07P43T9kIhqceSYbkTbtBzOqgN5BZzjGMKzUvfIgvrKAx7wRuIdXTaOv7VN7YBsTVjhzQYLpQLdpy6+ZL5epL98znw9LbbKXrS0bo0/3jBfmxbfXdDcfC3/4fEWMMIyPBeAG7aINJLkWTAXWjyp9UJU05qpOfJ1N96C8Jo7XQphlNwa4QahYbZslK29zVD+vyxwxk5O275WVZPJ6WI4RDR3odymBZP5spcJYvDkpRgaWzOyJZDKE5dW48/2LEMM1fVZya9TQgDrNpv/0nCFNOGEDMlvwxmWAVnQhJe1yNJBcPyt6cac8iZD+fNz0QhLw1B47i1p7i6qmRJQ60sDYBFvn3fIzofDOL5oRDgQxvEk5vbhiU37E4FzmKUbmtfceqO69Vo6zdU/5egYAsvBURIttsgWDWDNEvahsmPHWKYI14tu+1Mr5tCdJcWKCWRqPCPL2qEF9eVtb77a+31G9Nu0cOiVVOlJhvJ1V4wEeCaEeRBYcSNCJCWPZD3papxqvXciQQI1oaHhJeW1GfEDzuGG/pghga4+eXdlfGPbFjFj5JwUBKbkENsRFgVSVrSYoDpUe1Dpfip3P+GNti8bHV7c/fYB95XYKLCHp0Xuu0z5rRcCmRcLV5vzCghgHsKqQDqUq5PNM7J1lip3Cw3ahVKLKphjtN1gCL+5t8be063rnTfY+7df796ZEai8KFKuz0mc/wFG0jKTkCsBZgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2862e3ae532d390cddda5f26f3e7ebde/263a4/image1.webp 480w,
/static/2862e3ae532d390cddda5f26f3e7ebde/a6361/image1.webp 960w,
/static/2862e3ae532d390cddda5f26f3e7ebde/d71bc/image1.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2862e3ae532d390cddda5f26f3e7ebde/9aebd/image1.png 480w,
/static/2862e3ae532d390cddda5f26f3e7ebde/a91f8/image1.png 960w,
/static/2862e3ae532d390cddda5f26f3e7ebde/42a19/image1.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2862e3ae532d390cddda5f26f3e7ebde/42a19/image1.png&quot; alt=&quot;oliveYoung Let&apos;s go&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;이 이미지는 Google Gemini로 생성되었습니다.&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;️-멈춰버린-데이터-고속도로-기존-프로세스의-문제점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EB%A9%88%EC%B6%B0%EB%B2%84%EB%A6%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B3%A0%EC%86%8D%EB%8F%84%EB%A1%9C-%EA%B8%B0%EC%A1%B4-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90&quot; aria-label=&quot;️ 멈춰버린 데이터 고속도로 기존 프로세스의 문제점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⛔️ 멈춰버린 데이터 고속도로: 기존 프로세스의 문제점&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;올리브영 온라인몰은 고객에게 정확한 상품의 품절 여부(Sold Out Status) 정보를 제공하기 위해, 이 데이터를 장바구니, 검색 결과, 상품 상세 페이지 등 다양한 영역에서 핵심적으로 활용하고 있습니다.
하지만 개선 이전에는 품절 여부를 조회하기 위해 Oracle 함수를 직접 호출하는 구조를 사용하고 있었고, 내부적으로 복잡한 쿼리를 수행해야 했기 때문에 응답 속도와 처리 효율이 떨어졌습니다.
또한 별도의 캐시 처리 없이 모든 트래픽이 Oracle 함수를 직접 호출했습니다. 이로 인해 데이터베이스에 지속적인 부하가 발생했습니다.
특히 올영세일과 같은 대규모 트래픽 상황에서는 이 부하가 심화되어 품절 정보 조회 지연은 물론, 온라인몰 전체 서비스 품질에도 영향을 주는 문제가 발생했습니다.
이러한 한계를 해소하고 보다 안정적이며 확장 가능한 서비스를 제공하기 위해 저희는 &lt;strong&gt;품절 여부 관리 프로세스 개선을 진행&lt;/strong&gt;하게 되었습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 900px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1068px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f2e2c6dfb37f4d11670afd4741a2ef13/adc5a/image2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 47.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABy0lEQVR42lVSTWsUQRCd/2uO6iHgUfAgikiMEA9BBL15UkjEyLqGNVEJGhQ36+6GnZ3NzkfPzM7013w+qxsdx4Fiqql+r169akcrBRaECMIISigUdO5/nHNEUWSDMYaqqrpaTbkfBDj98hnn598gpYITRCF+XF5g5o7xc/IJmyTuAAZclqXN26ZBQ6G17uol1UWe4dX923h06wak0nA+XszxcniIk9MD3Ht8DevF5D9CpQukdWODVw1UbwJNzVrJcfzkIQb7exA8h3M2dvH15AMW30cYDI4QzqeoSElRFESmoWhkvXAhZjPo9ZpUKKvaNDN3SoqV50EKYZs5vyZTvD16h/fDId4cHGI5myLNMjDjWZ4jG48RbW/jamsLye4u0jSF7/vWT5MbcvXHBiklnITF8Lw1Lpcu/JWLJEm7kQoi1gQkOdCktKW/AfU95BHD87t38OLBDvnJ4ZglSMGRyxwbkSDjGwtsaYTCKKCRTN6YICV9wrptkSxX2L95Ha+fPoPIiDCOmVWWei4UkYvcAJUlqYigeyYENpvuL8UoVHTvbDTCYj6HMiMbH+IwwIZFyNIEkot/CuraGm+ei8n/njtC8s8uiOp261rjNz2GpPqbShFUAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f2e2c6dfb37f4d11670afd4741a2ef13/263a4/image2.webp 480w,
/static/f2e2c6dfb37f4d11670afd4741a2ef13/a6361/image2.webp 960w,
/static/f2e2c6dfb37f4d11670afd4741a2ef13/a22c7/image2.webp 1068w&quot; sizes=&quot;(max-width: 1068px) 100vw, 1068px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f2e2c6dfb37f4d11670afd4741a2ef13/9aebd/image2.png 480w,
/static/f2e2c6dfb37f4d11670afd4741a2ef13/a91f8/image2.png 960w,
/static/f2e2c6dfb37f4d11670afd4741a2ef13/adc5a/image2.png 1068w&quot; sizes=&quot;(max-width: 1068px) 100vw, 1068px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f2e2c6dfb37f4d11670afd4741a2ef13/adc5a/image2.png&quot; alt=&quot;as-is soldOut process&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;AS-IS 상품 품절 조회 process&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-새로운-환승-시스템을-설계하다-해결책-모색&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%99%98%EC%8A%B9-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EC%84%A4%EA%B3%84%ED%95%98%EB%8B%A4-%ED%95%B4%EA%B2%B0%EC%B1%85-%EB%AA%A8%EC%83%89&quot; aria-label=&quot; 새로운 환승 시스템을 설계하다 해결책 모색 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚉 새로운 환승 시스템을 설계하다: 해결책 모색&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;저희는 기존 DB 내부 로직(Oracle 함수 등)에 의존하던 일방향식 처리 구조를 벗어나, OGG(Oracle GoldenGate), AWS MSK(Managed Streaming for Apache Kafka), Kafka Streams를 활용한 Event-Driven Architecture(EDA) 기반의 새로운 데이터 환승 시스템을 설계하였습니다.
EDA를 도입한 주요 목적은 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;db-단일-의존성-제거와-안정성-확보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#db-%EB%8B%A8%EC%9D%BC-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A0%9C%EA%B1%B0%EC%99%80-%EC%95%88%EC%A0%95%EC%84%B1-%ED%99%95%EB%B3%B4&quot; aria-label=&quot;db 단일 의존성 제거와 안정성 확보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;DB 단일 의존성 제거와 안정성 확보&lt;/h3&gt;
&lt;p&gt;기존에는 Oracle DB에 집중된 레거시 구조로 인해, DB 부하나 장애가 발생하면 전체 서비스가 영향을 받는 단일 장애 지점(Single Point of Failure) 문제가 있었습니다.
이에 저희는 DB 의존성을 줄이고, 서비스 단위로 독립적인 확장과 안정적인 장애 대응이 가능한 구조로 전환하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;실시간-데이터-처리와-낮은-결합도-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC%EC%99%80-%EB%82%AE%EC%9D%80-%EA%B2%B0%ED%95%A9%EB%8F%84-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;실시간 데이터 처리와 낮은 결합도 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실시간 데이터 처리와 낮은 결합도 구현&lt;/h3&gt;
&lt;p&gt;과거에는 배치 중심의 데이터 전달 방식이라 지연(latency) 이 발생하고, 서비스 간 결합도가 높아 변경이 어려웠습니다.
이를 이벤트 기반 구조로 전환하면서, 데이터 변경 시 즉시 이벤트를 발행하고 필요한 서비스가 이를 구독하여 실시간으로 처리할 수 있게 되었습니다.
결과적으로 서비스 간 결합도가 낮아지고, 변경 대응이 유연해졌습니다.&lt;/p&gt;
&lt;h3 id=&quot;손쉬운-확장성과-유연한-비즈니스-대응&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%90%EC%89%AC%EC%9A%B4-%ED%99%95%EC%9E%A5%EC%84%B1%EA%B3%BC-%EC%9C%A0%EC%97%B0%ED%95%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%8C%80%EC%9D%91&quot; aria-label=&quot;손쉬운 확장성과 유연한 비즈니스 대응 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;손쉬운 확장성과 유연한 비즈니스 대응&lt;/h3&gt;
&lt;p&gt;Kafka 기반의 이벤트 파이프라인은 새로운 서비스가 필요할 때 단순히 이벤트를 구독하는 것만으로 손쉽게 확장할 수 있습니다.
또한 Kafka Streams를 활용해 이벤트를 실시간으로 가공, 조합함으로써, 기존 DB 중심 처리 방식으로는 구현하기 어려웠던 다양한 비즈니스 요구사항에도 유연하게 대응할 수 있게 되었습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 900px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1462px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/36806f77a025c9c29dc2bc71596adc88/4ab23/image3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABSUlEQVR42l2S226DMBBE8/+/2IcqahuSgG183TU2TAen7UORVrJgd3xmlktKCdYsWFeHWityzjx7uHUd5947RATeeyzLMspZN96d/SoNbuGMzUhBcTHLE/fbGx7TO+73B2LwgMw48gQpCfM8I/gV0VkOCzZRYD+wb42iBVUOlACUCJ6BS4wRhYOqLwopmc28nV1VdZCclK019H3H3jrGQ1GloJ6CFJMEaKbgY17wcTf4fBhM00SagM4mDQ0lyyAUrUOjH9SRihZ4aal0ExC9wpsD7rlDTsGVWcVE/7nAOffKbWNuWf8yTdoQtCNRbCPx0XccJD3daNmR/YG0EqJQ0FiD6+2Kr+cXjDHwISIwH0e7mcPWWhTdBt3W/1uWV4Y/ls8ahHa1cN4h0EIibaZYIHEiXeZfkAvDrxuUxBu/VXnV2DR3lCn4u5hvwTUd2v7LEYgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/36806f77a025c9c29dc2bc71596adc88/263a4/image3.webp 480w,
/static/36806f77a025c9c29dc2bc71596adc88/a6361/image3.webp 960w,
/static/36806f77a025c9c29dc2bc71596adc88/13d4d/image3.webp 1462w&quot; sizes=&quot;(max-width: 1462px) 100vw, 1462px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/36806f77a025c9c29dc2bc71596adc88/9aebd/image3.png 480w,
/static/36806f77a025c9c29dc2bc71596adc88/a91f8/image3.png 960w,
/static/36806f77a025c9c29dc2bc71596adc88/4ab23/image3.png 1462w&quot; sizes=&quot;(max-width: 1462px) 100vw, 1462px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/36806f77a025c9c29dc2bc71596adc88/4ab23/image3.png&quot; alt=&quot;to-be soldOut process&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;TO-BE 상품 품절 관리 process&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-실시간으로-흐르는-재고-데이터-새로운-아키텍처의-동작-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%9C%BC%EB%A1%9C-%ED%9D%90%EB%A5%B4%EB%8A%94-%EC%9E%AC%EA%B3%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot; 실시간으로 흐르는 재고 데이터 새로운 아키텍처의 동작 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔄 실시간으로 흐르는 재고 데이터: 새로운 아키텍처의 동작 방식&lt;/h2&gt;
&lt;br&gt;
&lt;p&gt;새로 설계한 아키텍처에서는 상품 재고의 변경 이벤트가 발생하는 순간부터 해당 정보가 실시간으로 흐르는 구조로 변경되었습니다.&lt;/p&gt;
&lt;p&gt;먼저, &lt;strong&gt;Event Data Producer 영역&lt;/strong&gt;에서는 재고 변경이 감지되면 CDC(Change Data Capture)를 통해 변경 이벤트를 Kafka Topic으로 즉시 발행합니다. 이 메시지는 &lt;strong&gt;Stream Data Processing 영역&lt;/strong&gt;으로 전달되어 Kafka Streams를 통해 필요한 로직이 실시간으로 처리되고, 최종적으로 OpenSearch에 상품의 품절 여부 정보가 저장됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;상품-유형별-재고-관리-구조&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%ED%92%88-%EC%9C%A0%ED%98%95%EB%B3%84-%EC%9E%AC%EA%B3%A0-%EA%B4%80%EB%A6%AC-%EA%B5%AC%EC%A1%B0&quot; aria-label=&quot;상품 유형별 재고 관리 구조 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상품 유형별 재고 관리 구조&lt;/h3&gt;
&lt;p&gt;이 아키텍처를 이해하려면 먼저 올리브영의 재고 관리 체계를 알아야 합니다. 추후 인벤토리 담당 스쿼드에서 통합 관리 예정이나, 현재 올리브영에서는 상품 유형에 따라 재고 데이터의 관리 주체가 다릅니다.&lt;/p&gt;
&lt;p&gt;올리브영의 상품은 크게 세 가지 재고 관리 체계로 구분됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;직매입 상품 (올리브영이 직접 재고 보유)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;관리 주체:&lt;/strong&gt; 인벤토리 스쿼드&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;처리 방식:&lt;/strong&gt; Inventory Service 내부에서 변경된 재고 정보를 감지하여 Kafka Topic으로 발행&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;위수탁 상품 (협력사 재고)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;관리 주체:&lt;/strong&gt; 상품 탐색 스쿼드&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;처리 방식:&lt;/strong&gt; Kafka Streams로 실시간 이벤트 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;예약/한정 상품 (한시적 재고)&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;관리 주체:&lt;/strong&gt; 상품 탐색 스쿼드&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;처리 방식:&lt;/strong&gt; Kafka Streams로 실시간 이벤트 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;kafka-streams를-통한-실시간-품절-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kafka-streams%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%92%88%EC%A0%88-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;kafka streams를 통한 실시간 품절 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Kafka Streams를 통한 실시간 품절 처리&lt;/h3&gt;
&lt;p&gt;특히 위수탁 및 예약/한정 상품의 경우, Kafka Streams는 다음과 같은 방식으로 품절 여부를 실시간 판단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;이벤트 감지&lt;/strong&gt;: 재고 변경 이벤트 중 &quot;모든 재고 소진&quot; 또는 &quot;재고 입고로 판매 재개&quot; 상태 전환을 감지&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;토픽 발행&lt;/strong&gt;: 감지된 이벤트를 &lt;code class=&quot;language-text&quot;&gt;SoldOut&lt;/code&gt; 토픽으로 발행&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;width: 900px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1273px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/22643133542b20efd879621272fd01cb/73c8b/image4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABh0lEQVR42pVTzUrDQBDO4/kEPoAvoAdBsC/gTS9iFU8KCh4EL4KoEPCgKMWfUkXUpmkwRZukMb+bzWY/J1sMtIK2C8vMzgzffDOzozXf2jjc34XZtVAeKaWSBYmYSyS5JH1om+Rol/oxTjfn8XirDwGLQslMSDgp4CRDfWLAh4s99PQaUqeJLAc4z5SDE4hHgP0/AMtq5Bh7jWUMSRpXZeV5DkGXF0N2JUs+DcPt9RW0bk7w3DhHFMcIwxBfvg/H89GPJQZE2B18wXUd+GQPgkDFeJ4Hy7Jg23ZlT9MU2sbCDOqLszhYqyFJQgqOSMboffRhDjhMXyCIGRgFx5Sw2+0qIMMw0G630el0lG6aJiV1ob03dVyfHuKucVP1JeccjAtV7iexHO/heO9+3uXVbMtAwUNkkQchBBhjKApBfcOvKZcY8p8vpB1tLuO5dYX76zNEUVg5ykG4BOhOOxR9aw71lSUc76xSejECqKacyOn+oWW8wHp9gtOzRhzlpjDaklRtysR4+AYWykuXRtiK2QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/22643133542b20efd879621272fd01cb/263a4/image4.webp 480w,
/static/22643133542b20efd879621272fd01cb/a6361/image4.webp 960w,
/static/22643133542b20efd879621272fd01cb/fe662/image4.webp 1273w&quot; sizes=&quot;(max-width: 1273px) 100vw, 1273px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/22643133542b20efd879621272fd01cb/9aebd/image4.png 480w,
/static/22643133542b20efd879621272fd01cb/a91f8/image4.png 960w,
/static/22643133542b20efd879621272fd01cb/73c8b/image4.png 1273w&quot; sizes=&quot;(max-width: 1273px) 100vw, 1273px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/22643133542b20efd879621272fd01cb/73c8b/image4.png&quot; alt=&quot;kafka streams soldOut process&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;Kafka Streams를 통한 재고 품절 처리&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;이러한 구조로 전환하면서 기존 DB 기반 배치 처리 방식에서 발생하던 &lt;strong&gt;시간 지연과 데이터 정합성 문제&lt;/strong&gt;가 크게 개선되었습니다. 각 단계가 이벤트 기반으로 긴밀하게 연결되어, 재고 변경부터 검색 결과 반영까지의 전체 프로세스가 실시간으로 처리됩니다. 마치 잘 정비된 고속 환승 시스템처럼 매끄럽게 이어지는 거죠.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-데이터-ktx를-소개합니다-kafka-streams란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%8D%B0%EC%9D%B4%ED%84%B0-ktx%EB%A5%BC-%EC%86%8C%EA%B0%9C%ED%95%A9%EB%8B%88%EB%8B%A4-kafka-streams%EB%9E%80&quot; aria-label=&quot; 데이터 ktx를 소개합니다 kafka streams란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚄 데이터 KTX를 소개합니다: Kafka Streams란?&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;위 아키텍처를 보면서 &quot;재고 변동이 일어날 때마다 어떻게 그렇게 빠르게 처리될까?&quot; 궁금하셨을 텐데요. 그 비밀은 바로 Kafka Streams에 있습니다.
Kafka Streams란 Kafka에서 공식적으로 제공하는 Kafka 클라이언트 Java 라이브러리로, 토픽 데이터를 낮은 지연과 빠른 속도로 처리가 가능합니다.
토픽 데이터를 가공하여 다른 토픽으로 발행하거나 2개 이상의 토픽을 조인하여 데이터 집계 처리하는 등 다양하게 활용될 수 있으며, 간단한 코드로 구현이 가능한 장점이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;카프카와 완벽한 호환&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Kafka에서 공식으로 제공되는 라이브러리로 Kafka 버전에 맞춰 호환성을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;딱 1번만(Exactly-once) 처리 보장&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;데이터가 유실되거나 중복 처리되지 않도록 한 번만 처리될 수 있는 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;별도 스케줄링 도구가 불필요&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Spark Streaming 등 별도의 스케줄링 도구가 필요 없이 Kafka Streams를 통하여 Stream Processing 처리 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streams DSL과 Processor API를 사용하여 간단한 구현 가능&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Streams DSL&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;미리 제공되는 함수들을 사용하여 간단하게 로직 구현 가능 (filter, map, join, window 등)&lt;/li&gt;
&lt;li&gt;KStream, Ktable, GlobalKtable 데이터 처리 구조 제공&lt;/li&gt;
&lt;li&gt;스트림 데이터 처리뿐 아니라 key-value 저장소로 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Processor API&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Streams DSL을 통한 구현이 불가할 경우 사용&lt;/li&gt;
&lt;li&gt;Streams DSL 보다 복잡한 코드 구조를 가지지만, Lower-Level에서 복잡하고 정교한 로직 구현 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 상태 저장소를 사용&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;상태 기반 처리를 위하여 RockDB를 사용하며, 장애가 발생하더라도 안전하게 복구 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;-kafka-streams의-동작-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-kafka-streams%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot; kafka streams의 동작 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧩 Kafka Streams의 동작 방식&lt;/h3&gt;
&lt;p&gt;Kafka Streams는 내부적으로 Topology(토폴로지) 구조로 동작합니다. Topology는 데이터를 처리하는 일련의 흐름을 노드(Processor) 단위로 구성한 형태로, 각 노드가 데이터를 받아 가공하고 다음 단계로 전달하는 파이프라인 역할을 합니다.&lt;/p&gt;
&lt;p&gt;Kafka Streams의 Topology는 크게 다음 세 가지 Processor로 구성됩니다.&lt;/p&gt;
&lt;h4 id=&quot;source-processor&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#source-processor&quot; aria-label=&quot;source processor permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Source Processor&lt;/h4&gt;
&lt;p&gt;Topology의 시작점으로, Kafka 토픽과 직접 연결되어 메시지를 가져오는 역할을 합니다. 즉, 스트림 처리를 시작하기 위해 가장 먼저 데이터를 받아오는 입구 노드입니다.&lt;/p&gt;
&lt;h4 id=&quot;stream-processor&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#stream-processor&quot; aria-label=&quot;stream processor permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Stream Processor&lt;/h4&gt;
&lt;p&gt;가져온 메시지를 실제로 가공·처리하는 핵심 노드입니다. 여기서는 join, window 등의 연산이나 다양한 비즈니스 로직을 적용하여 데이터를 원하는 형태로 변환하거나 조합합니다.&lt;/p&gt;
&lt;h4 id=&quot;sink-processor&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#sink-processor&quot; aria-label=&quot;sink processor permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Sink Processor&lt;/h4&gt;
&lt;p&gt;Topology의 마지막 노드로 가공, 집계된 데이터를 다른 Kafka 토픽으로 발행하는 역할을 합니다. 즉, 처리된 결과가 다음 시스템이나 서비스로 전달되는 출구 노드입니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 350px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 423px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4e0a1e4563140fc7e1816a6f918ef29f/6fe3b/image5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 133.806146572104%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB4klEQVR42q2V147CQAxF8//fxjNP9B56bwG8OpY8MsOQRLtraZRmX1/XZOLkfD7ruVwucjqdwvPr9Qo6RVHIbrfT9+hwDoeD3G43/Z55wNlsJpPJRPI8l+l0KoPBQK/P5zPo4KzT6agueuPxWHq9nmy323fA+/0eXiLGar/fKysEYK+TkswMW62WbDabYGisRqORdLtdvW+327JYLIIOtv4EQKhj5EPzOev3+zKfz1XP2H5leDwelRmhcXy4yPV6ldVqpczIW/w9GfJ6vdbEYpxiSKh8Hw6H1YC+KIQVF4UqW96oesrpB6AZU0F6yjuBnX0nPQb+jeVbH1KU5XIpj8cjVJhnD0B66MXKkH3jUgQKZO3iBdY4ssmoBMSAXDEBqfBwSBuR21Q+PwAJl1kFjGss5JGwiYL72GGWog2oNXNswD3sfPFKGXpDKmxhGeg3ZqWApkjINLRvcvLbaDR0faVASxkiTIdtGHJHOigMY2h70e+ATCoE42azqa1CuObMcolDlmxtQNuTsIFh7VkuCxsgtnQdyeooEVKqhX4NSOLjmf4TIHNryze1+v2pHTKz/W8M7X9C2/CbpeIwZt4pGAcdWqwSkOkADBCamRkHAEDyyv8IYHqSXNdiiGcAU3/FOOQf5JRFC6sdlOEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4e0a1e4563140fc7e1816a6f918ef29f/008d7/image5.webp 423w&quot; sizes=&quot;(max-width: 423px) 100vw, 423px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4e0a1e4563140fc7e1816a6f918ef29f/6fe3b/image5.png 423w&quot; sizes=&quot;(max-width: 423px) 100vw, 423px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4e0a1e4563140fc7e1816a6f918ef29f/6fe3b/image5.png&quot; alt=&quot;kafka streams topology&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;Kafka Streams 동작방식&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;-어째서-kafka-streams일까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%96%B4%EC%A7%B8%EC%84%9C-kafka-streams%EC%9D%BC%EA%B9%8C&quot; aria-label=&quot; 어째서 kafka streams일까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔑 어째서 Kafka Streams일까?&lt;/h3&gt;
&lt;p&gt;상품 재고 수량의 실시간 변동을 처리하기 위해 Kafka Streams를 선택한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;h4 id=&quot;1️⃣-streams-dsl을-활용해-구현이-간결합니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F%E2%83%A3-streams-dsl%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%B4-%EA%B5%AC%ED%98%84%EC%9D%B4-%EA%B0%84%EA%B2%B0%ED%95%A9%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;1️⃣ streams dsl을 활용해 구현이 간결합니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1️⃣ Streams DSL을 활용해 구현이 간결합니다.&lt;/h4&gt;
&lt;p&gt;Kafka Streams는 map, filter 등 기본 함수를 제공하여 복잡한 로직 없이도 원하는 비즈니스 조건을 간단한 코드로 표현할 수 있습니다.
이를 통해 개발 효율성을 높이고, 유지 보수가 용이한 구조를 확보할 수 있었습니다.&lt;/p&gt;
&lt;h4 id=&quot;2️⃣-애플리케이션-구조가-단순해집니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EF%B8%8F%E2%83%A3-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B5%AC%EC%A1%B0%EA%B0%80-%EB%8B%A8%EC%88%9C%ED%95%B4%EC%A7%91%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;2️⃣ 애플리케이션 구조가 단순해집니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2️⃣ 애플리케이션 구조가 단순해집니다.&lt;/h4&gt;
&lt;p&gt;기존 구조에서는 Consumer와 Producer 애플리케이션의 분리로 인해 불필요한 네트워크 통신이 발생하고 복잡도가 높았습니다. Kafka Streams는 Consumer와 Producer 로직을 단일 애플리케이션 내에서 통합 관리하여 구조를 간결하게 만들고, 유지 보수성을 크게 개선할 수 있었습니다.&lt;/p&gt;
&lt;h4 id=&quot;3️⃣-실시간-데이터-처리에-최적화된-아키텍처입니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EF%B8%8F%E2%83%A3-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC%EC%97%90-%EC%B5%9C%EC%A0%81%ED%99%94%EB%90%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%9E%85%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;3️⃣ 실시간 데이터 처리에 최적화된 아키텍처입니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3️⃣ 실시간 데이터 처리에 최적화된 아키텍처입니다.&lt;/h4&gt;
&lt;p&gt;Kafka Streams는 스트리밍 데이터의 특성에 맞춰 설계된 라이브러리로, 재고와 같이 지속적으로 변화하는 데이터를 낮은 지연과 높은 처리 안정성으로 다룰 수 있습니다.&lt;/p&gt;
&lt;p&gt;결과적으로 Kafka Streams는 실시간성과 단순성을 모두 갖춘 구조로, 상품 재고 처리 프로세스의 안정성과 효율성을 한층 높이는 핵심 역할을 하게 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;️-kafka-streams-예제-코드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-kafka-streams-%EC%98%88%EC%A0%9C-%EC%BD%94%EB%93%9C&quot; aria-label=&quot;️ kafka streams 예제 코드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⌨️ Kafka Streams 예제 코드&lt;/h3&gt;
&lt;p&gt;Kafka Streams의 가장 큰 장점 중 하나는 복잡한 실시간 데이터 처리를 단 몇 줄의 코드로 구현할 수 있다는 점입니다.
아래 예제는 Streams DSL을 사용하여 품절에 대한 이벤트를 감지하고, 가공된 데이터를 다른 토픽으로 발행하는 간단한 구현 예제 코드입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SoldOutStreamsProcessor&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; val goodsInventoryService&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;GoodsInventoryService&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; val streamsBuilder&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StreamsBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; val streamsProperty&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StreamsProperty&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    fun &lt;span class=&quot;token function&quot;&gt;soldOutInventory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        streamsBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stream&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamsProperty&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;optionInventory&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// streamsBuilder를 통하여 스트림 데이터를 consume&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mapValues &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; convertToMessage&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SoldOutVo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;              &lt;span class=&quot;token comment&quot;&gt;// 비즈니스 형식에 맞게 Message Convert&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;filter &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; goodsInventoryService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;validateSoldOut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;// 비즈니스 조건에 맞는 이벤트 감지&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertFromMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;afterInventory&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// produce topic 형식에 맞게 Message Convert&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;filter &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isNullOrEmpty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamsProperty&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;optionSoldOut&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;                                      &lt;span class=&quot;token comment&quot;&gt;// topic produce&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이처럼 Kafka Streams는 토픽 데이터를 읽고(consume), 비즈니스 로직을 적용한 뒤, 다시 토픽으로 발행(produce) 하기까지의 과정을 하나의 애플리케이션 내에서 간결하게 처리할 수 있습니다.
복잡한 네트워크 구성이나 별도의 Consumer/Producer 관리 없이도 안정적인 스트리밍 처리가 가능하다는 점이 큰 장점입니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;-열차-운행의-비밀-코드와-주의-사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%97%B4%EC%B0%A8-%EC%9A%B4%ED%96%89%EC%9D%98-%EB%B9%84%EB%B0%80-%EC%BD%94%EB%93%9C%EC%99%80-%EC%A3%BC%EC%9D%98-%EC%82%AC%ED%95%AD&quot; aria-label=&quot; 열차 운행의 비밀 코드와 주의 사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚄 열차 운행의 비밀: 코드와 주의 사항&lt;/h3&gt;
&lt;p&gt;다만 Kafka Streams를 도입할 때는 몇 가지 주의해야 할 점이 있습니다. 스트리밍 처리의 장점을 극대화하기 위해서는 아래 항목들을 유념해야 합니다.&lt;/p&gt;
&lt;h4 id=&quot;1️⃣-네트워크-통신이-필요한-고비용-작업은-지양할-것&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F%E2%83%A3-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%86%B5%EC%8B%A0%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B3%A0%EB%B9%84%EC%9A%A9-%EC%9E%91%EC%97%85%EC%9D%80-%EC%A7%80%EC%96%91%ED%95%A0-%EA%B2%83&quot; aria-label=&quot;1️⃣ 네트워크 통신이 필요한 고비용 작업은 지양할 것 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1️⃣ 네트워크 통신이 필요한 고비용 작업은 지양할 것&lt;/h4&gt;
&lt;p&gt;Kafka Streams는 메모리 기반의 실시간 데이터 처리에 최적화된 구조를 가지고 있습니다. 따라서 내부 프로세스에서 외부 API 호출이나 DB I/O 등 네트워크 통신이 필요한 작업을 수행하면 지연(latency)이 증가하고 전체 스트림 처리 효율이 저하될 수 있습니다. 이런 경우에는 별도의 비동기 처리나 외부 서비스 연동 구조로 분리하는 것이 좋습니다.&lt;/p&gt;
&lt;h4 id=&quot;2️⃣-토픽-간-조인join에는-제약이-존재함&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EF%B8%8F%E2%83%A3-%ED%86%A0%ED%94%BD-%EA%B0%84-%EC%A1%B0%EC%9D%B8join%EC%97%90%EB%8A%94-%EC%A0%9C%EC%95%BD%EC%9D%B4-%EC%A1%B4%EC%9E%AC%ED%95%A8&quot; aria-label=&quot;2️⃣ 토픽 간 조인join에는 제약이 존재함 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2️⃣ 토픽 간 조인(Join)에는 제약이 존재함&lt;/h4&gt;
&lt;p&gt;Kafka Streams는 토픽 간 조인을 지원하지만, 다음과 같은 조건을 만족해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동일한 Kafka Cluster 내에 토픽이 존재해야 합니다.&lt;/li&gt;
&lt;li&gt;조인 대상 토픽의 키(Key) 가 동일해야 합니다.&lt;/li&gt;
&lt;li&gt;토픽들이 동일한 파티션 수를 가지며 코파티셔닝(co-partitioning) 되어 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 조건 중 하나라도 만족하지 않으면 조인 동작이 정상적으로 수행되지 않을 수 있습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;-조인join이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A1%B0%EC%9D%B8join%EC%9D%B4%EB%9E%80&quot; aria-label=&quot; 조인join이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🙋‍ 조인(Join)이란?&lt;/h4&gt;
&lt;p&gt;Kafka Streams에서 조인(Join) 이란, 두 개 이상의 스트림(토픽)을 공통된 키(key)를 기준으로 결합하는 기능을 의미합니다.
즉, 서로 다른 토픽 혹은 상태 저장소(State Store)에서 유입된 데이터를 같은 키값으로 묶어 새로운 이벤트나 상태를 만들어내는 방식입니다.
예를 들어, 토픽 A와 토픽 B가 동일한 키를 가진 데이터를 각각 발행하면, Stream Processor는 두 데이터를 하나로 결합하여 새로운 토픽으로 발행하게 됩니다.
이 과정을 통해 여러 소스에서 들어오는 실시간 데이터를 하나의 통합된 이벤트로 다룰 수 있습니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 600px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 811px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ffe9af52b1b1f0085a1e977610e2fb44/55bfd/image6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 93.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAABe0lEQVR42p2Uia6CQAxF5/8/UFCIiRsuuKLgglBzmtSgYXg8m9wwMyl32ttOXZZlcjweFYfDQRaLhaxWq/cZuF6vkue5bDYb2W63stvtZDabSRzHst/v32en00ncZDKR8Xgso9FIBoOBDIdDRRiGEkWRrtM0VbA3P4APe4DfdDoVV1WVlGWpeD6fslwupSgKXXP2eDykrmuF+fI9n88amZ3hD5w0jJ9xwhlHn0FOeqQLWdM+CNFzPp+rhujmM0jwS5JEib2E9/tdxbdCtUWGXS4XJeLLP17CppEOVW0aMkCExj5rJbzdbholt6MpX6LhAjKgvShAL0KiIDoKZLpCSttQrGakbTq7rkpatIhPw9slXeakh5GypQnYg7bW6kVIZOv1WlOmIKTLuk3HPwlJ3d50V7P3JkRDGr3Z7KbvT4SkhX5Mk+9n9i9CGwgWJX34ff5zlWlme4pdZF5CWoIUaXAqyiBgoLIHXek7X0RMY4ZmEAQfQ9SGrG8avQAhxMu6hA7BogAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ffe9af52b1b1f0085a1e977610e2fb44/263a4/image6.webp 480w,
/static/ffe9af52b1b1f0085a1e977610e2fb44/80d25/image6.webp 811w&quot; sizes=&quot;(max-width: 811px) 100vw, 811px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ffe9af52b1b1f0085a1e977610e2fb44/9aebd/image6.png 480w,
/static/ffe9af52b1b1f0085a1e977610e2fb44/55bfd/image6.png 811w&quot; sizes=&quot;(max-width: 811px) 100vw, 811px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ffe9af52b1b1f0085a1e977610e2fb44/55bfd/image6.png&quot; alt=&quot;join&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;Kafka Streams Join&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h4 id=&quot;-코파티셔닝co-partitioning이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%BD%94%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9Dco-partitioning%EC%9D%B4%EB%9E%80&quot; aria-label=&quot; 코파티셔닝co partitioning이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🙋‍ 코파티셔닝(Co-partitioning)이란?&lt;/h4&gt;
&lt;p&gt;2개 이상의 스트림(토픽)이 동일한 파티셔닝 스키마를 따르는 것을 의미합니다.
즉, 서로 다른 두 개의 토픽이 파티션 개수가 같고 동일한 파티셔닝 전략(partition strategy)을 사용하면서, 조인이 되고자 하는 데이터가 메시지 키에 주입된 상태를 의미합니다.
아래 그림을 보면 각 토픽 및 파티션에 전달되는 데이터의 동일한 키를 가진 메시지는 동일한 파티션으로 할당되는 것을 볼 수 있습니다.
Kafka Streams에서는 각 파티션 별로 Task를 할당하여 데이터를 처리하기 때문에 데이터를 조인을 사용하기 위해서는 아래와 같이 코파티셔닝된 구조가 선행되어야 합니다.
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 600px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 918px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2c72ea2b636221acec2367d9d2d9b970/926ba/image7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.75000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3ElEQVR42i1TB3LcMBC7/38vk8RObJ/aqVDsTdIVIyAVzWC4hdzFLkaXbdsQQ0CKEVvO0EpBLAu2LddYTgn7vld8f3+jfMex1/wqBKZpgrOWsaPiUi7mbce4KAyTwG2R1e7Ghf5KW2KYV0gTWGTH8/VEZON8f8KnHUI5xO2OfDwQSOBSGJSCw7Tg2g3ohhEt0dDubxOGca7xWVoYT9Z8uCiPdZWwZKa0qdDWM8+CsYy0RSzDB8buL8bmDXP7hql7p/2Ouf9g7gvJTMjBYDYR19lCigVKyjquNRraWGifyJD0yx5zdHygkbwkVLVPGOYs9hxhfcCRA4RNUMYRZOXiyY5nZVh3yKIpcS95g2GnmaIUv7AvsY13ysJNyCzsq/+iQJlEVoq4H3fu9sU3/wuWgGRHIQ2mVROm7qxAqBO6MHEB9+yQ7IygZ3h5g1sHWDEgWpLgRBw5IVGUfpzQtB2+mgbXtsW1Iei3fU9RegphqS5VPhKiHigEm5CEsa6OHIJH5qouIUSOESDHhgIUUX5hvP6kML8xfP5g7A/W4ZOMbtijwjNrRNlBzDdoMcLIiQLd4LWo+z5FIbxVJwyV0yucOVH8YCWit3jc73g9HxTNIDneVSMcxy5ncpI/QcQ/5NX3oQM6ZMYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2c72ea2b636221acec2367d9d2d9b970/263a4/image7.webp 480w,
/static/2c72ea2b636221acec2367d9d2d9b970/b3ab4/image7.webp 918w&quot; sizes=&quot;(max-width: 918px) 100vw, 918px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2c72ea2b636221acec2367d9d2d9b970/9aebd/image7.png 480w,
/static/2c72ea2b636221acec2367d9d2d9b970/926ba/image7.png 918w&quot; sizes=&quot;(max-width: 918px) 100vw, 918px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2c72ea2b636221acec2367d9d2d9b970/926ba/image7.png&quot; alt=&quot;co-partitioning&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;Kafka Streams Co-partitioning&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;아쉽지만 위에 설명드린 조인(Join) 기능에 대해서는 품절 시스템에 활용하지는 못했습니다. 초기 프로모션 정보까지 포함되어 재고 관리가 되도록 기획되었으나 복잡한 비즈니스 관계로 인해 간소화되었고,
Kafka Streams를 통해서 상품 분류 별 재고 토픽의 필터링 처리하는 기능으로 사용하게 되었습니다.
하지만 Kafka Streams의 활용성은 무궁무진하니, 앞으로도 다양한 비즈니스에 적용해 볼 계획입니다!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-러시아워에-본-개선-결과-성공-그-자체&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%9F%AC%EC%8B%9C%EC%95%84%EC%9B%8C%EC%97%90-%EB%B3%B8-%EA%B0%9C%EC%84%A0-%EA%B2%B0%EA%B3%BC-%EC%84%B1%EA%B3%B5-%EA%B7%B8-%EC%9E%90%EC%B2%B4&quot; aria-label=&quot; 러시아워에 본 개선 결과 성공 그 자체 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 러시아워에 본 개선 결과: 성공 그 자체&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;상품 품절 여부 관리 프로세스 개선을 진행한 후 첫 올영세일을 맞이했습니다!
모니터링 결과는 놀랍게도 &lt;strong&gt;함수 호출 양의 약 86%가 감소&lt;/strong&gt;했습니다!
개선 전 일주일 동안 오라클 함수 호출 양은 2.34G (23억 4천만)이었으나, 개선 후 237M (2억 3천7백만)으로 함수 호출 양이 대폭 감소한 것을 확인할 수 있었습니다.👏🏻👏🏻👏🏻
그로 인해 올영 세일 기간에 보다 안정적인 서비스를 제공할 수 있게 되었습니다.&lt;/p&gt;
&lt;div style=&quot;width: 840px; max-width: 100%; margin: auto; overflow: hidden;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1214px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bb222144e456e4617e9bff735c9d04cf/2ed29/image8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3klEQVR42o1T225TMRA8//8TFUiAeESIJ0BBFCSKuCRQkChqk95ImjRRz8mxfXzbYdZOqz7wgKXV2N7d2bU9buYbg84GOOfghgHWOgzEECJ8CBi859qjNxY37RZtt0W37bHtDVJMiCnTH0p84rzph4jeRUQ6NXHw4Q4dUW3YWSmi+24ofh9qXoy5kBXCyC5SSvjfkXNGiJVIiympFovkKISBhJ7H0iEiyLSU5W5+H9VwO/9nMUGj96SVNNbHmrwyAhsEP1eoZCVd4Oj36R4pLaaKV71g2bPDek8Bh0tgvBCct4K3p4LZjeDJIdB5wWRRE47Wgs9/BFvufSIaFn11Asw7wZtZzW8GH3G88nj0HXhJ5/Mj4OkP4MOF4OG3GrQ3Bt6fC178Fjz7Bbw7EzyYCD5eAnsT4OBS8JjFv85JmHmZXy48RjNWOYnYP73FjNE0Y3+WSseKSjRSX8GE18eJMYnxgZ1mTDc8sp4/UAKWOrPWFjTG0HResW07LJcrXF+vsV5vsFgs0fe9PgMfor5ueTwqoFHJWOrK7rRVdbfToKtz3TfGFf2pDq2twq+KIGnK5YVjkQ1Fqb/AaHf8JZY/RlHX+hv09+hQMu0gUoO65zSWRe3ZFG58AHs1hwsJfwGqkE5t5I1bHQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bb222144e456e4617e9bff735c9d04cf/263a4/image8.webp 480w,
/static/bb222144e456e4617e9bff735c9d04cf/a6361/image8.webp 960w,
/static/bb222144e456e4617e9bff735c9d04cf/32dd4/image8.webp 1214w&quot; sizes=&quot;(max-width: 1214px) 100vw, 1214px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bb222144e456e4617e9bff735c9d04cf/9aebd/image8.png 480w,
/static/bb222144e456e4617e9bff735c9d04cf/a91f8/image8.png 960w,
/static/bb222144e456e4617e9bff735c9d04cf/2ed29/image8.png 1214w&quot; sizes=&quot;(max-width: 1214px) 100vw, 1214px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bb222144e456e4617e9bff735c9d04cf/2ed29/image8.png&quot; alt=&quot;before function call&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;개선 전 Oracle 함수 호출&lt;/figcaption&gt; 
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1205px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/96b897355d1ace8fb2a7f874f78831a2/79c97/image9.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABb0lEQVR42pVT7U7DMAzs+z8b4g8CAQIxBJvEhrqlab7sJIftwhhCIIh0shPbZye9DqutxxQJ3s/wIWCaPGJMiCkhqBXMcu6cx7h3OLgJ+4PDJPm1VrTWUAqBiG0/EFdwbRLocvBpNcga43Ys7H2JEbPFipAokXJ85AxEZM5fl5IujSpyJiMtwsFKKLFBu2kHTfwrflt25UQ6Yf8X6U+NhiwPGkv9QohjwonFYk9xmnMkLDJhEMJAANWOzF0ssAsd62l5M587DhkYY7ecIphJUIB9ApzEI+m5Xblh5xmX247VSNhMHTc7xsO+4eKlSVHF7WvH1QubXY2MtWu43jIexT/fNNztCPdjs9pBr6Kam+dgWgwhGlRbLF8v5Sx+MaginHOIEo9RIPrMVuttb1dWXal4Uy5GYlIoCjaozlQiRAvYNNe+4UPLgzpepgva8f3PUF+n1HOdUFeSSfhdvCllmSyLOhhp84R0cSb2GYkr3gB9Vl4XEK5mIgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/96b897355d1ace8fb2a7f874f78831a2/263a4/image9.webp 480w,
/static/96b897355d1ace8fb2a7f874f78831a2/a6361/image9.webp 960w,
/static/96b897355d1ace8fb2a7f874f78831a2/213b7/image9.webp 1205w&quot; sizes=&quot;(max-width: 1205px) 100vw, 1205px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/96b897355d1ace8fb2a7f874f78831a2/9aebd/image9.png 480w,
/static/96b897355d1ace8fb2a7f874f78831a2/a91f8/image9.png 960w,
/static/96b897355d1ace8fb2a7f874f78831a2/79c97/image9.png 1205w&quot; sizes=&quot;(max-width: 1205px) 100vw, 1205px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/96b897355d1ace8fb2a7f874f78831a2/79c97/image9.png&quot; alt=&quot;after function call&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;개선 후 Oracle 함수 호출&lt;/figcaption&gt; 
&lt;/div&gt;
&lt;h2 id=&quot;-우리의-여정은-계속됩니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%97%AC%EC%A0%95%EC%9D%80-%EA%B3%84%EC%86%8D%EB%90%A9%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; 우리의 여정은 계속됩니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🏃 우리의 여정은 계속됩니다&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;이번 글에서 소개한 상품 품절 여부 관리 프로세스 개선은 현재 진행 중인 상품 통합 프로젝트의 한 부분입니다.
다양한 팀과 협업하는 큰 프로젝트를 처음 진행하다 보니 개인적으로 많은 도전과 성장의 기회를 얻었습니다.
앞으로 넘어야 할 산이 참 많지만, 능력 있는 팀원들과 함께라면 남은 여정도 성공적으로 헤쳐나갈 수 있을 것이라 확신합니다!&lt;/p&gt;
&lt;p&gt;다음 포스트에는 더 많은 정보를 담아 풍성하게 돌아오겠습니다. &lt;br&gt;
많은 관심 부탁드리며 마무리 하겠습니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[QA 엔지니어가 AI로 만든 교육 영상, 25분짜리 인시던트 가이드 탄생기]]></title><description><![CDATA[안녕하세요! 올리브영의 QA팀 입니다.
이번 글에서는 저희 팀이 AI…]]></description><link>https://oliveyoung.tech/2025-12-08/creating-video-with-ai/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-08/creating-video-with-ai/</guid><pubDate>Mon, 08 Dec 2025 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영의 QA팀 입니다.
이번 글에서는 저희 팀이 AI 기술을 활용하여 제작한 인시던트 교육 영상에 대한 흥미로운 제작 과정을 소개하려 합니다.&lt;/p&gt;
&lt;p&gt;💡잠깐! 인시던트 관리에 대해 먼저 알고 싶다면?&lt;br&gt;
이 글은 인시던트 관리 정책을 효과적으로 전파하기 위한 &apos;영상 제작 과정&apos;에 초점을 맞추고 있습니다.
따라서 평소 올리브영의 인시던트 관리 문화와 체계에 대해 궁금하셨다면, &lt;a href=&quot;https://oliveyoung.tech/2024-01-23/incident/&quot;&gt;올리브영은 인시던트를 어떻게 관리하고 있는가?&lt;/a&gt;를 먼저 읽어보시길 권장드립니다.😉&lt;/p&gt;
&lt;h2 id=&quot;-왜-영상이었나-인시던트-교육의-새로운-시도&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%99%9C-%EC%98%81%EC%83%81%EC%9D%B4%EC%97%88%EB%82%98-%EC%9D%B8%EC%8B%9C%EB%8D%98%ED%8A%B8-%EA%B5%90%EC%9C%A1%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%8B%9C%EB%8F%84&quot; aria-label=&quot; 왜 영상이었나 인시던트 교육의 새로운 시도 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;💭 왜 영상이었나: 인시던트 교육의 새로운 시도&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;qa팀의-역할-확장-검증을-넘어-전파로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#qa%ED%8C%80%EC%9D%98-%EC%97%AD%ED%95%A0-%ED%99%95%EC%9E%A5-%EA%B2%80%EC%A6%9D%EC%9D%84-%EB%84%98%EC%96%B4-%EC%A0%84%ED%8C%8C%EB%A1%9C&quot; aria-label=&quot;qa팀의 역할 확장 검증을 넘어 전파로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;QA팀의 역할 확장: 검증을 넘어 전파로&lt;/h3&gt;
&lt;p&gt;QA팀이라고 하면 흔히 &quot;버그를 찾는 팀&quot; 또는 &quot;검증하는 팀&quot;으로 생각하기 쉽습니다. 물론 그것도 우리의 중요한 역할입니다. 하지만 올리브영의 QA팀은 단순한 확인을 넘어 서비스 안정성과 품질 향상을 주도하는 팀이기도 합니다.&lt;/p&gt;
&lt;p&gt;QA팀은 매년 다양한 인시던트를 분석하고, 그 원인과 대응 과정을 기록하며 서비스 품질 개선을 위해 꾸준히 노력해왔습니다. 하지만 아무리 정확한 가이드와 상세한 문서를 만들어도, 실제로 읽히지 않거나 제대로 이해되지 않으면 의미가 없습니다.&lt;/p&gt;
&lt;p&gt;특히 인시던트 대응은 초기 판단이 가장 중요합니다. 발생 후 1시간 이내에 정확한 레벨을 선언하고, 적절한 보고 체계를 따라야 피해를 최소화할 수 있습니다. 그래서 올리브영은 인시던트 레벨 선언 및 대응 프로세스를 강화해오고 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;2025-인시던트-레벨-개편-동일한-기준-공유의-필요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2025-%EC%9D%B8%EC%8B%9C%EB%8D%98%ED%8A%B8-%EB%A0%88%EB%B2%A8-%EA%B0%9C%ED%8E%B8-%EB%8F%99%EC%9D%BC%ED%95%9C-%EA%B8%B0%EC%A4%80-%EA%B3%B5%EC%9C%A0%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1&quot; aria-label=&quot;2025 인시던트 레벨 개편 동일한 기준 공유의 필요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2025 인시던트 레벨 개편: 동일한 기준 공유의 필요성&lt;/h3&gt;
&lt;p&gt;서비스의 성장에 맞춰 2025년 새로운 비즈니스 규모를 반영한 레벨 기준이 적용되면서, 전체 조직이 동일한 이해를 갖고 대응할 수 있도록 정확한 기준 공유가 무엇보다 중요해졌습니다.&lt;/p&gt;
&lt;p&gt;올리브영은 인시던트를 영향도에 따라 Level 0부터 Level 3까지 4단계로 분류하며, 각 레벨마다 보고 체계와 대응 절차가 다릅니다. 하지만 기존처럼 구두/문서 전파를 하자니 참고 자료로는 훌륭하지만 긴급 상황의 행동 지침으로는 한계가 있다고 판단했습니다. 단순히 &apos;정보를 제공&apos;하는 것을 넘어, 구성원들이 긴급 상황에서 &apos;정보를 기억하고 즉시 행동&apos;할 수 있도록 해야 했습니다.&lt;/p&gt;
&lt;p&gt;이러한 학습 효과의 관점에서 기존 방식을 재검토한 결과, 교육학에서 학습 방법에 따른 기억 보존율을 나타내는 &quot;학습 피라미드(Learning Pyramid)&quot; 이론을 주목하게 되었습니다. 수백 명의 관계자에 가르치기(Teaching) 및 실습(Practice)을 진행할 수 없는 현실적 제약 하에서, 시청각 자료를 제공하는 것이 단순 문서 제공 대비 2배 기억 보존율을 2배 이상 높일 수 있는 가장 현실적인 대안이라고 판단했습니다.&lt;/p&gt;
&lt;center&gt;
     &lt;div style=&quot;width: 550px !important; max-width: 100%; margin:auto&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1248px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fea9c1f2c812479044514399ad070fe8/f03ba/pyramid.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAADJElEQVR42j2Tu3MbRRyA76+BhpqKgoKWgcwwYcJACmZCGCZFKFKBG5wEPAo2EQYHx9jCiizrZck+yXeSfZL1lu6l0/vuZPkhAuG/+FgpA8U3u8Xut7/XSjOvzbXbEjS5dBtM3RoTt4LnnjFyi/TdU7pugY6bp+PnsQXWJI85UTEmCrp/vMDwFUxfRboSkssFdS6EbOpWOXfLQlhi7GlCeEJPCB0vjzHMUnfSVOwUZUG1l0b3hGyB+lo4HVeZjisLvEGJgVOg7+TpdlWcvkLfK2CNVBFhgWIzSiSzxnYswE5ylYy2jX1ewJzLvHn0BSR/XMQbaVz4ZXqdY3K5TaLRNSJRcSH7G52hSl5PcmYrQnyMPVaoWilOG1FOm/uU9SjmMCfKcYLjnyKNxOtz2u0kxVJYsEtB26FhJOmNVeTiJunylhAe0+pm6YwUMsozwvFV9jM/E8s8xejlcC8rIhsNqT/OMRKF1sp/EEutkjoMEo6uUG3GOak8J7h9l63oA5Sz5xzmg9TNuDhfxHCOULQQuUIIWfmdUjXG2C8hte0EB3JwQTLz02L1pxq1dpjA+m2e/HKb4NZdcto6qWyAo8KPWI6M3VFQ8iEUZYdsbptKJcHEryIZVgxF3SB3vM6R/FTsn1FrhFkOfMzS4/f57slHBIRUPlkjKa+QkB9ypAZwRd2vpo0Fs2mLq0lzMXJSx47jCGwzim3sUa9tc+/r97hx6w0+/+pt7j14l28ffUBK/p7d/W9YXb/D0sMP2di8z7WQXE9azM7bzMQ6Hz2pY+1j6nsYrRfozTA10YBIZIlHjz/jiy/f4cbNN7n56Vssr3zC8g+3CKzeIRlfQVNDFNUXFJUIJWUPvXK4+BySI1Ke07Xi9KwEPTGw476MO8hi63EKohQbv94nfbCCY6SZ+TVeTU3GtkYxt8vJYYh8eoeWdsBLt43UncvM1/T+k1pJ+vYBIzEmej2F1Tik08piVDO0zg64GFSYjRr87en84xsCi1eeycvRXDiXGUI2Zy41E/TNJAMhHHYyGI0EhpB2dZmukPbbKtfDOrOBaMagyZ+D1v/8NWzzL8UXOSbmJOM+AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fea9c1f2c812479044514399ad070fe8/263a4/pyramid.webp 480w,
/static/fea9c1f2c812479044514399ad070fe8/a6361/pyramid.webp 960w,
/static/fea9c1f2c812479044514399ad070fe8/92e18/pyramid.webp 1248w&quot; sizes=&quot;(max-width: 1248px) 100vw, 1248px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fea9c1f2c812479044514399ad070fe8/9aebd/pyramid.png 480w,
/static/fea9c1f2c812479044514399ad070fe8/a91f8/pyramid.png 960w,
/static/fea9c1f2c812479044514399ad070fe8/f03ba/pyramid.png 1248w&quot; sizes=&quot;(max-width: 1248px) 100vw, 1248px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fea9c1f2c812479044514399ad070fe8/f03ba/pyramid.png&quot; alt=&quot;pyramid&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;학습 피라미드(Learning Pyramid) 이론&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;무엇보다 이 교육 영상은 아래와 같은 효과가 기대되었고, 그래서 우리는 결심했습니다. 품질을 관리하는 팀이니까, 이제는 품질의 &apos;이해&apos;까지 관리하자고!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;반복 시청 용이성: 필요할 때마다 해당 챕터만 빠르게 확인 가능&lt;/li&gt;
&lt;li&gt;시각적 강조 효과: 중요한 기준을 화면과 음성으로 동시에 전달&lt;/li&gt;
&lt;li&gt;메시지 통일성: 누가 시청하든 동일한 내용과 방식으로 메시지를 전달&lt;/li&gt;
&lt;li&gt;확장성 확보: 신규 입사자 온보딩 교육에도 즉시 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;처음에는 저희도 스스로에게 물었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“QA팀이 영상을 만든다고?”&lt;br&gt;
“검증은 자신 있는데… 영상 제작은 또 다른 세계 아닐까?” 😅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;하지만 곰곰이 생각해보니, 인시던트 교육이야말로 QA가 가장 잘 설명할 수 있는 영역이었습니다. 인시던트의 본질은 &apos;무엇이 왜 잘못되었는가&apos;를 찾고, &apos;앞으로 어떻게 예방할 것인가&apos;를 정의하는 일! 즉, 서비스 품질을 개선하는 QA의 핵심 역할과 완전히 맞닿아 있었죠.
그렇게 QA팀의 첫 번째 AI 영상 제작 도전이 시작됐습니다. 영상 제작 경험은 없었지만, “정확하고 이해하기 쉬운 전달”이라는 목표 하나만큼은 누구보다 확실했죠.&lt;/p&gt;
&lt;h2 id=&quot;-ai-툴-평가-qa-관점의-의사결정-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-ai-%ED%88%B4-%ED%8F%89%EA%B0%80-qa-%EA%B4%80%EC%A0%90%EC%9D%98-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot; ai 툴 평가 qa 관점의 의사결정 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;🎬 AI 툴 평가: QA 관점의 의사결정 과정&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;교육-영상-제작-누구나-겪는-현실적-장벽&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B5%90%EC%9C%A1-%EC%98%81%EC%83%81-%EC%A0%9C%EC%9E%91-%EB%88%84%EA%B5%AC%EB%82%98-%EA%B2%AA%EB%8A%94-%ED%98%84%EC%8B%A4%EC%A0%81-%EC%9E%A5%EB%B2%BD&quot; aria-label=&quot;교육 영상 제작 누구나 겪는 현실적 장벽 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;교육 영상 제작, 누구나 겪는 현실적 장벽&lt;/h3&gt;
&lt;p&gt;기존 방식으로 교육 영상을 제작하는 데에는 상당한 어려움이 따릅니다. 긴 제작 일정부터 막대한 비용, 전문 기술 요구 사항까지 효과적인 교육 자료 제작의 장벽은 압도적으로 느껴질 수 있습니다.&lt;/p&gt;
&lt;p&gt;스크립트 작성, 소스 제작, 촬영, 편집, 후반 작업 등 전통적인 영상 제작은 각 단계마다 막대한 시간과 리소스를 요구합니다. 특히 저희처럼 전문 영상팀이 없는 조직은, 이러한 리소스 제약 때문에 외주 제작을 고려할 수밖에 없습니다. 하지만 전문 제작사에 의뢰하는 경우 15분 분량 교육 영상 하나에 300만원에서 500만원, 제작 기간만 최소 2주에서 한 달이 소요되는 것이 일반적입니다.&lt;/p&gt;
&lt;p&gt;저희처럼 전문 영상팀이 없는 조직의 경우 상황은 훨씬 더 복잡합니다. 전문 장비나 전문 지식 없이 고품질 영상을 제작하는 것은 거의 불가능에 가깝습니다. 저희 QA팀도 예외는 아니었습니다. 인시던트 교육의 중요성은 누구보다 잘 알고 있었지만, 아래와 같은 현실적인 제약이 발목을 잡았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영상 제작 경험이 전무한 QA 엔지니어 2명&lt;/li&gt;
&lt;li&gt;제한된 예산 (외주 제작은 선택지에서 제외)&lt;/li&gt;
&lt;li&gt;촉박한 일정 (2025년 9월까지 새 기준 전파 필수)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이러한 리소스와 확장성의 격차는 중요한 문제입니다. 영상은 학습을 위한 강력한 도구이지만, 일관성 있는 고품질 교육 콘텐츠를 제작하는 데 필요한 노력으로 인해 조직이 그 잠재력을 완전히 실현하지 못하는 경우가 많습니다. 바로 이 지점에서, AI가 게임 체인저가 될 수 있다고 판단했습니다.&lt;/p&gt;
&lt;h3 id=&quot;ai-툴이-바꾸는-콘텐츠-제작의-패러다임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ai-%ED%88%B4%EC%9D%B4-%EB%B0%94%EA%BE%B8%EB%8A%94-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%A0%9C%EC%9E%91%EC%9D%98-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84&quot; aria-label=&quot;ai 툴이 바꾸는 콘텐츠 제작의 패러다임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;AI 툴이 바꾸는 콘텐츠 제작의 패러다임&lt;/h3&gt;
&lt;p&gt;생성형 AI의 발전은 영상 제작의 진입 장벽을 극적으로 낮추고 있습니다. 전문 영상 제작자나 편집자 없이도, 고가의 장비 없이도, AI 툴의 조합만으로 방송 수준의 교육 콘텐츠를 만들 수 있는 시대가 열린 것입니다.
하지만 현실은 그렇게 단순하지 않았습니다. 시중에 수십 가지 AI 영상 툴이 넘쳐났지만, 이렇게 길이가 길고 메세지가 많은 콘텐츠에는 &quot;어떤 툴을 어떻게 조합해야 하는가&quot;에 대한 검증된 가이드는 찾기 어려웠습니다. 그래서 우리는 회사에 양해를 구하고 직접 실험하기로 했습니다.&lt;/p&gt;
&lt;h3 id=&quot;8가지-조합을-테스트한-2주간의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#8%EA%B0%80%EC%A7%80-%EC%A1%B0%ED%95%A9%EC%9D%84-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%9C-2%EC%A3%BC%EA%B0%84%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;8가지 조합을 테스트한 2주간의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;8가지 조합을 테스트한 2주간의 여정&lt;/h3&gt;
&lt;p&gt;영상 제작을 결심한 뒤 첫 번째 과제는 바로 툴 탐색과 조합 전략 수립이었습니다. 말씀드린대로 우리는 테스트 자동화와 품질 검증에는 익숙하지만 영상 편집은 처음이었습니다. 하지만 소프트웨어 툴을 평가하는 방법론은 알고 있었죠. 짧은 시간 안에 효율적으로 제작할 수 있으면서도 전달력이 뛰어난 조합을 찾는 것이 핵심이었습니다.&lt;/p&gt;
&lt;h4 id=&quot;1단계-후보군-선정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EB%8B%A8%EA%B3%84-%ED%9B%84%EB%B3%B4%EA%B5%B0-%EC%84%A0%EC%A0%95&quot; aria-label=&quot;1단계 후보군 선정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1단계) 후보군 선정&lt;/h4&gt;
&lt;center&gt;
     &lt;div style=&quot;width: 550px !important; max-width: 100%; margin:auto&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/10a7bae9060a92a104253e87ee17e7fc/7a2a7/ai_tools.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAADI0lEQVR42iWTSW/bVhSF9au6SJGgw4/povuiQRZFAadt2k0KB6jnppYNR7JmWaRIiRIpiRopU6I4a3CqDov2R3y9cRcH7z3gvfPuOffczB/plH/3S/7ZL9hvJ/y+HbNb28SxSRAZrKI2y7jFItK4jzWcSGUWq4zDOyy3xDCsYQdVJskd01Qh8/fG4dXBc15++xW2WUa9u8Q08sRRFz9s40UtlpH+SOilLfydgffQwfaq/Jp/Tal1zlX1DdaiiLNpktlvxvxw8DWvvn/OwCyiq7/hOnW22x7p1iTedglSA39t0B3mucz/jGJlqRpvGXg1ZonKNFYeMUuEcBNbItNmvxsK+ty7YxRNJ397wtXNEVlBTc0Sbg06do7rwiFF5ZTbu2P6bpl5ookFTeapjiP7TCKyPiAOW2xinfm9gd5poBvXtM0cWueG3qiEl+gEG7n3IBXvOgTvTVbyibf5H4u0jZu0yETiTRCK2b6GudQZ+k3itUa0Fu8CldGsTH9cwrRvsWT1fAUz7HA+UGmo5+SrJ1SUC9xAZ5UYZFZhQx42aLkNriYqxZmKFyi4q7pIz/Lk44/45NOnPH32hM8+f8ZoWsOTrjsrjflSfFsoOLL6sUGQdMgsw7oQ1vAFaVwnlHMgRkepxnhWpNY4p1o/o1I7Q5GGzV1F4tQikmrStCOx6rEIeqzXXcK4S2axEhmSo5UQrcIKWr9Ns1GhrpxQLB0zEpmxVBTHbXlkst30HhGH4t3KpDe1sSa2WNGXgkwynif5cXJYvSyBXxYJGgtXwivVTSZFolCnWjnm6JcD8u/ecHL0HTfXhzysLXabPs7cYrHoE/oDtnFPPFwWmM/ecXb6EkWi0O2cU6kcYrTeYnYuSaRpzqTMaFBgNiozsStEXhO7q/PNiwuZsDF/7Ua8TwbsogGZwCvIZEjasz9ynf0JQz+lVHiNLllrNy8IvQbrD5GSMXxILebjGoNujpvLC7784oBht0Cvdcta5O+TkUi+z7Gc51iI7KWTJ1iWSPwasX9HKt2OhDDyVGJPE2IDZ1hBr18y7ZVwZW+3i8ysGluvx5/JmP8A9eBKvqY4VZYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/10a7bae9060a92a104253e87ee17e7fc/263a4/ai_tools.webp 480w,
/static/10a7bae9060a92a104253e87ee17e7fc/a6361/ai_tools.webp 960w,
/static/10a7bae9060a92a104253e87ee17e7fc/0b34d/ai_tools.webp 1920w,
/static/10a7bae9060a92a104253e87ee17e7fc/a552e/ai_tools.webp 2268w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/10a7bae9060a92a104253e87ee17e7fc/9aebd/ai_tools.png 480w,
/static/10a7bae9060a92a104253e87ee17e7fc/a91f8/ai_tools.png 960w,
/static/10a7bae9060a92a104253e87ee17e7fc/ac7a9/ai_tools.png 1920w,
/static/10a7bae9060a92a104253e87ee17e7fc/7a2a7/ai_tools.png 2268w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/10a7bae9060a92a104253e87ee17e7fc/ac7a9/ai_tools.png&quot; alt=&quot;ai tools&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;AI 영상 제작 툴 비교&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;먼저 시장에서 주목받는 AI 영상 제작 툴들을 비교했습니다.
가격은 선택 과정에서 중요한 판단 요소였기 때문에, 2025년 여름(7월 기준) 가격을 조사해 아래 표에 정리했습니다.
이후 정책 변경이나 업데이트에 따라 변동될 수 있다는 점도 함께 고려했습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;도구&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;특징&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;비고&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;가격(월)&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Synthesia&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;글로벌 기업용 AI 아바타 영상 제작, 다국어 지원 강점&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;자연스러운 진행자형 영상 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~89&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Vyond&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;캐릭터 기반 애니메이션 연출, 스토리텔링에 유리&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;다양한 상황 연출 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~99&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Gemma&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;텍스트 기반 슬라이드 제작 도구로, 콘텐츠 콘셉트 시각화에 용이&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;빠르고 간단하지만 연출 한계가 있음&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~25&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Vrew&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;자막/컷 편집 지원, 영상 흐름과 템포 조절 가능, AI 목소리 학습 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;커스터마이징 자유도 높음&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~28&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;AI STUDIOS (DeepBrain AI)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;실제 인물형 AI 아바타가 자연스럽게 설명&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;교육 영상의 강의자 역할 구현에 적합&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~69&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Veo3&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;챕터 기반 영상 자동 생성, 이미지와 텍스트 기반 스토리 제작 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;깔끔하고 다양한 스타일 영상 제작 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~35&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Hailuo&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;슬라이드 기반 교육 콘텐츠 자동 영상화, 자막+음성+BGM 포함&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;문서-영상 전환에 용이&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~35&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Midjourney&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;고품질 이미지 생성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;영상 썸네일, 배경, 캐릭터 컷 활용에 적합&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~35&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Runway&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;영상 클립 편집, 음성 변조, 스타일 전환 등 생성형 영상 편집 AI&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;영상 흐름 보정, 효과 추가에 유용 (후반 작업용)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~35&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Heygen&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;실제 인물형 AI 아바타, 다국어 리핑 싱크 우수&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;글로벌 온보딩/리더 대상 영상에 효과적&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~29&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Hedra&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;캐릭터 + 배경 + 내레이션을 AI가 자동 생성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;캐릭터 기반 설명 영상에 적합, 스크립트 중심 제작에 강점&lt;/td&gt;
     &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~30&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Capcut&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;직관적 인터페이스의 영상 편집 툴! 템플릿 다양&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;초보자도 쉽게 클립 편집 가능, SNS 스타일 영상 연출에 유리&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~14&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;ChatGPT&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;스크립트 작성, 슬라이드 구조 설계, 내레이션 대본 자동화, 이미지 생성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;다른 툴과 연계해 전체 영상 설계 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;$0~3&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;h4 id=&quot;2단계-평가-기준-수립&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EB%8B%A8%EA%B3%84-%ED%8F%89%EA%B0%80-%EA%B8%B0%EC%A4%80-%EC%88%98%EB%A6%BD&quot; aria-label=&quot;2단계 평가 기준 수립 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2단계) 평가 기준 수립&lt;/h4&gt;
&lt;p&gt;QA팀답게 명확한 평가 매트릭스를 만들었습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;구분&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;한국어 자연스러움 (30점)&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;시각적 완성도 (25점)&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;작업 효율성 (20점)&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;확장성 (15점)&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;비용 효율 (10점)&lt;/th&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;구분&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;- TTS 발음 정확도 &lt;br&gt; - 억양 및 호흡 &lt;br&gt; - 전문용어 처리 &lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;- 아바타 자연스러움 &lt;br&gt; - 립싱크 정확도 &lt;br&gt; - 화면 구성 자유도&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;- 학습 곡선 &lt;br&gt; - 협업 가능 여부 &lt;br&gt; - 렌더링 속도&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;- 구독료 &lt;br&gt; - 사용량 제한 &lt;br&gt; - 무료 기능 범위&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;- 향후 콘텐츠 제작 재사용 가능성 &lt;br&gt; - 템플릿화 가능성 &lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;h4 id=&quot;3단계-실전-테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EB%8B%A8%EA%B3%84-%EC%8B%A4%EC%A0%84-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;3단계 실전 테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3단계) 실전 테스트&lt;/h4&gt;
&lt;p&gt;하나씩 테스트하며 평가해보니 각각의 장단점이 확실했습니다. 그리고 실제로 몇장을 샘플로 만들어보니 단일 툴보다는 3개 내외의 툴을 조합해 제작 효율을 높이는 방식이 현재 상황에서 가장 적합하다는 결론을 내리게 되었습니다. 그래서 아래 두 가지 조합으로 데모 작업에 착수했습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 936px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ad93010e567e76d8e52259bfc6986f64/6d612/aitoolmixture.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABc0lEQVR42lVSB3KEMAzk/y9M5kIOOHq36XWjFUcmgdG4yVskO10/oh8mREmBJCsRpzKmJYzt0bQdagljehkNgjBFGOfIilrzOa9qo3mN6TAIjmO7AfOy4hVlcL0Iedm8gSxaAWJi1w8gsS+Aui9kZdWibiysnHHPdj3meYFDhmGc5HKHbz9WYKos5MLfbz8OVfUMrpwwydENowIecsbvOE44x3nqomoMPh4eHu4LaV6polPO7mBeEKX4fPj4eoZKOC8L9v2QswuQeQ5ZeonWSh1EOu1NIn0/dgVh8OdeLLWltXGaf4HON9B5K6Rl2l3WFeu2Y9t3AXsn3OpkTQck5Zr2GcxlLOumfeDcIeO6bZpM4Lxo1C7rQgIGLxvJc8UqG+d6oXbcfyXwJIqqEUKrohw2ZJNLrFsQZpcScz0DztlJzvka6KasWxTyEvisqGgVdf+aYkUNkcmQCSgBGLzA/btG6sJcdaYrPh0S1Y3RPTaJtf0BLoCz4KRU+mEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ad93010e567e76d8e52259bfc6986f64/263a4/aitoolmixture.webp 480w,
/static/ad93010e567e76d8e52259bfc6986f64/a7d94/aitoolmixture.webp 936w&quot; sizes=&quot;(max-width: 936px) 100vw, 936px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ad93010e567e76d8e52259bfc6986f64/9aebd/aitoolmixture.png 480w,
/static/ad93010e567e76d8e52259bfc6986f64/6d612/aitoolmixture.png 936w&quot; sizes=&quot;(max-width: 936px) 100vw, 936px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ad93010e567e76d8e52259bfc6986f64/6d612/aitoolmixture.png&quot; alt=&quot;aitoolmixture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;AI 툴을 조합해 최고의 방법 찾기&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;두 조합을 사내 동료 5명에게도 보여준 후 피드백을 받았는데, 더 우수한 품질의 조합B가 제작 시간은 더 걸리지만 교육적 효과는 압도적으로 높았습니다. 이번 교육 영상의 목적이 &quot;구성원이 인시던트 프로세스를 정확히 이해하는 것&quot;이므로 최종적으로 조합 B를 선택했습니다.
확실히 이 2주간의 실험을 통해 &quot;AI는 만능 도깨비라, 알아서 다 해준다&quot;라는 환상에서 벗어나 단일 AI 툴의 한계를 깨달았습니다. 툴마다 강점이 다르다 보니 전략적 조합이 필수였습니다. 비록 제작 시간에 투자가 필요하긴 했지만 한번 조합을 확정하면, 이번에 구축한 파이프라인으로 향후 모든 테크팀 교육 콘텐츠에 활용할 수 있을 거라는 확신이 들었습니다.
이제 이 조합으로 최종 영상을 구체적으로 어떻게 만들었는지, 각 단계별로 그 과정을 상세히 공유하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;️-제작-파이프라인-ai-툴-조합으로-완성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%A0%9C%EC%9E%91-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-ai-%ED%88%B4-%EC%A1%B0%ED%95%A9%EC%9C%BC%EB%A1%9C-%EC%99%84%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;️ 제작 파이프라인 ai 툴 조합으로 완성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;⚙️ 제작 파이프라인: AI 툴 조합으로 완성하기&lt;/strong&gt;&lt;/h2&gt;
&lt;center&gt;
     &lt;div style=&quot;width: 550px !important; max-width: 100%; margin:auto&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/18426752c4ed61fe3c7f39e2ceb42ed6/97620/makingflow_new.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.58333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAADnklEQVR42j2Uy3LaBhSGeaROp05zWXbZPkOfoF131860mfQSx3Ecg2PsGMxFCJAEiIu4CgzibgzGIIHtJnbtZd/h64kn08U/aFh859f/nyPf/dWYf+/m3K77/O05XLptVm6LldfC9WzmXo2ZW2XqVjhbVRh7FiO3yNArMVqLLj+p+KChPPuGJ3m2N3/jeunQtnX0VJB4dIemncTpGijpPQ6jW2jmPu2RTlTbZV/ZYu/4D5x5ltMrS0DFz2ABaokgjx9v4M5sOk2dRi2Jng5SLIQYzyy6jRi1bACzFuFkqNNyVOzcLsmiDJhmBFj+7NR6kE+J+Xn06Evm0xrNuoquBQmHtlCTuzQdDTN3QDzykrCyTa2doNyMkYhvEoy+pD5IMbmqMlpZjNdlJpdVfKVimOfPf+J8WqZaiWKaRyiJHbL5IM1OkmzxiISxh6r7aQ80CrVj1Nw7Iqk3tMc6y49NLj40cW9OmF838PX7Gq1misWygreq4a6qLNdVZssS7V4Ks3SIIfByI4ozMsiV3qObQYzCAd1JFsuOUaqG0XL7DM7y+NTkDk+ebjC7KLKQ5uauxYVXZnpRwO6oqMY7QpFNtMyeDEiT1MWd8oYj+c8ZGJjWMbFciIPoNm2JyBeWLJ49+5rTWY4Lt8D5siBgyWRi0BlmaPSz5MwDzMIh5apEImVlzEPCkS2aLZVKI0WooHCsHVCvK/jiiVd8+903TGYZFgL8JFdCns3zFOS1DH2Po5CUEt6k3U5Tqymoqp9UMoDjGFilKOGDv4hIkSetNL5OJ8JorHE6SdEfJOj2RN0Eo5HGYJDGtuNSVoSWnWAyzuO0NWzZhmY9yWxiMR1bnA6KIjFyXseXN/38+MP3DAeqTN3iOPw7e4FfyMnudSXDRHybiLgzM0FGvQxJZVfW6A2KaDGtcnPV5eO6y+1Vn5t1D99x6AVPn3wlk6MU8wGswj4p9bUA/ExPddR4CP/OLo1KiH7HYNw3cZoapWyI+bgsJ9vjg5zsjSfQlQDD75/zaOMLuu0IRmqbaPhPDvd/FSevBKCSTuwSDLwgk/LTk4yclkYlHyWTDHI+tLi77HPr9fhn1edO5Cvm3hJ4+zOTgUJPoFU5qWIuwEk9LE5iWNJwXlanJBczHZqMuzn6LQO7pOCd1bn/BPL63HuDB/nOhjHJIiX2E5yPVRZnaZHGcmrIfZusZT+vFmX5eFTlt8b1hc3HxQm3yw53bu9B9+7gf/0HzAGnWdnhkk8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/18426752c4ed61fe3c7f39e2ceb42ed6/263a4/makingflow_new.webp 480w,
/static/18426752c4ed61fe3c7f39e2ceb42ed6/a6361/makingflow_new.webp 960w,
/static/18426752c4ed61fe3c7f39e2ceb42ed6/0b34d/makingflow_new.webp 1920w,
/static/18426752c4ed61fe3c7f39e2ceb42ed6/6c245/makingflow_new.webp 2358w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/18426752c4ed61fe3c7f39e2ceb42ed6/9aebd/makingflow_new.png 480w,
/static/18426752c4ed61fe3c7f39e2ceb42ed6/a91f8/makingflow_new.png 960w,
/static/18426752c4ed61fe3c7f39e2ceb42ed6/ac7a9/makingflow_new.png 1920w,
/static/18426752c4ed61fe3c7f39e2ceb42ed6/97620/makingflow_new.png 2358w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/18426752c4ed61fe3c7f39e2ceb42ed6/ac7a9/makingflow_new.png&quot; alt=&quot;makingflow new&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;영상 제작 플로우&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;구분&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Step 01&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Step 02&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Step 03&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Step 04&lt;/th&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;제작기간&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2주&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1주&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1주&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2일&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;참여인원&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2명&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2명&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2명&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1명&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;작업 시간 (1인)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;6시간&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2시간&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;3시간&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2시간&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;사용 툴&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;수기&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;AI STUDIOS&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Vrew&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;AI STUDIOS&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;최종 산출물&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;126페이지 PPT&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;AI 아바타&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;AI로 목소리 삽입&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;인사장면 삽입&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;h3 id=&quot;ppt-제작-콘텐츠-구조화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ppt-%EC%A0%9C%EC%9E%91-%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B5%AC%EC%A1%B0%ED%99%94&quot; aria-label=&quot;ppt 제작 콘텐츠 구조화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;PPT 제작: 콘텐츠 구조화&lt;/h3&gt;
&lt;p&gt;여러가지 방식을 시도해본 결과, 결국 정보 전달에 가장 적합한 도구는 PPT였습니다. 회사에서 이미 사용 중인 공식 템플릿이 있었고, 로고 라이브러리와 디자인 가이드가 정리되어 있어 일관성 확보와 함께 대외 공유에도 즉시 활용 가능한 수준이었습니다. AI로 PPT 초안을 생성하는 Gamma도 테스트해봤지만, 결과물이 우리의 요구사항과 맞지 않았습니다. 올리브영의 브랜드 컬러, 폰트, 레이아웃 가이드를 AI가 완벽히 따르기는 어려웠고, 결국 수동으로 하나씩 만드는 것이 더 빠르고 정확했습니다. 그러다 보니 디자인 감각이 부족한 저희에게 PPT로 제작된 회사 템플릿은 최고의 파트너였습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;챕터&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;내용&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;핵심 메세지&lt;/th&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1. 인시던트와 레벨 선언&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;인시던트 정의, 선언시점, 초기선언 원칙&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;1시간 이내 선언, 영향도가 불확실하면 최대 손실 기준으로 선언&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;2. 인시던트 레벨 정의&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Level 0~3 개요 및 선언 기준 요약&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;각 레벨의 핵심 판단 기준 숙지&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;3. 온/오프라인 판단 기준&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;세부 기준 제시 (시간, 손해금액, 건수 등 영향도)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;명확한 수치 기준으로 즉시 판단&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;4. 보고 체계 원칙&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;초기 보고, 최종 보고, 보고자/피보고자 역할&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;레벨별 보고 프로세스 준수&lt;/td&gt;
  &lt;/tr&gt;
   &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;5. 재발방지대책&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;후속관리 절차, 조치기한, 추적 방법&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;인시던트는 종료가 아닌 개선의 시작&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;그런데 PPT를 영상으로 변환할 때 가장 큰 난관이 발생했습니다. 바로 애니메이션 처리 부분이었습니다. 일반적인 PPT에서는 한 슬라이드 안에서 텍스트나 이미지가 순차적으로 나타나도록 애니메이션을 설정합니다. 하지만 AI Studios나 Vrew 같은 영상 툴은 각 슬라이드를
하나의 정적 이미지로만 인식합니다. 그래서 애니메이션마다 슬라이드를 분할해 강조 포인트를 추가하니 원래 45페이지였던 PPT가 126페이지로 증가했습니다.😱 한 장면의 강조를 위해 슬라이드 한 장을 더 만드는 고통… 공감하시죠? 하지만 영상에서 시청자의 시선을 정확히 유도하기 위해서는 필수적인 과정이었습니다.
이렇게 완성된 PPT는 그 자체로도 훌륭한 교육 자료였지만, &quot;몰입력 있는 교육&quot;으로 만들기 위해서는 시각적, 청각적 요소가 필요했습니다.&lt;/p&gt;
&lt;h3 id=&quot;시각화-및-아바타-합성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EA%B0%81%ED%99%94-%EB%B0%8F-%EC%95%84%EB%B0%94%ED%83%80-%ED%95%A9%EC%84%B1&quot; aria-label=&quot;시각화 및 아바타 합성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시각화 및 아바타 합성&lt;/h3&gt;
&lt;p&gt;단순한 나레이션이 깔린 슬라이드쇼보다, 사람이 설명하는 느낌을 주는 영상이 집중도를 훨씬 높인다는 것은 많은 연구에서 입증되었습니다. 이를 &quot;Personification Effect(의인화 효과)&quot;라고 합니다. 하지만 실제 사람이 126페이지 분량을 촬영하려면 스튜디오 대관, 전문 촬영 장비, 전문 강사 또는 아나운서 섭외, 지원 인력 투입, 편집, 시사 등 여러 제약이 존재하죠. 그런데 AI 아바타가 있으면 촬영 없이 스크립트만으로 생성이 되고, 수정이 간편하며, 일관된 톤과 표정을 유지할 수 있습니다. 필요하다면 다국어 지원도 가능해 영어 또는 중국어 등 다양한 버전으로 제작도 가능합니다.&lt;/p&gt;
&lt;p&gt;물론 시중에 좋은 AI 영상 툴이 많았지만 저희 콘텐츠에는 치명적인 문제가 있었습니다. 외국인 아바타에 한국어 음성을 입히면 어색하고 립싱크 또한 불일치하여 한국인과 비슷한 아바타를 보유한 툴이 많지 않았습니다. 애니메이션풍 캐릭터도 많았으나 급격한 몰입감 저하로 선호도가 낮았으며, 대부분 한국어 발음이 부자연스럽고 전문용어를 오독하기도 했습니다.&lt;/p&gt;
&lt;p&gt;반면 AI Studios(DeepBrain AI)는 다른 툴에서 아쉬웠던 문제를 대부분 해결했습니다. 실사 기반 한국인 아바타 4종 제공, 발음 정확도 90% 이상 체감되는 한국어 TTS 최적화, 립싱크 자동 매칭, 공동 작업이 가능한 편집 기능 등이 만족감을 높였습니다. 특히 협업 기능은 126개 슬라이드를 빠르게 처리하는 데 결정적이었습니다. 하지만 단점 역시 존재했습니다. 예를 들어 “Level 1”을 “레벨 원” 대신 “레벨 하나”로 읽는 경우가 있어, 영어와 숫자는 모두 한글로 변환해야 정확한 발음이 가능했습니다. 또한, 효과음을 지원하지 않아 강조가 필요한 구간은 다른 툴의 도움을 받아야 했습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1535px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/91db1ae805075ea71aa6871fbd6a203e/7383e/aistudiosex.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAADK0lEQVR42m2T62+TVRzHHzHRjJERDYZIiNsY8/KKF/If+JZ/wkRQhAEz0q0dlymRkMgmCmGQFrJlK6M31qeXzTJpF6es967Pennabh1jbL4wUaDdOrHtx8MzX4h6kk9+55wXn/P7npMjPcwFWS1GWZ4Pkpvzk1EmSSk+lOQEieQ4McVDJOkmrLgIKjLBlKgZNzMZl0DmviCUkwnnvYRz40jLCyGm/Q4CkzJPH6+wVvqFsqBUWuVJaYXHGo/4vbSssfprHlU0oD6Ioy7GBBECs3GmkneJ5YVwdSlM75lORkctojOF40c76OnW/4dTegPdJ3UE/AGej/X1CmtlQanM0zIk1CDhrBdpafEnTvd04PF4GR4a4tUtL7HrjR3s2N7Ea9saN2lsFOvtvCxJnD937m/hOhsbFSqVdW2dmQ8RzYgOi8UpDIZPkV1ubBYLu3fu5P19+3ivfS9tzW+xt7lZ4922Nl4X0m/6+zRBtVrVar1e12q2ECaR9iEVij/QpT+E0+nCbrVonbyzZw/trS283dpKe0uLRuvuXexvfpML57/6X6FaiJBM30XKLfjoNhzSIg/evIkkYj2P2dTQQNPWBjHfxitbJA58cADzhQFkq6wJarXaC8K8EM6lJ4VwfpzPdR8yPGymuLDAxa/7MF4b4PrVqxrG69e4/N0lojOzVMXlP/mtwh/VTck/hYVClEz6HlI+70EnhEaj6YUo/x71eo1a/U+eVWuU1yqbDyJYE49T2XhGQY2QTweQ1KxMb+8RDn98gotXzqLTn+DwwaPour8UfMHBjz7BoD/NsSMd6Lt66Dz+GWdOnaXzWCe6k130918iHleIh/wsqtNIqaQN2fktg6Y+bI7LmM19WEYGuDVqwm4bwjx0Bad9kFGx57DcwHrLiOwYxuUYwTM2gt83Rnp2hmxiiiX1PpIStzCXsJGM3WY2cltUG0rcjjXoxeM14XUOMOE28r3bJLhBwDdCPnWPgvim86kpCoogGeChkC2rM0IoRErUghKzMhe3CeykEndEDFkccEcwRjLqIhUfJ5OYQE1Ospj5kQeZaZayP2uSFfFLHmWDrGRC/AUUkxlFyWfgZAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/91db1ae805075ea71aa6871fbd6a203e/263a4/aistudiosex.webp 480w,
/static/91db1ae805075ea71aa6871fbd6a203e/a6361/aistudiosex.webp 960w,
/static/91db1ae805075ea71aa6871fbd6a203e/33007/aistudiosex.webp 1535w&quot; sizes=&quot;(max-width: 1535px) 100vw, 1535px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/91db1ae805075ea71aa6871fbd6a203e/9aebd/aistudiosex.png 480w,
/static/91db1ae805075ea71aa6871fbd6a203e/a91f8/aistudiosex.png 960w,
/static/91db1ae805075ea71aa6871fbd6a203e/7383e/aistudiosex.png 1535w&quot; sizes=&quot;(max-width: 1535px) 100vw, 1535px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/91db1ae805075ea71aa6871fbd6a203e/7383e/aistudiosex.png&quot; alt=&quot;aistudiosex&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;AI STUDIOS를 사용한 영상 제작과정&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;음성-클로닝-및-후처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%8C%EC%84%B1-%ED%81%B4%EB%A1%9C%EB%8B%9D-%EB%B0%8F-%ED%9B%84%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;음성 클로닝 및 후처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;음성 클로닝 및 후처리&lt;/h3&gt;
&lt;p&gt;국내 스타트업 VoyagerX가 개발한 AI 영상 편집 툴 Vrew를 최종 편집 툴로 선택한 이유는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;1️⃣ AI 보이스 학습 기능: 실제 사람의 목소리를 학습해 TTS로 생성 가능
&lt;br&gt;2️⃣ 효과음 및 배경음악 라이브러리: 약 980종 효과음, 270종 배경음악 무료 제공
&lt;br&gt;3️⃣ MP4 파일 병합 및 편집: AI Studios에서 만든 여러 영상을 하나로 합치기
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;무엇보다 &quot;CTO 등 실제 구성원의 목소리로 교육 영상을 만들 수 있다&quot;는 점이 결정적이었습니다. 왜 구성원의 목소리, 특히 그 중에서도 CTO 목소리가 필요했을까요? 사실 영상 제작 초기부터 어떻게 하면 구성원들이 이 콘텐츠를 끝까지 집중해서 볼지 고민이 컸습니다. 그런데 인시던트 관리는 기술총괄자 CTO님이 가장 중요하게 생각하는 영역입니다. 만약 그러한 CTO님이 직접 설명한다면, 구성원들은 &quot;이건 정말 중요한 내용이구나&quot;라고 인식할 것입니다. 이를 조직 심리학에서는 &quot;Authority Principle(권위 원칙)&quot;이라고 합니다. 하지만 현실적으로 CTO가 15분 분량을 직접 녹음하기는 어렵습니다. 스크립트 숙지, 일정 조율, 촬영 준비, NG를 포함한 실제 녹음 등에 투입해야 할 시간이 너무 많기 때문입니다.&lt;/p&gt;
&lt;p&gt;그런 상황에서 Vrew의 음성 학습 기능이 이 문제를 해결했습니다. 단 10분의 녹음으로, 수십장 분량의 나레이션을 CTO 목소리로 만들 수 있었습니다. 이에 CTO님께 Vrew에서 제공하는 30개 표준 문장을 읽어주시면 AI가 음성을 학습해 영상에 적용할 수 있다고 요청했고, 흔쾌히 승낙을 해주셔서 CTO 집무실에서 녹음을 진행할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 완벽해 보이는 Vrew에도 단점이 있었습니다. AI Studios처럼 하나의 프로젝트를 여러 명이 실시간으로 편집할 수 없어, 다음과 같이 작업을 분담했습니다. 각자 맡은 파트를 따로 작업한 후 각각의 영상을 합치는 방식으로 진행했습니다. 이렇게 병합하면 하나의 통 파일로 변환되기 때문에 이미지, TTS, 효과음 중 하나라도 수정이 필요하면 처음부터 다시 만들어야 했습니다. (하나라도 틀리면 다시 처음부터… 절대 안돼😭) 따라서 병합 전 철저한 검수가 필수였습니다. 우리는 2인이 각각 2회씩, 총 4회의 전체 시청 검수를 진행했습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/364b9b77eb18f195e70efb23d5972fb6/07f18/vrew.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 41.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAACBElEQVR42jWQ32/SUBTH+2Z8MNEHow/+msnUf8AlvpiY+LB/TM2ybPFB/weNc9lM/DnGGIwt2xi0UKC0ZWMCgzmoDAptoe3HWxLPvZ/cc885+Z5zr1StpGieZmjVZVqNHM1GlkY9g2Gk0MxtSoKikUDV4+QrMXLlH8h6DMXcRDb+EydnxDjSfyLp5S2S6QTpgyQ76W32Ugl2d5L87XWxh31Gjo3jDnGcAcdWl0KtxJ9Ok/5wwFDEbVfkPZdzq0G28gtJVb8zP/+Sh4/u8+TxLDN37/Fs7iln7XN6A5vByBHCI7yJz0EmQ+nUpD5w6Y7GdLwQywvo+dC+7JLXNpByyjovns9x8/oN7ty+xbWrV5ideUCr3Z4K2ZGgwBlPOGudcdFpEAYh/mTCwPXwxmMCcb/sX1DQYkiyus6790ssLizwdnmJN69fsSzOXq+H7/tT0eEoerIQtvtYVmsqNvEnBCIfhgGR2UOLohZHUoprotCaBkOxIgvCENfzROcAL5rCdfGF77gOHfF/I8fFjeLeeNpUbNHonIqWQMoXVimLUXU9hW5EpDHNXY5PDqjVDtFOcuSrWQr5JEU1xUl1n8bvAs2IeoQqfJVaNYOpJYWgsoKc+yj4gCJ/Enwmr6wK1lDzXzhSvrIvf+Nwb4Xs4TqVYgxN3UAvbgriGKUtzFKCajnJsbbDP62wKg4VVF9hAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/364b9b77eb18f195e70efb23d5972fb6/263a4/vrew.webp 480w,
/static/364b9b77eb18f195e70efb23d5972fb6/a6361/vrew.webp 960w,
/static/364b9b77eb18f195e70efb23d5972fb6/0b34d/vrew.webp 1920w,
/static/364b9b77eb18f195e70efb23d5972fb6/9ef56/vrew.webp 2237w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/364b9b77eb18f195e70efb23d5972fb6/9aebd/vrew.png 480w,
/static/364b9b77eb18f195e70efb23d5972fb6/a91f8/vrew.png 960w,
/static/364b9b77eb18f195e70efb23d5972fb6/ac7a9/vrew.png 1920w,
/static/364b9b77eb18f195e70efb23d5972fb6/07f18/vrew.png 2237w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/364b9b77eb18f195e70efb23d5972fb6/ac7a9/vrew.png&quot; alt=&quot;vrew&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;vrew를 사용한 영상 제작과정&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;실사-삽입-및-최종&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%82%AC-%EC%82%BD%EC%9E%85-%EB%B0%8F-%EC%B5%9C%EC%A2%85&quot; aria-label=&quot;실사 삽입 및 최종 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실사 삽입 및 최종&lt;/h3&gt;
&lt;p&gt;영상 제작이 거의 완료된 시점에서, 우리는 한 가지를 더 고민했습니다. AI로 만든 영상은 물론 투자 대비 완성도가 높지만, 뭔가 &apos;사람의 온기&apos;가 부족하지 않을까 하는 고민을요. YouTube 분석에 따르면, 시청자는 첫 10초 안에 영상을 계속 볼지 말지 결정한다고 합니다.
AI 아바타로 시작하는 것도 나쁘지 않지만, 실제 CTO가 직접 등장해 인사를 건네는 장면이 있다면 훨씬 강력한 인상을 줄 수 있고 그 첫 10초가 시청률을 결정한다고 생각했습니다.
이에 CTO님께 다시 한 번 협조를 구해, 약 10초 분량의 인트로 영상을 촬영했습니다. 그렇게 만들어진 실사 인트로와 Vrew에서 만든 영상을 합치는 작업은 Adobe Premiere Pro에서 진행했습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1535px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fd6777914693256e85b0a752c93eaaf4/7383e/adobepro.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAADYUlEQVR42jWSW1ObVRSG8w86VCCQfKfkI/maEEM4WJEZkfHWcRy9dpz+BA+0arXtQDnjWC/0ojrjSDuWo9hKhRaaEJKQBAggEL4cgZgEp6M/4nE3jhfPzdqzn73Wu7alkt+inEtSzid5XtnjeXWXv6opqpVtytUkZ9UExUqMQiVKvholdx4hU93ArIYxzzdIV9fZya+wnVuuYSnlNinl4xQzEX64O87E2A0mJ24yPv4lA4P9DI1cY/LOTSa+vsHgyFUGRvoZGLvK7cnPuDXaz1ff3SJhPiaVf8JObgVLMbPBnyebbEZ+QVM1bM3NSLIVRW3mck8PqqaLugPV0UKT5qKhvhGrtYkmQd2FiyiKwm/BKY5KIVLZp1hyZpCzYpRwaAZPmx+nz8DoMujs66LvzV7cbjeGpxXd5UZzCLGqYLdLyIosHpdx6W6Wnt3juLTBfnYNi2k+pVgMsR5+gNPjwuZ2oRoePIEuGkUXirj4sj+ArruQZRl3Swt+n5c2rx/Dq9De3cLy2s9kzyIcZoJYjo5/J1tYJRi+j0NTkWx2FEnCbmtGkRXRiYLP5xPCFhGFQmdHgO62LgI9Tt4dqOfKB36eJO6RP4mRzgrhH+lHmIVl1kJT4kUvDoeGrCnYVDuyKtXG/B9FZNzZ2UGr5qG17yVevVLHK687CR/cp1DYxMyGhPBoETP3mLXgj3jbe3BeasMtqbTrAbr8r6HJNhxCrGtSbfza0uz2WpaSYhO56wSD05ROE2TFgi0HBwtkMo94tvY9RqAHzddN2+X36H7jEwK97+MMvIW39x2UvrdRfR1oQqqoKqoi4hHLMVwG66szVE6TFMyIEO7Pkzl+SFAIFZcfq9rKRafOBdcl6pxu6h1eGvVW6nUfDZKGtUkSNNMkaGi0YhgGkeA856dbnGaiL4RzHB3Ms5ea5Zs71xke+piRwQ8Zvf0R48NXBf2Mi889MXyN0aEXfCpqnzMxcl3Uv+Dut2MU0uIv5xKUsnEsh3uzHO4Jqei0mF3ivLRCcHWRxbkpoqGfiG/MEFt/wE5ikWR0ge3YItvRX/m7nOKf8l6NipCVswkqAsvh7gz/McvBjpCn5tiOzwnRNFuxaVLxeXbiC+xvPRTnS6R3VzB3Vzk5CnOajlBKx6iaCUGSc3OLfwF19nqdc6PplgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fd6777914693256e85b0a752c93eaaf4/263a4/adobepro.webp 480w,
/static/fd6777914693256e85b0a752c93eaaf4/a6361/adobepro.webp 960w,
/static/fd6777914693256e85b0a752c93eaaf4/33007/adobepro.webp 1535w&quot; sizes=&quot;(max-width: 1535px) 100vw, 1535px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fd6777914693256e85b0a752c93eaaf4/9aebd/adobepro.png 480w,
/static/fd6777914693256e85b0a752c93eaaf4/a91f8/adobepro.png 960w,
/static/fd6777914693256e85b0a752c93eaaf4/7383e/adobepro.png 1535w&quot; sizes=&quot;(max-width: 1535px) 100vw, 1535px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fd6777914693256e85b0a752c93eaaf4/7383e/adobepro.png&quot; alt=&quot;adobepro&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;Adobe Premiere Pro를 사용한 영상 제작과정&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;영상을 배포하기 전, QA팀답게 아래의 체크리스트를 기반으로 철저한 품질 검증을 진행했습니다&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;항목&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;세부 내용&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;결과&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;정확성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;모든 수치와 기준이 최신 가이드와 일치하는가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 검증 완료&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;일관성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;용어 표기가 통일되어 있는가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 통일 완료&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;가독성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;글자 크기와 적절한가, 아바타가 글자를 가리진 않는가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 조정 완료&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;음질&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;음성이 명확하게 들리는가, 잡음이 없는가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 문제없음&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;타이밍&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;나레이션과 화면 전환이 자연스러운가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 동기화 완료&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;효과음&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;효과음이 과하거나 부족하지 않은가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 적절함&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;오타/오독&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;자막 오타, 음성 오류 없는가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 2건 수정&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;재생&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;다양한 기기에서 정상 재생되는가&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;✅ 테스트 완료&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;h2 id=&quot;️-제작한-교육-영상을-구성원에게-전달하기까지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%A0%9C%EC%9E%91%ED%95%9C-%EA%B5%90%EC%9C%A1-%EC%98%81%EC%83%81%EC%9D%84-%EA%B5%AC%EC%84%B1%EC%9B%90%EC%97%90%EA%B2%8C-%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80&quot; aria-label=&quot;️ 제작한 교육 영상을 구성원에게 전달하기까지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;🧚‍♀️ 제작한 교육 영상을 구성원에게 전달하기까지&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;그렇게 영상을 완성하고 나니, 다음 고민이 생겼습니다. &quot;이걸 어디에 올려야 구성원들이 진짜로 잘 볼까?&quot; 이 질문에 대한 답으로 단순히 영상을 업로드하는 것만으로는 부족했습니다.
누가 시청했는지 확인할 수 있고, 적당한 &apos;강제성&apos;을 부여할 수 있는 플랫폼이 필요했습니다. 아무리 좋은 콘텐츠라도 &apos;자율 시청&apos;에만 의존하면 놓치는 사람이 생기기 마련입니다. 특히 인시던트 기준처럼 전 구성원이 반드시 숙지해야 하는 내용은 더욱 그렇습니다. 그래서 올리브영에서 사용 가능한 교육 콘텐츠 배포 채널을 비교했습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 648px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7fcccce65586436c20592fb2ca44f082/244f0/placetoupload.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuklEQVR42m1TiY6bUAzk/3+uUretdrtsQrjvJFyPdzG1TYLSqpbMMdh+M7YJPsIY46QOvw0zLkmJaVZYlJb7NK8o6x5V00OtBvOyYlm04FnZoekGeWY8yIoG1jkYu7s2Fv1tQJrXSPIK92Gi714KZ2UjnmQVcrob6zGPHdRcyrOxFoGj4Ffbtg1aG8SU9Ezkd+89znGOH+9fiOJCMAol3NBFSa73G4JxWmCIFSewO2ZrDBTJnYnVQC1gfKUCZd1Jsa6/SwEmY62Gd+uRG3AQn/Zq3IvPc4qf7yd8nhLBLLUjoTacogwhYcuys3L6jk03D4YewVPm0xnkXswyDIV11YIzw/MllwMikv4VpWj72573qMGMg4/wgjitXgpuJHdFQliUFBjGSXA+pCWpZdXSVK977MYMB7glhlM5FaQexmmJ2308BsI2jDO+vf3G918hQpI+LeqQxH3y3j3iCXOWLqTC6X0o+I+xVO5VUba0d3sg+7/2VPQ0KciyWAJ9/ksy43V7hZPp75J5Z7OiRkrObJmhpaF4pyRGesgM9rXZDudV4r+Fl7u7DhJojENRddJbLiwF2K15rNxe8A/JgVLSCCAz6wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7fcccce65586436c20592fb2ca44f082/263a4/placetoupload.webp 480w,
/static/7fcccce65586436c20592fb2ca44f082/ca6a2/placetoupload.webp 648w&quot; sizes=&quot;(max-width: 648px) 100vw, 648px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7fcccce65586436c20592fb2ca44f082/9aebd/placetoupload.png 480w,
/static/7fcccce65586436c20592fb2ca44f082/244f0/placetoupload.png 648w&quot; sizes=&quot;(max-width: 648px) 100vw, 648px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7fcccce65586436c20592fb2ca44f082/244f0/placetoupload.png&quot; alt=&quot;placetoupload&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;영상 업로드를 위한 플랫폼 비교&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;최종 선택한 &quot;올리브라운지&quot;는 올리브영 구성원만 이용할 수 있는 사내 커뮤니케이션 플랫폼으로 공지, 사내방송, 교육, 공모 등 다양한 콘텐츠를 공유할 수 있습니다.
특히 시청 현황 확인 기능이 있어 교육 이행률을 실시간으로 관리할 수 있어 선택에 결정적이었습니다.
또한 이번 교육은 인시던트에 직·간접적으로 영향을 미치는 구성원들을 대상으로 진행된 만큼, 높은 참여가 중요했습니다. 이에 시청 완료율 목표를 90%로 잡았고,
올리브라운지를 통해 안내와 독려가 자연스럽게 이루어지면서 최종 95%라는 기대 이상의 완료율을 달성할 수 있었습니다. 이러한 성과 덕분에 아래와 같은 긍정적인 보이스도 들을 수 있었습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1535px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8bc3b557f3ebd507038dfe80c52e1c25/7383e/olivelounge.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.62500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAAC5UlEQVR42j2TzW8bRRjG/W9w4QDlAOLGASEhDi1FiENV0aqcQOLSADe4Ug4gThw5cUDiAEgU05SkiZ06sZ1PO3bqeNfr/bS9XjtuEjuCpknq2Ovd/fF6S7ujn2ZGmnneZ55Xm9hzyuzZJTy7SNNYxzHyWEYOQ19B0++j6mkUM03VSPFAX6Bs3GNbKJrzwlw8l+3F/0mR8OwCbXsrpr9vMjhscHhgc9h3OOjb7PdNvK6C29PoDUy6fYPOoU77QMOTublfZcdeEtIxiZa9TtNcxWuWiKKQZ190ckrkdcBrM2k1OXcaBK4r6xbR+fnzc0HgU3fXY7GKkyHhWHlsI0urUSCSEUx8Th4dcfzvI44HRxz1BjL/w+R4wPDxY0Lfl2oRz0YQ+hjtTSr2fRRnhYRpZjCMJRxnLa5oWxbXrlzhxofXuXb1Kh9/cYnvvr1J5pef+PX3JNXSNgvzKRbnFwjDSAiw3C1Ua5manSNRl7A1Cdu0s7Ggqii8euFlXnnhApc+eYmPZl7km69usL1bIruc48fUAq9dfoPX336T8XAkHkNst4BmraDbeRI16ZhSn8Owl587/OC9y1x86yIzt97n+qfvcOuHz9nrttHrFjNzd3n3y5t89vX3jM9O42c33CK6lRNTqyRU/W8UbVYEM7H9Kf7Yl7ADouBp8GEYcnr2hJPTMyaS8XgUMpq6kyZOm+K2SlhmHkcanKiJmKr+hWmlCUUhksvj4VAORnFGQSD7kc/ofChiUjCI4mK+P3laLJjgiWDTXKNlbYigiE3R1FnaEm6nU6LjbtJpl+m6Ux7QbVd46JXl4o6sqzzsqPQ8hYOewX63hmttxrStLXly9U9qU5QkSuUPlN3bqNWkNGcWs3ybytpvFDaTFDfukL73MzuFWXGTw9GyNOo5mvpqLOTFFMShCNR2RXA3iVa9Q12E6spdDHUeU1vErKWEJSwtQ0PPyu+Zp2Ws4RobtE15iYh0Y4rsWUX+AypCNwphZaPdAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8bc3b557f3ebd507038dfe80c52e1c25/263a4/olivelounge.webp 480w,
/static/8bc3b557f3ebd507038dfe80c52e1c25/a6361/olivelounge.webp 960w,
/static/8bc3b557f3ebd507038dfe80c52e1c25/33007/olivelounge.webp 1535w&quot; sizes=&quot;(max-width: 1535px) 100vw, 1535px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8bc3b557f3ebd507038dfe80c52e1c25/9aebd/olivelounge.png 480w,
/static/8bc3b557f3ebd507038dfe80c52e1c25/a91f8/olivelounge.png 960w,
/static/8bc3b557f3ebd507038dfe80c52e1c25/7383e/olivelounge.png 1535w&quot; sizes=&quot;(max-width: 1535px) 100vw, 1535px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8bc3b557f3ebd507038dfe80c52e1c25/7383e/olivelounge.png&quot; alt=&quot;olivelounge&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;올리브라운지에 영상 업로드&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
    &lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
      &lt;p&gt;&lt;strong&gt;🧔🏻‍♂️ B님:&lt;/strong&gt; &quot;이번 교육 영상에서 가장 인상 깊었던 부분은 바로 CTO님의 목소리였습니다. 너무 자연스러워서 순간 &quot;진짜 녹음하신 건가?&quot; 싶을 정도였죠. 그 덕분에 전달력이 훨씬 강해졌고, 콘텐츠에 집중할 수 있는 몰입도가 한층 높아졌습니다. 게다가 헷갈릴 수 있었던 레벨 구분도 화면 구성이 깔끔해서 이해하기가 훨씬 쉬웠어요. 듣는 재미랑 보는 재미가 동시에 살아 있어서, 교육 영상이라기보단 잘 만든 콘텐츠 보는 느낌이었어요😀&quot;&lt;/p&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
      &lt;p&gt;&lt;strong&gt;👩🏼 S님:&lt;/strong&gt; &quot;단순히 테스트만 하는 팀을 넘어 조직 구성원들이 인시던트에 대해 공통된 이해를 갖게 하고 서비스 안정성 확보에 적극적으로 참여하도록 주도하려는 의지가 느껴지는 영상이었습니다. 모두가 문제의 심각성을 동일하게 인식하고 즉시 대응할 수 있는 좋은 가이드가 될 것 같습니다! 앞으로도 좋은 품질 문화를 널리 전파해주세요~!&quot;&lt;/p&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
      &lt;p&gt;&lt;strong&gt;👱🏼‍♀️ G님:&lt;/strong&gt; &quot;대부분의 교육 영상은 다소 단조로운 음성으로 인해 집중하기 어려운 경우가 많은데, 이번 영상은 CTO님 목소리가 직접 들어가 있어서 신선하고 더욱 기억에 남았습니다😆 더불어, 인시던트는 관련 담당자가 아니라면 실제 대응 과정을 체감하기 어렵지만, 교육 영상을 통해 프로세스를 구체적으로 이해할 수 있어 많은 도움이 되었어요.&quot;&lt;/p&gt;
    &lt;/div&gt;
&lt;br&gt;
&lt;h2 id=&quot;-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot; 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;🌱 마치며&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;이번 인시던트 교육 영상 제작은 올리브영 QA팀에게 분명 새로운 영역으로의 도전이었습니다. 테스트 케이스를 설계하던 손으로 대본을 다듬고, 리포트 검증에 집중하던 눈으로 영상의 컷 편집을 맞추는 과정은 낯설었지만, 그만큼 신선하고 즐거운 경험이었습니다. 이 경험을 통해 우리가 얻은 가장 큰 인사이트는 &apos;좋은 품질은 결국 이해에서 시작된다&apos;는 확신입니다. 딱딱한 문서나 구두 전달에 의존하던 인시던트 정의와 레벨 기준이 영상이라는 새로운 형태로 전환되자, 구성원들의 반응도 평소와는 달랐습니다. 복잡한 규정도 쉽고, 자연스럽게 조직 전체의 공통 언어로 자리 잡을 수 있다는 가능성이 엿보였습니다.&lt;/p&gt;
&lt;p&gt;또한, AI의 실용적인 힘을 체감하는 계기가 되기도 했습니다. AI 아바타, 음성 합성 기술은 촬영, 편집, 녹음 등의 기술적 장벽을 효과적으로 제거하여, QA 엔지니어가 직접 &apos;전달 방식의 혁신&apos;에 집중할 수 있는 환경을 만들어주었습니다. 하지만 중요한 것은 &quot;무엇을, 어떻게 전달할 것인가&quot;라는 콘텐츠 기획 및 스토리텔링 역량은 여전히 사람, 즉 우리의 몫이라는 점입니다. AI는 훌륭한 도구이지만, 전달하려는 품질의 본질을 설계하는 것은 QA팀의 역할입니다.&lt;/p&gt;
&lt;p&gt;이처럼 올리브영 QA팀은 이제 단순한 &apos;품질 검증&apos; 조직을 넘어, &apos;품질 문화 전파&apos;를 주도하는 조직으로 그 영역을 확장해 나가고자 합니다. 앞으로도 새로운 기술과 방식을 적극적으로 탐색하여, 서비스의 완성도를 높이고 모두가 공감하는 품질 문화를 만들어 가겠습니다. 다음번 QA팀의 도전도 많이 기대해주세요! 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[iframe으로 웹앱 통합했더니 토큰 요청이 폭발했다]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-12-08/iframe-postmessage-authentication/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-08/iframe-postmessage-authentication/</guid><pubDate>Mon, 08 Dec 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영에서 프론트엔드 개발을 담당하고 있는 그냥 잇츠테드입니다.&lt;/p&gt;
&lt;p&gt;저희 팀은 올리브영의 새로운 성장 동력인 리테일 미디어 사업 확대를 위해, 광고 파트너 오피스 시스템을 기존 플랫폼과 매끄럽게 통합하는 프로젝트를 맡았습니다.&lt;/p&gt;
&lt;p&gt;목표는 명확했습니다. 부모 애플리케이션에서 자식 애플리케이션을 iframe으로 임베드하되, 사용자가 두 번 로그인하지 않도록 인증을 공유하는 것이었죠.&lt;/p&gt;
&lt;p&gt;자식 애플리케이션에는 자체 인증 시스템이 없었기 때문에, 부모 애플리케이션의 인증 토큰을 안전하게 공유하는 방법이 필요했습니다.&lt;/p&gt;
&lt;p&gt;간단해 보였던 이 과제는 생각보다 복잡했습니다.&lt;/p&gt;
&lt;p&gt;동시에 쏟아지는 토큰 요청, 401 에러의 연쇄 폭발, 무한 재시도 루프까지. 이를 하나씩 해결하며 안정적인 인증 시스템을 만들어낸 과정을 공유합니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이런 분들께 도움이 될 거예요.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iframe을 사용한 마이크로프론트엔드 아키텍처를 구현하는 개발자&lt;/li&gt;
&lt;li&gt;크로스 오리진 환경에서 안전한 토큰 관리가 필요한 분&lt;/li&gt;
&lt;li&gt;401 에러가 동시 다발적으로 발생하는 문제를 겪고 계신 분&lt;/li&gt;
&lt;li&gt;postMessage API의 실전 활용 사례가 궁금한 분&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;&lt;/a&gt; API를 활용해 이 문제를 어떻게 해결했는지, 그리고 예상치 못했던 4가지 난관과 해결 과정을 공유합니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#tldr&quot; aria-label=&quot;tldr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;iframe 환경에서 postMessage를 활용해 안전한 토큰 기반 인증 시스템을 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;핵심 포인트:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 동시 10개 요청 → 1개로 최적화 (Request Queue)&lt;/li&gt;
&lt;li&gt;✅ Promise 공유로 401 에러 동시 처리&lt;/li&gt;
&lt;li&gt;✅ 쿨다운 메커니즘으로 무한 루프 방지&lt;/li&gt;
&lt;li&gt;✅ Origin 검증 + Rate Limiting으로 보안 강화&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;핵심 코드 (Promise 공유 패턴):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; handling401Promise&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handling401Promise&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 이미 갱신 중이면 같은 Promise 기다림&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; handling401Promise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 첫 401만 실제 갱신 수행&lt;/span&gt;
  handling401Promise &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;refreshTokensDirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;문제-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9&quot; aria-label=&quot;문제 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 상황&lt;/h2&gt;
&lt;p&gt;부모 애플리케이션에서 자식 애플리케이션을 iframe으로 임베드하는 프로젝트가 시작되었습니다. 자식 애플리케이션은 자체 인증 시스템이 없었기 때문에 부모 애플리케이션의 인증 시스템을 활용해야 했고, 저는 이 &lt;strong&gt;인증 통신 시스템을 구현&lt;/strong&gt;하게 되었습니다.&lt;/p&gt;
&lt;img src=&quot;/2e7965e9ea9e022448861e6402591e34/iframe-architecture.svg&quot; alt=&quot;iframe 통합 구조&quot;&gt;
&lt;p&gt;iframe으로 통합하면서 가장 먼저 마주한 질문은 &lt;strong&gt;&quot;어떻게 안전하게 인증 정보를 공유할 것인가?&quot;&lt;/strong&gt; 였습니다. 서로 다른 도메인에서 실행되는 두 애플리케이션 사이에서 토큰을 주고받아야 하는데, 보안을 해치지 않으면서도 안정적으로 작동해야 했습니다.&lt;/p&gt;
&lt;h3 id=&quot;초기-고민들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B4%88%EA%B8%B0-%EA%B3%A0%EB%AF%BC%EB%93%A4&quot; aria-label=&quot;초기 고민들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;초기 고민들&lt;/h3&gt;
&lt;p&gt;처음에는 간단하게 생각했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;그냥 URL 파라미터로 토큰 넘기면 되지 않을까?&quot;&lt;/li&gt;
&lt;li&gt;&quot;&lt;code class=&quot;language-text&quot;&gt;localStorage&lt;/code&gt;는 같은 도메인에서만 공유되니까 안 되겠구나...&quot;&lt;/li&gt;
&lt;li&gt;&quot;Cookie는? 🤔 &lt;code class=&quot;language-text&quot;&gt;SameSite&lt;/code&gt; 정책 때문에 복잡해질 것 같은데...&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;내부 검토 및 기술 논의를 거쳐, 위 문제들을 가장 안전하고 유연하게 해결할 수 있는 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt; API&lt;/strong&gt;를 사용하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;왜-postmessage인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-postmessage%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;왜 postmessage인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 postMessage인가?&lt;/h2&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;는 서로 다른 출처(origin) 간에 안전하게 메시지를 주고받을 수 있는 Web API입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Origin(출처)이란?&lt;/strong&gt; 웹사이트의 프로토콜(https), 도메인(example.com), 포트번호를 합친 것입니다.
예를 들어 &lt;code class=&quot;language-text&quot;&gt;https://parent.com&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;https://child.com&lt;/code&gt;은 도메인이 다르므로 서로 다른 origin입니다.
보안상 브라우저는 기본적으로 다른 origin 간의 데이터 접근을 차단합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;제가 &lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;를 선택한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;보안성&lt;/strong&gt;: Origin 검증을 통해 신뢰할 수 있는 출처만 통신 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유연성&lt;/strong&gt;: 복잡한 데이터 구조(객체, 배열 등)도 전달 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;표준 API&lt;/strong&gt;: 별도 라이브러리 설치 없이 바로 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;양방향 통신&lt;/strong&gt;: 부모↔자식 양쪽 모두 메시지 송수신 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;URL 파라미터로 토큰을 넘기는 방법은 브라우저 히스토리에 노출되어 보안상 위험하고, Cookie는 &lt;code class=&quot;language-text&quot;&gt;SameSite&lt;/code&gt; 정책 때문에 크로스 오리진에서 제약이 많습니다. &lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;는 이런 문제 없이 안전하고 유연하게 통신할 수 있는 최적의 선택이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;기본-구조-설계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0-%EC%84%A4%EA%B3%84&quot; aria-label=&quot;기본 구조 설계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기본 구조 설계&lt;/h2&gt;
&lt;p&gt;먼저 간단한 구조부터 만들어보았습니다. 자식 iframe이 부모에게 토큰을 요청하면, 부모가 자신의 토큰을 전달하는 방식입니다.&lt;/p&gt;
&lt;h3 id=&quot;부모-뷰&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%80%EB%AA%A8-%EB%B7%B0&quot; aria-label=&quot;부모 뷰 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;부모 뷰&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// hooks/useIframeAuth.ts&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;useIframeAuth&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;iframeRef&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RefObject&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;HTMLIFrameElement&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleMessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Origin 검증&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CHILD_ORIGIN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;GET_PARENT_TOKEN&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;token&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; refreshToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;refresh_token&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// 토큰 전송&lt;/span&gt;
        iframeRef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;contentWindow&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;PARENT_TOKEN&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            token&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            refreshToken&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;message&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;message&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;iframeRef&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;자식-뷰-iframe&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EC%8B%9D-%EB%B7%B0-iframe&quot; aria-label=&quot;자식 뷰 iframe permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;자식 뷰 (iframe)&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 토큰 요청&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; requestToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;TokenResponse&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reject&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleMessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MessageEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;PARENT_TOKEN&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;message&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          token&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          refreshToken&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;refreshToken&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;message&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; handleMessage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;GET_PARENT_TOKEN&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;*&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기까지는 순조로웠습니다. 기본적인 토큰 전달은 잘 작동했습니다. 하지만 실제 서비스에 적용하려고 하자 예상치 못한 문제들이 하나씩 나타나기 시작했습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;첫-번째-난관-동시-다발적인-api-호출&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EB%82%9C%EA%B4%80-%EB%8F%99%EC%8B%9C-%EB%8B%A4%EB%B0%9C%EC%A0%81%EC%9D%B8-api-%ED%98%B8%EC%B6%9C&quot; aria-label=&quot;첫 번째 난관 동시 다발적인 api 호출 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;첫 번째 난관: 동시 다발적인 API 호출&lt;/h2&gt;
&lt;p&gt;컴포넌트가 마운트되면서 여러 API가 동시에 호출되는 상황이 발생했습니다. 문제는 &lt;strong&gt;각 API 요청마다 부모에게 토큰을 요청&lt;/strong&gt;하면서 불필요한 중복 요청이 발생한다는 것이었습니다. 10개의 API가 동시에 호출되면 부모에게도 10번의 토큰 요청이 날아갔고, 같은 토큰을 10번 반복해서 받아오는 비효율이 발생했습니다.&lt;/p&gt;
&lt;img src=&quot;/189db61f4de96175e28c690b04ec926e/concurrent-requests.svg&quot; alt=&quot;동시 다발적 토큰 요청&quot;&gt;
&lt;h3 id=&quot;해결-방법-request-queueing&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-request-queueing&quot; aria-label=&quot;해결 방법 request queueing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결 방법. Request Queueing&lt;/h3&gt;
&lt;p&gt;첫 번째 요청만 실제로 처리하고, 나머지는 큐에 넣어서 결과를 공유하도록 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; isHandlingTokenRequest &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pendingTokenRequests &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useRef&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;PendingTokenRequest&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleTokenRequest&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;messageType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 이미 처리 중이면 큐에 추가&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isHandlingTokenRequest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reject&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      pendingTokenRequests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reject&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; messageType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  isHandlingTokenRequest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 토큰 획득&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getValidToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 원본 요청 응답&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;sendTokenResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 큐에 대기 중인 요청들도 같은 토큰으로 응답&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; queuedRequests &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;pendingTokenRequests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    pendingTokenRequests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    queuedRequests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; resolve &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;sendTokenResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    isHandlingTokenRequest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하니 10개의 요청이 동시에 와도 실제로는 1번만 토큰을 갱신하게 되었습니다!&lt;/p&gt;
&lt;p&gt;📊 &lt;strong&gt;개선 효과&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동시 API 호출 시 불필요한 토큰 요청 &lt;strong&gt;95% 감소&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;네트워크 부하 감소로 평균 응답 속도 향상&lt;/li&gt;
&lt;li&gt;서버 토큰 발급 API 호출 횟수 대폭 절감&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;두-번째-난관-401-에러-폭탄&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%91%90-%EB%B2%88%EC%A7%B8-%EB%82%9C%EA%B4%80-401-%EC%97%90%EB%9F%AC-%ED%8F%AD%ED%83%84&quot; aria-label=&quot;두 번째 난관 401 에러 폭탄 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;두 번째 난관: 401 에러 폭탄&lt;/h2&gt;
&lt;p&gt;첫 번째 문제를 해결하고 나니 더 심각한 문제가 드러났습니다. 토큰이 만료되어 여러 API가 동시에 401 에러를 받으면서 각자 토큰 갱신을 시도하는 상황이었죠. 5개의 API가 동시에 실패하면 5번의 토큰 갱신 요청이 발생했고, 이는 서버 부하는 물론 경합 상태(race condition)를 유발했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Request A → 401 ─┐
Request B → 401 ─┼→ 동시에 토큰 갱신 시도? 😱
Request C → 401 ─┘&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;시행착오-1-단순-플래그로-제어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4-1-%EB%8B%A8%EC%88%9C-%ED%94%8C%EB%9E%98%EA%B7%B8%EB%A1%9C-%EC%A0%9C%EC%96%B4&quot; aria-label=&quot;시행착오 1 단순 플래그로 제어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시행착오 1: 단순 플래그로 제어&lt;/h3&gt;
&lt;p&gt;처음에는 간단하게 플래그 하나로 제어하려고 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; isRefreshing &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;401&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isRefreshing&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 이미 갱신 중이면 무시&lt;/span&gt;

  isRefreshing &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;refreshToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  isRefreshing &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 이 방법은 &lt;strong&gt;갱신이 완료되기 전에 들어온 요청들은 그냥 실패&lt;/strong&gt;하게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;해결-방법-promise-공유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-promise-%EA%B3%B5%EC%9C%A0&quot; aria-label=&quot;해결 방법 promise 공유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결 방법: Promise 공유&lt;/h3&gt;
&lt;p&gt;모든 401 에러가 하나의 갱신 작업을 공유하도록 수정했습니다. 첫 번째 401 에러만 실제 토큰 갱신을 수행하고, 나머지 요청들은 그 갱신 작업이 완료될 때까지 기다립니다. 갱신이 완료되면 대기 중이던 모든 요청이 새 토큰을 받아 자동으로 재시도됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; handling401Promise&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Response Interceptor&lt;/span&gt;
interceptor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;401&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; originalRequest &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 이미 갱신 중이면 그 Promise를 기다림&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;handling401Promise&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; handling401Promise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        originalRequest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;Authorization&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;newToken&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;axios&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 첫 번째 401만 실제 갱신 작업 수행&lt;/span&gt;
      handling401Promise &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;refreshTokensDirect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; result&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;token &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          handling401Promise &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newToken &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; handling401Promise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      originalRequest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;Authorization&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Bearer &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;newToken&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;axios&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalRequest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 여러 요청이 동시에 401을 받아도 &lt;strong&gt;하나의 갱신 작업만 수행&lt;/strong&gt;됩니다. 대기 중이던 요청들은 갱신이 완료되면 새 토큰을 헤더에 담아 원래 요청을 자동으로 재시도합니다. 사용자는 에러를 전혀 느끼지 못하고, 서버 부하도 최소화됩니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;세-번째-난관-refresh-token도-만료되면&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B8-%EB%B2%88%EC%A7%B8-%EB%82%9C%EA%B4%80-refresh-token%EB%8F%84-%EB%A7%8C%EB%A3%8C%EB%90%98%EB%A9%B4&quot; aria-label=&quot;세 번째 난관 refresh token도 만료되면 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;세 번째 난관: Refresh Token도 만료되면?&lt;/h2&gt;
&lt;p&gt;토큰 갱신 로직을 구현하면서 또 다른 케이스를 발견했습니다. Refresh Token마저 만료된 경우입니다. 이 경우 재로그인이 필요한데, 이걸 어떻게 처리할까요?&lt;/p&gt;
&lt;h3 id=&quot;에러-코드-분리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%90%EB%9F%AC-%EC%BD%94%EB%93%9C-%EB%B6%84%EB%A6%AC&quot; aria-label=&quot;에러 코드 분리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;에러 코드 분리&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ACCESS_TOKEN_EXPIRED&lt;/strong&gt;: Access Token 만료 → 갱신 시도&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REFRESH_TOKEN_EXPIRED&lt;/strong&gt;: Refresh Token 만료 → 재로그인 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;status &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;401&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; errorCode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;code&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Refresh Token 만료&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorCode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;REFRESH_TOKEN_EXPIRED&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;clearToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;token&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;clearToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;refresh_token&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 부모에게 인증 실패 알림&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;environmentState&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;isIframe&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;AUTHENTICATION_FAILED&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;*&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Access Token 만료 - 갱신 시도&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;errorCode &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ACCESS_TOKEN_EXPIRED&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ... 토큰 갱신 로직&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;부모 뷰에서는 이 메시지를 받으면 전체 페이지를 새로고침하여 재로그인 플로우로 진입합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 부모 뷰&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;AUTHENTICATION_FAILED&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;[PARENT] Authentication failed, clearing tokens&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;clearToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;token&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;clearToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;refresh_token&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;네-번째-난관-무한-재시도-방지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%84%A4-%EB%B2%88%EC%A7%B8-%EB%82%9C%EA%B4%80-%EB%AC%B4%ED%95%9C-%EC%9E%AC%EC%8B%9C%EB%8F%84-%EB%B0%A9%EC%A7%80&quot; aria-label=&quot;네 번째 난관 무한 재시도 방지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;네 번째 난관: 무한 재시도 방지&lt;/h2&gt;
&lt;p&gt;토큰 갱신이 실패했을 때 무한히 재시도하면 서버에 부하를 줄 수 있습니다. 이를 방지하기 위해 &lt;strong&gt;쿨다운 메커니즘&lt;/strong&gt;을 도입했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tokenRefreshState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  failedAttempts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  lastFailureTime&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  maxRetries&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  cooldownMs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 5초&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 갱신 시도 전 쿨다운 체크&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; timeSinceLastFailure &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lastFailureTime&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;failedAttempts &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxRetries &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  timeSinceLastFailure &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cooldownMs
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;[AXIOS] Token refresh in cooldown period&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 갱신 시도&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;refreshToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;failedAttempts &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 성공 시 리셋&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;failedAttempts&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  tokenRefreshState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lastFailureTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;3번 연속 실패하면 5초 동안 재시도를 막습니다. 서버도 보호하고 불필요한 요청도 줄일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;전체-통신-흐름&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%84%EC%B2%B4-%ED%86%B5%EC%8B%A0-%ED%9D%90%EB%A6%84&quot; aria-label=&quot;전체 통신 흐름 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;전체 통신 흐름&lt;/h2&gt;
&lt;p&gt;최종적으로 완성된 통신 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;img src=&quot;/1c2fcd908cfc63c99d93c2fb3f73d2df/final-flow.svg&quot; alt=&quot;최종 통신 흐름&quot;&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;iframe 로드&lt;/strong&gt;: 부모로부터 토큰 요청&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 호출&lt;/strong&gt;: 토큰을 헤더에 담아 전송&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;401 발생&lt;/strong&gt;: 동시성 제어 후 토큰 갱신&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;갱신 완료&lt;/strong&gt;: 부모와 동기화 후 재시도&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인증 만료&lt;/strong&gt;: 재로그인 플로우&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;보안-고려사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%B4%EC%95%88-%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD&quot; aria-label=&quot;보안 고려사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;보안 고려사항&lt;/h2&gt;
&lt;p&gt;개발 중에는 편의를 위해 Origin 검증을 비활성화했지만, &lt;strong&gt;프로덕션에서는 반드시 활성화&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ❌ 개발 중 (임시)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// if (event.origin !== ADORA_ORIGIN) return;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ✅ 프로덕션&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ALLOWED_ORIGINS&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;https://your-iframe-domain.com&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;https://your-parent-domain.com&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ALLOWED_ORIGINS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;[PARENT] Invalid origin:&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;origin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또한 Rate Limiting을 추가하여 악의적인 공격도 방어했습니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;REQUEST_RATE_LIMIT&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 초당 최대 10개&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;REQUEST_WINDOW&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;// 1초&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestCount&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;current &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;REQUEST_RATE_LIMIT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;Too many token requests&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;&quot;그냥 토큰 하나 전달하면 되겠지&quot;라고 생각했던 과제는 생각보다 복잡했습니다. 동시성 제어, 자동 재시도, 무한 루프 방지, 보안까지. 하나씩 해결하면서 안정적인 인증 시스템을 만들 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;주요-해결-방법-요약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9A%94-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95-%EC%9A%94%EC%95%BD&quot; aria-label=&quot;주요 해결 방법 요약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;주요 해결 방법 요약&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;✅ 동시성 제어&lt;/strong&gt;
→ Request Queue + Promise 공유&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 자동 재시도&lt;/strong&gt;
→ Axios Interceptor 활용&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 무한 루프 방지&lt;/strong&gt;
→ 쿨다운 메커니즘 (3회/5초)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 보안&lt;/strong&gt;
→ Origin 검증 + Rate Limiting&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ 상태 동기화&lt;/strong&gt;
→ 부모-자식 토큰 일치 보장&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;핵심-교훈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B5%EC%8B%AC-%EA%B5%90%ED%9B%88&quot; aria-label=&quot;핵심 교훈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;핵심 교훈&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 동시성은 예상보다 복잡하다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여러 API가 동시에 401을 받는 상황은 단순 플래그로 해결되지 않았습니다.
Promise를 공유하는 패턴이 핵심이었죠. 처음엔 &quot;이미 갱신 중이면 무시&quot;하는 방식으로 접근했다가, 대기 중인 요청들이 실패하는 문제를 겪었습니다.
결국 모든 요청이 하나의 갱신 작업을 공유하도록 만들어 해결했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 사용자 경험이 최우선이다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기술적 구현도 중요하지만, 사용자가 인증 오류를 느끼지 않도록 하는 것이 가장 중요했습니다.
토큰이 만료되어도 자동으로 갱신하고 재시도하기 때문에, 사용자는 로그인 상태가 끊김없이 유지된다고 느낍니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 보안은 프로덕션에서 필수이다(당연하지만!)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;개발 중에는 편의를 위해 Origin 검증을 비활성화했지만, 프로덕션에서는 반드시 활성화해야 합니다. 악의적인 사이트가 우리 토큰을 요청하는 것을 막으려면 Origin 검증과 Rate Limiting이 필수입니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;iframe 환경에서 인증 통신을 구현하면서 마주쳤던 문제들과 해결 과정을 공유해 봤습니다.
비슷한 과제를 하시는 분들께 시행착오를 줄이는 데 도움이 되었으면 좋겠습니다.
더 나은 방법이나 궁금한 점이 있으시다면 댓글로 공유해 주세요!&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;참고 자료&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Window/postMessage&quot;&gt;MDN - Window.postMessage()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jwt.io/introduction&quot;&gt;JWT 토큰 인증&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[배달대행사 API 연동과 장애 대응 - 오늘드림 서비스 개발기]]></title><description><![CDATA[오늘 주문하면 오늘 받는 오늘드림 서비스, 이를 위해 뒤에서는 얼마나 바쁘게 돌아갈까요? 안녕하세요. 올리브영 배송 및 물류 스쿼드에서 백엔드 개발을 담당하고 있는 맹곰이입니다.✌️ 올리브영은 택배 배송과 오늘드림 두 가지 배송 서비스를 제공하고 있는데요.
이 중 택배 배송과 관련 있는 물류시스템과 데이터 전송 방식은 "올리브영 물류시스템에서는 데이터를 어떻게 주고 받을까?"라는 제목으로 지난 해에 소개드렸습니다.  오늘은 물류시스템만큼 많은 트래픽을 발생하는 오늘드림 서비스에서 외부 배달대행사와 API 통신을 통해서 데이터를 주고 받는 부분을 소개해드리려고 찾아왔습니다!
또한, 배달대행사와 안정적으로 연동하기 위한 API 설계전략과, 실제 운영 중 겪었던 트랜잭션 장애 사례를 공유합니다. 오늘드림 우선 다들 아시겠지만 오늘드림부터 간단히 소개해 드릴게요. 오늘드림을 한줄로 표현해보면, 원하는 상품을 당일에 빠르게 배송 받을 수 있는 즉시배송 서비스입니다. 오늘드림은 빠름 배송 기준, 보통 1시간 이내로 여러분의 집으로 도착을 하는데요. 이렇게 빠르게 배송이 가능한 이유는 전국 여기저기에 올리브영 매장이 여러분 집 주변에 있기도 하지만, 빠르게 배송을 해주는 배달대행사도 한 몫을 하고 있습니다.
오늘은 이 배달대행사와 데이터를 주고 받는 부분을 소개해드리려고 합니다. 시스템 간에 데이터를 주고 받는 방법은 저번 시간에 소개해드린거 처럼 여러가지 방법이 있는데요.
배달대행사와 데이터를 주고 받는 부분은 API를 사용해서 연동을 했습니다. 여러분이 오늘드림으로 주문하면 매장에서 상품 포장이 완료되면 배달대행사를 호출하게 되는데요.
올리브영은 배달대행사와 긴밀한 API 연동을 통해 '오늘드림' 서비스를 운영합니다. 이 연동은 크게 두 단계로 이루어집니다. 올리브영 -> 배달대행사 API 호출 : 고객의 주문이 접수되고 매장에서 상품 포장이 완료되면, 올리브영 시스템은 배달대행사 주문 접수 API를 호출하여 배송을 요청합니다. 배달대행사 -> 올리브영 API 호출 (콜백) : 배달대행사는 라이더 배차, 상품 픽업, 배송 완료 등 배송상태가 업데이트 될 때마다 올리브영 API를 호출하여 그 정보를 전달합니다. 오늘드림 주문 배달대행사 접수 API 우선 올리브영에서 호출하는 배달대행사 주문 접수 API 연동부터 살펴볼까요? API는 상황에 따라서 여러 문제가 발생할 수 있는데요.
Read Timeout, Connection Timeout, 서버 에러, 클라이언트 에러 등 여러 문제가 있습니다.
이 에러들을 재시도하면 성공할 수 있을지 여부에 따라서 나누어서 생각해보려고 합니다. 해당 개념을 쉽게 이해하도록 Read Timeout, Connection Timeout을 간단하게 전화에 비유해서 표현해 볼게요. Read Timeout : 전화를 걸어 상대방이 받았으나, 일정 시간 동안 상대방이 응답이 없어 내가 끊어버린 경우 Connection Timeout : 전화를 걸었으나, 일정 시간동안 상대방이 받지를 않아 내가 끊어버린 경우 Read Timeout, Connection Timeout, 서버 에러 Read Timeout, Connection Timeout 등의 에러는 일시적으로 발생하는 에러로 대부분 잠시 후에 재시도할 경우 에러가 발생하지 않는 케이스입니다.
그렇기에 이러한 케이스는 내부적으로 잠시 후 재시도하도록 처리했습니다. 그러나 여기서 Read Timeout은 중복처리의 문제가 있을 수도 있습니다. 클라이언트 입장에서는 Timeout으로 주문 접수 실패로 판단했지만, 서버인 배달대행사에는 설정된 Timeout값보다 오래 걸렸지만 접수가 처리된 경우도 있죠. 다행히 대부분의 배달대행사에서는 똑같은 주문번호로 접수를 하게 되면 중복접수로 응답을 하는 기능이 있습니다. 중복접수 응답이 오면 내부적으로는 접수성공으로 판단을 하여 해당 문제를 해결하고 있습니다. 그리고 서버 에러의 경우에는 일시적인 에러일수도 있지만, 그렇지 않은 경우도 있습니다. 그렇지 않는 경우라 함은, API를 호출하면 서버에서 지속적으로 에러로 응답할 것이고 될 때까지 기다릴수는 없기에... 재시도 횟수 또한 제한 하고 있습니다. 클라이언트 에러 클라이언트 에러는 대부분 다시 시도를 해도 계속해서 에러가 발생할 텐데요. 이러한 경우에는 주문접수를 할 수가 없고 여러분이 주문한 상품을 배달을 할수가 없습니다.
이러한 상황으로는 대표적으로 주소가 잘못되었거나 배달대행사에서 라이더가 부족하다거나 기상악화로 배달을 못 하는 경우가 있습니다. 아래는 주문접수 API 호출 시 사용하는 코드 샘플이며, 주문접수 성공 시에는 http status가 200으로 응답, 중복접수 시에는 409, 잘못된 요청은 400, 서버 에러는 500으로 응답이 온다를 가정했습니다. 오늘드림 콜백 API 다음은 올리브영이 제공하는 배달대행사에서 라이더가 배차가 되고, 상품을 픽업, 배송완료 시 호출하는 API입니다.
이러한 콜백 API는 라이더 배차, 상품 픽업, 상품 배송 완료, 배달 취소 등의 기능으로 구성됩니다. 라이더 배차 : 라이더가 배차가 되었다고 알려주는 기능입니다.  상품 픽업 : 라이더가 상품을 픽업했다고 알려주는 기능입니다.  상품 배송완료 : 라이더가 상품을 배달완료했다고 알려주는 기능입니다.  배달 취소 : 배달을 수행 할 수 없을 때 알려주는 기능입니다.  이 API들은 얼마전에 대규모 리뉴얼을 진행했습니다. 리뉴얼 하게 된 이유 배달대행사마다 같은 기능을 하는 각자 다른 api로 이루어져 있어 하나로 합치자.  리뉴얼을 하게 된 이유는 여러가지가 있지만, 그 중에서도 각기 다른 API로 이루어진 기능을 한 개 API로 통합한 이유는 이렇습니다. 기존에는 제일 처음 도입된 A 배달대행사 전용 콜백 API가 있었습니다. 그리고 B 배달대행사가 추가되면 B 배달대행사에 맞춰서 콜백 API를 개발을 했고, C 배달대행사가 추가되면 C 배달대행사 콜백 API를 개발 했습니다. 이렇게 1개씩 추가되다보니... 동일 기능을 하는 API가 여러개가 되어 관리해야하는 API 숫자가 늘게 되었습니다. 그러다보니 발생하는 문제가 기능 수정 시 여러 API를 수정해야하기에 일부 API에는 누락될 가능성이 많아졌습니다. 또한 내부 로직은 같은 코드 사용하도록 하여 수정은 한곳만 하도록 구현하더라도 테스트는 각각 API를 해야 하기에 시간이 더 많이 필요한 등의 이슈가 있습니다. 이러한 문제점들을 해결하고 관리 효율성을 위해 1개 기능을 하는 API는 1개가 되도록 리뉴얼을 하게 되었습니다! 덕분에 아래 그림처럼 API가 12개에서 4개로 줄어들었고, 운영상에서도 봐야할 지표 및 로그 종료도 그만큼 줄어들어 관리 효율성이 증대되었습니다. 트랜잭션 에피소드 위 그림처럼 배대사 API는 1개로 리뉴얼은 성공적으로 완료되었습니다!! 리뉴얼하면서 발생한 기술적인 에피소드가 하나 있어 추가로 소개해드릴려고 합니다.
개발을 하다보면 많은 에피소드가 있을텐데요. 그중 오늘 소개해드리려는 에피소드로 단 한 줄의 코드 때문에 서버가 멈춰버린 이슈를 풀어보겠습니다.🫢🫣 이는 트랜잭션을 무분별하게 사용하면서 서버에 과부하가 발생한 결과였습니다. 그렇다면 문제의 한 줄이 무슨 내용이었을지 짐작 가시나요? 그 1줄은 UPDATE 쿼리의 트랜잭션 전파옵션입니다. 트랜잭션 전파옵션 간단설명! REQUIRED : 기존 트랜잭션과 동일한 트랜잭션을 사용합니다. REQUIRES_NEW : 새로운 트랜잭션을 생성합니다. 트랜잭션 전파옵션 스프링 공식 문서 UPDATE 쿼리를 실행하는 메소드에 트랜잭션 전파옵션을 REQUIRED로 설정했는데, 동일 트랜잭션을 사용할 수 있는 메소드가 있었습니다. 그렇지만 이 메소드의 트랜잭션을 분리해 REQUIRES_NEW로 변경했습니다. 이로 인해 메소드 호출 한 번당 트랜잭션이 기존 대비 2배로 증가하게 되었습니다. 평소에는 동시 트랜잭션 수가 Connection Pool 크기를 넘지 않아 문제가 없었지만, 특정 시간대에 해당 메소드 호출이 급증하면서 상황이 달라졌습니다.
Connection Pool 크기를 초과하는 트랜잭션 요청이 발생했고, Connection을 얻지 못한 스레드들이 대기 상태로 쌓이기 시작했습니다. 대기 스레드가 계속 증가하면서 서버에 과부하가 걸리니 결국 서버가 응답하지 않는 상황까지 이어진거죠.
(사전에 Connection Pool 용량이 충분한지 계산해봤더라면 이런 상황을 예방할 수 있었을텐데요😭) 다행히 MSA가 적용되어 일시적인 일부 장애로 끝났고 서버 증설로 임시 대응이 가능하였으며, 현재는 트랜잭션 전파옵션을 변경해서 이슈를 해결하였습니다. 마무리 올리브영 오늘드림 주문건수는 계속해서 늘고 있는데요. 그에 비례해서 배달대행사와 연동하는 데이터 양도 많아졌습니다.
또한 이로 인하여 트래픽도 계속해서 늘고 있는데요. 위 사례을 겪으면서 배운 세 가지 교훈을 정리해 볼게요!! MSA는 왜 해야하는가 : MSA가 적용되어 있어 올리브영 전체 서비스 장애로 이어지지 않았습니다. 트랜잭션 전파옵션의 중요성 : 트랜잭션 전파옵션 1줄로 서버가 멈추는 결과까지 이르게 되었습니다.😭 부하 테스트의 중요성 : 부하 테스트 한번 해보았으면 해당 문제는 배포 전에 발견했을 거 같아요. 여러분의 프로젝트에서도 외부 시스템 연동이나 트랜잭션 관리로 고민하고 계신다면, 이 글이 시행착오를 줄이는 데 도움이 되길 바랍니다. 그럼 전 이만 또 다른 기능을 개발하러 가보겠습니다!!! 감사합니다.]]></description><link>https://oliveyoung.tech/2025-12-01/o2o_delivery/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-12-01/o2o_delivery/</guid><pubDate>Mon, 01 Dec 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;오늘 주문하면 오늘 받는 오늘드림 서비스, 이를 위해 뒤에서는 얼마나 바쁘게 돌아갈까요?&lt;/p&gt;
&lt;p&gt;안녕하세요. 올리브영 배송 및 물류 스쿼드에서 백엔드 개발을 담당하고 있는 맹곰이입니다.✌️&lt;/p&gt;
&lt;p&gt;올리브영은 택배 배송과 오늘드림 두 가지 배송 서비스를 제공하고 있는데요.
이 중 택배 배송과 관련 있는 물류시스템과 데이터 전송 방식은 &lt;a href = &quot;https://oliveyoung.tech/2024-10-17/oy-delivery-mq/&quot;&gt;&quot;올리브영 물류시스템에서는 데이터를 어떻게 주고 받을까?&quot;&lt;/a&gt;라는 제목으로 지난 해에 소개드렸습니다. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;오늘은 물류시스템만큼 많은 트래픽을 발생하는 오늘드림 서비스에서 외부 배달대행사와 API 통신을 통해서 데이터를 주고 받는 부분을 소개해드리려고 찾아왔습니다!
또한, 배달대행사와 안정적으로 연동하기 위한 API 설계전략과, 실제 운영 중 겪었던 트랜잭션 장애 사례를 공유합니다.&lt;/p&gt;
&lt;figure style=&quot;display:flex; justify-content:center; gap:10px; flex-direction:column;&quot;&gt;
  &lt;div style=&quot;display:flex; gap:10px;&quot;&gt;
    &lt;div style=&quot;max-width: 480px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1848px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ee061732102cb1dd5a38d25fea2bac41/13727/center.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAWJAAAFiQFtaJ36AAACwUlEQVR42nWR20/TcBTH90/6JiRqDCa+aMILwcQHRQhEiEIIUnbhNpwwuhW2rpuFrpet67oLMGE3BgxnCBmPPhhJv57fikBifPjmnH578jmXn0evtXCregsaRYPF6l/vpCfjnvQGqXkOrdHuSa23HfP0B6vrelyAW6hSXu90cHl1Cbt1RtBjF3DTjCnTOEW6UoUkhqCk1l1Ja46sJqExIJvm++UFOiSWFwh0cNaGdXzam/YWVrs38VEdWjKEohJCTg4hLwedrCkjXW0x4AlB2j0ZtIpaPWE/7mD/iLapNmGkBZQMAZYaha3yjlU0XKB61IB2WINGXfWjGrL1JjI18sj/H1CvNpBRBZQzW7C1KIpaxLEKOhQG1GwTaiKI7E4YlsJjJxaELATAfP24Q3ehw9fPe9Jrxy6UJsykoygbUeTVCApq+A6YK1k4UNeRlVZgy2vYFXwoyEHkynk6/iH0vTyMgwLUPZte9wyZVodWrvdW3stuoZSN0ZT8HbBQKaJpCwh8GAI3OYil6WG07CjMygEyX6ewH36A3Hofipt9SPJjkHImDFq7nBNxmNuCLq2iWRKdQjlzMyF1rmjrmB17iZmxAXhHn9PEIaiFMqrWNNrSI4jcAH7azyD5HyK+OY751Wl8s3g081vwjr/Ar4uysx2ZwXI80vXsphahJeYQCb7D5uobRFbfQhNnkUwsQ+GHoC73IxV4gn3hMfS1fpgbT/F55TXSiVkYEgchOEKvvOyIkQnw4amuJyV4Eefnkdr2Q44tIkHfSbpjUuCw5pvA+5FBrHCj+LI0ibmpV/B+HIYiUl3Ui9jmJ8jxJRYdORagB/V3PWLEh1iYI3MBIu/F9sY8RR+txhq5HquRBH8vblNtnHmk2AbnxjDnJBiH9155TIW7zqUXSNy1mXZz5pkKeeyf4vq33m1+V0vxt6V6QfnlH8v7RvPIgF4aAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ee061732102cb1dd5a38d25fea2bac41/263a4/center.webp 480w,
/static/ee061732102cb1dd5a38d25fea2bac41/a6361/center.webp 960w,
/static/ee061732102cb1dd5a38d25fea2bac41/27be8/center.webp 1848w&quot; sizes=&quot;(max-width: 1848px) 100vw, 1848px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ee061732102cb1dd5a38d25fea2bac41/9aebd/center.png 480w,
/static/ee061732102cb1dd5a38d25fea2bac41/a91f8/center.png 960w,
/static/ee061732102cb1dd5a38d25fea2bac41/13727/center.png 1848w&quot; sizes=&quot;(max-width: 1848px) 100vw, 1848px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ee061732102cb1dd5a38d25fea2bac41/13727/center.png&quot; alt=&quot;center&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;figcaption style=&quot;text-align:center;&quot;&gt;올리브영 택배배송&lt;/figcaption&gt;
    &lt;/div&gt;
     &lt;div style=&quot;max-width: 480px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1855px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8719840422c38533c8c8fc894b46fd9b/a3c40/o2o.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAWJAAAFiQFtaJ36AAADGklEQVR42m2RW0+cVRSGvx+j0Qu9aGJM40UTL4xJK9ZEC140GtoqGqaJxqSSghbKoe3QFhiYAzAHBubwDXVgpnM+fHMqhaHAwAwwB1OLpytbvfEHPK5vrE2bePFm7b32Wu9+1t6KdlghfbCDVq+Q0uPhLrn6f6o8t35ReTnb/7nKwVGVQqPS3ut5RW9q/Frj778aNCXqh9ozo8r/mmYFonpU48kfdf58XKfxS02gnhoWmxV++q3K0e81HonWWxUyB9tCutPWi6ZC0tglXXvAvVKAzZKPjZKf7TU/6wUvmZ0SitbcJ9tqihpoPzYoPGxRfNgUtci3Dp8zrDwzzFTLbKRnuReZZC02TSlqohSZQtvKoaxobtyBb3EFBlgI9ONSL2P3ijyXWE6YZbxd9HfONWvtqNV1w03Wsw4xmmYtYaEUN1OKzZDdFkM1fJXb148xOnSc8WtvYRw5zpX+N7hy+XXcXoMY7JE73JLiPNn9rfbo6b0yZc3BRspCOW2TaOO+GGd0wrgms6uXSKj95IMDRIU0qg6Q9H1DKmUn0joiuZlndayLtNDmW/IBOmHGQVHGLCSsFIWuKLT6pUq2EMQVtDMe8OD5wYoxuMwNdQnrHQfZ8CSa5zx5cxe5iU/wrLqIbRZJ7KxzPzUno1p5lDPxIGmmEDWTKguhZ7GbTPwWqytGfCtmhv1uRj3zzKhzJAN95G68hOvLl7n6RQdO41kiwe8wubrkE/rYK1rJWbrZCA2xpY3jcJ9DcVneJez6mKi7k6W5C8xMX8BiOodtqhvvbCfqyJsUxl5h4qvXsAydoHj7VRbN7xBcPEPMdxaP+UOCrk7Zf4R3/iRK2DFGZGGMgHWQ6MIwUecgIfv3JJdGWLEPc+3rM4z2nOCu8TTuwQ/o+/RtJvp7BOA6Iccwy7ZBws4R7jpHuWMbQvFbe1FtBvyWXgK2i6hWAz6Rv61enJM9zN38DMeUgYVpA/M3z7No+lxqDQRmL+Jr9xmkr7e9VmKrHfIuJ4mHOtDXcV0hPXdK9u+RDJ8WvS/5UyRD/5619bRWr9H7o1Kv5/8BU+cHErSCvd0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8719840422c38533c8c8fc894b46fd9b/263a4/o2o.webp 480w,
/static/8719840422c38533c8c8fc894b46fd9b/a6361/o2o.webp 960w,
/static/8719840422c38533c8c8fc894b46fd9b/14772/o2o.webp 1855w&quot; sizes=&quot;(max-width: 1855px) 100vw, 1855px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8719840422c38533c8c8fc894b46fd9b/9aebd/o2o.png 480w,
/static/8719840422c38533c8c8fc894b46fd9b/a91f8/o2o.png 960w,
/static/8719840422c38533c8c8fc894b46fd9b/a3c40/o2o.png 1855w&quot; sizes=&quot;(max-width: 1855px) 100vw, 1855px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8719840422c38533c8c8fc894b46fd9b/a3c40/o2o.png&quot; alt=&quot;o2o&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;figcaption style=&quot;text-align:center;&quot;&gt;올리브영 오늘드림&lt;/figcaption&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;오늘드림&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%EB%8A%98%EB%93%9C%EB%A6%BC&quot; aria-label=&quot;오늘드림 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오늘드림&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;우선 다들 아시겠지만 오늘드림부터 간단히 소개해 드릴게요. 오늘드림을 한줄로 표현해보면, 원하는 상품을 당일에 빠르게 배송 받을 수 있는 즉시배송 서비스입니다.&lt;/p&gt;
&lt;p&gt;오늘드림은 빠름 배송 기준, 보통 1시간 이내로 여러분의 집으로 도착을 하는데요. 이렇게 빠르게 배송이 가능한 이유는 전국 여기저기에 올리브영 매장이 여러분 집 주변에 있기도 하지만, 빠르게 배송을 해주는 배달대행사도 한 몫을 하고 있습니다.
오늘은 이 배달대행사와 데이터를 주고 받는 부분을 소개해드리려고 합니다.&lt;/p&gt;
&lt;figure style=&quot;display:flex; justify-content:center; gap:10px; flex-direction:column;&quot;&gt;
  &lt;div style=&quot;max-width: 480px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a19a77136cdbde4418c06115c36519c5/13ce4/api_call.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 88.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAC4jAAAuIwF4pT92AAACpElEQVR42o1Uy27UQBD0/585w4ELiBNCSAgkFCES5QEJUUgi8tqw6/V6/Vg/xvZ4PLaL7vY6hJBEGanX49mZ6q7qGjvPnr/B1s4+Dg5/Yu/7D5yeXWA2X2I698Gj73Ezuq6T58bWPl68eoft3W/Y2dvHxpdNHB6d4NfFNZzzyQxKKRRFgTzPUNcabdthGa5wd/Rr9OuZBz+IUdI5pXKkaYKmMUgyBSeME9lkbStAXEVZaSyW0YOAnKw2ZjjX9jB2WBfAqbugynIkSUqZMnn6QQDXW0oCTjRG01gBdb0Apq5wfKXw8lOMj3sZNOErYuksgggtVcUZORpr6Y8Snh/cVDXGqCHTbZoGRdVh6lfwohodFZkpAoxW6T90eBja/JiGyzCG1rXMWfdK64FymsNZJZm8uIHBlVej0B1RtY8Cen6IoiypgQbGNALYEjMuzkmzjEQFXn+O8X47x+m1gm1qzBchbWrv0RBiq4oqZL3zXCFeJQReDxXG6wpLUjVXJVU3Uo65pns1lAqLStasZUatrLNjnHg1ABpqiK5KmXMly+gRDakpo26cZEw0aEhW4W3HkwqHlxWi1BDlhnwYr6v6G2wj0XsRQJGGmkDLqhK7WNIwJOoOe481fLuZ4POBwrlbkHa10Oq6VuiMwYcE0Bs0ZP+yhkEYkS8NVqLhmjJ7kK/gUyjzLWKv8rskouB5JBqum2JtQ0D1kwD9WxrebhbL5yT0wy7fPikkFuR6a41U0ffdjegcYzdnnk8+1KjozjNlkY0YhlHCxk5RNz0+7Cb4epTj0lWkhxYfjiB3NeT7z2DcDPYga6jJhzEbe7x6nKEqb2koPnxYQ12b/9bFNmeXUwSk15Q/qq4Pn4B+UwVsjbsf2PHgjLo8kW9ihDm5gdnw/GIywx8BsGvSe7tr9gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a19a77136cdbde4418c06115c36519c5/263a4/api_call.webp 480w,
/static/a19a77136cdbde4418c06115c36519c5/a6361/api_call.webp 960w,
/static/a19a77136cdbde4418c06115c36519c5/0b34d/api_call.webp 1920w,
/static/a19a77136cdbde4418c06115c36519c5/30e03/api_call.webp 2006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a19a77136cdbde4418c06115c36519c5/9aebd/api_call.png 480w,
/static/a19a77136cdbde4418c06115c36519c5/a91f8/api_call.png 960w,
/static/a19a77136cdbde4418c06115c36519c5/ac7a9/api_call.png 1920w,
/static/a19a77136cdbde4418c06115c36519c5/13ce4/api_call.png 2006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a19a77136cdbde4418c06115c36519c5/ac7a9/api_call.png&quot; alt=&quot;api call&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
  &lt;figcaption style=&quot;text-align:center;&quot;&gt;올리브영과 배달대행사간의 API 호출&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;시스템 간에 데이터를 주고 받는 방법은 저번 시간에 소개해드린거 처럼 여러가지 방법이 있는데요.
배달대행사와 데이터를 주고 받는 부분은 API를 사용해서 연동을 했습니다.&lt;/p&gt;
&lt;p&gt;여러분이 오늘드림으로 주문하면 매장에서 상품 포장이 완료되면 배달대행사를 호출하게 되는데요.
올리브영은 배달대행사와 긴밀한 API 연동을 통해 &apos;오늘드림&apos; 서비스를 운영합니다. 이 연동은 크게 두 단계로 이루어집니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;올리브영 -&gt; 배달대행사 API 호출 : 고객의 주문이 접수되고 매장에서 상품 포장이 완료되면, 올리브영 시스템은 배달대행사 주문 접수 API를 호출하여 배송을 요청합니다.&lt;/li&gt;
&lt;li&gt;배달대행사 -&gt; 올리브영 API 호출 (콜백) : 배달대행사는 라이더 배차, 상품 픽업, 배송 완료 등 배송상태가 업데이트 될 때마다 올리브영 API를 호출하여 그 정보를 전달합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;오늘드림-주문-배달대행사-접수-api&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%EB%8A%98%EB%93%9C%EB%A6%BC-%EC%A3%BC%EB%AC%B8-%EB%B0%B0%EB%8B%AC%EB%8C%80%ED%96%89%EC%82%AC-%EC%A0%91%EC%88%98-api&quot; aria-label=&quot;오늘드림 주문 배달대행사 접수 api permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오늘드림 주문 배달대행사 접수 API&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;우선 올리브영에서 호출하는 배달대행사 주문 접수 API 연동부터 살펴볼까요? API는 상황에 따라서 여러 문제가 발생할 수 있는데요.
Read Timeout, Connection Timeout, 서버 에러, 클라이언트 에러 등 여러 문제가 있습니다.
이 에러들을 재시도하면 성공할 수 있을지 여부에 따라서 나누어서 생각해보려고 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;해당 개념을 쉽게 이해하도록 Read Timeout, Connection Timeout을 간단하게 전화에 비유해서 표현해 볼게요.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read Timeout : 전화를 걸어 상대방이 받았으나, 일정 시간 동안 상대방이 응답이 없어 내가 끊어버린 경우&lt;br&gt;&lt;/li&gt;
&lt;li&gt;Connection Timeout : 전화를 걸었으나, 일정 시간동안 상대방이 받지를 않아 내가 끊어버린 경우&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h5 id=&quot;read-timeout-connection-timeout-서버-에러&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#read-timeout-connection-timeout-%EC%84%9C%EB%B2%84-%EC%97%90%EB%9F%AC&quot; aria-label=&quot;read timeout connection timeout 서버 에러 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Read Timeout, Connection Timeout, 서버 에러&lt;/h5&gt;
&lt;p&gt;Read Timeout, Connection Timeout 등의 에러는 일시적으로 발생하는 에러로 대부분 잠시 후에 재시도할 경우 에러가 발생하지 않는 케이스입니다.
그렇기에 이러한 케이스는 내부적으로 잠시 후 재시도하도록 처리했습니다.&lt;/p&gt;
&lt;p&gt;그러나 여기서 Read Timeout은 중복처리의 문제가 있을 수도 있습니다. 클라이언트 입장에서는 Timeout으로 주문 접수 실패로 판단했지만, 서버인 배달대행사에는 설정된 Timeout값보다 오래 걸렸지만 접수가 처리된 경우도 있죠.&lt;/p&gt;
&lt;p&gt;다행히 대부분의 배달대행사에서는 똑같은 주문번호로 접수를 하게 되면 중복접수로 응답을 하는 기능이 있습니다. 중복접수 응답이 오면 내부적으로는 접수성공으로 판단을 하여 해당 문제를 해결하고 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 서버 에러의 경우에는 일시적인 에러일수도 있지만, 그렇지 않은 경우도 있습니다. 그렇지 않는 경우라 함은, API를 호출하면 서버에서 지속적으로 에러로 응답할 것이고 될 때까지 기다릴수는 없기에... 재시도 횟수 또한 제한 하고 있습니다.&lt;/p&gt;
&lt;h5 id=&quot;클라이언트-에러&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%97%90%EB%9F%AC&quot; aria-label=&quot;클라이언트 에러 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;클라이언트 에러&lt;/h5&gt;
&lt;p&gt;클라이언트 에러는 대부분 다시 시도를 해도 계속해서 에러가 발생할 텐데요. 이러한 경우에는 주문접수를 할 수가 없고 여러분이 주문한 상품을 배달을 할수가 없습니다.
이러한 상황으로는 대표적으로 주소가 잘못되었거나 배달대행사에서 라이더가 부족하다거나 기상악화로 배달을 못 하는 경우가 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;아래는 주문접수 API 호출 시 사용하는 코드 샘플이며, 주문접수 성공 시에는 http status가 200으로 응답, 중복접수 시에는 409, 잘못된 요청은 400, 서버 에러는 500으로 응답이 온다를 가정했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;webClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;uri&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;URI 주소&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;accept&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bodyValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;인증 값&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;retrieve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 접수성공&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;onStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;is2xxSuccessful&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
      response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bodyToMono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ResponseDto&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; errorBody &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
          &lt;span class=&quot;token class-name&quot;&gt;Mono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;onStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
      response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bodyToMono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ResponseDto&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; errorBody &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// 중복접수&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statusCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONFLICT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;중복 접수&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
              &lt;span class=&quot;token class-name&quot;&gt;Mono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              when &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;token comment&quot;&gt;// 클라이언트 에러&lt;/span&gt;
                  response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statusCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;is4xxClientError &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                      logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;접수 실패&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                      &lt;span class=&quot;token class-name&quot;&gt;Mono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                  &lt;span class=&quot;token comment&quot;&gt;// 서버 에러&lt;/span&gt;
                  &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                      &lt;span class=&quot;token class-name&quot;&gt;Mono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;token class-name&quot;&gt;WebClientResponseException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                          &lt;span class=&quot;token string&quot;&gt;&quot;주문접수 실패&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                          response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;rawStatusCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                          response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;statusCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;reasonPhrase&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                          response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;asHttpHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                          errorBody&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toByteArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                          &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
                        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bodyToMono&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ResponseDto&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;오늘드림-콜백-api&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%EB%8A%98%EB%93%9C%EB%A6%BC-%EC%BD%9C%EB%B0%B1-api&quot; aria-label=&quot;오늘드림 콜백 api permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오늘드림 콜백 API&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;다음은 올리브영이 제공하는 배달대행사에서 라이더가 배차가 되고, 상품을 픽업, 배송완료 시 호출하는 API입니다.
이러한 콜백 API는 라이더 배차, 상품 픽업, 상품 배송 완료, 배달 취소 등의 기능으로 구성됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;라이더 배차 : 라이더가 배차가 되었다고 알려주는 기능입니다. &lt;br&gt;&lt;/li&gt;
&lt;li&gt;상품 픽업 : 라이더가 상품을 픽업했다고 알려주는 기능입니다. &lt;br&gt;&lt;/li&gt;
&lt;li&gt;상품 배송완료 : 라이더가 상품을 배달완료했다고 알려주는 기능입니다. &lt;br&gt;&lt;/li&gt;
&lt;li&gt;배달 취소 : 배달을 수행 할 수 없을 때 알려주는 기능입니다. &lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 API들은 얼마전에 대규모 리뉴얼을 진행했습니다.&lt;/p&gt;
&lt;h5 id=&quot;리뉴얼-하게-된-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A6%AC%EB%89%B4%EC%96%BC-%ED%95%98%EA%B2%8C-%EB%90%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;리뉴얼 하게 된 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;리뉴얼 하게 된 이유&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;배달대행사마다 같은 기능을 하는 각자 다른 api로 이루어져 있어 하나로 합치자. &lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;리뉴얼을 하게 된 이유는 여러가지가 있지만, 그 중에서도 각기 다른 API로 이루어진 기능을 한 개 API로 통합한 이유는 이렇습니다.&lt;/p&gt;
&lt;p&gt;기존에는 제일 처음 도입된 A 배달대행사 전용 콜백 API가 있었습니다. 그리고 B 배달대행사가 추가되면 B 배달대행사에 맞춰서 콜백 API를 개발을 했고, C 배달대행사가 추가되면 C 배달대행사 콜백 API를 개발 했습니다.&lt;/p&gt;
&lt;p&gt;이렇게 1개씩 추가되다보니... 동일 기능을 하는 API가 여러개가 되어 관리해야하는 API 숫자가 늘게 되었습니다.&lt;/p&gt;
&lt;p&gt;그러다보니 발생하는 문제가 기능 수정 시 여러 API를 수정해야하기에 일부 API에는 누락될 가능성이 많아졌습니다. 또한 내부 로직은 같은 코드 사용하도록 하여 수정은 한곳만 하도록 구현하더라도 테스트는 각각 API를 해야 하기에 시간이 더 많이 필요한 등의 이슈가 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 문제점들을 해결하고 관리 효율성을 위해 1개 기능을 하는 API는 1개가 되도록 리뉴얼을 하게 되었습니다!&lt;/p&gt;
&lt;p&gt;덕분에 아래 그림처럼 API가 12개에서 4개로 줄어들었고, 운영상에서도 봐야할 지표 및 로그 종료도 그만큼 줄어들어 관리 효율성이 증대되었습니다.&lt;/p&gt;
&lt;figure style=&quot;display:flex; justify-content:center; gap:10px; flex-direction:column;&quot;&gt;
  &lt;div style=&quot;max-width: 1000px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ddbd7e9db8b8c4e93f32700f13080b44/ec90d/api.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 23.333333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA4ElEQVR42n2Q3W6EIBCFff8XbNMme2FWFByQHxFF9HRge7NJ00kIZDgcvjOdECPkTJiNRVgjhmGAtQ4fXw+MkwLpBfks+Kvu+7WvccPn9wPee3QzGRCbKVqwpcRmFpEF1bxW2g/kfOJkUzUbOOfZILH2QCkXrutqmi3tTd9VGu8DQoztJyFEI3Tca4b80DoHbRb0zxFaG+6lpiU+933PdxbrL0A3cORJzRz5RUhELI5vhEfOHO/GXs9HZsO9EZVS2nojlIo4roGQBB8ipJSNeFKayQLP17TI/1X9qI6jmv4AS4KBuu775Z0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ddbd7e9db8b8c4e93f32700f13080b44/263a4/api.webp 480w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/a6361/api.webp 960w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/0b34d/api.webp 1920w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/da28f/api.webp 2880w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/98b7d/api.webp 3840w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/bd1c1/api.webp 5080w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ddbd7e9db8b8c4e93f32700f13080b44/9aebd/api.png 480w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/a91f8/api.png 960w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/ac7a9/api.png 1920w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/f9c26/api.png 2880w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/5da7e/api.png 3840w,
/static/ddbd7e9db8b8c4e93f32700f13080b44/ec90d/api.png 5080w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ddbd7e9db8b8c4e93f32700f13080b44/ac7a9/api.png&quot; alt=&quot;api&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align:center;&quot;&gt;리뉴얼 전후 API 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h5 id=&quot;트랜잭션-에피소드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%97%90%ED%94%BC%EC%86%8C%EB%93%9C&quot; aria-label=&quot;트랜잭션 에피소드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;트랜잭션 에피소드&lt;/h5&gt;
&lt;p&gt;위 그림처럼 배대사 API는 1개로 리뉴얼은 성공적으로 완료되었습니다!! 리뉴얼하면서 발생한 기술적인 에피소드가 하나 있어 추가로 소개해드릴려고 합니다.
개발을 하다보면 많은 에피소드가 있을텐데요. 그중 오늘 소개해드리려는 에피소드로 단 한 줄의 코드 때문에 서버가 멈춰버린 이슈를 풀어보겠습니다.🫢🫣&lt;/p&gt;
&lt;p&gt;이는 트랜잭션을 무분별하게 사용하면서 서버에 과부하가 발생한 결과였습니다. 그렇다면 문제의 한 줄이 무슨 내용이었을지 짐작 가시나요?&lt;/p&gt;
&lt;p&gt;그 1줄은 UPDATE 쿼리의 트랜잭션 전파옵션입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;트랜잭션 전파옵션 간단설명!&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REQUIRED : 기존 트랜잭션과 동일한 트랜잭션을 사용합니다.&lt;br&gt;&lt;/li&gt;
&lt;li&gt;REQUIRES_NEW : 새로운 트랜잭션을 생성합니다.&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href = &quot;https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html&quot;&gt;트랜잭션 전파옵션 스프링 공식 문서&lt;/a&gt;&lt;br&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;UPDATE 쿼리를 실행하는 메소드에 트랜잭션 전파옵션을 REQUIRED로 설정했는데, 동일 트랜잭션을 사용할 수 있는 메소드가 있었습니다. 그렇지만 이 메소드의 트랜잭션을 분리해 REQUIRES_NEW로 변경했습니다. 이로 인해 메소드 호출 한 번당 트랜잭션이 기존 대비 2배로 증가하게 되었습니다. 평소에는 동시 트랜잭션 수가 Connection Pool 크기를 넘지 않아 문제가 없었지만, 특정 시간대에 해당 메소드 호출이 급증하면서 상황이 달라졌습니다.
Connection Pool 크기를 초과하는 트랜잭션 요청이 발생했고, Connection을 얻지 못한 스레드들이 대기 상태로 쌓이기 시작했습니다. 대기 스레드가 계속 증가하면서 서버에 과부하가 걸리니 결국 서버가 응답하지 않는 상황까지 이어진거죠.
(사전에 Connection Pool 용량이 충분한지 계산해봤더라면 이런 상황을 예방할 수 있었을텐데요😭)&lt;/p&gt;
&lt;p&gt;다행히 MSA가 적용되어 일시적인 일부 장애로 끝났고 서버 증설로 임시 대응이 가능하였으며, 현재는 트랜잭션 전파옵션을 변경해서 이슈를 해결하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;올리브영 오늘드림 주문건수는 계속해서 늘고 있는데요. 그에 비례해서 배달대행사와 연동하는 데이터 양도 많아졌습니다.
또한 이로 인하여 트래픽도 계속해서 늘고 있는데요.&lt;/p&gt;
&lt;p&gt;위 사례을 겪으면서 배운 세 가지 교훈을 정리해 볼게요!!&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;MSA는 왜 해야하는가 : MSA가 적용되어 있어 올리브영 전체 서비스 장애로 이어지지 않았습니다.&lt;br&gt;&lt;/li&gt;
&lt;li&gt;트랜잭션 전파옵션의 중요성 : 트랜잭션 전파옵션 1줄로 서버가 멈추는 결과까지 이르게 되었습니다.😭&lt;br&gt;&lt;/li&gt;
&lt;li&gt;부하 테스트의 중요성 : 부하 테스트 한번 해보았으면 해당 문제는 배포 전에 발견했을 거 같아요.&lt;br&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;여러분의 프로젝트에서도 외부 시스템 연동이나 트랜잭션 관리로 고민하고 계신다면, 이 글이 시행착오를 줄이는 데 도움이 되길 바랍니다.&lt;/p&gt;
&lt;p&gt;그럼 전 이만 또 다른 기능을 개발하러 가보겠습니다!!!&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;!-- end --&gt;</content:encoded></item><item><title><![CDATA[SDUI의 성능 병목을 넘어: 올리브영 로컬 캐시 기반 백엔드 최적화 성공기]]></title><description><![CDATA[들어가며 빠르게 변화하는 이커머스 환경에서, SDUI(Server-Driven UI)는 선택이 아닌 필수가 되어가고 있습니다. SDUI…]]></description><link>https://oliveyoung.tech/2025-11-11/sdui_with_caffein/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-11/sdui_with_caffein/</guid><pubDate>Tue, 11 Nov 2025 15:30:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;들어가며&lt;/h2&gt;
&lt;p&gt;빠르게 변화하는 이커머스 환경에서, &lt;b&gt;SDUI(Server-Driven UI)&lt;/b&gt;는 선택이 아닌 필수가 되어가고 있습니다.&lt;/p&gt;
&lt;p&gt;SDUI는 &apos;서버가 주도하는 사용자 인터페이스&apos;라는 뜻으로, 쉽게 말해 스마트폰 앱이나 웹사이트 화면을 구성하는 요소들(버튼, 이미지, 텍스트 배치 등)을 클라이언트(앱/웹 브라우저)가 알아서 만드는 대신, 서버가 &apos;이렇게 만들어라&apos; 하고 설계도(JSON 데이터)를 전달해 주는 방식입니다.&lt;/p&gt;
&lt;p&gt;이를 통해, 과거에는 iOS, Android, Web 등 각 플랫폼이 UI 컴포넌트를 개별적으로 구현해야 했지만, 이제는 서버의 UI 구성을 통해 업데이트 없이도 서비스 화면을 실시간으로 바꿔줄 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;이는, UI 일관성 확보는 물론, 개발 리소스 절감, 비개발자의 직접적인 UI 수정 가능성 등의 이점을 가져왔습니다.&lt;/p&gt;
&lt;p&gt;그러나 SDUI의 특성 상 모든 클라이언트가 서버로부터 UI 구성을 요청·수신하기 때문에 API 응답 속도 및 네트워크 지연이 사용자 경험에 직접적인 영향을 미칩니다. 저희 팀 역시 성능 테스트 과정에서 트래픽 증가에 따른 API 지연 문제를 경험했고, 이에 대한 해결 방안으로 &lt;b&gt;로컬 캐시(Local Cache)&lt;/b&gt;를 활용한 최적화 전략을 채택했습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 올리브영이 SDUI를 도입하며 겪었던 문제점과, &lt;strong&gt;로컬 캐시 기반의 Backend API 최적화 전략&lt;/strong&gt;을 통해 어떻게 성능 병목을 해결하고 안정적인 서비스를 구축했는지 그 경험을 상세히 공유하고자 합니다.&lt;/p&gt;
&lt;p&gt;올리브영 개발 조직이 특히 더 신경쓰는 기간은 &apos;올영세일&apos; 인데요. &lt;strong&gt;초당 6만 건이 넘는 트래픽 속에서도 응답 속도 1ms 미만을 달성&lt;/strong&gt;한 사례 기반 SDUI의 도입부터 운영까지, 저희의 시행착오와 실제 서비스로 검증된 해결책이 여러분의 서비스에 큰 도움되기를 바랍니다.&lt;/p&gt;
&lt;h2 id=&quot;배경-올리브영이-sdui를-선택한-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%EA%B2%BD-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%B4-sdui%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;배경 올리브영이 sdui를 선택한 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배경: 올리브영이 SDUI를 선택한 이유&lt;/h2&gt;
&lt;p&gt;개발자라면 한 번쯤 겪어봤을 겁니다. iOS, Android, Web 등 각 플랫폼에서 동일한 UI를 중복으로 구현해야 하는 비효율, 작은 UI 수정에도 앱스토어 배포 과정을 거치며 중요한 마케팅 기회를 놓치는 답답함. 올리브영 역시 빠르게 변화하는 시장과 고객 요구사항 앞에서 이러한 문제들에 직면했습니다.&lt;/p&gt;
&lt;p&gt;우리가 겪었던 핵심적인 어려움은 다음과 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;비효율적인 개발:&lt;/strong&gt; 플랫폼별 UI 중복 구현으로 개발 리소스가 낭비되고, 팀의 피로도가 높았습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;느린 반영 속도:&lt;/strong&gt; UI 수정 시 앱스토어 배포가 필수였기에, 시장 변화에 민첩하게 대응하기 어려웠습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;일관성 없는 사용자 경험:&lt;/strong&gt; 플랫폼 간 미묘한 UI 불일치로 브랜드 경험의 일관성이 저해되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;즉, &apos;어떻게 하면 더 빠르고, 효율적이며, 일관된 사용자 경험을 제공할 수 있을까?&apos;라는 질문에 대한 답으로 &lt;strong&gt;SDUI&lt;/strong&gt; 도입을 결정했습니다.&lt;/p&gt;
&lt;p&gt;서버에서 JSON Schema 기반의 UI 컴포넌트 데이터를 제공하고 클라이언트가 이를 렌더링하는 방식은, 앱 업데이트 없이도 실시간으로 UI를 변경할 수 있는 유연성을 제공했습니다.&lt;/p&gt;
&lt;p&gt;우리는 SDUI의 잠재력을 확인하고, 우선적으로 올리브영 앱의 핵심 영역인 &lt;strong&gt;탭바&lt;/strong&gt;와 &lt;strong&gt;테마 드로워&lt;/strong&gt;에 도입을 시도하며 새로운 기술 여정의 첫걸음을 내디뎠습니다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황-성능-병목-현상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9-%EC%84%B1%EB%8A%A5-%EB%B3%91%EB%AA%A9-%ED%98%84%EC%83%81&quot; aria-label=&quot;문제 상황 성능 병목 현상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 상황: 성능 병목 현상&lt;/h2&gt;
&lt;p&gt;SDUI 전환 후 &apos;올영세일&apos;과 같은 대규모 트래픽 상황을 모의하기 위해, 실제 서비스 환경과 유사한 조건에서 다음과 같은 시나리오로 테스트를 진행했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트 도구: nGrinder&lt;/li&gt;
&lt;li&gt;테스트 환경: N개의 nGrinder 에이전트&lt;/li&gt;
&lt;li&gt;테스트 시나리오:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;사용자 수: M명의 가상 사용자 동시 접속
테스트 시간: X초 동안 지속
요청 방식: M명의 가상 사용자가 X초 동안 SDUI API에 대해 요청(Request) → 응답(Response) → 다음 요청(Next Request)을 동기 방식으로 반복 수행했습니다. 이는 실제 사용자가 UI를 로드하고 다음 액션을 수행하는 과정을 충실히 모사하여, 서버가 지속적인 부하를 처리하는 능력을 측정하기 위함입니다.
트래픽 목표: 6월 &apos;올영세일&apos; 피크 트래픽의 2배 수준의 부하 생성
측정 지표: API 응답 시간(Response Time), 초당 처리량(TPS), 네트워크 트래픽 등&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 테스트 환경과 시나리오를 통해, SDUI Backend API가 실제 대규모 트래픽 상황에 어떤 성능 변화를 보이는지 명확하게 측정할 수 있었고, 아래와 같은 문제를 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;API 응답 속도 저하&lt;/strong&gt;&lt;br&gt;
&apos;올영세일&apos; 기준 트래픽으로 성능 테스트 시 트래픽으로 인한 P50 서버 응답 시간이 733ms에 따라서 초기 로딩 시간 증가가 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;네트워크 트래픽 증가&lt;/strong&gt;&lt;br&gt;
동일한 탭바 및 테마드로워 데이터를 대규모 사용자가 반복적으로 요청하다 보니 서버와 캐시인 Redis 사이 구간의 네트워크 트래픽이 증가했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;사용자 경험 악화&lt;/strong&gt;&lt;br&gt;
API 응답 속도 저하 및 네트워크 트래픽 증가에 따른 응답 지연에 따른 테마드로워 / 탭바 렌더링이 느려지고 사용자 경험이 악화되는 것을 확인했습니다.&lt;/p&gt;
&lt;p&gt;즉, 성능 최적화 없이는 SDUI의 장점이 오히려 단점으로 전환될 수 있음을 확인했습니다.&lt;/p&gt;
&lt;h2 id=&quot;해결-전략-이중-캐시의-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EC%A0%84%EB%9E%B5-%EC%9D%B4%EC%A4%91-%EC%BA%90%EC%8B%9C%EC%9D%98-%EB%8F%84%EC%9E%85&quot; aria-label=&quot;해결 전략 이중 캐시의 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결 전략: 이중 캐시의 도입&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1차 캐시: Caffeine (Local Cache)  ➡️  네트워크 트래픽 감소 및 API 응답 속도 극대화
2차 캐시: Redis (Remote Cache)  ➡️  DB로의 부하를 최소화하고, 다중 서버 환경에서 캐시 일관성 유지
3차 저장소: Oracle DB (관계형 데이터베이스)  ➡️  백오피스에서 UI 구성 요소를 관리하는 최종 데이터 원천&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;1-캐시-정책&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%BA%90%EC%8B%9C-%EC%A0%95%EC%B1%85&quot; aria-label=&quot;1 캐시 정책 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 캐시 정책&lt;/h3&gt;
&lt;p&gt;동일 파라미터에 대한 요청은 캐시 응답으로 반환하도록 구현했고, API 단위의 키로 관리했습니다.&lt;br&gt;
(예: commonContents:tabbar::ALL:v2)&lt;/p&gt;
&lt;h3 id=&quot;2-api-응답-구조-개선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-api-%EC%9D%91%EB%8B%B5-%EA%B5%AC%EC%A1%B0-%EA%B0%9C%EC%84%A0&quot; aria-label=&quot;2 api 응답 구조 개선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. API 응답 구조 개선&lt;/h3&gt;
&lt;p&gt;캐시 효율성을 극대화하기 위해 API 설계 단계에서 &lt;b&gt;멱등성(Idempotency)&lt;/b&gt;을 보장했습니다.&lt;br&gt;
멱등성은 &apos;동일한 요청을 여러 번 보내도 항상 같은 결과가 반환되는 특성&apos;을 말합니다. 마치 리모컨으로 TV를 켤 때 한 번 누르나 여러 번 누르나 TV는 결국 켜지거나 꺼진 상태를 유지하는 것과 같습니다.&lt;br&gt;
이러한 특성을 통해 동일 입력값에 대하여 항상 동일한 출력이 반환되도록 구조화하여 캐시의 유효성을 높였습니다.&lt;/p&gt;
&lt;h3 id=&quot;3-캐시-무효화-전략-실시간성-확보와-안정성-유지를-위한-투-트랙&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%BA%90%EC%8B%9C-%EB%AC%B4%ED%9A%A8%ED%99%94-%EC%A0%84%EB%9E%B5-%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%84%B1-%ED%99%95%EB%B3%B4%EC%99%80-%EC%95%88%EC%A0%95%EC%84%B1-%EC%9C%A0%EC%A7%80%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%88%AC-%ED%8A%B8%EB%9E%99&quot; aria-label=&quot;3 캐시 무효화 전략 실시간성 확보와 안정성 유지를 위한 투 트랙 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 캐시 무효화 전략: 실시간성 확보와 안정성 유지를 위한 투 트랙&lt;/h3&gt;
&lt;p&gt;캐시는 데이터를 빠르게 제공하는 이점이 있지만, 데이터 변경 시 &lt;strong&gt;오래된 데이터(Stale Data)&lt;/strong&gt; 문제가 발생할 수 있습니다.&lt;br&gt;
SDUI는 UI가 실시간으로 변경되어야 하는 요구사항이 강하므로, 데이터 변경에 따른 캐시 무효화 전략이 매우 중요했습니다. 우리는 두 가지 전략을 조합하여 이를 해결했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3-1. 백오피스(Back Office) 연동을 통한 즉시 무효화&lt;/strong&gt; &lt;br&gt;
백오피스에서 SDUI 관련 데이터(예: 탭바 메뉴, 테마 드로워 컨텐츠)를 수정하면, 해당 변경 이벤트를 감지하여 관련된 Redis 캐시를 즉시 삭제하도록 구현했습니다.&lt;br&gt;
이로써 관리자가 UI를 변경하는 즉시 사용자에게 최신 UI가 반영될 수 있도록 했습니다.&lt;br&gt;
또한, 백오피스의 접근은 사내 내부 네트워크에서만 가능하고, 데이터 등록/수정/삭제는 명확한 인증/인가 프로토콜을 통해 등록된 특정 관리자 계정만 데이터 조작을 가능케 제한하여서 캐시 오염 시나리오를 방지하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3-2. 주기적인 배치(Batch) 작업을 통한 캐시 갱신&lt;/strong&gt; &lt;br&gt;
혹시 모를 캐시 누락이나 시스템 오류로 인한 데이터 불일치를 방지하고, 캐시의 안정성을 확보하기 위해 하루에 한 번(일 1회 주기) 배치(Batch) 작업을 실행했습니다.&lt;br&gt;
이 배치는 단순히 캐시를 비우는 것을 넘어, 주요 SDUI 데이터를 미리 조회하여 캐시를 신규 데이터로 &lt;strong&gt;사전 캐싱(Pre-warming)&lt;/strong&gt; 하는 역할도 수행합니다. 이는 시스템 부하가 적은 새벽 시간대에 실행하여, 주간 피크 타임에 콜드 캐시(Cold Cache)로 인한 응답 지연이 발생하는 것을 최소화했습니다.&lt;/p&gt;
&lt;p&gt;이 두 가지 전략을 통해 우리는 UI 변경의 실시간성과 캐시 데이터의 안정성이라는 두 마리 토끼를 모두 잡을 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;4-아키텍쳐-및-caffeine-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EB%B0%8F-caffeine-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;4 아키텍쳐 및 caffeine 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 아키텍쳐 및 Caffeine 설정&lt;/h3&gt;
&lt;p&gt;올리브영 SDUI Backend API는 Spring Boot와 Kotlin 기반으로 개발되었으며, 클라이언트 요청 처리부터 데이터 조회까지 효율적으로 분배하기 위해 다음과 같은 계층 구조를 가집니다. 각 계층은 명확한 책임을 가지며, 특히 캐시 전략이 서비스 로직과 유기적으로 결합되어 있습니다.&lt;/p&gt;
&lt;p&gt;우리는 &lt;strong&gt;Caffeine을 1차 로컬 캐시&lt;/strong&gt;로 활용하여 JVM 메모리 내에서 고성능 캐싱을 구현했습니다. 다음은 &lt;code class=&quot;language-text&quot;&gt;CacheConfig&lt;/code&gt;에 정의된 Caffeine 캐시 설정의 주요 부분입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@EnableCaching&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CacheConfig &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;token annotation builtin&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;localCacheManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CacheManager &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cacheManager &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SimpleCacheManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; caches &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;CaffeineCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                CaffeineCacheKeys&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LOCAL_TABBAR_CACHE_KEY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                Caffeine&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expireAfterWrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;TTL_TEN_SECONDS&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TimeUnit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SECONDS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;CaffeineCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                CaffeineCacheKeys&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LOCAL_THEMEDRAWER_CACHE_KEY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                Caffeine&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expireAfterWrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;TTL_TEN_SECONDS&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TimeUnit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SECONDS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        cacheManager&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setCaches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;caches&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cacheManager
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// RedisCacheManager 설정 등&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;expireAfterWrite(TTL_TEN_SECONDS, TimeUnit.SECONDS)&lt;/code&gt;:&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;캐시 데이터가 마지막 쓰기(생성/업데이트) 시점으로부터 10초가 지나면 만료되도록 설정했습니다.&lt;br&gt;
올리브영 트래픽 특성 상 피크 타임의 트래픽만 무사히 넘기면 되기 때문에 TTL을 짧게 설정했습니다.&lt;br&gt;
또한, 클라우드 환경 특성 상 로컬 캐시에 대한 중앙 관리가 어려운 점이 있어, 짧은 TTL을 이용하여 데이터 갱신 시 최신 데이터 반영 시간을 최소화하고자 했습니다.&lt;/p&gt;
&lt;h3 id=&quot;5-코드-예시-캐시-계층별-역할&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EC%BD%94%EB%93%9C-%EC%98%88%EC%8B%9C-%EC%BA%90%EC%8B%9C-%EA%B3%84%EC%B8%B5%EB%B3%84-%EC%97%AD%ED%95%A0&quot; aria-label=&quot;5 코드 예시 캐시 계층별 역할 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 코드 예시: 캐시 계층별 역할&lt;/h3&gt;
&lt;p&gt;TabbarController, IntegratedSduiService, TabbarService는 각각의 역할을 분리하여 단일 책임 원칙(SRP)을 준수하고, 캐시 로직과 비즈니스 로직을 명확히 구분합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TabbarController (클라이언트 요청 처리)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@RequestMapping&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/tabbar/information&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; method &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;RequestMethod&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;GET&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchTabbarInformation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@RequestParam&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;tabbarType&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; tabbarType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TabbarResponse &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; integratedSduiService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchTabbarInformation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tabbarType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;TabbarController&lt;/code&gt;는 클라이언트의 HTTP 요청을 받아 &lt;code class=&quot;language-text&quot;&gt;IntegratedSduiService&lt;/code&gt;를 호출하는 게이트웨이 역할을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IntegratedSduiService (1차 로컬 캐시 담당)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Cacheable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    cacheManager &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;localCacheManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;localTabbar&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{#tabbarType}&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    unless &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#result == null&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchTabbarInformation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tabbarType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TabbarResponse &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; tabbarService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getTabbarData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tabbarType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;IntegratedSduiService는 SDUI에 대한 통합 비즈니스 로직을 담당하며, 여기에 &lt;b&gt;1차 로컬 캐시(Caffeine)&lt;/b&gt;를 적용했습니다.&lt;br&gt;
&lt;code class=&quot;language-text&quot;&gt;@Cacheable&lt;/code&gt;은 로컬 캐시에 데이터가 존재하면 네트워크 호출 없이 즉시 반환하여 가장 빠른 응답을 제공하는 어노테이션입니다. 만약 로컬 캐시에 데이터가 없다면, 하위 계층인 tabbarService를 호출하여 다음 캐시 계층으로 조회를 위임합니다. 단,
&lt;code class=&quot;language-text&quot;&gt;unless = &quot;#result == null&quot;&lt;/code&gt; 조건을 통해 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; 응답은 제외하고 유효 데이터만 캐싱합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TabbarService (2차 원격 캐시 및 DB 조회 담당)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Cacheable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    cacheManager &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;redisCacheManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;redisTabbar&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{#tabbarType}&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    unless &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#result == null&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTabbarData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tabbarType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TabbarResponse &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; tabbarRepository&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getTabbarData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tabbarType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;TabbarService는 &lt;b&gt;2차 원격 캐시(Redis)&lt;/b&gt;를 담당합니다.&lt;br&gt;
로컬 캐시에 데이터가 없을 경우 redisCacheManager를 통해 Redis에서 데이터를 조회합니다.&lt;br&gt;
Redis에도 데이터가 없다면 최종적으로 tabbarRepository를 호출하여 Oracle DB에서 데이터를 가져옵니다.&lt;br&gt;
이처럼 캐시 계층을 명확히 분리함으로써, DB 부하를 최소화하고 다중 서버 환경에서도 캐시 데이터의 일관성을 효과적으로 유지할 수 있도록 설계했습니다.&lt;/p&gt;
&lt;h2 id=&quot;결과-성과-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC-%EC%84%B1%EA%B3%BC-%EC%A7%80%ED%91%9C&quot; aria-label=&quot;결과 성과 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과: 성과 지표&lt;/h2&gt;
&lt;p&gt;SDUI, 그리고 로컬 캐시의 성과 지표는 9월 &apos;올영세일&apos;에서 명확하게 드러났습니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1042px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5be3148ecc7b018382305ca23d775641/b0f6f/sdui_response_time.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA50lEQVR42qWS6W7DIBCE/f7vmdaJYu77mu5CDjW/2sTSJ7DBszMLm3UO+/mC6yEgpMIYA588W4wJSisYawiL1trbYuxlYwHjy0S7AhsbfO5IpaPU8Wdq42RjOYyxIqWOTEKBBBkXqJCrVOiJfZlb39Y32hfo/xk55Uwx++xd7/1G+zUfY62vcYGX97XOkfvAIRSk1OSM3KaEUgpSLsiFoeohzgOLKU94n7EOxtjHKJWeopu2Fadd4vR1xfcucL4oil9uTV4nzn0+BN8CCa0NtSbDh/DAeQ9PTMG71RV5xW79HuF/cA9/AO15dCttELCiAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5be3148ecc7b018382305ca23d775641/263a4/sdui_response_time.webp 480w,
/static/5be3148ecc7b018382305ca23d775641/a6361/sdui_response_time.webp 960w,
/static/5be3148ecc7b018382305ca23d775641/a36cd/sdui_response_time.webp 1042w&quot; sizes=&quot;(max-width: 1042px) 100vw, 1042px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5be3148ecc7b018382305ca23d775641/9aebd/sdui_response_time.png 480w,
/static/5be3148ecc7b018382305ca23d775641/a91f8/sdui_response_time.png 960w,
/static/5be3148ecc7b018382305ca23d775641/b0f6f/sdui_response_time.png 1042w&quot; sizes=&quot;(max-width: 1042px) 100vw, 1042px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5be3148ecc7b018382305ca23d775641/b0f6f/sdui_response_time.png&quot; alt=&quot;세일 기간 평균 응답 시간&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;2025년 9월 &apos;올영세일&apos; 첫째날 평균 응답 시간 (출처: DataDog APM 대시보드)&lt;/figcaption&gt;
    &lt;/div&gt;
    &lt;div class=&quot;photo-item&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1081px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/11dfaa049116783db511209ee039afce/0c139/sdui_request_count.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA80lEQVR42n2R65aDIAyEff8H3dPWVigCgYQwG7yc1h+76ncgjA5xmEQaciaUUtFaw39X71euWt/WpsqMsK5YY0St/CV2NO2QgzE35U/GPZ5pdHXt7PNSqorMHbV9KAOxueybnOjR8qSqoCLIRZFK26F9fAcGZQbXhmp6zgIiAXMDi9W2IRnRNOLDcAyvJeB+d2ZAG+taEGwkqmbGVid4Hyxn3rJ+h4jFvnGGdwHz7C0y2g1jbphN+Lk9cXs4PJ4eleUSOFvOi3OGR0zJsq5mXKzjvNXpWNs7HP/f1HJUiGUpIlum58GcjGiaoV+a6olu9TD8BbJ7JgHBt4a+AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/11dfaa049116783db511209ee039afce/263a4/sdui_request_count.webp 480w,
/static/11dfaa049116783db511209ee039afce/a6361/sdui_request_count.webp 960w,
/static/11dfaa049116783db511209ee039afce/35a5e/sdui_request_count.webp 1081w&quot; sizes=&quot;(max-width: 1081px) 100vw, 1081px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/11dfaa049116783db511209ee039afce/9aebd/sdui_request_count.png 480w,
/static/11dfaa049116783db511209ee039afce/a91f8/sdui_request_count.png 960w,
/static/11dfaa049116783db511209ee039afce/0c139/sdui_request_count.png 1081w&quot; sizes=&quot;(max-width: 1081px) 100vw, 1081px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/11dfaa049116783db511209ee039afce/0c139/sdui_request_count.png&quot; alt=&quot;세일 기간 최대 요청 수&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;2025년 9월 &apos;올영세일&apos; 첫째날 최대 요청 수 (출처: DataDog Infrastructure 모니터링)&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;최대 초당 63.3k TPS (Transactions Per Second)를 견뎠으며, 이는 &lt;strong&gt;1초에 약 6만 3천 건의 요청을 처리&lt;/strong&gt;했다는 의미입니다.&lt;br&gt;
P90 Response Time은 최대 1ms가 되지 않는 결과를 보여줬습니다. P90은 &apos;전체 요청 중 90%가 이 시간 안에 처리되었다&apos;는 성능 지표인데요, &lt;strong&gt;대부분의 사용자가 1ms보다 훨씬 빠른 시간 안에 응답&lt;/strong&gt;을 받았다는 압도적인 성능 개선을 의미합니다.
&lt;br&gt;뿐만 아니라, iOS, Android, Web 등 모든 플랫폼에서 동일한 탭바와 테마드로워 화면을 볼 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;결론-및-향후-계획&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0-%EB%B0%8F-%ED%96%A5%ED%9B%84-%EA%B3%84%ED%9A%8D&quot; aria-label=&quot;결론 및 향후 계획 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결론 및 향후 계획&lt;/h2&gt;
&lt;p&gt;본 사례를 통해 &lt;strong&gt;로컬 캐시&lt;/strong&gt;가 SDUI 도입의 안정성과 성능을 동시에 보장하는 핵심 요소임을 확인했습니다.&lt;br&gt;
성능 테스트를 기반으로 한 문제 인식과 그에 대한 해결책을 제안하는 과정을 통해서 단순히 이론이나 설계로만 답을 찾는 경험이 아닌 실제 데이터 기반의 답을 찾는 경험을 한 점은 인상 깊었습니다.&lt;br&gt;
향후에는 SDUI를 적용하는 영역을 점차 확대할 예정이며, 다른 API 영역에도 &lt;strong&gt;로컬 캐시 도입을 적극적으로 검토&lt;/strong&gt;할 예정입니다.&lt;br&gt;
올리브영의 사례가 여러분의 서비스 최적화에 실질적인 영감이 되길 바라며, 궁금한 점이나 비슷한 경험이 있다면 댓글로 공유해 주세요.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 3. Nx를 활용한 마이크로프론트엔드]]></title><description><![CDATA[안녕하세요! 발레를 좋아하는 올리브영의 프론트엔드 개발자 개발레리나🩰 입니다. 마이크로프론트엔드(Micro Frontend Architecture,이하 MFE…]]></description><link>https://oliveyoung.tech/2025-11-10/what-is-MFE-part3/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-10/what-is-MFE-part3/</guid><pubDate>Mon, 10 Nov 2025 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 발레를 좋아하는 올리브영의 프론트엔드 개발자 개발레리나🩰 입니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;마이크로프론트엔드(Micro Frontend Architecture,이하 MFE)&lt;/strong&gt;는 하나의 거대한 애플리케이션을 작고 독립적인 모듈로 나누고, 각각의 모듈을 별도로 개발하고 운영할 수 있는 환경을 구축하는 아키텍처 패턴입니다.&lt;/p&gt;
&lt;p&gt;이렇게 모듈을 분리하면 각 도메인이나 기능 단위로 코드를 관리할 수 있어 대규모 코드베이스에서도 리스크를 줄이고, 빠른 개발 속도를 유지할 수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;만약 MFE에 대한 개념 자체가 생소하거나, 더 자세한 개념과 장단점이 궁금하시다면,&lt;/p&gt;
&lt;p&gt;➡️ 시리즈 첫 번째 글인 &lt;strong&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-07-22/what-is-MFE-part1/&quot;&gt;대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 1. 마이크로프론트엔드 너 뭐야?&lt;/a&gt;&lt;/strong&gt; 먼저 읽고 오시는 걸 추천드려요 😊&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;MFE환경을 구축할 수 있는 대표적인 기술로는 Webpack을 활용한 모듈 페더레이션이 있습니다. 이 기술을 사용하면 서로 다른 앱을 런타임에 하나로 통합할 수 있게 되는데요,&lt;/p&gt;
&lt;p&gt;이 기술에 대한 자세한 설명과 실제 구현 방식이 궁금하시다면,&lt;/p&gt;
&lt;p&gt;➡️ 시리즈 두 번째 글인 &lt;strong&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-11-06/what-is-MFE-part2/&quot;&gt;대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 2. 모듈 페더레이션 PoC&lt;/a&gt;&lt;/strong&gt;  글을 참고해 주세요! 🦊&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;모듈 페더레이션은 멀티레포뿐 아니라 모노레포 환경에서도 구현할 수 있습니다. 특히 이번에 소개할 &lt;u&gt;&lt;strong&gt;Nx&lt;/strong&gt;는 &lt;strong&gt;간단한 명령어와 설정만으로 모듈 페더레이션을 손쉽게 적용할 수 있도록 지원&lt;/strong&gt;&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p&gt;또한 모노레포의 특성상 런타임 통합뿐 아니라 빌드타임 통합도 가능하며, 공통 설정과 의존성 관리까지 체계적으로 구성할 수 있기 때문에 협업과 유지보수 측면에서도 큰 장점이 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;올리브영은 지속적인 서비스 성장과 함께 프론트엔드 코드베이스가 빠르게 확장되고 있습니다.&lt;/strong&gt; &lt;br/&gt;
이에 따라 여러 도메인과 기능이 복잡하게 얽히면서, &lt;u&gt;각 모듈 간의 의존성을 명확히 파악하고 관리하는 것&lt;/u&gt;이 점점 더 중요해지고 있습니다.
또한 새로운 기능을 빠르게 출시해야 하는 비즈니스 요구사항과 함께, &lt;u&gt;각 팀이 독립적으로 개발하고 배포할 수 있는 유연성도&lt;/u&gt; 필요해지고 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;이런 상황에서 저희는 모노레포 환경에서 MFE를 구축할 수 있는 도구들을 검토하게 되었고, 그 과정에서 Nx를 실험하고 연구하게 되었습니다. Nx는 단순히 모노레포를 관리하는 도구를 넘어서, MFE와 같은 복잡한 아키텍처를 안정적으로 운영하는 데 필요한 기능들을 통합적으로 제공한다는 점에서 저희의 선택지가 되었습니다. (&lt;a href=&quot;https://nx.dev/&quot;&gt;&lt;strong&gt;Nx 공식 사이트 바로가기&lt;/strong&gt;&lt;/a&gt;)&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;이번 글에서는 &lt;strong&gt;⌜대규모 프론트엔드 아키텍처의 새로운 패러다임⌟&lt;/strong&gt; 시리즈의 마지막 편으로, 올리브영이 왜 이 시점에 Nx를 고민하고 실험하게 되었는지 그 배경을 함께 살펴보고, Nx라는 모노레포 도구를 활용해 빌드타임과 런타임 환경에서 마이크로프론트엔드를 구현하는 방법을 실제 코드 예시와 함께 소개합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;한-줄-요약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%9C-%EC%A4%84-%EC%9A%94%EC%95%BD&quot; aria-label=&quot;한 줄 요약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📝 한 줄 요약 &lt;/h2&gt;
&lt;p&gt;&lt;mark&gt;Nx의 특징을 이해하고, 이 도구를 활용하여 빌드타임 통합 방식과 런타임 통합 방식으로 MFE를 구현하는 과정을 실제 코드로 실습하며 최종 결과물을 확인합니다.😁&lt;/mark&gt;&lt;/p&gt;
&lt;center&gt;
&lt;table style=&quot;border: 2px solid #e0e0e0; border-collapse: collapse; width: 100%; max-width: 800px; margin: 20px auto; box-shadow: 0 4px 8px rgba(0,0,0,0.1); table-layout: fixed;&quot;&gt;
   &lt;tbody&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 15px; text-align: center; background-color: #f8f9fa; font-weight: bold; font-size: 16px; word-wrap: break-word; width: 50%;&quot;&gt;
            &lt;center&gt;빌드타임 통합 방식&lt;/center&gt;
         &lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 15px; text-align: center; background-color: #f8f9fa; font-weight: bold; font-size: 16px; word-wrap: break-word; width: 50%;&quot;&gt;
            &lt;center&gt;런타임 통합 방식(w. 모듈 페더레이션)&lt;/center&gt;
         &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 20px; text-align: center; width: 50%;&quot;&gt;
            &lt;div style=&quot;width: 100%; max-width: 350px; display: block; margin: 0 auto;&quot;&gt;
                &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1324px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4924ead4971bb54508047aedaebf9ce3/a6a86/1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 163.54166666666669%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAABYlAAAWJQFJUiTwAAADpElEQVR42u2W228TRxSH/XegqlBQUGlE8MZeX9a79vruxHaauCS0uXFHqM9BVILEmIgSAaJSVfpUqeoDIXXVqoQSKqW5OAZkQnZtTNUCgUAfqlwgBN4AEf96ZjAtUpwLEk8VD5/O+szMt3PWM7tjcK43QTIbsbpMwPp1dpjKnShbXYmydwWsXSOiYoMCS4Ub778n8pzDGkY00Ai/uhluKQrjBhlrVpVj3TsVsGzywOCxReC0BKFYq0kcgt3kg2Ty03UApo0qPiizQihX4JOjCAabYJLq4bWFsMVdC5+zFl57A+o9bQhK2yCLIRi8MjUoMYR9HyPk3oyAq46IwSvXwGmrphv44bJXU1sMdZFtUF0NCCq1qA9sQZWnHhFvM2K+T1GltNAN6mAwb6KSBBe8ShiqXAXZFuCIRhfMRgVmwQmrWYVxox3WShU2huiFLIUgWwOQLF7q54DA2gUVBocY4GVajB74HGG0RpvQHP4ELRRbo838d0ukCVvpmuU4kUbq04jWmmb45Qhs/BH5wVwGt1QDj+NDWCqD6NizH8jkURjRgXSRi9n/4isUWC5znY8RhQA85GEug2qPwk2Igh/tu9rwZCCDqXNDmO1LYe5CmuTavwLG/PBVzvOhUZ7v2N1GJfu5g7kWCO/9cAHjZ3pxN3ke2W+TuNndi9FvenAv2Ye/qG1+MAMMj6IwdIWE+vLCqf4M7vRdJNK4cS6F8fNp/P7zIMZ/GcEE5WeHNTwayeJhSsfTy/lFhNIL4UES9uYm8YU+g1P6NE5lZzhfF+NXlP9ybIqYxAltGj/pf6OdhGIpoaUoPK1PIq49wNHcLD5nZIuROJp7yOmi3CH9Pk5np5YX9mQncVi7j/jATXQM3EAiPYH40DhxCwd+zfPYmb6LTv0BulciPEPCBAkTw7fR/tufOJS6jfjgLRzs/wOf9WpcnhiZQGKlwm4SsnK6WInX5njpvNRrVGp+7kXMvUbJSXqGR7QZHB+bxjFtIccJ9occoSq+p76lha8sm2eX8pijJfGYlsZSsD7PFl02RWEHCdliLbBdQLthKQor2SkvhWxbvdxii/H8dYRvfIZvhW+F/z9hWqMP0ZsU0re2kBoDlpnlioTx3ftwp+csps/2AySdH7zCB5aCtbEDAHvlmYUSQnaXzr378WPXSVz/LglczqHAZsNmWwLedilHM9y3cIYuOtIpdO6rUmPY27AHu2I7sb12O3bU7ViSndSvWv2Ij2UOJvwHbEPLaYpHBGwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4924ead4971bb54508047aedaebf9ce3/263a4/1.webp 480w,
/static/4924ead4971bb54508047aedaebf9ce3/a6361/1.webp 960w,
/static/4924ead4971bb54508047aedaebf9ce3/17eeb/1.webp 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4924ead4971bb54508047aedaebf9ce3/9aebd/1.png 480w,
/static/4924ead4971bb54508047aedaebf9ce3/a91f8/1.png 960w,
/static/4924ead4971bb54508047aedaebf9ce3/a6a86/1.png 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4924ead4971bb54508047aedaebf9ce3/a6a86/1.png&quot; alt=&quot;빌드타임 통합 방식 결과 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
            &lt;/div&gt;
         &lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 20px; text-align: center; width: 50%;&quot;&gt;
            &lt;div style=&quot;width: 100%; max-width: 350px; display: block; margin: 0 auto;&quot;&gt;
                &lt;img src=&quot;/0d629bf1c3ea0d2aa5a729aabfe82c31/2.gif&quot; style=&quot;max-width: 100%; height: auto; border-radius: 8px; object-fit: contain;&quot; alt=&quot;런타임 통합 방식 결과 이미지&quot;&gt;
            &lt;/div&gt;
         &lt;/td&gt;
      &lt;/tr&gt;
   &lt;/tbody&gt;
&lt;/table&gt;
&lt;/center&gt;
&lt;h1 id=&quot;nx가-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#nx%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;nx가 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Nx가 무엇인가요?&lt;/h1&gt;
&lt;p&gt;Nx는 &lt;b&gt;대규모 프로젝트에서 여러 앱과 라이브러리를 하나의 저장소에서 체계적으로 관리할 수 있도록 돕는 모노레포 도구&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;간단한 명령어를 통해서 특정 모듈의 변경을 감지하고, 변경이 일어난 모듈에 대해서만 명령을 실행하는 등의 기능을 제공합니다. 특히 remote caching을 지원하여 이전에 빌드된 모듈의 경우 빠르게 빌드 결과물을 불러올 수 있습니다.&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;또한 React, Next.js, Angular 같은 프레임워크를 위한 템플릿과 CLI를 제공하여 독립된 앱을 빠르게 구성하여 출시할 수 있게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;주요-기능-요약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9A%94-%EA%B8%B0%EB%8A%A5-%EC%9A%94%EC%95%BD&quot; aria-label=&quot;주요 기능 요약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;주요 기능 요약&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Computation Caching&lt;/strong&gt;: 이전에 실행한 빌드/테스트 결과를 캐싱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Affected&lt;/strong&gt;: 변경된 코드에만 영향을 받는 앱/라이브러리만 선택적으로 빌드/테스트&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graph&lt;/strong&gt;: 앱과 라이브러리 간의 의존성을 시각화해 확인할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI&lt;/strong&gt;: Angular, React, Next.js, Express 등 개발 환경 구성 지원 → 초기 개발 비용 절감&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;왜-nx인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-nx%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;왜 nx인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 Nx인가요?&lt;/h1&gt;
&lt;p&gt;Nx는 모노레포 환경에서 MFE를 구축하기 위한 최적화된 구조를 제공합니다.&lt;/p&gt;
&lt;p&gt;다만, 이러한 유사한 기능을 제공하는 다른 도구들도 있는데요, 그렇다면 다른 선택지들 중에서도 왜 Nx를 선택해야 할까요?&lt;/p&gt;
&lt;p&gt;올리브영처럼 지속적으로 성장하는 플랫폼에서는 단순히 빌드 속도를 개선하는 것을 넘어서, 장기적인 관점에서 개발 생산성과 유지보수성을 극대화할 수 있는 도구가 필요합니다. 특히 복잡하게 얽히는 앱과 라이브러리 간의 의존성을 한눈에 파악할 수 있는 기능과, 새로운 기술 스택 도입 시 확장성이 뛰어난 플러그인 생태계는 대규모 코드베이스를 관리하는 데 필수적이라고 판단했습니다.&lt;/p&gt;
&lt;p&gt;Nx와 유사한 기능을 제공하는 대표적인 도구인 Turborepo와 비교해보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;nx-vs-turborepo&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#nx-vs-turborepo&quot; aria-label=&quot;nx vs turborepo permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Nx vs Turborepo&lt;/h3&gt;
&lt;center&gt;
&lt;table style=&quot;border: 2px solid #e0e0e0; border-collapse: collapse; width: 100%; max-width: 800px; margin: 20px auto; box-shadow: 0 4px 8px rgba(0,0,0,0.1);&quot;&gt;
   &lt;tbody&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 15px; text-align: center; background-color: #f8f9fa; font-weight: bold; font-size: 16px;&quot;&gt;
            &lt;center&gt;항목&lt;/center&gt;
         &lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 15px; text-align: center; background-color: #f8f9fa; font-weight: bold; font-size: 16px;&quot;&gt;
            &lt;center&gt;Nx&lt;/center&gt;
         &lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 15px; text-align: center; background-color: #f8f9fa; font-weight: bold; font-size: 16px;&quot;&gt;
            &lt;center&gt;Turborepo&lt;/center&gt;
         &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; font-weight: 500;&quot;&gt;의존성 시각화&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #28a745;&quot;&gt;지원&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #dc3545;&quot;&gt;미지원&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; font-weight: 500;&quot;&gt;Affected 기능&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #28a745;&quot;&gt;지원&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #28a745;&quot;&gt;지원&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; font-weight: 500;&quot;&gt;Plugin 생태계&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #28a745;&quot;&gt;풍부함 (Next.js, React, Storybook 등)&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #ffc107;&quot;&gt;상대적으로 제한적&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; font-weight: 500;&quot;&gt;초기 학습 난이도&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #ffc107;&quot;&gt;다소 높음&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #28a745;&quot;&gt;낮음&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; font-weight: 500;&quot;&gt;설정 유연성&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #28a745;&quot;&gt;높음&lt;/td&gt;
         &lt;td style=&quot;border: 1px solid #e0e0e0; padding: 12px; text-align: center; color: #ffc107;&quot;&gt;보통&lt;/td&gt;
      &lt;/tr&gt;
   &lt;/tbody&gt;
&lt;/table&gt;
&lt;/center&gt;
&lt;p&gt;Turborepo도 강력한 모노레포 도구이지만, Nx는 MFE와 같은 복잡한 아키텍처를 구성할 때 더 많은 가이드와 기능을 제공합니다. 특히 특히 CLI 기반의 프로젝트 생성과 통합, 의존성 시각화와 같은 기능은 실무에서 개발 생산성을 크게 높여줍니다.&lt;/p&gt;
&lt;p&gt;무엇보다도 &lt;u&gt;Nx에서는 이 모든 구성 요소들은 프로젝트의 목적과 규모에 따라 점진적으로 도입할 수 있도록 각 기능이 모듈화된 구조&lt;/u&gt;로 설계 되어 있습니다. 그래서 작은 팀이든 대규모 조직이든, 필요에 따라 유연하게 적용할 수 있다는 점이 Nx의 큰 장점 중 하나입니다.&lt;/p&gt;
&lt;h1 id=&quot;nx-기반-모노레포에서-마이크로프론트엔드-환경-구축해보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#nx-%EA%B8%B0%EB%B0%98-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC%EC%97%90%EC%84%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%B4%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;nx 기반 모노레포에서 마이크로프론트엔드 환경 구축해보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Nx 기반 모노레포에서 마이크로프론트엔드 환경 구축해보기&lt;/h1&gt;
&lt;p&gt;이제 실제 코드 예시를 통해 Nx에서 마이크로프론트엔드를 빌드타임과 런타임 방식으로 어떻게 구현할 수 있는지 살펴보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;빌드타임-통합하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%8C%EB%93%9C%ED%83%80%EC%9E%84-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;빌드타임 통합하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;빌드타임 통합하기&lt;/h2&gt;
&lt;p&gt;빌드타임 통합은 여러 앱이나 모듈을 하나의 메인 앱 안에 포함시켜, 최종적으로 하나의 번들로 함께 빌드하고 배포하는 방식입니다. &lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b8ed6963b6b92f85045552456656d086/a52c7/3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABvUlEQVR42oWTO5ebMBCF/f9/xNYpUm+bMlXKNLsnxw62eRr0fgK6GQnj2EVOig+hkbga3RkO67oipfRCjmWWZcG+vsfiPCPE+JhvJCz390N+5A2cc0gpX8SsNbBGFoyW8E5j6FtU1YliqmDKuoJzBpF0DjOd6JUC6zrI2w3R+/tpCZzVkOw32uYTbf0Byc80P4LdfkHLK4b2Ez2t5T1aXOFD2AQTiYAmoHEm1pSvCRJs4I0AqxuM1yscZWnVGYofoSmzc8tw6QWU4jDyQp/vgloBghdma4sFMc6YxhqWjwhNA5cFxQQjKvDhA90w4DJGVDePiU2b4J6hDSuMTzCUpHWePCHvrMPQVXTGCCUcBNOUiUBII5iqwChu6DvtFvJ+IgueBJlJ6DUwEMYvAF2ZLCSxBoKyrwaH0+hxu9QYvryhe/9K1z4j2J4s6YpY9vQhKFwCsyhYP1NBllIUQR4qzSkjuhYhxwHixzeIn9/Jz3PBE1aeimAI9ypnv+K8bMQnQd6WiorpBDGewGjUroUy9RYjcoXFeCRbsmDY+nDNDbzz1Id5QwgekcYHcWNb+0vuwZzc4bXj/8HTn/I//gBaQlGgeBE4ZwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b8ed6963b6b92f85045552456656d086/263a4/3.webp 480w,
/static/b8ed6963b6b92f85045552456656d086/a6361/3.webp 960w,
/static/b8ed6963b6b92f85045552456656d086/0b34d/3.webp 1920w,
/static/b8ed6963b6b92f85045552456656d086/b0b5b/3.webp 2024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b8ed6963b6b92f85045552456656d086/9aebd/3.png 480w,
/static/b8ed6963b6b92f85045552456656d086/a91f8/3.png 960w,
/static/b8ed6963b6b92f85045552456656d086/ac7a9/3.png 1920w,
/static/b8ed6963b6b92f85045552456656d086/a52c7/3.png 2024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b8ed6963b6b92f85045552456656d086/ac7a9/3.png&quot; alt=&quot;빌드타입 통합 구조도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;빌드타임 코드 통합 시 MFE 구조도&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;1. 워크스페이스 생성하기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Nx에서 제공하는 CLI를 사용해 모노레포 워크스페이스 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;shell&quot;&gt;&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;npx create-nx-workspace@latest &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1802px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ecbc0efb806e4a6c0da72b49b3cfbd88/794cd/4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 39.37500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABF0lEQVR42o2RWXKDMBBEfZCYXQgMAiEhic32Xw6QHCAfuULu3xkUm1S5KjgfXaX1TffMoaxbiKrA6/sH3j6/0CiDsqqRFyccwwRB9KMwTr0e9/ezYxjjJYhwaKSEMhZN0yDPGVhegPHSP1wfPGr9+Jc8UNgLQqoQZzmilCHNOFLGkWQE54V3mlERXp4Q0/0e1AP7cYHqLTptIFUPITuUdYOUVwgTtsWLkmyLtgt044jlcsUwzdDWwc0TpCZwqyA6jYQVCG7x92Ab0LgBbiSYIdgwwZLcdIZ1E6pWe6fH8Dns1yE5G+czhnkhlwvMMEPoEXVnUFSCeskpavov6G3KCi1Fk0r7Hq7rtqO4rSSHioZR0cDu0OfAb6jF57qRuE89AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ecbc0efb806e4a6c0da72b49b3cfbd88/263a4/4.webp 480w,
/static/ecbc0efb806e4a6c0da72b49b3cfbd88/a6361/4.webp 960w,
/static/ecbc0efb806e4a6c0da72b49b3cfbd88/57146/4.webp 1802w&quot; sizes=&quot;(max-width: 1802px) 100vw, 1802px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ecbc0efb806e4a6c0da72b49b3cfbd88/9aebd/4.png 480w,
/static/ecbc0efb806e4a6c0da72b49b3cfbd88/a91f8/4.png 960w,
/static/ecbc0efb806e4a6c0da72b49b3cfbd88/794cd/4.png 1802w&quot; sizes=&quot;(max-width: 1802px) 100vw, 1802px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ecbc0efb806e4a6c0da72b49b3cfbd88/794cd/4.png&quot; alt=&quot;워크스페이스 생성 콘솔창&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;이 명령어를 실행하면 위 이미지와 같이 프롬프트에서 새로운 워크스페이스 구성을 위한 여러 설정들을 단계별로 선택할 수 있습니다. &lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;저는 nx-example이라는 이름으로 Nx 워크스페이스를 생성하였고, main-app이라는 이름의 Next.js 기반의 웹 어플리케이션을 초기 앱으로 설정하였습니다.&lt;/p&gt;
&lt;p&gt;그 외의 ESLint, Prettier, 스타일 라이브러리나 테스팅 도구 등의 개발환경설정들도 자유롭게 선택하시면 됩니다.&lt;/p&gt;
&lt;p&gt;모든 설정이 완료되면 아래와 같은 구조로 워크스페이스가 구성됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 732px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a67511c6bb8a92bca182f2991a387417/61a54/5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 157.70833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAYAAAASYli2AAAACXBIWXMAABYlAAAWJQFJUiTwAAAC8ElEQVR42p1VWW8SURidH2BhGGCAWWE6LHbKOuwwFGirNdaoMdoaY0za+OCDiT7584/3XLpQE1tmHr62aXpPz/Z9KMViEYZhoFQqwbIsmKaJQqEAXdeRy+X+O/l8Xr7hEINv+LPCL67rwvM8CcZ5CmwbkGAPAPlDueyi2TyUf3T/IIdsNvv4aPyubU0WimGYsG0H3V6I9foYbrkCVc1A0whK6fFGWZ5d4PzDFa5//MG7i+/4cvULl99+4uv1b6xefUZ0+hGLF5+enKOXl5iu3kLx64doBB2EwwlOz87x+s17tHtDdMIh/HoAr3qw81T2G1DyuazwLAtTJB32etLPvb1nclQ1jUxGhbbraBmGwpQKqPoehsOhTJj1ua9QUZjNoPI7jUIjTauAWt3GdDrDYDDAZDLBYrHAbDaD7/uCZeZBAx4bWRu9aMD2qpgLgDAM0RPS+XtN03YC+QdQR6FkwvEbiOZzIXuE0WiEdrstN4jdigVIyYZZFAnZ6PcHaLVa0svxeCwCKkuWu8q9ASwK83XUa4bwbC7ByJAech3j+LeRLBmWRBd9zIVkAk2nU8mQScf1UdHFzpbKPmqDCGG3I9kxXTLlssf28O58GZvzZdu29I4XKG7CEpDlpVesC5PlsIv8R3HZbRgKWeWKJ2VSbrfbRaPRkBuTCLBQJOA+1qsVjk9OZBi3BzOR5A3DipRMMCa8XC4ly0QMbVtHreaIUg+lbK6dqqri2uwlBwwCD1F0JFlGUST7SD8dx4nfQ8sSW1J3RRg9MR1UhHwOq8MNid1Dx9Hx/HlZ+DeRzAjEPrKHSbp4I3lfhDEXYRzJUAhMP/v9fjLAgwNe6/HdyjEY9jCVSsUH1E0XjVYXx+tNDxkKQekf044NaFl5cbpcwWrTQ8okIDem2WwmCKVaRSAk9kVl6B8ZcpeDIBD9rMUHLFWq8IK2PF237DqdjgRKp9PxJefFw0w6hZR4zBC4IbdAcS711keALj9P+Xh7khwGAv4FfexWwfvbWPcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a67511c6bb8a92bca182f2991a387417/263a4/5.webp 480w,
/static/a67511c6bb8a92bca182f2991a387417/02596/5.webp 732w&quot; sizes=&quot;(max-width: 732px) 100vw, 732px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a67511c6bb8a92bca182f2991a387417/9aebd/5.png 480w,
/static/a67511c6bb8a92bca182f2991a387417/61a54/5.png 732w&quot; sizes=&quot;(max-width: 732px) 100vw, 732px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a67511c6bb8a92bca182f2991a387417/61a54/5.png&quot; alt=&quot;main app 생성 폴더 구조 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이제 아래 명령어를 입력하면 main-app을 작동시킬 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;shell&quot;&gt;&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nx g @nx/react:lib libs/sub-app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1324px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4fd3e12fdbd5881a587ad9fde61f70f7/a6a86/6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 163.54166666666669%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAABYlAAAWJQFJUiTwAAACrUlEQVR42u2WS08TURTH53MYIygpEQnQoZ0+pjPtdKadvrGtFBQK8g5xDXElLty48hsY48YHqAkCkqDyEhXlIZGFbsCFG1E0blyYSP+eewGLgahNXHbxz5w5j989ZxZzruAtt0G2W1FiEVFe5oat0gtLSS0sR0QcK5VQXaHCUe3H8aMS93mcMSTMJgS1evjlBKwVCkoPVaLscDUcNToE3RWH1xGC6owSOAy3LQDZFiTbhK1KwwmLE2KlioCSQCjUDJucgeEKo9GfRMCbhOFuQEbvQ0hugyKFIRgKBdQ0YoHTCPvrYfpSpDQMpQ5eV5QOCMLnjlIsjVS8DZqvASE1iYzZiIieQdzIIh04h4jaQgekINhraCTRB0ONQVMiUFwml2T1wW5VYRe9cNo1WKvccNZqcDFJBhQ5DMVpQnYYlOeByOKiBsEjmXxMh1VHwBNDa6IZ2dgZtNCzNZHl7y3xZpwlm/m44k2U04TWuiyCShwu/omCYCzBL9dB95yEozaEgZ7zwPxr5GaXgSc7mnuVf+5RjvnmV3mNJJrQicNYguZOwE+SxCAudPXh++N5bIxM4cuDGXwdnwVml/JQpukFrtzUS+4f6O6jkYOcwVj7gOt3xvH25gjWBseweG0Ib26M4PnV21gfGsc70o/JF4UBv03MYXNsGpujU/gw/AifqNv39ybw8f4kPo9OFwYcICCerSA3s7g96q7YqLt2ISNzIBVvUXLulxby9g6sMODc8nbynuKDVAQWgUVgEVgEFvzHXsLW5P8E0q7lO+UvXf4T8GJ3P9ZuDWNj+CFA0C3acLnf9kteLMb2D9uUdvEAIDvlUu953L18BavXB7e3H+uGdXuAeOzpCnXYv79DH13pVLr3RbQ0eht60JXuRHuyHR2pjj+qk/Ki2ileyxgM+BPp1eIRIy+cEgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4fd3e12fdbd5881a587ad9fde61f70f7/263a4/6.webp 480w,
/static/4fd3e12fdbd5881a587ad9fde61f70f7/a6361/6.webp 960w,
/static/4fd3e12fdbd5881a587ad9fde61f70f7/17eeb/6.webp 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4fd3e12fdbd5881a587ad9fde61f70f7/9aebd/6.png 480w,
/static/4fd3e12fdbd5881a587ad9fde61f70f7/a91f8/6.png 960w,
/static/4fd3e12fdbd5881a587ad9fde61f70f7/a6a86/6.png 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4fd3e12fdbd5881a587ad9fde61f70f7/a6a86/6.png&quot; alt=&quot;메인앱 구동 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;기본 메인앱 구동 화면&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;2. 메인 앱에 통합될 서브 모듈 생성하기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이제 libs 디렉토리에 메인 앱에 통합될 서브 모듈(라이브러리 앱)을 생성해보겠습니다. &lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;shell&quot;&gt;&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nx g @nx/react:lib libs/sub-app&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1576px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/657f03893790f28396d768ecd0ce3fbe/f31ea/7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 18.958333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAwklEQVR42oXOTQ6CMBAFYK6hUlraolBBfspPQRcewIWJiTHu3Zi48wDe/FmKiXHl4subzbwZLwp97A8XnO4vHG9PnB9X5L3GbE5AWAhCR+yT/3lpXiFRCpHkkEJgKRNQJhAw7hBHOD7l8APmLAh1xpkE34Ne129hhh06qzEDWqvUNQrdoLKyQiPTHTa1gcoKLOPEUohVaq0RrWIwGSMIxVTYmt4WNGg647LULXTd2uUSkcrttxw0HIkfjEtnnKcy7grfJQJ2JIarBikAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/657f03893790f28396d768ecd0ce3fbe/263a4/7.webp 480w,
/static/657f03893790f28396d768ecd0ce3fbe/a6361/7.webp 960w,
/static/657f03893790f28396d768ecd0ce3fbe/8cb6d/7.webp 1576w&quot; sizes=&quot;(max-width: 1576px) 100vw, 1576px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/657f03893790f28396d768ecd0ce3fbe/9aebd/7.png 480w,
/static/657f03893790f28396d768ecd0ce3fbe/a91f8/7.png 960w,
/static/657f03893790f28396d768ecd0ce3fbe/f31ea/7.png 1576w&quot; sizes=&quot;(max-width: 1576px) 100vw, 1576px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/657f03893790f28396d768ecd0ce3fbe/f31ea/7.png&quot; alt=&quot;서브 모듈 생성 콘솔창&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;저는 sub-app이라는 이름으로 React 기반의 라이브러리 앱을 생성하였습니다. &lt;/p&gt;
&lt;p&gt;위와 같이 설정을 완료하면 다음과 같이 libs 폴더 안에 sub-app이라는 새로운 라이브러리 앱이 추가됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 720px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4480a4a6c8726cbc6ae1ebf54f3a2c7e/242a6/8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 208.54166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAqCAYAAACz+XvQAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEcUlEQVR42p1XW2/iZhDlB2QTYny38f0CxpiLMWAIhiTdbKtW7UPvqqqVqkp9a///0+nMR1htVSmx8zAycuzjM3NmznzpGYYB27ZhmiZc14XjONA0DYPBALIsvxj8Lge/q6oqLMtCT9d1DIdDhGEogDj4j6+BcVyeZwx+h689vsEM5/O5AOabbdhdgp+9PM9XwZApF0WB0+mEyWSCfr/fCfTz6DFVz/Pw/v175HmO7XaLqqrw8PAg6ilJUndAjjiOsV6vsd/vBWNOv20t/weoKAopZGOz2SCKIpEup83suqbeOyukUNoWFouFEIjl5xa6tFMXUGLIsitIUhvH41GkfDgcxG8Oriuz5SxaAXLbKARqOAFWqxXKshSKc8NeXV11T1nUkEBNP8JutxMqcy05giB4m8qaTq0TcA2XmM0KUUsGZ+Vvb29bp/uphpomIwpVbOsaNQWn3jRHZNTk3QEV6kNNRTqJsKpWAvAC6pHSDHgZrzbRMwwTlhdiVD/i7u6Au0ODx8cv8ECRpCP6mA7DtKDTc22id3z6AU/ffcRXv/6FDz/+iW9+/xvffvwHT9//gebpJxw//NwuvvwF91//hp4fpQjjMZLJAkXdIBrnCNMJfLrnkvJuELcKj3FGOXpcSFUdIPBJ6SSFRYag0vTc3LzDbf+GathvFdJAgmYa3Ng6jZiBJDEQ5XNM1jsE4ww61U0jYEWRhcovBpNiX3W986TouoYotjFalBgt1xgvKxFDP8CAG/u1tiF1FV4BjkuA3Ng0ZlaQCMCs2hLLGuFkCou+2Gr0PgdU6esm/QhWDUJKNcoLJLMF4mIOg1xHovp0YsijZ5BFRVmMeDpDRDEu15T6CsGIa9nCvv4LSMZgqkhi7RmwILBKsGS2BhnvGwA1eL4BfzRBmOVICDidl6K2rVKmYECT1gaNHm89C4Zuwnxe2hyWZT6bgtLNvvjlIPDJGNbYbHdYLktymkZswi5eyGWRJN7LxGpIrrIpc9TVFNWClA4DsfHaujU/ZxpUNtc6A1qU+2aZYbeZYTkbw/c9saDas5OhkwXalsGAOpmAi5rSbY4nnO7vcX9/oqPJrFPtPvkht0wUGWT7Z3Nd0pKSZZUW1Dvx5c6L3rI0jFITh80U5SzBthzjVE+xX2dIQoecRBIG8RKIRMw43TgYnhnGxDBLfCyLGA45D9fDIMOQB3IrYfgZi0TxWRTLOqe8LBLsqjHSyBVq+a6NyLdpgZHachuGOiJmaNs6UvLCchqjmidYUdqLaSTYFllI1qa+WkvB0NQREIkej12SmGi2UxGHTU5MM3iOhZu+1FphBgw9AtRMG0PaH01zELuYFzyfZnmCrq+v27fNcxBDhVI2aez2YoXW9V4A81GEz4t8xmkrjBg9k1w5pfld0YTU6xlNyxzrcorxKKGJ8VsdOhnMIJWdIU2KYTtw0gxZPMQk9UgIHzldNToz9vu3rWvIgO6QFr1KN3hd9qmB+yQCC3HzfPzoOnoi5UtK53V5WZnym/4D4NH7F6d9WiAAAEy+AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4480a4a6c8726cbc6ae1ebf54f3a2c7e/263a4/8.webp 480w,
/static/4480a4a6c8726cbc6ae1ebf54f3a2c7e/df77d/8.webp 720w&quot; sizes=&quot;(max-width: 720px) 100vw, 720px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4480a4a6c8726cbc6ae1ebf54f3a2c7e/9aebd/8.png 480w,
/static/4480a4a6c8726cbc6ae1ebf54f3a2c7e/242a6/8.png 720w&quot; sizes=&quot;(max-width: 720px) 100vw, 720px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4480a4a6c8726cbc6ae1ebf54f3a2c7e/242a6/8.png&quot; alt=&quot;서브모듈 생성 폴더 구조 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;명령어 두 줄만으로 하나의 Next.js 기반의 메인 앱과 React 기반의 서브 앱이 추가된 새로운 Nx 워크스페이스가 생성되었습니다!! 👍&lt;/p&gt;
&lt;p&gt;이렇게 만들어진 Nx 워크스페이스의 주요한 폴더와 기능을 알아보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;apps 폴더&lt;/strong&gt;: 실제 실행 가능한 앱이 위치합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;libs 폴더&lt;/strong&gt;: 여러 앱에서 공통으로 사용하는 공유 라이브러리나 서브 모듈을 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nx.json&lt;/strong&gt;: Nx의 핵심 설정 파일로, 프로젝트의 구조와 스크립트 명령어 실행 규칙 등을 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tsconfig.base.json&lt;/strong&gt;: 워크스페이스 전역에서 공유하는 TypeScript 설정 파일입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;3. 서브모듈을 메인앱에서 사용하기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이제 libs/sub-app에서 만든 모듈을 apps/main-app에서 가져와서 실제로 사용해보겠습니다.&lt;/p&gt;
&lt;p&gt;sub-app에는 기본적으로 lib/sub-app.tsx라는 컴포넌트가 생성되어 있습니다. 이 컴포넌트를 불러오려면 @nx-example/sub-app 경로를 임포트하면 Nx에서 자동으로 경로를 매칭해줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// apps/main-app/pages/index.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; SubApp &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@nx-example/sub-app&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;HomePage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt; Hello there, &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        Welcome main-app 👋
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;SubApp&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 main-app 안에서 sub-app 내에 있는 컴포넌트를 불러오면 별다른 설정 필요없이 이 모듈은 빌드 시점에 main-app에 함께 통합됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1324px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4924ead4971bb54508047aedaebf9ce3/a6a86/1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 163.54166666666669%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAABYlAAAWJQFJUiTwAAADpElEQVR42u2W228TRxSH/XegqlBQUGlE8MZeX9a79vruxHaauCS0uXFHqM9BVILEmIgSAaJSVfpUqeoDIXXVqoQSKqW5OAZkQnZtTNUCgUAfqlwgBN4AEf96ZjAtUpwLEk8VD5/O+szMt3PWM7tjcK43QTIbsbpMwPp1dpjKnShbXYmydwWsXSOiYoMCS4Ub778n8pzDGkY00Ai/uhluKQrjBhlrVpVj3TsVsGzywOCxReC0BKFYq0kcgt3kg2Ty03UApo0qPiizQihX4JOjCAabYJLq4bWFsMVdC5+zFl57A+o9bQhK2yCLIRi8MjUoMYR9HyPk3oyAq46IwSvXwGmrphv44bJXU1sMdZFtUF0NCCq1qA9sQZWnHhFvM2K+T1GltNAN6mAwb6KSBBe8ShiqXAXZFuCIRhfMRgVmwQmrWYVxox3WShU2huiFLIUgWwOQLF7q54DA2gUVBocY4GVajB74HGG0RpvQHP4ELRRbo838d0ukCVvpmuU4kUbq04jWmmb45Qhs/BH5wVwGt1QDj+NDWCqD6NizH8jkURjRgXSRi9n/4isUWC5znY8RhQA85GEug2qPwk2Igh/tu9rwZCCDqXNDmO1LYe5CmuTavwLG/PBVzvOhUZ7v2N1GJfu5g7kWCO/9cAHjZ3pxN3ke2W+TuNndi9FvenAv2Ye/qG1+MAMMj6IwdIWE+vLCqf4M7vRdJNK4cS6F8fNp/P7zIMZ/GcEE5WeHNTwayeJhSsfTy/lFhNIL4UES9uYm8YU+g1P6NE5lZzhfF+NXlP9ybIqYxAltGj/pf6OdhGIpoaUoPK1PIq49wNHcLD5nZIuROJp7yOmi3CH9Pk5np5YX9mQncVi7j/jATXQM3EAiPYH40DhxCwd+zfPYmb6LTv0BulciPEPCBAkTw7fR/tufOJS6jfjgLRzs/wOf9WpcnhiZQGKlwm4SsnK6WInX5njpvNRrVGp+7kXMvUbJSXqGR7QZHB+bxjFtIccJ9occoSq+p76lha8sm2eX8pijJfGYlsZSsD7PFl02RWEHCdliLbBdQLthKQor2SkvhWxbvdxii/H8dYRvfIZvhW+F/z9hWqMP0ZsU0re2kBoDlpnlioTx3ftwp+csps/2AySdH7zCB5aCtbEDAHvlmYUSQnaXzr378WPXSVz/LglczqHAZsNmWwLedilHM9y3cIYuOtIpdO6rUmPY27AHu2I7sb12O3bU7ViSndSvWv2Ij2UOJvwHbEPLaYpHBGwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4924ead4971bb54508047aedaebf9ce3/263a4/1.webp 480w,
/static/4924ead4971bb54508047aedaebf9ce3/a6361/1.webp 960w,
/static/4924ead4971bb54508047aedaebf9ce3/17eeb/1.webp 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4924ead4971bb54508047aedaebf9ce3/9aebd/1.png 480w,
/static/4924ead4971bb54508047aedaebf9ce3/a91f8/1.png 960w,
/static/4924ead4971bb54508047aedaebf9ce3/a6a86/1.png 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4924ead4971bb54508047aedaebf9ce3/a6a86/1.png&quot; alt=&quot;빌드타임 통합 결과 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;빌드타임 통합 화면&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;현재 main-app과 sub-app은 서로 다른 디렉토리에 존재하고 각자 독립적인 구조를 가지고 있지만, 빌드 시점에 하나의 앱으로 통합되어 최종 번들에 함께 포함됩니다. 또한 Nx가 제공하는 Affected, 캐싱, 의존성 분석 기능을 함께 활용하면, 서브 모듈이 많아져도 전체 앱을 매번 재빌드할 필요 없이 변경된 부분만 빠르게 빌드할 수 있어 개발 환경에서 매우 효율적으로 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그러나 이 방식은 모든 모듈이 하나의 배포 파이프라인을 따르기 때문에, 각 모듈이 독립적으로 배포되어 각 팀에서 자율적으로 배포 주기를 관리해야야하는 경우에는 적합하지 않습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;다음으로는 위와 같은 단점을 보완하여 &lt;strong&gt;앱을 완전히 분리된 단위로 개발하고 개별적으로 배포할 수 있는 런타임 통합 방식&lt;/strong&gt;을 살펴보겠습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;런타임-통합하기-feat-모듈-페더레이션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%9F%B0%ED%83%80%EC%9E%84-%ED%86%B5%ED%95%A9%ED%95%98%EA%B8%B0-feat-%EB%AA%A8%EB%93%88-%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98&quot; aria-label=&quot;런타임 통합하기 feat 모듈 페더레이션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;런타임 통합하기 (feat. 모듈 페더레이션)&lt;/h2&gt;
&lt;p&gt;런타임 통합은 각 앱을 완전히 독립적으로 개발 및 배포한 뒤, 런타임 시점(어플리케이션 실행 시점)에 하나로 통합되어 서로 코드를 공유하는 방식입니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/94f0b13f8c30fe3a81ca906acc22e6fb/a52c7/9.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABvElEQVR42n2Se0/bMBTF+/0/1IRAGhvShopWVCE2oGuSdknb+B0/krNjF1CFgD+O7Ht9/fN9eDaOI6QQqNZr7LoOKSVk30fK5yEmpHFCLyR2uwOGIRQ7xohZDAHRWgStkbhm+3PgCGsEnOmw3Txg9biEllsMNoOHI3BSGtCmKHmPiReLpukdAUpsIQ8rNNU97pY/CWzQ759OgEYCWVYh5Ix5K6vsWcZLdnkfQoToG0I6+H0P13LVkm2rn4HRsYwbjOkaE26hlnPsz84h53NY52CsQ11vsVpVEELDDR5d+xfq8A9Du4OtGwQp+MgrkH2Ll0jxAglXULfXEGcXMAROY87SMHAL5zbcH0oPtWqhej6wY6ltDd1vIGkXYAgDgX8YeE/l1ZUpOh5OIwip2bfv1DfGLagjUMsN2vYOVTWHUjX7+pphLEEpTcc12xyMVgrGeAat2T9WkL7y/Bd9A79XBSXXsPYSWn+B97+Z4YaxDrP3vkaeZkqRgfxzoaW9oH9RKvE+8jKnLCoYfUPgD4Kf6Gs+Bp5+Ys9sj1XkCfsyZaM6iP0DBL/OUY8s+bmHnwFfoG+VMxkGeyJTfPmx//4zUdKQN3axAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/94f0b13f8c30fe3a81ca906acc22e6fb/263a4/9.webp 480w,
/static/94f0b13f8c30fe3a81ca906acc22e6fb/a6361/9.webp 960w,
/static/94f0b13f8c30fe3a81ca906acc22e6fb/0b34d/9.webp 1920w,
/static/94f0b13f8c30fe3a81ca906acc22e6fb/b0b5b/9.webp 2024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/94f0b13f8c30fe3a81ca906acc22e6fb/9aebd/9.png 480w,
/static/94f0b13f8c30fe3a81ca906acc22e6fb/a91f8/9.png 960w,
/static/94f0b13f8c30fe3a81ca906acc22e6fb/ac7a9/9.png 1920w,
/static/94f0b13f8c30fe3a81ca906acc22e6fb/a52c7/9.png 2024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/94f0b13f8c30fe3a81ca906acc22e6fb/ac7a9/9.png&quot; alt=&quot;런타임 통합 구조도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;런타임 코드 통합 시 MFE 구조도&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;1. 워크스페이스 생성하기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;먼저 Nx CLI를 사용해 워크스페이스 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;shell&quot;&gt;&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;npx create-nx-workspace@latest &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1704px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a427d2b7e217f91f3ef00b20b476f835/8b940/10.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 44.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABFElEQVR42o2SSW6EMBBFuQizwQOGhjbGTOnsEkXKNvtIOULur5+yO5MiRc3iq1AtHq9cFVWcQ7cnPL++4+HlDVwoZAVDnOYhaVYgoZr4Svnq/5fIXB5Rsgp5lqIsSijdgcvmJ6qBaDSE0mC1uAmN3Lph2XZMy4bRLRiMxWkw0P2IWrVkW4JJgbyqbtoF4Eywdb/ALSsGa9GfTQA2XY92GINVnJWIk/wgkAydt5sc1u0O07zCzgtm6nW9Qa3PKLg6BPsE7sHOg3z1Ywvdo+IypCDDnPHjQOtmspsJuMC6q5n/9j3jVnoCGls0SIuDb2jvnyCkgtRt2Kqkjcrmuoz417kcOZkArOjvNd2ez/eYrD484l/gBw3bBROkQ6gPAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a427d2b7e217f91f3ef00b20b476f835/263a4/10.webp 480w,
/static/a427d2b7e217f91f3ef00b20b476f835/a6361/10.webp 960w,
/static/a427d2b7e217f91f3ef00b20b476f835/a4d2c/10.webp 1704w&quot; sizes=&quot;(max-width: 1704px) 100vw, 1704px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a427d2b7e217f91f3ef00b20b476f835/9aebd/10.png 480w,
/static/a427d2b7e217f91f3ef00b20b476f835/a91f8/10.png 960w,
/static/a427d2b7e217f91f3ef00b20b476f835/8b940/10.png 1704w&quot; sizes=&quot;(max-width: 1704px) 100vw, 1704px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a427d2b7e217f91f3ef00b20b476f835/8b940/10.png&quot; alt=&quot;워크스페이스 생성 콘솔창&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;runtimeExample이라는 workspace를 만들었습니다. 이 명령어를 통해 기본 앱이 생성이 되는데, 아래에서 host앱과 remote앱을 함께 생성하는 generator 명령어를 사용해 두 앱을 따로 만들어볼 예정입니다. 이 때 이미 생성된 기본 앱은 제거해주세요.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 768px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/43f0e1e19802a0834a6b245b71c3bc96/fe486/11.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 114.16666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAABYlAAAWJQFJUiTwAAACBUlEQVR42p1U23KiUBDkBywVELmqqFxEZPGK4l3z4v9/UO/MbGKlUm4SeJg6iJ62p7tnlG63i16vB8/z4Pu+PPNpmiY0TYOu6y+r0+nAtm04jiPFv3ddF4phGAiCAMPhEKqqgv+Aiy/8D+wz6EfxZ8ZSNAJhRkmSYDKZyBetVkvAq1S73ZaOlOFohPligfvbG46nE8bE1ie2/L5OKTGx2u/3eDwemE6niKIICZ38PorjShXSXdHQITFZUDZkMBig3++LLtxClWK5FMuyCDnE8XjEcrnEdrvFgiRgXX4y5ZVJikGOjsZjXC4XHA4HhGEopnwXmW8BO12LjAhxPp9wu92EaVEUIkFVUAF0Xc6hj/V6LebkeS5BrcruCei5OmVwhM1mKxoy4Ijs52zWYmjaBmbZlMzYCSC3XJYl0jStB2jQPKb5HMVmQyw3uF6vksVms1mvZdfRqM2Y2K1ER44Nj+FvZvm1hl6HouITuwJZlj3nmBlWzeK7yzpms4hicxH95vP50/Ex5ZOH/rds313WkE4D7HalgHAGGZT340878SVg17aQEQCzu93v4nBMg95oNKTlKloKoOWYSNJEHGYwPllLXri8kSsz9AY2sjyVdst9SVqecaK9yK0zaGUN7SBFvCiwyP9gtVqTljtZELVb1lRa3eq/9f0RE36unUP908U6IF8B/wKXWF85FrnHRQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/43f0e1e19802a0834a6b245b71c3bc96/263a4/11.webp 480w,
/static/43f0e1e19802a0834a6b245b71c3bc96/dc9b9/11.webp 768w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/43f0e1e19802a0834a6b245b71c3bc96/9aebd/11.png 480w,
/static/43f0e1e19802a0834a6b245b71c3bc96/fe486/11.png 768w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/43f0e1e19802a0834a6b245b71c3bc96/fe486/11.png&quot; alt=&quot;워크스페이스 생성 폴더 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;2. host앱과 remote 앱 추가하기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;다음 명령어를 사용하면 모듈 페더레이션이 적용된 host/remote 앱을 동시에 생성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;물론 각각의 앱을 따로따로 만들어 모듈 페더레이션관련 설정들을 추가하는 방식으로도 구현이 가능하지만, 이렇게 만들면 좀 더 간편하게 host앱과 reomte 앱을 만들 수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;shell&quot;&gt;&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;npx nx g @nx/react:host apps/host &lt;span class=&quot;token parameter variable&quot;&gt;--remotes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;remote&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 주의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;워크스페이스 이름이나 remote 앱 이름에 -, @ 같은 특수 문자가 포함되면 generator 실행 시 오류가 발생합니다. &lt;br/&gt;예: &quot;Invalid remote name: @nx-exmaple/remote-app&quot; &lt;br/&gt; &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1746px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/02402863cb878fe9c958177d970e6a11/1805d/12.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAzUlEQVR42oWPS27DMAxEfZA4lmTTikT96E9aBCiafe9/niltdJmgi8HjRjN63f78QSoNqQpyW5HrivnGcOQxGId+MLhcDfojg/03Xdnv2D8fWJSy7SeLCKosOtLgU4MZCVc7oteByzEwvC/vvuUJKRtqXlCSFqUFmZtSf8tykkNBCIKZEpwhkLvBWnpd+KhfSLEi6qMYMqLerElckXWAY4GfIybnQVMAUThvY8bXhaFkbPcPrH/KB9u6nbpctLQJfGDVnmBV3dGs6vat9i8bN5HGWRMVpAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/02402863cb878fe9c958177d970e6a11/263a4/12.webp 480w,
/static/02402863cb878fe9c958177d970e6a11/a6361/12.webp 960w,
/static/02402863cb878fe9c958177d970e6a11/35b21/12.webp 1746w&quot; sizes=&quot;(max-width: 1746px) 100vw, 1746px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/02402863cb878fe9c958177d970e6a11/9aebd/12.png 480w,
/static/02402863cb878fe9c958177d970e6a11/a91f8/12.png 960w,
/static/02402863cb878fe9c958177d970e6a11/1805d/12.png 1746w&quot; sizes=&quot;(max-width: 1746px) 100vw, 1746px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/02402863cb878fe9c958177d970e6a11/1805d/12.png&quot; alt=&quot;특수 문자 폴더명 에러 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;따라서 &lt;strong&gt;프로젝트나 워크스페이스의 이름에 특수기호가 들어가지 않도록&lt;/strong&gt; 설정하세요! &lt;br/&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;그러면 이렇게 hostApp과 remoteApp이 동시에 생성됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 766px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8525a586223377fef27ee4cc7da4b578/e4da8/13.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 258.95833333333337%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAA0CAYAAACKJRg7AAAACXBIWXMAABYlAAAWJQFJUiTwAAAFoElEQVR42qVX2XraZhDVCzRmk0AbCLQCQuxrHNuxm+Sie5v2onf98v7PcHpmZFyagsHuxXwC8XM0c2bmzMhotVrwPE+t3W6rdTodyP16vY5Go3HUTNPUM7ZtqzWbTTiOA8OyLPR6PQRB8HRITO6fAtubgOxNzsv/DPEiDEOkaQrf9/VgrVZT786ZnNufrVarejUkxNFohPv7ezw8PKinEr6Av8SEJjEjz3Pc3Nzg+x9+QE7gwWCI4fBlNhgMMJ1OMR6PYUj8LVo3jpFN52pe0EWD7pvkRXg9b+RTuLedEtCyeLNhIohTtMNIealWKqWRm3PJkYdbCmiXgI2GBc9tIMm6iMdzBEmKcJDTRuj1h2cBJRkt14PtuCWglotjEyhBOBwpUDZbKljLdcts0tNatYI6Px8FZA2KPQKyUG0XfpQhEs8ImoxnBBzAbXfgMIty9Xuh8vR1wZeA7tcettCJIwwWG/TnK8TFBIPlVoHDfvmQlAkT0K+9FEBbQnbdfzhs+w3E/R4B5ugvVkgmMwRphurVFWr7BMnnI0kqOTzwsMEM+z5bMO7SkzGiYYEoH8PxfOXtsH9PJoVgBxyasMiNHybIJnOCFWqSmEhDXcDr9pgcencE9AiHkhS2W9wnh2uGPVUOh6stutmAzd/SIn+ubP7NoVnWYTdss2QK9UpMQMVTy2oeLZeTIUule+yQiJ6FrL2kmKqnUodSLqe4Oxpyo0HpqVzBbnsIsoxJyRGNxloiQdontxKuqWeq1cpRq/A3s8UWphlJnyFmBfJihOl6g/ntJyzuaLcfMd3dY3HzCcPZFr1ogDDJj1ovHiIbzZEMJjBuPnyG2Pvv/sSHz19of+Hj71/w9tvfsL37UW33/mdcP/x62u5/wc3HP7C7/wmGlgwzKDx02AW+lAeLuF6vaZmInVVvJsyUUdBStbEIKIOKatPvkruVZlgSkz6Wj9aeJsa8TG2aMmD4RZIgsiXZzaZLqk/GDFtPnkrbnVYbV82wdGpZpdqEKT0ra088jEcT7WcR3SBK9UEOZ9BZtWmycB2XahP1GOK0bL9hCSoq7vciBe0yAsc/C1hy2CaHUdYjh0st6pSgXVGbyhsVCDFRnJMhH7aetJbnc1AlpYdlyBMdVsKhiKpplarUMM+03p5Dy+E6EsZI2CXlPMlLtSF4SpEV8NK759TmEJBJ8Uh8f75ULRQOJXThsPHMjnNEYEsOfZcrSdxhyDPyN1MORf7lc5Pj8bnF6QnwiUPy4tGTkPIfUv+kmDOKajfra8NfNEYPp55F4v2uaGG/7BJ2iI5Rggt3ws85D58EVtcwhuQ6Jtodr5xwLGAB7j52jtsJnhVY4VhWEe1l2elk44oZco/CEPa66LR91NlmqoEnau+YlyavhmyfCTcG2cAWiwW2ux3Gk0l54HEZOrvbHExFQ2QnihPc3d3i3bt3Cn7F+XspyH8ALcYdcpW7pYd3d3fq6fX1tS6d9TP1dxTQ9SzyF2C1WmO73eryKD9etMYdBWRBj0Yx1ustt9CJbqKylcrO/cqQLRSTMTbrNZbLhfK42Wz+ByDHZEGvlsywhC08CthLE7OvCg15PE4wm83o4Vyvr/FO1mrRBcPj1pXELmZFhGEaoCKbaq2uVq9fBlajd0Hb4UroSJZNFHmAt8shdssB5kWC5STh9wF6gYeqjMiXAMr4zAc+QWKsaItxhJzj1G41L+4SiUYe3mmLOLg+RkzK292Gb1O3zPaKHCb45k1FD76MR3JoOxaLOcJiVmCzYrbnBcZ5ioxi6zqti7pFuLbtpprhUV1GRYr1JMJ2nmK3yJTDnPu279nKzyUhhww5kJDtUF4l+M6WdFAMQ0xyzt+OizdX1YvA9jXoMRqJyKhXy9evak1eUWt6lSe+RLb2XspENA5fAEszXyVd++I2XvvnU738N2goY/JksxKKAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8525a586223377fef27ee4cc7da4b578/263a4/13.webp 480w,
/static/8525a586223377fef27ee4cc7da4b578/1d625/13.webp 766w&quot; sizes=&quot;(max-width: 766px) 100vw, 766px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8525a586223377fef27ee4cc7da4b578/9aebd/13.png 480w,
/static/8525a586223377fef27ee4cc7da4b578/e4da8/13.png 766w&quot; sizes=&quot;(max-width: 766px) 100vw, 766px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8525a586223377fef27ee4cc7da4b578/e4da8/13.png&quot; alt=&quot;특수 문자 폴더명 에러 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이제 모듈 페더레이션설정 관련 파일들을 살펴보겠습니다. &lt;br/&gt;
각각의 앱 내부를 보면 &lt;strong&gt;module-federation.config.ts&lt;/strong&gt; 파일이 자동으로 생성되어있습니다.&lt;/p&gt;
&lt;br/&gt;
이 파일은 Webpack 모듈 페더레이션 설정을 Nx가 추상화하여 쉽게 사용할 수 있도록 제공하는 기능으로, 각 앱 간의 연결 관계와 노출할 컴포넌트를 정의하는 핵심 설정파일입니다.
&lt;p&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 모듈 페더레이션 관련 설정 확인&lt;/strong&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&amp;#x3C;Host App&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Host 앱은 Remote 앱을 소비하는 쪽이기 때문에, remotes 항목을 통해 어떤 앱과 연결할지 명시해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// apps &gt; hostApp &gt; module-federation.config.ts&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ModuleFederationConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@nx/module-federation&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModuleFederationConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;hostApp&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/**
* To use a remote that does not exist in your current Nx Workspace
* You can use the tuple-syntax to define your remote
*
* remotes: [[&apos;my-external-remote&apos;, &apos;https://nx-angular-remote.netlify.app&apos;]]
*
* You _may_ need to add a `remotes.d.ts` file to your `src/` folder declaring the external remote for tsc, with the
* following content:
*
* declare module &apos;my-external-remote&apos;;
*
*/&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;remotes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;remoteApp&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
* Nx requires a default export of the config to allow correct resolution of the module federation graph.
**/&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;ul&gt;
&lt;li&gt;name: 현재 host앱의 이름입니다.&lt;/li&gt;
&lt;li&gt;remotes: 런타임 시점에 연결할 remote 앱의 이름입니다. 실제 remote앱의 이름과 일치해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// apps &gt; hostApp &gt; src &gt; app &gt; app.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; NxWelcome &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./nx-welcome&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Link&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Route&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Routes &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react-router-dom&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; RemoteApp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;remoteApp/AppComponent&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;React.Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Link&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Home&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Link&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Link&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/remote-app&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;RemoteApp&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Link&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Routes&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Route&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;NxWelcome&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hostApp&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Route&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;/remote-app&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;RemoteApp&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Routes&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;React.Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; App&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&amp;#x3C;RemoteApp&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Remote 앱은 자신의 내부 모듈 중 어떤 것을 외부에 공유할지 명시해야 하며, Host 앱은 이 이름을 기준으로 해당 모듈을 동적으로 불러옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;
&lt;span class=&quot;token comment&quot;&gt;// apps &gt; hostApp &gt; module-federation.config.ts&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ModuleFederationConfig &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@nx/module-federation&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModuleFederationConfig &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;remoteApp&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;exposes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;./AppComponent&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/app/app.tsx&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * Nx requires a default export of the config to allow correct resolution of the module federation graph.
 **/&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;ul&gt;
&lt;li&gt;name: 현재 remote 앱의 이름입니다. (host앱에서 이 이름을 통해 접근합니다.)&lt;/li&gt;
&lt;li&gt;exposes: 외부에 노출할 컴포넌트 혹은 모듈 경로를 정의합니다.
&lt;ul&gt;
&lt;li&gt;여기서AppComponent은 host에서 import할 때 사용되는 경로이고,&lt;/li&gt;
&lt;li&gt;&apos;./src/app/app.tsx&apos;은 실제 컴포넌트 파일의 위치입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;4. 타입 에러 방지를 위해 타입 선언 로직 추가 및 타입 설정 파일 수정&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Host App에서 Remote App 모듈을 사용할 때 타입스크립트 에러를 방지하려면 아래와 같이 글로벌 타입 선언 파일을 추가해주세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// apps &gt; host &gt; src &gt; remotes.d.ts&lt;/span&gt;
declare module &lt;span class=&quot;token string&quot;&gt;&quot;RemoteApp/AppComponent&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; App&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;그리고 Host App이 Remote App의 코드를 인식하고 타입 컴파일을 할 수 있도록 tsconfig.app.json의 rootDir과 include를 수정합니다:&lt;/p&gt;
&lt;br/&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// apps &gt; host &gt; tsconfig.app.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;extends&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../../tsconfig.base.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;compilerOptions&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;outDir&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dist&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;tsBuildInfoFile&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dist/tsconfig.app.tsbuildinfo&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;jsx&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react-jsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;lib&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;dom&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;types&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;node&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;@nx/react/typings/cssmodule.d.ts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;@nx/react/typings/image.d.ts&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 아래 로직 수정 루트 디렉토리에 remoteApp 패키지도 포함되도록 root 디렉토리 설정&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;rootDir&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../..&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;src&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;exclude&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;out-tsc&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;dist&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.spec.ts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.test.ts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.spec.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.test.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.spec.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.test.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.spec.jsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.test.jsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;jest.config.ts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;eslint.config.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;eslint.config.cjs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;eslint.config.mjs&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;include&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.jsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.ts&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;src/**/*.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 아래 로직 추가 =&gt; remoteApp 패키지도 타입스크립트 컴파일 목록에 포함시키기&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;../../apps/remoteApp/src/**/*&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;references&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
이제 Nx 명령어로 이 두 앱을 실행시켜보겠습니다! 🥁🥁&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;참고로 remote app을 실행하는 명령어를 입력하면, remoteApp과 함께 이를 참조하고있는 hostApp도 자동으로 실행됩니다.&lt;/p&gt;
&lt;br/&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;shell&quot;&gt;&lt;pre class=&quot;language-shell&quot;&gt;&lt;code class=&quot;language-shell&quot;&gt;nx serve remoteApp&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 100%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/53d2d37171da490a8508c7c945bc6e41/35d52/14.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABdElEQVR42nVSS3aDMAzMFfj/DASIIbYhCUkLBRbZtPc/0lRymr4s2sU8y0IejUbs9rLDcDqh73u0bYs8z5EJgXxfoapryhsoraHNAHM2kLqFKLgmhcipjuK8FAjDCGEcYxdGMS6XC6ZpQlEUcF0XSZIgyzIYY7AsC7Ztw7auuH+ueJsHwhm3+QIzKJheU0MFpRQyErMLwxBd12EcR1RVBc/z4Pu+VdqT8g8impaZCEaMVyK6jZRXaHUDITJbJ2giFhOTkB0/ZlVP8P2Zq5oaZb1HSqMlWUo5/hbCcT04jmvhUswi+IziH8K/wEVlWUIdj5BSoulqSNWglnuKKzSyJpWSmlbWR1GI/wmDILAKNS1jXRfChvU+4f614DqdCSdc5xN52Fn/jseOlqZpUeQhK3n69qrOcRzoYcD7x4zx/YZhHGwDXtTh0KAo80dzP4Af8OmTwvixlCiKfsk45t8mSVN6VJJ/mfWP4dlmj4Z88iSv4JG/AVDQ2w1fHEsBAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/53d2d37171da490a8508c7c945bc6e41/263a4/14.webp 480w,
/static/53d2d37171da490a8508c7c945bc6e41/a6361/14.webp 960w,
/static/53d2d37171da490a8508c7c945bc6e41/0b34d/14.webp 1920w,
/static/53d2d37171da490a8508c7c945bc6e41/e3dad/14.webp 2214w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/53d2d37171da490a8508c7c945bc6e41/9aebd/14.png 480w,
/static/53d2d37171da490a8508c7c945bc6e41/a91f8/14.png 960w,
/static/53d2d37171da490a8508c7c945bc6e41/ac7a9/14.png 1920w,
/static/53d2d37171da490a8508c7c945bc6e41/35d52/14.png 2214w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/53d2d37171da490a8508c7c945bc6e41/ac7a9/14.png&quot; alt=&quot;리모트앱 실행 시 콘솔창&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;콘솔에 명령어를 실행하면 위와 같이 두 앱이 병렬로 실행되고있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;현재 각각의 앱은 다음과 같은 주소에서 접근 가능합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;ul&gt;
&lt;li&gt;hostApp: &lt;u&gt;&lt;a href=&quot;http://localhost:4200&quot;&gt;http://localhost:4200&lt;/a&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;remoteApp: &lt;u&gt;&lt;a href=&quot;http://localhost:4201%C2%A0&quot;&gt;http://localhost:4201 &lt;/a&gt;&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;각 주소에 접근해 보면 다음과 같이 각 앱이 실행되고 있는 모습을 확인할 수 있습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;center&gt;
  &lt;div style=&quot;display: flex; gap: 10px;&quot;&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1324px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/77bfe293d2b8060fd90da4efe63f6cb6/a6a86/15.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 163.54166666666669%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAABYlAAAWJQFJUiTwAAACwElEQVR42u2Wy08TURTG+39oTBRSNVDb0plpO+30MZ0+6LQ8gyK2NeG1cCvoCvxTTDBGBSJqARGBtjwUBAF1oUZ0YWShEVxpxIR+nnuRgKLS6LaLL/d+N+f8cs6ZxRmDx2iD02aGscwOj1yLmHYKXkcMLiECt1CJgKsapuMKjKUOiOU+lJVIKD1QjpKDFhgPVeDoYRuOcG+CZFZhUClZkcLwOuPwOavgcUTpzqRDtoVgPuYieAjWcg/8sg49moBJrIb9hI9iYvBJ9WgIdCIsn4VbjMAQcNdAU+qga40Ie+sR9NSQauF3xTncUaEh4I5DsUco5iSqoymo/kboagOi1E08kESddg6VShIa5RkEswLJ6kVA0eFzV8LtCJHCEOlNsCokDySbFxVmNxyCCtHigSz44ZUjcNmDkCUVgsUFq8kJu9UHg0sM0Qw1SBYVmktHKn4GCf00knSm4gnuk7Gt+9Yb800U04RUVQJBdwwOG4GFIBjL4JeroNLgpYowutsvArNPkZ9cBKZ+aHrp5zvXIvLMzz7jOaI1BJU4jGXw0cfwk0RrEF2tHdgYn8X79ATWh3P4RMrnFnZAU4+BSTqzT5DPPOJv3W0d1HKQMxhrD/BN/128vJ7G695hPL92Byu9Q5i/3Iflnpt4dWMEq7f7sDHZg/zEAq90X+Dn0WmsDWbxcTCDD+kM1odyWB24j3cDo+Sz5EfwdewWVTi/P7CbgHiwTDNkre3S9gy5pzO3VFjLl9o6sZmZw9veQXy5N418dp4n7oh8ls7sXIHA9gv4NvYQL670Y+XqANbS47wqnkzw3SoIyFumIP5lmX6B/CNw8bcVFYFFYBFYBBaB/wHsaj3Pd+/mxNwvu2SvWAxbXGxTCta/VTizxJcTttfAH8RjZpapws69FbL/QcWuI+qrR0tdC5prmwsSi2U5LJcxGPA74aDhyoGXE9UAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/77bfe293d2b8060fd90da4efe63f6cb6/263a4/15.webp 480w,
/static/77bfe293d2b8060fd90da4efe63f6cb6/a6361/15.webp 960w,
/static/77bfe293d2b8060fd90da4efe63f6cb6/17eeb/15.webp 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/77bfe293d2b8060fd90da4efe63f6cb6/9aebd/15.png 480w,
/static/77bfe293d2b8060fd90da4efe63f6cb6/a91f8/15.png 960w,
/static/77bfe293d2b8060fd90da4efe63f6cb6/a6a86/15.png 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/77bfe293d2b8060fd90da4efe63f6cb6/a6a86/15.png&quot; alt=&quot;호스트앱 실행 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
         &lt;figcaption&gt;호스트앱 실행 화면&lt;/figcaption&gt;
    &lt;/div&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1324px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/62d7b1f41eccea2514f4d8db805eeb24/a6a86/16.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 163.54166666666669%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAABYlAAAWJQFJUiTwAAACe0lEQVR42u2WS08TURTH53OoCQZiYySlLdN359F2pmU60KciFSnREjeuNQFj3eEjgKChLcJXMiGo0RhjfBBEBHTBQqCd/j33FnRBgC5ddPHPOffMOb9zz9ncERSbiKDbiY4LLti6AhC7Fdg6etF51o6ucw7ud9tCCCuXkYzloUsZKB4DsteE6h+A85KE82e6KbcHXkcUQpSCircPss8ksIGAqCMoxsiPQbQruNjpJj8Of6+OuJJBJl2EGByERLGYmoMWuIrB6B30BW9AokaCRh11OYd+PQ8jcgVxNUuiRCkFxW9Sgxg0OQmJmpraIFKJAiKRISQoN0HnAW0EOf02EnIBupKF4HbI8LpUKupHOGRC8hlcHkcYbocCt1Ol7xH02mX4RY3iKk2gQQmYCFGToEejnBBc9gB8rjCEkCfOx/Q6o9BVA6PDWVwfSmP0WvZAORSYHT60WW5H8mnux8IGraO5IsYSIsEUoqEULdTEg3u3ACyi0Vgg+4J0aI/6jQazS7zG05PgDMYSwoEkOUkKGiiNF/F7dx7rG7PY2p7D9s9nVFg9gC1wnzWzGkuo1ys8Vhofg9tucAZjHQF+/DSFt+8e4/OXaay8foRXbx7i5fIkllcm8f7DFNZWn2D3213UavOtAMews/Mcm1vNG278mMX696dYW5vB19VpOs/h1/YM9jZLdMNya0C2I8uqHoza1L+xm77VWGxx5IkiT6rVyrzgOFn1cus7ZElsHMuqnKi/wIn/HjjeBraBbWAb2AaeCrzPgVXU9udRZ+/KCartl3kuA3pOe6Qsq0yvW+VEsVse+wSw/zzZ2w9Tz6BYyOPmSGtiuayG1TIGA/4BpA/qth9wGx0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/62d7b1f41eccea2514f4d8db805eeb24/263a4/16.webp 480w,
/static/62d7b1f41eccea2514f4d8db805eeb24/a6361/16.webp 960w,
/static/62d7b1f41eccea2514f4d8db805eeb24/17eeb/16.webp 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/62d7b1f41eccea2514f4d8db805eeb24/9aebd/16.png 480w,
/static/62d7b1f41eccea2514f4d8db805eeb24/a91f8/16.png 960w,
/static/62d7b1f41eccea2514f4d8db805eeb24/a6a86/16.png 1324w&quot; sizes=&quot;(max-width: 1324px) 100vw, 1324px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/62d7b1f41eccea2514f4d8db805eeb24/a6a86/16.png&quot; alt=&quot;리모트앱 실행 화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
         &lt;figcaption&gt;리모트앱 실행 화면&lt;/figcaption&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br/&gt;
&lt;p&gt;그리고 아래와 같이 hostApp에서는 remoteApp에서 정의한 컴포넌트가 모듈 페더레이션을 통해 동적으로 로드되어 렌더링되는 것을 확인할 수 있습니다. 😍&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 50%; display:block;&quot;&gt;
        &lt;img src=&quot;/0d629bf1c3ea0d2aa5a729aabfe82c31/2.gif&quot; style=&quot;max-width: 100%; height: auto; border-radius: 8px; object-fit: contain;&quot; alt=&quot;런타임 통합 방식 결과 이미지&quot;&gt;
        &lt;figcaption&gt;런타임 통합 방식 결과 화면&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이렇게 런타임 통합 방식은 각 앱이 독립적으로 빌드 및 배포되면서도 동적으로 통합되어 하나의 어플리케이션처럼 동작할 수 있게 해줍니다. &lt;/p&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h1&gt;
&lt;p&gt;지금까지 Nx 기반의 모노레포 환경에서 마이크로프론트엔드를 빌드타임 방식, 런타임 방식으로 통합하는 방법을 알아보았습니다.&lt;/p&gt;
&lt;p&gt;이 글을 작성하는 지금 시점에서도 몇 달 전과 비교해보면 설정 방식이나 CLI 명령어 등 사용법이 미묘하게 달라진 부분이 있더라구요...! 😲&lt;/p&gt;
&lt;p&gt;그만큼 Nx가 얼마나 빠르게 변화하고 꾸준히 개선되고 있는지를 체감할 수 있었습니다. (빠르게 변화가 일어난다는 점은 커뮤니티와 수요가 그만큼 활발하다는 뜻이기도 하겠죠?)&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Nx는 다소 러닝커브가 있지만, 그만큼 제공하는 기능이 다양하고 강력합니다. &lt;u&gt;빌드 캐싱, Affedted 기반 최적화, 앱 간의 의존성 그래프 시각화 등의 기능은 특히 규모가 큰 프로젝트일수록 유지보수와 협업 측면에서 큰 효과를 발휘할거라고 생각합니다. &lt;/u&gt;&lt;/p&gt;
&lt;p&gt;또한 &lt;mark&gt;단순히 모노레포 환경을 지원하는 도구를 넘어서, MFE 환경을 손쉽게 구축할 수 있도록 CLI 기반으로 모듈 페더레이션을 구현할 수 있도록 도와준다는 점에서 정말 좋은 도구&lt;/mark&gt;라고 생각합니다.&lt;/p&gt;
&lt;p&gt;물론 모든 팀과 모든 프로젝트에 정답이 될 수는 없겠지만, 이러한 도구가 어떤 문제를 해결할 수 있는지를 아는 것만으로도 앞으로 기술적인 문제를 해결해나가는 데 도움이 될 수 있다고 생각해요! 💪&lt;/p&gt;
&lt;p&gt;이 시리즈가 마이크로프론트엔드 구조 도입을 고민하고있는 팀, 혹은 MFE에 궁금증을 가지고계셨던 프론트엔드 개발자분들에게 조금이나마 도움이 되었기를 바랍니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;이번 글에서는 Nx를 활용한 MFE 환경 구축의 기본적인 방법을 다뤘지만, 실제 대규모 프로덕션 환경으로 확장하기 위해서는 더 많은 고민이 필요합니다. 상태 관리 전략, 공통 라이브러리 운영, 모니터링 및 디버깅, 성능 최적화 등 고도화된 과제들이 남아있죠. 올리브영 테크 팀은 이러한 영역들을 지속적으로 탐구하고 실전 경험을 쌓아가며, 그 과정에서 얻은 인사이트를 여러분과 함께 나누고자 합니다.&lt;/p&gt;
&lt;p&gt;올리브영 테크 팀과 함께 성장하고 새로운 기술 패러다임을 만들어나가고 싶다면, 저희의 다음 글도 기대해주세요! 🚀&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[하이브리드 앱에 구축하는 iOS 개발자모드]]></title><description><![CDATA[안녕하세요! 올리브영의 iOS Junior Engineer Ted입니다.
저는 오늘, 올리브영 iOS…]]></description><link>https://oliveyoung.tech/2025-11-06/swiftui-developer-mode/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-06/swiftui-developer-mode/</guid><pubDate>Thu, 06 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영의 iOS Junior Engineer Ted입니다.&lt;br&gt;
저는 오늘, 올리브영 iOS 앱에 &lt;strong&gt;개발자모드를 구축했던 경험&lt;/strong&gt;에 대해 이야기하려 합니다.&lt;br&gt;
신입 개발자로 올리브영에 합류하여 첫 프로젝트로 진행했던 개발자모드 구축을 통해 &lt;strong&gt;하이브리드 앱&lt;/strong&gt; 구조에 적응하고, &lt;strong&gt;FE와 소통&lt;/strong&gt;하는 법을 배우며, &lt;strong&gt;SwiftUI로 개발&lt;/strong&gt;한 경험을 공유하겠습니다 💪🏻💪🏻💪🏻&lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;-주니어-개발자가-경험한-올리브영-ios-앱&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EA%B2%BD%ED%97%98%ED%95%9C-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-ios-%EC%95%B1&quot; aria-label=&quot; 주니어 개발자가 경험한 올리브영 ios 앱 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👶🏻 주니어 개발자가 경험한 올리브영 iOS 앱&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;올리브영-앱은-하이브리드-앱&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%95%B1%EC%9D%80-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%EC%95%B1&quot; aria-label=&quot;올리브영 앱은 하이브리드 앱 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영 앱은? 하이브리드 앱!&lt;/h3&gt;
&lt;p&gt;신입 iOS 엔지니어로 올리브영에 합류했을 때 마주친 수많은 &lt;strong&gt;WebView&lt;/strong&gt;에 적잖이 당황했었습니다 😯
입사 전에는 네이티브 위주의 개발을 주로 경험했고, WebView는 가볍게만 사용해보았습니다.
팀에 합류했을 때에는 많은 서비스가 웹 기반이었기 때문에 iOS 웹뷰에 대한 이해가 많이 필요했습니다.
처음으로 개발해보는 하이브리드 앱의 구조가 어렵기도 하면서 새롭다고 느꼈던 것 같습니다.
아래는 올리브영 앱의 일부 구조입니다. 한번 같이 보시죠!&lt;br&gt;&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt; &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5ba55663b367c1f2cace69725c1255f9/18ef5/image1-app.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.37499999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAACxLAAAsSwGlPZapAAADwElEQVR42l2U60/bVhjG8wdW2qf9Cf2yfd5Fk+rYhra0lbqtIKZdKJvYqq0qSyl0LRQSQi6ExDFxnPgS2yF2bOee2DgJuXDOO5vSiu3DIx0dPf6d9/H7nhMSL5vfyNAihUubYAYKnfc0SsI2IUEzHOzfVAXaBH+hk8xApgMJMzN8w0dL0Pg65C8oDbyF4rz5YLucXHkjZZ6I0L9fgcHC/4GBL27xj7f5o5VIPrqaH+kPFXDpGx4qpKAG6aiyOCiXJ/O64YBtu0NNmfbOhAw7rBKBKTD7PsqrVStjTbuAZsOFVtuZ1vSpo1fS7Lh254MvpLgaNThJGyNBgEGxiFFdR1DTwU3FuUxXCIuo4UfphhU/4rSQNMGqAVIlDBKD9PI+SLG/WLZbCYsfgLKnUKPMiTHPcDBLctjdP0WX2So4OZZjziVCnAfAfljuifSUe2eOhCy0s3HspnbQ66NHsPV6iVWHNlH2E1wBT0cK1drdM0qbUShs7+Pi3d9Qfz0BajTGpbsSIV9XKPfK9KwYNwdHa2C+WsL6qwU0/J2A5lOSZWbNwHcN9GSqFjswLnc4SN3ZwDv0Kprtc1CL+cC+X+FHoEhPmLfm7DQCvdgadmK/IO14FYToD2zBqRHluUW/B7oyxW79bdh/7kH+XgQnbq+g+rM3UHi5VYi3eHLzZPe7ZLt8T/P/tZf5x5pwUXBSEexlXiJh83vIbzxkSxOb0KBHqv7BIXmkUafPnxvRxz/B0epTHFteQ0fL61CKRArMuRoueNVFMejyQKPHO7ctiH4K1h+f4PazW+js51vAr3/BMkMz/CK3+21MZ5ZCihd0OWM0sixIB3Gsx+Ool2Wgk0lx2RuRFVehneMdE6lpGPIpPCylEVayfvMO2KyjEvmBepe70OmQ7CrUOZs3oNkEV1PxWFcRNEzoMen/ADUf2MvmzPNKDS7ODDzTLQTNLpwXWR9YuYp8PYcK1c2lDafIQjOXxFY2hQZSATq5w6s5VKBDqeAQQYXjk4zlSTK4oohRu4NgNoexIrDZrvDxAoQ4VKdsSzYmLRsco4rHRhXN2w0w2yqX6JTJhFFYOrT5+0KrtDgs8SayG3BZNzH3dg/t/boB/HaELU6MIMl7IA8WLXpmuTvxhnKr3le7dq8/H40q03Y60eKpd5XjR3tnzINSR1jwjhOie3I8tA/2+y8WF/vLn30+OvxxJclP66Q/NgGQDlVw6ysRGjSPLDJ/ofk6I8tgkf6JV6r5cQNd3ee5RWrBx65CdsAlAYBsgEfy/v71a/Plv+EG1GECXfCVAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5ba55663b367c1f2cace69725c1255f9/263a4/image1-app.webp 480w,
/static/5ba55663b367c1f2cace69725c1255f9/a6361/image1-app.webp 960w,
/static/5ba55663b367c1f2cace69725c1255f9/0b34d/image1-app.webp 1920w,
/static/5ba55663b367c1f2cace69725c1255f9/da28f/image1-app.webp 2880w,
/static/5ba55663b367c1f2cace69725c1255f9/fcee9/image1-app.webp 2956w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5ba55663b367c1f2cace69725c1255f9/9aebd/image1-app.png 480w,
/static/5ba55663b367c1f2cace69725c1255f9/a91f8/image1-app.png 960w,
/static/5ba55663b367c1f2cace69725c1255f9/ac7a9/image1-app.png 1920w,
/static/5ba55663b367c1f2cace69725c1255f9/f9c26/image1-app.png 2880w,
/static/5ba55663b367c1f2cace69725c1255f9/18ef5/image1-app.png 2956w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5ba55663b367c1f2cace69725c1255f9/ac7a9/image1-app.png&quot; alt=&quot;올리브영 앱 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;&lt;figcaption&gt;올리브영 앱 구조&lt;/figcaption&gt;
&lt;p&gt;위의 올리브영 앱 구조를 보시면 아시겠지만, 많은 부분이 웹 기반의 서비스입니다.&lt;br&gt;
이런 웹 기반의 환경에서 고객에게 서비스를 제공하기위해, iOS 파트에서는 다음과 같은 일들을 FE와 소통하여 개발하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. URL 기반으로 올리브영 앱의 전반적인 Navigation을 관리합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 각 서비스 영역에서 필요한 네이티브 기능들을 제공합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;느낌이 오지 않나요? 네 맞아요. 올리브영 앱은 원활한 고객경험을 위해 FE와 상당히 많은 부분을 고민하고 함께 개발합니다.
이런 상황에서 FE 개발자와 협업하는 방식도 저에게는 새로운 경험이었습니다.
새롭게 알아가야 할 게 많은 상황에서 저는 첫 프로젝트였던 &lt;strong&gt;개발자 모드&lt;/strong&gt; 구축을 통해 WebView에 익숙해지며, FE 개발자분들과 협업하는 방식을 배웠습니다.&lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;첫-프로젝트-개발자모드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%AA%A8%EB%93%9C&quot; aria-label=&quot;첫 프로젝트 개발자모드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;첫 프로젝트: 개발자모드&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-매일-매일-들어오는-디버깅-요청-개발자모드의-등장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A7%A4%EC%9D%BC-%EB%A7%A4%EC%9D%BC-%EB%93%A4%EC%96%B4%EC%98%A4%EB%8A%94-%EB%94%94%EB%B2%84%EA%B9%85-%EC%9A%94%EC%B2%AD-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%AA%A8%EB%93%9C%EC%9D%98-%EB%93%B1%EC%9E%A5&quot; aria-label=&quot; 매일 매일 들어오는 디버깅 요청 개발자모드의 등장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧑🏻‍💻 매일 매일 들어오는 디버깅 요청: 개발자모드의 등장&lt;/h3&gt;
&lt;br&gt;
&lt;p&gt;올리브영 앱을 개발하다보면 FE개발자 분들과 소통할 일이 정말 많습니다. 웹과 iOS 앱이 함께 기능을 개발하며 고객에게 서비스를 전달하기 때문입니다.
필요한 기능을 웹에 API 형태로 전달하기도 하고 웹에서 전달받은 데이터를 이용해 앱에서 특정 기능을 동작하도록 합니다.
이 과정에서 iOS 웹뷰인 WKWebView의 &lt;code class=&quot;language-text&quot;&gt;WKScriptMessage&lt;/code&gt;를 통해 필요한 데이터를 주고받습니다.
(올리브영에서는 이를 편의상, JS Interface라고 부르고 있습니다)&lt;br&gt;&lt;/p&gt;
&lt;p&gt;그러다보면, Slack에서 다음과 같은 대화가 잦아집니다.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt; &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1192px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/95e640d8974a5e87851303e8f33b950d/bb7f4/image5-slack.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 29.583333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA+klEQVR42nWQy07EMAxF+/9fxR4JCfZISECmjZM0rzal7eRim1mAxFiy7Li1z7WH1laktmFZG9Z1RWOvpSDnjBgjUkqY51nzP5E9hKDvWiuWZdH+IcUFD8+PcMVAbN93vJkR5uMdZCcQORhj4IPnnOCc00FEHpfLqMOP49De1hqGnCumlyfU7LQYY2K654QB55fWer+qEhnofYCjCcG+IjgDYgBZqxBROmzbhh/ruGfneaLwGWT9nPkcKSKHT+RIrDAqrHBd1h7kht7zOpblM9UzfRwnVkEcR1imy1qBlcl/MrT3/+F6Q9lb6CK33qK8f7t+u9UlSuM9/wau0sxfsyxA4QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/95e640d8974a5e87851303e8f33b950d/263a4/image5-slack.webp 480w,
/static/95e640d8974a5e87851303e8f33b950d/a6361/image5-slack.webp 960w,
/static/95e640d8974a5e87851303e8f33b950d/d36b8/image5-slack.webp 1192w&quot; sizes=&quot;(max-width: 1192px) 100vw, 1192px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/95e640d8974a5e87851303e8f33b950d/9aebd/image5-slack.png 480w,
/static/95e640d8974a5e87851303e8f33b950d/a91f8/image5-slack.png 960w,
/static/95e640d8974a5e87851303e8f33b950d/bb7f4/image5-slack.png 1192w&quot; sizes=&quot;(max-width: 1192px) 100vw, 1192px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/95e640d8974a5e87851303e8f33b950d/bb7f4/image5-slack.png&quot; alt=&quot;슬랙의 수많은 디버깅 요청 내역&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;&lt;figcaption&gt;슬랙의 수많은 디버깅 요청&lt;/figcaption&gt;
&lt;p&gt;앱과 웹에서 데이터가 제대로 송수신이 되었는지 확인하기위해서는 직접 Xcode에서 빌드하여 디버깅을 해야합니다.
이에 더해서 Safari 웹 인스펙터를 이용하여 웹도 함께 디버깅을 해야하는 상황이 매번 연출되었습니다.
하이브리드 앱을 디버깅하기 위해서는 이렇게 양방향으로 디버깅을 해야하는 어려움이 있습니다.
이런 일이 계속 있다고 한다면? 상당히 비효율적이고 시간도 많이 소요됩니다.
이런 이유로 앱 개발자와 FE 개발자의 생산성 향상을 위해 올리브영에 &lt;strong&gt;개발자모드&lt;/strong&gt;가 도입되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;개발자모드-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%AA%A8%EB%93%9C-&quot; aria-label=&quot;개발자모드  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발자모드? 🤔&lt;/h3&gt;
&lt;p&gt;먼저, 개발자모드가 뭔지 알아야겠죠?&lt;br&gt;
올리브영의 개발자모드는 앱 실행중에 다양한 디버깅과 테스트 기능들을 지원하는 툴 입니다.
앱 개발자뿐만 아니라, FE 개발자들도 실행중인 앱 내에서 로그를 파악하고, 테스트 목적으로 특정 기능을 실행할 수 있는 &lt;strong&gt;필수적인 툴&lt;/strong&gt; 입니다.
물론 개발자들의 디버깅을 돕는 외부 라이브러리도 있습니다! 하지만, 하이브리드 앱 구조를 고려한 커스텀 기능들을 위해 직접 개발자모드를 구축했습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;올리브영 iOS의 개발자모드는 &lt;strong&gt;SwiftUI&lt;/strong&gt;를 이용하여 개발했습니다.
개발자 모드는 외부 사용자에게 노출되지 않고, 개발환경에서만 사용되는 기능입니다.따라서 UI 완성도보다는 빠른 구현 속도와 유연한 변경이 더 중요합니다.
SwiftUI를 선택한 가장 큰 이유는 &lt;strong&gt;선언형 UI의 생산성&lt;/strong&gt;입니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;화면의 상태를 데이터로 표현하고, 이를 기반으로 UI를 선언하듯 작성하면 기존 UIKit 대비 훨씬 빠르게 UI를 만들 수 있습니다.
간단한 &lt;code class=&quot;language-text&quot;&gt;View&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;@State&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;@Binding&lt;/code&gt;만으로도 필요한 화면을 모두 구현할 수 있었고, 짧은 주기로 기능을 실험하고 수정하는 개발자모드 특성에 딱 맞았습니다.
결과적으로, SwiftUI를 통해 개발자 모드의 화면은 &lt;strong&gt;기획 → 개발 → 배포&lt;/strong&gt;까지의 사이클이 크게 단축되었고, 개발자들에게 필요한 기능들을 빠르게 구현하고 배포할 수 있었습니다.&lt;br&gt;
(SwiftUI 덕분에 빠르게 개발하고 퇴근이 가능해졌습니다!)
&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 400px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 735px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/18349e616e956586bda81ae02e235d04/64a54/image10-gohome.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 80%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAQABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAFNFDWMw0hP/8QAGhAAAgMBAQAAAAAAAAAAAAAAAAESEyECA//aAAgBAQABBQLJy2fKK0yso8k//8QAFhEBAQEAAAAAAAAAAAAAAAAAABES/9oACAEDAQE/AYy//8QAFhEBAQEAAAAAAAAAAAAAAAAAABEh/9oACAECAQE/Abqv/8QAHBAAAgICAwAAAAAAAAAAAAAAABEBIQIyEkFx/9oACAEBAAY/AtUj0lHLHbsdUTTP/8QAGhABAAMBAQEAAAAAAAAAAAAAAQARIVFBMf/aAAgBAQABPyG1VXqchYg4NOwSF6wvBxmL6PxNhdd4z//aAAwDAQACAAMAAAAQbz//xAAXEQEBAQEAAAAAAAAAAAAAAAABABFB/9oACAEDAQE/EDZsh5f/xAAXEQEBAQEAAAAAAAAAAAAAAAABABFx/9oACAECAQE/EFGLu//EAB4QAQEAAgICAwAAAAAAAAAAAAERACExQVFhgaHR/9oACAEBAAE/EGjragm3L8v3lh+DDti9ZuJY3eImqdCVrD1IbxKeYLCdx6PzISg0mS73XmTP/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/18349e616e956586bda81ae02e235d04/263a4/image10-gohome.webp 480w,
/static/18349e616e956586bda81ae02e235d04/c4c4c/image10-gohome.webp 735w&quot; sizes=&quot;(max-width: 735px) 100vw, 735px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/18349e616e956586bda81ae02e235d04/a3e66/image10-gohome.jpg 480w,
/static/18349e616e956586bda81ae02e235d04/64a54/image10-gohome.jpg 735w&quot; sizes=&quot;(max-width: 735px) 100vw, 735px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/18349e616e956586bda81ae02e235d04/64a54/image10-gohome.jpg&quot; alt=&quot;빠르게 개발하고 퇴근하자!&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;출처: MBC &apos;무한도전&apos; 캡처&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;h2 id=&quot;whats-in-my-개발자모드-️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#whats-in-my-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%AA%A8%EB%93%9C-%EF%B8%8F&quot; aria-label=&quot;whats in my 개발자모드 ️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What&apos;s in my 개발자모드? ⚙️&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;개발자모드-버튼&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%AA%A8%EB%93%9C-%EB%B2%84%ED%8A%BC&quot; aria-label=&quot;개발자모드 버튼 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발자모드 버튼&lt;/h3&gt;
&lt;p align=&quot;center&quot;&gt; &lt;img src=&quot;/efc1a31b27070ff1a7a61bb234cb4416/image3-button.gif&quot; alt=&quot;개발자 모드 플로팅 버튼 동작 애니메이션&quot; width=&quot;200&quot;&gt;
&lt;/p&gt;&lt;figcaption&gt;개발자 모드 버튼&lt;/figcaption&gt;
&lt;p&gt;그럼 올리브영 iOS 앱의 개발자 모드에 대해 소개해드리겠습니다.&lt;br&gt;
올리브영 개발자 모드는, 플로팅 버튼을 통해 진입할 수 있습니다. (물론 개발환경에서만 진입할 수 있습니다!)
앱과 별도의 UIWindow에 개발자 버튼을 배치하여 항상 모든 화면보다 개발자 모드가 위에 있을 수 있도록 처리했습니다.
올리브영 개발자들은 버튼을 클릭하여 개발자모드에 진입하여 다양한 기능들을 사용할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;또한 버튼에 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;앱 버전,&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;iOS 버전,&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;현재 서버,&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;활성화된 기능,&lt;/code&gt;&lt;/strong&gt; 을 표시해두어 빠르게 기기 버전과 앱 버전을 파악할 수 있게 했습니다.
덕분에 Slack이나 Jira를 통해 전달받는 이슈에서도 개발자 모드 버튼만 보면 앱 버전과 서버를 즉시 확인할 수 있어, 이슈 추적 속도가 크게 빨라졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;webview-debugger&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#webview-debugger&quot; aria-label=&quot;webview debugger permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;WebView Debugger&lt;/h3&gt;
&lt;p align=&quot;center&quot;&gt; &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 800px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6c5135307cb73a2d43d3c1cdca5fa600/78d47/image4-web.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 67.91666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAACxLAAAsSwGlPZapAAABaUlEQVR42pVTXVODMBDk/7/75G/yoaNPdkbU2qItUCAkIV+33hFbH5tmZmcnQJa9vVylJofm0NJ30wlDuO9mTKNjLEUYB5uCB7N5rOaZD04a57NC2w7oGNNkYGyAMb4I9h9UOedQ1zWGYYAsIsK9i7WgHUGrhSrvpeQ9uxr/XieRvUvQRWAJqyAqHwlnC1h+MFlCMxIGQ6vTciTEGKFEULHIZk/46AXAcwPsBim93KGIhRAxK4cqJULi05E5xMyR9y7kfanLmFJ2KOWJq+0JePoCXplfeL85AJ3ODqjEoQ/Z4eqM1SWHC1yQTNL611QI+V5N7DDGnF/dEd6ZtyduzkKrr9IYRfCaoeTUGzAIxxk4KL5Tnq538u4uizqWFrR0IM/ttUeQ7UH6BxTd2u1bgheHXHKsKPGtVLuMaQca35g/uSMvgFcoaUtijZwjQWaZzJKgbSRjE89wzOAcjbk9z1o7AfHAQY3Lwy8AuEj4pFcXhgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6c5135307cb73a2d43d3c1cdca5fa600/263a4/image4-web.webp 480w,
/static/6c5135307cb73a2d43d3c1cdca5fa600/cf465/image4-web.webp 800w&quot; sizes=&quot;(max-width: 800px) 100vw, 800px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6c5135307cb73a2d43d3c1cdca5fa600/9aebd/image4-web.png 480w,
/static/6c5135307cb73a2d43d3c1cdca5fa600/78d47/image4-web.png 800w&quot; sizes=&quot;(max-width: 800px) 100vw, 800px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6c5135307cb73a2d43d3c1cdca5fa600/78d47/image4-web.png&quot; alt=&quot;개발자 모드 내의 WebView 메뉴 스크린샷&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;&lt;figcaption&gt;개발자 모드 : WebView 메뉴&lt;/figcaption&gt;
&lt;p&gt;다양한 기능들이 있지만, 올리브영 개발자 모드는 웹뷰에 관련된 기능을 가장 많이 사용합니다.
이 기능들을 통해 FE 개발자와 앱 개발자들은 로그를 확인할 수 있게 되었고, 런타임에 다양한 기능들을 사용할 수 있게 되었습니다.
더 이상 Xcode에서 디버깅을 하지 않아도 되었고, FE-App 개발자 모두 앱에서 로그를 확인할 수 있어 불필요한 커뮤니케이션이 사라졌습니다!
그럼 어떤 기능이 있는지 간략히 보여드리겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;URL Editor&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최상단 웹뷰의 URL의 정보와 User Agent 정보를 확인할 수 있으며, URL 이동, Query Parameter 수정이 가능합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;웹 로그 탐색기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;화면별 WKScriptMessage 로그, javascript 실행 로그를 확인하여 디버깅이 가능합니다.&lt;/li&gt;
&lt;li&gt;URL 이동로그, 외부링크 앱 진입 로그를 통해 어떤 경로로 앱의 Flow를 디버깅할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;웹 스토리지 뷰어&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최상단 웹뷰의 세션 스토리지, 로컬 스토리지, 쿠키 값을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Javascript Console&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;최상단 웹뷰에 런타임에 스크립트를 앱에서 실행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;스택 스크린 로그&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앱의 뷰 계층 구조(View Hierarchy)를 시각화할 수 있으며, 각 화면별 로그를 그룹화 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;로컬-플래그-기능-플래그-스위치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A1%9C%EC%BB%AC-%ED%94%8C%EB%9E%98%EA%B7%B8-%EA%B8%B0%EB%8A%A5-%ED%94%8C%EB%9E%98%EA%B7%B8-%EC%8A%A4%EC%9C%84%EC%B9%98&quot; aria-label=&quot;로컬 플래그 기능 플래그 스위치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;로컬 플래그: 기능 플래그 스위치&lt;/h3&gt;
&lt;p&gt;개발자 모드의 또 다른 주인공은 바로 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;로컬 플래그&lt;/code&gt;&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;로컬 플래그는 다양한 이유로 도입된 기능입니다. 올리브영의 일부 서비스는 안정적인 운영을 위해 기능 플래그를 이용하고 있습니다.
새로운 기능을 배포할 때 기능 플래그를 이용해 혹시 모르는 상황에 대비해 언제든지 기존 동작으로 돌아갈 수 있도록 설계하고 있습니다.
앱은 롤백이 불가능하고, 배포하기 위한 프로세스가 복잡하다보니, 중요한 기능들에 대해서는 기능 플래그를 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;기능 플래그를 이용해 개발을 하면 다음과 같은 프로세스가 추가적으로 발생합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;개발시에는 개발환경에만 필요한 플래그와 데이터를 서버에 등록해두어야 합니다.&lt;/strong&gt; (운영 환경에 영향을 미치면 안됩니다!)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QA 시에는 해당 서버의 기능 플래그를 직접 On/Off하며 기존 동작과 새로운 기능이 모두 잘 동작하는지 테스트 해야합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;이런 프로세스가 추가적으로 생기면서, 따라오는 비효율과 리스크가 발생했습니다.
개발 혹은 QA 시에 매번 기능 플래그를 On/Off를 해야하니 비효율이 증가했습니다. 단순 true/false 플래그가 아닌, 리스트형태로 갖고 있는 데이터도 변경해야하다보니 꼼꼼하게 검수도 해야 했습니다.
또한, 개발시에 사용하는 플래그와 운영에서 사용하는 플래그가 분리되어있고, QA 테스트에서 On/Off를 수시로 해야한다면, 실수로 운영 환경에 플래그를 변경할 &lt;strong&gt;위험&lt;/strong&gt;이 있습니다.
이와 같은 이유로 기능 플래그 값을 직접 수정하지 않고 테스트폰에서 직접 플래그 값을 조절할 수 있는 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;로컬 플래그&lt;/code&gt;&lt;/strong&gt; 를 개발하게 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content: center;&quot;&gt;
  &lt;div style=&quot;width: 400px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1204px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0600e1ec0c8cc5f7dd5979b300771811/1ea01/image6-flag.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 110.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAYAAADAQbwGAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC40lEQVR42o1Uy04UQRTtGRQTNwpGf8DowugfmOhfuCTRpTEkJsSV0ZVR3Iv4Ba5cuHMBsnEhSBgYCEMmIQ4M8+iZnul3V1c/jvfWdPMSmLnJye2+VffUuXWrStM0DYxCoYBisah8HhuGsbExhRPxPHBjYhKPHz7C3dt31H+RiAu80BnIx25N3sSDe/fxbOopJq9N5KTMShgnXCVcIVwaAZcz8PzrJGo8q+zFq5eY/TmP2aUv+KD8PPnPI+P94hzeLXzCR8p9PjMNLU2AJEhh6i76bRu24SENKSaGIyVAEvibc+IUmml68HyBvmkpWLYD1/Phi/BCBEKi59lYMypoOF3FYZouNMcRiKII5XIZpVIJq6t/sL+/h4ss5rLIvukLeLIxA0OaMKUDzw2h2XYAISI4jqdgWS6CIFTwadXj/nhcihhNUrbYXEHLNSBCSfnBgNA0fRwc6Gg2u9jba6HR6KJe1wltFavVmofj7Hms2TLQqHdgd3y4dkhCAjCXImR4noRLktnzNpwGzzkd8x2JyE3h2PSf8WgsU8qYVrCJkMvmkgWSJEUcJ2ciimNqKfDXbeBr/QeafhdCDoSopnDyzk4FW1tbqFar6HQ6IzXlu76EqY3X6Ek6HXlTWGGapicTSAHHhiEh4hqp1KkpKYlSTRkoTLC5uYn19XWlslarKeLTC/2nlPIi2i6j01MiXFcMFLJFkaS9lOpM8gIjKaR5gRAIKS/JFXJn2HSjj8r2Nu3lDnZ3d6kxAYYZk7AAm24Xkw+aQoRcWEPvYWV5Ge22rsi4hGHGc3huv2eq7ivCXCEP5iR5OcNKZpNJBD8WR03JCVu0saXSmmpOpVKhMuxzG5Nkx+ZXv4Q31Tl0QxNu5GfHJitZNywiKxORdaju3L3LCH9bZbwlwrYwSGUA18kehyCQCOk58v1QPRRhGJOXFyKgJyyWdGxorkc3SwTR0V22LJ/ewcFdVN8jwrQ85W1rkMeE/wAfOeZlFWUihwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0600e1ec0c8cc5f7dd5979b300771811/263a4/image6-flag.webp 480w,
/static/0600e1ec0c8cc5f7dd5979b300771811/a6361/image6-flag.webp 960w,
/static/0600e1ec0c8cc5f7dd5979b300771811/dbf17/image6-flag.webp 1204w&quot; sizes=&quot;(max-width: 1204px) 100vw, 1204px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0600e1ec0c8cc5f7dd5979b300771811/9aebd/image6-flag.png 480w,
/static/0600e1ec0c8cc5f7dd5979b300771811/a91f8/image6-flag.png 960w,
/static/0600e1ec0c8cc5f7dd5979b300771811/1ea01/image6-flag.png 1204w&quot; sizes=&quot;(max-width: 1204px) 100vw, 1204px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0600e1ec0c8cc5f7dd5979b300771811/1ea01/image6-flag.png&quot; alt=&quot;개발자모드 기능 플래그&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;개발자모드 기능 플래그&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;로컬 플래그를 &lt;code class=&quot;language-text&quot;&gt;On&lt;/code&gt;하면 개발자가 설정하는 값으로 기능이 동작합니다&lt;/li&gt;
&lt;li&gt;로컬 플래그에서 값을 변경하면 사용자가 식별하기 쉽게 파란 동그라미와 변경됨 표시를 제공합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;해당 기능을 통해, 원격 서버의 값 변경없이 개발 &amp;#x26; QA 시에 테스트 기기에서 플래그값을 조절 할 수 있게 되었습니다.
이 덕분에, 운영상에 영향을 미칠 리스크가 사라졌습니다. 그뿐만이 아닙니다. QA 엔지니어와 앱 개발자 간에 플래그 조절을 위한 커뮤니케이션도 할 필요 없게 되었습니다.
앱 개발자는 이제 기능 개발에 집중할 수 있게 되었고, 이는 또 빠른 퇴근으로 이어질 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만, 이 기능을 구현하는데에 있어 많은 고민들이 필요했었는데, 그 고민의 결과를 잠깐 공유하고자 합니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;고민의 결과&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;로컬 플래그 기능은 여러 버전을 거쳐서 현재의 안정적인 버전으로 자리를 잡았는데요.
이 과정에서 많은 리팩토링이 있었습니다. 리팩토링을 하며 모듈화, 추상화, 디자인패턴 등 많은 것을 경험할 수 있었습니다.
많은 고민들이 있었지만 구현상의 중요한 고민은 다음과 같았습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;어떻게 런타임에 리모트 플래그를 볼 것인지, 로컬 플래그를 볼 것인지 결정할 수 있을까?&lt;/strong&gt; (리모트 - 서버에 저장된 값, 로컬 - 테스트 단말의 저장된 값)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;운영 환경에는 절대 로컬 플래그로 동작하면 안된다!&lt;/strong&gt; (그것은 사고니까..)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;현재의 결과를 보며 어떻게 그 고민이 해소되었는지 보여드리겠습니다. 먼저 모듈 관계와 코드를 통해 OYFlag 모듈의 역할을 먼저 같이 보시죠!&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt; &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1526px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2b913e80453882048635271f4f0b48b5/2fa46/image11-module.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAACi0lEQVR42qVU22vTYBTfP6IvCm7ogxdQmaJThhdw0znnNthgF8TLvOA2nduEsUfxQUUExacxEHzxBvrkg/NNBUFLu96SdVuXNGnXxqRp2jTNz/N9se2YFBVP+JGT75z8cs53fl/qMpkMYrEYZFmGJElQVRX/Y3WmaYKRGoaBdDoN3dB5IOvoWCmIHEv5CJYJzJcLi3DcYm3C9Qslt8TvT6QpdPq34ny4Cdcix3ElcgQDwUb0zO/AZ/39r1znT4Quv5jdjw+jw78FF8IHcUNow5jQjoHQXnQGGjCnveY5zr8QPoiPEmE9LlNlY2I7kZ7CYKgRXYFt+Ki9qU3oukSxBuWWZxJ3qKJGDEdbMC6exS3xDLV9lCpuwldj7m9b9qpkZpVMqHacQ7GXkCAwP1WUUUKp9lA0TYOckKGmVKhJBdoPDXpWRzabhWXmUcjZsHNFDuZbpgWD4vo6GFkDjuOgTlEUREMCVCmF+IIEu2D/NvUy2JbUsvKW8ZbTjgIx74dY8CNq+RDJfYOQ83H9rdWc4WgQrQAEi/Jy37088tmaWTKqQ5mO9dNEG3AxfJg2/RAuhZvRF9xNmtvOXyrbc/UhWnwbMEh6ZMMZijRzv9W3EW9XZzxCVsF0rI9LYlLs4tO8vdCN69ET6A/uQcD8UiF8ptxDB+mQxZiMJsVuDAstXF6vUk+rhFOxXkqsxxBVxiRyleTRM78TvcFd8Gc/VQhnE3fR5t9Ep8cT+0i0lQrZR2ub8SL5uCqbRyvjvIWbwmlMLnR5pNFjXIOL+VCF8N3qLM6F9mNUOIkJypmg3BGhlT5wAB8yL6uE7EeQtCXCCmnO0x57ThfVyslhVnDzSNkyj3n6XOY+W7NdTx0/AXSmHg2dPaebAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2b913e80453882048635271f4f0b48b5/263a4/image11-module.webp 480w,
/static/2b913e80453882048635271f4f0b48b5/a6361/image11-module.webp 960w,
/static/2b913e80453882048635271f4f0b48b5/88408/image11-module.webp 1526w&quot; sizes=&quot;(max-width: 1526px) 100vw, 1526px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2b913e80453882048635271f4f0b48b5/9aebd/image11-module.png 480w,
/static/2b913e80453882048635271f4f0b48b5/a91f8/image11-module.png 960w,
/static/2b913e80453882048635271f4f0b48b5/2fa46/image11-module.png 1526w&quot; sizes=&quot;(max-width: 1526px) 100vw, 1526px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2b913e80453882048635271f4f0b48b5/2fa46/image11-module.png&quot; alt=&quot;플래그와 개발자모드 간의 모듈 관계도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;&lt;figcaption&gt;플래그 &amp;amp; 개발자모드 모듈 관계&lt;/figcaption&gt; &lt;p&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// MARK: OYFlag Module&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/// 실제 플래그 값을 가지고 있는 Object&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RemoteFlag&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; flags&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;fetchFlags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// MARK: OYFlag Module&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/// Flag 값을 return 할 수 있는 프로토콜&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;protocol&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagProvidable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Decodable&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;우선 모듈 관계입니다. Common Layer에 Flag Module이 있습니다. 이 플래그모듈의 역할은 단순합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;플래그 값을 서버로부터 받아와서 가지고 있습니다.&lt;/li&gt;
&lt;li&gt;이 플래그 값을 Return 하는 &lt;strong&gt;프로토콜 타입&lt;/strong&gt;이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 모듈이 제공하는 프로토콜을 이용해서 이제, 운영에서는 운영용 Flag Provider를, 개발환경에서는 개발용 Flag Provider를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음으로는 이 프로토콜을 사용하는 OYDevMode의 모듈의 코드의 일부를 보시죠!&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// MARK: OYDevMode&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OYFlag&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DevFlagProvider&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagProvidable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token attribute atrule&quot;&gt;@Published&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; isLocalFlagMode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;token attribute atrule&quot;&gt;@Inject&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; localFlag&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LocalFlag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 로컬 플래그는 컨테이너에 넣어두고, 개발자모드에서도 사용합니다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; remoteFlag &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RemoteFlag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Decodable&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    isLocalFlagMode &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; localFlag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; remoteFlag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;개발환경에서만 사용할 FlagProvider입니다. 운영에서 사용할 FlagProvider는 운영 모듈에서 따로 구현하여 Remote Flag 값만 Return 할 수 있게 했습니다.
따라서 이제는 앱이 메모리에 올라올 때에, 이 전역적으로 사용할 Provider의 Concrete 타입만 잘 정해서 넣어주면 됩니다. 이렇게 하면 운영에서는 운영 Provider를 사용할 수 있고 개발 환경에서는 개발용 Provider를 사용할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/// Container Register&lt;/span&gt;
&lt;span class=&quot;token directive property&quot;&gt;&lt;span class=&quot;token directive-name&quot;&gt;#if&lt;/span&gt; DEBUG&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 개발 환경&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;OYContainer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;FlagProvidable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token omit keyword&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DevFlagProvider&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token directive property&quot;&gt;&lt;span class=&quot;token directive-name&quot;&gt;#else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 운영 환경&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;OYContainer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;FlagProvidable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token omit keyword&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagProvider&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token directive property&quot;&gt;&lt;span class=&quot;token directive-name&quot;&gt;#endif&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;여기서 잠깐. &lt;code class=&quot;language-text&quot;&gt;OYContainer&lt;/code&gt;가 무엇이냐고요? &lt;code class=&quot;language-text&quot;&gt;OYContainer&lt;/code&gt;는 올리브영 iOS 프로젝트에서 자체적으로 개발하여 사용중인 &lt;strong&gt;경량 DI 컨테이너&lt;/strong&gt;입니다. 일반적인 컨테이너와 같은 기능을 지원하고, &lt;code class=&quot;language-text&quot;&gt;@Inject&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;@LazyInject&lt;/code&gt;와 같은 커스텀 프로퍼티 래퍼를 이용하여 의존성 주입을 합니다. FlagProvidable를 해당 컨테이너에 넣어두고 여러 Feature 모듈에서 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;아무쪼록 Protocol과 컨테이너를 이용하여 Flag를 사용하는 코드에서 운영환경이든 개발환경이든 관계 없이 코드를 작성할 수 있습니다.
Flag를 필요로하는 비즈니스 로직 혹은 View 어느 곳이든 Container를 통해 FlagProvider를 이용할 수 있습니다. 바로 다음과 같이 사용하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OYFlag&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductView&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token attribute atrule&quot;&gt;@Inject&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; flagProvider&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FlagProvidable&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; flagProvider&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProductKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;featureA&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 기능 플래그에 따른 특정 기능&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;그럼 개발자 모드에서는 어떻게 값을 변경하냐고요? 개발자모드에서는 간단하게 로컬 플래그를 변경할 수 있습니다!
컨테이너에 등록되어 있는 로컬 플래그 Object의 값을 변경하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LocalFlagView&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 컨테이너에 등록되어 있는 LocalFlag&lt;/span&gt;
  &lt;span class=&quot;token attribute atrule&quot;&gt;@ObservedObject&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; localFlag &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OYContainer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;LocalFlag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Toggle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isOn&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Binding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; localFlag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flagDict&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; newValue &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; localFlag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flagDict&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Toggle 변경시 LocalFlag 값 변경&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token class-name&quot;&gt;VStack&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 추상화와 모듈화를 이용한 설계 덕분에 해결하고 싶었던 문제를 해결할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;✅ 개발자모드에서 로컬 플래그를 이용하여 런타임에 값을 변경 &lt;br&gt;
✅ 운영 환경과 개발 환경의 Flag Provider를 완전히 분리하여 운영에 미칠 리스크 원천 차단 &lt;br&gt;
✅ OCP(Open/Closed principle)을 지킴으로써 향후 A/B 테스트를 위한 ABFlagProvider가 추가되어도 수정없이 확장가능&lt;/p&gt;
&lt;p&gt;처음에는 이렇게 깔끔하게 설계하지 못했었는데, 동료들과 고민하고 보완해나가니 배우는 것도 많았고 특히 추상화와 모듈화의 실용적인 적용을 경험할 수 있어 좋았습니다 😎
어때요 개발자모드 참 쉽죠?&lt;/p&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 350px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 497px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4d9a5f72cbdedc74a0afab0e26fa6f51/27076/image9-easy.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/2gAMAwEAAhADEAAAAbuYjSPLBr//xAAdEAABBAIDAAAAAAAAAAAAAAACAAEDBBESEyEi/9oACAEBAAEFAg9DP2Vc7HHs6jHJ6sK//8QAFhEBAQEAAAAAAAAAAAAAAAAAARAh/9oACAEDAQE/AQ2f/8QAFhEAAwAAAAAAAAAAAAAAAAAAEBEh/9oACAECAQE/AXB//8QAGhAAAgMBAQAAAAAAAAAAAAAAAAECESIxcf/aAAgBAQAGPwK3FCUHRnSs6Q9KR//EABwQAQACAgMBAAAAAAAAAAAAAAEAESExQVFhkf/aAAgBAQABPyF/eGpuBVt1LgsFLS5lRwqD7kd8Ro4Tyf/aAAwDAQACAAMAAAAQJ8//xAAYEQACAwAAAAAAAAAAAAAAAAAAAREhUf/aAAgBAwEBPxBFsJP/xAAXEQADAQAAAAAAAAAAAAAAAAAAAREh/9oACAECAQE/EG2CH//EABwQAQADAAIDAAAAAAAAAAAAAAEAESExUXGBof/aAAgBAQABPxALQKR5j4iMohUjMqvc0jYdYZbEWq0UZT3OLgcE0W+wUaZwkJ//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4d9a5f72cbdedc74a0afab0e26fa6f51/263a4/image9-easy.webp 480w,
/static/4d9a5f72cbdedc74a0afab0e26fa6f51/5a053/image9-easy.webp 497w&quot; sizes=&quot;(max-width: 497px) 100vw, 497px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4d9a5f72cbdedc74a0afab0e26fa6f51/a3e66/image9-easy.jpg 480w,
/static/4d9a5f72cbdedc74a0afab0e26fa6f51/27076/image9-easy.jpg 497w&quot; sizes=&quot;(max-width: 497px) 100vw, 497px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4d9a5f72cbdedc74a0afab0e26fa6f51/27076/image9-easy.jpg&quot; alt=&quot;어때요, 참 쉽죠&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;출처: EBS의 &apos;그림을 그립시다&apos;의 밥로스&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;-0건의-디버깅-요청-그리고-주니어의-성장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-0%EA%B1%B4%EC%9D%98-%EB%94%94%EB%B2%84%EA%B9%85-%EC%9A%94%EC%B2%AD-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%A3%BC%EB%8B%88%EC%96%B4%EC%9D%98-%EC%84%B1%EC%9E%A5&quot; aria-label=&quot; 0건의 디버깅 요청 그리고 주니어의 성장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚀 0건의 디버깅 요청, 그리고 주니어의 성장&lt;/h2&gt;
&lt;p&gt;개발자모드 도입의 성과는 명확했습니다. 가장 큰 변화는 두 가지입니다.
첫째로, 웹뷰 관련 단순 &lt;strong&gt;디버깅 요청은 0건&lt;/strong&gt;으로 수렴했습니다.
두번째로, 기능 플래그 때문에 서버의 값을 바꾸는 비효율적이고 위험한 일은 하지 않아도 됩니다.
FE 개발자분들은 필요한 로그를 직접 앱에서 확인하고, QA 엔지니어분들은 로컬 플래그를 통해 서버 값 변경 없이도 자유롭게 테스트를 진행할 수 있습니다.
&quot;개발자 모드에 디버깅에 필요한건 다 있어서 개발이 많이 편해졌다&quot;라는 FE 개발자분들의 긍정적인 피드백도 받을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;결과적으로 앱 개발자는 불필요한 커뮤니케이션 비용을 줄이고 본연의 기능 개발에 더 집중할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;신입 개발자로서 첫 프로젝트를 진행하며, 올리브영 앱의 하이브리드 아키텍처와 도메인 지식을 빠르게 습득할 수 있었습니다.
SwiftUI의 높은 생산성 덕분에 UI 개발 시간은 아끼고, &lt;code class=&quot;language-text&quot;&gt;FlagProvidable&lt;/code&gt; 프로토콜 설계처럼 더 중요하고 근본적인 고민에 시간을 쏟을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;개발자모드&lt;/code&gt;는 단순히 편의 기능을 만든 것이 아니라, &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;개발자들의 비효율을 해결하는 엔지니어링&lt;/code&gt;&lt;/strong&gt; 이었습니다.
앞으로도 올리브영에서 동료들의 생산성을 높이고, 나아가 고객에게 더 나은 서비스를 제공하는데 기여하는 엔지니어로 성장하겠습니다! 💪🏻💪🏻
여러분의 경험과 질문을 댓글로 남겨주시면 함께 고민하고 성장할 수 있을 것 같습니다! 🙇🏻&lt;/p&gt;</content:encoded></item><item><title><![CDATA[7,000줄 PL/SQL 프로시저와의 결별: 클레임 로직 Java 모듈 이관기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-11-06/claim-Procedure/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-06/claim-Procedure/</guid><pubDate>Thu, 06 Nov 2025 18:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%86%8C%EA%B0%9C&quot;&gt;소개&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B1%B0%EB%8C%80%ED%95%9C-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80%EC%9D%98-%EA%B7%B8%EB%A6%BC%EC%9E%90&quot;&gt;거대한 프로시저의 그림자&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C%EC%9D%84-%EA%B2%B0%EC%8B%AC%ED%95%98%EB%8B%A4&quot;&gt;레거시 탈출을 결심하다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A7%88%EC%A3%BC%ED%95%9C-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4&quot;&gt;마주한 시행착오&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EA%B0%80-%EC%96%BB%EC%9D%80-%EA%B2%83&quot;&gt;우리가 얻은 것&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot;&gt;마치며&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B8%80%EC%93%B4%EC%9D%B4&quot;&gt;글쓴이&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;소개&lt;/h2&gt;
&lt;p&gt;안녕하세요.&lt;br&gt;
올리브영 온라인몰에서 교환·반품 서비스를 담당하고 있는 클레임 스쿼드 &lt;strong&gt;현앙&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 수천 줄짜리 &lt;code class=&quot;language-text&quot;&gt;PL/SQL&lt;/code&gt; 프로시저에 얽혀 있던 교환·반품 로직을 &lt;code class=&quot;language-text&quot;&gt;Java&lt;/code&gt;로 이관하며, 기능별로 분리하고 유지보수성을 높였던 경험을 공유하려 합니다.&lt;/p&gt;
&lt;p&gt;우리가 마주한 수천 줄 짜리 프로시저라는 문제는 단순한 기술 부채에 그치지 않았습니다. 그 영향은 곧바로 고객 경험으로 이어졌죠.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;환불이 왜 지연되나요?&quot; = 고객님 죄송합니다.. 오류가났네요..&lt;br&gt;
&quot;반품신청했는데 회수를 안해가요&quot;  = 고객님 죄송합니다.. 반품접수가 안됐어요..&lt;br&gt;
&quot;교환이 접수된 건 맞나요?&quot; = 고객님 죄송합니다...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이런 문의들의 대부분은 &lt;strong&gt;개발자가 디버깅하기 어려운 구조&lt;/strong&gt;와 모든 로직이 한데 엉켜있는 레거시 프로시저에서 비롯된 경우가 많았습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
   &lt;div style=&quot;width: 80%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1658px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/815d44fda801c98ed94b2b9761af83a7/62cc8/blackBoxProcedure.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACIElEQVR42o2T3UuTURzHn6su+gfqwi56gQpCKnqxLnJmREUXUSSrULekRC8iukqqlUlvNi80mi2JItLaGmTRVhQ5dZvbVApz69JUGiMIdbP27j6d8+iYUZIX3+d7nnPO7/17lHTEQzLqYfqHk8xPH8lJF6kpN0kJsZackDy3/z8oTHu5/aSOA+cPMRR4BrF+EhO9pKN9ZEWA+cgZzQ80u+fJO0xH3FQbT7K6oogXH5oh3o/MOjzuYGi4XcXgp8eCO2aznHQzI5KQAeTdhKzwjwzjAzRbLrGlppjRkVfEJnqEgY/Q6Gu6Pffw+Nro7TPjFhyfcDET8RIM2AgGn9PtfUDLUwOfh60w1y5FfvQ3K1lftQvLW6OI6ifwxUpFw3G0l8vQXStHf72cIxcPU992TjXssBvZUVvK1prdrNVtp/TsftVpVmSukBik1XZFHBbzbcyuGjh67rJk3wqWHlxJwbFNLDu6AWXPcuGkRD032xoo0BaysVrDOn0Rq05sxtHVCr/8MsNeqm7pKDxdgvVdk9j0Ehpz8LDzhmpotl3lvmCTtR6700RGDCs8/obGR3VozuxFayjDZKkn+t2Z7+Edi4FttRq+jrxUJ5wSjSb18S9kRTWpKZdQgp+YaNWQ6GU49F4dTG7iSkb8nGrUsaZyJ51dLcgAceF0IaRyuhQspSWhrufrsKn9Ql6Hog+JRYo4+Q/BK7I89YXkeJHOFnopvwFA/dGeOZJc8gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/815d44fda801c98ed94b2b9761af83a7/263a4/blackBoxProcedure.webp 480w,
/static/815d44fda801c98ed94b2b9761af83a7/a6361/blackBoxProcedure.webp 960w,
/static/815d44fda801c98ed94b2b9761af83a7/08259/blackBoxProcedure.webp 1658w&quot; sizes=&quot;(max-width: 1658px) 100vw, 1658px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/815d44fda801c98ed94b2b9761af83a7/9aebd/blackBoxProcedure.png 480w,
/static/815d44fda801c98ed94b2b9761af83a7/a91f8/blackBoxProcedure.png 960w,
/static/815d44fda801c98ed94b2b9761af83a7/62cc8/blackBoxProcedure.png 1658w&quot; sizes=&quot;(max-width: 1658px) 100vw, 1658px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/815d44fda801c98ed94b2b9761af83a7/62cc8/blackBoxProcedure.png&quot; alt=&quot;blackBoxProcedure&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;개발자도 프로시저 내에서 발생한 오류를 추적하는 것이 불가능했고, 블랙박스와도 같은 느낌이었기에....&quot;&lt;br&gt;
&quot;프로시저 디버그는 왜이렇게 어려운걸까요.. 수정과 컴파일은.. 왜.. 또.. 복잡하고... 어렵...고..난리야...&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;거대한-프로시저의-그림자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B1%B0%EB%8C%80%ED%95%9C-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80%EC%9D%98-%EA%B7%B8%EB%A6%BC%EC%9E%90&quot; aria-label=&quot;거대한 프로시저의 그림자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;거대한 프로시저의 그림자&lt;/h2&gt;
&lt;p&gt;우리 시스템에는 커머스에서 가장 중요한 모든 흐름이 &lt;strong&gt;하나의 프로시저&lt;/strong&gt; 안에 들어 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;주문 완료&lt;/li&gt;
&lt;li&gt;주문 취소&lt;/li&gt;
&lt;li&gt;교환 접수/철회/완료&lt;/li&gt;
&lt;li&gt;반품 접수/철회/완료&lt;/li&gt;
&lt;li&gt;옵션 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 모든 것이 &lt;strong&gt;7,000줄이 넘는 &lt;code class=&quot;language-text&quot;&gt;PL/SQL&lt;/code&gt; 코드&lt;/strong&gt;로 구성된 단일 프로시저 안에 뒤섞여있었습니다.
문제는 단순히 코드가 길다는게 아니라 비즈니스 로직 경계가 무너져 반품 절차 하나만 수정하려 해도 주문, 교환 로직에까지 영향을 미치는 구조였습니다. 이로 인해 작은 수정이 전사적인 서비스 리스크로 이어졌고, 결국 누구도 건드릴 수 없는 **기술 부채의 핵심(Core Tech Debt)**으로 남게 되었습니다.&lt;/p&gt;
&lt;p&gt;이 구조는 모두가 기술 부채임을 알고 있었지만, 어디서부터 손대야 할지 모르는 상태로 오랜시간 유지되었습니다. 프로시저 안에는 수년간의 패치, 신규 정책이 누적되어 있었고, 문서화 되지 않은 수많은 분기 처리가 있었기 때문입니다.
결국 누구도 쉽게 들어가서 수정할 수 없는 ... 건드리면 터져버리는 폭탄이 되어버린거죠. 그로인해 아무도 고치고싶어하지않는, 고치지않는 코드가 되어버렸습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
  &lt;div style=&quot;width: 80%; text-align: center;&quot;&gt;
    &lt;figure style=&quot;margin: 0;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/75c561022cdb29686df2b47786e24cb2/8c935/returnProcedure1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 67.29166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAADX0lEQVR42lWT60/bVRzG+U9M1MQXZiYu8YUvjGb+D3uFUYMzilsmwsYdysq1LeWeDlp6obT0AmtpKRI6sPQC9MZEChMZ42JMjBcoXRktBT6e/hZjfHVyzsn3+T7P8/0+Jbl0mLOjEIXMMvmTMHu7XuaWHrK97SK16cS3OEQoamBt3UpsbZytp1NEokZ2d6ZZ37BzLmpeHi0JjCXOMwlKzk8inKUjHO7PwMsYiaSFbmMt88Fhfoho6TJUY/OpmAtocM6ppSZap5z4EwsTMyqusqtcibrLF8sCME4Jp1HU1nreK7+BwdvB8Z8LPNsT3VN2okkz/vAwj8MjBFf1rCTGWI6biMSMBFZ04jQIJY+k/9SmncKLJCWXQurX3WW89fkHVGnKeXbgxupXMeyV80D/HQPWJpSmGtr09+g03KfLWM3ARBMahwzH990sChUtIxWY3G1cZtcEw1yCKX8vb35yndi6BQrrbKacWGeVKG21dFqr0ThlqM119FsbJbAhezMWr4JxjwL/ygiHh7NcCcm5kxglhUyET9tLeefLj7nd9wUbOw4sfiX62XY0Lhk9tnpax6p4YKqUzl5LPQpDDUOOZmSmCnonG0htOSmI4eRPih6exTHPKHi99F3mI8PSYH4S/hXlaD2t9IiC/qkmWowVNOnvUqu7TY3uG+r1d+gy3WfCp2Rj08FF5l9AMaUG7bdcu/URPfYGnh96cC724FhUMyCAFI4aWserkFsqqRZAckMlHWP3qBu9g9JSKzX+47f5/wCvsitUDZXzdtmHKMZryB4FCCeMmH1dGKfb0T9qp9/eSJ9oVmQmN1fSOHpXsmJQNCxac/T74/9LnphT8UbpdQJRPcV7bM2M1adA65Iz6G6mf7IRlb2OPiFfZa9FbauTvNN72rAtdHMgdvhCzCJfHMqFWJubspu8Vvo+t1Sfsb3vwhMZxDDbwaivjTFvp2DZJq1Jr7OBAQGunWmlz9bAQ3cL3tAg2b8CnKdD5F8tdgx/ZISvVGU82bAJhklBf1lE0MNCSCciNyGZHk2Yxc7pCMdMJMXbj1s2KX6nfwel2OaOQ6+il08vcXm6Crk4F1kRQ+FhPh0kIxLz89NJ9gXw/u40z3fc7P7i4tc9LwfijeyyiFtYMAuSK7I7FiwzMf4BQJwg9XKCDf8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/75c561022cdb29686df2b47786e24cb2/263a4/returnProcedure1.webp 480w,
/static/75c561022cdb29686df2b47786e24cb2/a6361/returnProcedure1.webp 960w,
/static/75c561022cdb29686df2b47786e24cb2/0b34d/returnProcedure1.webp 1920w,
/static/75c561022cdb29686df2b47786e24cb2/36b94/returnProcedure1.webp 1940w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/75c561022cdb29686df2b47786e24cb2/9aebd/returnProcedure1.png 480w,
/static/75c561022cdb29686df2b47786e24cb2/a91f8/returnProcedure1.png 960w,
/static/75c561022cdb29686df2b47786e24cb2/ac7a9/returnProcedure1.png 1920w,
/static/75c561022cdb29686df2b47786e24cb2/8c935/returnProcedure1.png 1940w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/75c561022cdb29686df2b47786e24cb2/ac7a9/returnProcedure1.png&quot; alt=&quot;반품 절차 구조도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;figcaption style=&quot;font-size: 0.9em; color: #666; margin-top: 6px;&quot;&gt;
        미로같은 프로시저이기에, 변경 범위와 영향도를 예측하기 어렵다는 게 가장 큰 문제였습니다.
      &lt;/figcaption&gt;
    &lt;/figure&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse:collapse;width:100%;&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;문제 영역&lt;/th&gt;
      &lt;th scope=&quot;col&quot; style=&quot;border:1px solid #e5e7eb;padding:8px;text-align:left;&quot;&gt;상세 설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;디버깅 난이도&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;DB 내부에서의 함수 호출, 로그성 데이터 부재&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;인력 의존성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;소스를 이해하는 인력이 극소수라, 변경 자체가 리스크&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;&lt;strong&gt;우회 로직 남발&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;border:1px solid #e5e7eb;padding:8px;&quot;&gt;프로시저 수정을 피하기 위해 호출 앞뒤에 로직을 붙여 임시 대응 발생&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;난 반품로직만 수정하고 싶은데.. 왜 주문이 안되는거야^.ㅠ....&quot;와 같은
작은 변경이 곧 전사적 리스크가 되는 상황, 바로 이것이 우리가 탈출하고자 했던 현실이었습니다.&lt;br&gt;
바로 이 상황에서 &quot;이제는 프로시저와 이별하자!&quot;라는 결심이 시작되었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;레거시-탈출을-결심하다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%83%88%EC%B6%9C%EC%9D%84-%EA%B2%B0%EC%8B%AC%ED%95%98%EB%8B%A4&quot; aria-label=&quot;레거시 탈출을 결심하다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;레거시 탈출을 결심하다&lt;/h2&gt;
&lt;p&gt;우리가 내린 해법은 단순했습니다.&lt;br&gt;
&lt;strong&gt;“프로시저를 쪼개고, 기능별 로직을 &lt;code class=&quot;language-text&quot;&gt;Java&lt;/code&gt;에서 다루자.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;하지만 실행은 쉽지 않았습니다.&lt;br&gt;
&lt;code class=&quot;language-text&quot;&gt;PL/SQL&lt;/code&gt;은 데이터베이스 내에서 실행되므로 네트워크 오버헤드가 없고 DB 내부에서 최적화된다는 장점 때문에, Java로 변경시 성능 저하 우려가 있었으며 7,000줄짜리 코드에는 지뢰같은 예외 케이스도 숨어 있었습니다.
또한 올리브영은 MSA를 목표로 하고 있기 때문에 각 모듈별 중요 로직들을 신규 API로 이관을 해야하는지에 대한 고민이 있었습니다.
다만 프로시저 로직에 대한 이해도가 낮았고, 커머스에 가장 중요한 로직이기 때문에 완벽히 신규로 만드는 것에 대한 부담이 있었습니다.&lt;/p&gt;
&lt;p&gt;따라서 우리는 &lt;strong&gt;무중단 점진적 이관 전략&lt;/strong&gt;을 택했습니다.&lt;br&gt;
올리브영 온라인몰은 24시간 많은 고객님들이 들어오는 시스템기에 무중단 점진적 전환이 매우 중요했습니다. 저희 클레임 스쿼드는 문제 해결 방법으로 레거시 전환의 모범 사례로 알려진 Strangler Pattern을 적용했습니다. 이는 기존 레거시 시스템을 새로운 시스템으로 점진적으로 대체하는 전략이자 올리브영 개발 조직의 개발 철학 중 하나라, 저희는 이 전략을 택하였습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;설계&lt;/strong&gt;: 주문·교환·반품·옵션 변경 기능별 경계를 재정의&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도입&lt;/strong&gt;: 프로시저 호출 구간 앞단에 Java 모듈을 두어, 기존 프로시저는 유지한 채 점진적으로 전환&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실행&lt;/strong&gt;: 기능 단위로 이관하면서 장애 발생 시 [Fallback] 모드로 즉시 원복 가능하도록 설계&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검증&lt;/strong&gt;: 단위 및 통합 테스트 강화, 주요 지표 모니터링 체계화&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;점진적-이관--step-by-step&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%90%EC%A7%84%EC%A0%81-%EC%9D%B4%EA%B4%80--step-by-step&quot; aria-label=&quot;점진적 이관  step by step permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;점진적 이관 : Step-by-Step&lt;/h2&gt;
&lt;h3 id=&quot;-phase-0-사전-준비-preparation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-phase-0-%EC%82%AC%EC%A0%84-%EC%A4%80%EB%B9%84-preparation&quot; aria-label=&quot; phase 0 사전 준비 preparation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧩 Phase 0. 사전 준비 (Preparation)&lt;/h3&gt;
&lt;p&gt;목표 : 리스크 최소화와 빠른 의사결정을 위한 기능 분석 및 구조 파악&lt;/p&gt;
&lt;ul style=&quot;margin-left: 1rem;&quot;&gt;
  &lt;li&gt;&lt;strong&gt;모듈별 기능 분석&lt;/strong&gt; : 미사용 분기처리, 예외처리 등 중요로직 파악을 위한 모듈별 흐름도 작성&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;트랜잭션 경계 맵핑&lt;/strong&gt; : COMMIT/ROLLBACK 지점, 격리수준, 재시도·보상 로직 정리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 계약 정리&lt;/strong&gt; : 입력/출력 스키마, 에러 계약, 신·구 모듈 간 호환 규칙 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-phase-1-도메인-모델링-domain-modeling&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-phase-1-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8%EB%A7%81-domain-modeling&quot; aria-label=&quot; phase 1 도메인 모델링 domain modeling permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧭 Phase 1. 도메인 모델링 (Domain Modeling)&lt;/h3&gt;
&lt;p&gt;목표 : 레거시 의존을 줄이고, 기능별 경계를 명확히 정의&lt;/p&gt;
&lt;ul style=&quot;margin-left: 1rem;&quot;&gt;
  &lt;li&gt;&lt;strong&gt;모듈간 경계 분석&lt;/strong&gt; : 주문·교환·반품·옵션 변경 기능별 도메인 경계 재정의 &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;도메인 엔티티/Value Object 설계&lt;/strong&gt; : 도메인 경계별 필요 엔티티 정의&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fallback 로직 설계&lt;/strong&gt; : 문제 발생시 프로시저 버전으로 원복될 수 있도록 로직 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-phase-2-모듈-이관-migration&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-phase-2-%EB%AA%A8%EB%93%88-%EC%9D%B4%EA%B4%80-migration&quot; aria-label=&quot; phase 2 모듈 이관 migration permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚀 Phase 2. 모듈 이관 (Migration)&lt;/h3&gt;
&lt;p&gt;목표 : 기능 단위로 점진적 전환 및 안정성 확보&lt;/p&gt;
&lt;ul style=&quot;margin-left: 1rem;&quot;&gt;
  &lt;li&gt;&lt;strong&gt;Java 모듈 구현 및 테스트&lt;/strong&gt; : 단위 테스트, 통합 테스트 진행&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Shadow 트래픽 검증&lt;/strong&gt; : 신규 모듈과 구 레거시를 병행 처리하여 결과 비교&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;모니터링 강화&lt;/strong&gt; : 주요 지표가 기준선을 유지하는지 실시간 검증  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-phase-3-레거시-해체-decommission&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-phase-3-%EB%A0%88%EA%B1%B0%EC%8B%9C-%ED%95%B4%EC%B2%B4-decommission&quot; aria-label=&quot; phase 3 레거시 해체 decommission permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧹 Phase 3. 레거시 해체 (Decommission)&lt;/h3&gt;
&lt;p&gt;목표 : 완전 전환 후 불필요한 복잡도 제거&lt;/p&gt;
&lt;ul style=&quot;margin-left: 1rem;&quot;&gt;
  &lt;li&gt;나머지 모듈 Phase0 ~ 2까지 계속 진행&lt;/li&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;mermaid&quot;&gt;&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;    A&lt;span class=&quot;token text string&quot;&gt;[기존 PL/SQL 프로시저]&lt;/span&gt; 
    A &lt;span class=&quot;token arrow operator&quot;&gt;--&gt;&lt;/span&gt;&lt;span class=&quot;token label property&quot;&gt;|분리|&lt;/span&gt; B[주문 모듈&lt;span class=&quot;token text string&quot;&gt;(Java)]&lt;/span&gt;
    A &lt;span class=&quot;token arrow operator&quot;&gt;--&gt;&lt;/span&gt;&lt;span class=&quot;token label property&quot;&gt;|분리|&lt;/span&gt; C[교환 모듈&lt;span class=&quot;token text string&quot;&gt;(Java)]&lt;/span&gt;
    A &lt;span class=&quot;token arrow operator&quot;&gt;--&gt;&lt;/span&gt;&lt;span class=&quot;token label property&quot;&gt;|분리|&lt;/span&gt; D[반품 모듈&lt;span class=&quot;token text string&quot;&gt;(Java)]&lt;/span&gt;
    A &lt;span class=&quot;token arrow operator&quot;&gt;--&gt;&lt;/span&gt;&lt;span class=&quot;token label property&quot;&gt;|분리|&lt;/span&gt; E[옵션 변경 모듈&lt;span class=&quot;token text string&quot;&gt;(Java)]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;li&gt;미사용 분기 제거 및 신규 모듈로의 완전 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;마주한-시행착오&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%A3%BC%ED%95%9C-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4&quot; aria-label=&quot;마주한 시행착오 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마주한 시행착오&lt;/h2&gt;
&lt;p&gt;1년간의 이관 과정에서 다양한 시행착오를 겪었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;트랜잭션 문제&lt;/strong&gt;&lt;br&gt;
기존에는 프로시저에서 암묵적으로 처리되던 부분이 Java로 옮겨지며 데이터 반영이 끊기는 문제가 발생했습니다.&lt;br&gt;
→ 트랜잭션 익셉션을 재설계해 안정성을 확보했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;로직 중복&lt;/strong&gt;&lt;br&gt;
&lt;code class=&quot;language-text&quot;&gt;PL/SQL&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;Java&lt;/code&gt;가 병존하면서 소스 이원화 문제가 있었습니다.&lt;br&gt;
→ Feature Toggle 관리로 혼선을 줄였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;교훈&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;모든 걸 한 번에 옮기려 하지 말 것.&lt;/li&gt;
&lt;li&gt;모니터링은 철저하게 할 것!&lt;/li&gt;
&lt;li&gt;작은 단위로 쪼개어 옮기고, 실패 시 안전하게 되돌릴 수 있어야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;우리가-얻은-것&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EA%B0%80-%EC%96%BB%EC%9D%80-%EA%B2%83&quot; aria-label=&quot;우리가 얻은 것 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;우리가 얻은 것&lt;/h2&gt;
&lt;p&gt;이관 이후 예상보다 큰 효과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;유지보수성&lt;/strong&gt;&lt;br&gt;
기능별 모듈화 변경 범위가 명확해지고, 영향도 예측이 가능해졌습니다. 또한 반품접수 로직을 쉽게 고칠 수 있어 고객에게 편리한 기능도 쉽게 제공할 수 있게 되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;디버깅 가능, 모니터링 강화&lt;/strong&gt;&lt;br&gt;
Java 전환 이후 로그와 예외 추적이 가능해지면서 장애 원인 파악 속도가 획기적으로 빨라졌습니다.
이 변화는 단순한 기술 개선이 아니라, 우리가 업무 병목을 ‘보이게 만든’ 결정적 진전이었습니다. 그전까지는 반품 접수에서 완료까지 걸리는 리드타임을 정확히 알 수 없었습니다.
어느 구간에서 지연이 발생하는지, 환불이 왜 늦어지는지조차 추적이 불가능했죠. 하지만 이제는 로그를 통해 반품 완료 단계의 오류를 실시간으로 식별할 수 있고,
이를 기반으로 병목 구간을 명확히 찾아내어 개선할 수 있게 되었습니다. 결과적으로 그동안 “언제부턴가 해결이 어려워진 문제들”까지도 이제는 근본 원인을 추적하고, 데이터 기반으로 해결해나갈 수 있는 환경이 만들어졌습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
   &lt;div style=&quot;width: 80%; text-align: center;&quot;&gt;
     &lt;figure style=&quot;margin: 0;&quot;&gt;
       &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/675336c51d3fb87d2f57df33fa7b9c2b/2206a/monitoring.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.750000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABHElEQVR42nWQS0/DMBCE8/9/DCdOSIAEN5CgokJULW3aRn0maeI8HD/ifDgNcEB0rLVH69lZe4P0dMI5R9d1/0bbthRlTZykSNnQ45K2UZrAXbjsnPbn0KjXWG/cOc/dH51frR3CtyLoTOqJL/zt/M1N7NMSzBanY1oHjXC0ejD5eUhvUqYOXbvBUB4eMfGDr133LudkD52vUPtbqu09qoioK3h5Sxi/CqwaNOfd+3zMJIt5g6ocQbSMWM3H7FcjssMEYxSyikk3G9bTkOVsgZaW8JhxNXnmevbEKIzIKomxhs0x524ScvM+5XN3JMhESVVKRJIjaukHK8nzEmss1rrz13rUlUKInFxKsuJEUZckIkMp5TUtymo/FscXWF/Pi7BBcOQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/675336c51d3fb87d2f57df33fa7b9c2b/263a4/monitoring.webp 480w,
/static/675336c51d3fb87d2f57df33fa7b9c2b/a6361/monitoring.webp 960w,
/static/675336c51d3fb87d2f57df33fa7b9c2b/0b34d/monitoring.webp 1920w,
/static/675336c51d3fb87d2f57df33fa7b9c2b/9ad5f/monitoring.webp 2002w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/675336c51d3fb87d2f57df33fa7b9c2b/9aebd/monitoring.png 480w,
/static/675336c51d3fb87d2f57df33fa7b9c2b/a91f8/monitoring.png 960w,
/static/675336c51d3fb87d2f57df33fa7b9c2b/ac7a9/monitoring.png 1920w,
/static/675336c51d3fb87d2f57df33fa7b9c2b/2206a/monitoring.png 2002w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/675336c51d3fb87d2f57df33fa7b9c2b/ac7a9/monitoring.png&quot; alt=&quot;반품접수 모니터링&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
       &lt;figcaption style=&quot;font-size: 0.9em; color: #666; margin-top: 6px;&quot;&gt;
         이제! 모니터링이 가능해졌어요! 고객님의 문제를 빠르게 해결할게요 😉
       &lt;/figcaption&gt;
     &lt;/figure&gt;
   &lt;/div&gt;
 &lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;확장성 확보&lt;/strong&gt;&lt;br&gt;
주문/반품/교환 등 모듈별 기능확장이 자유롭게 가능해졌습니다. 예를들어 반품접수 단계에서 고객에게 환불 예정금액이나, 반품 배송비를 사전에 안내하는 기능도 쉽게 도입이 가능해졌습니다. 이러한 개선 사례는 다음 포스팅에서 자세히 다룰 예정입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 레거시 탈출은 단순한 기술 전환이 아니라, &lt;strong&gt;도메인 중심의 로직 재설계 과정&lt;/strong&gt;이었습니다.
또한, 기술 부채 해결 과정이 고객 신뢰와 비즈니스 성장으로 직결됨을 깨닫게 된 중요한 작업이었습니다.&lt;/p&gt;
&lt;p&gt;앞으로는 고객의 불만이 더 깊어지지않고 신뢰받을 수 있는 올리브영이 되도록 교환/반품 모듈의 개선은 계속될 예정입니다. 저희의 경험이 비슷한 고민을 하는 분들께 도움이 되기를 바랍니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 2. 모듈 페더레이션 PoC]]></title><description><![CDATA[📖 목차 개요 모듈 페더레이션(Module Federation - MF) 넌 뭐니? MF의 특징은 뭐가 있을까? MF의 구현 방법은? MF…]]></description><link>https://oliveyoung.tech/2025-11-06/what-is-MFE-part2/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-06/what-is-MFE-part2/</guid><pubDate>Thu, 06 Nov 2025 15:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;-목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot; 목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📖 목차&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot;&gt;개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%AA%A8%EB%93%88-%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98module-federation---mf-%EB%84%8C-%EB%AD%90%EB%8B%88&quot;&gt;모듈 페더레이션(Module Federation - MF) 넌 뭐니?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mf%EC%9D%98-%ED%8A%B9%EC%A7%95%EC%9D%80-%EB%AD%90%EA%B0%80-%EC%9E%88%EC%9D%84%EA%B9%8C&quot;&gt;MF의 특징은 뭐가 있을까?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mf%EC%9D%98-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95%EC%9D%80&quot;&gt;MF의 구현 방법은?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#mf%EB%A5%BC-%EA%B5%AC%EC%84%B1%ED%95%B4%EB%B3%B4%EA%B3%A0-%EB%8A%90%EB%82%80-%EC%A0%90%EC%9D%80&quot;&gt;MF를 구성해보고 느낀 점은?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot;&gt;참고 자료&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot; aria-label=&quot;개요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개요&lt;/h2&gt;
&lt;p&gt;안녕하세요, 올리브영 내 프론트엔드 개발을 담당하고 있는 &lt;code class=&quot;language-text&quot;&gt;노땅&lt;/code&gt; 문지훈🦊 입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-07-22/what-is-MFE-part1/&quot;&gt;대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 1. 마이크로프론트엔드 너 뭐야?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 2. 모듈 페더레이션 PoC&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-11-10/what-is-MFE-part3/&quot;&gt;대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 3. Nx를 활용한 마이크로프론트엔드&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br /&gt;
&lt;p&gt;Part 1에서는 MFE(Micro Frontend)의 개념을 정리했습니다. 이제 구현으로 시선을 옮겨 보겠습니다.
Part 2의 핵심은 모듈 페더레이션입니다. 레포지토리 전략과 무관하게, 런타임에 모듈을 공유해 MFE를 구현하는 아키텍처 패턴이죠.
이어지는 Part 3에서는 모듈 페더레이션을 더 쉽게 적용하도록 돕는 도구인 Nx를 다룰 예정입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 내부 PoC(Proof of Concept)를 통해 확인한 모듈 페더레이션의 핵심 개념, 장단점, 그리고 구현 방법을 차례로 살펴보겠습니다. 그럼 저와 같이 출발해 보실까요~ 🚀&lt;/p&gt;
&lt;h2 id=&quot;모듈-페더레이션module-federation---mf-넌-뭐니&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%93%88-%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98module-federation---mf-%EB%84%8C-%EB%AD%90%EB%8B%88&quot; aria-label=&quot;모듈 페더레이션module federation   mf 넌 뭐니 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모듈 페더레이션(Module Federation - MF) 넌 뭐니?&lt;/h2&gt;
&lt;center&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 448px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/572d0273087c7edf083fcf996cc6247d/8115b/image1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 101.33928571428572%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEX0lEQVR42lWU6VdTRxiH85f29IuitZHFIi1L2aWpRPTYqsfluFZ7qq2oIIIQEoVQEggIBLIQAoVoWEL25eYSn75zb6inH+bcmXfuPPPOu/wsZf0zmoxSuUJRq1AoVciXjsgV1TDXap4tHFXnsi/rQtFcqzPqrFb+jGJZSroYq7D88WEZGQFk8jpp+Xp2MkxtpUjllc2058Ser15YLH2BWhQsrx0ZsGNQKqcOVNhIFJkI7/F2IcLoXAhHaI/AXp687CWzuuG14bkBPjKcsiiyLq5miuYzDQ/UDzJfWI9jd65R99xL3TM3tskAnkCMQk6BIC3ffBWqi3easCzxjIbvYxpN+8xupowjckB4J8FDd5DakRVqnBGaBmdo/muKUzK3jvi5/T5AeDuBY/2AnaSGLmcXYhm2k0UsiVwZ/26WYXmOZ/OA574ozWN+Tk2EqXf4afvNzYB9hSv2VToezdAwscJp2WsaW+GPuQhz0QOGAnv44zkS2TIWldGy3LC2X2TQvYL10QT1b1dpf+HDdmWaH6yjXGzfwN65ScvZMWyXpugc9Bn/WB87eepaZHW3QLGafUu6qEsidOYkk7emQ3w7tEjvg3H6O+a4/lOS/u5NTn79kBNf3efnjnVu2FLYOxbouzuO9fUi1+X57mjSiGdW4m9RyUhJGUQOijyYCXPCEaTnoZdfumN0fD9H/ekh7F0RBnq2aJB52/kZ2Ytju+elZjLIrakgYfEwlVVASYqqMxVHvQxPZtc5MR6g4947fu2N0du6TGeTl0vdURmbtJ+fpaflA9cu7NJ95x2n5PI7AtSLkMzoVQ8FeCDAyhH87lHAoADf01Y7Sd03w/R3Rrjcu82AAO2dGzScGab1rIuu267/gEclOPwC1I0nr0rB3p8JyZNDxpNv9h3SWDtO3elX9LX6ZawY83PWEdnLYLvr5eRkiFvvgyzFstUnq6QUVHvpLHzMcHM6yJmX8wJ0cP1imAtNy/S2rNL63RTNDS56mpfpOb/Ejf4offcdWF/Nc80VwLuVNoCqKSyq3UrSdku7eV64/dQ9nqR2fI2WoQV+tI8w0LXK1d5drvR84nKXxPfiG9pe+qiTWJ994uRP1wfmd3IUpR0LMiz7WY0P4t1rKezZrUMGF7doG12mZjREvTNE+zMPtv6/sdncdD71cM4ZpGYsRMuoKuwN5jeTvAnt49tOE09pZustfsoYxb0nrTch7RT9lObpbJhzw0ucdGzQ+NIj7eeWmEWoH17mkYRmK5bGEU4QO9SoaLAmnbKTLGEpaKbsqMSooKqsJ/Om+8vRfa6+C9I46KFRxOGyxMsXjos4VKSQRZ1yZu2polZaoCm1UcBc6YsGJkW6DqWMVIDXpdgn1xM4lzZxLGzgkvlavEBOIKruUlnTCSV1x9poyFehKq7ZKjidN0spI3AF9oqSTP+TFrtcmlN6aWZUeadgSg8NkVUeHsu/gppgE2qCTa+VOufyZp0pW7Zq+59iV0P3L4+3bsyOkX+HAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/572d0273087c7edf083fcf996cc6247d/e8dab/image1.webp 448w&quot; sizes=&quot;(max-width: 448px) 100vw, 448px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/572d0273087c7edf083fcf996cc6247d/8115b/image1.png 448w&quot; sizes=&quot;(max-width: 448px) 100vw, 448px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/572d0273087c7edf083fcf996cc6247d/8115b/image1.png&quot; alt=&quot;MF 로고&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;MF 로고&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;MF는 JavaScript 애플리케이션의 분산화를 위한 아키텍처 패턴입니다. (서버 측의 마이크로서비스와 유사) 이를 통해 여러 JavaScript 애플리케이션(또는 마이크로 프론트엔드) 간에 코드와 리소스를 공유할 수 있습니다.&lt;/p&gt;
&lt;p&gt;MF를 도입해서 얻는 이점은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;코드 중복 감소&lt;/li&gt;
&lt;li&gt;코드 유지 관리성 개선&lt;/li&gt;
&lt;li&gt;애플리케이션의 전체 크기 감소&lt;/li&gt;
&lt;li&gt;애플리케이션 성능 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;&lt;strong&gt;간단히 런타임에 여러 코드 및 리소스를 병합하는 기술이라고 이해하면 됩니다.&lt;/strong&gt; &lt;em&gt;쉽죠잉?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A 애플리케이션 모듈을 수정한다고 가정해봅시다.
만약 빌드 타임에 통합하면, A를 사용하던 B, C 애플리케이션도 다시 빌드/배포해야 합니다. 그러면 마이크로프론트엔드의 장점인 독립적 배포가 어려워지죠.
반대로 런타임에 통합하면, B, C 애플리케이션에서 별도 배포 없이 A 애플리케이션의 변경 사항을 실시간 반영할 수 있습니다. 다시 말해 런타임에 각 서비스를 개별 배포하고, 동적으로 로딩할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;자주-사용되는-개념&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-%EA%B0%9C%EB%85%90&quot; aria-label=&quot;자주 사용되는 개념 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;자주 사용되는 개념&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;호스트(Host) 애플리케이션:&lt;/strong&gt; 다른 마이크로 프론트엔드를 로드하여 통합하는 주 애플리케이션입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리모트(Remote) 애플리케이션:&lt;/strong&gt; 자신의 모듈을 외부에 노출하여 다른 애플리케이션에서 이를 사용할 수 있도록 하는 독립적인 마이크로 프론트엔드입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모듈:&lt;/strong&gt; Webpack 또는 Vite로 번들링 가능한 리소스입니다. (JS, CSS, HTML, JSON, Asset ..)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local:&lt;/strong&gt; 현재 애플리케이션 내 단일 빌드에 포함되는 일반적인 모듈입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expose:&lt;/strong&gt; 원격 모듈로 노출할 부분을 지정합니다. (expose하면 Host가 원격 모듈의 코드를 사용할 수 있게 됩니다.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container:&lt;/strong&gt; 다른 애플리케이션에서 로드 가능한 단위입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared Dependencies:&lt;/strong&gt; 여러 애플리케이션(Host, Remote) 간에 공유할 의존성(라이브러리)입니다. MF는 설정된 공유 의존성을 버전까지 고려하여 중복 로드를 방지하고 효율적으로 관리해 줍니다. 예를 들어, Host와 Remote가 모두 React v18을 사용한다면, 브라우저에는 React v18 코드가 한 번만 로드됩니다. &lt;strong&gt;(Singleton 개념)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h3 id=&quot;언제-사용할까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C&quot; aria-label=&quot;언제 사용할까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;언제 사용할까?&lt;/h3&gt;
&lt;p&gt;MF는 다음 시나리오에 적합합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;대규모 애플리케이션:&lt;/strong&gt; 대규모 애플리케이션의 경우 애플리케이션을 여러 개의 마이크로 프론트엔드로 나누고 MF를 사용하여 이들 간에 코드와 리소스를 공유할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;마이크로 프론트엔드 아키텍처:&lt;/strong&gt; MF는 마이크로 프론트엔드 아키텍처를 구축하는 데 이상적인 도구입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;큰 규모의 개발팀:&lt;/strong&gt; MF는 여러 팀이 대규모 애플리케이션을 공동으로 개발하는 데 도움을 줄 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;MAU 800-900만 명을 돌파하며 성장 중인 올리브영은 거대한 트래픽과 수많은 기능이 업데이트되는 환경인만큼, 기존의 모놀리식 프론트엔드 구조는 배포 병목, 코드베이스 복잡성 등 개발 속도를 저해하는 한계에 봉착했습니다.&lt;/p&gt;
&lt;p&gt;이러한 문제 해결과 올리브영의 넥스트 비전 달성을 위해, 저희는 MF를 어떠한 구성 방식으로 사용하면 좋을 지 검토하였고, 이 구조적 문제를 해결할 핵심 기술인 MF의 Next.js 기반 PoC 과정과 결과를 이번에 공유하게 되었습니다.&lt;/p&gt;
&lt;h3 id=&quot;이를-통해-해결하고자-하는-문제가-뭐야&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EB%A5%BC-%ED%86%B5%ED%95%B4-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B3%A0%EC%9E%90-%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C%EA%B0%80-%EB%AD%90%EC%95%BC&quot; aria-label=&quot;이를 통해 해결하고자 하는 문제가 뭐야 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;이를 통해 해결하고자 하는 문제가 뭐야?&lt;/h3&gt;
&lt;p&gt;프로젝트 규모가 커지고 복잡해짐에 따라 다양한 문제에 직면하게 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;중복 개발:&lt;/strong&gt; 프로젝트가 커질수록 비슷한 기능이나 컴포넌트가 여러 부분에서 중복 구현되는 경우가 많아집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;업무 결합도 증가:&lt;/strong&gt; 프로젝트가 커질수록 하나의 팀이 아닌 다양한 팀이 하나의 코드베이스를 공유하게 되어 의존성 및 복잡성이 증가합니다. 또 팀 간 협업 과정에서 충돌과 병목현상이 발생합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;응집도 낮음:&lt;/strong&gt; 다양한 기능이 하나의 애플리케이션에 있다보니 응집도가 떨어집니다. 특정 기능을 수정하거나 확장할 때 전체 시스템에 대한 이해가 필요하여 코드 품질 관리가 어려워집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장애파급범위:&lt;/strong&gt; 시스템의 한 부분에서 발생한 문제가 전체 애플리케이션에 영향을 미칠 수 있습니다. 또한 문제 발생 시 근본 원인을 찾아 해결하는데 비교적 많은 시간과 노력이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배포시간증가:&lt;/strong&gt; 전체 애플리케이션을 한 번에 빌드하고 배포해야 하므로 시간이 오래 걸립니다. 작은 변경사항에도 전체 애플리케이션을 다시 배포해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;이런 문제들을 해결하고자 하는 니즈는 누구나 가지고 있을 것입니다. 그 문제를 해결하는 여러 방법 중 하나가 &lt;strong&gt;MF&lt;/strong&gt;라고 생각하면 될 것 같아요.&lt;/p&gt;
&lt;h4 id=&quot;마이크로-프론트엔드에서-말하는-각-통합-방식-추천-환경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-%EB%A7%90%ED%95%98%EB%8A%94-%EA%B0%81-%ED%86%B5%ED%95%A9-%EB%B0%A9%EC%8B%9D-%EC%B6%94%EC%B2%9C-%ED%99%98%EA%B2%BD&quot; aria-label=&quot;마이크로 프론트엔드에서 말하는 각 통합 방식 추천 환경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마이크로 프론트엔드에서 말하는 각 통합 방식 추천 환경&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;빌드 통합 방식 추천 상황
&lt;ul&gt;
&lt;li&gt;소규모 팀으로 단일 레포지토리에서 개발하는 경우&lt;/li&gt;
&lt;li&gt;성능과 SEO가 매우 중요한 경우&lt;/li&gt;
&lt;li&gt;기술 스택이 통일된 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;런타임 통합 방식 추천 상황
&lt;ul&gt;
&lt;li&gt;여러 팀이 독립적으로 개발하는 경우&lt;/li&gt;
&lt;li&gt;독립적인 배포와 확장성이 중요한 경우&lt;/li&gt;
&lt;li&gt;다양한 기술 스택을 사용해야 하는 경우&lt;/li&gt;
&lt;li&gt;점진적인 마이그레이션이 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;mf의-특징은-뭐가-있을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mf%EC%9D%98-%ED%8A%B9%EC%A7%95%EC%9D%80-%EB%AD%90%EA%B0%80-%EC%9E%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;mf의 특징은 뭐가 있을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MF의 특징은 뭐가 있을까?&lt;/h2&gt;
&lt;center&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e5bf3554e8421049c6e02768d142e73b/1e185/image2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAADoElEQVR42iWT2VNTVwCH8y/1qS99aTtTbYsFBbKwZb9b7s3NvTcxhOgIkYEWWqwRQbHTqdMZKqNUZF9SWQIIhMpSiAiCSJlO+1d8PbYP5+U8fOe3HZec/Qspc47UfIacfoeSfoucPKat64zb9865dfeMW3dO6eo5obvnmN6+Ezo7D7AS2ySM3zGUEoa0Tjz6gnhkBZeSPcds/RPrxjvi2VO0q8fEUm/Qk4fYza9Jpl/hJPdJJvdIObtcTe0S11+SiG1iaiUSyjoJ+QUJaQUzXMQVaznDaXtHsvUUs+UY1TlEcw7QrFco5j5RAYsKkGRvEzVfoimb/6uShSpJqIquCmXLJMJLWOFFXFrzWxRhUbYPUa3XAnRAzNpHN8oE7D0+GijjfXSAe7DMh9+tE1JXMaUNDGHRFBbNUBEzuEgisIAVnBcKhUUteYTknCA7xyjWEUpcZBQvCfg6n94s8mVHkc/b5/mkZRJVWsCRlgRwGTW0hhFawQgskfD/hu0vCKCA6VaZXuUR16JzpPV5Wu1xTHsDy1rD0VdERkXi8jK2uoQtL+EoBe7q9+n33yfpGyPVMInVWMBpnBWWRfjv2yrXVnEaruao5mPy3m465Qna/cPkYkMCtkp3ZIj+UC9Wwyyx4CqDnhx/N17gTaiWhYoqTO84yYZpXLoIP25sMVjjMFJvMXQlwLWGAYoek/NgJceeCvqqc4y4m/nHX8lR42UeV2h0evOM1obEvcaPFyUS3jGS9RO4DPMPUcAOUb2MpOwgqTuE7VXyIVU84OeXhgrar3zD9+4OhoPVDHt9fOsOEZFn0OoKxDwzGL457PcKvaO44vFtjNhLDE2MVC0Je+uivRJNziQxdRQ1NkI2skBzpIAafYIaeUxYfSoym8aqn8T2TWB7xnDcz0i5RwRQjFTXNtGVjf+29X4OenSFTOMy96smeFA1yRNnkSFjnr6LY9y5OEqmdoaETxQhbNqeUWwBst2/kqodxpUQquzoGlZE7Cq4jBUo4gQWsQPzZIMFsqE5rgfnyDZNc61pkhaRk+0bJ1UzQVo8mKocJXVphIw46csCKIm91We38Gc2ibVuEWzZxJcuIatFsoECOQHMRWbp0p/THpqhrXGCjHuciPMz9e330HIPUW/+RF2uF73pIa66VIkv+stU9+5SN7BPRX5P/IhdauOLdHjGeRCdYkCZ4gd5mrznGbeFteuXnvLVnR4+e36DK9Nfc2EqxwezNn49z7+w9dfZ0uYW0AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e5bf3554e8421049c6e02768d142e73b/263a4/image2.webp 480w,
/static/e5bf3554e8421049c6e02768d142e73b/a6361/image2.webp 960w,
/static/e5bf3554e8421049c6e02768d142e73b/0b34d/image2.webp 1920w,
/static/e5bf3554e8421049c6e02768d142e73b/58bdf/image2.webp 2340w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e5bf3554e8421049c6e02768d142e73b/9aebd/image2.png 480w,
/static/e5bf3554e8421049c6e02768d142e73b/a91f8/image2.png 960w,
/static/e5bf3554e8421049c6e02768d142e73b/ac7a9/image2.png 1920w,
/static/e5bf3554e8421049c6e02768d142e73b/1e185/image2.png 2340w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e5bf3554e8421049c6e02768d142e73b/ac7a9/image2.png&quot; alt=&quot;MF 런타임 통합&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;MF 런타임 통합&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;h3 id=&quot;모놀리식과-차이점은&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%86%80%EB%A6%AC%EC%8B%9D%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90%EC%9D%80&quot; aria-label=&quot;모놀리식과 차이점은 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모놀리식과 차이점은?&lt;/h3&gt;
&lt;p&gt;일반적인 모놀리식과의 차이를 비교해봤습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 5px;&quot;&gt;특징&lt;/th&gt;
    &lt;th style=&quot;padding: 5px;&quot;&gt;모놀리식 프론트엔드&lt;/th&gt;
    &lt;th style=&quot;padding: 5px;&quot;&gt;마이크로 프론트엔드&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;코드베이스&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;단일 거대 코드베이스&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;여러 개의 작은 코드베이스&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;개발&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;모든 팀이 동일 코드베이스 작업&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;팀별 독립적인 코드베이스 작업&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;배포&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;전체 애플리케이션 단위 배포&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;마이크로 앱 단위 독립 배포 가능&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;기술 스택&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;단일 기술 스택 (변경 어려움)&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;기술 스택 다양성 가능 (점진적 변경 용이)&lt;/td&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;팀 구조&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;기능별 팀 또는 거대 단일 팀&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;기능 중심 자율적 팀&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;결합도&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;높음 (변경 영향 범위 큼)&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;낮음 (변경 영향 범위 제한적)&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;복잡성&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;초기에는 낮으나, 규모 증가 시 급증&lt;/td&gt;
    &lt;td style=&quot;padding: 5px; border-bottom: 1px solid #ddd;&quot;&gt;초기 설정 복잡성 높음, 개별 앱은 단순&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;h3 id=&quot;mf를-사용하면-얻을수-있는-이점과-발생할-수-있는-이슈는&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mf%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%96%BB%EC%9D%84%EC%88%98-%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%A0%90%EA%B3%BC-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%8A%88%EB%8A%94&quot; aria-label=&quot;mf를 사용하면 얻을수 있는 이점과 발생할 수 있는 이슈는 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MF를 사용하면 얻을수 있는 이점과 발생할 수 있는 이슈는?&lt;/h3&gt;
&lt;h4 id=&quot;이점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EC%A0%90&quot; aria-label=&quot;이점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;이점&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;개발 속도 향상 -&lt;/strong&gt; 여러 팀이 동시에 독립적으로 작업할 수 있어 전체 개발 속도가 빨라집니다. 다른 팀의 작업을 기다릴 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유지보수성 향상 -&lt;/strong&gt; 코드베이스가 작고 집중되어 있어 이해하기 쉽고 디버깅하기도 쉬워집니다. 거대한 코드베이스에서 버그 찾는 것보다 훨씬 효율적입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;확장성 개선 -&lt;/strong&gt; 새로운 기능이나 팀을 쉽게 추가할 수 있습니다. 기존 코드를 건드리지 않고 새 마이크로 프론트엔드를 추가하면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기술 부채 관리 -&lt;/strong&gt; 전체 애플리케이션을 한 번에 리팩토링할 필요 없이 한 번에 하나의 마이크로 프론트엔드를 현대화할 수 있습니다. &quot;이번 분기에는 검색 기능만 React 18로 업그레이드하자!&quot; 같은 접근이 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장애 격리 -&lt;/strong&gt; 한 마이크로 프론트엔드의 문제가 전체 애플리케이션에 영향을 미치지 않습니다. 결제 시스템에 버그가 있어도 사용자는 여전히 콘텐츠를 탐색할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;점진적 업그레이드 -&lt;/strong&gt; 레거시 애플리케이션을 한 번에 하나씩 마이크로 프론트엔드로 전환할 수 있습니다. 빅뱅 방식의 리팩토링보다 훨씬 안전합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h4 id=&quot;발생할-수-있는-이슈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%9D%B4%EC%8A%88&quot; aria-label=&quot;발생할 수 있는 이슈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;발생할 수 있는 이슈&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. 성능 관련 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;초기 로딩 시간 증가
&lt;ul&gt;
&lt;li&gt;여러 마이크로 프론트엔드를 별도로 로드해야 하므로 초기 로딩 시간이 길어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;여러 자바스크립트 번들을 다운로드하는 과정에서 네트워크 요청이 증가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;런타임 오버헤드
&lt;ul&gt;
&lt;li&gt;여러 애플리케이션이 동시에 실행되면서 메모리 사용량이 증가할 수 있습니다.&lt;/li&gt;
&lt;li&gt;브라우저 리소스 경쟁으로 인한 성능 저하가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 의존성 관리 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;공유 라이브러리 충돌
&lt;ul&gt;
&lt;li&gt;여러 마이크로 프론트엔드가 다른 버전의 동일한 라이브러리를 사용할 경우 충돌이 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;예를 들어, 한 마이크로 프론트엔드는 React 18을 사용하고 다른 하나는 React 17을 사용하면 문제가 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전역 상태 관리 복잡성
&lt;ul&gt;
&lt;li&gt;여러 마이크로 프론트엔드 간 상태 공유가 복잡해집니다.&lt;/li&gt;
&lt;li&gt;Redux, Context API 등을 통한 상태 관리가 어려워질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. 스타일 관련 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSS 충돌
&lt;ul&gt;
&lt;li&gt;글로벌 CSS가 다른 마이크로 프론트엔드의 스타일을 덮어쓸 수 있습니다.&lt;/li&gt;
&lt;li&gt;클래스 이름 충돌이 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일관된 디자인 시스템 유지 어려움
&lt;ul&gt;
&lt;li&gt;여러 팀이 독립적으로 개발할 경우 일관된 UI/UX를 유지하기 어려울 수 있습니다.&lt;/li&gt;
&lt;li&gt;테마 관리, 다크 모드 등의 전역 스타일 적용이 복잡해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. 라우팅 관련 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;라우팅 충돌
&lt;ul&gt;
&lt;li&gt;각 마이크로 프론트엔드가 자체 라우터를 가질 경우 라우팅 충돌이 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;브라우저 히스토리 관리가 복잡해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;딥 링킹 문제
&lt;ul&gt;
&lt;li&gt;특정 상태나 페이지로 직접 이동하는 딥 링크 구현이 어려울 수 있습니다.&lt;/li&gt;
&lt;li&gt;브라우저 새로고침 시 상태가 유지되지 않을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. 통신 관련 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컴포넌트 간 통신 복잡성
&lt;ul&gt;
&lt;li&gt;마이크로 프론트엔드 간 이벤트 기반 통신이 복잡해질 수 있습니다.&lt;/li&gt;
&lt;li&gt;타입 안전성 보장이 어려울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;API 중복 호출
&lt;ul&gt;
&lt;li&gt;여러 마이크로 프론트엔드가 동일한 API를 중복 호출할 가능성이 있습니다.&lt;/li&gt;
&lt;li&gt;캐싱 전략이 복잡해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;6. 개발 및 배포 복잡성&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;환경 구성 복잡성
&lt;ul&gt;
&lt;li&gt;개발, 테스트, 스테이징, 프로덕션 환경 구성이 복잡해집니다.&lt;/li&gt;
&lt;li&gt;로컬 개발 환경 설정이 어려울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배포 파이프라인 복잡성
&lt;ul&gt;
&lt;li&gt;각 마이크로 프론트엔드의 배포 버전 관리가 복잡해집니다.&lt;/li&gt;
&lt;li&gt;롤백 전략 구현이 어려울 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;7. 테스트 관련 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;통합 테스트 어려움
&lt;ul&gt;
&lt;li&gt;마이크로 프론트엔드 간 통합 테스트가 어려워집니다.&lt;/li&gt;
&lt;li&gt;E2E 테스트 구성이 복잡해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모킹 복잡성
&lt;ul&gt;
&lt;li&gt;다른 마이크로 프론트엔드의 기능을 모킹하는 것이 복잡해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;8. SEO. 및 접근성 이슈&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SEO 최적화 어려움
&lt;ul&gt;
&lt;li&gt;클라이언트 사이드에서 동적으로 로드되는 마이크로 프론트엔드는 SEO 최적화가 어려울 수 있습니다.&lt;/li&gt;
&lt;li&gt;메타 태그 관리가 복잡해질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;접근성(A11y) 관리
&lt;ul&gt;
&lt;li&gt;여러 팀이 개발할 경우 일관된 접근성 표준을 유지하기 어려울 수 있습니다.&lt;/li&gt;
&lt;li&gt;스크린 리더와 같은 보조 기술과의 호환성 문제가 발생할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;MF를 구성하다보면 위와 같은 이슈들이 발생할 수 있기 때문에 MF를 구성하기 전 고려해야 할 부분들을 확실하게 고민해보고 설계하고 선택하면 좋을 것 같습니다.
&lt;br /&gt;&lt;/p&gt;
&lt;h4 id=&quot;고려해야-할-부분들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%A0%EB%A0%A4%ED%95%B4%EC%95%BC-%ED%95%A0-%EB%B6%80%EB%B6%84%EB%93%A4&quot; aria-label=&quot;고려해야 할 부분들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;고려해야 할 부분들&lt;/h4&gt;
&lt;p&gt;런타임 통합 방식의 마이크로 프론트엔드는 많은 이점을 제공하지만, 다양한 기술적 이슈가 발생할 수 있습니다. &lt;a href=&quot;https://oliveyoung.tech/2025-11-10/what-is-MFE-part3/&quot;&gt;대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 3. Nx를 활용한 마이크로프론트엔드&lt;/a&gt; 에서 공유드릴 내용이기도 하지만 Nx를 활용하면 이러한 이슈들을 효과적으로 해결하면서 마이크로 프론트엔드의 장점을 활용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;해결책을 구현할 때는 다음과 같은 방법을 고려하세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;설계 단계부터 고려 -&lt;/strong&gt; 마이크로 프론트엔드 경계, 통신 방식, 공유 리소스 등을 미리 설계&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;팀 간 표준화 -&lt;/strong&gt; 코딩 표준, 컴포넌트 설계 원칙, 스타일 가이드 등 공유&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;점진적 도입 -&lt;/strong&gt; 한 번에 모든 것을 마이크로 프론트엔드로 전환하기보다 점진적으로 도입&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모니터링 및 성능 최적화 -&lt;/strong&gt; 지속적인 모니터링과 성능 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;만약 Next.js를 사용 중이라면 놓치지 말아야 할 이슈가 있습니다. 현재 Page Router만 지원하며, 2026년에는 deprecated 처리될 예정입니다. &lt;a href=&quot;https://github.com/module-federation/core/issues/3153&quot;&gt;등록된 이슈&lt;/a&gt;를 참고해 구현하세요. 😭&lt;/p&gt;
&lt;p&gt;이러한 접근 방식을 통해 런타임 통합 방식의 마이크로 프론트엔드를 성공적으로 구현하고 유지할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;mf의-구현-방법은&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mf%EC%9D%98-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95%EC%9D%80&quot; aria-label=&quot;mf의 구현 방법은 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MF의 구현 방법은?&lt;/h2&gt;
&lt;p&gt;예제로 올리브영 웹에 Next.js 프레임워크의 두 가지 환경에서 모듈 페더레이션을 구현해보았습니다. &lt;strong&gt;디플롯&lt;/strong&gt;이라는 서비스에서 응답되는 상품에 대한 데이터를 &lt;strong&gt;올리브영 상품카드 컴포넌트&lt;/strong&gt;를 리모트로 가져와서 병합 후 데이터를 주입하여 사용하는 케이스 예시입니다.&lt;/p&gt;
&lt;p&gt;모든 코드는 모듈 페더레이션 GitHub을 참고해 테스트용으로 구성했습니다. (&lt;a href=&quot;https://github.com/module-federation/module-federation-examples/blob/master/nextjs-ssr/home/next.config.js&quot;&gt;모듈페더레이션.io 예제 github&lt;/a&gt;)&lt;/p&gt;
&lt;h3 id=&quot;webpack-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#webpack-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;webpack 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Webpack 설정&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;적용 시나리오&lt;/strong&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3e4a459ec545c56c44d83ba9e2d9910d/4e069/image3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAADbklEQVR42m2SbWxTZRiGC4lR+YMzRhJ++JGoWfArRmPDosAATZiBqBMD0jEzqzBkThR0CNHERJgFGWRhUYRuWEKGY7Qr0HUp68fWMSjd3BKTxYg6x7r1nHYbpV17Tk+7y3MOxM3EN7nfjyfnvs77Ps9jkLMKkqyg5BVEUaIvlKI/nCLgi+LvHKfbJ+DzRPS12xsl4BkjqO6vBSfp60kwNSmj5G4zNJZBm9IZBcjR7kywxniD0uUR3l4RZdNKgV3lMb54b4K95glqTHHMK0TeX65JoMIYJeRL6d6M9D9Atz3B1tcFvqme5OsP43y6XsRcHFXNUSqLBSqXCWwpEti2VKSqKEbl8yIhzx1gZg5Qo2vBgDtBTZlIbbXA/iqRr8pFdrwm8EmJwE5Vu0pEalTtVrWnJMbuV0QGgreB0twb6jnM5bQ4wnCKwSt/MRXJIk1AejLP9B1lVMlTM//RjGbL52ZzmJGzMAPe4SHMl2xUuK184PmJ6mAL2wMt7Om9yL6wh33XOtnb28E2fxvb/eepCrj4ONDOFs9FQpFRnaGxDNOShDYsV90YLGbubvic+fWfYTi8k3savmSx1cLDTXUsOnGIgmMHuffoAeYdsWCoszD/0EEMtbVYBwd0xrQka0BZPxzu6+Lxk9+xwf0zq8418VhTPcWtNt7pcLDOeZbCk1aMZ05T4fFgcnfwlK2ZhQ1W7jpynFO/DumMtJSdBX57xU/B9xaW2W08YzvG/fV1FDYeZ7W9hRebm3mw4Uce+qGJl8/YKWp2sOjoKeYdaMSw/wSNA0OzN0zLsv5+1/XfeKOthXJPO5u7/GzweXm3p4e1LQ7eamunzBvE5L3MZn+IMn8Yky/MRleQUoeX4MjYbA6zWa3CeRQlp/8lHxWQzzuRLjhRXE6mHK2kO93kg15ynS4Utx2lQ5XHzszNCd2jebUK61VOq0WJxeOkEkmta5g8d5bfHyngz6cX88eTDzC6cgmRN4sYLTUysupR/jYuZOSl+xhZuoBEdwdawqSMNAu8lcxwfXicUVHQgTcvtHJjdSHC1nWMr3+BsdJnET9aS3TTc0Q3LiG+41UE0xOMrVlA8uol1KZDluYApUyOW8ks8VQSWVGbNyaqH3aR6u8lFe4mqSr1y2VSferar8YH1X1/QD17kdQny2q65Gz2X+A/ftt/8+m/aV0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3e4a459ec545c56c44d83ba9e2d9910d/263a4/image3.webp 480w,
/static/3e4a459ec545c56c44d83ba9e2d9910d/a6361/image3.webp 960w,
/static/3e4a459ec545c56c44d83ba9e2d9910d/0b34d/image3.webp 1920w,
/static/3e4a459ec545c56c44d83ba9e2d9910d/6beb8/image3.webp 2324w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3e4a459ec545c56c44d83ba9e2d9910d/9aebd/image3.png 480w,
/static/3e4a459ec545c56c44d83ba9e2d9910d/a91f8/image3.png 960w,
/static/3e4a459ec545c56c44d83ba9e2d9910d/ac7a9/image3.png 1920w,
/static/3e4a459ec545c56c44d83ba9e2d9910d/4e069/image3.png 2324w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3e4a459ec545c56c44d83ba9e2d9910d/ac7a9/image3.png&quot; alt=&quot;MF 아키텍쳐&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;MF 아키텍쳐&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;호스트와 리모트의 개념을 아는게 중요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;최상위 Host APP:&lt;/strong&gt; 라우팅 또는 상태를 관리하는 컨테이너가 최상단 Host 역할&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HomeModule APP:&lt;/strong&gt; 최상위 Host APP의 Remote 역할과 CommonUI의 Host 역할&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CommonUI:&lt;/strong&gt; HomeModule APP에서 사용하는 Remote App 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;HomeModule은 CommonUI에서 제공하는 ProductCard 컴포넌트를 활용하여 페이지를 구성하고 최상위 Host에서 해당 HomeModule을 활용하여 페이지를 구성합니다.&lt;/p&gt;
&lt;h4 id=&quot;host&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#host&quot; aria-label=&quot;host permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Host&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; NextFederationPlugin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@module-federation/nextjs-mf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;webpack&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rules&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;videoFileLoaderOptions&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; isServer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getRemotes&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; location &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; isServer &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ssr&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chunks&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      HomeModule&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;HomeModule@http://localhost:3001/_next/static/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;location&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/remoteEntry.js&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;plugins&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NextFederationPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;host&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      remotes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRemotes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      filename&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;static/chunks/remoteEntry.js&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      exposes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      extraOptions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        debug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        automaticAsyncBoundary&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enableImageLoaderFix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enableUrlLoaderFix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      shared&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// react-query의 의존성을 같이 공유해서 싱글톤 객체로 사용한다. 버전이 달라서 생기는 문제를 해소할 수 있다. (완전해소 아님)&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tanstack/react-query&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          singleton&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          requiredVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tanstack/core&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          singleton&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          requiredVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;remote&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#remote&quot; aria-label=&quot;remote permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Remote&lt;/h4&gt;
&lt;p&gt;HomeModule (CommonUI APP의 호스트이자 Host APP의 리모트)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; NextFederationPlugin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@module-federation/nextjs-mf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;webpack&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; plugins &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;plugins&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; isServer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getRemotes&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; location &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; isServer &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ssr&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chunks&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      host&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;host@http://localhost:3000/_next/static/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;location&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/remoteEntry.js&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	    CommonUI&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;CommonUI@http://localhost:3002/_next/static/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;location&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/remoteEntry.js&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  plugins&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NextFederationPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;HomeModule&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      filename&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;static/chunks/remoteEntry.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      exposes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&apos;./home&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/pages/home-module&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      remotes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRemotes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      extraOptions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        debug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        exposePages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        automaticAsyncBoundary&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enableImageLoaderFix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enableUrlLoaderFix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      shared&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// react-query의 의존성을 같이 공유해서 싱글톤 객체로 사용한다. 버전이 달라서 생기는 문제를 해소할 수 있다. (완전해소 아님)&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tanstack/react-query&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          singleton&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          requiredVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tanstack/core&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          singleton&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          requiredVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;공통-컴포넌트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%B5%ED%86%B5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8&quot; aria-label=&quot;공통 컴포넌트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;공통 컴포넌트&lt;/h4&gt;
&lt;p&gt;CommonUI&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; NextFederationPlugin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@module-federation/nextjs-mf&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;webpack&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; plugins &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;plugins&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; isServer &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; options&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getRemotes&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; location &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; isServer &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ssr&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;chunks&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      HomeModule&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;HomeModule@http://localhost:3001/_next/static/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;location&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/remoteEntry.js&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  plugins&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NextFederationPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;CommonUI&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      filename&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;static/chunks/remoteEntry.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      exposes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;./ProductCard&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./src/ProductCard/index.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;./ProductCards&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./src/ProductCards/index.tsx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      remotes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRemotes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      extraOptions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        debug&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        exposePages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        automaticAsyncBoundary&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enableImageLoaderFix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        enableUrlLoaderFix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      shared&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// react-query의 의존성을 같이 공유해서 싱글톤 객체로 사용한다. 버전이 달라서 생기는 문제를 해소할 수 있다. (완전해소 아님)&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tanstack/react-query&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          singleton&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          requiredVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;@tanstack/core&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          singleton&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          requiredVersion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;csr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#csr&quot; aria-label=&quot;csr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSR&lt;/h3&gt;
&lt;h4 id=&quot;적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;적용&lt;/h4&gt;
&lt;p&gt;첫 번째는 CSR 환경입니다. react 환경에서 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;HomeModule - CommonUI에 있는 ProductCard와 ProductCards를 런타임에 병합하여 가져옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lazy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Suspense &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Layout &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@common/components&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ProductCard &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CommonUI/ProductCard&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ProductCards &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CommonUI/ProductCards&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;로딩 중...&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;length&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productProps&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Anchor&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;productTargetUrl&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductCards&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;TYPE_3&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductCard&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;productProps&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductList&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Anchor&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          )
        }
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  );
};

export default Page;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Host&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lazy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Suspense &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Layout &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@common/components&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; HomeModule &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;HomeModule/home&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;로딩 중...&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;HomeModule&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Page&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과&lt;/h4&gt;
&lt;center&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;video style=&quot;max-width: 500px; aspect-ratio: 2 / 3; width: 90%;&quot; controls playsinline preload&gt;
      &lt;source src=&quot;/a45446862ce561ab4fa549cb99807c64/csr.mp4&quot; type=&quot;video/mp4&quot; /&gt;
    &lt;/video&gt;
    &lt;figcaption&gt;MF CSR 결과 - 올리브영 앱에서 특정 페이지 일부를 MF 적용한 모습&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;CommonUI의 상품 카드 컴포넌트를 리모트로 가져와서 HomeModule 컴포넌트에 일부분에 적용하였습니다. CSR일 경우 Nextjs에서 응답한 HTML에 해당 컴포넌트에 대한 HTML이 첨부되지 않은 채로 응답됩니다.&lt;/p&gt;
&lt;h3 id=&quot;ssr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ssr&quot; aria-label=&quot;ssr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;SSR&lt;/h3&gt;
&lt;h4 id=&quot;적용-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%81%EC%9A%A9-1&quot; aria-label=&quot;적용 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;적용&lt;/h4&gt;
&lt;p&gt;두 번째는 SSR 환경입니다. 예제 코드에서 보시다시피 nextjs 환경에 영향을 받습니다.&lt;/p&gt;
&lt;p&gt;HomeModule - CommonUI에 있는 ProductCard와 ProductCards를 런타임에 병합하여 가져옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; lazy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Suspense &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; dehydrate&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; QueryClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; useQuery &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@tanstack/react-query&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Layout &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@common/components&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ProductCard &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CommonUI/ProductCard&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ProductCards &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; window &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;undefined&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lazy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CommonUI/ProductCards&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 예시&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;queryOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    queryKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;products&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    queryFn
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;fallback&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;로딩 중...&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;products&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productProps&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; idx&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
		        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Anchor&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;productTargetUrl&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductCards&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;layout&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;TYPE_3&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductCard&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;productProps&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
              &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProductList&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Anchor&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          )
        }
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Suspense&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  );
};

export default Page;

export async function getServerSideProps() &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; queryClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;QueryClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; queryClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;prefetchQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;queryOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    queryKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;products&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    queryFn&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    staleTime&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    props&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      dehydratedState&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;dehydrate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queryClient&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Host&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; GetServerSidePropsContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; dynamic &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;next/dynamic&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Layout &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@common/components&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; HomeModule &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;dynamic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;HomeModule/home&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ssr&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;loading&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Loading...&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ShopPage&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Layout&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getServerSideProps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GetServerSidePropsContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Remote &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;HomeModule/home&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Remote&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getServerSideProps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;결과-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC-1&quot; aria-label=&quot;결과 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과&lt;/h4&gt;
&lt;center&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;video style=&quot;max-width: 500px; aspect-ratio: 2 / 3; width: 90%;&quot; controls playsinline preload&gt;
      &lt;source src=&quot;/2b280762f7d45f4910010211bfcbf861/ssr1.mp4&quot; type=&quot;video/mp4&quot; /&gt;
    &lt;/video&gt;
    &lt;figcaption&gt;MF SSR 결과 - 올리브영 앱에서 특정 페이지 일부를 MF 적용한 모습&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;center&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;video style=&quot;max-width: 500px; aspect-ratio: 2 / 3; width: 90%;&quot; controls playsinline preload&gt;
      &lt;source src=&quot;/ae04bb4f955306fd10c9102d5be9c764/ssr2.mp4&quot; type=&quot;video/mp4&quot; /&gt;
    &lt;/video&gt;
    &lt;figcaption&gt;MF SSR 결과 - 파싱된 HTML 결과&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;서버사이드 렌더링을 판단하는 기준은 Nextjs에서 응답된 HTML에 비동기로 받아온 데이터를 기준으로 HTML 내에 렌더한 엘리먼트 노드들이 첨부되어 있는 것을 확인하면 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;mf를-구성해보고-느낀-점은&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mf%EB%A5%BC-%EA%B5%AC%EC%84%B1%ED%95%B4%EB%B3%B4%EA%B3%A0-%EB%8A%90%EB%82%80-%EC%A0%90%EC%9D%80&quot; aria-label=&quot;mf를 구성해보고 느낀 점은 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MF를 구성해보고 느낀 점은?&lt;/h2&gt;
&lt;p&gt;Module Federation 필요성과 원격 모듈 로딩 원리에 대해서 파악해 보았으나 아직은 사용하기에 불편한 점이 PoC간에 몇 가지 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원격 모듈을 추가할 때마다 웹팩 설정을 함께 수정해야 했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아주 큰 규모의 개발팀과 어플리케이션에 적합한 아키텍쳐라고 설명하고 있는데 만약에 수많은 모듈들과 함께 사용하게 된다면 expose를 계속적으로 수정해야 할텐데 이 부분이 파일명 오타 및 exposes 목록 내 누락등의 휴먼에러가 발생할 여지가 있어 이를 자동화 할 수 있는 방법이 필요하다고 생각했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;원격 모듈을 추가할 때마다 호스트 어플리케이션에서 타입을 정의해 주어야 했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;원격 모듈을 추가할 때마다 호스트 어플리케이션을 수정해야 했었고 이는 독립적 개발 및 배포라는 장점과 모순되는 부분이기도 했습니다. 또한 type safe 하게 설정해야 하는데 이를 자동화 할 수 있는 방법을 구현해 활용하지 못한게 많이 아쉬웠는데 이 부분을 자동화 한다면 원격모듈 추가가 쉬워질 것이라 생각했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SSR 환경의 모듈을 구현하기가 매우 어려웠습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;레퍼런스를 보니 왜 SPA 위주의 MF 예제만 있는지 이해할 수 있었습니다. Nextjs의 page router만 지원하는 것과 getServerSideProps 등을 가져와야 하는 부분들등이 구현하기 매우 까다로웠습니다.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;이처럼 MF의 잠재력은 확실하지만, 대규모 프론트엔드 환경에서 수많은 모듈과 팀이 이 기술을 효율적이고 안전하게 활용하려면 PoC 과정에서 발견된 이 불편함들을 반드시 자동화하고 체계화해야 합니다. 과연 올리브영 프론트엔드 개발자들은 이 &apos;수동 설정&apos; 과 &apos;타입 안전성&apos; 이라는 난관을 어떻게 극복하고, Next.js 기반 MFE 아키텍처를 성공적으로 정착시키려고 할까요?&lt;/p&gt;
&lt;p&gt;다음 Part 3에서는 이 문제들을 해결하기 위한 선택지, Nx를 활용한 올리브영의 MFE 아키텍처 구현 전략을 구체적으로 공개합니다. Part 3도 놓치지 마세요! 그리고 못다 한 이야기는 향후 이런 구성으로 풀어보겠습니다. 기대 많이 해주세요!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MF를 실제 운영 환경에 배포하고 모니터링 해본 경험&lt;/li&gt;
&lt;li&gt;MF를 Type Safe하게 사용해보려 시도한 내용&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.zephyr-cloud.io/tools/module-federation-devtools&quot;&gt;Zephyr MF Devtools&lt;/a&gt; 활용한 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;이상으로 MF PoC편을 마치도록 하겠습니다. 딱딱하고 긴 글이었는데 읽어주셔서 정말 감사드립니다.&lt;/p&gt;
&lt;h2 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;참고 자료&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://module-federation.io&quot;&gt;모듈페더레이션.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://module-federation.io/guide/framework/nextjs.html&quot;&gt;모듈페더레이션.io 공식 Nextjs 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/module-federation/module-federation-examples/blob/master/nextjs-ssr/home/next.config.js&quot;&gt;모듈페더레이션.io 예제 github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[Spring Cloud Config & Bus-Refresh 도입기]]></title><description><![CDATA[🎶 마이크로서비스 오케스트라 지휘법: Spring Cloud Config…]]></description><link>https://oliveyoung.tech/2025-11-04/spring-cloud-config-server/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-04/spring-cloud-config-server/</guid><pubDate>Tue, 04 Nov 2025 17:38:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;-마이크로서비스-오케스트라-지휘법-spring-cloud-config로-실시간-설정-변경하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%98%A4%EC%BC%80%EC%8A%A4%ED%8A%B8%EB%9D%BC-%EC%A7%80%ED%9C%98%EB%B2%95-spring-cloud-config%EB%A1%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0&quot; aria-label=&quot; 마이크로서비스 오케스트라 지휘법 spring cloud config로 실시간 설정 변경하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎶 마이크로서비스 오케스트라 지휘법: Spring Cloud Config로 실시간 설정 변경하기&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;배포 없이 실시간으로 설정을 바꾸는 지휘자의 비밀 노트&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요! 올리브영 상품통합프로젝트의 하모니를 책임지고 있는 불광동꿀주먹👊입니다.&lt;/p&gt;
&lt;p&gt;&quot;설정값 딱 하나 바꾸려는데, 전체 서버를 다 재시작해야 한다고요? 그게 최선인가요?&quot; 🤔&lt;/p&gt;
&lt;p&gt;이런 고민, 개발자라면 한 번쯤 해보셨을 겁니다. 저희 팀도 얼마 전까지 설정 파일의 작은 수정 하나 때문에 전체 오케스트라의 연주(서버 배포)를 멈춰야 하는 아찔한 상황을 겪곤 했죠. 각자 다른 악보(설정)를 보고 연주하는 연주자들처럼, 서비스마다 설정이 꼬이는 일도 상상하기 싫은 현실이었습니다.&lt;/p&gt;
&lt;p&gt;하지만 이 모든 혼란을 잠재울 멋진 &lt;strong&gt;지휘봉&lt;/strong&gt;을 발견하며 저희 프로젝트는 새로운 국면을 맞이했습니다. 바로 &lt;strong&gt;Spring Cloud Config Server와 Bus-Refresh&lt;/strong&gt;를 도입한 것이죠. 🎼&lt;/p&gt;
&lt;p&gt;설정 변경을 위해 더 이상 서비스 전체를 멈추지 않아도 되는, 우아하고 조화로운 저희 팀의 개발 여정을 여러분께 공유해 드립니다. 자, 이제 저희 오케스트라의 변화를 함께 감상해 보시죠! 🚀&lt;/p&gt;
&lt;h3 id=&quot;-문제의-시작-조율되지-않는-오케스트라&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%AC%B8%EC%A0%9C%EC%9D%98-%EC%8B%9C%EC%9E%91-%EC%A1%B0%EC%9C%A8%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%98%A4%EC%BC%80%EC%8A%A4%ED%8A%B8%EB%9D%BC&quot; aria-label=&quot; 문제의 시작 조율되지 않는 오케스트라 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎼 문제의 시작: 조율되지 않는 오케스트라&lt;/h3&gt;
&lt;p&gt;저희는 상품통합프로젝트를 진행하면서 여러 마이크로서비스(MSA)를 운영하고 있습니다. 그러다보니 여러 프로젝트가 공통으로 사용하는 설정값, 예를 들면 온라인 상품 조회 API가 변경될 때마다 문제가 발생했죠.&lt;/p&gt;
&lt;p&gt;하나의 설정을 바꾸기 위해 연관된 모든 서비스를 다시 빌드하고 배포하는 과정은 비효율의 극치였습니다. 연주 중간에 악보 한 장을 바꾸려고 전체 오케스트라를 무대 뒤로 보내는 것과 같았죠.&lt;/p&gt;
&lt;p&gt;저희에게는 모든 연주자(서비스)에게 변경된 악보(설정)를 실시간으로 전달해 연주의 흐름을 끊지 않을 방법이 절실했습니다.&lt;/p&gt;
&lt;h3 id=&quot;-우리의-지휘봉을-찾아서-해결책-모색&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9A%B0%EB%A6%AC%EC%9D%98-%EC%A7%80%ED%9C%98%EB%B4%89%EC%9D%84-%EC%B0%BE%EC%95%84%EC%84%9C-%ED%95%B4%EA%B2%B0%EC%B1%85-%EB%AA%A8%EC%83%89&quot; aria-label=&quot; 우리의 지휘봉을 찾아서 해결책 모색 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💡 우리의 지휘봉을 찾아서: 해결책 모색&lt;/h3&gt;
&lt;p&gt;이러한 문제를 해결하기 위해 저희는 &lt;strong&gt;Spring Cloud Config Server&lt;/strong&gt;를 도입했습니다.&lt;/p&gt;
&lt;p&gt;이 솔루션은 분산된 여러 서비스의 설정값(악보)을 &lt;strong&gt;Git과 같은 중앙 저장소에서 통합 관리&lt;/strong&gt;하고, 변경이 생겼을 때 각 서비스에 실시간으로 동기화하는 지휘자 역할을 합니다.&lt;/p&gt;
&lt;p&gt;다양한 솔루션을 비교 검토한 결과, Spring Cloud Config Server가 저희 오케스트라에 가장 적합한 지휘봉이라고 판단했습니다.&lt;/p&gt;
&lt;p&gt;특히 &lt;strong&gt;Git을 통한 버전 관리 및 롤백 기능,&lt;/strong&gt;
&lt;strong&gt;Spring Cloud Bus와 연동하여 여러 서비스에 한 번에 설정을 전파하는 &lt;code class=&quot;language-text&quot;&gt;bus-refresh&lt;/code&gt; 기능&lt;/strong&gt; 은 저희에게 꼭 필요한 기능이었습니다.&lt;/p&gt;
&lt;p&gt;마지막까지 고민했던, AWS Parameter Store 와의 비교자료는 아래와 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;spring-cloud-config-server--aws-parameter-store&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#spring-cloud-config-server--aws-parameter-store&quot; aria-label=&quot;spring cloud config server  aws parameter store permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Spring Cloud Config Server 🆚 AWS Parameter Store&lt;/h3&gt;
&lt;style type=&quot;text/css&quot;&gt;
.tg  {border-collapse:collapse;border-spacing:0; width: 100%;}
.tg td{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
  overflow:hidden;padding:10px 5px;word-break:normal;}
.tg th{border-color:black;border-style:solid;border-width:1px;font-family:Arial, sans-serif;font-size:14px;
  font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal; font-weight: bold}
.tg .tg-0pky{border-color:inherit;text-align:left;vertical-align:top}
.tg .tg-0pky:first-child {font-weight: bold}
&lt;/style&gt;
&lt;table class=&quot;tg&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;항목&lt;/th&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;Spring Cloud Config Server&lt;/th&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;AWS Parameter Store&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;&lt;strong&gt;기본 기능&lt;/strong&gt;&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;Git 기반 설정파일 외부화, 분산 어플리케이션 구성 관리&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;Key-Value 설정 저장, 버전관리, IAM 인증&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;&lt;strong&gt;특징&lt;/strong&gt;&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;Git, Vault 등 외부 저장소와 연동, 복잡한 구성 가능&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;AWS Managed, IAM 권한 기반 접근 제어&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;강력한 설정 파일 구조화 및 프로파일별 관리&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;서버리스, 쉬운 접근, 고가용성&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;자체 구성 필요, 운영 오버헤드 발생 가능&lt;/td&gt;
      &lt;td class=&quot;tg-0pky&quot;&gt;Git 버전관리 불가, 설정 계층 표현이 제한적&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;️-조화로운-연주를-위한-설계도-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%A1%B0%ED%99%94%EB%A1%9C%EC%9A%B4-%EC%97%B0%EC%A3%BC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%84%A4%EA%B3%84%EB%8F%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;️ 조화로운 연주를 위한 설계도 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🏗️ 조화로운 연주를 위한 설계도: 아키텍처&lt;/h3&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1820px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/af6bea70407355d272639c869c14be18/ebd6b/image1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.958333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABFklEQVR42qVSu07DQBD0J9PyK7QUVDQ0lDQpkEARESYIKQkv8QjYOD6f77033EOxoEFGWWml1dzd7OzsFRgRzkpYtYEPtfcU0g+5jVgTEYoxhEQuEf2KSBAwZzxWdYm76jLBIwnpT3z5Nsf140UmjKBzLqmQhtDI2JXwxbqkII3iCL1hUFaksV3dwy2qrDC8FbpHL1kUjWL98QkuwkUjMXvhOJw2eK8Y9g+OwJUE5x20sLh6nmC1vkHU1J2WqPeOYZyBt8D06QyT+5OssGmawVxjXeik0yOhzWC2CwrbtgVjLC8pnEnGh8X0XIBtulTv7KFVhGU9w211vttSsrqML17nKB/+seWf/26bSilorXPDYElKInwDnme/uNvrXBoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/af6bea70407355d272639c869c14be18/263a4/image1.webp 480w,
/static/af6bea70407355d272639c869c14be18/a6361/image1.webp 960w,
/static/af6bea70407355d272639c869c14be18/287e6/image1.webp 1820w&quot; sizes=&quot;(max-width: 1820px) 100vw, 1820px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/af6bea70407355d272639c869c14be18/9aebd/image1.png 480w,
/static/af6bea70407355d272639c869c14be18/a91f8/image1.png 960w,
/static/af6bea70407355d272639c869c14be18/ebd6b/image1.png 1820w&quot; sizes=&quot;(max-width: 1820px) 100vw, 1820px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/af6bea70407355d272639c869c14be18/ebd6b/image1.png&quot; alt=&quot;image1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Spring Cloud Config &amp;amp; Bus-Refresh 도입 아키텍처&lt;/figcaption&gt;
&lt;p&gt;저희는 위와 같은 아키텍처를 통해 실시간 설정 변경 시스템을 구축했습니다.&lt;/p&gt;
&lt;p&gt;전체 연주(프로세스)는 이렇게 진행됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;작곡가 (AWS General User) 가 악보(설정)를 수정하여 Git에 Push합니다.&lt;/li&gt;
&lt;li&gt;지휘자 (Spring Cloud Config Server) 가 TeamCity의 트리거를 통해 변경을 감지합니다.&lt;/li&gt;
&lt;li&gt;지휘자는 &lt;strong&gt;&lt;code class=&quot;language-text&quot;&gt;/actuator/busrefresh&lt;/code&gt;&lt;/strong&gt; 엔드포인트를 호출하여 &quot;악보가 바뀌었어!&quot;라고 외칩니다.&lt;/li&gt;
&lt;li&gt;이 외침은 음파 (Amazon MQ) 를 통해 모든 연주자 (Spring Boot MicroService) 에게 순식간에 퍼져나갑니다.&lt;/li&gt;
&lt;li&gt;각 연주자는 새로운 악보를 받아 즉시 연주에 반영합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;️-악기-튜닝하기-상세-구현-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%95%85%EA%B8%B0-%ED%8A%9C%EB%8B%9D%ED%95%98%EA%B8%B0-%EC%83%81%EC%84%B8-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;️ 악기 튜닝하기 상세 구현 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🛠️ 악기 튜닝하기: 상세 구현 과정&lt;/h3&gt;
&lt;p&gt;지휘봉을 손에 넣었으니, 이제 각 악기(서비스)를 세심하게 튜닝할 차례입니다.&lt;/p&gt;
&lt;h4 id=&quot;적용-범위&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%81%EC%9A%A9-%EB%B2%94%EC%9C%84&quot; aria-label=&quot;적용 범위 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;적용 범위&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;이 시스템은 API 설정값이나 DB Endpoint처럼 외부에서 주입받는 값들에 적용하기 좋습니다.
(단, DB Endpoint와 같은 DataSource 설정을 실시간으로 변경하려면, 해당 DataSource 빈(Bean)에도 @RefreshScope를 적용하고 Spring 2.x 이상 버전의 DataSource 구성 방식을 사용해야 합니다.)&lt;/p&gt;
&lt;p&gt;하지만 주의할 점도 있습니다. Java나 Kotlin의 DTO 클래스 필드처럼 &lt;strong&gt;컴파일 시점에 구조가 고정되는 경우에는 적용할 수 없습니다.&lt;/strong&gt;  연주 중간에 바이올린을 첼로로 바꿀 수 없는 것과 같은 이치죠.&lt;/p&gt;
&lt;p&gt;Spring의 &lt;code class=&quot;language-text&quot;&gt;@RefreshScope&lt;/code&gt;는 빈(Bean)을 새로고침하는 수준이지, 클래스의 구조 자체를 바꾸지는 못하기 때문입니다.&lt;/p&gt;
&lt;h4 id=&quot;️-설정-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;️ 설정 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⚙️ 설정 방법&lt;/h4&gt;
&lt;p&gt;🚨 주의사항 &lt;strong&gt;: Spring Boot의 버전&lt;/strong&gt;에 따라 설정 방법이 상이합니다&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud&quot;&gt;https://spring.io/projects/spring-cloud&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;해당 사이트를 참고하여, 각 스프링부트 버전에 대응하는 spring-cloud-dependencies:2020.0.3 버전을 설정해주셔야 합니다&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1406px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/afa534576d8e9a9524deff714a812fcf/ace50/image2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 76.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAABwElEQVR42n1T7W6DMBDr+z/hpK2FQiAQviEJ0Fa6nY9SUcb6w0qAi2P7jlPnPH2fA7pcIwpVQsM4k5vvsm5hpxvVvaWf4Epf32c6hxHvQwHe4zvqTv04UVj0pMpWCKNUUxAraq3jgukFO83yLsmMrBCCFej9+Kpjwpl0O5JpPWVlTdpUpIuSeil4V9haT0XTiQM8r9jWnXo/s0JHilUGUSyW0rwgh2IQPYHndngq5LV3rMpPb0DdaRhvojBvLWVFRakpxN5WAQBVHR/K2QXQ897tagCxHJWO0mqgq0oFDUI+aAqUqSynvGo475Qu7ChKNIVxIucQiRCqmvNrLMU6e33Y5yQK2aY2JRkmjFOuZbKYI4h1LjHhuxCmzUKI25Q2rzz2GXZ80UqILEEKIuyBThRKUyzp2kp+yHE/g1vLIAApbGK8ICKI1EIIhUtTPDVupun+IH97/Al6axkNMTUrZGLFVkGO/fpDiMKotKKyHka2N/2v0C6zKjFMm0gwi345JwoTztB046GyN4VMiPzsbkbXC2VshnVsmHTfiKOmIONPdWI5YLsZ53hk9cjypzoem5vMYWWPJ38L/B2mbj/W/AKxsHzual+evgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/afa534576d8e9a9524deff714a812fcf/263a4/image2.webp 480w,
/static/afa534576d8e9a9524deff714a812fcf/a6361/image2.webp 960w,
/static/afa534576d8e9a9524deff714a812fcf/68819/image2.webp 1406w&quot; sizes=&quot;(max-width: 1406px) 100vw, 1406px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/afa534576d8e9a9524deff714a812fcf/9aebd/image2.png 480w,
/static/afa534576d8e9a9524deff714a812fcf/a91f8/image2.png 960w,
/static/afa534576d8e9a9524deff714a812fcf/ace50/image2.png 1406w&quot; sizes=&quot;(max-width: 1406px) 100vw, 1406px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/afa534576d8e9a9524deff714a812fcf/ace50/image2.png&quot; alt=&quot;image2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Spring Cloud Config Dependencies&lt;/figcaption&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Spring Boot 2.3.x 이하:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;bootstrap.yml&lt;/code&gt; 파일 필요&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;spring.cloud.config.uri&lt;/code&gt; 설정 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Spring Boot 2.4.x 이상:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;spring.config.import&lt;/code&gt; 방식 (본 가이드 기준)&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;bootstrap.yml&lt;/code&gt; 불필요 (선택적)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes#config-file-processing&quot;&gt;참고: Spring Boot 2.4 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;build.gradle.kts (domain, interface패키지 하위) 의존성 추가&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;dependencies {
   implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)
   implementation(&quot;org.springframework.cloud:spring-cloud-starter-config&quot;)
   implementation(&quot;org.springframework.cloud:spring-cloud-starter-bus-amqp&quot;)
}

dependencyManagement {
    imports {
        mavenBom(&quot;org.springframework.cloud:spring-cloud-dependencies:2020.0.5&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;application.yml 또는 application-&lt;profile&gt;.yml에 아래와 같이 설정&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;spring:
  application:
    name: test-application
  config:
    import: optional:configserver:http://spring-cloud-config-server.com
      activate:
        on-profile: &quot;qa&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;설정 적용을 위한 actuator endpoint 활성화&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;management:
  endpoints:    
    web:      
      exposure:        
        include: health, info, refresh, busrefresh&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rabbitMQ 설정 추가(계정정보는 config-server에서 암호화되어 설정)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;rabbitmq:    
  host: b-1234-4567-8900-abcd-123456789.mq.ap-northeast-2.on.aws    
  port: 5671    
  virtual-host: /    
  ssl:      
    enabled: true         `&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;호출하려는 클래스에 @RefreshScope 어노테이션 추가, @Value 어노테이션으로 값 주입&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;@RefreshScope 
@Service 
class TestServiceImpl(    
    val testRepository: TestRepository 
) : TestService {      
    @Value(&quot;\${config.goods-api-url}&quot;)   
    lateinit var goodsApi: String 
}   &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-클라이맥스-실시간-변경의-순간&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%81%B4%EB%9D%BC%EC%9D%B4%EB%A7%A5%EC%8A%A4-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EB%B3%80%EA%B2%BD%EC%9D%98-%EC%88%9C%EA%B0%84&quot; aria-label=&quot; 클라이맥스 실시간 변경의 순간 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✨ 클라이맥스: 실시간 변경의 순간&lt;/h3&gt;
&lt;p&gt;모든 튜닝이 끝나고, 드디어 지휘를 시작할 시간입니다. 개발자가 &lt;strong&gt;Git에 변경된 설정값을 Push&lt;/strong&gt;하자, TeamCity가 이를 감지하여 &lt;code class=&quot;language-text&quot;&gt;busrefresh&lt;/code&gt;를 자동으로 호출합니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;BusRefresh 수동 호출&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Teamcity Trigger 설정 자동호출&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;curl --location --request POST &apos;http://spring-cloud-config-server.com/actuator/busrefresh&apos;&lt;/code&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;github push Trigger로 동작하여 actuator/busrefresh 호출!&lt;br&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1621px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5b5a9fce5e2301c723ca4a3a87706d97/a2372/image3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 19.583333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsElEQVR42n2Q207DMBAF8/+/CAGE5boRSWPHd2N7cC+qeIEjjfZpdWZ32o6A+DJon9HmQAjB6XxGSEVKiWtaa7Te4T8emdRmmeXKbgPOe/ygtc5l14QQKKXcqLXyd/qTadkdn8MwlW9yjqR4t9KHQ6oLehT5VPAhsn+8s82vbG+/mF+IzlPa/ZLparbsdixlWs3PvsM61tVgjCWkSIzjAiWxUuDHjIt6cKKO19z8eucHWxw1eRmKDCUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5b5a9fce5e2301c723ca4a3a87706d97/263a4/image3.webp 480w,
/static/5b5a9fce5e2301c723ca4a3a87706d97/a6361/image3.webp 960w,
/static/5b5a9fce5e2301c723ca4a3a87706d97/5ac36/image3.webp 1621w&quot; sizes=&quot;(max-width: 1621px) 100vw, 1621px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5b5a9fce5e2301c723ca4a3a87706d97/9aebd/image3.png 480w,
/static/5b5a9fce5e2301c723ca4a3a87706d97/a91f8/image3.png 960w,
/static/5b5a9fce5e2301c723ca4a3a87706d97/a2372/image3.png 1621w&quot; sizes=&quot;(max-width: 1621px) 100vw, 1621px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5b5a9fce5e2301c723ca4a3a87706d97/a2372/image3.png&quot; alt=&quot;image3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;곧이어 서비스 로그에는 변경된 설정 키들이 새로고침되었다는 메시지가 나타납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;2025-06-21 01:45:19.733 [INFO] (RefreshListener.java:50) Keys refreshed [config.goods-api-url] [cite: 149]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;그리고 마법처럼, 서버 재시작 없이 API 응답값이 즉시 변경된 것을 확인할 수 있었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이로써 단순 설정 변경을 위한 &lt;strong&gt;배포 시간이 기존 평균 20분에서 1분 이내로 단축되었고,&lt;/strong&gt;
긴급 상황 발생 시 운영 서버의 재시작 부담 없이 신속하게 대응할 수 있는 안정성을 확보했습니다.&lt;/p&gt;
&lt;p&gt;이 모든 과정이 끝나면 Slack으로 알림 메시지가 전송되어, 모든 오케스트라 단원들이 변경 사항을 인지하게 됩니다.&lt;/p&gt;
&lt;div style=&quot;display:flex; gap: 20px;&quot;&gt;
  &lt;div style=&quot;width: 550px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 676px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7ca80b08e017c7124a1007c09cca0aef/ff08e/image4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABm0lEQVR42s2S227TQBRFJ/xDS9KSxHaci+3E4xlfYk9I0tAojahUCLQCCUH+/ysWY5dCkSAvvPCwtM/Ze3TmZYuFTJgd3pLtH5jffaE4HNG7A3r7jnR/b/170puPZLsPZLefye6+kr8/kt9+IopTglASRMlPhBq5BN8kulzTHwTEaUW52pKbDeZqR2Z1qitk/hqZGmaqJNaGdL7CH83oOmN67qTB8SaIwhuxWd6gVMaw16ZIZmyvltYzrBeGRZGSWy9LYoskih612XVCrhWZUlSZIgxjRO4NkNOJfTCl0DGliiik3eOg0VKFjVfpiGQWMRzbPLFYr84qHSKjAM8b0nN8RHzZ5YUQiJbFaqv1ROtRxS/ED1p/4ClrDv5m/Cv//0Hd9+i0L3lpaXdeNdq56DZzzdmz7Nzq2fnFSYR0fdLcUC3fYFbXjCNFUa0ozRqz3DTllWrezCotbfciy/SviHgY4c13OPk1TrbBq/Y40tB3RvTdMa4te13Yeq613k8hhvEGf31EzpdMQoXrTxnYnwb2oxrPDxuez6f4Dnp0RZiAKIUmAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7ca80b08e017c7124a1007c09cca0aef/263a4/image4.webp 480w,
/static/7ca80b08e017c7124a1007c09cca0aef/c7990/image4.webp 676w&quot; sizes=&quot;(max-width: 676px) 100vw, 676px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7ca80b08e017c7124a1007c09cca0aef/9aebd/image4.png 480w,
/static/7ca80b08e017c7124a1007c09cca0aef/ff08e/image4.png 676w&quot; sizes=&quot;(max-width: 676px) 100vw, 676px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7ca80b08e017c7124a1007c09cca0aef/ff08e/image4.png&quot; alt=&quot;Config Server 알럿 메시지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;figcaption&gt;Config Server 알럿 메시지(Config 변경 내역 전송)&lt;/figcaption&gt; 
  &lt;/div&gt;
  &lt;div style=&quot;width: 550px;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 805px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4b8ec80d7f1d9eecb72b084ea132d4b8/0469b/image5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.12500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB40lEQVR42m2S7Y+TQBDGSerFaGJM6x0UKJQChS6wu+VlobTXt3jqJfrRb/7/f8fj7GqjZ/zwyzwzJMPMs2N5X89Ixmekp29YP33HaviIuL8i3X9BQoTqCrtUcLZH2M0FzvCZ9AkPtou5F8ALVi+w4h8CXPbwoxTLNEe5VRDdHlWzozhiEeeYvH5reGV4gztiMrnDu/cz+GH8smHXnbDJc6T+A1a+DVlk6EQBnicoshSSrcHyDJzRzza5iSzLUemc8MIEcz/603DXVbj2ApdB4mmUeGw5zlS7KE51jk8jx77hOHQSYyNwVIIiN0NokmSNNEkRLGO4umFTb6FqiVoK1IKj3WotKQpD30iIktF0KU1K02ZrFPkvtNYI2mIZUcNFBItJhaKqUfAGoulRkYclaWaoTYyzEmHCEKVEssFqXZDfDCH5a7SpbYyflhoGDMMOIT1KQES0Qt8rPB5G9FQ/H/eomxYZ40RlYkk/0jonzSqJTSkIicUygcXViLLuMJ0vMKVTmDk+bBrd9kPYQQSHfLE90m5A5i8NDuUm/tY3zKN8cFy4rgdn7mI6u4ftzHFvOybqXKMNj2ml20uaUyH8v/Lb+VhMtGhpyrbfkw8MdTegUTt0w4HWEsZb/a2gNf+9uf/xE0j9IO/M417GAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4b8ec80d7f1d9eecb72b084ea132d4b8/263a4/image5.webp 480w,
/static/4b8ec80d7f1d9eecb72b084ea132d4b8/91236/image5.webp 805w&quot; sizes=&quot;(max-width: 805px) 100vw, 805px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4b8ec80d7f1d9eecb72b084ea132d4b8/9aebd/image5.png 480w,
/static/4b8ec80d7f1d9eecb72b084ea132d4b8/0469b/image5.png 805w&quot; sizes=&quot;(max-width: 805px) 100vw, 805px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4b8ec80d7f1d9eecb72b084ea132d4b8/0469b/image5.png&quot; alt=&quot;Config Client 알럿 메시지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;figcaption&gt;Config Server 알럿 메시지(Config 변경 내역 전송)&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-앙코르-표준화와-다음-연주를-향하여&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%95%99%EC%BD%94%EB%A5%B4-%ED%91%9C%EC%A4%80%ED%99%94%EC%99%80-%EB%8B%A4%EC%9D%8C-%EC%97%B0%EC%A3%BC%EB%A5%BC-%ED%96%A5%ED%95%98%EC%97%AC&quot; aria-label=&quot; 앙코르 표준화와 다음 연주를 향하여 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚀 앙코르: 표준화와 다음 연주를 향하여&lt;/h3&gt;
&lt;p&gt;저희는 이 경험을 바탕으로, 해당 시스템 구성을 올리브영 개발센터의 &lt;strong&gt;표준 아키텍처 템플릿(&lt;code class=&quot;language-text&quot;&gt;olive-plate&lt;/code&gt;)에 적용&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;이제 누구나 이 강력한 지휘봉을 사용하여 조화로운 마이크로서비스 오케스트라를 구성할 수 있게 된 것이죠.&lt;/p&gt;
&lt;p&gt;급변하는 기술의 세계 속에서, 마치 시간에 쫓기는 토끼처럼 쉼없이 달려가던 저희는 잠시 멈춰 서서 이 오케스트라를 조율하는 법을 배운 셈입니다.&lt;/p&gt;
&lt;p&gt;이제 저희의 오케스트라는 지휘자의 손짓 하나(&lt;code class=&quot;language-text&quot;&gt;git push&lt;/code&gt;)에 일사불란하게 연주를 바꾸는 환상적인 하모니를 자랑하게 되었습니다.&lt;/p&gt;
&lt;p&gt;저희의 다음 연주가 궁금하시다면, 올리브영 기술 블로그를 계속 주목해주세요! 🎶&lt;/p&gt;
&lt;h3 id=&quot;references&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#references&quot; aria-label=&quot;references permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud&quot;&gt;Spring Cloud 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud-config&quot;&gt;Spring Cloud Config 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-bus/docs/current/reference/html/&quot;&gt;Spring Cloud Bus 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[SDUI로 네이티브 운영 민첩성 높이기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-11-04/server-driven-technical-exploration/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-11-04/server-driven-technical-exploration/</guid><pubDate>Tue, 04 Nov 2025 17:30:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;들어가며&lt;/h2&gt;
&lt;p&gt;안녕하세요, 올리브영 웰니스서비스 개발팀에서 안드로이드 앱을 개발하고 있는 차니혀니 입니다. (집에서는 두 아들의 아빠이기도 합니다.)&lt;/p&gt;
&lt;p&gt;앱을 운영하다 보면 스토어 심사라는 벽 앞에서 한숨 쉬어본 경험, 다들 있으실 겁니다. 특히 빠르게 변화하는 이커머스 환경에서 네이티브 앱의 업데이트 속도는 비즈니스의 발목을 잡기도 합니다. 이러한 &apos;속도의 한계&apos;를 극복하고, 스토어 심사없이 서버 설정만으로 화면을 변경하는 라는 여정을 시작했습니다. 이 글에서는 저희가 SDUI를 어떻게 설계하고, 어떤 문제들을 고민하였는지 그 경험을 공유하고자 합니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;우리의-고민-속도-일관성-효율-그리고-sdui&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EC%9D%98-%EA%B3%A0%EB%AF%BC-%EC%86%8D%EB%8F%84-%EC%9D%BC%EA%B4%80%EC%84%B1-%ED%9A%A8%EC%9C%A8-%EA%B7%B8%EB%A6%AC%EA%B3%A0-sdui&quot; aria-label=&quot;우리의 고민 속도 일관성 효율 그리고 sdui permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;우리의 고민: 속도, 일관성, 효율 그리고 SDUI&lt;/h2&gt;
&lt;p&gt;웹과 달리 네이티브 앱은 배포 후 이슈가 생겨도 스토어 심사로 인해 즉각 대응이 어렵습니다. 그래서 빠르게 수정이 필요할 때 사용자에게 반영되기까지 시간이 소요됩니다. 그래서 저희의 고민은 명확했습니다. 앱 배포를 어떻게 해야 빠르면서, 일관성있고, 효율적일까? 이런 맥락에서 속도, 일관성, 효율 이라는 세 가지 핵심 문제를 극복하기 위한 솔루션이 바로 &lt;strong&gt;SDUI(Server-Driven UI)&lt;/strong&gt; 입니다. SDUI는 화면 정보를 서버에서 내려주고, 앱은 그 데이터를 기반으로 UI를 그려내는 방식입니다. 서버는 ‘무엇을, 어떤 순서로’ 보여줄지에 집중하고, 클라이언트는 그 정보를 받아 ‘어떻게’ 빠르고 자연스럽게 그릴지에 집중할 수 있습니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;아키텍처-정의-작게-시작하여-점진적으로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%A0%95%EC%9D%98-%EC%9E%91%EA%B2%8C-%EC%8B%9C%EC%9E%91%ED%95%98%EC%97%AC-%EC%A0%90%EC%A7%84%EC%A0%81%EC%9C%BC%EB%A1%9C&quot; aria-label=&quot;아키텍처 정의 작게 시작하여 점진적으로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;아키텍처 정의: 작게 시작하여 점진적으로!&lt;/h2&gt;
&lt;p&gt;처음에는 역할을 나누고, 리스크를 낮추기 위해 스코프를 작게 잡은 뒤 점진적으로 확장하는 전략을 택했습니다. 우선 서버와 클라이언트가 각자 무엇에 집중해야 할지부터 정의했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;서버의 역할: 강제 업데이트, 앱 버전 등 앱의 &apos;운영&apos;과 관련된 큰 그림을 관리하고, 화면을 그릴 &apos;설계도(스키마)&apos;를 제공하는 데 집중합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;클라이언트의 역할: 서버가 준 설계도를 네이티브 UI로 &apos;렌더링&apos;하고, 알 수 없는 정보가 오더라도 앱이 죽지 않도록 &apos;안전하게 폴백&apos;하며, 사용자의 행동(이벤트, 트래킹)을 실행합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 초기 범위 결정이 중요했습니다. 처음부터 모든 레이아웃 속성(margin, padding 등)까지 서버가 제어하려 하면 복잡도가 걷잡을 수 없이 커질 거라 판단했습니다. 그래서 초기 범위는 &apos;섹션, 컴포넌트, 데이터, 액션&apos; 4가지로 한정하고, 레이아웃은 클라이언트의 디자인 시스템에 위임하여 단순함을 유지했습니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;확장-가능한-설계의-핵심-스키마-정의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%99%95%EC%9E%A5-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%84%A4%EA%B3%84%EC%9D%98-%ED%95%B5%EC%8B%AC-%EC%8A%A4%ED%82%A4%EB%A7%88-%EC%A0%95%EC%9D%98&quot; aria-label=&quot;확장 가능한 설계의 핵심 스키마 정의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;확장 가능한 설계의 핵심: 스키마 정의&lt;/h2&gt;
&lt;p&gt;SDUI의 성패는 서버와 클라이언트 간의 &apos;약속&apos;, 즉 &lt;strong&gt;스키마&lt;/strong&gt;를 얼마나 잘 설계하는지에 달려있습니다. 스키마가 너무 복잡하면 유지보수가 어렵고, 너무 단순하면 SDUI의 장점을 살릴 수 없습니다. 저희는 &apos;이커머스 서비스&apos;의 특성을 고려하여 다음과 같이 4가지 핵심 요소를 정의했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;섹션 (Section): &apos;오늘의 특가&apos;, &apos;실시간 랭킹&apos;처럼 독립적인 목적을 가진 컴포넌트 이상의 단위&lt;/li&gt;
&lt;li&gt;컴포넌트 (Component): 섹션을 구성하는 &apos;상품 카드&apos;, &apos;헤더 타이틀&apos; 같은 최소 단위&lt;/li&gt;
&lt;li&gt;데이터 (Data): 컴포넌트에 채워질 실제 내용 (예: 상품명, 가격, 이미지 URL)&lt;/li&gt;
&lt;li&gt;액션 (Action): 버튼 클릭 시 특정 페이지로 이동하거나, 분석 로그를 보내는 등 사용자의 행동을 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;앞서 네 가지 핵심 요소를 정리했으니, 이제 각 요소의 역할과 스키마 예시를 차례로 살펴보겠습니다. 첫 번째로 &apos;섹션&apos;입니다.&lt;/p&gt;
&lt;h3 id=&quot;섹션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B9%EC%85%98&quot; aria-label=&quot;섹션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;섹션&lt;/h3&gt;
&lt;p&gt;정확한 목적을 가지고 있는 &lt;strong&gt;독립적인 UI 단위&lt;/strong&gt;라 설명할 수 있습니다. 예를 들면 전시화면에서의 상단 배너섹션, 타임딜 섹션 등을 예로 들 수 있을 것 같습니다.
섹션은 아래 코드와 같이 꼭 컴포넌트 단위의 구성이 아니더라도, 해당 섹션만을 위한 필드들을 구성할 수 있습니다. 그럼 서버 관점에서 매번 컴포넌트(header, more_button, title)를 조합하지 않고 미리 정의된 데이터만 채워서 내려주면 되니 단순해집니다. 물론 공통의 요소들을 컴포넌트 단위로 정의하고 사용한다면 재사용성을 높이고 일관된 UI를 표현할 수 있는 장점도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;// 타임딜 섹션 샘플
{
  &amp;quot;type&amp;quot;: &amp;quot;TIME_DEAL_SECTION&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;10&amp;quot;,
  &amp;quot;header&amp;quot;: {
    &amp;quot;title&amp;quot;: &amp;quot;타이틀&amp;quot;,
    &amp;quot;style&amp;quot;: &amp;quot;HEADER_LARGE&amp;quot;
  },
  &amp;quot;limitTime&amp;quot;: &amp;quot;2025-12-31T23:59:59Z&amp;quot;,
  &amp;quot;totalCount&amp;quot;: 2,
  &amp;quot;itemList&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;CARD_TYPE&amp;quot;,
      &amp;quot;id&amp;quot;: 1,
      &amp;quot;code&amp;quot;: &amp;quot;상품code&amp;quot;,
      &amp;quot;title&amp;quot;: &amp;quot;올리브영 상품1&amp;quot;
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;CARD_TYPE&amp;quot;,
      &amp;quot;id&amp;quot;: 2,
      &amp;quot;code&amp;quot;: &amp;quot;상품code&amp;quot;,
      &amp;quot;title&amp;quot;: &amp;quot;올리브영 상품2&amp;quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;컴포넌트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8&quot; aria-label=&quot;컴포넌트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;컴포넌트&lt;/h3&gt;
&lt;p&gt;컴포넌트는 하나의 섹션을 만들기 위해 여러 개의 컴포넌트가 조합될 수 있는 요소로 정의할 수 있습니다. 물론 하나의 컴포넌트로 한 영역을 담당 할 수 도 있습니다. 예를 들면 header, productCard가 될 수 있겠네요. 아래 코드를 통해 몇 가지 샘플을 살펴보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;HEADER_COMPONENT&amp;quot;,
  &amp;quot;id&amp;quot;: 10,
  &amp;quot;title&amp;quot;: &amp;quot;타이틀&amp;quot;,
  &amp;quot;style&amp;quot;: &amp;quot;HEADER_LARGE&amp;quot; // HEADER_MEDIUM, HEADER_REGULAR
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;HEADER_COMPONENT&amp;quot;,
  &amp;quot;id&amp;quot;: 11,
  &amp;quot;title&amp;quot;: &amp;quot;타이틀&amp;quot;,
  &amp;quot;handler&amp;quot;: &amp;quot;이벤트 처리&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;PRODUCT_CARD&amp;quot;,
  &amp;quot;id&amp;quot;: 12,
  &amp;quot;code&amp;quot;: &amp;quot;10001&amp;quot;,
  &amp;quot;title&amp;quot;: &amp;quot;올리브영 상품&amp;quot;,
  &amp;quot;price&amp;quot;: 9900,
  &amp;quot;imageUrl&amp;quot;: &amp;quot;https://...&amp;quot;,
  &amp;quot;badge&amp;quot;: &amp;quot;HOT&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;FOOTER_COMPONENT&amp;quot;,
  &amp;quot;id&amp;quot;: 13,
  &amp;quot;title&amp;quot;: &amp;quot;더 보기&amp;quot;,
  &amp;quot;handler&amp;quot;: &amp;quot;이벤트 처리&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위의 3가지를 조합하여(HEADER_COMPONENT + PRODUCT_CARD + FOOTER_COMPONENT) 하나의 섹션을 만들어 사용할 수 있습니다. 상황에 따라 헤더 또는 푸터가 없을 수도 있어서 스키마 존재에 따라 유연하게 반응해야 합니다. 가로형 상품카드에 쓰이는 상품카드 타입은 단독으로도 노출할 수 있어야 하며, 다양한 섹션 또는 컴포넌트와 조합 가능하도록 구조화가 필요합니다.&lt;/p&gt;
&lt;div style=&quot;display:flex;&quot;&gt;
  &lt;div style=&quot;width: 550px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1270px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6b0b12416b0ce89657d6c2469cbcaf17/6b882/sdui-horizontal-scroll.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABMklEQVR42n2S23qCMBCEff/3q62fopADIQdM0N5PJwu02mIv/o9sMgw7G3Z9mmDjBBMybLiSLLUQCtx4x/7Y4o3U9ffZJgU7S0PNog8J1hgMYYSJNzL9kH7VrwiL4bpxUk42H0X99c7OPtGzu8qWkV0/KIZcaJ+hWOwbhW7Isymp6/1JM26H97PBR6PRuiuULyTLc9XVWgwNcxsWPo5oLp086zylOx/RdhrNucXxonG4WNghwqdRdD1nrsOEVG5MMYnpbFgN3ABHkaOJoVCiDAHKOnnZpSKRq6FxXj5mfZL5xzzxfDGUyGy1HtTYStZ1jkX2D52b9x5ZtDrMuo5GehnT06VsceDcJMU/Gvt4y0Y6/IsixjM6f6WeMXW4beqe4CXt6jDrz70JO7MxCy81TxR8Ac1+6lqRUVdkAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6b0b12416b0ce89657d6c2469cbcaf17/263a4/sdui-horizontal-scroll.webp 480w,
/static/6b0b12416b0ce89657d6c2469cbcaf17/a6361/sdui-horizontal-scroll.webp 960w,
/static/6b0b12416b0ce89657d6c2469cbcaf17/ad398/sdui-horizontal-scroll.webp 1270w&quot; sizes=&quot;(max-width: 1270px) 100vw, 1270px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6b0b12416b0ce89657d6c2469cbcaf17/9aebd/sdui-horizontal-scroll.png 480w,
/static/6b0b12416b0ce89657d6c2469cbcaf17/a91f8/sdui-horizontal-scroll.png 960w,
/static/6b0b12416b0ce89657d6c2469cbcaf17/6b882/sdui-horizontal-scroll.png 1270w&quot; sizes=&quot;(max-width: 1270px) 100vw, 1270px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6b0b12416b0ce89657d6c2469cbcaf17/6b882/sdui-horizontal-scroll.png&quot; alt=&quot;케러셀 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
     &lt;figcaption&gt;가로형 상품섹션 구성도&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;데이터-모든-플랫폼의-공통-언어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%93%A0-%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%9D%98-%EA%B3%B5%ED%86%B5-%EC%96%B8%EC%96%B4&quot; aria-label=&quot;데이터 모든 플랫폼의 공통 언어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터: 모든 플랫폼의 &apos;공통 언어&apos;&lt;/h3&gt;
&lt;p&gt;스키마의 세 번째 요소는 컴포넌트에 실제 내용을 채우는 &apos;데이터&apos;입니다. SDUI는 Android, iOS, Web이 모두 같은 스키마를 바라보기 때문에, 이 &apos;공통 언어&apos;의 규칙을 명확히 정의하는 것이 무엇보다 중요했습니다. (must not be null 오류를 수도 없이 경험했던 기억이... 😅)&lt;/p&gt;
&lt;p&gt;저희는 다음과 같은 엄격한 데이터 규칙을 세웠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;필수값 (Required): id, type처럼 이 값이 없으면 렌더링 자체가 불가능한 값&lt;/li&gt;
&lt;li&gt;옵션값 (Optional): subtitle처럼 값이 없어도 UI가 깨지지 않는 값&lt;/li&gt;
&lt;li&gt;기본값 (Default): isShow = false처럼, 값이 누락되었을 때 클라이언트가 안전하게 처리할 기본 상태&lt;/li&gt;
&lt;li&gt;포맷 (Format): date 포맷, ImageUrl 경로 등 데이터의 형식을 통일&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;PRODUCT_CARD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;12&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;상품명&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;price&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9900&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;imageUrl&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;이미지 URL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;badge&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;SALE&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;rating&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4.6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;reviewCount&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;isShow&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;액션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%A1%EC%85%98&quot; aria-label=&quot;액션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;액션&lt;/h3&gt;
&lt;p&gt;각 컴포넌트의 이벤트 및 로그 등을 담당하게 됩니다. 해당 설계방향은 서비스 특성에 따라 달라지게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;PRODUCT_CARD&amp;quot;,
  // .....
  &amp;quot;handler&amp;quot;: {
    &amp;quot;action&amp;quot;: {
      &amp;quot;type&amp;quot;: &amp;quot;Link&amp;quot;,
      &amp;quot;value&amp;quot;: &amp;quot;url 정보&amp;quot;
    },

    //1안
    &amp;quot;event1&amp;quot;: {
      &amp;quot;amplitude&amp;quot;: {
        &amp;quot;name&amp;quot;: &amp;quot;product_select&amp;quot;,
        &amp;quot;parameters&amp;quot;: { &amp;quot;param1&amp;quot;: &amp;quot;value1&amp;quot;, &amp;quot;param2&amp;quot;: &amp;quot;value2&amp;quot; }
      },
      &amp;quot;appsflyer&amp;quot;: {
        &amp;quot;name&amp;quot;: &amp;quot;product_select&amp;quot;,
        &amp;quot;parameters&amp;quot;: { &amp;quot;param1&amp;quot;: &amp;quot;value1&amp;quot;, &amp;quot;param2&amp;quot;: &amp;quot;value2&amp;quot; }
      }
    },

    //2안
    &amp;quot;event2&amp;quot;: [
      {
        &amp;quot;type&amp;quot;: &amp;quot;amplitude&amp;quot;,
        &amp;quot;event_type&amp;quot;: &amp;quot;impression&amp;quot;,
        &amp;quot;name&amp;quot;: &amp;quot;product_impression&amp;quot;,
        &amp;quot;parameters&amp;quot;: { &amp;quot;param1&amp;quot;: &amp;quot;value1&amp;quot;, &amp;quot;param2&amp;quot;: &amp;quot;value2&amp;quot; }
      },
      {
        &amp;quot;type&amp;quot;: &amp;quot;appsflyer&amp;quot;,
        &amp;quot;event_type&amp;quot;: &amp;quot;click&amp;quot;,
        &amp;quot;name&amp;quot;: &amp;quot;product_select&amp;quot;,
        &amp;quot;parameters&amp;quot;: { &amp;quot;param1&amp;quot;: &amp;quot;value1&amp;quot;, &amp;quot;param2&amp;quot;: &amp;quot;value2&amp;quot; }
      }
    ]
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Action의 경우 Type에 따라 동작이 달라집니다. 아래와 같이 정의해보았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Link : URL을 통해 외부,내부 브라우저로 이동 하거나 딥링크가 동작&lt;/li&gt;
&lt;li&gt;API : API BaseUrl을 제외한 Path 정보를 제공함으로써 앱에서 별도의 하드코딩 없이 API를 요청
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@GET&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPageInfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token annotation builtin&quot;&gt;@Url&lt;/span&gt; path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token annotation builtin&quot;&gt;@Query&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cursor&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; cursor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Result&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ComponentInfoResponse&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Event의 경우 1안은 명시적이고, 이벤트 타입이 한 곳에 모여 있는 장점이 있지만, 새로운 이벤트 추가 시 확장성이 떨어지는 단점이 있습니다.
2안이 적합한 이유에는 &apos;확장성&apos; 외에 &apos;플랫폼 간 일관성&apos;이나 &apos;로깅 정책 통일&apos; 관점에서도 유효한 점이 있어 권장하지만, 각 이벤트별로 파라미터 및 제공하는 스펙이 다르면 데이터 구조를 통일하는 어려움이 있습니다.
하지만 확장성을 고려하면 가장 이상적인 방법이라 볼 수 있고, 자체 로그 시스템이 존재한다면 이벤트별 라우팅을 담당하도록 위임하는게 가장 좋습니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;사용자-경험을-극대화하는-sdui-성능-최적화-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98%EC%9D%84-%EA%B7%B9%EB%8C%80%ED%99%94%ED%95%98%EB%8A%94-sdui-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;사용자 경험을 극대화하는 sdui 성능 최적화 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;사용자 경험을 극대화하는 SDUI 성능 최적화 전략&lt;/h2&gt;
&lt;p&gt;SDUI는 서버가 정의한 데이터를 클라이언트가 그대로 렌더링하는 데서 그치지 않습니다. 다양한 상황에 즉각 대응하기 위해 여러 각도에서 고려해야 할 요소들이 있습니다. 이번에는 그 고려 사항을 공유해보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;atfabove-the-fold&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#atfabove-the-fold&quot; aria-label=&quot;atfabove the fold permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;ATF(Above-The-Fold)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;스크롤 없이 가장 처음에 보이는 초기화면 영역입니다. 사용자는 해당 영역으로 서비스의 가치를 판단하기도 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ATF 전용 API 분리: 첫 화면 로딩에 필수적인 데이터만 담은 가벼운 API를 먼저 호출하고, 스크롤해야 보이는 하단 영역 데이터는 별도 API로 분리하여 비동기 처리했습니다.&lt;/li&gt;
&lt;li&gt;초기 데이터 최소화: ATF 영역에 표시되는 섹션의 종류와 개수를 최소화하여, 클라이언트가 화면을 그리는 데 필요한 연산 부담을 줄였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 최적화는 서버 응답 시간(Latency)과 클라이언트 렌더링 성능을 꾸준히 프로파일링하며 최적의 균형점을 찾아나가는 과정이 중요합니다.&lt;/p&gt;
&lt;h3 id=&quot;pagination-끊김-없는-스크롤-경험&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pagination-%EB%81%8A%EA%B9%80-%EC%97%86%EB%8A%94-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B2%BD%ED%97%98&quot; aria-label=&quot;pagination 끊김 없는 스크롤 경험 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;Pagination: 끊김 없는 스크롤 경험&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Pagination은 사용자가 스크롤을 내릴 때, 다음 페이지가 로드되고 있다는 사실조차 느끼지 못하게 만드는 것이 핵심입니다.&lt;/p&gt;
&lt;p&gt;저희는 사용자가 리스트의 마지막에 도달하기 전, 특정 인덱스의 섹션이 보이기 시작할 때 다음 페이지 데이터를 미리 요청하도록 구현했습니다. &apos;미리 로딩&apos;을 시작하는 시점(인덱스)은 섹션이 화면을 차지하는 높이나 개수에 따라 달라질 수 있으므로, 각 화면의 특성에 맞게 유연하게 결정하는 것이 좋습니다.&lt;/p&gt;
&lt;h3 id=&quot;lazy-loading&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#lazy-loading&quot; aria-label=&quot;lazy loading permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;Lazy Loading&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;서버 응답 시간에 영향을 미치는 개인화 영역이 있습니다. 개인화 영역의 일부 섹션이나 컴포넌트는 서버에서 캐싱 처리가 어렵기 때문입니다. 이때 Lazy Loading 기법을 활용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Lazy Loading을 활용한 개인화 영역은 처음부터 모든 데이터를 받지 않습니다. 서버 응답을 받을 때 스켈레톤 로딩을 해당 영역에 노출하고 비동기로 API를 호출하여 유저 경험에 부정적 영향을 주지 않도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lazy Loading 동작 메커니즘&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;페이지정보 요청&lt;/li&gt;
&lt;li&gt;섹션 타입에 스켈레톤 Prefix가 붙은 섹션은 Lazy Loading이 필요한 요소&lt;/li&gt;
&lt;li&gt;path를 통해서 해당 섹션에 대해 비동기 통신&lt;/li&gt;
&lt;li&gt;새로운 응답값을 통해 UI 바인딩 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;// 개인화 추천 섹션 템플릿
{
  &amp;quot;type&amp;quot;: &amp;quot;SKELETON_SECTION_HORIZONTAL_CAROUSEL_LIST&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;5&amp;quot;,
  &amp;quot;path&amp;quot;: &amp;quot;해당 스켈레톤 섹션을 통해 요청할 API Path&amp;quot;
}

// Path를 통해 새롭게 받아온 개인화 데이터
{
  &amp;quot;type&amp;quot;: &amp;quot;HORIZONTAL_CAROUSEL_LIST&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;5&amp;quot;,
  &amp;quot;header&amp;quot;: { &amp;quot;title&amp;quot;: &amp;quot;타이틀&amp;quot; },
  &amp;quot;items&amp;quot;: [
    { &amp;quot;type&amp;quot;: &amp;quot;PRODUCT_CARD&amp;quot;, &amp;quot;id&amp;quot;: &amp;quot;상품1&amp;quot; },
    { &amp;quot;type&amp;quot;: &amp;quot;PRODUCT_CARD&amp;quot;, &amp;quot;id&amp;quot;: &amp;quot;상품2&amp;quot; }
  ],
  &amp;quot;footer&amp;quot;: null
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;그-외에-고민한-요소는&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B7%B8-%EC%99%B8%EC%97%90-%EA%B3%A0%EB%AF%BC%ED%95%9C-%EC%9A%94%EC%86%8C%EB%8A%94&quot; aria-label=&quot;그 외에 고민한 요소는 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;그 외에 고민한 요소는?&lt;/h2&gt;
&lt;h3 id=&quot;데이터가-비어있을-경우empty-section&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EB%B9%84%EC%96%B4%EC%9E%88%EC%9D%84-%EA%B2%BD%EC%9A%B0empty-section&quot; aria-label=&quot;데이터가 비어있을 경우empty section permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터가 비어있을 경우(Empty Section)&lt;/h3&gt;
&lt;p&gt;전시지면에서 정상적인 상품 리스트를 받아오지 못한 경우 에러뷰를 노출 합니다. 이런 경우에 아래와 같이 Error Section을 관리하면 에러 타입별 문구와 재시도 동작 또한 정의해서 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsonc&quot;&gt;&lt;pre class=&quot;language-jsonc&quot;&gt;&lt;code class=&quot;language-jsonc&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;ERROR_SECTION&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;5&amp;quot;,
  &amp;quot;imageUrl&amp;quot;: &amp;quot;이미지 Path&amp;quot;,
  &amp;quot;title&amp;quot;: &amp;quot;상품이 존재하지 않습니다&amp;quot;,
  &amp;quot;subtitle&amp;quot;: &amp;quot;다시 검색해주세요&amp;quot;,
  &amp;quot;buttonTitle&amp;quot;: &amp;quot;재시도&amp;quot;,
  &amp;quot;handler&amp;quot;: {
    &amp;quot;action&amp;quot;: { &amp;quot;type&amp;quot;: &amp;quot;API&amp;quot;, &amp;quot;value&amp;quot;: &amp;quot;재시도할 API PATH&amp;quot; }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;클라이언트-상태와-서버-상태의-동기화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%83%81%ED%83%9C%EC%99%80-%EC%84%9C%EB%B2%84-%EC%83%81%ED%83%9C%EC%9D%98-%EB%8F%99%EA%B8%B0%ED%99%94&quot; aria-label=&quot;클라이언트 상태와 서버 상태의 동기화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;클라이언트 상태와 서버 상태의 동기화&lt;/h3&gt;
&lt;p&gt;위에서 설명한 필터 버튼을 클릭한 후 활성화 여부를 클라이언트에서 관리하지 않습니다. 아래 코드에서 handler를 통해 API를 요청하고, 응답받은 데이터에 id가 5인 필터는 &lt;code class=&quot;language-text&quot;&gt;isSelected = true&lt;/code&gt; 값으로 내려오고, 이전에 선택된 필터는 &lt;code class=&quot;language-text&quot;&gt;false&lt;/code&gt;로 내려오도록 동기화되어 있습니다.
이 부분은 응답 속도에 따라 영향을 받을 수 있어, 먼저 선택한 값을 활성화하고 응답 여부에 따라 다시 롤백 또는 유지도 가능하기 때문에 상황에 맞게 메커니즘을 결정하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;FILTER_CHIP&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;5&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;필터1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;isSelected&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;handler&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
       &lt;span class=&quot;token property&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;API&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;token property&quot;&gt;&quot;value&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;API PATH&quot;&lt;/span&gt;
       &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;결과-그래서-무엇이-달라졌나&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC-%EA%B7%B8%EB%9E%98%EC%84%9C-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EB%82%98&quot; aria-label=&quot;결과 그래서 무엇이 달라졌나 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과: 그래서 무엇이 달라졌나?&lt;/h2&gt;
&lt;p&gt;아직 정식 배포 전이지만, 저희는 이미 SDUI를 설계하고 구축하는 과정에서부터 &lt;strong&gt;문제를 즉각 통제할 수 있다&lt;/strong&gt;는 강력한 변화를 체감하고 있습니다.&lt;/p&gt;
&lt;p&gt;과거에는 치명적인 핫픽스조차 스토어 배포와 심사라는 긴급 대응이 불가능한 프로세스에 의존해야 했습니다. 하지만 이제는 문제가 된 섹션을 서버 설정만으로 즉시 비활성화 하거나 안전한 구성으로 대체할 수 있는 기술적 기반이 마련되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A/B 테스트와 같은 실험을 진행하는 방식 자체도 근본적으로 달라졌습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;과거에는 동일한 실험을 위해 iOS, Android 각 플랫폼별로 개별 개발과 배포 일정을 조율하는 복잡한 과정이 필요했습니다. 이제는 기획이 확정되면 서버에서 스키마를 정의하는 단 한 번의 작업으로, iOS, Android, Web 모든 플랫폼에 &apos;동시에&apos; 새로운 UI를 노출할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;이는 팀이 같은 타이밍에 같은 실험을 시작할 수 있음을 의미하며, 실험 준비에 드는 리드 타임이 &apos;플랫폼 배포 주기(수일에서 수주)&apos;에서 &apos;서버 배포 주기(수분에서 수시간)&apos;로 바뀌는 질적인 변화를 의미합니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;마치며-sdui는-만능인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-sdui%EB%8A%94-%EB%A7%8C%EB%8A%A5%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;마치며 sdui는 만능인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며: SDUI는 만능인가?&lt;/h2&gt;
&lt;p&gt;지금까지 네이티브 앱의 고질적인 &apos;속도의 한계&apos;를 극복하기 위해 저희가 SDUI를 탐색하고 고민해 온 여정을 공유해 드렸습니다. 이 여정을 거치며 저희가 내린 결론은, SDUI는 만능 해결책이 아니라는 것입니다.&lt;/p&gt;
&lt;p&gt;모든 기술이 그렇듯, SDUI 역시 명확한 트레이드오프가 존재합니다. 서버와 클라이언트가 &apos;스키마&apos;라는 강력한 약속으로 연결되기에 초기 설계의 복잡도가 높고, 문제가 발생했을 때 양쪽을 모두 살펴봐야 하므로 디버깅 난이도도 올라갑니다. 무엇보다 사용자의 네트워크 환경에 절대적으로 의존한다는 제약도 무시할 수 없습니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 핵심은 모든 화면을 SDUI로 전환하는 것이 아니라, 각 화면의 목적에 맞게 네이티브, 웹, SDUI를 전략적으로 조합하여 최적의 사용자 경험을 제공하는 것이었습니다. 어떤 화면은 네이티브의 성능이 절대적으로 중요하고, 어떤 화면은 웹의 범용성이 필요합니다.&lt;/p&gt;
&lt;p&gt;그렇다면 SDUI의 진정한 가치는 어디에 있을까요? 저희는 SDUI가 단순히 콘텐츠를 보여주는 기술을 넘어, 네이티브의 느린 대응 속도를 보완하는 &lt;strong&gt;효율적인 운영 도구&lt;/strong&gt;가 되는 지점에 있다고 생각합니다.&lt;/p&gt;
&lt;p&gt;SDUI 도입을 고민해봤거나, 하고 있는 분들에게 저희의 경험이 조금이나마 도움이 되기를 바랍니다. 그리고 기회가 된다면, SDUI와 안드로이드 Jetpack Compose를 결합하여 적용한 사례도 소개하는 시간을 만들어 보겠습니다.
&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id=&quot;appendix-sdui-도입을-위한-실전-가이드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#appendix-sdui-%EB%8F%84%EC%9E%85%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%8B%A4%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; aria-label=&quot;appendix sdui 도입을 위한 실전 가이드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[Appendix] SDUI 도입을 위한 실전 가이드&lt;/h2&gt;
&lt;h3 id=&quot;자주-받는-질문faq&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EC%A3%BC-%EB%B0%9B%EB%8A%94-%EC%A7%88%EB%AC%B8faq&quot; aria-label=&quot;자주 받는 질문faq permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;자주 받는 질문(FAQ)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;어떤 화면부터 SDUI로 바꾸면 좋을까요?
&lt;ul&gt;
&lt;li&gt;변화가 잦고, 실험이 활발하며, 크리티컬하지만 네이티브 변경 부담이 큰 전시/리스트 영역부터 시작하는 것을 추천합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 레이아웃을 서버로 옮겨야 하나요?
&lt;ul&gt;
&lt;li&gt;아닙니다. 초기에는 레이아웃을 제외하고 섹션/컴포넌트/데이터/액션만 다루는 것이 안정적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실험/롤백은 어떻게 빠르게 하나요?
&lt;ul&gt;
&lt;li&gt;스키마 버저닝과 기능 토글(서버 설정)을 결합해 섹션 단위로 비활성화/치환/롤백이 가능하도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;도입-시-흔한-함정과-회피법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EC%9E%85-%EC%8B%9C-%ED%9D%94%ED%95%9C-%ED%95%A8%EC%A0%95%EA%B3%BC-%ED%9A%8C%ED%94%BC%EB%B2%95&quot; aria-label=&quot;도입 시 흔한 함정과 회피법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;도입 시 흔한 함정과 회피법&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;스키마 과설계: 너무 많은 옵션/레이아웃을 한 번에 넣지 않습니다. 항상 최소 스펙으로 시작합니다.&lt;/li&gt;
&lt;li&gt;알 수 없는 타입 처리 누락: 모르는 타입/필드는 안전 폴백하고 로그로만 남기는 것이 좋습니다.&lt;/li&gt;
&lt;li&gt;단일 거대 API: ATF 전용 API를 분리해 첫 화면 체감 속도를 먼저 확보합니다.&lt;/li&gt;
&lt;li&gt;추상화 누락: 공통 컴포넌트(상품 카드, 헤더 등)는 재사용을 전제로 스키마와 뷰를 함께 설계합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;빠른-도입-체크리스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%A0%EB%A5%B8-%EB%8F%84%EC%9E%85-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;빠른 도입 체크리스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;빠른 도입 체크리스트&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 섹션/컴포넌트/데이터/액션 스키마 초안&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 알 수 없는 타입/필드 폴백 정책&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; ATF 전용 API, 하단 영역 분리 API&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 페이지네이션, Lazy Loading 동작 기준&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 이벤트/트래킹 스펙(공통 파라미터, 라우팅)&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled&gt; 섹션 단위 비활성화/치환/롤백 방법&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[RabbitMQ Classic Queue 메모리 장애와 Quorum Queue 전환기]]></title><description><![CDATA[안녕하세요. 쿠폰 스쿼드 포덕입니다. 2025년 새해 첫 대량 쿠폰 발급을 진행하던 중, 예상치 못한 일이 벌어졌습니다.
1,500만 건의 쿠폰을 발급하던 중에 RabbitMQ…]]></description><link>https://oliveyoung.tech/2025-10-28/coupon-mq-issue/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-10-28/coupon-mq-issue/</guid><pubDate>Tue, 28 Oct 2025 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 쿠폰 스쿼드 포덕입니다.&lt;/p&gt;
&lt;p&gt;2025년 새해 첫 대량 쿠폰 발급을 진행하던 중, 예상치 못한 일이 벌어졌습니다.
1,500만 건의 쿠폰을 발급하던 중에 RabbitMQ 클러스터에 문제가 생겼습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 긴급 상황에서의 대응 과정부터 AWS TAM(Technical Account Manager)과 함께 찾아낸 근본 원인,
그리고 Classic Mirrored Queue에서 Quorum Queue로 전환하기까지의 여정을 공유해드리려고 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;-장애-발생과-초기-대응&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9E%A5%EC%95%A0-%EB%B0%9C%EC%83%9D%EA%B3%BC-%EC%B4%88%EA%B8%B0-%EB%8C%80%EC%9D%91&quot; aria-label=&quot; 장애 발생과 초기 대응 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚨 장애 발생과 초기 대응&lt;/h1&gt;
&lt;center&gt;
  &lt;img src=&quot;https://media.tenor.com/2bgUC0TWiecAAAAM/elmo-elmos-fire.gif&quot; width=&quot;600&quot;&gt;
  &lt;figcaption&gt;https://media.tenor.com/2bgUC0TWiecAAAAM/elmo-elmos-fire.gif&lt;/figcaption&gt;
&lt;/center&gt;
&lt;p&gt;2025년 1월, 새해 첫 대량 쿠폰 발급을 진행하던 평범한 오후였습니다. 1,500만 건의 쿠폰을 발급하던 중 갑자기 &lt;strong&gt;쿠폰 발급이 완전히 멈춰버렸습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;처음에는 단순한 일시적 지연으로 생각했지만, 시간이 지날수록 상황은 심각해졌습니다. &lt;strong&gt;다수의 고객 문의&lt;/strong&gt;가 쏟아졌고, &lt;strong&gt;매출 손실&lt;/strong&gt; 발생 가능 이슈로 긴급 대응 채널이 생성되었습니다.&lt;/p&gt;
&lt;p&gt;문제의 핵심은 &lt;strong&gt;RabbitMQ 클러스터의 메모리 과다 점유&lt;/strong&gt;였습니다. 메모리가 부족해지면서 브로커가 Critical Status로 변경되었고, 클러스터 내 특정 노드가 &lt;strong&gt;unsynchronized 상태&lt;/strong&gt;로 전환되면서 메시지 처리가 완전히 멈춰버린 것이었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1376px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/84597da3bd55bc7fe62dbdeb22fa6ac9/9f622/2025-01-11-issu-graph.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAn0lEQVR42oVRCQ4DIQj0/59tG08EnCqru3bTpiQT5BpGdd4HPJ4v1Mowa7is/YewwIeIGBMKEdw525pBm57YiVf9G3ZzZ3LmWRnSyaQJRMW89eBzsOHK7eRuD1S1E8pUBItl5g6wLRnqf6l1+9bVbGy3qyxVh/oLelPvYg79MRNC9uapJDATiHL/qII6fAfPs4za6KmEnCOoZqTJUSjjDXDs2s9FdDJXAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/84597da3bd55bc7fe62dbdeb22fa6ac9/263a4/2025-01-11-issu-graph.webp 480w,
/static/84597da3bd55bc7fe62dbdeb22fa6ac9/a6361/2025-01-11-issu-graph.webp 960w,
/static/84597da3bd55bc7fe62dbdeb22fa6ac9/b46f4/2025-01-11-issu-graph.webp 1376w&quot; sizes=&quot;(max-width: 1376px) 100vw, 1376px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/84597da3bd55bc7fe62dbdeb22fa6ac9/9aebd/2025-01-11-issu-graph.png 480w,
/static/84597da3bd55bc7fe62dbdeb22fa6ac9/a91f8/2025-01-11-issu-graph.png 960w,
/static/84597da3bd55bc7fe62dbdeb22fa6ac9/9f622/2025-01-11-issu-graph.png 1376w&quot; sizes=&quot;(max-width: 1376px) 100vw, 1376px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/84597da3bd55bc7fe62dbdeb22fa6ac9/9f622/2025-01-11-issu-graph.png&quot; alt=&quot;쿠폰 발급량 저하&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 876px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9183ffa2c5e8318e6a7c566959c69742/566b8/2025-01-11-unsync.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 13.541666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAz0lEQVR42h2NTUuEUABF/f/EuHFV7eoh02qCyBkoESaoJ2o1ULYLhqkxn/M+VNBZnazF4XAXh+sZYxjHkWEY/mm7FuUUpjeTG3SrUW2DdQfGXcXRWJpp92OPtW7ykX7qlFJ0XYfn+z5/BEHA7GTG6eUZiUlYqzX3KiY9pMR1wsP2jmJxzia94Xa/JKtStl8Vcq+RnztWqyVRFOGFYYgQgvnVHHEhuI4WFO6ZV7Mh0xml+6BoCnJdEE8nuXnhsX7iXb1RN5pSWcrvH/IsQ0rJLySS0QWyYUyiAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9183ffa2c5e8318e6a7c566959c69742/263a4/2025-01-11-unsync.webp 480w,
/static/9183ffa2c5e8318e6a7c566959c69742/9d58c/2025-01-11-unsync.webp 876w&quot; sizes=&quot;(max-width: 876px) 100vw, 876px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9183ffa2c5e8318e6a7c566959c69742/9aebd/2025-01-11-unsync.png 480w,
/static/9183ffa2c5e8318e6a7c566959c69742/566b8/2025-01-11-unsync.png 876w&quot; sizes=&quot;(max-width: 876px) 100vw, 876px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9183ffa2c5e8318e6a7c566959c69742/566b8/2025-01-11-unsync.png&quot; alt=&quot;쿠폰 발급 Queue 노드 상태 변경&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;쿠폰 발급 처리 감소 및 노드 메모리 부하&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h1 id=&quot;-초기-대응과-긴급-복구&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%B4%88%EA%B8%B0-%EB%8C%80%EC%9D%91%EA%B3%BC-%EA%B8%B4%EA%B8%89-%EB%B3%B5%EA%B5%AC&quot; aria-label=&quot; 초기 대응과 긴급 복구 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔧 초기 대응과 긴급 복구&lt;/h1&gt;
&lt;p&gt;처음 문제가 접수되었을 때는 평소처럼 DB, 트래픽, WAS부터 확인했습니다. 하지만 모든 지표가 정상이었습니다. 쿠폰 관련 리소스들을 하나씩 점검하던 중에서야 &lt;strong&gt;MQ 상태 이상&lt;/strong&gt;이 발견되었고, 그제서야 진짜 원인을 찾을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;인스턴스 증설&lt;/strong&gt;부터 시도해봤지만 예상과 달리 효과가 없었습니다. 메모리를 늘려도 unsynchronized 상태는 그대로였는데, 이는 &lt;strong&gt;이미 동기화가 깨진 상태에서는 단순히 리소스를 늘리는 것만으로는 해결되지 않기 때문&lt;/strong&gt;입니다. RabbitMQ의 미러링 메커니즘에서 한 번 out of sync 상태가 되면, 모든 노드가 다시 동기화될 때까지 메시지 소비가 중단되는데, 이 과정에서 메모리 증설은 근본적인 해결책이 되지 못합니다. &lt;strong&gt;브로커 재시작&lt;/strong&gt;을 시도하려 했지만 Memory Alarm이 해소된 후에야 재시작이 가능하다는 안내를 받았습니다. 관리형 서비스 특성상 즉시 재시작이 제한되었습니다. &lt;strong&gt;Queue Purge&lt;/strong&gt;도 Admin UI로만 가능하고 Force Purge는 지원되지 않아서, 긴급 상황에서의 세밀한 제어가 어려웠습니다.&lt;/p&gt;
&lt;p&gt;복구가 계속 지연되면서 비즈니스 영향이 커지는 상황에서, &lt;strong&gt;일단 서비스부터 살리기로&lt;/strong&gt; 했습니다. 기존 장애 브로커와 동일한 설정으로 &lt;strong&gt;새로운 RabbitMQ 브로커를 생성&lt;/strong&gt;하고, 복잡한 클러스터 재구성 대신 독립 운영 방식을 선택했습니다.&lt;/p&gt;
&lt;p&gt;애플리케이션의 MQ 접속 정보를 신규 브로커로 변경하면서 기존 메시지 손실을 방지하기 위해 전환 시점을 신중하게 조율했습니다. 점진적으로 트래픽을 전환하면서 실시간 모니터링으로 정상 동작을 계속 확인한 결과, &lt;strong&gt;약 30분 만에 메시지 처리가 정상화&lt;/strong&gt;되었고 쿠폰 발급 서비스가 완전히 복구되었습니다.&lt;/p&gt;
&lt;p&gt;긴급 대응이 끝나고 나서는 &lt;strong&gt;재발 방지를 위한 체계적인 원인 분석&lt;/strong&gt;에 착수했습니다. 메모리 부족이 브로커의 메시지 처리에 미치는 영향과 당시 진행 중이던 대량 발급의 연관성을 가설로 설정하고, 장애 상황을 개발 환경에서 재현해서 정확한 원인을 찾아보기로 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;-원인-분석과-재현-실험&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9B%90%EC%9D%B8-%EB%B6%84%EC%84%9D%EA%B3%BC-%EC%9E%AC%ED%98%84-%EC%8B%A4%ED%97%98&quot; aria-label=&quot; 원인 분석과 재현 실험 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔍 원인 분석과 재현 실험&lt;/h1&gt;
&lt;p&gt;정확한 원인을 찾기 위해 사고 발생 전 상황을 자세히 조사했습니다. 당시 &lt;strong&gt;세 가지 작업이 동시에 진행&lt;/strong&gt;되고 있었습니다. 1,500만 건 규모의 전체 회원 대상 대량 발급, 전체 매장 대상 쿠폰의 동시 발급 및 사용 처리, 그리고 평상시 트래픽과 겹치면서 Queue 메시지가 급격히 쌓인 상황이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;병목 지점&lt;/strong&gt;들을 분석해보니 1,500만 건 대상자를 조회하는 과정에서 DB에 부하가 발생했고, count-up 로직에서 지연이 누적되면서 메시지 처리 속도가 떨어졌습니다. 결국 처리 속도보다 메시지가 더 빠르게 들어오면서 &lt;strong&gt;메모리 사용량이 급증&lt;/strong&gt;한 것이 문제의 시작이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;rabbitmq-미러링-메커니즘의-구조적-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#rabbitmq-%EB%AF%B8%EB%9F%AC%EB%A7%81-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98%EC%9D%98-%EA%B5%AC%EC%A1%B0%EC%A0%81-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;rabbitmq 미러링 메커니즘의 구조적 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;RabbitMQ 미러링 메커니즘의 구조적 한계&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Classic Mirrored Queue&lt;/strong&gt;는 고가용성을 보장하기 위해 Master 노드와 Mirror 노드 간의 동기화 구조로 동작합니다. Master 노드가 메시지를 실제로 저장하고 Consumer가 연결되는 주 노드 역할을 하고, Mirror 노드는 Master와 동일한 데이터를 갖도록 유지되는 백업 노드입니다.&lt;/p&gt;
&lt;center class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bae41a276c539f8e5cb50e25ff09c247/3a775/mirror-flow.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAoUlEQVR42o2OSw+EIBCD+f9/zxN3SYyIL3whaDedZMxusgcn6QGm/ToGL6ZpGgzD8MYKc10XVPd9/1XbtgJUz/eoR3dm2zaUUrDvO0II6LpOwjFGTNOEuq5RVRWstXJp3/c/GscR67oKjAwBco7jEABhNPE/pfSUqLhnkHuW0sOcZsyyLHKhXsM2mqicsxQ55+C9lwDf/KeYO89TCuZ5BlkfB26EV9YMnLwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bae41a276c539f8e5cb50e25ff09c247/263a4/mirror-flow.webp 480w,
/static/bae41a276c539f8e5cb50e25ff09c247/a6361/mirror-flow.webp 960w,
/static/bae41a276c539f8e5cb50e25ff09c247/0b34d/mirror-flow.webp 1920w,
/static/bae41a276c539f8e5cb50e25ff09c247/c0666/mirror-flow.webp 2004w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bae41a276c539f8e5cb50e25ff09c247/9aebd/mirror-flow.png 480w,
/static/bae41a276c539f8e5cb50e25ff09c247/a91f8/mirror-flow.png 960w,
/static/bae41a276c539f8e5cb50e25ff09c247/ac7a9/mirror-flow.png 1920w,
/static/bae41a276c539f8e5cb50e25ff09c247/3a775/mirror-flow.png 2004w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bae41a276c539f8e5cb50e25ff09c247/ac7a9/mirror-flow.png&quot; alt=&quot;RabbitMQ 미러링 구조 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Master 노드와 Mirror 노드 간의 동기화 과정에서 메모리 부족 시 동기화 실패가 발생&lt;/figcaption&gt;
&lt;/center&gt;
&lt;p&gt;문제는 &lt;strong&gt;동기화 실패 시나리오&lt;/strong&gt;에서 발생합니다. Master 노드가 메시지를 받으면 모든 Mirror 노드에 복제 요청을 보내는데, Mirror 노드가 메모리 부족으로 제대로 응답하지 못하면 동기화가 실패합니다. 그러면 해당 Queue가 out of sync 상태가 되고, &lt;strong&gt;RabbitMQ는 데이터 안전성을 위해 out of sync가 해소되기 전까지 메시지 소비 처리를 완전히 중단&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;p&gt;AWS MQ 공식 문서를 보면, RabbitMQ는 메모리 사용량이 임계점에 도달할 경우 &lt;strong&gt;Memory Alarm&lt;/strong&gt;을 작동시켜 새로운 메시지 수신을 차단합니다. 하지만 이미 처리 중인 메시지의 미러 노드 동기화 과정에서는 새로운 메시지 publish가 차단되고, 기존 메시지의 동기화 요청에 대한 응답이 지연되거나 무시되며, Mirror 노드의 동기화 상태 확인 프로세스가 중단되는 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이런 보수적인 방식은 데이터 무결성 측면에서는 안전하지만, 서비스 가용성 측면에서는 치명적인 약점이 될 수 있습니다. &lt;strong&gt;RabbitMQ 팀에서도 이런 한계를 인정하고 Classic Mirrored Queue를 deprecated로 지정&lt;/strong&gt;했으며, 대신 Quorum Queue 사용을 권장하고 있습니다.&lt;/p&gt;
&lt;center class=&quot;photo-item&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1582px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b814f7991c827ae96c1b41081d11291f/d3b5d/aws_mq_guide.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 27.083333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA40lEQVR42lWQiY6CMBRF+f8fdBQtdN8oZRPGBLhTK5qxycm5vX1pmhaX2uBENE4lBSEVGJegTEBIBal0ttLmsAUXEtP9judatx3bfrC9KMy0QsQFqlvgxwXuwA7zx2F+YNqAcQXSOPrHhvh78Ml76tOFTFlQacC1A1UOTHtUwoBwnbLL+dlz0+QzYQOka78g3OTexwGFqhnorQInNaxQCMahtT77P62xiM6j8wF9E9A6/zXbpO+YYodCljVoSVCfb6jO12yW9u+OXlL/k7gqOOlgmcz06WVT6DGGLrnD2ETMccAfs2N2rm+D4RsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b814f7991c827ae96c1b41081d11291f/263a4/aws_mq_guide.webp 480w,
/static/b814f7991c827ae96c1b41081d11291f/a6361/aws_mq_guide.webp 960w,
/static/b814f7991c827ae96c1b41081d11291f/ec992/aws_mq_guide.webp 1582w&quot; sizes=&quot;(max-width: 1582px) 100vw, 1582px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b814f7991c827ae96c1b41081d11291f/9aebd/aws_mq_guide.png 480w,
/static/b814f7991c827ae96c1b41081d11291f/a91f8/aws_mq_guide.png 960w,
/static/b814f7991c827ae96c1b41081d11291f/d3b5d/aws_mq_guide.png 1582w&quot; sizes=&quot;(max-width: 1582px) 100vw, 1582px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b814f7991c827ae96c1b41081d11291f/d3b5d/aws_mq_guide.png&quot; alt=&quot;AWS MQ 메모리 관리 가이드 문서 캡처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;AWS MQ 공식 문서의 메모리 임계점 관리 가이드라인&lt;br&gt;
  https://docs.aws.amazon.com/amazon-mq/latest/developer-guide/troubleshooting-action-required-codes-rabbitmq-memory-alarm.html&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-상황-재연과-근본-원인-확정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%83%81%ED%99%A9-%EC%9E%AC%EC%97%B0%EA%B3%BC-%EA%B7%BC%EB%B3%B8-%EC%9B%90%EC%9D%B8-%ED%99%95%EC%A0%95&quot; aria-label=&quot; 상황 재연과 근본 원인 확정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔍 상황 재연과 근본 원인 확정&lt;/h1&gt;
&lt;p&gt;정확한 분석을 위해 운영 환경과 똑같은 조건으로 개발 환경을 구성해서 다양한 테스트를 해봤습니다. AWS MQ 브로커를 운영 환경과 동일한 인스턴스 타입 및 설정으로 구성하고, Classic Mirrored Queue에 운영 환경과 같은 미러링 정책을 적용했습니다.&lt;/p&gt;
&lt;center&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1777px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bd9dbf6b85ac932f932769a097a590b9/6b6d7/inci-case2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 61.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABeUlEQVR42oVT2W7CQAzs/39bH1AlkKoWqkKahNzn5oSpx8W0RKBG2jhre8fj8ebJ933UdY1hGHA6ndD3PTzPw263Q9M06pvnWdc0Tbro3+/3msezljOOI574Wj7n8xlVVeFejIAEoV0+BL4LyGpJkoDsLU5fmqYIw1C/L6Uv6x9ABtq2RZ7nmIWJtURm9HF/73kIyJYJSK0miX+s1miKEq7rUBSFtssC1D0JDrIfHwPS2cnBTJiUZYlUWt88rxB++QLoVFvGCcbv7etG9u4qyxWQrAaZMFk1dYVeDjnXKpvWOfG3mrMsHktB59yCoSR2dYPt5gWB94n39Urtm1gCBUFwc0UMmFeMXVCeX0COX9Yo2kRxjCzLEEVHnXIURdoGD7Iw42RkXTGHQ4rFz05YSBnqEEQPJpMFE1g5v+hoDMosQZHG15Z5ljG7+DcaUmAD4n3jnpag1uLR92Qd0Alj6tapzj+WXZDlzVDMLpf5qdHfX9F+R2LYVfoGzCulzaM0s/4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bd9dbf6b85ac932f932769a097a590b9/263a4/inci-case2.webp 480w,
/static/bd9dbf6b85ac932f932769a097a590b9/a6361/inci-case2.webp 960w,
/static/bd9dbf6b85ac932f932769a097a590b9/2e804/inci-case2.webp 1777w&quot; sizes=&quot;(max-width: 1777px) 100vw, 1777px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bd9dbf6b85ac932f932769a097a590b9/9aebd/inci-case2.png 480w,
/static/bd9dbf6b85ac932f932769a097a590b9/a91f8/inci-case2.png 960w,
/static/bd9dbf6b85ac932f932769a097a590b9/6b6d7/inci-case2.png 1777w&quot; sizes=&quot;(max-width: 1777px) 100vw, 1777px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bd9dbf6b85ac932f932769a097a590b9/6b6d7/inci-case2.png&quot; alt=&quot;재연 프로세스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;figcaption&gt;재연 프로세스&lt;/figcaption&gt;
&lt;/center&gt;
&lt;h2 id=&quot;1차-재연-시도-부분적-성공&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EC%B0%A8-%EC%9E%AC%EC%97%B0-%EC%8B%9C%EB%8F%84-%EB%B6%80%EB%B6%84%EC%A0%81-%EC%84%B1%EA%B3%B5&quot; aria-label=&quot;1차 재연 시도 부분적 성공 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1차 재연 시도: 부분적 성공&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;테스트 시나리오 1&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 일반 쿠폰 발급 진행 (점진적 증가)
2. 대량발급 약 1,500만 건 일괄 요청  
3. 대량발급 Queue 메시지 적재 상태 확인
4. 일반 쿠폰 발급 지속 유입
5. 1시간 정도 계속 모니터링&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;결과&lt;/strong&gt;: &lt;strong&gt;Memory High Critical Alarm은 발생했지만 unsynchronized 상태까지는 재현되지 않았습니다&lt;/strong&gt;. 메모리 사용량이 임계점에 도달한 후 시간이 지나면서 자연스럽게 해소되는 패턴을 보였습니다.&lt;/p&gt;
&lt;h2 id=&quot;2차-재연-브로커-재시작-시나리오-추가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EC%B0%A8-%EC%9E%AC%EC%97%B0-%EB%B8%8C%EB%A1%9C%EC%BB%A4-%EC%9E%AC%EC%8B%9C%EC%9E%91-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4-%EC%B6%94%EA%B0%80&quot; aria-label=&quot;2차 재연 브로커 재시작 시나리오 추가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2차 재연: 브로커 재시작 시나리오 추가&lt;/h2&gt;
&lt;p&gt;운영 환경에서 문제 해결을 위해 브로커 재시작을 시도했다는 점에 주목해서, 테스트 시나리오를 확장해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;테스트 시나리오 2&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;1. 일반 쿠폰 발급 진행
2. 대량발급 약 1,500만 건 일괄 요청
3. 대량발급 Queue 메시지 적재 확인 
4. 일반 쿠폰 발급 지속 유입
5. 1시간 정도 모니터링 진행
6. 🔥 Consumer 처리 중 broker 강제 재시작
7. 재시작 후 동기화 상태 모니터링&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;center&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1058px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/70e81d05798daea57fee5e7fc8c3b991/e5cc9/unsync-check.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA9klEQVR42o1S2aqFMAzs/3+aT74Jgvu+oHXfzWECvfTBe46BaEqSyUxacd83wcZxpDzPaZ5nOs+TjuOgdV0pjmMqy5LjNyYUIP7btnFjlmUURRGFYUhJkjDgsiycgyPGwEdAfKZpoq7rmE3btn9gaZqy4xwEAbmuS77vs5JhGJ4BMR0NjuOQ53nU9z0PADMAYAAYKYZYyVfJ+77zztTe4FVVMVuwxMCiKDivTK3pX8m6oRgsbNsmy7IYFOxlK99din64rosBsU+wAlPESsGToV53oScgU0pJdV3zM9KHvDWhs2uahi/EMAwyTZPBf4GhBmrw1ND/ARWiuxXAMG/8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/70e81d05798daea57fee5e7fc8c3b991/263a4/unsync-check.webp 480w,
/static/70e81d05798daea57fee5e7fc8c3b991/a6361/unsync-check.webp 960w,
/static/70e81d05798daea57fee5e7fc8c3b991/1de89/unsync-check.webp 1058w&quot; sizes=&quot;(max-width: 1058px) 100vw, 1058px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/70e81d05798daea57fee5e7fc8c3b991/9aebd/unsync-check.png 480w,
/static/70e81d05798daea57fee5e7fc8c3b991/a91f8/unsync-check.png 960w,
/static/70e81d05798daea57fee5e7fc8c3b991/e5cc9/unsync-check.png 1058w&quot; sizes=&quot;(max-width: 1058px) 100vw, 1058px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/70e81d05798daea57fee5e7fc8c3b991/e5cc9/unsync-check.png&quot; alt=&quot;테스트 환경에서 재현된 unsynchronized 상태&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/center&gt;
&lt;p&gt;드디어 &lt;strong&gt;브로커 재시작 과정에서 unsynchronized 상태를 재현&lt;/strong&gt;할 수 있었습니다!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;메모리 부족 + 브로커 재시작 = Unsynchronized 상태&lt;/strong&gt;라는 공식이 확정되었습니다. 재시작 과정에서 브로커가 일시적으로 메인터넌스 모드로 전환되고, 클러스터 내에서 노드 역할이 재배치되면서 누적된 메시지를 처리하는 과정에서 순간적으로 메모리가 급증합니다. 이때 클러스터 내 노드 간 복제본 동기화 과정에서 타임아웃이 발생하는 것이 문제의 핵심이었습니다.&lt;/p&gt;
&lt;p&gt;노드의 메모리 사용량이 높은 상태에서 브로커를 재시작하면 오히려 상황이 더 악화될 수 있다는 것을 확인했습니다. &lt;strong&gt;Classic Mirrored Queue의 구조적 한계가 고부하 상황에서 더욱 명확하게 드러났습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-aws-tam-협업을-통한-공식-해결책-도출&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-aws-tam-%ED%98%91%EC%97%85%EC%9D%84-%ED%86%B5%ED%95%9C-%EA%B3%B5%EC%8B%9D-%ED%95%B4%EA%B2%B0%EC%B1%85-%EB%8F%84%EC%B6%9C&quot; aria-label=&quot; aws tam 협업을 통한 공식 해결책 도출 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🤝 AWS TAM 협업을 통한 공식 해결책 도출&lt;/h1&gt;
&lt;center&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1536px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/840ae843e4998e285be5747cb348dc87/e8464/aws-tam-meet.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAADJElEQVR42gEZA+b8APDfvvDevfTkxPbox/PiwfLgvt3OsMC2n8i9psi9psS5osO5osS6o8S6o8W6o7+1nuDQsvLgv+7dvPDfvgDw3r3y48LbxaXMsI7ey6v35cPQw6jUzLzYzr7Rybnq4dDp4M7h2Mjj2snr4tHJwrPbzbH86cf35sTy4L8A9eXE0baUoV4ypWtCsJJy9+jGr6OLqaOVyL2txrur/PHd8efT5dzKy8Oz9OzZ39nGd3BgV1NFiIBs7929APfox7aae6JtSOW2i9e8mLqvlTIqIVBAL7Cmlu/k0ergzejfzZGIeCsmG2BdVMa+rohvVT41KBETDrWojgD968jTxKfZsIfvu4rRtZOOg2+lgmPGnHO3qpb37tzi18T17Nm1oIemh2ZoWUaxqpvguY/fsoZgU0HEt5sAzr+jYVtNrItppoVkz72eaWBPsYhjrYZihn1u6+LR4tnH6eHRwquT0aF138Gix8O2tJV29MCPt5x63M2uADo4LyMjHT45Ly4sJF5bUIKNkId+cXh3cImMg8y/pMq+pbuxmIR4XLGXe5KGaoF6X7KjiKCGZsKlfsStiAAxLyg0MikwLydKRTk+PTV7j5yMoa2Moa+ira3l07Ht27ysnn1iVSp5bUdxYzhqXDK4nnjKrobOsYjStIkAPDguNDIpMzEqUUo8WE8+gIJ+uZ+Df3pwipuii4t+QENAQkRCTks7dWUzZlkucWM9x6qD4sGU07WL0rWLAN6vg3NgSisrJSYoI66Ma+W3i7qbfJR7YHx4bpuNdj1APjY+Qj9EQ4lzUmtdOpZ7Vb6eeti7j82thMunfwDBm3bouIu2knCYfF+4k3CkhmWqjGu6j2S4i1zIlGCXd1V8Z09pW0ptXEiYfV7ltIiuj3DPqoPltormuIsAW0w4rYZc06Bu26ZzxJZptYpd1p5l051m1J5o0Jxn3aRquoxbYVVCREVAZlxMmnxd16RyzJxuw5x0rJRyAI5qR5hwSpZvSZRuSJlxSpxzTJhxS5hxS5lxS5pxS5hwSpdwSolnRWZQOWtSOodkQppwSZVuSJBrR11MODGrwaGNz9rIAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/840ae843e4998e285be5747cb348dc87/263a4/aws-tam-meet.webp 480w,
/static/840ae843e4998e285be5747cb348dc87/a6361/aws-tam-meet.webp 960w,
/static/840ae843e4998e285be5747cb348dc87/f3efb/aws-tam-meet.webp 1536w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/840ae843e4998e285be5747cb348dc87/9aebd/aws-tam-meet.png 480w,
/static/840ae843e4998e285be5747cb348dc87/a91f8/aws-tam-meet.png 960w,
/static/840ae843e4998e285be5747cb348dc87/e8464/aws-tam-meet.png 1536w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/840ae843e4998e285be5747cb348dc87/e8464/aws-tam-meet.png&quot; alt=&quot;AWS TAM 미팅 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;GPT 생성 이미지&lt;/figcaption&gt;
&lt;/center&gt;
&lt;p&gt;분석 결과를 바탕으로 &lt;strong&gt;AWS TAM&lt;/strong&gt;과 미팅을 진행했습니다. 개발 환경에서 성공적으로 재연한 과정, 상세한 브로커 로그 분석, 클러스터 상태 변화 추이, 그리고 AWS MQ 제약사항을 공유하면서 공식적인 해결책을 찾아나갔습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AWS 측 분석 결과&lt;/strong&gt;는 명확했습니다. &lt;strong&gt;&quot;이 현상은 RabbitMQ 3.11.28 버전에서 확인된 알려진 이슈(Known Issue)일 가능성이 높습니다. Classic Mirrored Queue의 메모리 관리와 동기화 로직에서 발생하는 구조적 한계로, 고부하 상황에서 브로커 재시작 시 더욱 자주 발생할 수 있습니다.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TAM에서 제시한 해결책은 &lt;strong&gt;2단계 접근법&lt;/strong&gt;이었습니다. 즉시 적용 가능한 해결책으로는 &lt;strong&gt;RabbitMQ 버전 업그레이드&lt;/strong&gt;를 통해 최신 버전에서 개선된 미러링 관련 동기화 로직과 메모리 관련 동작 안정성을 확보하는 것이었습니다.&lt;/p&gt;
&lt;p&gt;구조적 개선 방안으로는 &lt;strong&gt;Classic Mirrored Queue에서 Quorum Queue로의 전환&lt;/strong&gt;을 권고했습니다. Quorum Queue는 Raft consensus 알고리즘 기반으로 더 안정적인 동기화를 제공하고, 비정상 replica 자동 제거 및 재동기화 기능이 내장되어 있으며, 네트워크 분할(Split-brain) 상황에서의 강력한 복구 능력을 갖추고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;1차-개선-브로커-버전-업그레이드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EC%B0%A8-%EA%B0%9C%EC%84%A0-%EB%B8%8C%EB%A1%9C%EC%BB%A4-%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C&quot; aria-label=&quot;1차 개선 브로커 버전 업그레이드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1차 개선: 브로커 버전 업그레이드&lt;/h2&gt;
&lt;p&gt;AWS TAM의 권고에 따라 바로 &lt;strong&gt;RabbitMQ 브로커 버전을 3.11.28에서 3.12.14로 업그레이드&lt;/strong&gt;했습니다. 주요 개선사항으로는 메모리 알람 처리 로직 개선과 미러링 동기화 안정성 강화가 있었습니다.&lt;/p&gt;
&lt;p&gt;동일한 테스트 시나리오로 다시 검증해본 결과, &lt;strong&gt;unsynchronized 상태가 발생하지 않는 것을 확인&lt;/strong&gt;했습니다. 메모리 사용량이 임계점에 도달해도 동기화 상태가 안정적으로 유지되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;2차-개선-quorum-queue-전환-결정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EC%B0%A8-%EA%B0%9C%EC%84%A0-quorum-queue-%EC%A0%84%ED%99%98-%EA%B2%B0%EC%A0%95&quot; aria-label=&quot;2차 개선 quorum queue 전환 결정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2차 개선: Quorum Queue 전환 결정&lt;/h2&gt;
&lt;p&gt;버전 업그레이드로 당장의 위험은 해소되었지만, &lt;strong&gt;장기적인 안정성을 위해 Quorum Queue 전환을 결정&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;center class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 679px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d0e88871beb9e43548198b792be53d1a/a96cc/queue_compare.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 112.70833333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAAAsTAAALEwEAmpwYAAADNUlEQVR42o1Via6cMAzk//+uUqW2qvr2vcdyhCNc4Sbrzhh4XXpIRbJgY8cZjyfeIEpyabtBXD/KPc7k/Z5KiHfn9rVna7pe4rSQKDESRkaatpd+uMYEZzIm4AYG0Vw/yTDO2DCpOd04iMlb+fS1lFtYS5I7ibNe7sYB1Cgj4oOq6bBx0pPqxiEZnb3UeNuqFfq5zjXG5baVb7cSVVRi61YSYyWG5bbWWCB08ng8hE9eVvIaJlpOGBstn5SQiryoZF4WfBskHoX7WFWWW6C2enjnegnOZHzSrJTPX35oAJ09qFCO3M7VOM3yhkPe7kYicMkkNVAxNiuslFUjweb9R0IiNEhKJ43fBUrRbxyiJZe1Noabt83Lum36npdV1nW7ImQzMotmTF4at4obN11nDKP80+H/egJywFK4hSSHCUmu5I53lFpFXQAVEbHTJJ58EjHXKuyhn79VNiSWkImCzmmala+BNp76GvB7Ut/tPZbvL6E2jIqg1GpVQqfAPkrWsp7K/9tDjt6gApOXaIST0hJZIXGSoUG1HhD4/0h4+vzDo+xJ6m6Tpoe5TSp8d6MX24DzAU2pmxYlo1MgvG475SwDR5QQv7Wb8LEhy7oA0c5vmJR473YLoVPrZFqQMDGFLGg5EaSmlBaamuZF+VDtjbOcuBeUnKLcHDLigSn2xqZRkW/runeZm4iQpTMJ0XBtFzLucE+BD8faoFxp522j8W4gGNllhY/gd974k5tt7eQ1quU9ruUlrCTJKKlGItMhWYeOT7/2POuQkqBRh6ozSKdFtwoMgaIawctDOqAYZ69okqyBRHrdw1h21mvDdgsIn0akFCyHAXm9qxSaS5fZlMTAf1w9JuM99kfTaEGc5rgRufJoQDaFS9HqgAAK7x+7ISHvKycR/RTyedglIS87u8RL3uPyExVPJ4/P6GgrOklpnTP02fc4YgOq3R5ccDjwGj6ffE24aewZfx5OUGlmldOACyaz6mSZqi86geKC4EiYnTHQrHKN4XsHDRxpnAsBobNj4/E3wEWOew6CPxOuH0OBw4I63S/A/mZ1AU/jKeSGgeWBdG+Gvxj/AhIgI0qivXAse/xPLVnuPvtsnikAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d0e88871beb9e43548198b792be53d1a/263a4/queue_compare.webp 480w,
/static/d0e88871beb9e43548198b792be53d1a/6943c/queue_compare.webp 679w&quot; sizes=&quot;(max-width: 679px) 100vw, 679px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d0e88871beb9e43548198b792be53d1a/9aebd/queue_compare.png 480w,
/static/d0e88871beb9e43548198b792be53d1a/a96cc/queue_compare.png 679w&quot; sizes=&quot;(max-width: 679px) 100vw, 679px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d0e88871beb9e43548198b792be53d1a/a96cc/queue_compare.png&quot; alt=&quot;Classic Queue vs Quorum Queue 비교표&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Classic Mirrored Queue와 Quorum Queue의 핵심 차이점 비교 - 안정성과 복구 능력에서 Quorum Queue의 우위가 명확합니다&lt;/figcaption&gt;
&lt;/center&gt;
&lt;p&gt;&lt;strong&gt;Quorum Queue&lt;/strong&gt;는 &lt;strong&gt;Raft Consensus 알고리즘&lt;/strong&gt;을 기반으로 해서 Classic Mirrored Queue의 한계를 근본적으로 해결합니다. 리더(Leader) + 팔로워(Follower) 구성으로 과반수 합의를 통한 안정적인 메시지 처리가 가능합니다.&lt;/p&gt;
&lt;center class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1011px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1806a400682b6ddd1560fe9850677b11/0d97d/raft-flow.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.041666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3ElEQVR42mWR6QqEMAyE+/7P5w9BRAVv8b7P7H4Bl3U3ENvayUwmNfKOZVkkDEPpuk7GcZR5njWnaZK6rsXzPPF9XxzHkaqqZN93rTmOQ37D8GmaRgE3eVEUEkWRFsdxrGdIsixTAYT6vtd9WZYq/iDM81yJAKVpql1SdIsQwzCoQBAESgTmPE/FJUmiYvwzdGBZlti2rbZQpLtt2x5WKIaU0dxkxLquKoRLOjWAsESXzJDkkoLrunQlmRfJHSuCEOPqzzIBACuQt22rwNs6iRB2Xdf9zBRiRL8f5QXuVM5Sr7F71QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1806a400682b6ddd1560fe9850677b11/263a4/raft-flow.webp 480w,
/static/1806a400682b6ddd1560fe9850677b11/a6361/raft-flow.webp 960w,
/static/1806a400682b6ddd1560fe9850677b11/372ad/raft-flow.webp 1011w&quot; sizes=&quot;(max-width: 1011px) 100vw, 1011px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1806a400682b6ddd1560fe9850677b11/9aebd/raft-flow.png 480w,
/static/1806a400682b6ddd1560fe9850677b11/a91f8/raft-flow.png 960w,
/static/1806a400682b6ddd1560fe9850677b11/0d97d/raft-flow.png 1011w&quot; sizes=&quot;(max-width: 1011px) 100vw, 1011px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1806a400682b6ddd1560fe9850677b11/0d97d/raft-flow.png&quot; alt=&quot;Raft Consensus 알고리즘 기반 Quorum Queue 동작 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;  
&lt;figcaption&gt;리더(Leader) + 팔로워(Follower) 구성으로 과반수 합의를 통한 안정적인 메시지 처리&lt;/figcaption&gt;
&lt;/center&gt;
&lt;p&gt;&lt;strong&gt;RAFT Consensus 알고리즘의 핵심 장점&lt;/strong&gt;은 리더 노드 장애 시 즉시 새로운 리더 선출로 서비스 연속성을 보장하고, 메시지 커밋 시 과반수 노드의 승인으로 데이터 일관성을 확보하며, 장애 노드 복구 시 자동으로 클러스터에 재참여하고, 네트워크 분할 상황에서도 과반수 원칙으로 안정성을 유지한다는 것입니다.&lt;/p&gt;
&lt;p&gt;운영 관점에서는 &lt;strong&gt;수동 개입 없이 자동 장애 복구&lt;/strong&gt;가 가능하고, 복잡한 동기화 상태 관리가 불필요하며, 예측 가능한 성능과 안정성, 그리고 확장성과 내결함성의 균형을 제공합니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-전환-이후-성과와-개선-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A0%84%ED%99%98-%EC%9D%B4%ED%9B%84-%EC%84%B1%EA%B3%BC%EC%99%80-%EA%B0%9C%EC%84%A0-%EC%A7%80%ED%91%9C&quot; aria-label=&quot; 전환 이후 성과와 개선 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📊 전환 이후 성과와 개선 지표&lt;/h1&gt;
&lt;p&gt;전환 후 3개월간의 운영 결과를 보면 &lt;strong&gt;극적인 개선&lt;/strong&gt;을 확인할 수 있었습니다. 가장 눈에 띄는 변화는 장애 발생 빈도입니다. &lt;strong&gt;월 평균 2-3회 발생하던 장애가 전환 후 0회&lt;/strong&gt;로 완전히 줄어들었습니다.&lt;/p&gt;
&lt;p&gt;성능 면에서도 개선이 있었습니다. &lt;strong&gt;평균 메시지 처리 지연시간이 150ms에서 120ms로 20% 단축&lt;/strong&gt;되었고, 무엇보다 중요한 것은 대량 발급 상황에서의 안정성입니다. 이전에 문제가 되었던 &lt;strong&gt;1,500만 건 규모의 대량 쿠폰 발급을 무장애로 처리&lt;/strong&gt;할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;운영 관점에서도 큰 변화가 있었습니다. 이전에 &lt;strong&gt;월 평균 5-7건씩 발생하던 Memory Critical 알람이 0건&lt;/strong&gt;으로 현저히 줄어들어 운영 부담이 크게 감소했습니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;항목&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Classic Mirrored Queue&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Quorum Queue&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;개선율&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;메모리 사용 효율성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;높음 (단순 복제)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;중간 (로그 기반)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;-15%&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;동기화 안정성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;낮음 (단일 실패점)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;높음 (과반수 합의)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;+85%&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;장애 복구 시간&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;수동 개입 필요&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;자동 복구 (30초 이내)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;+90%&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;네트워크 분할 대응&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;취약&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;강력 (Split-brain 방지)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;+100%&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Quorum Queue 전환과 함께 &lt;strong&gt;포괄적인 모니터링 시스템&lt;/strong&gt;도 구축했습니다. CloudWatch 기반으로 메모리 사용률 알람(80% 이상 시 경고, 90% 이상 시 Critical), Queue Length 모니터링, Consumer Lag 추적, 클러스터 상태 감시를 실시간으로 진행하고 있습니다. SNS 기반 즉시 알림 체계를 통해 Slack 채널과 연동한 실시간 장애 알림, 심각도별 escalation 정책, 자동 복구 가능 여부 판단 로직도 포함했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-향후-개선-계획과-확장-방향&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%96%A5%ED%9B%84-%EA%B0%9C%EC%84%A0-%EA%B3%84%ED%9A%8D%EA%B3%BC-%ED%99%95%EC%9E%A5-%EB%B0%A9%ED%96%A5&quot; aria-label=&quot; 향후 개선 계획과 확장 방향 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚀 향후 개선 계획과 확장 방향&lt;/h1&gt;
&lt;center&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1835px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/97797bd0e4736fefe1fa996d13c7851b/2e51b/tobe_path2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABtElEQVR42pVTa2+DMAzk//+7fag2aaNZgZJAeLeUQh83n2mmado+NJKV0PjO53Ma4Y91u91wOp0wTRPGcdQz43A8ovQezjn0fY+u7/Aex3jdbGBtjvP5jIjg6/WK+/2uZNzneVaCQM7EEMuyaFwuF/1uuw55lmI6jYqL9vs9kiRBURQ4igKqOhwO6CSRQCaxCIPkjHCmkLZtkabpd8Eoz60SNk2tCVzceRnOoWVVKAXmR9COsvQw21jwzaqQ0seRgElixCgqnbVa1Xwa5HmOuq4VUFUVMrsKyLJMfLOgIN6VZbkqTHc7AX5KcgPvHbZSzfsKtOIj/kBunSaHtq+PVmlLkqzYRtqmRbQhomx+cB+GQcFBDX0lsTFG734OhRYQx2nT+29CkxhtgW2FadMGnhlMIojEWyHWVuXMghwgcyiGIoiNfOWVjBXC0/m9qIYAhqWnVY1COrHqn7QrBTltkkcB9B/Zz7v1SbH1WQQMeIvf8LJ5kSkbuMKp0ghPLHpIEG3g4j+lECJ63T28fJqQMc8Lhl6GZlNpvYKTt7g+vycV0stlmRXcdS128uSSREiLUn/j/Rfq/OqVnrxpyAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/97797bd0e4736fefe1fa996d13c7851b/263a4/tobe_path2.webp 480w,
/static/97797bd0e4736fefe1fa996d13c7851b/a6361/tobe_path2.webp 960w,
/static/97797bd0e4736fefe1fa996d13c7851b/42392/tobe_path2.webp 1835w&quot; sizes=&quot;(max-width: 1835px) 100vw, 1835px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/97797bd0e4736fefe1fa996d13c7851b/9aebd/tobe_path2.png 480w,
/static/97797bd0e4736fefe1fa996d13c7851b/a91f8/tobe_path2.png 960w,
/static/97797bd0e4736fefe1fa996d13c7851b/2e51b/tobe_path2.png 1835w&quot; sizes=&quot;(max-width: 1835px) 100vw, 1835px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/97797bd0e4736fefe1fa996d13c7851b/2e51b/tobe_path2.png&quot; alt=&quot;향후 계획&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/center&gt;
&lt;p&gt;단일 브로커 장애에 대비해서 &lt;strong&gt;이중화 구성으로 서비스 연속성을 확보&lt;/strong&gt;하는 것이 다음 목표입니다. Primary 브로커가 평상시 모든 트래픽을 처리하고, Secondary 브로커는 동일한 설정으로 standby 상태를 유지하면서 큐 설정 및 메시지 라우팅 규칙을 실시간으로 동기화합니다. 30초 간격으로 서로의 상태를 확인하는 Health Check도 구축할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Route 53 Private DNS를 활용한 무중단 전환&lt;/strong&gt;도 계획하고 있습니다. &lt;code class=&quot;language-text&quot;&gt;coupon-mq.internal.oliveyoung.com&lt;/code&gt;(예시) 같은 고정 도메인을 통해 장애를 감지하면 자동으로 Secondary 브로커로 라우팅을 변경하고, DNS 레코드 TTL을 30초로 설정해서 빠른 전환이 가능하도록 할 것입니다. Route 53 Health Check와 연동해서 자동 failover가 이루어지면 &lt;strong&gt;평균 전환 시간을 90초 이내&lt;/strong&gt;로 단축할 수 있을 것으로 예상합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;예방적 운영 체계 강화&lt;/strong&gt;를 위해 메모리 사용률 70% 도달 시 자동 인스턴스 증설 검토, Queue Length가 임계값을 초과하면 Consumer 자동 증설, 매일 오전 브로커 상태 및 성능 지표 자동 점검 등의 자동화된 장애 대응 시스템을 구축할 예정입니다. 상황별 단계별 대응 절차를 문서화한 장애 대응 플레이북, 장애 심각도별 담당자 및 대응 시간을 명시한 에스컬레이션 정책, 장애 복구 후 정상 동작 확인 항목을 정리한 복구 검증 체크리스트도 체계화할 계획입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;메시징 시스템 고도화 로드맵&lt;/strong&gt;으로는 단기적으로(3-6개월) 가용 영역 장애에 대비한 Cross-AZ 브로커 배치, 네트워크 대역폭 효율성 개선을 위한 메시지 압축 최적화, 처리량과 지연시간 균형점 탐색을 위한 Consumer 그룹 최적화, 실패 메시지 처리 및 재시도 로직 개선을 위한 Dead Letter Queue 고도화를 진행할 예정입니다.&lt;/p&gt;
&lt;p&gt;중기적으로는(6-12개월) 대용량 로그성 데이터 처리를 위한 &lt;strong&gt;RabbitMQ Streams 도입&lt;/strong&gt;, 쿠폰 상태 변경 이력 관리 고도화를 위한 &lt;strong&gt;Event Sourcing 패턴 적용&lt;/strong&gt;, 머신러닝을 활용한 트래픽 예측 및 자동 스케일링 등을 검토하고 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-결론과-인사이트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B2%B0%EB%A1%A0%EA%B3%BC-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8&quot; aria-label=&quot; 결론과 인사이트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💡 결론과 인사이트&lt;/h1&gt;
&lt;p&gt;이번 RabbitMQ 장애를 통해 &lt;strong&gt;대용량 메시징 시스템 운영의 복잡성과 중요성&lt;/strong&gt;을 다시 한 번 깊이 깨달았습니다. 특히 단순한 기술적 해결책을 넘어선 구조적 개선의 필요성과 예방적 접근의 중요성을 명확하게 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;서비스 안정성 지표&lt;/strong&gt;에서 &lt;strong&gt;장애 발생 빈도가 월 2-3회에서 0회로 100% 개선&lt;/strong&gt;되었고, &lt;strong&gt;평균 복구 시간이 2시간에서 90초로 98.7% 단축&lt;/strong&gt;되었습니다. 1,500만 건 무장애 처리를 달성했고, &lt;strong&gt;고객 만족도 측면에서도 VoC 건수가 80% 감소&lt;/strong&gt;(2,800건 → 560건/월)했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;운영 효율성&lt;/strong&gt;에서도 False Positive 알람이 90% 감소하고, 긴급 대응 인력이 50% 절감되었으며, 정기 점검 시간이 70% 단축되는 성과를 거두었습니다.&lt;/p&gt;
&lt;h2 id=&quot;핵심-인사이트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B5%EC%8B%AC-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8&quot; aria-label=&quot;핵심 인사이트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;핵심 인사이트&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;메모리 상태와 큐 동기화의 직접적 상관관계&lt;/strong&gt;를 확인했습니다. RabbitMQ Classic Mirrored Queue 환경에서 메모리 사용량이 임계점에 도달하면 &lt;strong&gt;미러 노드 간 동기화 실패 확률이 급격히 증가&lt;/strong&gt;합니다. 특히 고부하 상황에서 브로커 재시작은 문제를 악화시킬 수 있으므로, 메모리 기반 알람과 예방적 대응이 필수입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AWS MQ 관리형 서비스의 운영적 제약사항&lt;/strong&gt;도 명확해졌습니다. 관리형 서비스의 편의성과 안정성은 분명한 장점이지만, &lt;strong&gt;브로커 레벨의 세밀한 제어가 제한&lt;/strong&gt;됩니다. 장애 발생 시 수동 복구 옵션이 제한되므로, 구성 단계에서부터 안정성을 확보하는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Queue Type 선택의 중요성&lt;/strong&gt;도 재확인했습니다. Classic Mirrored Queue는 단순하고 메모리 효율적이지만 동기화 실패에 취약합니다. &lt;strong&gt;Quorum Queue는 메모리 사용량이 다소 높지만 Raft 합의 알고리즘을 통한 강력한 내결함성&lt;/strong&gt;을 제공합니다. 비즈니스 중요도와 트래픽 패턴을 고려한 신중한 선택이 필요합니다.&lt;/p&gt;
&lt;h2 id=&quot;대용량-메시징-시스템-운영-가이드라인&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%A9%94%EC%8B%9C%EC%A7%95-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9A%B4%EC%98%81-%EA%B0%80%EC%9D%B4%EB%93%9C%EB%9D%BC%EC%9D%B8&quot; aria-label=&quot;대용량 메시징 시스템 운영 가이드라인 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;대용량 메시징 시스템 운영 가이드라인&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;사전 설계 단계&lt;/strong&gt;에서는 트래픽 패턴과 안정성 요구사항을 고려한 적절한 Queue Type 선택, 평상시 사용량의 2-3배 메모리 할당을 통한 메모리 여유도 확보, Single Point of Failure 제거를 위한 브로커 이중화 구성, 예방적 알람과 자동 대응 로직을 포함한 모니터링 체계 구축이 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;운영 단계&lt;/strong&gt;에서는 대량 처리 시 단계적 부하 적용을 통한 점진적 부하 증가, 메모리, CPU, 네트워크 사용률 추적을 위한 실시간 성능 모니터링, 운영 환경과 동일한 조건의 부하 테스트를 통한 정기적 성능 테스트, 정기적인 DR(Disaster Recovery) 테스트 실시를 통한 장애 대응 훈련이 중요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장애 대응 단계&lt;/strong&gt;에서는 임시 해결책보다 원인 규명에 집중하는 근본 원인 분석 우선, 개발 환경에서의 정확한 재현으로 해결책을 검증하는 체계적 재현 시도, AWS TAM, 커뮤니티 등 외부 전문 리소스를 적극 활용하는 전문가 협업, 단순 복구를 넘어선 근본적 개선 방안 수립이 필요합니다.&lt;/p&gt;
&lt;p&gt;바람 잘 날 없는 쿠폰 도메인이지만... 🤯 이런 어려움들이 더 견고한 시스템을 만드는 밑거름이 되고 있습니다. 모든 커머스의 쿠폰 담당자분들께 진심으로 리스펙을 보내며, &lt;strong&gt;올리브영 쿠폰팀은 앞으로도 더욱 안정적이고 효율적인 시스템 구축을 위해 지속적으로 노력하겠습니다&lt;/strong&gt;.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;️-올영-쿠폰-개선-일지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%98%AC%EC%98%81-%EC%BF%A0%ED%8F%B0-%EA%B0%9C%EC%84%A0-%EC%9D%BC%EC%A7%80&quot; aria-label=&quot;️ 올영 쿠폰 개선 일지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⚙️ 올영 쿠폰 개선 일지&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2022-09-28/Oliveyoung-Coupon-Count-Moving-Redis/&quot;&gt;올리브영 쿠폰 발급 개선 이야기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-08-07/async-process-of-coupon-issuance-using-redis/&quot;&gt;Redis Pub/Sub을 활용한 쿠폰 발급 비동기 처리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-09-18/oliveyoung-coupon-rabbit/&quot;&gt;쿠폰 발급 RabbitMQ도입기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-12-11/oliveyoung-coupon-mess-issue/&quot;&gt;올리브영 초대량 쿠폰 발급 시스템 개선기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;️-관련-시리즈-예고&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EA%B4%80%EB%A0%A8-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EC%98%88%EA%B3%A0&quot; aria-label=&quot;️ 관련 시리즈 예고 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🗓️ 관련 시리즈 예고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&quot;올리브영 쿠폰 시스템 아키텍처 Deep Dive&quot; (언젠가 다시 만나겠습니다..🥲)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다! 궁금한 점이나 경험 공유는 언제든 환영합니다. 💪&lt;/p&gt;</content:encoded></item><item><title><![CDATA[KPT 회고, 이렇게 했더니 스쿼드 문화가 바뀌었습니다: 올리브영 주문결제 스쿼드의 애자일 성장기]]></title><description><![CDATA[안녕하세요! 트랜잭션서비스개발팀 주문결제 스쿼드의 따끈따끈한 입사자 써니입니다! 지난…]]></description><link>https://oliveyoung.tech/2025-10-17/review-of-orderpay-squad/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-10-17/review-of-orderpay-squad/</guid><pubDate>Fri, 17 Oct 2025 14:00:00 GMT</pubDate><content:encoded>&lt;br&gt;
&lt;p&gt;안녕하세요! 트랜잭션서비스개발팀 주문결제 스쿼드의 따끈따끈한 입사자 써니입니다!&lt;/p&gt;
&lt;p&gt;지난 1분기, 올리브영 주문결제 시스템은 급변하는 고객 요구와 복잡한 운영 이슈 속에서 분주한 시간을 보냈습니다.&lt;/p&gt;
&lt;p&gt;간편 결제 연동, 장바구니 로직 최적화 등 굵직한 프로젝트들을 성공적으로 완수했지만, 스쿼드 내부적으로는 &apos;휴먼 에러&apos;로 인한 잦은 장애와 개선되지 않는 스쿼드 문화라는 고민을 안고 있었습니다.&lt;/p&gt;
&lt;p&gt;특히 도메인 규모가 크고 복잡한 주문결제 영역 특성상, 스쿼드원들이 각자의 업무에만 몰입하기 쉬웠고, 그 과정에서 코드 리뷰가 형식적이 되거나, 사소한 실수가 큰 장애로 이어지는 경우가 종종 발생했습니다. 바쁜 일정 속에서 스쿼드원들의 피로도는 누적되었고, 단순히 &apos;회상&apos;에 그치는 기존의 회고 방식으로는 이러한 근본적인 문제들을 해결하기 어렵다는 것을 깨달았습니다.&lt;/p&gt;
&lt;div style=&quot;width: 80%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 792px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/acf4939f6847c4c932ec4747a4d791ba/59f54/oypgchallenge.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB40lEQVR42rVUaW/iMBDl//+7/UAVlaN0CSZxDuI4PhLHeTt2gLaoQizbtTTKxB6/eXN4FvjhtbjdmKYpftu2xXq9xmq1QpqmUd/tdkiSBHVdf7G9C3hZzjlIKaNYa9F1HZRS8TsMw+MMfzzk/wo4jiOMMRTqLEF3tPc0oBAtGJfYcxOFFTIW63nARoDzHPWpQllW4DknJ+IJwMmTjOikQMF/I88ZjlkW26cVf8vQO0qejaq1igB3YOyIjGcUfgalNXzw+RBgYDboq7kbJPLsnQAzHBlDWhxhBhtPB/8I4NjPEojSrUZ1yHkaARkB7nkBrtzsjM6H6Q5gPFOcrMz5WQFSE8NGgdUjmtYhpbY5hPMzkPGz4+8Z+hGTzDH16lMKWgI210vSW5SjhPNzL/YEaP0t4PmB66aAPCToiv1HwkdGoOL6L5zGwdbozkULIZ/c1wJdGVZVje3bO7VHHl9IJDgWVKcm6kVZYvX6iu16g+1mE4dGGBEVVaenEC5RREDvPfVaTn22pzbh0MZSBhSU+AUtlyE4aG1ik5dVSc4r9DRxEhGkx5scIJ2/Pw/r+oTl8iXOwb7vvx1vjB3IropkLk9ycQv0MRgcGYk4/64pIJvPEiILEs4v9xdhIzAIEvR/HV9/AJF3IeDh8dX2AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/acf4939f6847c4c932ec4747a4d791ba/263a4/oypgchallenge.webp 480w,
/static/acf4939f6847c4c932ec4747a4d791ba/60a83/oypgchallenge.webp 792w&quot; sizes=&quot;(max-width: 792px) 100vw, 792px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/acf4939f6847c4c932ec4747a4d791ba/9aebd/oypgchallenge.png 480w,
/static/acf4939f6847c4c932ec4747a4d791ba/59f54/oypgchallenge.png 792w&quot; sizes=&quot;(max-width: 792px) 100vw, 792px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/acf4939f6847c4c932ec4747a4d791ba/59f54/oypgchallenge.png&quot; alt=&quot;oypgchallenge&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
                이 이미지는 Napkin으로 생성되었습니다.&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;올리브영-주문결제-스쿼드-kpt를-만나다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%A3%BC%EB%AC%B8%EA%B2%B0%EC%A0%9C-%EC%8A%A4%EC%BF%BC%EB%93%9C-kpt%EB%A5%BC-%EB%A7%8C%EB%82%98%EB%8B%A4&quot; aria-label=&quot;올리브영 주문결제 스쿼드 kpt를 만나다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영 주문결제 스쿼드, KPT를 만나다&lt;/h3&gt;
&lt;p&gt;우리는 이 문제들을 직시하고, 지속 가능한 성장을 위한 해결책을 모색했습니다. 단순히 &apos;회상&apos;에 그치는 회고가 아닌, 구체적인 실행 과제를 도출하는 강력한 도구가 필요했죠.&lt;/p&gt;
&lt;p&gt;그 해답으로 우리는 KPT(Keep, Problem, Try) 회고를 선택했습니다.&lt;/p&gt;
&lt;p&gt;KPT는 &apos;Keep(지속할 점)&apos;, &apos;Problem(개선할 점)&apos;, &apos;Try(시도할 점)&apos;이라는 단순한 3단계 구조를 가집니다. 이 패턴은 복잡한 도메인 속에서 스쿼드의 경험을 명확히 정리하고, 히스토리 기반의 인사이트를 담아 실질적인 개선으로 이어지게 합니다.&lt;/p&gt;
&lt;br&gt;
우리 스쿼드는 &apos;더 빠르게, 더 안전하게, 더 편리하게&apos;라는 명확한 비전을 바탕으로, 회고를 진행하기전 다음과 같은 4가지 원칙을 도입해 보았습니다.
&lt;div style=&quot;width: 80%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 768px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/55c9e878ef16edf096e39d9641a37913/fe486/KPTprinciple.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABvUlEQVR42q1TzY7TMBjsY/BK+yocOPAI+wIckDgh7ogD4sCPxJGVqLTABZpukzRurTZp0qSx0580/hscB0KrrsQBIjn5xt94PJk4A/zna3DfZFmWSNMUjDFsNhvkee5qznmPW85fBI0bxhgQQjD2PND53NWTyQSz2QzLxQJhEPS4X3afoNWB0ud9c8m/6Et71+eCHSVLviHynmHLY4e3FQf59BKr8Wdo07Ho1y+gH97jUFWOU61LxG+/Y+0v+x0GMMrVSfQE45sHSOhHh3lGMX96BfL6Go3UzkX84jno40fIo8hx2GiJ5OE7kFe3nS1z4lAcUyv2BofDtrefj29Q0EmP6yRGPBxCCNkZshmthgFYUpw47DM0lniShlZohIK2T5huXmkNodRZwI0Utq0vM2zqxGZzi+a47nAWQKwjNHaoXedApCtIOoeuDw4faQlBOcSC2zn55yu37uJ4Ad/3UBSd4CpeYkam8O/usCkKVNUWZDpFFIYgNsO6rkHCCKEfwPsxcufTCbZi7ZBSomkaV7vAGbebJEizzL66sHEI7HY7MHu49/s9lH31Fme2zzhz651g2xC/FvwW+5df7ydnyJrKBwr/GAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/55c9e878ef16edf096e39d9641a37913/263a4/KPTprinciple.webp 480w,
/static/55c9e878ef16edf096e39d9641a37913/dc9b9/KPTprinciple.webp 768w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/55c9e878ef16edf096e39d9641a37913/9aebd/KPTprinciple.png 480w,
/static/55c9e878ef16edf096e39d9641a37913/fe486/KPTprinciple.png 768w&quot; sizes=&quot;(max-width: 768px) 100vw, 768px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/55c9e878ef16edf096e39d9641a37913/fe486/KPTprinciple.png&quot; alt=&quot;KPTprinciple&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
            이 이미지는 Napkin으로 생성되었습니다.&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;1. 회고 주제 사전 공유: 깊이 있는 논의를 준비하기&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;회고 3일 전, 스쿼드원들에게 가볍게 생각해볼 수 있는 질문을 미리 공유하여 깊이 있는 논의를 유도했습니다. 이는 각자가 지난 분기를 충분히 돌아볼 시간을 제공하여, 회고 당일 더욱 풍성한 인사이트가 나올 수 있는 기반을 마련했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 시니어-주니어 페어링 디스커션: 침묵을 깨고 핵심 문제 파고들기&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;단순히 의견을 모으는 것을 넘어, 우리는 시니어와 주니어 개발자가 한 팀을 이루어 주문결제 고객 여정 속 숨어있는 문제점을 각자의 시각에서 정의하고 끈질기게 고민하는 디스커션 세션을 진행했습니다. 초기에는 주니어 개발자들이 시니어 앞에서 의견을 내는 것을 부담스러워하는 경향이 있었습니다.&lt;/p&gt;
&lt;p&gt;이러한 장벽을 해소하기 위해, 우리는 &apos;답을 찾는 것보다 질문을 던지는 것이 중요하다&apos;는 원칙을 강조하고, 모든 의견이 존중받는다는 분위기를 조성하는 데 집중했습니다. 예를 들어, 한 주니어 개발자는 &quot;레거시 코드 때문에 신기능 구현에 항상 시간이 더 걸린다&quot;는 문제를 제기했고, 이에 시니어 개발자는 해당 레거시 코드가 과거 어떤 배경으로 만들어졌는지 설명하며 함께 개선 방안을 모색하는 의미 있는 대화가 오고 갔습니다. 이는 주니어의 시각에서 놓칠 수 있는 문제점을 발굴하고, 시니어의 경험으로 현실적인 해결책을 찾는 시너지로 이어졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 타임 박싱으로 효율 높이기: 핵심에 집중하는 기술&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;주제별 발표와 논의 시간을 엄격하게 제한하는 &lt;strong&gt;타임 박싱&lt;/strong&gt;을 적용해 회고의 효율성을 극대화했습니다. 초기에는 시간을 초과하는 경우가 잦았지만, 퍼실리테이터의 능숙한 개입과 스쿼드원들의 적극적인 참여로 점차 핵심에 집중하는 문화가 정착될 수 있었습니다. 이 덕분에 우리는 불필요한 논쟁을 줄이고, 가장 중요한 문제와 해결책에 대한 깊이 있는 토론에 집중할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. &quot;이번 분기엔 이것 하나만!&quot; 집중할 항목 선정: 모두의 실행력을 높이는 전략&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KPT 회고를 통해 개선할 점(Problem)은 항상 많았지만, 모든 것을 한 번에 해결하려는 욕심은 오히려 스쿼드의 실행력을 저해한다는 것을 경험했습니다. 그래서 우리는 &lt;strong&gt;이번 분기엔 가장 임팩트 있는 것 하나만!&lt;/strong&gt; 이라는 원칙을 세웠습니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 지난 회고에서는 장바구니/주문 영역의 복잡한 로직으로 인한 로딩 시간 증대 및 DB 커넥션풀 과다 사용 문제가 큰 &apos;Problem&apos;으로 지목되었습니다. 처음에는 &apos;전면적인 시스템 개편&apos;과 같은 큰 목표가 나왔지만, 이를 쪼개어 가장 시급하고 파급력이 큰 &lt;strong&gt;장바구니 캐싱 도입&lt;/strong&gt;을 첫 번째 &apos;Try&apos; 항목으로 선정했습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;kpt-회고로-얻은-놀라운-변화들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kpt-%ED%9A%8C%EA%B3%A0%EB%A1%9C-%EC%96%BB%EC%9D%80-%EB%86%80%EB%9D%BC%EC%9A%B4-%EB%B3%80%ED%99%94%EB%93%A4&quot; aria-label=&quot;kpt 회고로 얻은 놀라운 변화들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;KPT 회고로 얻은 놀라운 변화들&lt;/h2&gt;
&lt;h3 id=&quot;우리는-앞으로-무엇을-해야하는가-어떻게-달성할수-있는가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%95%9E%EC%9C%BC%EB%A1%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94%EA%B0%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%AC%EC%84%B1%ED%95%A0%EC%88%98-%EC%9E%88%EB%8A%94%EA%B0%80&quot; aria-label=&quot;우리는 앞으로 무엇을 해야하는가 어떻게 달성할수 있는가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;우리는 앞으로 무엇을 해야하는가? 어떻게 달성할수 있는가?&lt;/h3&gt;
&lt;div style=&quot;width: 80%; margin: 0 auto; text-align: center;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1536px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9eb4de98f6fdd84d8e0f62eefae14551/e8464/order_payment_KPT.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAADHElEQVR42iWS609SAQDF73/Sem45zbZqrVZbzuZaufpQs8JK23wQmi7TiWjiMxCfiAoiioIoFxUEEbnA5XGvwL0qoDYfICUKvjCniYBYm2Ftvw/nyznnwzlAyIf/J+jFj31Te+5JRD+6aIPGlaBMNiASC3BEweri8gQ9P1fQsBcLerGQF/8PcOb8p4Lr2MnG1OaSKa24KrusFoFluVTay0/lTG7n59r6Rg474sMCq+aAx3y0ZgmuWUNeDDizRfPWsWjKn60Zvwt9/rEsmUjGTPIMSk0nnxf2YfVsNqSSnO7Zwxszka0ott87s+Fo85nNix37MP8yvLOoXZ/XwNCIWS8VDvAHJKI9lzG4impUoGvOEPhuMvCploGv2CANFVb5FzTRZmx3CbaqBZ55eNdt9czqdp360wNHr0T0rKI9sRK8V8RGNODeN+VYE2mM/h6XMmF+xVjjhwO3CTjewF24fG5SHtpe9LuwbTc+DQ+6zMP0Lk4mjRpXoT6Xy++XCJ0sgpV4wVZ4HSJd6yVcWlJzItt2ILhmQYcbHAZJeGfBrhn0r1hNKpFFzk5hiC/m8G7ltV7J54kFTE8vCcu5vES5ieTFc9/E2OXN4Y1pYH/FMER/iyp6Fu0Go1Y6qVeoRnoJZNpDqvBVB5zKRa8WCChUsqHmhYhwHiLFKbJie9JiFA3vzqbadyNadq64/YtOwppXNjnAUlzwCWzMbGptqG5pqaMXU8tymB8SdF8SxgrvgFnXZMR4cVaslkUMRZsjmzPq7lIK4Qa7IKmf8gikJCork0Yrn6hYORAjpS//troiAWE8VtOfdpc8rc9+0E66z8q+a5E2RzcDTrZm9EPMDsrr7vqiOWSkr7UcbCtFIfFzUkEBtZxckg91kTnUdH5Rsqw2XUTLUAnrzAqOTSs49uFA9C7LqHiQQdSKGPgEH1N2btuG7aahNh6npYsr7GnxO6S6fpqqMWO6r2SoOtUq71iflnodyiOPGfj3NYtvdvyX27jvhEMe5NBt+OXURn7ojpwTh8sTATdsV7YvGwXBNbN/ATpYMQZWJ0Ne65HH/BePJxGTriJYlwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9eb4de98f6fdd84d8e0f62eefae14551/263a4/order_payment_KPT.webp 480w,
/static/9eb4de98f6fdd84d8e0f62eefae14551/a6361/order_payment_KPT.webp 960w,
/static/9eb4de98f6fdd84d8e0f62eefae14551/f3efb/order_payment_KPT.webp 1536w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9eb4de98f6fdd84d8e0f62eefae14551/9aebd/order_payment_KPT.png 480w,
/static/9eb4de98f6fdd84d8e0f62eefae14551/a91f8/order_payment_KPT.png 960w,
/static/9eb4de98f6fdd84d8e0f62eefae14551/e8464/order_payment_KPT.png 1536w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9eb4de98f6fdd84d8e0f62eefae14551/e8464/order_payment_KPT.png&quot; alt=&quot;order payment KPT&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
이 이미지는 GPT-5로 생성되었습니다.&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;이번 1분기 회고 워크숍에서는 이전의 워크숍과는 달리 조금 특별한 시도를 도입했습니다.&lt;/p&gt;
&lt;p&gt;시니어와 주니어 개발자가 한 팀을 이루어, &lt;strong&gt;주문결제 고객 여정 속 숨어있는 문제점&lt;/strong&gt;을 각자의 시각에서 정의하고,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;무엇이 문제인지 (What)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;왜 그것이 문제인지 (Why)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;어떻게 해결할 수 있을지 (How)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;함께 끈질기게 고민하는 &lt;strong&gt;디스커션 세션&lt;/strong&gt;을 진행했습니다.&lt;/p&gt;
&lt;p&gt;이렇게 머리를 맞대어 도출된 소중한 액션 아이템들은 다음 분기 실행의 밑거름이 되었고, 실제로 2분기에는 눈에 보이는 변화로 이어졌습니다.&lt;/p&gt;
&lt;h2 id=&quot;-kpt-회고로-얻은-놀라운-변화들-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-kpt-%ED%9A%8C%EA%B3%A0%EB%A1%9C-%EC%96%BB%EC%9D%80-%EB%86%80%EB%9D%BC%EC%9A%B4-%EB%B3%80%ED%99%94%EB%93%A4-&quot; aria-label=&quot; kpt 회고로 얻은 놀라운 변화들  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🌟 KPT 회고로 얻은 놀라운 변화들 🌟&lt;/h2&gt;
&lt;div style=&quot;display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;&quot;&gt;
  &lt;div style=&quot;background:#f8f9fa; padding:20px; border-radius:12px; box-shadow:0 2px 6px rgba(0,0,0,0.1);&quot;&gt;
    &lt;h3&gt;🛠 코드 품질&lt;/h3&gt;
    &lt;p&gt;&lt;b&gt;Problem:&lt;/b&gt; &apos;휴먼 에러&apos;로 인한 인시던트 발생&lt;/p&gt;
    &lt;p&gt;&lt;b&gt;Try:&lt;/b&gt; PR 템플릿 정의 및 코드 리뷰 활성화&lt;/p&gt;
    &lt;p&gt;&lt;b&gt;Result:&lt;/b&gt; 코드리뷰 퀄리티 향상 → 코드 이해도 증대 및 실수 감소&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#f8f9fa; padding:20px; border-radius:12px; box-shadow:0 2px 6px rgba(0,0,0,0.1);&quot;&gt;
    &lt;h3&gt;🤝 스쿼드 문화&lt;/h3&gt;
    &lt;p&gt;&lt;b&gt;Problem:&lt;/b&gt; 과중한 업무로 인한 피로 누적 및 소통 부재&lt;/p&gt;
    &lt;p&gt;&lt;b&gt;Try:&lt;/b&gt; 데일리스크럼 문화 정착&lt;/p&gt;
    &lt;p&gt;&lt;b&gt;Result:&lt;/b&gt; 업무 가시성 증대 &amp; 협업 만족도 상승&lt;/p&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#f8f9fa; padding:20px; border-radius:12px; box-shadow:0 2px 6px rgba(0,0,0,0.1);&quot;&gt;
    &lt;h3&gt;💳 서비스 경험&lt;/h3&gt;
    &lt;p&gt;&lt;b&gt;Problem:&lt;/b&gt; 결제 직전 복잡한 코드로 쿼리 과다 호출/에러 증가&lt;/p&gt;
    &lt;p&gt;&lt;b&gt;Try:&lt;/b&gt; 장바구니/주문 영역 전면 개편 논의 및 초기 개선&lt;/p&gt;
    &lt;p&gt;&lt;b&gt;Result:&lt;/b&gt; 장바구니 캐싱 도입으로 DB 부하 감소&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt; 
&lt;h3 id=&quot;pr-템플릿의-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pr-%ED%85%9C%ED%94%8C%EB%A6%BF%EC%9D%98-%EB%8F%84%EC%9E%85&quot; aria-label=&quot;pr 템플릿의 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;PR 템플릿의 도입&lt;/h3&gt;
&lt;p&gt;저번 분기에는 &apos;휴먼 에러&apos;로 인한 인시던트가 발생했습니다. 코드 리뷰를 진행했음에도, 핵심 변경 사항이 명확하게 공유되지 않아 문제가 발생했습니다. 따라서 &apos;코드 리뷰를 열심히 하자&apos;는 의지뿐만 아니라, 체계적인 PR 템플릿을 정의하여 코드 리뷰를 더 쉽고 효과적으로 만들자는 방안을 논의했습니다.&lt;/p&gt;
&lt;p&gt;새로운 PR 템플릿은 코드 리뷰의 비효율을 해결하기 위해 다음과 같은 필수 항목들을 담았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;    ## 작업명 / JIRA
    [CJOYPG-0000] 결제 실패 시 재시도가 안 되는 버그 수정 
    ## 변경 요약 (1~3줄)
    - 실패 시 즉시 종료 → 최대 3회 재시도 후 실패 처리 로직으로 변경
    
    ## 영향 범위 ⚠️
    - 결제 로직의 실패 부분 (OrderServiceImpl A메서드)
    - 주문 상태 변경 로직
    
    ## 리뷰어가 꼭 봐야 할 부분 👀 (선택)
    - PaymentRetryHandler.java 45-47 라인: 재시도 로직
    - 레거시 호환을 위해 A방법 대신 B방법 사용
    
    ## 특이사항 (선택)
    - 공통코드 ORD032에 88 추가
    
    [파일 10개 이상 또는 ±400라인 이상 변경 시]
    - 상세 설명
    - 로직 설명 또는 관련 문서 링크
    - 플로우차트나 다이어그램 링크&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 템플릿은 리뷰어가 &lt;strong&gt;변경 사항의 의도를 빠르게 파악하고&lt;/strong&gt;, &lt;strong&gt;영향 범위를 정확히 예측하며&lt;/strong&gt;, &lt;strong&gt;중요하게 검토할 지점을 놓치지 않도록&lt;/strong&gt; 도왔습니다. 특히 &apos;리뷰어가 꼭 봐야 할 부분&apos; 항목은 핵심 로직에 대한 집중적인 검토를 유도하여, 과거에 놓쳤던 잠재적 버그나 비효율적인 코드를 사전에 발견하는 데 결정적인 역할을 했습니다.&lt;/p&gt;
&lt;p&gt;PR 템플릿 도입 후 코드 리뷰의 퀄리티가 눈에 띄게 좋아졌습니다. 핵심 변경 사항에 대한 이해도가 증대되었고, 이번 분기에는 휴먼에러의 90% 이상이 배포 전 조기 발견되는 성과를 이루었습니다.&lt;/p&gt;
&lt;h3 id=&quot;데일리-스크럼-문화-정착&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%BC%EB%A6%AC-%EC%8A%A4%ED%81%AC%EB%9F%BC-%EB%AC%B8%ED%99%94-%EC%A0%95%EC%B0%A9&quot; aria-label=&quot;데일리 스크럼 문화 정착 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데일리 스크럼 문화 정착&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;그러나 PR 템플릿만으로 모든 문제가 해결되지는 않았습니다.&lt;/strong&gt;
바쁜 일정 속에서 스쿼드원들의 피로도가 누적되고, 서로의 업무 상황을 파악하기 어려워 소통이 부족해지는 문제가 있었습니다. 우리는 &apos;서로 잘 도와줄 수 있는 환경을 만들자&apos;는 목표 아래, Slack 내에 데일리 스크럼 봇을 도입하여 매일 아침 간단하게 업무 진행 상황을 공유하는 문화를 정착시켰습니다.&lt;/p&gt;
&lt;p&gt;도입 이후 업무의 가시성이 크게 향상되었고, 여러 커뮤니케이션을 하나의 스레드에서 통합 관리하면서 협업 비용이 줄었습니다. 서로의 업무를 더 잘 이해하게 되면서 문제 발생 시 빠르게 해결책을 찾고 부담을 나눌 수 있게 되었습니다.
중간 점검 설문에서도 전원 긍정적인 평가를 주었고, 이후 데일리 스크럼은 스쿼드의 자연스러운 문화로 자리잡았습니다.&lt;/p&gt;
&lt;div style=&quot;width: 100%; text-align: center;&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4262ea18ff94a095a518c8612c1fdf8a/46dcf/dailyscrum.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 24.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAk0lEQVR42p3OQQ6CMBSEYe5/L+PGxAWiCyVSRNpCQosUGivyS7wAhJfMbubLi+LjnktyIE1vZJlAa421lq0XmaYiz64zdkcIgdIS0xjavmUcP3NGvtO0Huw6R1E8kaWklBKlFHVVU+qSdv7UvAwuuH95YhmOvPfkj4LGmlWDRdBWOVVy5hTvkNYx+DfD0BNC2AT+AHHThaNP9WfMAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4262ea18ff94a095a518c8612c1fdf8a/263a4/dailyscrum.webp 480w,
/static/4262ea18ff94a095a518c8612c1fdf8a/a6361/dailyscrum.webp 960w,
/static/4262ea18ff94a095a518c8612c1fdf8a/0b34d/dailyscrum.webp 1920w,
/static/4262ea18ff94a095a518c8612c1fdf8a/63696/dailyscrum.webp 1948w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4262ea18ff94a095a518c8612c1fdf8a/9aebd/dailyscrum.png 480w,
/static/4262ea18ff94a095a518c8612c1fdf8a/a91f8/dailyscrum.png 960w,
/static/4262ea18ff94a095a518c8612c1fdf8a/ac7a9/dailyscrum.png 1920w,
/static/4262ea18ff94a095a518c8612c1fdf8a/46dcf/dailyscrum.png 1948w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4262ea18ff94a095a518c8612c1fdf8a/ac7a9/dailyscrum.png&quot; alt=&quot;데일리 스크럼 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
            우리의 데일리 스크럼&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;장바구니-캐싱-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EB%B0%94%EA%B5%AC%EB%8B%88-%EC%BA%90%EC%8B%B1-%EB%8F%84%EC%9E%85&quot; aria-label=&quot;장바구니 캐싱 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;장바구니 캐싱 도입&lt;/h3&gt;
&lt;p&gt;PR 템플릿과 데일리 스크럼을 통해 협업 방식은 개선되었지만, 기술적인 문제 역시 남아 있었습니다.
장바구니/주문 영역의 복잡한 로직으로 인해 로딩 시간 증가와 DB 커넥션 풀 과다 사용 문제가 심각했기 때문입니다. 전면 개편 논의 끝에, 가장 시급하고 효과가 큰 첫 단계로 장바구니 캐싱을 도입했습니다.&lt;/p&gt;
&lt;p&gt;그 결과 장바구니 조회 쿼리 사용량이 54% 이상 감소하며 성능 개선 효과를 입증했습니다. 특히 9월 세일 기간에도 DB 커넥션풀 부하가 완화되어 안정적인 운영이 가능했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 80%; margin: 0 auto; text-align: center;&quot;&gt;
  &lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1318px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e3912e8b6084b21727538f92250691ee/90694/caching.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.37499999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABN0lEQVR42q1S7VIDIQzs+79j1XF0bOtxB8c3CayBq636Q+1oZpaEu7DZBHZKLXh4fMLL8bThcELOBd1qa2g3oNaKXUwJq7NIKV/Qf7QbyS6EfWFm/MXaWHiQ/gthraKQFyEk7FgI6SfC9jnuSrplGZe1TmCxHJ9ROW8Kien7+eAaA+1CaMyK/f4OFCzU9Irg5l+03M/W65ZLFWUF3nsEISpxRUkeuSRodeqEDSlGeG1glIKTqmG1gs2becEi1ZdJjVirCWY6IPgFpTgk57ZCIkrPQtjlx+jgXE+QisWfYccBoiDJHvQe14Q+dy5CcB9AsW5NyGKd3hSSJGRBYgHx5s/IX0HnHCJEab37LOqy+CJ3MQi1MUg5iYIiakhmE2Ck5R6PbwKttTz6+CHHY7Wr3CyPPXMZbb8BFz5gqLjrt58AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e3912e8b6084b21727538f92250691ee/263a4/caching.webp 480w,
/static/e3912e8b6084b21727538f92250691ee/a6361/caching.webp 960w,
/static/e3912e8b6084b21727538f92250691ee/ccb1e/caching.webp 1318w&quot; sizes=&quot;(max-width: 1318px) 100vw, 1318px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e3912e8b6084b21727538f92250691ee/9aebd/caching.png 480w,
/static/e3912e8b6084b21727538f92250691ee/a91f8/caching.png 960w,
/static/e3912e8b6084b21727538f92250691ee/90694/caching.png 1318w&quot; sizes=&quot;(max-width: 1318px) 100vw, 1318px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e3912e8b6084b21727538f92250691ee/90694/caching.png&quot; alt=&quot;caching&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;margin-top: 8px; font-size: 14px; color: #555;&quot;&gt;
      장바구니 캐싱으로 경량화된 쿼리와 함께 우상향하는 9월 세일 DB 커넥션풀!
    &lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/div&gt;
&lt;h3 id=&quot;마무리-3개월-후-우리가-깨달은-것들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC-3%EA%B0%9C%EC%9B%94-%ED%9B%84-%EC%9A%B0%EB%A6%AC%EA%B0%80-%EA%B9%A8%EB%8B%AC%EC%9D%80-%EA%B2%83%EB%93%A4&quot; aria-label=&quot;마무리 3개월 후 우리가 깨달은 것들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리: 3개월 후, 우리가 깨달은 것들&lt;/h3&gt;
&lt;p&gt;KPT 회고를 시작하기 전엔 &quot;회고에 시간만 낭비하는 건 아닐까?&quot;, &quot;바쁜데 또 하나의 루틴만 추가되는 건 아닐까?&quot; 하고 걱정했던 게 사실입니다. 하지만 3개월을 돌아보니 예상보다 훨씬 실용적이고 의미 있는 변화들이 많았다는 것을 깨달았습니다.
가장 크게 느낀 변화는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장애 대응 시간이 확실히 줄었습니다.&lt;/strong&gt; 새로 도입한 PR 템플릿 덕분에 코드 리뷰가 꼼꼼해지면서 배포 전 휴먼 에러의 90% 이상을 조기에 잡아내는 경우가 늘었습니다. 결과적으로 쿼리 과다 호출로 인한 DB 부하도 54% 이상 감소하면서 안정적인 서비스 운영에 큰 도움이 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;이거 누가 알고 있지?&quot;라는 말이 줄어들었습니다.&lt;/strong&gt; Slack 데일리 스크럼 봇 덕분에 스쿼드원 각자가 어떤 업무를 진행하고 있는지 명확히 알게 되면서, 업무의 투명성과 가시성이 크게 향상되었습니다.&lt;/p&gt;
&lt;p&gt;주니어 개발자인 저의 입장에서도, 질문을 어떻게/누구에게 해야 할지에 대한 고민이 줄고 질문이 쉬워졌습니다. 시니어-주니어 페어링 디스커션 이후로 서로에 대한 이해가 깊어지고 신뢰가 쌓이면서 자연스럽게 소통이 활발해진 것 같습니다. 이는 스쿼드 전체의 코드 이해도 증대로 이어지는 선순환을 만들었습니다.&lt;/p&gt;
&lt;p&gt;물론, 아직 완벽하지 않습니다. 바쁠 때는 여전히 코드 리뷰가 충분히 깊지 못하게 넘어갈 때도 있고, 회고에서 정한 액션 아이템을 미처 실행하지 못하고 다음 분기로 미루는 경우도 있습니다.
하지만 가장 중요한 건, 우리가 과거에는 단순히 &apos;불편함&apos;에 적응하고 있었던 문제들에 대해, 이제는 &quot;이 불편함을 어떻게 해소할 수 있을까?&quot;라는 질문을 던지게 되었다는 점입니다. 이것이 KPT 회고가 올리브영 주문결제 스쿼드에 가져다준 가장 크고 본질적인 변화라고 생각합니다. 앞으로도 완벽한 스쿼드가 되려는 욕심보다는, 조금씩이라도 함께 나아지려는 스쿼드가 되고 싶습니다.&lt;/p&gt;
&lt;p&gt;이번 회고를 통해 얻은 실행력을 바탕으로, 앞으로도 스쿼드원 모두가 문제의 원인을 함께 찾고 해결하는 문화를 더욱 공고히 하며 진화해 나갈 예정입니다.&lt;/p&gt;
&lt;p&gt;앞으로도 주문결제 여정을 &lt;strong&gt;더 빠르게, 더 안정적으로, 더 편리하게&lt;/strong&gt; 만들어가겠습니다. 읽어주셔서 감사합니다! 🚀&lt;/p&gt;</content:encoded></item><item><title><![CDATA["이 버튼 왜 안 눌려요?" 물류 현장의 목소리로 PDA 시스템 완성하기]]></title><description><![CDATA["삐빅, 삐빅." 1분 1초가 급박하게 돌아가는 물류센터. 이곳에서 현장 작업자의 손에 들린 PDA(산업용 단말기)의 작은 화면은 전쟁터의 레이더와 같습니다. 하지만 이 레이더가 낡고 해상도가 낮다면? 브라우저 툴바가 화면의 1…]]></description><link>https://oliveyoung.tech/2025-09-24/wms-pda-web-app/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-09-24/wms-pda-web-app/</guid><pubDate>Wed, 24 Sep 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;삐빅, 삐빅.&quot; 1분 1초가 급박하게 돌아가는 물류센터. 이곳에서 현장 작업자의 손에 들린 PDA(산업용 단말기)의 작은 화면은 전쟁터의 레이더와 같습니다. 하지만 이 레이더가 낡고 해상도가 낮다면? 브라우저 툴바가 화면의 10% 이상을 차지하고, 키패드로 다음 단계를 넘어가려 해도 아무 반응이 없다면? 버튼이 작아서 장갑 낀 손으로는 화면 버튼이 제대로 안 눌린다면? 실제로 저희가 마주한 현실이었습니다.&lt;/p&gt;
&lt;p&gt;이는 단순한 불편함을 넘어 물류 전체의 속도와 정확성을 저해하는 심각한 문제이자 저희가 개발하고 있는 &lt;strong&gt;WMS(Warehouse Management System)&lt;/strong&gt; 의 사용자 경험(UX)에 대한 중요한 질문이기도 했습니다.&lt;/p&gt;
&lt;p&gt;저희 조직은 이러한 문제를 해결하기 위해, 네이티브 앱이 아닌 웹 기술이라는 현실적인 선택을 했습니다. 그리고 &lt;strong&gt;PWA(Progressive Web App)&lt;/strong&gt; 도입과 키패드 이슈 해결 등 현장 피드백을 통해 시스템을 개선하여 이번에 저 &lt;strong&gt;무계획&lt;/strong&gt;이 대표로 올리브영 테크팀이 물류 현장의 &apos;진짜&apos; 목소리를 듣고, 이를 기술적으로 해결해 나간 생생한 여정을 담아 보았습니다.&lt;/p&gt;
&lt;p&gt;포스트 작성은 처음이지만 비슷한 고민을 안고 있는 개발팀에게는 이 여정에서 얻은 인사이트가 분명 도움이 될 것이라 생각해 기록을 공유하니 많은 관심 부탁드립니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;우리가-마주한-올리브영-물류-현장의-현실적-제약사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EA%B0%80-%EB%A7%88%EC%A3%BC%ED%95%9C-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%AC%BC%EB%A5%98-%ED%98%84%EC%9E%A5%EC%9D%98-%ED%98%84%EC%8B%A4%EC%A0%81-%EC%A0%9C%EC%95%BD%EC%82%AC%ED%95%AD&quot; aria-label=&quot;우리가 마주한 올리브영 물류 현장의 현실적 제약사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;우리가 마주한 올리브영 물류 현장의 현실적 제약사항&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;물류센터 운영에는 다양한 프로세스가 존재합니다. 각 프로세스는 단일 시스템을 통해 관리되지만, 각 파트의 담당자들은 서로 다른 역할을 수행하며 지속적으로 데이터를 업데이트합니다. 그리고 그 프로세스는 &lt;strong&gt;다양한 형태의 인터페이스&lt;/strong&gt;를 통해서 진행됩니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1068px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/adc5a/wms_pda_system.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.04166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABw0lEQVR42n1SyyttcRTef4YRKbojf4CJiZIZA9dAGCiFUijcboc7ooiJiUfewjHgKCnHwOU69xgoeQ2cwSnPfZwO2z77dfbzfH5r08+r/OqrX6v1fWutby0hk8nAcRxYluXDNM0P0DQNkiT5MAyD5xFs2+YgDdISiPTdo2RRFJFIJOC67re5pCWQ6udH1WRZhq7r/p860/WX7lRVhaIo8DzvC8/v8L2g7Xo+TEY0jBcxP5FVzpgW75g4VITwXvh1ZAu6JuP4YB1n/4KYGWqBeB3jSUeRNUS3pnD2P4R47JDHk2Ice9sLiO6tYie8yCZQWDEHgu14SN3FMNhdicmpRrQ2/MDFSZQThwM1+NNRgrHJOoQ3l3j8cD+EvtZiTA/Uov93NZ6kJPPYg8D0kLw6xa+aAnS3FaG5vhAXx2+CgfYqlJbmoKklH/MjnTy+v7uBirI8/CzPRVNVIR7uL5HNgjp0oT0lsLscwNZ0F1ZGOyHexDkxEp7F5lwvQhM9OD9Y4/HH1C0if1cRDI5jaLgHD48pZL0sBDKWjFY1A5KcZkYzw5m5uq75WzYttvG0hrTKtsw8orukOPGUtOJvnc6FLoCW8gzme4k8K9b3cwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/263a4/wms_pda_system.webp 480w,
/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/a6361/wms_pda_system.webp 960w,
/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/a22c7/wms_pda_system.webp 1068w&quot; sizes=&quot;(max-width: 1068px) 100vw, 1068px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/9aebd/wms_pda_system.png 480w,
/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/a91f8/wms_pda_system.png 960w,
/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/adc5a/wms_pda_system.png 1068w&quot; sizes=&quot;(max-width: 1068px) 100vw, 1068px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cd060fa72f5e3360ebe1d6b0c2a2e17b/adc5a/wms_pda_system.png&quot; alt=&quot;GMS 시스템 인터페이스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;GMS 시스템 인터페이스&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;본격적인 진행에 앞서서 이번 프로젝트 진행시 존재했던 제약 조건들을 먼저 살펴보겠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;하드웨어의 한계&lt;/strong&gt;: PDA 단말기는 일반 스마트폰에 비해 해상도, 처리 속도, 메모리 용량 등에서 제약이 많습니다. 이러한 제약은 복잡한 UI나 무거운 애플리케이션을 실행하는 데 어려움을 줍니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용 상황의 특성&lt;/strong&gt;: 장갑을 끼고 한손으로 작업하는 경우가 많아, 작은 버튼이나 복잡한 인터페이스는 사용하기 어렵습니다. 또한, 물건을 이동하거나 들고있는 상황에서 빠르게 작업을 처리해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;개발 리소스 및 일정의 한계&lt;/strong&gt;: 네이티브 앱 개발 및 운영을 위한 리소스가 부족했으며, 개발 일정 또한 넉넉하지 않았습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;pda-단말을-통한-물류-작업&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pda-%EB%8B%A8%EB%A7%90%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%AC%BC%EB%A5%98-%EC%9E%91%EC%97%85&quot; aria-label=&quot;pda 단말을 통한 물류 작업 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;PDA 단말을 통한 물류 작업&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;올리브영 물류센터의 상품관리 시스템은 PDA 디바이스를 핵심 인터페이스로 활용합니다. PDA의 내장 스캐너는 바코드(또는 QR코드)를 인식하여 시스템에 필요한 입력 데이터를 만들어냅니다. 이러한 스캔 데이터를 통해 작업자는 재고 관리, 상품 이동, 분류 등 다양한 물류 작업을 수행합니다.&lt;/p&gt;
&lt;p&gt;물류센터 내 모든 상품은 물론, 물품 보관 설비인 랙(Rack)의 위치를 나타내는 로케이션까지 바코드로 관리하고 있습니다. 이를 스캔할 수 있는 PDA는 상품관리 영역 전반에서 정확한 데이터 입력을 위한 핵심적인 역할을 수행합니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 734px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/980a8fae953226010a0a4751cc61c6fa/e42f2/wms_pda_barcode.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAACaUlEQVR42n2SyU9TURSHq3GrO/8GV2pcamLi2gULo9HogoXDQrdGUYkikBiHFgJlaEGhMx2oyCACYgtImRYVUqYgNSAQsK997/W9FiOYz/swNK48yS/n5uae797cfCZFUdA0jbSURlElMpk035MpJCmFmpWRZRlVVclms6iKTEba2oucTqFkxBlZQlMz5HRNcHRM+fw2idk4QyPvGRudxGeL4Wnqo693kImJIX6k1tmvhfk5GhsbcLjduNt8tAX8+ERcXg/RqRn0XB5TLpdjbGwEe70P9+sIAX8XdY112G1OsQ6wvpEsAJfX0nj744SjCUKRaULRGTo+zxIU/VP8G5quG0CdoK8fy0s7ofYgHq8Pl9tDq7sJR0Mn45G5AvDL7BJ2dy+ejmGcb4dpCUVo8HygprUbm68LWXyfAGo0W99RXvYci9mCvdlGKBjGcq+H62fcxAYWCsDxqSlq6510htrpDngJut5gq3qCrbqC6qpXSOkMpu2fOuZyHzeulNFot1L3oo37lzooPmvHanaxsbVSAMZGR3hacpOp2lNES47gvXWUyZqTRCsOU3+3iFRGM4A5nj1q4fSxYq6df0zRiQpuXzbTZHfQHg6wuDhfAA5HBwlWnmO69ACOqwcxXzhKxFpK+M4hHl48zpYkY1JVhcT0Vzr8Ufwtg0QGYiwnl1hdXSWZXMbQar+kzRVWhi2s9T8g6iwjFrCyORMn0VPJR7FWVPHCfwd2dn6RTkvowqf/1e7ub2QxbEQTluxXPi+0McQ1XDSk1LO6EFR0Pbcn+350sWfoZXQtq4ozWXHp3+TFviG1MacqCn8ATJidR7fRXKYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/980a8fae953226010a0a4751cc61c6fa/263a4/wms_pda_barcode.webp 480w,
/static/980a8fae953226010a0a4751cc61c6fa/b6719/wms_pda_barcode.webp 734w&quot; sizes=&quot;(max-width: 734px) 100vw, 734px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/980a8fae953226010a0a4751cc61c6fa/9aebd/wms_pda_barcode.png 480w,
/static/980a8fae953226010a0a4751cc61c6fa/e42f2/wms_pda_barcode.png 734w&quot; sizes=&quot;(max-width: 734px) 100vw, 734px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/980a8fae953226010a0a4751cc61c6fa/e42f2/wms_pda_barcode.png&quot; alt=&quot;바코드 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;바코드 예시&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;PDA 디바이스에 최적화된 클라이언트를 제공하기 위해 &lt;strong&gt;네이티브 앱&lt;/strong&gt;과 &lt;strong&gt;웹 앱&lt;/strong&gt; 사이에서 많은 고민이 있었습니다. 하지만 개발 리소스 및 일정의 제약과 PDA의 단순 작업 위주 운영이라는 현실을 고려했을 때, 웹 기반 시스템이 가장 합리적인 선택이었습니다.&lt;/p&gt;
&lt;p&gt;물론, 웹 기반으로 구현할 경우 PDA의 스캐너 기능을 웹에서 인식해야 한다는 기술적인 도전 과제가 남아 있었습니다. 그럼에도 불구하고 우리는 웹의 유지보수 용이성과 높은 개발 생산성이라는 장점을 극대화하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;다음 표는 당시 우리가 고려했던 두 가지 방식의 장단점을 비교한 내용입니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;항목&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Native App&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;Web App&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;바코드 스캐너 연동&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;SDK로 직접 제어, 안정적&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;연동 가능하지만 제약 존재&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;사용자 경험&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;스캔 → 응답 → 처리 플로우 최적화 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;브라우저 기반으로 인해 UX 튜닝 한계 존재&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;기기 기능 제어&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;진동, 알림음, 화면 제어 자유로움&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;제한적&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;보안성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;OS 권한 기반 제어 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;HTTPS 기반&lt;/td&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;오프라인 지원&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;로컬DB와 sync 처리 가능&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;제한적 (PWA 일부 지원)&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;개발 생산성&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;앱개발 스택 필요&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;웹 기술 스택으로 가능&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;유지보수&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;패키지 배포 등 부담스러운 업데이트 절차&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;서버 배포를 통해 비교적 용이한 유지보수 가능&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;1차-해결-웹-기반-스캐너-연동-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EC%B0%A8-%ED%95%B4%EA%B2%B0-%EC%9B%B9-%EA%B8%B0%EB%B0%98-%EC%8A%A4%EC%BA%90%EB%84%88-%EC%97%B0%EB%8F%99-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;1차 해결 웹 기반 스캐너 연동 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1차 해결: 웹 기반 스캐너 연동 구현&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;위에서 말한 물류 현장의 현실적 제약사항들 앞에서, 저희가 가장 먼저 부딪힌 벽은 바로 &apos;스캔 기능&apos;이었습니다. 물류 작업의 핵심인데, 웹에서는 하드웨어 제어가 쉽지 않다는 딜레마가 있었습니다.&lt;/p&gt;
&lt;p&gt;무엇보다 PDA 디바이스의 스캐너 기능을 웹에서 활용하려면 몇 가지 기술적 제약을 극복해야 했습니다. 웹 기반 시스템은 하드웨어와 직접적으로 연결되지 않아 스캔 이벤트를 감지하거나 입력 필드의 포커스를 세밀하게 제어하기 어렵기 때문입니다.&lt;/p&gt;
&lt;p&gt;그럼에도 불구하고 저희는 개발 생산성과 유지보수의 용이성을 위해 웹 기반 시스템을 선택했고, 이 잠재적인 단점을 극복하는 데 집중했습니다.&lt;/p&gt;
&lt;p&gt;가장 먼저 해결해야 할 문제는 스캔 데이터 처리였습니다. PDA 스캐너는 바코드를 인식한 후 그 결과값을 키보드 입력처럼 전달합니다. 저희는 이 특성을 활용해 특정 input 요소에 포커스를 유지시키고, 스캔된 문자열을 바로 처리하는 로직을 구현했습니다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 예상치 못한 문제가 발생했습니다. 일부 기기에서는 스캔 후 엔터(Enter) 입력이 누락되어 이벤트가 트리거되지 않았습니다. 이를 해결하기 위해 PDA 단말기 자체의 설정을 변경해, 스캔이 완료되면 항상 엔터 입력을 보내도록 구성했습니다.&lt;/p&gt;
&lt;p&gt;더 나아가, 사용자가 손을 최소한으로 움직이도록 하기 위해 포커스 관리 로직을 고도화했습니다. 상위 컴포넌트에서 keydown 이벤트 리스너를 활용해 앱 전반에서 스캔 기능을 즉시 사용할 수 있게 했고, 각 비즈니스 로직에 맞춰 input을 유기적으로 초기화하도록 구현했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;2차-해결-pwa-도입으로-사용성-극대화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EC%B0%A8-%ED%95%B4%EA%B2%B0-pwa-%EB%8F%84%EC%9E%85%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%EC%84%B1-%EA%B7%B9%EB%8C%80%ED%99%94&quot; aria-label=&quot;2차 해결 pwa 도입으로 사용성 극대화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2차 해결: PWA 도입으로 사용성 극대화&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;서비스를 모바일 웹으로 제공하다보니 또 다른 불편사항이 존재했습니다. PDA 디바이스 특성상 화면의 크기와 해상도에 한계가 있습니다. 현장에서 사용하는 디바이스는 일반적으로 우리가 사용하는 스마트폰보다 해상도 사양이 더 좋지 않습니다. 이런 제한사항은 사용자들에게 보여줄 수 있는 영역을 더 좁게 만들었습니다. 특히 모바일 브라우저를 사용하기 때문에 상단의 툴바 영역을 추가적으로 제외해야 하므로 어플리케이션 노출 영역은 더더욱 좁아졌습니다.&lt;/p&gt;
&lt;p&gt;이러한 제약을 해결하기 위해 PWA (Progressive Web App) 기술을 도입했습니다. 네이티브 앱 수준의 기능이 꼭 필요한 상황이 아니라면, PWA는 충분한 대안이 될 수 있습니다.
아래에서 PWA의 일반적인 장점을 살펴보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앱 설치한 후 캐싱으로 인한 데이터와 시간을 절약&lt;/li&gt;
&lt;li&gt;변경한 콘텐츠만 업데이트할 수 있음&lt;/li&gt;
&lt;li&gt;홈 화면에 앱 아이콘 적용 가능&lt;/li&gt;
&lt;li&gt;전체화면으로 앱 실행 가능&lt;/li&gt;
&lt;li&gt;시스템 알림 및 푸시 메시지 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
사용자에게 더 넓은 화면 영역을 제공할 수 있을 뿐만 아니라, 서비스 로딩시 로고 적용, 앱 아이콘 적용 그리고 캐시 등 더 완성도 높은 서비스를 제공할 수 있게 되었습니다. 전체화면 활용으로 인해 다른 url 입력을 통해 서비스를 이탈할 수 있는 행위도 방지할 수 있게 되었습니다.
&lt;p&gt;PWA 적용을 위해 다음과 같은 작업들을 진행했습니다.&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #000000; padding-left: 16px;&quot;&gt;
   &lt;span style=&quot;color:rgb(20, 18, 18);&quot;&gt;
      1. Manifest 파일 생성: PWA의 메타데이터를 정의하는 manifest.json 파일을 생성했습니다. 이 파일에는 앱 이름, 아이콘, 시작 URL, 디스플레이 모드 등을 정의합니다. GMS는 vite 번들러를 사용하고 있어서 아래와 같이 vite.config.js에 작성했습니다.
   &lt;/span&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// vite.config.js&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
      &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
       &lt;span class=&quot;token function&quot;&gt;VitePWA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token literal-property property&quot;&gt;registerType&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;autoUpdate&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;token literal-property property&quot;&gt;manifest&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;short_name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;GMS&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;start_url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;standalone&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;orientation&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;portrait-primary&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;background_color&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#fafafb&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;theme_color&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#000000&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token literal-property property&quot;&gt;icons&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
             &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token literal-property property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/icon_192x192.png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;token literal-property property&quot;&gt;sizes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;192x192&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;image/png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
             &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
             &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token literal-property property&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/icon_512x512.png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;token literal-property property&quot;&gt;sizes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;512x512&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;image/png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
             &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
           &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
       &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote style=&quot;border-left: 4px solid #000000; padding-left: 16px;&quot;&gt;
   &lt;span style=&quot;color:rgb(20, 18, 18);&quot;&gt;
        2. 아이콘 이미지 등록: PWA 적용을 위해 특정 사이즈(192x192, 512x512)의 PNG 파일이 필수적입니다. 규격에 맞지 않는 경우 PWA가 올바르게 동작하지 않을 수 있습니다.
    &lt;/span&gt;
&lt;/blockquote&gt;
&lt;blockquote style=&quot;border-left: 4px solid #000000; padding-left: 16px;&quot;&gt;
   &lt;span style=&quot;color:rgb(20, 18, 18);&quot;&gt;
        3. 홈 화면 설치 유도: 사용자에게 브라우저를 통해 앱 설치를 유도하고, 바탕화면에 바로가기 아이콘을 추가했습니다.
    &lt;/span&gt;
&lt;/blockquote&gt;
&lt;div style=&quot;width: 100%; display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width:200px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 399px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c5f91afbd063788f9477030e2bd605dd/f7599/wms_pda_app_icon.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 208.02005012531328%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAqCAYAAACz+XvQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAJKElEQVR42o2XyW9b5xXF+f9k16LrLlN03VW7CLrovmiLFmiArrrolDZjndixPMSDLMqaKFEkxenN8/we3+PjIJKabEWW7NSShzg5PY82YHRXARefSFE/3XvPufd9Kj16dIbBaIDBeIjh7hj9YT6P7E30Bn0c7c1wNJtinGWY8PXryDHJc+wNh3gwmWB/PMbp2WOUvH6MJb2DZaWDG7UN1BMP9dhD23fRtW3o0xF+/Yff4er7f8Q3bogzWcVzXcO5ruOlZWNarUG5dgPhUhl7/QwlM4+wNnJR7XvY8HW0+hFaWQixF0GJQhjM5NLqMjZX7+MkjPDMcfDKdfHtm/NcN7BbreLrVgv7vYTAfoitWYD2XgZplEJJIgjDBN1Jiu4oQSf2YU1GcHO2xHJwFjh4Fnp46b+FIgyZrYVpHKFk5CHW93zcs0SsseyqoWDd07ARGqh6OoRegCbD8EL03ABjQmaEPCLwOeMlX7+0HbZBxzQpgP0Alb0Ai+Iqbm7dR7ldx82dCpaUFiqGBCEJIIUB9CBkxIgdHxP2duo6hHqvy2c8N4zXQKvnYyuz0e/fwmaXoNo2RLahM4rQHgToRi60wIcRBHCYZURw6jlIHRd9P8BTZvnK894CzcTFVqShHTvo+jYERicw0QpNNCMb7R7VTn0ogQeZQklpCJWgnuli5tg4c6x5hi8M8zXQpkV22Ksd1+BpoBNahDhQDwfQHh+gu59ip++glTioJTZ2aCs3jpEQduSYOHcLYbw3wJhAKtZ0KP14gN3hAF32tDXw8bcvP8ZvfvsrXKuvoj0JoYxTxAcjmOM+YteCwT7uE/gicP8XaHo27EGCWnUDyd2PMDsd406njh+/8wO894Of4Kc//xnkaYb4ax/jkzXMHmcQaCWZrZkwkW8CG9/5/tuSVZagjRMs9Q2C1zE+HKLabeJH776L0js/xC9++R48juD4OMXkqYkg3IGQU3mWnJgW9tmqC7bthWtj1mOGOn14b+KiPvKxE7NHlgqDH7hbuY8PP/kHBElATFUtUYKkKFgWttBkL0XfIdDEyNKwFxTimJgUJWt9mnrXws2wi9t6AzVXZkkWGr6GbVtGNzBghDYacgdr3QZWaf56ZNEFFmwK0vNc7LMFx7TNiBNT0lIP5bGOrYmH2shBvW+jllnYHjrY5nvbEx/VXR+NsY927kPknLeZUcejG0Jm6Zq0j45jLoxhAdTps+W+go2BgY2c4IGJraGFzaGNdbZifc9DZUZobjFbExIVnvuTvm1yrrXIwcAugArGUfQWuDoysD5k8KwMTQIIHtmochPVc5sQA6alI6Cyc2cQ7NkWRPa3GflwbAO9pMiQhr3fk+YZrg/0OXCLitcSHbXUQINn02NfbQU+BejbKgaOhrEpwzc1dFjyNsdTMg304wJIZe/HXWykKjZ7KueaUxNoEF0NgqtAIkh2VGgsKyIwowv2HQV9QkVmu0nF6wyZ7ycczZLGDMuxgI0CFsqo+RJ/KMM1aR9TgcpMVEINNt5iWb5lQKcgm3RHLeNiTh3228c2V15cZKimLpZy9XWGgYCG1YVJUMBsHP5VjWCRZSqEhbaJnO+LvoXyKMQm+1sZeGwRFwyzTuIAJYmr//rEwMquTZVNgvV5qSqztMwiFFiE5Cx5aGvwKEzC3tW59jYIEmkhiWO4zl4GxeiJBH65Z+LOUYjFwxC3DwKUc24eW4JmyCxbRcxMC5jKqLv6XO2cWceuynmW8ZDhcxlHRckil+jlQwdXj0Nce0DgIYH0YIcf9phZxGz8N/1sEbLD0hxDxSGn6NgW8YCCHRM4VerIObKl7jDEpYcerj4McGuf/aQHa64ISxMojAyHIJ2ZipaELqPNXy5eHxL8hE/Jx75BsAdFkJDwcVHqsLkfHru4cmTj7ogPqEhguV10HJ6uhCZnu+1IaAQqZ5iT1OPDK1RZtoyMmyanyc8p0opuwSy2jcBl+vFDe97HciZglRZaiyWsJyrWUw3rNHmlp2OFI1ke21TXxCqF22amLbbEo/IZPdnQVfQiZihmDi7PVCxMNNzNulhOBCylEhYHKpY5OWsEbiQKVvo622FgcWThPs1f8Qq/cmIYAUWz5S4yjmBJ7Nm4njYJkHAnl7DEMVxOFf4S57snz71ZCfjMTmTOvIoyR3SVwAatlbHsfbajbzLkHaTxHGjhSq+OxaTDielgJWpjNSrOLsoJQZHIKwpbERVQCZsh+0p1TU1ETnEeUsAjvs6kBlI+n0pSUgAbuD4QcKsvzqFLcRt3UxG3+xJDxr1UnsM3/C6qVhvbjI7ehUTvjWmfB0W2cgu9kBkKzPAygVdGMj7f1XBtIOOrTMRXhN3g9zeGCm4NFCwRuBJ0serypuYXpwiZ81vM/Neeil3Cw0KUTmrjk0EHX+yq+ILCXB4ruErQAuNL2miBUcDvElgOBawQuMhFssHFcEDQEf14Rvv4hgs5om3aBH4wFvDpVMO/JwVUJ1TFFcZl/pGFkYpFql5m/1aZ4W328srUh56YeOIp2Ofl4DtOSNMI0Ih6KDV5r/n7RMLHMx2f7un4YqrjKmFFuQs8b+YKNp0OGmYLm4wruYZ/8fqXEnZA00ccxzHXWU3iPBclN3nN+GAs4lOW+lnexedZB5fTFq7FTVxP2lh0+RwWaqhIVdz02/iM/vyIBm+zd9uc4xqfhgljKDe5sSlKi8b+64CqzvjsPR7AOOjB2EthT3uwGO4oxrSf8Oyh/XCEzukMzoNd7PJeLvLebU+H8/1oSm0M50CqfImWCXaHqGzv4NtXr/D/fn3//feQDQvHJyfwxeZroBbwoS7XMMgiGLxAPjn/BhfPL3D+7BxPi/PiKZ7wPHv5DKevXuD85XPg2QUuXjzHfy4ukB8eYHowxZnZxi5vuqWEkofNNQhiBS2O2YbXwTp9tsZxK2ca59fBV5zfz09SLBxnXPcBt4oLl4l0LBO3uU+rsYbH3E6TAhhToVb1HsqdFdwQVnFVr+CSvY1/0nt/tqr4fWsZf6rdwfutJfxlcxEL3RpucIYjsYGu0MRnhoBrvLZYeouXJW7sk5NHSFPenbMEUZ4gGPR422KMMzh5D2Yawcxi2FyeLld8miTI+Q/QqJ8iY4QMn/8I5XmK09NT/BdpgQ8R6++EsgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c5f91afbd063788f9477030e2bd605dd/9792e/wms_pda_app_icon.webp 399w&quot; sizes=&quot;(max-width: 399px) 100vw, 399px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c5f91afbd063788f9477030e2bd605dd/f7599/wms_pda_app_icon.png 399w&quot; sizes=&quot;(max-width: 399px) 100vw, 399px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c5f91afbd063788f9477030e2bd605dd/f7599/wms_pda_app_icon.png&quot; alt=&quot;PWA App 아이콘&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;PWA App 아이콘&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;PWA 적용의 효과는 기대 이상이었습니다. 가장 큰 변화는 화면 활용도의 극적인 개선입니다.&lt;/p&gt;
&lt;p&gt;기존 모바일 웹 환경에서는 브라우저 상·하단 UI가 약 10%의 화면을 차지했지만, PWA의 전체 화면 모드(display: &apos;standalone&apos;) 덕분에 이 영역을 모두 콘텐츠 표시 영역으로 활용할 수 있게 되었습니다. 저해상도 PDA 환경에서 이는 &lt;strong&gt;체감 성능을 높이는&lt;/strong&gt; 매우 유효한 개선이었습니다. 또한, 홈 화면에 추가된 앱 아이콘은 작업자들이 매번 URL을 입력하거나 북마크를 찾아 헤맬 필요 없이, 터치 한 번으로 시스템에 바로 진입할 수 있게 하여 작업 시작 시간을 눈에 띄게 단축시켰습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 734px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f7ea63c64b47fcac19dc8003ce2183aa/e42f2/wms_pda_display.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 98.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAE2klEQVR42m2UW2ibZRjHcyd4KUwGgleboPN2dcwbRZmCJ4Q5N5k36tCL4YVMhwzBqThEkR3UbejG7NJ1S7p2bdqkadqsOTVNvuTLlzTJl+bQpOlxPSRtmq5ttu7nk7Tsyhf+fA/PC7/vOb4GXdfp7LRgMrXRYmzFaLzB9estjW9bWztm821yuRxTU1N03LFgNt1u3LfcaG3IaGyhq6uHLosVRQlhMJnN7Nq9m6amvezft599Tft44fk97Nz5DLt27WbHjh0EAgHur9e42NyFsc3Jv7ccDV1rtfOP0co1sS8be3F6BBgIKnz/0xnOnv9DdJFzFy7y6+/nOf3jL3x3+me++uYU+UKBtY1NnMEcocQ9wvoc4eQcofgsgZFp/LFZ3JFpRtKTGManZlHiBTR9Ci05S1QXpWYaqvuUkXGq9zdYWV3D4R9lODrZkF+bYEgt4ha5wkUcgTG0VBFDNj+B0xfFG0zhU9IMKRl8wQye4RQuv06fO8ZyZZXq2gZ2AbojBbzRCTwRgYUnuBuawCFAW3AbmBkrMjCk4VUS+NVRhsJpPME0LgHeFaDdM8KSAFckSuuQTjA5jpaZRpUMwpKRkpwhKJn0hXJSinEMqUwBm0vBpyYIxtIENIlSlSjDEo2SwnpXpbxcbQC7/UkBFlHTs6iZuS2lpaaj09vAAoYRPdsARlNjxMQRTYn0vNQvj5rIYhkIUCpXWJGU73hH6B5KYFcn6B+Zpz82T1/0Hg5tkt5QFi09IcBUDrPNRafDJ/XSJEWNXpeGzR3BOhimo29oG1ij3ReReqXpj8zgipdwJZYYjJdxxeboC48RzU5uRWiyOekZ9OPwhqSekUZNHT6VXneIdruXxfIyq2sPueXzcyPQS0fYhyUWpicewxrX6BbbosQYyU5hiCUzXO/pobPfj3VAxSFd7XcnGPAkxI7TKSkvLC1RW4e/Bm/yUseTvNzzFK/bnuVN+3O8an+a/d1PcMr5NemxMgZdn+C0+Qfebn6R91v3csT0GofNr3DQ1MS7rXs4Z71AtfJAIqzRFdS4qdi4FerjtjrA7cgAZtWBKWLFogbRx2YEOFrkguUSX7Yf5UTnJ5zs/oJvbZ9z0vYpJ3qO0OxsobpSo7q+gSuSlzGZI5iYR0ksEEouyuiUiOiLDCemSeUl5cRoDqc3TlApooQnCalTqLJGdUW0GdxBneWVFdnlB/Qrozhl+J2hNHfDGQals4OyBK6Q2GqO9PgshtFsAbtbod8XwjkcYVCK6xa5AlGxozjlW1m53wDaZQ77h5OyalncMneecF1ZvOoYHjVPpnhvC9hhd2NxeOlx+mV0QvTJXNoHg1u2J0ylep+12kOCqXtE84skCkvohQrJwjL6+HLDjuUrFGakKSkBtve6sQlswKPiHIpuS2uoDqw/DPMLi5w+c4Hfzl/h3MVm/vy7lctX27h0xcRV4x3OXmrBF4hgKBSnZIeTBERaIkcknkWNZxp2KDZKWNZx48FDCoU8bxw4wAcHD/LhoUN8fPQox459xtGPDnP8+HHee+cteXxvYVhbW6N+Njc3WVhYeKy5uTn+/zzi0aNNyuUSs7MzrK+vP76p1WoYKpUKq6vymkgnS6USi4uLDeD8/DxLMtB11f11X/2uVC4LrPz4x0vLS1Sr1S1GdYX/AIccLZhZvdIvAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f7ea63c64b47fcac19dc8003ce2183aa/263a4/wms_pda_display.webp 480w,
/static/f7ea63c64b47fcac19dc8003ce2183aa/b6719/wms_pda_display.webp 734w&quot; sizes=&quot;(max-width: 734px) 100vw, 734px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f7ea63c64b47fcac19dc8003ce2183aa/9aebd/wms_pda_display.png 480w,
/static/f7ea63c64b47fcac19dc8003ce2183aa/e42f2/wms_pda_display.png 734w&quot; sizes=&quot;(max-width: 734px) 100vw, 734px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f7ea63c64b47fcac19dc8003ce2183aa/e42f2/wms_pda_display.png&quot; alt=&quot;PWA 도입을 통한 해상도 개선&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;PWA 도입을 통한 해상도 개선&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;3차-해결-현장-테스트를-통한-키패드-이슈-해결&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EC%B0%A8-%ED%95%B4%EA%B2%B0-%ED%98%84%EC%9E%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%ED%86%B5%ED%95%9C-%ED%82%A4%ED%8C%A8%EB%93%9C-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0&quot; aria-label=&quot;3차 해결 현장 테스트를 통한 키패드 이슈 해결 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3차 해결: 현장 테스트를 통한 키패드 이슈 해결&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;PWA 도입으로 기술적 문제는 어느 정도 해결했다고 생각했습니다. 하지만 진짜 시험대는 따로 있었습니다. 바로 현장이었죠. 사실 서비스 개발 과정에서 실제 사용자의 목소리를 가까이에서 들을 기회는 쉽게 찾아오지 않는데, 이번 프로젝트에서는 작업자의 생생한 피드백을 들을 수 있는 귀중한 기회가 있었습니다. 특히 현장 테스트를 통해 PDA 시스템의 사용성을 크게 개선할 수 있는 핵심 인사이트들을 발견할 수 있었죠.&lt;/p&gt;
&lt;div style=&quot;width: 100%; display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width:600px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bf97310cb0c9ce52ec5e7fff14a3f53b/27773/wms_pda_research.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC1ElEQVR42pWQ3U9adxyH+Yt2ue1iF4vbnBN8RfAA4kIPKIp1amAO3WbSpK4paU3F+AoK+EIPeuCAWqcizrWdMYJR0LulvWgvumSXzZJePf2BTrfLXjz5/s7bcz6fr87osfEhNLktBDWVzHkRJX9M5jTP2kmeRCGPKqauqauND6Gx00owraKVijw9P+bvVye8fFFgv3Qp1tm8EhV8V/PqbBXTPGCuzMv7FizeVoy9zUxkVNJCuHN2zP55gYOLAjvFK+HQrIEyw3M3DM7o+XHGQCBixC/OfvH8h5laRsINBBbsTK7H0YpFEqKyIqqWua5c3ktzl5WPv/maj6q+4JOaauodJrLJcf65SBOeGqGuQ8Io3sltP+bdmyK5nMbyH89JnRZICsm/3Ai7rbgdNfjkampqP6f+WxN/nSTg9Q6HG5NC2IrU087L0m/w9k9K+RyLT38XwpOK5L9cC12ygb4OPfrGr/jS0sTdewPsrj6kz99JfaeFus5W+n7qZX8rxtjCPeJHR5WEl6LCjbClx0ajqOPrMnC/X09LSzXVQmjqtdLcY8HgNNFgN2Jsl+jw2xgMVrGoeknncjwWO0yWd1fhuIKuXshsHonA902MD9YhmcoJG7EN2LH72mlwmqmTTVjldqbmf2Zy04SWHWZ7bxNFJNNKZ6TOTi8pnoqEHgcToy4OQm52pzsYuC2SOcyYu0Rytx3JJeHol7HdvoXT28as2k0k4yGZihHazBD5dZ2FrRt0K7FJZkadxAMO1LGy3MPS7C80u7r5VHLjHfKxoc6jKmG01XmeZdc4zCWJLYyzpsyRVELXqOJaFw09IDslsxGU2QzeQnskE41MUOv8js8kFyOB+2SfLKMmwuxtxdlKx3ieU1GWp9nQYqynov9DtxQNooTuEJ+7g1ImfJe52TFRW0ZvsdHv9/JEfLiyNMXqyjSLIlkqESImfrquRdHUCJlkhPQV7wEy/q2CVbTWygAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bf97310cb0c9ce52ec5e7fff14a3f53b/263a4/wms_pda_research.webp 480w,
/static/bf97310cb0c9ce52ec5e7fff14a3f53b/a6361/wms_pda_research.webp 960w,
/static/bf97310cb0c9ce52ec5e7fff14a3f53b/0b34d/wms_pda_research.webp 1920w,
/static/bf97310cb0c9ce52ec5e7fff14a3f53b/a5deb/wms_pda_research.webp 2412w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bf97310cb0c9ce52ec5e7fff14a3f53b/9aebd/wms_pda_research.png 480w,
/static/bf97310cb0c9ce52ec5e7fff14a3f53b/a91f8/wms_pda_research.png 960w,
/static/bf97310cb0c9ce52ec5e7fff14a3f53b/ac7a9/wms_pda_research.png 1920w,
/static/bf97310cb0c9ce52ec5e7fff14a3f53b/27773/wms_pda_research.png 2412w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bf97310cb0c9ce52ec5e7fff14a3f53b/ac7a9/wms_pda_research.png&quot; alt=&quot;현장 작업자의 목소리를 직접 들으며 UX 개선 중인 올리브영 개발팀&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;현장 작업자의 목소리를 직접 들으며 UX 개선 중인 올리브영 개발팀&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;그 중 의미 있던 개선을 이룬 케이스를 다루려합니다. 화면에 input이 여러 개가 있는 경우 키패드 버튼을 통해서 next step으로 넘어가는 부분에 대한 개선이 필요했습니다.&lt;/p&gt;
&lt;p&gt;PDA 작업자들은 주로 한 손으로 작업하며 빠르게 입력을 진행합니다. 그러나 기본 PDA에서 제공하는 키패드의 &lt;strong&gt;다음(→|) 버튼&lt;/strong&gt;이 의도한대로 동작하지 않거나, 포커스가 엉뚱한 위치로 이동하는 경우가 있었습니다.&lt;/p&gt;
&lt;div style=&quot;width: 100%; display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width:400px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 912px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fba3b0174b6d977503963e30eb6316d9/4a2a8/wms_pda_keypad.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.04166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAADJUlEQVR42iWTW4/iRhCF/RN2Bwy+Y2NjYxgwmIu5DTcDA5MNO7tRskl2mdUkihRFkaK85DFSfvmXwjyUuqu7uqrOOdXK/vGRzy+vqKpKGIZ0Oh1M0yRutWiJaZrG07sz290BtVzi588XdvsjplXlj7/+ZL3f4NQ1Lr9+oTdMUA6nJ366vFIulWg0GpIoviWPIgLx1XKZdb5jsdpIzB3n5w/MFisppHJ5/cp4NsLxqnz49EyrF6NYli6XFXRdw7atIrBe94q957rUao5YDcuyioKuZxEEnlggBR1poEa338SPDKK2j7LJtwInZzafsn88sN6uyfc509mY49ORh+Wcbb4p7Ph0It+tOJ4e2R/2HI453306czrv+fb5yPvvP6KMszH9tEfS79JNOgzHQ3r9jvAX0UnajEYDer0Ok9mEgez7aVfuuwwGfYmPWazHpFmbybLPcNJHqVTu0A1VIFcwDIFulIvVtk0cxxDTcWwdy9Rkvfomnufg1iyi0COMLeL7Gs2Og26XUVT1TiBvWQi04Tilfd9kvVkxzobsBNZ8uWC73bDaLFlKzGQyIpO4h8WMXOKyWSJvB6x3U0l4h/LmzVs6XYHYbhUKR81A1khUbkjytvghcbtJs9UkjiPiKJTOAlpxU0ZMRqsTkKQh92kDzS6hRIFGt63T8CtYhiYzqAn8GwWaXi7ouFJwo8EQtQ1R3cJ1bfF1UdtilLkiVky3Z6FsHxr88/cPbJc+0+mY+ULUFpUns4yZCJFlI5areXFewJWY0Sgl7Sf0RcRUBPzmXY9///tNkrZQknuL4z6VLm/Vr134fk1Itwvy69KJfxXBkbksZlK6E2EciXVdh4ZXY961eL9rM02kw5qtkghkxyzRbTmyt6jJsNfloVEp4wgNtlHFlUJXs8yqFL2dRaHGQmzbNDhkdY6RhrKYNvj9l5NAjskfIpZTj8vLhcvXFzT1Lb4kDn2Xw37Her0Wser4wRWBwaBncxw4LId1fvzyxHkiX89zq6TJFVZVHlaIgooQ7RP4dTTp0NKrGCKQaV5n0cC8CidmyHf15M3AqzB3Siwjnbyu8z9Se6ns/xylNAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fba3b0174b6d977503963e30eb6316d9/263a4/wms_pda_keypad.webp 480w,
/static/fba3b0174b6d977503963e30eb6316d9/d1de2/wms_pda_keypad.webp 912w&quot; sizes=&quot;(max-width: 912px) 100vw, 912px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fba3b0174b6d977503963e30eb6316d9/9aebd/wms_pda_keypad.png 480w,
/static/fba3b0174b6d977503963e30eb6316d9/4a2a8/wms_pda_keypad.png 912w&quot; sizes=&quot;(max-width: 912px) 100vw, 912px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fba3b0174b6d977503963e30eb6316d9/4a2a8/wms_pda_keypad.png&quot; alt=&quot;상황에 따라 액션 정의가 필요한 모바일 화면 속 다음(→|) 버튼&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;상황에 따라 액션 정의가 필요한 모바일 화면 속 다음(→|) 버튼&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;디바이스에 따라 결과가 다를 수 있지만, 일부 기기에서는 화면에 다중으로 input이 존재하는 경우 키패드의 다음(→|) 버튼이 submit이 아니라 &lt;strong&gt;다음 탭으로 이동&lt;/strong&gt;으로 작동됩니다. 실 사용자는 submit이 적용되는 것을 원하기 때문에 이를 해결할 방법을 찾기 시작했습니다.&lt;/p&gt;
&lt;p&gt;다양한 옵션과 함수를 사용해 보았지만 원하는 방향으로 작동하지 않았고, 시행착오를 거쳐서 방법을 찾아냈습니다. 특정 input에 포커스가 있는 경우 다른 input을 disabled 처리해서 다음(→|) 버튼을 submit으로 작동시켰습니다. 구현은 간단했지만, 그 효과는 명확했습니다. 이 로직을 적용한 후, 현장 작업자의 불편함은 해소되었습니다. 작은 아이디어 하나가 현장의 작업 효율에 미치는 긍정적인 영향을 직접 확인할 수 있었던 의미 있는 개선이었습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1528px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9498b52cc036c6c9b90e2c94711e224b/1debb/wms_pda_input_improve.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 34.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABF0lEQVR42l1Ri26DMAzk/z9ylZA2CgnkRR4E2psvlHaaJcvGPl/OpnN+xWI9tHFwPmArG0IIsNbCeyfuQXs+nzBOcIvBpJeW55xb3wjWOcHKXDfNFtoGtGj8OWidDArhmjCMCo/HA8dx4HvUUIvHoEzD0pwQkiznAjXP6KgmpoSUMhZjcLt9YZg03JrhQsSPkJCQPiqNu1owaoP7NKHvexiZKaWIby3vyra9V9okd84KeZLvs8YHGakwxoTLuC4JGC8L6yqEwn7Zvu9tcBe/LMb4hzC+67We2OMftqMavtKikBPAe6R2hiRHPxVyZaqlgPjq1VpxzXPTlQqpig1GNpRSLVbWBcT4UVVf9yrt7w7D8CGUGjl+AVdUHtdp7qv9AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9498b52cc036c6c9b90e2c94711e224b/263a4/wms_pda_input_improve.webp 480w,
/static/9498b52cc036c6c9b90e2c94711e224b/a6361/wms_pda_input_improve.webp 960w,
/static/9498b52cc036c6c9b90e2c94711e224b/3951f/wms_pda_input_improve.webp 1528w&quot; sizes=&quot;(max-width: 1528px) 100vw, 1528px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9498b52cc036c6c9b90e2c94711e224b/9aebd/wms_pda_input_improve.png 480w,
/static/9498b52cc036c6c9b90e2c94711e224b/a91f8/wms_pda_input_improve.png 960w,
/static/9498b52cc036c6c9b90e2c94711e224b/1debb/wms_pda_input_improve.png 1528w&quot; sizes=&quot;(max-width: 1528px) 100vw, 1528px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9498b52cc036c6c9b90e2c94711e224b/1debb/wms_pda_input_improve.png&quot; alt=&quot;input 비활성화를 통해 다중 입력 액션을 제어하게된 개선 로직&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;input 비활성화를 통해 다중 입력 액션을 제어하게된 개선 로직&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;프로젝트를-통해서-배우다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%ED%86%B5%ED%95%B4%EC%84%9C-%EB%B0%B0%EC%9A%B0%EB%8B%A4&quot; aria-label=&quot;프로젝트를 통해서 배우다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;프로젝트를 통해서 배우다&lt;/h2&gt;
&lt;hr&gt;
&lt;div style=&quot;width: 100%; display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width:700px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1062px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/90d7e6aa96d4797dd8c57794efec7f6e/3e86e/wms_pda_summary.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.87500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABbUlEQVR42l2SaW7CQAyFc/8D0WuUFon+YYuAEBKSkH2t68+dqVQiWTPx8vz8PEFVVTL0vbRtK23TSN91Et9uEkWR+Rr14S+fT2nq2v6rspTT8Wh5neZ3mtcrRq3xgORpHA2od8HoepXr5SLjMBgYlmeZxfHVSmK/28lNm/JPjZ0aDxoNLvMs5/NZkiSxOwCwgQkgNDR2as+iEGqMvfvHqAE4oOh7WeR9/SGHw1H4ssdDkvvdWJFEnJOxU20KAHHu/oS1MQRwmiZBSz8emjICZ+eYePDB+Qc3PrX4RvagtQFdYbBavcnnZmNFaEPXR5oaA8YmGTkARr/wdDI77PfmAwMZgrL8BVzryGEYWldYw8prCSNGQssiz60JsrBlDECa/WkI7VwTuXfu+bwaQP5Z+dfwejdAGNB5u/2SOI5Vj95ORsbueveb55y1eapSZMrKS4L/H0N+eMiFbo8CEx2h3QJo6N8r5pfmDR91LOUHwSn/iiSC1jgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/90d7e6aa96d4797dd8c57794efec7f6e/263a4/wms_pda_summary.webp 480w,
/static/90d7e6aa96d4797dd8c57794efec7f6e/a6361/wms_pda_summary.webp 960w,
/static/90d7e6aa96d4797dd8c57794efec7f6e/9f8de/wms_pda_summary.webp 1062w&quot; sizes=&quot;(max-width: 1062px) 100vw, 1062px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/90d7e6aa96d4797dd8c57794efec7f6e/9aebd/wms_pda_summary.png 480w,
/static/90d7e6aa96d4797dd8c57794efec7f6e/a91f8/wms_pda_summary.png 960w,
/static/90d7e6aa96d4797dd8c57794efec7f6e/3e86e/wms_pda_summary.png 1062w&quot; sizes=&quot;(max-width: 1062px) 100vw, 1062px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/90d7e6aa96d4797dd8c57794efec7f6e/3e86e/wms_pda_summary.png&quot; alt=&quot;경험한 문제점과 해결방안&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
  &lt;figcaption style=&quot;text-align: center;&quot;&gt;경험한 문제점과 해결방안&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;개선 작업 이후, 연속된 로케이션 조회시 초기화 프로세스 단계 제외할 수 있어 25% 이상의 작업 시간이 단축되었습니다.
또한 &quot;PDA를 사용하는데 불편한 부분이 있다&quot;라는 현장의 피드백이 사라졌습니다.&lt;/p&gt;
&lt;p&gt;PDA 시스템 최적화 여정을 통해 저희는 다음과 같은 교훈을 얻었습니다. 이 글을 읽는 개발자분들 중 비슷한 고민을 안고 있는 분들께도 저희의 경험이 실질적인 도움이 되기를 바랍니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;현장으로 목소리를 들어봅시다: 완벽한 이론을 적용하는것도 필요하지만, 때에 따라 &apos;왜 안 돼요?&apos; 같은 현장의 목소리를 직접 듣는 것이 더욱 중요할 수 있습니다.&lt;/li&gt;
&lt;li&gt;작은 개선부터 적용합시다: 완벽한 솔루션을 기다리지 말고, 작은 불편함이라도 즉시 해결하며 시스템을 발전시켜 나가봅시다.&lt;/li&gt;
&lt;li&gt;현실적인 솔루션을 선택합시다: 저희가 네이티브 앱이 없어도 웹과 PWA만으로 충분히 문제를 해결할 수 있었던 것처럼, 주어진 제약 속에서 가장 효율적인 기술을 찾아야 집중합시다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;이렇게 만들어진 저희 GMS에 관심있는 분들은 시나브로우님의 아래 포스팅도 참고 부탁드립니다.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://oliveyoung.tech/2025-07-23/gms-open-story/&quot; target=&quot;_blank&quot;&gt;
제로베이스 WMS 구축기: Kafka 기반 분산 물류 시스템 설계와 Out-of-Order Events 해결
&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[메시징 시스템 QA, 정합성을 지켜낸 올리브영의 이야기]]></title><description><![CDATA[올리브영이 데이터의 품질을 높이기 위한 여정 "고객이 100원 상품을 클릭했는데 결제하고 보니 10,00…]]></description><link>https://oliveyoung.tech/2025-09-24/API-testing-v1/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-09-24/API-testing-v1/</guid><pubDate>Wed, 24 Sep 2025 09:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;올리브영이-데이터의-품질을-높이기-위한-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%B4-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%ED%92%88%EC%A7%88%EC%9D%84-%EB%86%92%EC%9D%B4%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;올리브영이 데이터의 품질을 높이기 위한 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영이 데이터의 품질을 높이기 위한 여정&lt;/h2&gt;
&lt;p&gt;&quot;고객이 100원 상품을 클릭했는데 결제하고 보니 10,000원 짜리 상품이였다면?&quot;
이커머스에서 이런 일이 발생하면 고객 신뢰도는 급격히 떨어집니다. 문제의 근본 원인은 바로 &apos;데이터 정합성&apos;! 즉, 여러 시스템 간 데이터가 서로 일치하지 않는 것이죠.
올리브영도 예외는 아니었습니다. 기존엔 개발자들이 데이터 정합성을 책임졌지만, 실시간 메시지 기반 시스템으로 전환하면서 상황이 복잡해졌습니다.
하지만 데이터 정합성 또한 QA의 영역이라는 생각에 QA팀에서 데이터 정합성 확보를 위한 다양한 시도를 해왔습니다.
이번 편에서는 그중 하나인 API를 이용한 데이터 정합성 확보 전략에 관해 이야기할 예정입니다.&lt;/p&gt;
&lt;h3 id=&quot;데이터-정합성이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1%EC%9D%B4%EB%9E%80&quot; aria-label=&quot;데이터 정합성이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 정합성이란?&lt;/h3&gt;
&lt;p&gt;쉽게 말해 &quot;같은 상품 정보가 모든 시스템에서 동일하게 보이는 것&quot;입니다.
립스틱 가격이 15,000원이라고 예를 들어 보겠습니다. 이 립스틱은 상품 관리 시스템, 주문 시스템, 고객용 앱에 동일한 제품으로 등록된 이상 서로 일치하는 정보를 표기해야 합니다. 이를 데이터 정합성이라 합니다.&lt;/p&gt;
&lt;h3 id=&quot;데이터-정합성을-확보하기-위한-상품통합tf의-히스토리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1%EC%9D%84-%ED%99%95%EB%B3%B4%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%83%81%ED%92%88%ED%86%B5%ED%95%A9tf%EC%9D%98-%ED%9E%88%EC%8A%A4%ED%86%A0%EB%A6%AC&quot; aria-label=&quot;데이터 정합성을 확보하기 위한 상품통합tf의 히스토리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 정합성을 확보하기 위한 상품통합TF의 히스토리&lt;/h3&gt;
&lt;p&gt;현재 올리브영의 상품통합TF는 올리브영은 고객에게 더욱 신속하고 정확한 상품 정보를 제공하기 위해 기존 상품의 정보를 새로운 시스템으로 이전하는 과정을 거치는 중입니다.&lt;br&gt;
기존의 배치 기반 데이터 처리 방식을 탈피해 실시간 메시지 기반의 데이터 처리 구조를 새롭게 도입했습니다.&lt;br&gt;
시스템이 완전히 바뀌면서 새로운 과제에 직면했습니다. 데이터가 여러 시스템을 거치며 실시간으로 흐르게 되자, 데이터 정합성이 문제가 발생할 수 있다는 것이었습니다. 즉, 상품 정보가 최종적으로 고객에게 도달하는 과정에서 데이터가 서로 불일치하는 상황이 생길 수 있었습니다. 수많은 상품 정보가 오가는 올리브영에서는 치명적인 문제였습니다.&lt;br&gt;
물론 이 테스트를 기존처럼 상품 정보에 대한 부분을 등록, 수정하면서 최종 테이블에 정상적으로 저장되는 것을 확인할 수는 있지만 모든 경우의 수를 테스트하기에는 한계가 있었습니다.&lt;br&gt;
그래서 상품통합TF에서 낸 아이디어 중 하나가 구간마다 데이터를 조회하는 API를 만들어서 그 &lt;strong&gt;API를 통해 데이터 정합성을 확보&lt;/strong&gt;하는 것이었습니다.&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;데이터-정합성-확보-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%ED%99%95%EB%B3%B4-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;데이터 정합성 확보 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 정합성 확보 전략&lt;/h3&gt;
&lt;p&gt;2가지 전략을 세웠습니다.
첫 번째는 UI 테스트를 통해 데이터 정합성을 확보하는 것이고, 두 번째는 API를 이용하여 데이터 정합성을 확보하는 것입니다.&lt;/p&gt;
&lt;h4 id=&quot;전략-1-ui-테스트를-통한-데이터-정합성-확보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%84%EB%9E%B5-1-ui-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%ED%99%95%EB%B3%B4&quot; aria-label=&quot;전략 1 ui 테스트를 통한 데이터 정합성 확보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;전략 1. UI 테스트를 통한 데이터 정합성 확보&lt;/h4&gt;
&lt;p&gt;UI 테스트는 실제 사용자와 유사한 환경에서 애플리케이션을 테스트하는 방법입니다. 상품 정보에 영향을 미칠만한 항목들을 등록, 수정하여 최종적으로 상품 정보를 제공하는 테이블에 정상적으로 적재되었는지 확인하는 방식입니다.
이 방식으로 기본적인 등록, 수정을 통해서 정상적으로 데이터가 적재되는지를 확인할 수 있었습니다. 이 과정에서는 상품 정보에 영향을 줄 수 있는 항목별로 개발 과정에서 누락되거나, 반영 로직이 잘못되어있는 경우를 찾아낼 수 있었습니다.&lt;/p&gt;
&lt;h4 id=&quot;전략-2-api를-이용한-데이터-정합성-확보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%84%EB%9E%B5-2-api%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%ED%99%95%EB%B3%B4&quot; aria-label=&quot;전략 2 api를 이용한 데이터 정합성 확보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;전략 2. API를 이용한 데이터 정합성 확보&lt;/h4&gt;
&lt;p&gt;상품, 프로모션등의 데이터를 등록, 수정할 때 최종 테이블까지 적재되는 데까지는 여러 단계가 있습니다.
UI 테스트로는 단계별로 확인하지 않고 최종 테이블에 정상적으로 적제되는지를 확인하였습니다. 최종 테이블은 1개의 테이블을 가지고서 가공되는 것이 아니라 여러 테이블에 저장된 데이터를 가지고서 최종적으로 가공하여 만든 테이블입니다.
그러므로 최종 테이블의 정합성을 정확하게 확인하기 위해서는 단계마다 데이터를 모두 확인하는 것이 좋습니다. 다만 리소스 문제로 인해 단계별 데이터를 모두 확인하는 것에 어려움이 있으니, 각 구간에서 제공하는 데이터 조회 API를 통해서 정합성을 자동으로 확인하면 좋을 것으로 생각했습니다.
그래서 일단 QA 서버에서 테스트하는 기간에 단계의 데이터를 API로 조회하여 자동으로 정합성을 확인하여 알림을 주는 시스템을 만들고 이렇게 만든 시스템을 현재는 STG, 운영에서 스케줄링하여 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;시스템은 Postman과 Teamcity, Datadog을 이용하여 구축하였습니다.&lt;/p&gt;
&lt;p&gt;여기서 잠깐! 왜 Postman, Teamcity, Datadog을 이용했는지 궁금하신 분들이 있을 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Postman: API 테스트 자동화 도구로 GUI 환경에서 스크립트를 작성하고 관리할 수 있어 각각의 API의 응답값을 비교할 수 있는 로직을 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;Teamcity: CI/CD 도구로, Postman에서 작성한 테스트 스크립트를 스케쥴링 하여 실행하고 결과를 관리 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Datadog: 모니터링 및 알림 도구로, 테스트 결과를 시각화하고 이상 징후 발생 시 알림을 받을 수 있습니다. 시스템 상태를 실시간으로 모니터링하는 데 유용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;저희가 정합성을 맞춰야 하는 테이블은 A, B, C, D, 기존 최종 테이블, 신규 최종 테이블 크게 6개의 테이블이 있습니다. (그 외에도 더 있지만 이해를 돕기 위해서 구성도를 간략하게 하였고 테이블도 6개로 축약하였습니다.)&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1200px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.791666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAACGUlEQVR42nWSyW7UQBCG/S7cELwkXHkWxIg55IKEFDhBJCCAJgmzerxvM2735u2nqseWAgJLJXe73F/99Vd7xhhIKV1Ya6G1hlLKBa85OPe/vbUGTSPcN2Z5USGQVgpJKZGdFRqXsC75GDKD5oLGaAI1dEbjmJ4QZBR5DS8+W6QaSPSIXPZo2w5dpzEMHcWAvu8xP23bUq7DOI5uzx0losPGD7Dd7xCWCt7L+1s8+7jA0+s3eLW9w0jV0+IKQjwQgCHWgRmitSFgj4kHY1sUckBYSBwzCRbnfU5LLJMEb+MY3yqBgRQqE6Dtagz9+IdCVsdwYFJIFbNmRFzFiPNbgubwyto6JaQetRpcy+OkiGH/Bs4WkF1iJP/XiOIr8jGkoZwNcvIvI1gmu4tHwwXEcPZthnKO18YIercuX5DvObWdNR2iSsMLTwaJoirTR/5pfmaF8xAYKKVAdXpP7zV1NSCnM4UCSgNExPJKYckLNhg4qd750v81ZWsVwYybakd5pfa0r6j44G5GQYJKPQFfrL7iyfVrPP+wwDKKMNI9S/N3EM2aDl2uRll9IlU3pEpP12aesnVdccsMDbnlm/KERZZiSbGVFj3ftT4gdTWpuwxmGHIXbMfjoXA3Z1JZU2FBNud03ls9+AgPGY77FHfrI/aHA+IoRRTF8H3fRRQlFCkOlON9EAQIwxC73Q7ff/n4Qed+bgJ8WW3wG0qxlfjKv/YLAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/36e3f511b89e47e1997cf61e8a2923d6/263a4/flow2.webp 480w,
/static/36e3f511b89e47e1997cf61e8a2923d6/a6361/flow2.webp 960w,
/static/36e3f511b89e47e1997cf61e8a2923d6/cbd37/flow2.webp 1200w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/36e3f511b89e47e1997cf61e8a2923d6/9aebd/flow2.png 480w,
/static/36e3f511b89e47e1997cf61e8a2923d6/a91f8/flow2.png 960w,
/static/36e3f511b89e47e1997cf61e8a2923d6/64756/flow2.png 1200w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/36e3f511b89e47e1997cf61e8a2923d6/64756/flow2.png&quot; alt=&quot;flow2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(확인 해야하는 정합성 테이블들)&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;1. 정합성을 자동으로 확인하기 위한 플로우 정의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 먼저 정합성을 테스트 해야하는 구간과 어떤 값을 비교해야 할 것인지를 정의 하였습니다.
예시 플로우를 하나 설명드리겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step1)&lt;/strong&gt; 오늘 날짜에 변경된 상품 리스트를 조회하는 API를 호출하여 조회된 상품 리스트의 상품 코드값을 가져옵니다.&lt;br&gt;
&lt;strong&gt;Step2)&lt;/strong&gt; A테이블에 상품 코드값을 넘겨서 상품 상세 정보를 조회하는 API를 호출합니다.&lt;br&gt;
&lt;strong&gt;Step3)&lt;/strong&gt; B테이블에 상품 코드 값을 넘겨서 상품 상세 정보를 조회하는 API를 호출하여 A테이블과 B테이블에서 조회된 상품 상세 정보를 비교합니다.
&lt;br&gt;
&lt;br&gt;
&lt;strong&gt;2. API 정의서 바탕으로 API 마다 같은 값이 반환되어야 하는 값들 정의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;API마다 응닶값의 필드명이 다른 경우도 있고 같은 경우도 있어서 이 부분을 정리하여 정의하였습니다.
&lt;br&gt;
&lt;br&gt;
&lt;strong&gt;3. Postman을 이용하여 스크립트 작성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Postman을 이용하여 각 API를 호출하고 응답 값을 비교하는 스크립트를 작성하였습니다. 각 API의 응답 값에서 비교해야 하는 필드명을 추출하여 비교하는 로직을 작성하였습니다.
각 Step에서 작성된 스크립트와 플로우를 간단하게 설명하겠습니다.
&lt;br&gt;
&lt;br&gt;
&lt;strong&gt;Step1) 오늘 날짜에 변경된 상품 리스트를 조회하는 API를 호출하여 조회된 상품 리스트의 상품 코드값을 추출&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;{
  &quot;modifiedDateStart&quot;: &quot;{{todayDate}}&quot;,
  &quot;modifiedDateEnd&quot;: &quot;{{todayDate}}&quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;body에 오늘 날짜를 넣어서 변경된 상품 리스트를 조회하는 API를 호출합니다.
이 경우 todayDate를 넘겨주어야 해서 pre-request script에 오늘 날짜를 구하는 코드를 작성하였습니다.
&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pre-request script&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 오늘 날짜를 YYYY-MM-DD 형식으로 생성&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; today &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; yyyy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; today&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getFullYear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; mm &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;today&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getMonth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; dd &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;today&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getDate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;padStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;0&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; formattedDate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;yyyy&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;mm&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;dd&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 환경 또는 글로벌 변수에 저장&lt;/span&gt;
pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;todayDate&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; formattedDate&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;오늘 날짜에 변경된 API를 조회하게되면 아래와 같은 형태로 넘어오게 됩니다.
이때 받은 number값은 그 다음 A테이블에 상품 상세 정보를 조회하는 API를 호출할 때 사용하게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;SUCCESS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Requested Successful&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;List&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;A0000000122&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;A0000000123&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;받은 응답 값의 상품 번호를 20개씩 자르는 과정을 진행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;여기서 잠깐!&lt;/strong&gt; 왜 상품 번호를 20개씩 자르는 과정을 거쳤는지 궁금하신 분들이 있을 수 있습니다.&lt;/em&gt;
&lt;br&gt;
아래에 알림을 주는 과정을 설명할 예정인데 그 과정에서 한 번에 많은 오류가 발생 시 알림을 줄 수 있는 글자 수를 초과하거나 Postman 또는 Newman테스트 결과서가 잘려서 만들어지는 경우들이 발생하였습니다.
또한 API 자체에도 너무 많은 상품 번호를 넘겨주게 되면 API에서 처리할 수 있는 양을 초과하여 Timeout이 발생하는 때도 있었습니다.
그래서 상품 번호를 20개씩 잘라서 API를 호출하도록 하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;post-request script&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; res &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; rawList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;List &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 테스트 제외 상품 등록&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; excludeList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 필터링&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; goodsNumbers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rawList
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;number&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;num&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;excludeList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 20개씩 잘라서 저장했을 때 현재 테스트 할 index를 환경 변수에 저장&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; index &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;currentChunkIndex&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;currentChunkIndex&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; index&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 상품 번호를 20개씩 잘라서 저장&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; chunks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; pageSize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; goodsNumbers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; pageSize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   chunks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;goodsNumbers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; pageSize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;goodsChunks&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chunks&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 현재 테스트한 페이지 수의 상품 번호가 null로 넘어오면 테스트 종료&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;chunks&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;execution&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setNextRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;Step2) A테이블에 상품 코드값을 넘겨서 상품 상세 정보를 조회하는 API를 호출&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A 테이블에서 상품 상세 정보를 조회하기 위해서는 어떤 body로 넘겨주어야 하는지 살펴보면 아래와 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;goodsNumbers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;A0000000122&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;A0000000123&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Step2에서 받은 number값을 body로 쓰기 위해서는 2번의 post-request, 3번의 pre-request script에 아래와 같이 작성하여 호출하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pre-request script&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Step1에서 넘겨준 상품 정보 (20개씩 잘려서 넘어오는 상품 정보&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; chunks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;goodsChunks&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; currentChunk &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; chunks&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;currentGoodsNumbers&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentChunk&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

 &lt;span class=&quot;token comment&quot;&gt;// 요청해야할 JSON 형식으로 변환&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; requestBody &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
     &lt;span class=&quot;token literal-property property&quot;&gt;goodsNumberList&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; currentChunk
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;finalRequestBody&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestBody&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Step2에서 받은 상품 상세정보에 대한 응답 값을 상품 번호 기준으로 빠르게 찾기 위해서 hashmap 방식으로 저장하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;post-request script&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; res2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; step2Data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; res2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 현재 시간에서 10분(600,000ms) 전의 시간 계산 (정합성 정확성을 위해서 Message가 반영되지 않았거나 배치 타이밍에 대한 부분때문에 정합성에 부정확함이 생길 수 있습니다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 그래서 10분 이상 지난 데이터만 필터링하여 정합성을 테스트 하는 대상에서 제외합니다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; tenMinutesAgo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oneAndHalfHoursAgo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// status가 10이 아니고, modifiedDate가 10분 이상 지난 애들만 필터링&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; filteredStep2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; step2Data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; modifiedTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;modifiedDate&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getTime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; modifiedTime &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; tenMinutesAgo &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; modifiedTime &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; oneAndHalfHoursAgo
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; goodsNumberList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; filteredStep2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;goodsNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// goodsNumber로 빠르게 찾기 위해 Hash map 방식으로 저장&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; step2Map &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
filteredStep2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   step2Map&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;goodsNumber&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;step2Map&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;step2Map&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;Step3) B테이블에 상품 코드 값을 넘겨서 상품 상세 정보를 조회하는 API를 호출하여 A테이블과 B테이블에서 조회된 상품 상세 정보를 비교&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pre-request script에서 통해서 B테이블의 데이터를 호출하는 과정은 Step2와 동일하여 설명은 생략하겠습니다.
Step3에서는 Post-request script에서 B테이블의 응답 값을 A테이블과 비교하는 과정이 중요합니다.
(전체 발췌는 어려워 일부 코드 발췌하였습니다.)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Step3 post-request script&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;필드명이 같을 경우는 다음 스크립트를 사용하였습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; res3 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; step3Data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; res3&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Step2에서 저장한 Hash map 가져오기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; step2Map &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;environment&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;step2Map&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 비교 대상 필드 premiumBrandFlag 제외, statusName&lt;/span&gt;
fieldsToCompare &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;goodsName&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 원하는 항목 여기에 추가&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 결과 로그&lt;/span&gt;
step3Data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; goodsNo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item3&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;goodsNumber&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; item2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; step2Map&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;goodsNo&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    
    fieldsToCompare&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;field&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; val2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item2&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; val3 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item3&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - 필드 테스트: &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;field&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; A vs B&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val2 &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; val3&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;❌ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;field&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; mismatch | A: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val2&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; | B: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val3&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;expect&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;❌ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;field&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; mismatch | A: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val2&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; | B: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val3&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;필드명이 다를 경우 다음 스크립트를 사용하였습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 필드 매핑 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fieldMapping &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token literal-property property&quot;&gt;goodsName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;goodsName&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;goodsSectionCode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;goodsSectionCode&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;goodsType&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;goodsTypeCode&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;underAgeSalePossibleFlag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;underageSalesFlag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;duplicateItemFlag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;multipleOptionFlag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;freeDeliveryFlag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deliveryFreeFlag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;progressStateCode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;mediaType&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mediaCode&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;soldOutFlag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;soldOutFlag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;searchDisplayFlag&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;searchExposureFlag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;displayStartDateTime&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;displayStartDateTime&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;displayEndDatetime&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;displayEndDatetime&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

step3Data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; goodsNo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;goodsNumber&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; match &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;step2Map&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;goodsNumber &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; goodsNo&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;sourceField&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; targetField&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fieldMapping&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; val2&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; val3&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        val2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;sourceField&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        val3 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;targetField&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        
        pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;sourceField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; vs &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; 비교&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val2 &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; val3&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;❌ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - 필드 불일치: &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; (A: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val3&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;) vs &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;sourceField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;(B: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val2&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                pm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;expect&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;❌ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - 필드 불일치: &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;(A: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val3&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;) vs &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;sourceField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;(B: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;val2&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;✅ &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;goodsNo&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; - &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;sourceField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; vs &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetField&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; 일치&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 작성된 스크립트를 Postman에서 Postman에서 제공하는 CLI 기능을 통해서 Teamcity, cronjob에 연결하여 테스트를 자동으로 하게 하였습니다.
&lt;br&gt;
&lt;br&gt;
&lt;strong&gt;4. Teamcity OR Cornjob을 이용하여 정기적으로 테스트 실행 및 리포트 생성&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;999&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Running with currentChunkIndex=&lt;span class=&quot;token variable&quot;&gt;$i&lt;/span&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# 20개씩 끊어서 테스트를 진행함으로 currentChunkIndex를 이용하여 page수를 증가시켜서 테스트 하도록 함&lt;/span&gt;
    postman collection run &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; --env-var &lt;span class=&quot;token assign-left variable&quot;&gt;currentChunkIndex&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$i&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--reporters&lt;/span&gt; cli,json --reporter-json-export result.json 
    
    &lt;span class=&quot;token comment&quot;&gt;# 실패 수 추출&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;PRE_COUNT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;jq &lt;span class=&quot;token string&quot;&gt;&apos;.run.summary.prerequestScripts.executed&apos;&lt;/span&gt; result.json&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;# 테스트 한 것이 없으면 break&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$PRE_COUNT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-eq&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;token builtin class-name&quot;&gt;break&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;SUMMARY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &apos;
            .run.summary.tests.executed as $executed &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;
            .run.summary.tests.failed as $failed &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;📦 *A &amp;lt;-&gt; B &amp;lt;-&gt; C 테스트 요약[prd] *&lt;span class=&quot;token entity&quot; title=&quot;\n&quot;&gt;\n&lt;/span&gt;✅ Total: \(&lt;span class=&quot;token variable&quot;&gt;$executed&lt;/span&gt;), ❌ Failed: \(&lt;span class=&quot;token variable&quot;&gt;$failed&lt;/span&gt;)&quot;&lt;/span&gt; +
              &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;.run.executions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
                  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.tests &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; null&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; .tests&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
                  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;.error &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; null&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot; • \(.error.message)&quot;&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token entity&quot; title=&quot;\n&quot;&gt;\n&lt;/span&gt;&quot;&lt;/span&gt; + join&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token entity&quot; title=&quot;\n&quot;&gt;\n&lt;/span&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; end
              &lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&apos; result.json&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;# 실패 수 추출&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;FAILED_COUNT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;jq &lt;span class=&quot;token string&quot;&gt;&apos;.run.summary.tests.failed&apos;&lt;/span&gt; result.json&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;# 실패 시에만 메시지 전송&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$FAILED_COUNT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-gt&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-X&lt;/span&gt; POST &lt;span class=&quot;token parameter variable&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--data&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;text&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;: &lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$SUMMARY&lt;/span&gt;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;}&quot;&lt;/span&gt; https://hooks.slack.com/services/&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
        &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Slack 메시지 전송 완료&quot;&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 작성된 스크립트는 Teamcity 또는 Cronjob에서 정기적으로 실행되며, 테스트 결과를 JSON 형식으로 저장합니다.&lt;br&gt;
그러나 여전히 타이밍적인 이슈가 발생하는 경우가 있어 2번 연속 문제가 발생했을 때 알림이 오길 바랬고 문제가 있는 상품의 번호의 히스토리를 한번에 볼 수 있는 대시보드가 필요했습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Datadog을 이용하여 대시보드 구축 및 알림&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기존 실행 방식에 datadog에 적재할 저장할 log를 만드는 부분을 추가하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# 총 테스트 결과&lt;/span&gt;
jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;
  &quot;Type=API_Monitoring,&quot; +
  &quot;TotalCase=\(.run.summary.tests.executed),&quot; +
  &quot;Passed=\(.run.summary.tests.passed), &quot; +
  &quot;Failed=\(.run.summary.tests.failed)&quot;
&apos;&lt;/span&gt; result.json &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-a&lt;/span&gt; /Users/app/oliveyoung/Logs/result.log

&lt;span class=&quot;token comment&quot;&gt;# 실패 수 추출&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;FAILED_COUNT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;jq &lt;span class=&quot;token string&quot;&gt;&apos;.run.summary.tests.failed&apos;&lt;/span&gt; result.json&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 실패 시에만 메시지 전송&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$FAILED_COUNT&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-gt&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# 실패 발생 시 오류 메시지를 result.log 파일에 저장&lt;/span&gt;
  &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$Datadog&lt;/span&gt;&lt;span class=&quot;token entity&quot; title=&quot;\n&quot;&gt;\n&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; /Users/app/oliveyoung/Logs/result.log
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Datadog Agent를 통해서 Logs폴더 하위에 생성되는 *.log 파일을 읽어가도록 만들었고 Datadog에서 읽어간 로그를 parsing하여 대시보드에 표시하도록 하였습니다.
&lt;br&gt;
이렇게 구축된 대시보드는 정합성 테스트 결과를 실시간으로 모니터링할 수 있게 해주며, 문제가 발생했을 때 빠르게 대응할 수 있는 환경을 제공합니다.
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 25.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAFABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAduwkR//xAAWEAEBAQAAAAAAAAAAAAAAAAABEBH/2gAIAQEAAQUCwWf/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFH/8QAFRABAQAAAAAAAAAAAAAAAAAAEDH/2gAIAQEABj8Cj//EABoQAQACAwEAAAAAAAAAAAAAAAEAMREhQZH/2gAIAQEAAT8hwKhSp2Ju32f/2gAMAwEAAgADAAAAEAAv/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEB/9oACAEDAQE/EKuv/8QAFREBAQAAAAAAAAAAAAAAAAAAAAH/2gAIAQIBAT8QSH//xAAYEAEAAwEAAAAAAAAAAAAAAAABABEhMf/aAAgBAQABPxAY4NJpAqhy44q32P/Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5867867edb4d3b62db3b81a79726c473/263a4/DatadogDashBoard.webp 480w,
/static/5867867edb4d3b62db3b81a79726c473/a6361/DatadogDashBoard.webp 960w,
/static/5867867edb4d3b62db3b81a79726c473/95f20/DatadogDashBoard.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5867867edb4d3b62db3b81a79726c473/a3e66/DatadogDashBoard.jpg 480w,
/static/5867867edb4d3b62db3b81a79726c473/fb816/DatadogDashBoard.jpg 960w,
/static/5867867edb4d3b62db3b81a79726c473/33266/DatadogDashBoard.jpg 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5867867edb4d3b62db3b81a79726c473/33266/DatadogDashBoard.jpg&quot; alt=&quot;DatadogDashBoard&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;(Datadog 대시보드)&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;일시적인 타이밍으로 인한 오류는 제외하기 위해서 알림은 이 중에 1시간에 2번 이상 같은 오류를 발생시킨 경우에만 알림이 오도록 설정하였습니다.
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAc6BIB//xAAZEAABBQAAAAAAAAAAAAAAAAAAARAREiH/2gAIAQEAAQUCmwuN/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFRABAQAAAAAAAAAAAAAAAAAAECH/2gAIAQEABj8Cr//EABgQAQADAQAAAAAAAAAAAAAAAAEAEBEh/9oACAEBAAE/IdLelhp//9oADAMBAAIAAwAAABAP/wD/xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAwEBPxBn/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPxA//8QAGhAAAgIDAAAAAAAAAAAAAAAAASEAgRARQf/aAAgBAQABPxAJMgk8dTeCDvH/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d04473b945875003269daac2ad0eb3ba/263a4/alert.webp 480w,
/static/d04473b945875003269daac2ad0eb3ba/601b1/alert.webp 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d04473b945875003269daac2ad0eb3ba/a3e66/alert.jpg 480w,
/static/d04473b945875003269daac2ad0eb3ba/953fe/alert.jpg 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d04473b945875003269daac2ad0eb3ba/953fe/alert.jpg&quot; alt=&quot;alert&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;(Datadog 알림)&lt;/figcaption&gt;
&lt;p&gt;&lt;em&gt;Datadog에 대해서 조금 더 자세히 알고 싶다면 &lt;a href=&quot;https://oliveyoung.tech/2024-04-11/Datadog_QA/&quot;&gt;올리브영 QA는 Datadog을 어떻게 활용하고 있을까?&lt;/a&gt;를 참고해 주세요.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;현재-운영되고-있는-데이터-정합성-확보를-위한-자동화-테스트-리스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%98%84%EC%9E%AC-%EC%9A%B4%EC%98%81%EB%90%98%EA%B3%A0-%EC%9E%88%EB%8A%94-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%ED%99%95%EB%B3%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A6%AC%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;현재 운영되고 있는 데이터 정합성 확보를 위한 자동화 테스트 리스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;현재 운영되고 있는 데이터 정합성 확보를 위한 자동화 테스트 리스트&lt;/h3&gt;
&lt;p&gt;위에서 기본적인 플로우를 설명해 드렸는데 위의 방식을 변형, 응용하여 현재 운영되고 있는 데이터 정합성 확보를 위한 자동화 테스트 리스트는 다양합니다
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 695px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAe5ZoKP/xAAWEAADAAAAAAAAAAAAAAAAAAABEDH/2gAIAQEAAQUCEQi//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFhAAAwAAAAAAAAAAAAAAAAAAACAx/9oACAEBAAY/AiL/AP/EABsQAAICAwEAAAAAAAAAAAAAAAABESEQMVFx/9oACAEBAAE/IUUNEWIXBqeZ/9oADAMBAAIAAwAAABDDD//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABwQAAICAwEBAAAAAAAAAAAAAAABEUEhMWGRof/aAAgBAQABPxB7Foq4Rxg+iXTwqKELcfSeH//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/52c041352fd23382b95f608848b0507a/263a4/apitestlist.webp 480w,
/static/52c041352fd23382b95f608848b0507a/7e955/apitestlist.webp 695w&quot; sizes=&quot;(max-width: 695px) 100vw, 695px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/52c041352fd23382b95f608848b0507a/a3e66/apitestlist.jpg 480w,
/static/52c041352fd23382b95f608848b0507a/1412e/apitestlist.jpg 695w&quot; sizes=&quot;(max-width: 695px) 100vw, 695px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/52c041352fd23382b95f608848b0507a/1412e/apitestlist.jpg&quot; alt=&quot;apitestlist&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;figcaption&gt;(현재 운영 중인 정합성 자동화 테스트 플로우 리스트)&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;api를-이용한-데이터-정합성-테스트를-통해서-얻은-효과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#api%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%ED%86%B5%ED%95%B4%EC%84%9C-%EC%96%BB%EC%9D%80-%ED%9A%A8%EA%B3%BC&quot; aria-label=&quot;api를 이용한 데이터 정합성 테스트를 통해서 얻은 효과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;API를 이용한 데이터 정합성 테스트를 통해서 얻은 효과&lt;/h2&gt;
&lt;p&gt;효과는 3가지 측면으로 나눌 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 리소스 절감 측면&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;테스트 기간의 리소스 절감 측면에서 확인한 수치는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;범위 부분 : &lt;span style=&quot;color:red;&quot;&gt;75%&lt;/span&gt; 절감 - 전체 확인해야하는 구간 중에 75%를 API를 이용한 정합성 자동화 테스트로 진행하였습니다.&lt;/li&gt;
&lt;li&gt;인적 리소스 부분 : &lt;span style=&quot;color:red;&quot;&gt;45%&lt;/span&gt; 절감 - 리소스 측면에서 절감 효과가 범위 절감보다 낮은 이유는 상품, 프로모션등을 케이스별로 등록, 수정하는 작업들은 리소스를 많이 소비하기 때문입니다.&lt;br&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;2. 수동 테스트로 발견할 수 있는 케이스의 한계 극복&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;전체 발견 이슈 중 약 &lt;span style=&quot;color:red;&quot;&gt;51%&lt;/span&gt; 이슈 발견 하였습니다. &lt;br&gt;
정합성 자동화 테스트로 발견한 대표적인 이슈 예시를 보면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메시지 발행 타이밍&lt;/li&gt;
&lt;li&gt;메시지 중복 발행&lt;/li&gt;
&lt;li&gt;다수 메세지 발행 시 성능 문제&lt;/li&gt;
&lt;li&gt;DB 특정상으로 인한 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;3. 실 운영 서버에서 타이밍적인 부분, 예외 케이스에 대한 검증 측면&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;QA서버에서 발견할 수 있는 이슈들의 케이스는 한정적이기 때문에 운영 서버에 API를 통한 자동화 정합성 시스템을 구축해놓음으로써 운영 서버에서 발생할 수 있는 타이밍적인 부분이나 예외 케이스에 대한 검증을 할 수 있었습니다.&lt;br&gt;
이 부분에서 좀 더 다양한 사례들을 확인하여 정합성을 확보하는 데 가장 중요한 역할을 하였습니다.
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;데이터-정합성을-확보하려는-자동화-테스트의-핵심-요소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1%EC%9D%84-%ED%99%95%EB%B3%B4%ED%95%98%EB%A0%A4%EB%8A%94-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%ED%95%B5%EC%8B%AC-%EC%9A%94%EC%86%8C&quot; aria-label=&quot;데이터 정합성을 확보하려는 자동화 테스트의 핵심 요소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 정합성을 확보하려는 자동화 테스트의 핵심 요소&lt;/h2&gt;
&lt;p&gt;데이터 정합성에 대한 품질을 높이는 것은 QA팀이 혼자서 의지를 가진다고 할 수 있는 부분이 아닙니다.&lt;br&gt;
개발팀에서도 필요성이 있다고 인식이 되어야 서로 협업을 통해서 구축할 수 있습니다. 왜냐하면 어떻게 데이터 정합성 테스트를 진행할 것인지 결정하고, 결정된 방향성에 대해 개발팀의 도움이 필요합니다.
상품통합TF에서 API를 이용하여 데이터 정합성을 테스트 하기로 결정된 후 API를 통해 데이터를 조회할 수 있는 API가 있어야 하며, API의 응답 값에 대한 정의서가 있어야 했습니다.
필요성이 없다고 인식이 되었다면 이러한 부분에 대한 협의가 원활하게 이루어질 수 없습니다. 이것이 얼마나 필요한 것인지에 설득하는 것은 QA의 역할이라고 생각이 듭니다.&lt;br&gt;
이번에 데이터 정합성을 높이기 위해 API 자동화 테스트 시스템을 구축하는 과정은 기존에 해본 API 테스트와는 조금 다른 방식이었습니다. 그러므로 중간에 많은 변화 과정을 거치게 되었는데 개발팀에서도 이런 방식은 어떨지 이런 것은 가능할지 등의 문의와 제안을 주셨습니다. 이 과정에서 저는 품질을 위해서 다 같이 고민하고 있다는 생각이 들었습니다.&lt;br&gt;
그러므로 데이터 정합성 확보를 위한 자동화 테스트를 구축하기 데 필요한 것은 &lt;strong&gt;품질을 위해서 다같이 고민 문화&lt;/strong&gt;라고 생각합니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;이번 글은 올리브영에서 API 테스트를 활용해 데이터 정합성을 확보한 첫 번째 사례입니다. 이 글을 통해 저희가 리소스 절감과 운영 안정성을 어떻게 동시에 확보했는지 알려드리고 싶었습니다. 앞으로도 API를 활용해 품질을 높이는 다양한 방식을 꾸준히 소개해 드릴 예정이니 많은 관심 부탁드립니다.
&lt;br&gt;
&lt;br&gt;
이번 데이터 정합성 자동화 테스트 시스템을 성공적으로 구축할 수 있었던 건, 저희 QA팀의 노력뿐만 아니라 품질 향상을 위해 함께 고민하고 협력해 주신 상품통합TF💚 덕분이었습니다. 함께 한 모든 분들께 다시 한번 진심으로 감사드립니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center; margin-top: 20px; padding: 20px; background-color: #fafafa; border-radius: 12px;&quot;&gt;
  &lt;p style=&quot;font-size: 1.4em; font-weight: bold; margin-bottom: 25px;&quot;&gt;저희와 함께 일할 동료를 열린 마음으로 기다리고 있습니다. 🤩&lt;/p&gt;
  &lt;div style=&quot;display: flex; justify-content: center; gap: 20px; flex-wrap: nowrap;&quot;&gt;
    &lt;!-- 코어플랫폼 QA --&gt;
    &lt;div style=&quot;background-color: white; padding: 20px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.08); width: 300px; text-align: center; transition: transform 0.2s;&quot;&gt;
      &lt;p style=&quot;margin-bottom: 15px; font-weight: 600; font-size: 1em; line-height: 1.3em;&quot;&gt;
        상품, 회원, 프로모션 등&lt;br&gt;올리브영 플랫폼의 코어를 책임지는 QA
      &lt;/p&gt;
      &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250807033067&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 28px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 6px; font-weight: bold; font-size: 0.95em; transition: background-color 0.2s;&quot;&gt;채용공고 보기&lt;/a&gt;
    &lt;/div&gt;
    &lt;!-- SCM QA --&gt;
    &lt;div style=&quot;background-color: white; padding: 20px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.08); width: 300px; text-align: center; transition: transform 0.2s;&quot;&gt;
      &lt;p style=&quot;margin-bottom: 15px; font-weight: 600; font-size: 1em; line-height: 1.3em;&quot;&gt;
        물류/재고 관리 시스템의 안정성을&lt;br&gt;책임지는 SCM QA
      &lt;/p&gt;
      &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033063&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 28px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 6px; font-weight: bold; font-size: 0.95em; transition: background-color 0.2s;&quot;&gt;채용공고 보기&lt;/a&gt;
    &lt;/div&gt;
    &lt;!-- 백오피스 QA --&gt;
    &lt;div style=&quot;background-color: white; padding: 20px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.08); width: 300px; text-align: center; transition: transform 0.2s;&quot;&gt;
      &lt;p style=&quot;margin-bottom: 15px; font-weight: 600; font-size: 1em; line-height: 1.3em;&quot;&gt;
        내부 시스템의 품질을 책임지는&lt;br&gt;백오피스 QA
      &lt;/p&gt;
      &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033551&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 28px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 6px; font-weight: bold; font-size: 0.95em; transition: background-color 0.2s;&quot;&gt;채용공고 보기&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;style&gt;
  div[style*=&quot;transition: transform 0.2s;&quot;]:hover {
    transform: translateY(-5px);
  }
  a[style*=&quot;transition: background-color 0.2s;&quot;]:hover {
    background-color: #45a049;
  }
&lt;/style&gt;</content:encoded></item><item><title><![CDATA[빅뱅 배포, QA는 어떻게 살아 남았나: GMS 프로젝트 테스트 전략 백서]]></title><description><![CDATA[올리브영에서 SCM 도메인 QA 엔지니어 겸민입니다. 이 글은 올리브영의 글로벌 WMS 시스템인 GMS(Global Warehouse Management System…]]></description><link>https://oliveyoung.tech/2025-09-08/gms-qa-strategy/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-09-08/gms-qa-strategy/</guid><pubDate>Mon, 08 Sep 2025 18:00:00 GMT</pubDate><content:encoded>&lt;style type=&quot;text/css&quot;&gt;
    .info-box {
        background: #fcfcfc;
        border: 1px solid #ccc;
        border-radius: 5px;
        color: #333;
        margin: 10px 0 1em 0;
        min-height: 20px;
        padding: 10px 10px 10px 36px;
    }
    .info-box span, li,
    .info-box-red span, li,
    .info-box-yellow span, li,
    .text-box span {
        font-size: 14px;
    }

    .info-box li:before {
        background-color: #a4d233 !important;
    }

    .info-box p,
    .info-box-red p {
        margin: 0; important;
        font-size: 15px;
    }
    figcaption {
        margin: 0 !important;
        padding: 0 !important;
    }
    .side-by-side {
        width: 100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
        flex-wrap: wrap;
        overflow-x: hidden;
    }
&lt;/style&gt;
&lt;p&gt;올리브영에서 SCM 도메인 QA 엔지니어 &lt;strong&gt;겸민&lt;/strong&gt;입니다. 이 글은 올리브영의 글로벌 WMS 시스템인 &lt;strong&gt;GMS&lt;/strong&gt;(Global Warehouse Management System)의 &lt;strong&gt;구축부터 서비스 안정화에 이르기&lt;/strong&gt;까지의
검증 여정을 담았습니다. 특히, &lt;strong&gt;테스트 전략 수립, 시나리오 기반 통합 테스트 및 운영 환경 검증&lt;/strong&gt; 등 실제 프로젝트 현장에서의 구체적인 경험과 노하우를 소개 드립니다.&lt;/p&gt;
&lt;h1 id=&quot;왜-gms-프로젝트-테스트는-특별해야-했을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-gms-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94-%ED%8A%B9%EB%B3%84%ED%95%B4%EC%95%BC-%ED%96%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;왜 gms 프로젝트 테스트는 특별해야 했을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 GMS 프로젝트 테스트는 특별해야 했을까?&lt;/h1&gt;
&lt;p&gt;물류 시스템을 새로 구축하는 프로젝트는 늘 복잡하지만, GMS 프로젝트는 특히 어려운 도전이었습니다. 기존 시스템을 단계적으로 전환하는 대신, &lt;strong&gt;‘빅뱅 배포(Big Bang Deployment)’, 즉 모든 시스템을 한 번에 전환하는&lt;/strong&gt; 방식을 선택했기 때문입니다. 이는 마치 기존 도로를 완전히 폐쇄하고 새로운 고속도로를 단숨에 개통하는 것과 같은 일이었습니다.&lt;/p&gt;
&lt;p&gt;특히 백오피스, CBT(Cross-Border Trade), WCS(Warehouse Control System) 등 수많은 연계 시스템이 복잡하게 얽힌 환경에서는 단 하나의 오류라도 발생하면 시스템 전체가 마비되는 치명적인 리스크를 안고 있었습니다. 따라서 우리의 테스트 전략은 단순히 기능 검증을 넘어, &lt;strong&gt;전환 직후의 안정성과 데이터 정합성&lt;/strong&gt;까지 전방위적으로 점검해야 했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 550px !important; max-width: 100%; margin:auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6f1325a7b7a935c5890b32e25f7177b5/2eb59/gms_qa_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEz0lEQVR42gHEBDv7AKaIdaqLd7CQe7GRfL2agsejh9GrjWBLNScWBpViNJ9pNaVsNqRsNp1nNJZiMk4yGDgoFDUkESsdDSobCwCrjHmsjXqykn6zlH/FoYfJpYnYsJJfTT4kFgaTYTSdaDelbTqhazmaZziVYzRSNx1FMx5DLxg1JRExIRAAwJuAwJ2DvJuEwp+Gy6eLyqWJ2rKUe2BJLRYBl2M0kmAzilowl2M0lF0rjVgoRCgOOSQOOycQLx4MKx0MAMGcgsSfhsGehsaiicumisulidmyk5x/ZTwvIGNGLB4RBxQIA0wvGJB4Xo96YV5ZTEI/NDQsHjInGDQmEwC9moG6l4HFoYjJpYrFoorEoYjYsJCwnIszRFATFBYcCgJuPia2b0NxjZNVor1MhJguX3ciXX0kWHQjTWMAupiAuZeCx6OJzKeMyaWLx6OI2LCQtJ+MIisvGxEMVywVoVw63YdTZoSGNIeoF1BoG1JwJnKcJmWHIG6XAMGdgsSghcikicumi82oi86pi9Sri8OWdyYbFTkdEXQ9IW46JLptRHt2Z3aDXXpVWxoxQBg+UhUwPxQ3SwDJooXNpYjQp4rLponLqIzFoISLWj67akJUNCMgGhZmOB5oNB2xZUJePyhDKwJRIxAzHw4lFwkcFAwkGxIAiHVofXBndG1opYp1z6aGvZV3ols2wHFIUj0uKyMbOxwJYTAVw3A/pYhyeHJvTEY7SDkra1M+VT0pZUgwACc5SjI1Ry8vQmNaU2RmY0xdY0ZISTtAQhsyPhkxPRsrNCIuNT1CQ0dSVTlQXFZdXoBmCm5sNUJfcTZKUgAZJTAzJzQ2JzdOTEoeNEEAIzoMNU8NPVwJMEkGKD4NNE4KMUoCIzsCIDYAHzgcLjKeix/FzqrP7Pyr1OQAFyw6KCg4PCs8MTQ0FSMsABAjFztUNmqLDSxEBhoqMmSFJlFtFzRIM2iLAyA2ESEupa6ZyeHayuTvmtjvABYuPR8lNUUtQSInKgsYISJafzpxkg80TTJ3ohtGYQYbKgYgMhNBXypbeQMvTRgpM4dyAaOta8ro+sTi6gAdOUgaKjs+OU8hLDMNGycQO1okSWEKJjoUPFgPLkQGHzIIIjZLk7xUt+wKXpAXIyqblky3zLHG5fbH4usAHzxKFSc4KS5CGyoyCx0sABElFztTJE5pJU5pFjJFABsvAR0zByc9CCtDACE4Gis4m73Qrd7yvtvlt9zqAClJVwsnOQkpPhAkMQ8gJgghMRE2UA04VBE/XAgpQAEdLwMeMgEcLwEbLQAZLhUpNafHzcHv/dXv95vb8wAcMTwaIygrKitGNyGXewiTbRyUJD1qJC4ZGxYREhIJDQ8KDxENEhUVGx4dJCUjJyxBSFBsdnqIm6GlvcUAPCsfJhkPJhcQMyERim8IpW0evjFHjDY4PzYmJxoSEQkEIRgRJRwVYk8+jXNdooI2qoQPnnpGiGpUgGdRAD8uIDAqHVNHMmRRGH1fFWpMMFg0LCgWERUPChcRCxQPChYRDBQQCkI0J3thTZd3TqB/Q6mHXXtgSkc0IwB2WEaoVVDKW1nGfDjGnTk9LBUHAwISCgYMCAUJCAQJCAQJCAQODAcAAAAwJyCegGwvJSMYEg9hTT1OPS8XKZFv/j421wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6f1325a7b7a935c5890b32e25f7177b5/263a4/gms_qa_01.webp 480w,
/static/6f1325a7b7a935c5890b32e25f7177b5/a6361/gms_qa_01.webp 960w,
/static/6f1325a7b7a935c5890b32e25f7177b5/0b34d/gms_qa_01.webp 1920w,
/static/6f1325a7b7a935c5890b32e25f7177b5/96d48/gms_qa_01.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6f1325a7b7a935c5890b32e25f7177b5/9aebd/gms_qa_01.png 480w,
/static/6f1325a7b7a935c5890b32e25f7177b5/a91f8/gms_qa_01.png 960w,
/static/6f1325a7b7a935c5890b32e25f7177b5/ac7a9/gms_qa_01.png 1920w,
/static/6f1325a7b7a935c5890b32e25f7177b5/2eb59/gms_qa_01.png 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6f1325a7b7a935c5890b32e25f7177b5/ac7a9/gms_qa_01.png&quot; alt=&quot;테스트 전략을 어떻게 해야될까...&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;이 이미지는 Google Gemini로 생성되었습니다.&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h1 id=&quot;문제-해결의-시작-리스크-식별과-전략-수립&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%98-%EC%8B%9C%EC%9E%91-%EB%A6%AC%EC%8A%A4%ED%81%AC-%EC%8B%9D%EB%B3%84%EA%B3%BC-%EC%A0%84%EB%9E%B5-%EC%88%98%EB%A6%BD&quot; aria-label=&quot;문제 해결의 시작 리스크 식별과 전략 수립 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 해결의 시작: 리스크 식별과 전략 수립&lt;/h1&gt;
&lt;p&gt;처음 GMS 프로젝트 합류했을 당시, GMS 프로젝트는 전체 개발 진척도가 40% 정도 수준이었고, 2주간의 짧은 분석·설계 기간 후 곧바로 기능 단위 테스트(Feature Test)를 시작해야 했습니다. 다행히 PO와 개발자분들의 세심한 지원 덕분에 바쁜 일정 속에도 GMS의 핵심 구조와 흐름을 빠르게 파악할 수 있었고, 이 자리를 빌려 감사의 말씀을 전합니다.&lt;/p&gt;
&lt;p&gt;도메인 구조를 파악한 후, QA 관점에서 시스템 안정성에 영향을 줄 수 있는 다양한 리스크를 선제적으로 식별했습니다. 이 리스크들을 &lt;strong&gt;&quot;Risk Factor&quot;로 정리해 테스트 전략 수립의 기초 자료로 활용&lt;/strong&gt;하였습니다. Risk Factor를 도출하는 과정에서는 GMS와 연계된 시스템의 구조와 물류 데이터 흐름을 면밀히 분석하고 명확하게 정리하는 작업도 병행했습니다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;
    &lt;h3&gt;✅ Risk Factor란?&lt;/h3&gt;
    &lt;span&gt;QA 입장에서 말하는 &lt;b&gt;Risk Factor는 단순한 &apos;버그 가능성&apos;을 넘어서, 시스템 안정성에 영향을 줄 수 있는 모든 잠재 리스크&lt;/b&gt;를 의미합니다.&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;GMS와 같이 여러 서비스가 유기적으로 연동되는 시스템의 경우 &lt;b&gt;인터페이스 규격 불일치, 메시지 큐(이벤트) 지연·중복 등으로 발생하는 데이터 정합성 훼손 등&lt;/b&gt;은&lt;br&gt; 모두 테스트 설계 초기부터 고려해야 할 주요 Risk Factor였습니다.&lt;/span&gt;
    &lt;div style=&quot;align-items: center; padding: 0 !important; margin:0 !important; border: 1px solid&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d8e43257b82eecf5a2ab1d18b0e03d6a/ac7a9/gms_qa_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABa0lEQVR42pVT2W6DMBDk//+secpbD6VSIyUKhIAx2Bif010nISQ91K408oF3PN5ZilpK1EJDjxbOeYQQkFJETAkhxr8hRHjK43lhJou6lTgJCWsdmk7j2CgMSsN5D7+Au8AHD86r6hajmSC6Pq9ZUMFKvCdFMYGjkSM+Sg2hIk69RzME1NKjHyOWkSjPOkd5kS4Jec0o+Il8ix4NJrqlVyN2VUfEJpP0OkASBnNLYjARv4jHuNjLCq9yOZy1eH15xn6/m5V8F5ngQgQeL3vFYwLXar1eY7PZ/Er45YJHwutYlgesVk8QosV/YiZkq/nJo7H5Qys6bHcV9EQ18j+rS2Qi1z7XksyZTbkV+nywHwa8vW8hlYNxCZOLGcbeYN35fFoou3N5IGc7qah+ITf44SigjMM4ncm4rdwCPqTczEqb/CPwfHaZScpjg/2hJnWKSAIq6jtFbTJZTo53Cq6u8p9RN13uDm72M2HCJ1IdXq7VqdH2AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d8e43257b82eecf5a2ab1d18b0e03d6a/263a4/gms_qa_02.webp 480w,
/static/d8e43257b82eecf5a2ab1d18b0e03d6a/a6361/gms_qa_02.webp 960w,
/static/d8e43257b82eecf5a2ab1d18b0e03d6a/0b34d/gms_qa_02.webp 1920w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d8e43257b82eecf5a2ab1d18b0e03d6a/9aebd/gms_qa_02.png 480w,
/static/d8e43257b82eecf5a2ab1d18b0e03d6a/a91f8/gms_qa_02.png 960w,
/static/d8e43257b82eecf5a2ab1d18b0e03d6a/ac7a9/gms_qa_02.png 1920w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d8e43257b82eecf5a2ab1d18b0e03d6a/ac7a9/gms_qa_02.png&quot; alt=&quot;Risk Factor Example&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
        &lt;figcaption&gt;Risk Factor 정리 예시&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;이를 토대로 &lt;strong&gt;상품 → 입고 → 출고 → 배송&lt;/strong&gt;이라는 주요 카테고리를 기준으로 전체 워크플로우를 설계했습니다. 그 과정에서 &lt;strong&gt;개발 단계에서도 E2E(End-to-End) 통합 테스트가 반드시 필요하다는 점&lt;/strong&gt;을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;그러나 현실적인 제약도 존재했습니다. 각 연계 시스템의 개발 환경 지원 수준이 달랐기 때문에, 먼저 &lt;strong&gt;E2E 테스트 가능 범위를 확인&lt;/strong&gt;해야 했습니다. 예를 들어, 특송사 샌드박스는 응답이 불안정해 운송장 생성 단계에서 테스트가 제한되었고, WCS는 아예 개발 환경이 없어 출고 관련 E2E 테스트 자체가 불가능했습니다. 이러한 제약 속에서 저희는 협력 부서와 여러 차례 논의를 거듭하며 &lt;strong&gt;&quot;개발 단계에서 검증 가능한 영역 vs. 불가능한 영역&quot;을 명확히 구분&lt;/strong&gt;했습니다. 이후 각 구간별로 &lt;strong&gt;테스트 전략을 분리·수립&lt;/strong&gt;함으로써 한정된 환경에서도 유연하고 실효성 있는 테스트를 이어갈 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;단계별-전략적-테스트-견고한-시스템을-만드는-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A8%EA%B3%84%EB%B3%84-%EC%A0%84%EB%9E%B5%EC%A0%81-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%AC%EA%B3%A0%ED%95%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EB%A7%8C%EB%93%9C%EB%8A%94-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;단계별 전략적 테스트 견고한 시스템을 만드는 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;단계별 전략적 테스트: 견고한 시스템을 만드는 과정&lt;/h1&gt;
&lt;p&gt;GMS는 앞서 언급한 것처럼 백오피스, 글로벌몰, CBT, WCS 등의 다양한 외부 시스템과 연계되는 구조로 설계되어 있으며
데이터 Interface 방식 또한 API, MQ(MSK), BATCH 등 여러 방식을 활용하도록 구성되었습니다.&lt;/p&gt;
&lt;div style=&quot;width: 800px !important; max-width: 100%; border: 1px solid; margin: auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2adf8f337358d85c7daf5570e7190768/5d273/gms_qa_03.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 90.20833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAC4jAAAuIwF4pT92AAACcUlEQVR42pWUCW/aQBSE8///V6W2idSk5CAc4QY1GNt7+GCv6ew6CialrQJCthbtp3lv5r0r9D5GBLSZBwLg6zVsdgu4GtJrjNoxhBeoNFAUgFANvlzfYThZQLTAjnctr15FkHMOxli0wkFlNYIlXG9QHcaAlSi8xLSZonBFAkoJZLnG1x+PeHieo2yAV54f3RswhADvPIz0aAoDbwOF7dAW0ygbpZPYVBs+ywRUqgPeDJ7xNFlCEJhVfSC/3gccS0dVLYE8q1fw4j4BCwIXaoHcFtCqU7g/KNz8fMbjeIlSW+ylPQG997CWZcsOGvgHmjVCBLLk0iss68V7ye8KI5AKZWWwZyPPgI4dPQoPnZ0UhnJAhTIpjMDc5mcKr++GeBjRlMojU/7Pkg2BTW6SwtBsYcXoXWHs4UWF466H+4+mOJpCIbAlEtA3O5hy0gGdwkqtCSz/CswuAW0PiIYli0ECCuZwrQmkKVV1AVgHAt3/gCdTRDRFL3EwBygaJxji10zi++0T7mMOK4uDMh9NcWclnwGdxq7eojCMjT6ZMpmv8bLgORXu+zmMk2Kt/QeQPdQr5BFIN4WwCTiczAll4GPJFT5RMoGph6ZkyQ5F2RKo2b8ZxrMIxOeA0eVttU1A3XP5fjjF6GWFooqmhE8AacrsOEs5rKlEvwGTwvmWwbbI1fFjsC8BBz3gy1sOHaRoUw+jy3FSSk7Kvj8p1lmuLwMn+J53wMB9aIohD7rlsJI05VjQYct92KQefouxGXXrK02K7a+vGB0211UxR3weSzQqI7CG9jV+mVcoq9HwcvxJLtjJYoPVLgN3AySXbFywvwFT0momAV6xUwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2adf8f337358d85c7daf5570e7190768/263a4/gms_qa_03.webp 480w,
/static/2adf8f337358d85c7daf5570e7190768/a6361/gms_qa_03.webp 960w,
/static/2adf8f337358d85c7daf5570e7190768/0b34d/gms_qa_03.webp 1920w,
/static/2adf8f337358d85c7daf5570e7190768/da28f/gms_qa_03.webp 2880w,
/static/2adf8f337358d85c7daf5570e7190768/98b7d/gms_qa_03.webp 3840w,
/static/2adf8f337358d85c7daf5570e7190768/84908/gms_qa_03.webp 6863w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2adf8f337358d85c7daf5570e7190768/9aebd/gms_qa_03.png 480w,
/static/2adf8f337358d85c7daf5570e7190768/a91f8/gms_qa_03.png 960w,
/static/2adf8f337358d85c7daf5570e7190768/ac7a9/gms_qa_03.png 1920w,
/static/2adf8f337358d85c7daf5570e7190768/f9c26/gms_qa_03.png 2880w,
/static/2adf8f337358d85c7daf5570e7190768/5da7e/gms_qa_03.png 3840w,
/static/2adf8f337358d85c7daf5570e7190768/5d273/gms_qa_03.png 6863w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2adf8f337358d85c7daf5570e7190768/ac7a9/gms_qa_03.png&quot; alt=&quot;주요 워크플로우 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;상품, 입고, 출고, 배송 기준 워크플로우&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;상품·입고·출고·배송이라는 4대 주요 카테고리를 중심으로 구성한 워크플로우를 시스템 관점에서 실제 연동 방식과 데이터 흐름 기준으로 재정의했습니다. 이 워크플로우를 시스템 연동 방식과 데이터 흐름에 따라 &lt;strong&gt;Interface Spec(인터페이스 명세)으로 세분화&lt;/strong&gt;하였습니다. 정리된 11개의 Interface Spec은 &lt;strong&gt;시스템 간 인터페이스 검증 기준이자 E2E 테스트용 Test Suite&lt;/strong&gt;의 기반이 되었습니다.&lt;/p&gt;
&lt;p&gt;동시에 &lt;strong&gt;GMS의 기능 단위를 Feature 단위로 나누어 Feature List를 작성&lt;/strong&gt;하여 이는 기능별 단위 테스트의 검증 기준으로 삼았습니다. 이처럼 정리된 Interface Spec과 Feature List를 기반으로 Test Case를 체계적으로 설계한 뒤, 기능별·단계별 테스트 일정을 마일스톤 기반으로 스케줄링하여 전체 테스트 플랜을 수립했습니다.&lt;/p&gt;
&lt;div class=&quot;side-by-side&quot;&gt;
    &lt;div style=&quot;display: inline-flex; width:100%; border: 1px solid&quot;&gt;
      &lt;div style=&quot;width:800px;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5216aba31847d3cd849c060937d88879/4e069/gms_qa_04_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 39.583333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABVUlEQVR42lWRiW7EIAxE8/8f2VWT3STkAnKQA8jU4616IFmAbB4zdvFqeryjQ9uNMBoTzvNCShnXlXDFiMhIcr4i9uPEIfkYL+ScZP+NwrpFHkeMk8M4Ovh5hbUe/TAh7IfCRrkv66Y5zfsZbt5QmRW9E7h8wjpGscsjrr6f8PF4onq2qMpagVkU4r7RyXldA7awI0gkKo0ZvY9wa8Sdb63jXtACFxV+lo3ar6oanVi/pShLkfOLqqXVEA4F7mfCazgxeLFN2HdtkXNWIG2WAiqrBo9HBWMGLWB+3YIC/fwGZwFuR8Sj3WHspXU/QFrgZZD+0W4rIEKpOAmMuX6wapnqqJKDCAIsO/kkJJFzq0oF0g4n5tyMpu60l3VtMIuaG++fx8lr/w6BcfpUfcj06/GEZQ//WrYKPBVgTI9OFBLKYallKVxEHe+c9KEKBSg9NPY/kPEFeytqhIw9ThcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5216aba31847d3cd849c060937d88879/263a4/gms_qa_04_01.webp 480w,
/static/5216aba31847d3cd849c060937d88879/a6361/gms_qa_04_01.webp 960w,
/static/5216aba31847d3cd849c060937d88879/0b34d/gms_qa_04_01.webp 1920w,
/static/5216aba31847d3cd849c060937d88879/6beb8/gms_qa_04_01.webp 2324w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5216aba31847d3cd849c060937d88879/9aebd/gms_qa_04_01.png 480w,
/static/5216aba31847d3cd849c060937d88879/a91f8/gms_qa_04_01.png 960w,
/static/5216aba31847d3cd849c060937d88879/ac7a9/gms_qa_04_01.png 1920w,
/static/5216aba31847d3cd849c060937d88879/4e069/gms_qa_04_01.png 2324w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5216aba31847d3cd849c060937d88879/ac7a9/gms_qa_04_01.png&quot; alt=&quot;Interface Spec 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
      &lt;div style=&quot;width:30%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1554px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3ef856af2778f1d7b0543f9cffe064ee/f0e87/gms_qa_04_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 102.50000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAABYlAAAWJQFJUiTwAAABxUlEQVR42q1Ui26DMAzk/z9yq+iDd3mTBkg8n0MQnVYVtiFZdaC53PkcB0l2p2ucUZQUpMeR/voEaV5RWbV0rxo631LqesU5r8uGD5jkfVZU9NAjqYeWQN52A5V1y7l+eh/0gyJjrSyqppMcvwDBUzLgZ3gTgNlYmmYjoaeZD5zXtY/ALlSttStt5H7dMVB4Tahn5u6jC/lu6SnwKqAXgP4Bs8tSCvfNhTF2zbfxFrBm+adzTINST+yNMWu+jVUyauhZNO1ARVkvgD1do4xQ60OAZlM3Ixtc3jDgKYxIKf0/klHDjxNcdgwBhJjZUZ9vYxfgGS4Pj30M0Q44beS+GhZZ2Hxfati0TjKk76qhr5VZ/uRkmbWGHZsRpwWleUkj3xwP+hLwJ5nYqJST2LLj4SUWxvMG5BAgWsjXDK10iVLK77WUZnfbjHwvccEF8DHKUNBLb8Y8iaq6k4PemuIBIWdiYzzDkgG0nuSmYAqhuXHgr9oGG8EQrCoeUTAF0weO+0MPNTZMGbwp3Ie3JJcBDObemEOmYLD2673uWXIiQxj1dAwPAmJjmpUyIFDDW5xTwS5vZb425dsfvEwwA1PIh9vI9wB+Afb9c/fotP3qAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3ef856af2778f1d7b0543f9cffe064ee/263a4/gms_qa_04_02.webp 480w,
/static/3ef856af2778f1d7b0543f9cffe064ee/a6361/gms_qa_04_02.webp 960w,
/static/3ef856af2778f1d7b0543f9cffe064ee/80059/gms_qa_04_02.webp 1554w&quot; sizes=&quot;(max-width: 1554px) 100vw, 1554px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3ef856af2778f1d7b0543f9cffe064ee/9aebd/gms_qa_04_02.png 480w,
/static/3ef856af2778f1d7b0543f9cffe064ee/a91f8/gms_qa_04_02.png 960w,
/static/3ef856af2778f1d7b0543f9cffe064ee/f0e87/gms_qa_04_02.png 1554w&quot; sizes=&quot;(max-width: 1554px) 100vw, 1554px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3ef856af2778f1d7b0543f9cffe064ee/f0e87/gms_qa_04_02.png&quot; alt=&quot;Feature List 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;Interface Spec &amp;amp; Feature List 예시&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;설계된 테스트 플랜을 바탕으로 테스트는 단계별로 세분화하여 진행하였으며, 각 단계에서는 &lt;strong&gt;단순 통과율보다는 &apos;무엇을 검증해야 하는가&apos;에 초점을 맞춘 전략적 접근을 적용&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;-기능-단위-테스트-feature-test-결함-발견율에-집중하다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B8%B0%EB%8A%A5-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-feature-test-%EA%B2%B0%ED%95%A8-%EB%B0%9C%EA%B2%AC%EC%9C%A8%EC%97%90-%EC%A7%91%EC%A4%91%ED%95%98%EB%8B%A4&quot; aria-label=&quot; 기능 단위 테스트 feature test 결함 발견율에 집중하다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 기능 단위 테스트 (Feature Test): 결함 발견율에 집중하다&lt;/h2&gt;
&lt;p&gt;기능 단위 테스트는 개발 완료된 기능 모듈별로 진행했습니다. 이 단계에서는 &lt;strong&gt;테스트 종료 기준(Exit Criteria)을 단순한 결함 수정률이 아닌, 결함 발견율 자체&lt;/strong&gt;에 두고 운영하였습니다.
테스트 목적은 &lt;strong&gt;‘완성된 기능을 빠르게 통과시키는 것’이 아니라, 가능한 많은 취약 지점을 사전에 식별&lt;/strong&gt;하는 것이었기 때문입니다.&lt;/p&gt;
&lt;div style=&quot;width: 550px !important; max-width: 100%; margin: auto; border: 1px solid&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 625px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/07ad64bf7078339ab53283351374fc0a/b1b4f/gms_qa_05.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 91.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAsTAAALEwEAmpwYAAACoklEQVR42p1UW0/UQBTuj8MYn/wZGkJIfPDJZ02M8dX4gIo+iIH4qIg+AJFgiCESCRezC8ve2t3ett3O9DZtP89Mt0VhIdGTnJyZzpmv33fOabUH6+8wu/oCc2uvMP95Efe+vMGd1QXMry2qZ3OfXmKW4q3lx5h5+xA3lh795TPkt1ee4CbFux8XoKUiQ14UyrMiR5bn5zGnMwCx50JEoVrndDbdC4gsg6Yu0iKOYxQoavBCOj2XFvzYRWzodEqA8iVTAOVdSUAB5uRpmqoLRQUmgSeAzt42WPNIrQvK/TOndjqTWFqeFwpQMsRFwLwEbO4sYffr02sBcQ5YMkyS5BJgKbvA4NcWGnvLCLldyrsesGRSMbxomYhhjY5gnX7H/vZzsMSanBSXcyVgNpEghKAo11kdpYmUwx4dI9a7+Pb+Pn4evp4okc3I6nxcrKFsylSGeQLLLRvCfR0H689qiVcylDZmNhqdDTS6m+gMtnFCsdHeQEvfhGEfIAxjyKbbw2McNj6g2dlEu79FcR2t3g6qXmhZVgImSQTT6sN2DETxGO5oiKHZQxj5iGioXddBHEVUmlQ9P88bwDT7SqGcXyVZDnZEydOsUjcOBcJEoFJ0OU/iEEOZzzmHrutlHQhcNkh6JYNHCfp2AN1hOBv4ap+kNBmpUMzknTzPqxoWaljdkU+1MKl2Q5geR9/y0GwbVE8Hth/BGXnwvRECqqXhBDjtDtDWTVheiDPdUfuYZlmTH7TUnhKjIAjAGCd25Vz64wA8jJRszkM6ZzUbzlhdJvlRMNqnxFgLeYRWq4f9oxM0KfborfrAgu36xFTAcAVJTeGxrK7pFVMzkRxzOEMdpx0DxtBCu2uo39D/mAJMRAQW+vB8BkayvDFTkquG/IvLe78Bp9xdC0aWCg0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/07ad64bf7078339ab53283351374fc0a/263a4/gms_qa_05.webp 480w,
/static/07ad64bf7078339ab53283351374fc0a/529ab/gms_qa_05.webp 625w&quot; sizes=&quot;(max-width: 625px) 100vw, 625px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/07ad64bf7078339ab53283351374fc0a/9aebd/gms_qa_05.png 480w,
/static/07ad64bf7078339ab53283351374fc0a/b1b4f/gms_qa_05.png 625w&quot; sizes=&quot;(max-width: 625px) 100vw, 625px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/07ad64bf7078339ab53283351374fc0a/b1b4f/gms_qa_05.png&quot; alt=&quot;기능 단위 테스트 결함 추이&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;기능 단위 테스트 기간 결함 추이&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;이를 위해 기능 단위에서의 오류 처리와 필수 파라미터 누락, 예외 케이스 등을 고려한 Negative Test를 설계했습니다. 그 결과, &lt;strong&gt;로직의 견고함과 에러 핸들링의 완성도&lt;/strong&gt;를 높일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;특히 입고/출고와 같은 재고 처리 로직은 기능 단위 테스트에서 정상 동작이 확인되더라도, 실제 운영 환경에서는 여러 사용자가 동시에 요청을 보낼 수 있는 상황이 빈번하게 발생합니다. 따라서 정상 기능 검증과 별개로 &lt;strong&gt;동시성에 대한 안정성 검증&lt;/strong&gt;이 반드시 병행될 필요가 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;-동시성-테스트-concurrency-test-보이지-않는-충돌을-막아라&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%85%8C%EC%8A%A4%ED%8A%B8-concurrency-test-%EB%B3%B4%EC%9D%B4%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%B6%A9%EB%8F%8C%EC%9D%84-%EB%A7%89%EC%95%84%EB%9D%BC&quot; aria-label=&quot; 동시성 테스트 concurrency test 보이지 않는 충돌을 막아라 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 동시성 테스트 (Concurrency Test): 보이지 않는 충돌을 막아라&lt;/h2&gt;
&lt;p&gt;기능 단위 테스트가 기능 자체의 견고함을 검증하는 단계였다면, 동시성 테스트는 &lt;strong&gt;복잡한 운영 환경에서 발생할 수있는 충돌 가능성을 선제적으로 제거하기 위한 보완적 검증 단계&lt;/strong&gt;였습니다.
특히 입고·출고와 같이 재고 수량을 처리하는 로직에서는 &lt;strong&gt;동일 데이터를 여러 요청이 동시에 접근·변경하는 상황&lt;/strong&gt;이 자주 발생합니다. 이 경우 예기치 못한 &lt;strong&gt;Race Condition이나 데이터 충돌&lt;/strong&gt;이 발생할 가능성이 높기 때문에, 기능 단위 테스트만으로는 안정성을 보장하기 어렵습니다.&lt;/p&gt;
&lt;p&gt;이에 따라 저희는 &lt;strong&gt;동일 상품에 대해 복수의 입고 요청이 동시에 발생하는 시나리오&lt;/strong&gt;를 구성했습니다. 그리고 GMS가 내부적으로 요청을 어떻게 직렬화하고 충돌 없이 처리하는지, 또 Lock 처리 메커니즘이 정상적으로 작동하는지를 집중적으로 검증했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 550px !important; max-width: 100%; margin: auto; border: 1px solid&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1485px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/377bc581e2dffca6be6d6ba39ca2f2b0/9aaae/gms_qa_06.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAC4jAAAuIwF4pT92AAADT0lEQVR42p2U61NaVxTF/WPbDx1n+pgxzUz6IU3HdkxTTcaooUotBm2qElpNCEoeYm0iQUFQQLhcvKjhIUKCvC+vX8/FokbFznTPMHMPe5911tlr7dPF/4xms3np/12XJRuNJh3qT2o/ZI/Y2AxwlMujqtWPAXP5PP7gDl6/jGtTIroXExsbVwJKssKwfgb9wz8IR6InuRbg0VEeo+kVN0ee851+FbvDh9W2jKzEThhrv9Mb1IknkgS2w4SkCJnMeyqVyilg4uCQ/pFpJv50MmiwYbM7GdCZeb0WaAEUCgWcri0BdMw6l8vhcruw2RZZtttxOFZZWFigVqsdA2qFsiwTich4PW78fj+eTb9gkWgBhCK79A9NUS5XqIpNbdCEyKdSKbLZLOl0+pShFqVSibzopZbUihv1GnJEYWHpDbsvbDy/8Q2ht07mra8oFUutPfV6nWq1elHls6GqaqtQi0g0wc9TFlau9xC9/hX2az0MP3xKsVhu5cvl8glgW6yu8+ppgBrbwLbE4osVjgolzBOPmPzkM16a5jjMFnhqXSKdybQAz1uuq5MtInsH6AW7qLKLYcbKoPEJU/MruLcCjBjmqKrVzsY+H42qSiYWp1ipUREbfVIUy8s3/O3wogoRPsSSNI5P/29A7WTFasFz7UsSmx6a4lpCLdRgkGZYoi7ckHGtofy1TPZfF5wFvtDDjKLg/qKbre5P2e7rpbSzQ2N/n3zqQJOVgjCwr/dbXJ93473TR7Ve68CwLUqxQOj+PeSBO6RCQVRxOVn08/HiBqnD92KGs4RFTjtQNuipXSWKZpd4MsVe/ABl9x1R0auY6OWaK8DdcRt7+wmS6QyeVSfe+SdI4Qg70XcfjeUFwN8emZgxWzFOzTJwX8+QbkJYKEgsmSEeT6AbM/Lg198xzs4xOj7FvUGd8KJ6/vk6XmyHIty8a+LW7VGGxmb5sV/Hjb5Jxmfsgk2UJUeQr29P80P/GD29E3z/0yjDvzzm9VvP5QwV8WwZzA6cnhDP7D4c6z4s9i1MzxyolRK25Q2mLetigmJMzq+Lpy6MedGNcyOo0erkw+aJ4vl8DllYZTeqEApJSFKI7YCPfaF6u7YNdKWxz46T9t1et79P1xcn5R83UL8oeB5ojQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/377bc581e2dffca6be6d6ba39ca2f2b0/263a4/gms_qa_06.webp 480w,
/static/377bc581e2dffca6be6d6ba39ca2f2b0/a6361/gms_qa_06.webp 960w,
/static/377bc581e2dffca6be6d6ba39ca2f2b0/db5f9/gms_qa_06.webp 1485w&quot; sizes=&quot;(max-width: 1485px) 100vw, 1485px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/377bc581e2dffca6be6d6ba39ca2f2b0/9aebd/gms_qa_06.png 480w,
/static/377bc581e2dffca6be6d6ba39ca2f2b0/a91f8/gms_qa_06.png 960w,
/static/377bc581e2dffca6be6d6ba39ca2f2b0/9aaae/gms_qa_06.png 1485w&quot; sizes=&quot;(max-width: 1485px) 100vw, 1485px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/377bc581e2dffca6be6d6ba39ca2f2b0/9aaae/gms_qa_06.png&quot; alt=&quot;동시성 테스트 시나리오&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;입고 작업 시 동시성 발생 시나리오&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;동시성 테스트를 통해 다음과 같은 &lt;strong&gt;주요 리스크를 사전에 확인&lt;/strong&gt;할 수 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;동일 요청이 거의 동시에 발생했을 때 재고 데이터가 중복 처리되는 현상&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lock 처리 로직이 제대로 동작하지 않아 발생하는 Race Condition&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;비정상 상태에서의 Deadlock 또는 데이터 정합성 오염&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;이러한 리스크를 사전에 식별하고 검증함으로써 &lt;strong&gt;운영 단계에서 발생할 수 있는 데이터 충돌 이슈를 효과적으로 차단&lt;/strong&gt;할 수 있었습니다. 또한 API 로직이 동시 요청 환경에서도 안정적으로 동작한다는 신뢰를 확보하여, QA 관점에서 시스템 안정성 보장의 핵심적인 검증 과정이 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;-통합-테스트--e2e-시나리오-검증-데이터-정합성까지-놓치지-않는-심화-검증&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8--e2e-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4-%EA%B2%80%EC%A6%9D-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1%EA%B9%8C%EC%A7%80-%EB%86%93%EC%B9%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%8B%AC%ED%99%94-%EA%B2%80%EC%A6%9D&quot; aria-label=&quot; 통합 테스트  e2e 시나리오 검증 데이터 정합성까지 놓치지 않는 심화 검증 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 통합 테스트 &amp;#x26; E2E 시나리오 검증: 데이터 정합성까지 놓치지 않는 심화 검증&lt;/h2&gt;
&lt;p&gt;통합 테스트 단계에서는 &lt;strong&gt;기능 단위 테스트를 통해 검증된 기능들을 실제 운영 흐름과 동일한 방식으로 연결&lt;/strong&gt;하여 &lt;strong&gt;E2E 시나리오 기반 테스트&lt;/strong&gt;를 중점적으로 수행했습니다. 특히 GMS는 백오피스, 글로벌몰, CBT, WCS 등 다양한 외부 시스템과 연동되어 있었고, 이들 시스템 간 인터페이스는 대부분 &lt;b&gt;Kafka 기반 MQ(MSK)&lt;/b&gt;로 이루어져 있었습니다. 따라서 &lt;strong&gt;메시지 기반 비동기 통신의 신뢰성&lt;/strong&gt;과 &lt;strong&gt;데이터 정합성 검증&lt;/strong&gt;이 핵심 테스트 포인트가 되었습니다. 이번 통합 테스트에서 가장 중점적으로 둔 전략적 포인트는 두 가지입니다.&lt;/p&gt;
&lt;h3 id=&quot;-1-interface-spec-기반-e2e-test-suite-구성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-1-interface-spec-%EA%B8%B0%EB%B0%98-e2e-test-suite-%EA%B5%AC%EC%84%B1&quot; aria-label=&quot; 1 interface spec 기반 e2e test suite 구성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔹 1. Interface Spec 기반 E2E Test Suite 구성&lt;/h3&gt;
&lt;p&gt;초기에 정의한 총 11개의 Interface Spec을 기준으로, 실제 사용자 행위를 반영한 &lt;strong&gt;업무 중심의 시나리오(E2E Test Suite)를 설계&lt;/strong&gt;했습니다. 각 테스트 케이스는 단순 기능 검증에 머물지 않고, &lt;strong&gt;업무 단위(예: 출고 지시 → 재고 할당 → 출고 확정)의 연계를 검증&lt;/strong&gt;하는 방향으로 구성했습니다. 이를 통해 시스템 간 메시지 흐름이 정상적으로 연계되는지, 전체 프로세스가 의도한 대로 작동하는지를 종합적으로 검증할 수 있었습니다.&lt;/p&gt;
&lt;div style=&quot;width: 700px !important; max-width: 100%; margin: auto; border: 1px solid&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e89a5e1ab6624082c30f943bd8d3cb5c/98f2e/gms_qa_07.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 102.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACHElEQVR42p1UCW7bQAz0/99XtGlcX7Lu+7a8OpgZOkJsQ4rRCCC4q90dXkNuoiSTg+VKVlSSZqWUVSMJ9E+/TXvpxHFDSdMCgIUkaa4Sx5nqLC91HSf4j3NqGl0FHMdRfD8W6+SIbftiO4EcD7ac4LXrRSoW1uezJzYM244vDqQoa8mLWuq6lRwRFXklfd/LJoYXfphIFOeSwpu6vcgESzS09E0TzyY9n7B5FIbcdhoqrZewSslhLYhS8YNEwiiDsUxCSAVvus6oGGOWQ75ZHOV6NTIMI2RQ1wlyOLliIQ37owPgVO98eTotAzIXUcLkV8ICzZfnB1T3j+9DXARsEHKMCvtBKh08KOtGwSlVfZGquaihq+nl0l31DoX7lZAnzcfsncFFhsZCbXdn+bc/K0+5b1CwlyE/H8x78m0LsB3y9w7gZ+79N6AXxPLn/Shv2xMALeyTB9BVQFa470et8Hh3qUIuE+SWuWTRZtq8BCSZyTk+IqkHGgB1lohNEJ5TSO5FQFaO/cqQDPh3Gw6FeueiJWfvalSb51djVMx3VabM4c46x/T5/XevpKamgUd+Tt8D3gj8dZGNz6IQkPoXQI8YEOykH1WZoe8wdQ6WpyFT6HX/CpCdwZzlaMEO+XwGX3u4Og9JhfizAOwEJr/Ev6JslCZsTWr+rz6FbFgtCulh+kHHPzX7tG1voJYdiOOFOnEYMkcc1yT9uBLyB3swFcPyufLWAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e89a5e1ab6624082c30f943bd8d3cb5c/263a4/gms_qa_07.webp 480w,
/static/e89a5e1ab6624082c30f943bd8d3cb5c/a6361/gms_qa_07.webp 960w,
/static/e89a5e1ab6624082c30f943bd8d3cb5c/0b34d/gms_qa_07.webp 1920w,
/static/e89a5e1ab6624082c30f943bd8d3cb5c/64c32/gms_qa_07.webp 2012w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e89a5e1ab6624082c30f943bd8d3cb5c/9aebd/gms_qa_07.png 480w,
/static/e89a5e1ab6624082c30f943bd8d3cb5c/a91f8/gms_qa_07.png 960w,
/static/e89a5e1ab6624082c30f943bd8d3cb5c/ac7a9/gms_qa_07.png 1920w,
/static/e89a5e1ab6624082c30f943bd8d3cb5c/98f2e/gms_qa_07.png 2012w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e89a5e1ab6624082c30f943bd8d3cb5c/ac7a9/gms_qa_07.png&quot; alt=&quot;인터페이스 테스트 시나리오&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;인터페이스 테스트 시나리오 예시&lt;/figcaption&gt;
&lt;br&gt;
&lt;h3 id=&quot;-2-interface-contract-test--데이터-정합성-및-스키마-호환성-검증&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-2-interface-contract-test--%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%EB%B0%8F-%EC%8A%A4%ED%82%A4%EB%A7%88-%ED%98%B8%ED%99%98%EC%84%B1-%EA%B2%80%EC%A6%9D&quot; aria-label=&quot; 2 interface contract test  데이터 정합성 및 스키마 호환성 검증 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔹 2. Interface Contract Test – 데이터 정합성 및 스키마 호환성 검증&lt;/h3&gt;
&lt;div class=&quot;info-box&quot;&gt;
    &lt;i&gt;여기서 말하는 Interface Contract Test는, &lt;b&gt;일반적인 Consumer Contract Test보다 확장된 개념으로, MQ 기반 메시지와 수신 시스템(DB) 간의 스키마 정합성까지 포함&lt;/b&gt;한 테스트를 의미합니다.&lt;/i&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;MQ 기반 인터페이스의 특성상, &lt;strong&gt;메시지가 성공적으로 전송되었다고 해서 실제 데이터가 문제없이 반영된다고 보장할 수는 없습니다.&lt;/strong&gt; 수신 시스템의 &lt;strong&gt;DB 스키마&lt;/strong&gt;와 &lt;strong&gt;메시지 필드&lt;/strong&gt; 간에 다음과 같은 사양 차이가 존재할 수 있기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;데이터 타입(Type)&lt;/li&gt;
&lt;li&gt;컬럼 크기(Size)&lt;/li&gt;
&lt;li&gt;NULL 허용 여부&lt;/li&gt;
&lt;li&gt;ENUM/코드값 정의 여부&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;이 경우 &lt;strong&gt;데이터 유실&lt;/strong&gt;, &lt;strong&gt;오류&lt;/strong&gt;, 또는 &lt;strong&gt;Silent Failure&lt;/strong&gt;가 발생할 수 있습니다. 이를 방지하기 위해 시나리오 테스트와 함께 &lt;strong&gt;Interface Contract Test&lt;/strong&gt;를 함께 수행했고, 주요 검증 항목은 다음과 같습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;메시지 필드 ↔ DB 컬럼 간 데이터 타입 불일치 여부&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VARCHAR 컬럼 크기 초과&lt;/strong&gt;로 인한 값 절단 또는 예외 발생 여부&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NULL 제약 조건 위반&lt;/strong&gt; 상황의 처리 여부&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ENUM/코드값 등의 미정의 값 처리&lt;/strong&gt; 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;div style=&quot;margin: auto; border: 1px solid&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5254ed280cda54995be9decb6334b04e/34cf9/gms_qa_08.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 41.041666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABqElEQVR42j2RWW/bMBCE/f9/UpEW6ENf6iaAk7SpFV9SdFiyJZEidR9fV1JQAjwwi5md5Wz6vqetM2wRk6fB8u66mnmVTUduG8IklbtGl82CN11D0Rj8JCBRyfKedea1maaJYRhQShHHMW3bMozjUrRVQ5xp3ODKXRm0rRa8H3psXRJcQ+75HVPZRWMRHPqOInepjE9bRbLFZaMYpeP7x5UX78bpZnFVxyUtmYSYm5zAXklHRTZpkjalbNdmm7oyXJwHzvsveIdv3PzvqPRAK24eX/d8/fnCdu/y6PjsjgF9VRHdIp5Oz+zOr/wJ9rxFDqa2q2AvDssiFGcxRvsUypOxi6XouCHbvxd+uzH7MMcJ0gVXpeaSe3g6wFM+H8K3TbkKzkd2/8A7bblFz9TGZfz8w64fln/TtqQoa+quW/BhHMSRQVnFvcikQfGfswjekyOHtwf88w9K7TBNa7Hten7N4777OKFCaxlLQhvbjkN05um44yDcvEiYmoapbVbBOWkY5R4krTX+Nc1BgpFxvSteojHiUqxIYANRGnEU0SALUSZjEkzI/ANqIGD4hrgyDQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5254ed280cda54995be9decb6334b04e/263a4/gms_qa_08.webp 480w,
/static/5254ed280cda54995be9decb6334b04e/a6361/gms_qa_08.webp 960w,
/static/5254ed280cda54995be9decb6334b04e/0b34d/gms_qa_08.webp 1920w,
/static/5254ed280cda54995be9decb6334b04e/c8d93/gms_qa_08.webp 2858w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5254ed280cda54995be9decb6334b04e/9aebd/gms_qa_08.png 480w,
/static/5254ed280cda54995be9decb6334b04e/a91f8/gms_qa_08.png 960w,
/static/5254ed280cda54995be9decb6334b04e/ac7a9/gms_qa_08.png 1920w,
/static/5254ed280cda54995be9decb6334b04e/34cf9/gms_qa_08.png 2858w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5254ed280cda54995be9decb6334b04e/ac7a9/gms_qa_08.png&quot; alt=&quot;Interface Contract Test Sample&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;Interface Contract Test 예시&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;예를 들어, 고객 이름(user_name)과 주소(address) 필드가 서비스간 전송될 때 &lt;strong&gt;암호화/복호화 과정&lt;/strong&gt;을 거쳐 전달되는 시나리오를 가정해 보겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;글로벌몰 → GMS&lt;/strong&gt;: user_name은 &lt;strong&gt;VARCHAR(20)&lt;/strong&gt;, address는 &lt;strong&gt;VARCHAR(100)&lt;/strong&gt; 으로 정의&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GMS → 특송사&lt;/strong&gt;: 복호화 후 전달되지만, 특송사 스키마에서는 user_name이 &lt;strong&gt;VARCHAR(10)&lt;/strong&gt; 으로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;이처럼 글로벌몰에서 전달된 20자 이상의 이름 정보는 특송사에 저장되는 과정에서 &lt;strong&gt;Schema Mismatch&lt;/strong&gt;가 발생해 데이터 저장 오류가 나거나, 오류 메시지 없이 값이 잘리는 &lt;strong&gt;Silent Failure&lt;/strong&gt;이 발생할 수 있습니다. 이러한 &lt;strong&gt;Interface Contract Test&lt;/strong&gt;는 서비스 간 사소한 스키마 불일치가 운영 환경에서 &lt;strong&gt;예기치 않은 데이터 정합성 문제&lt;/strong&gt;로 이어지지 않도록
사전에 차단하는 핵심 검증 과정이 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;운영-환경-최종-검증--uat의-진정한-의미&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B4%EC%98%81-%ED%99%98%EA%B2%BD-%EC%B5%9C%EC%A2%85-%EA%B2%80%EC%A6%9D--uat%EC%9D%98-%EC%A7%84%EC%A0%95%ED%95%9C-%EC%9D%98%EB%AF%B8&quot; aria-label=&quot;운영 환경 최종 검증  uat의 진정한 의미 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;운영 환경 최종 검증 : UAT의 진정한 의미&lt;/h1&gt;
&lt;p&gt;개발 환경에서의 기능 단위 테스트와 통합 테스트를 통해 GMS의 주요 기능 안정성은 충분히 검증되었습니다. 하지만 &lt;b&gt;WCS(Warehouse Control System)와 실제 물류센터 운영 환경은 개발 환경에서 충분히 테스트가 불가능한 ‘그레이존’&lt;/b&gt;으로 남아 있었습니다. 특히 검수 확정 이후 출고 처리까지 이어지는 물류센터 내 프로세스와 시스템 간 연동은, &lt;strong&gt;실제 운영 환경 검증&lt;/strong&gt; 없이는 &lt;strong&gt;사전 리스크 파악이 어려운 영역이&lt;/strong&gt;었습니다.&lt;/p&gt;
&lt;p&gt;이러한 배경 속에서 우리는 실제 센터 환경에서 진행하는 &lt;strong&gt;UAT(User Acceptance Test)를 설계&lt;/strong&gt;하게 되었습니다. 다만 UAT를 수행하는 동안 물류센터 작업이 일시중단되어야 했기 때문에, 단순 시나리오 테스트가 아닌 &lt;strong&gt;&quot;반드시 검증해야 할 항목&quot;만 담은 정제된 테스트 시나리오&lt;/strong&gt;가 필요했습니다.&lt;/p&gt;
&lt;p&gt;저희가 설계한 UAT 시나리오와 체크리스트는 다음 기준을 따랐습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;글로벌 매출 기준으로 상위 국가와 주요 특송사 조합을 선정&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;실제 센터 운영에서 발생 가능한 대표 케이스를 중심으로 구성&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;각 케이스를 E2E 통합 시나리오 흐름에 맞춰 체계화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단순 성공 여부가 아니라, WCS 동작 상태와 데이터 정합성까지 포함한 체크리스트 작성&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;UAT 제안 초기에는 센터 운영 중단에 대한 부담과 우려가 컸습니다. 그러나 GMS는 &lt;strong&gt;기존 시스템과 병행 운영이 불가능한 ‘빅뱅 배포(Big Bang Deployment)’ 방식&lt;/strong&gt;이었고, &lt;strong&gt;사전 검증 없이 오픈하는 것은 곧 운영 리스크로 직결&lt;/strong&gt;된다는 의미였죠. QA는 이 부분을 꾸준히 설득했고, 결국 논의를 거쳐 &lt;strong&gt;2차에 걸친 UAT를 정식으로 진행&lt;/strong&gt;하기로 결정했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 550px !important; max-width: 100%; margin: auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1563px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0bf473b73d52560720cde768a023447d/c15e7/gms_qa_09.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 64.79166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAACX0lEQVR42n1T207bQBTMF1RVKZK9vq+9jm+52YkviR1IaCkhgBIIQm2F1KqIfkRv6pdPz5pKfQD6MDrehx3PzJntfPv+C7PZEpNihrKqMR4XEH4IhwtYtgfLcqHxLphIwLz4YTp01m0ww3mEzvp6g/5ViiSbopvP4eYHcGcnELN3GBQ1gmoJc3wANllAy4/ARg2Y/IFmPU34wtqHceuQwgZp2SCqFoinSyI9Bm9WsJtTaESopUSUzR9mnEHr5dD85JHSjhG6sM89JL0MwyzHmFDkJQbSenEIUS6h1yvo02M46QxalILZAszywEz3scJXewoWpGi32yGKe9i3fOiOD4Pyk1OVl6QKsqhpJhgz6Py03ZbwfHeJ1eoCd18+4ejoDaKSVMUjWkwE7nVhmg40SSiJpSqpjn72LOH13Q2SsxTDtERezVGUU4zzis4FhpMKYd6AF7QQWpReHbeQ+T275T2dgS8EZZhCBAnCZIi4PyL7fXgDCj5rCHOwtIbVm8CiRbQxGM8R7jO8v/qIi90Wu+0GPIjx2k1gcr/tom1zMFV/gMyuhbzMnwAR/vj5G+uzLU7WZ9huLjGtDxD2M3iUoUlqdL8HlQotoRguFNm/v0v6BxMqTVUSvt2consVIxnmiMaUXTlvCz3Ia4j8EEazhktddOancKlCnGIx3KB9MZqICdRF0YPuRTDoVXVeKgpEk2AQ5fCTDMwNodhdqDxoX4S8zL0ANkXASbUrQphExLp9aBJ+n76HMIi4JbQdgXpS4vPtB3y9v4eggqvBqL0obTsukVNuKtmUdhVp9z+W/wCz9oJC+cbpuwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0bf473b73d52560720cde768a023447d/263a4/gms_qa_09.webp 480w,
/static/0bf473b73d52560720cde768a023447d/a6361/gms_qa_09.webp 960w,
/static/0bf473b73d52560720cde768a023447d/e7b97/gms_qa_09.webp 1563w&quot; sizes=&quot;(max-width: 1563px) 100vw, 1563px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0bf473b73d52560720cde768a023447d/9aebd/gms_qa_09.png 480w,
/static/0bf473b73d52560720cde768a023447d/a91f8/gms_qa_09.png 960w,
/static/0bf473b73d52560720cde768a023447d/c15e7/gms_qa_09.png 1563w&quot; sizes=&quot;(max-width: 1563px) 100vw, 1563px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0bf473b73d52560720cde768a023447d/c15e7/gms_qa_09.png&quot; alt=&quot;UAT 상황창&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;UAT 진행 스레드&lt;/figcaption&gt;
&lt;br&gt;
&lt;p&gt;그 결과, 실제 테스트 과정에서 예상치 못했던 여러 이슈들을 사전 탐지할 수 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WCS 장비 환경 전환 시 발생하는 네트워크 오류&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;간헐적인 &lt;strong&gt;출고 검수 확정 실패&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;재고이동/재고보충 기능이 실제 운영 환경과 달랐던 부분 등&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;이러한 경험은 &lt;strong&gt;실제 운영 환경 기반 QA의 필요성을 명확히 보여주는 사례&lt;/strong&gt;였고, 안정적인 GMS 오픈을 위한 핵심 기반이 되었습니다. 이처럼 UAT는 단순한 운영자 확인 절차를 넘어, &lt;b&gt;운영 중단이라는 리스크를 감수하고서라도 반드시 수행해야 하는 ‘운영 환경 최종 검증 단계’&lt;/b&gt;였고 그 중심에는 QA가 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;오픈-이후의-qa-조용한-실패까지-잡아내는-모니터링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%ED%94%88-%EC%9D%B4%ED%9B%84%EC%9D%98-qa-%EC%A1%B0%EC%9A%A9%ED%95%9C-%EC%8B%A4%ED%8C%A8%EA%B9%8C%EC%A7%80-%EC%9E%A1%EC%95%84%EB%82%B4%EB%8A%94-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&quot; aria-label=&quot;오픈 이후의 qa 조용한 실패까지 잡아내는 모니터링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오픈 이후의 QA: 조용한 실패까지 잡아내는 모니터링&lt;/h1&gt;
&lt;p&gt;UAT를 통해 실제 운영 환경에서의 기능 안정성은 검증했습니다. 하지만 &lt;strong&gt;오픈 이후에도 품질 확보는 멈추지 않습니다.&lt;/strong&gt; 이 시점에서 QA 역할은 단순히 운영 중 발생한 이슈를 수정하는 것에 머무르지 않고, 대신 &lt;strong&gt;결함을 조기에 감지하고, 인시던트로 확대되기 전에 선제적으로 대응하는 체계&lt;/strong&gt;를 만드는 데 있습니다. GMS는 여러 외부 시스템과 &lt;b&gt;MQ 기반 인터페이스(Kafka/MSK)&lt;/b&gt;로 연동되므로,  &lt;strong&gt;서버 상태 모니터링만으로는 품질 리스크를 찾기 어렵습니다.&lt;/strong&gt; 예를 들어 서버 Health Check API가 정상(&lt;code class=&quot;language-text&quot;&gt;200 OK&lt;/code&gt;) 반환해도, &lt;strong&gt;Kafka 프로듀서가 메시지를 발행하지 않는 경우&lt;/strong&gt;(커넥션 단절, 전송 오류 등)도 발생합니다. 이러한 상황을 탐지하기 위해 저희는 &lt;strong&gt;Datadog 기반의 커스텀 모니터링 룰&lt;/strong&gt;을 구축했습니다.&lt;/p&gt;
&lt;h2 id=&quot;-실제-모니터링-적용-예시&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%8B%A4%EC%A0%9C-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%A0%81%EC%9A%A9-%EC%98%88%EC%8B%9C&quot; aria-label=&quot; 실제 모니터링 적용 예시 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📌 실제 모니터링 적용 예시&lt;/h2&gt;
&lt;p&gt;&lt;b&gt;출하 지시(Shipment Order)&lt;/b&gt;는 일정 주기마다 자동 배치로 내려가야 하는 구조였는데, 만약 Kafka 프로듀서가 침묵한 채 메시지를 전송하지 않는다면 &lt;strong&gt;센터의 작업 지시가 멈춰 물류가 중단되는 리스크&lt;/strong&gt;가 발생합니다. 이를 방지하기 위해 &lt;b&gt;Datadog에서 ‘배치 주기 내 출하지시 메시지 수가 0건일 경우 알림’&lt;/b&gt;이 발생하도록 설정했습니다. 덕분에 실제로 운영에서도 &lt;strong&gt;센터 작업이 완전히 멈추기 전에 사전 조치&lt;/strong&gt;를 취할 수 있었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;건수 기반 임계치 알림&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메시지 지연 감지&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;이 외에도 입고 확정, 재고 동기화 등 주요 메시지 플로우에 대해 추가 모니터링하여, 기능 오류가 아닌 &lt;strong&gt;‘조용한 실패(Silent Failure)’도 실시간으로 탐지&lt;/strong&gt;할 수 있도록 구성했습니다.&lt;/p&gt;
&lt;div style=&quot;width: 550px !important; max-width: 100%; margin: auto&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/979bbf4dcf929f1dbc8f18f624bae5be/3643c/gms_qa_10.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 58.958333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB30lEQVR42o1TTWvbUBDUD+ihtGCcSJZsy1L0bcuSLdmSLceOnDQ4kBAHciiEHNqeArmYEHroXyj0B0/fbhDEbVR6WN5Db3d2ZnckPTx8xfb6GvF4iklWIJnMkKQ5xiKm+QKON4BpeVDbPbS7JrSOgZam14Z0f3+D2/ITiuIE+XzJoOl0xmCL4xLRKBXgmTgn3NT1QxwqbcitzpshvXvfQCCbSJMMpu1zlwNZ4yIKultOwMyPxDuFonbrARWtA7ftCCATigCTqbuyD9gzHQSDWLAbYBCOuGkdS6nRaMLO1hif3wiWE2i6jaZmQFZ1ZkKzo8R/sdoD1A0X26tL7HaPeH7+jmRRQvUT2MEQXhCid+T+NxgD/vr5A3ff7hCNM7GMOXKxjFk+x7xY4ni15kX4/YhBX8+2FrB5oKDlO4jFJkeiOIwSBGHMVjEEO08wJVDL8cU3F7ZYENmndoYXFxssypJZkCU8ljpkQNouJdESdMNiD1JUxW+NQnrafcHV5y2K5RqnZxssV6eYFSusTs5wvrlkb9Io4tEUUZzynUxf2esvwA8fDyF7Drz+kOXQn0HMqDsV0el4fTY4vZGSYPCixhc1umHvyZcUVUgyXjz4GqRKIBbdnsWFlcwqjyz1p+zfvTtlAjBGxMcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/979bbf4dcf929f1dbc8f18f624bae5be/263a4/gms_qa_10.webp 480w,
/static/979bbf4dcf929f1dbc8f18f624bae5be/a6361/gms_qa_10.webp 960w,
/static/979bbf4dcf929f1dbc8f18f624bae5be/08048/gms_qa_10.webp 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/979bbf4dcf929f1dbc8f18f624bae5be/9aebd/gms_qa_10.png 480w,
/static/979bbf4dcf929f1dbc8f18f624bae5be/a91f8/gms_qa_10.png 960w,
/static/979bbf4dcf929f1dbc8f18f624bae5be/3643c/gms_qa_10.png 1400w&quot; sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/979bbf4dcf929f1dbc8f18f624bae5be/3643c/gms_qa_10.png&quot; alt=&quot;Datadog Slack Alaram&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;모니터링 알림 예시&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;Datadog 기반의 실시간 모니터링은 단순 대응을 넘어, &lt;strong&gt;‘관측 가능한 QA’로의 전환&lt;/strong&gt;을 이끈 핵심 전략이었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;마치며-qa-정답은-없지만-방향은-있다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-qa-%EC%A0%95%EB%8B%B5%EC%9D%80-%EC%97%86%EC%A7%80%EB%A7%8C-%EB%B0%A9%ED%96%A5%EC%9D%80-%EC%9E%88%EB%8B%A4&quot; aria-label=&quot;마치며 qa 정답은 없지만 방향은 있다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며: QA, 정답은 없지만 방향은 있다&lt;/h1&gt;
&lt;p&gt;이번 GMS 프로젝트는 저에게 WMS 시스템을 분석하고 설계부터 런칭까지 직접 경험한 &lt;strong&gt;두 번째 사례&lt;/strong&gt;였지만, 같은 도메인이라고 해서 같은 방식으로 접근할 수는 없었습니다.
시스템 구조는 유사할 수 있어도, &lt;strong&gt;QA의 접근 방식은 매번 새로운 고민과 선택의 연속&lt;/strong&gt;이었습니다. 프로젝트를 진행하며 다시 한 번 깨달은 것은, &lt;b&gt;“이 시스템은 이렇게 검증하면 된다”&lt;/b&gt;는 &lt;strong&gt;절대적인 정답은 없다&lt;/strong&gt;는 점입니다. 각 프로젝트는 &lt;strong&gt;도입 기술&lt;/strong&gt;, &lt;strong&gt;연동 환경&lt;/strong&gt;, &lt;strong&gt;조직의 역량&lt;/strong&gt;, &lt;strong&gt;팀의 속도&lt;/strong&gt; 등 &lt;strong&gt;수많은 변수를 기반&lt;/strong&gt;으로 움직이므로, QA는 이러한 요소를 고려해 &lt;strong&gt;최적의 테스트 전략을 매번 설계&lt;/strong&gt;해야 합니다. 무엇이 효과적인 접근법인지 끊임없이 고민하고 방향을 조정하는 과정 자체가 &lt;strong&gt;QA가 지녀야 할 가장 본질적인 자세&lt;/strong&gt;라는 것을 이번 경험을 통해 배웠습니다. 이 글이 &lt;strong&gt;새로운 시스템을 처음 검증해야 하는 QA 엔지니어&lt;/strong&gt;분들께 실질적인 인사이트와 용기를 전하길 바랍니다.&lt;/p&gt;
&lt;p&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;혹시 이 글을 읽고 &lt;strong&gt;올리브영 QA팀의 일&lt;/strong&gt;이 흥미롭게 느껴지셨나요? 저희와 함께 새로운 품질 경험을 만들어 갈 분들을 기다리고 있습니다.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250807033067&quot;&gt;
QA Engineer (코어플랫폼) 지원하러 가기
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033551&quot;&gt;
QA Engineer (Back Office) 지원하러 가기
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033063&quot;&gt;
QA Engineer (SCM) 지원하러 가기
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;/br&gt;또한, 아래 글들도 함께 읽어보시면 GMS 프로젝트와 QA 업무 전반에 대해 더 깊이 이해하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://oliveyoung.tech/2025-07-23/gms-open-story/&quot;&gt;
제로베이스 WMS 구축기: Kafka 기반 분산 물류 시스템 설계와 Out-of-Order Events 해결
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://oliveyoung.tech/2025-06-10/chaos/&quot;&gt;
버그가 아니라 장애를 잡아라!! QA와 카오스 엔지니어링의 만남
&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영, 블록 기반 CMS로 콘텐츠 제작 시간을 99% 줄이다]]></title><description><![CDATA[안녕하세요! 올리브영 프론트엔드 개발자 쩡다입니다. "하나의 콘텐츠를 만드는 데 정말 7일이나 걸려야 할까요?" 이 질문에서 시작된 올리브영의 콘텐츠 제작 프로젝트를 소개합니다. 
직접 구축한 블록 기반 CMS로 제작 시간을 7일에서 2…]]></description><link>https://oliveyoung.tech/2025-09-04/article-editor/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-09-04/article-editor/</guid><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영 프론트엔드 개발자 쩡다입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;&quot;하나의 콘텐츠를 만드는 데 정말 7일이나 걸려야 할까요?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 질문에서 시작된 올리브영의 콘텐츠 제작 프로젝트를 소개합니다. &lt;br&gt;
직접 구축한 블록 기반 CMS로 제작 시간을 &lt;strong&gt;7일에서 20분&lt;/strong&gt;으로 단축하고, &lt;br&gt;
Vue에서 Lit 웹 컴포넌트로 전환하며 얻은 확장성 인사이트를 공유하겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;단순히 기술 전환의 이야기가 아니라,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존 발행 방식이 가진 비효율성과 병목 지점&lt;/li&gt;
&lt;li&gt;CMS를 모듈화하면서 얻은 아키텍처 인사이트&lt;/li&gt;
&lt;li&gt;프레임워크 종속성과 확장성 문제를 어떻게 풀었는지&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
이 과정을 정리해 보려 합니다. CMS 구축을 고민하는 개발팀이나 콘텐츠 워크플로우 개선이 필요한 분들께 도움이 되길 바랍니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;7일-걸리던-콘텐츠-제작-20분으로-단축하다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#7%EC%9D%BC-%EA%B1%B8%EB%A6%AC%EB%8D%98-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%A0%9C%EC%9E%91-20%EB%B6%84%EC%9C%BC%EB%A1%9C-%EB%8B%A8%EC%B6%95%ED%95%98%EB%8B%A4&quot; aria-label=&quot;7일 걸리던 콘텐츠 제작 20분으로 단축하다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;7일 걸리던 콘텐츠 제작, 20분으로 단축하다&lt;/h1&gt;
&lt;hr&gt;
&lt;h3 id=&quot;기존-발행-과정의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A1%B4-%EB%B0%9C%ED%96%89-%EA%B3%BC%EC%A0%95%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;기존 발행 과정의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기존 발행 과정의 한계&lt;/h3&gt;
&lt;p&gt;기존 에디토리얼 콘텐츠 발행은 아래의 단계를 거쳤고, 하나의 콘텐츠가 완성되기까지 &lt;strong&gt;평균 7일 이상&lt;/strong&gt;이 걸렸습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;콘텐츠 기획 → 디자인 → 퍼블리싱 → 콘텐츠 발행&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;특히 디자인과 퍼블리싱은 외주 인력이 담당하다 보니 커뮤니케이션에만 수십 시간이 소요되었고, &lt;br&gt;
이 과정에서 다음과 같은 문제가 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;제작 효율 저하&lt;/strong&gt;: 같은 모양의 블록을 매번 새로 작성해야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;수요 대응 불가&lt;/strong&gt;: 인력이 제한되어 콘텐츠 수요를 따라가기 어려움&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용자 경험 한계&lt;/strong&gt;: 발행량 부족으로 개인 맞춤형 추천 같은 고도화가 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;문제-해결을-위한-접근&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A0%91%EA%B7%BC&quot; aria-label=&quot;문제 해결을 위한 접근 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 해결을 위한 접근&lt;/h3&gt;
&lt;p&gt;이런 문제 때문에 &lt;strong&gt;CMS를 직접 만들기로 결정&lt;/strong&gt;했습니다.
핵심 목표는 다음과 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;퍼블리싱 제거&lt;/strong&gt; – 더 이상 HTML을 직접 작성하지 않도록 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디자인 제거&lt;/strong&gt; – 모듈 단위 블록 시스템을 도입해 디자이너 개입을 최소화&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;누구나 제작 가능&lt;/strong&gt; – 기획자나 BPO도 개발자 도움 없이 콘텐츠 작성 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;👉 즉, 블록 기반 CMS 에디터를 통해 &quot;&lt;strong&gt;기획만 하면 바로 발행&lt;/strong&gt;&quot;이 가능하도록 구조를 바꾼 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;성과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EA%B3%BC&quot; aria-label=&quot;성과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;성과&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;콘텐츠 제작 시간이 기존 &lt;strong&gt;40시간&lt;/strong&gt; → &lt;strong&gt;약 20분&lt;/strong&gt;으로 대폭 단축&lt;/li&gt;
&lt;li&gt;디자인/퍼블리싱 단계를 제거해 외주 커뮤니케이션 비용 감소&lt;/li&gt;
&lt;li&gt;에디토리얼 콘텐츠 발행량이 &lt;strong&gt;약 2배&lt;/strong&gt; 증가&lt;/li&gt;
&lt;li&gt;현재 올리브영 내 발행 콘텐츠의 &lt;strong&gt;약 40%&lt;/strong&gt; 가 CMS를 통해 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;👉 이로써 콘텐츠 발행 주기가 크게 단축되었고,
사용자에게 더 풍부한 맞춤형 콘텐츠를 제공할 수 있는 기반이 마련되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;새로운-접근-모듈화된-cms-아이디어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%A0%91%EA%B7%BC-%EB%AA%A8%EB%93%88%ED%99%94%EB%90%9C-cms-%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4&quot; aria-label=&quot;새로운 접근 모듈화된 cms 아이디어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;새로운 접근: 모듈화된 CMS 아이디어&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;저희가 세운 목표는 &lt;strong&gt;&quot;누구나 빠르게 콘텐츠를 만들고 발행할 수 있는 시스템&quot;&lt;/strong&gt; 이었습니다.&lt;/p&gt;
&lt;p&gt;이를 위해 다음과 같은 방향으로 설계했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;모듈 단위 설계&lt;/strong&gt;: 이미지, 텍스트 등 콘텐츠 요소를 독립 모듈로 제작&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;위치 이동&lt;/strong&gt;: Drag &amp;#x26; Drop으로 손쉽게 배치&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;필수 정보 입력 후 자동 생성&lt;/strong&gt;: 디자인 및 퍼블리싱 리소스 없이 즉각적인 콘텐츠 발행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;👉 이렇게 하면 &lt;strong&gt;퍼블리셔 의존도가 크게 줄고&lt;/strong&gt;, 콘텐츠 제작 속도도 &lt;strong&gt;눈에 띄게 빨라집니다.&lt;/strong&gt;&lt;/p&gt;
&lt;video autoplay muted loop style=&quot;width: 100%; max-width: 600px;&quot;&gt;
    &lt;source src=&quot;/0fb2359b68a5cbac0162afbc676063f7/cms_1.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;첫-번째-구현-vue-기반-cms&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EA%B5%AC%ED%98%84-vue-%EA%B8%B0%EB%B0%98-cms&quot; aria-label=&quot;첫 번째 구현 vue 기반 cms permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;첫 번째 구현: Vue 기반 CMS&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;처음에는 &lt;strong&gt;Vue를 활용해 CMS를 구현&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;선택 이유&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;팀에서 이미 Vue를 사용하고 있었고, 빠른 시제품 개발이 가능했기 때문&lt;/li&gt;
&lt;li&gt;에디터는 contentEditable 대신 CKEditor5를 사용
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;contentEditable&lt;/code&gt;은 표준이 아니어서 브라우저마다 태그 처리, 줄바꿈 방식, &lt;code class=&quot;language-text&quot;&gt;execCommand&lt;/code&gt; 동작 등이 달라 일관성 부족&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
  &lt;br&gt;
&lt;p&gt;&lt;strong&gt;구현 방법&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모듈 단위 Vue 컴포넌트 작성&lt;/li&gt;
&lt;li&gt;부모 컴포넌트에서 상태 관리 및 렌더링&lt;/li&gt;
&lt;/ul&gt;
  &lt;br&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;빠르게 프로토타입을 완성&lt;/li&gt;
&lt;li&gt;실제 BPO/디자이너가 바로 써볼 수 있는 환경을 제공&lt;/li&gt;
&lt;/ul&gt;
  &lt;br&gt;
&lt;p&gt;&lt;strong&gt;한계&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vue 종속성 때문에 다른 프로젝트에서 재사용하기 어려움&lt;/li&gt;
&lt;li&gt;유지보수 관점에서 확장성이 부족&lt;/li&gt;
&lt;li&gt;기능이 늘어날수록 상태 관리 복잡성이 커짐&lt;/li&gt;
&lt;/ul&gt;
  &lt;br&gt;
&lt;br&gt;
&lt;p&gt;👉 이러한 한계점들을 통해 &lt;strong&gt;프로토타입 속도만큼이나 장기적인 재사용성과 확장성이 중요하다&lt;/strong&gt;는 교훈을 얻었고,
이는 이후 &lt;strong&gt;Lit 기반 웹 컴포넌트 전환&lt;/strong&gt; 결정의 핵심 근거가 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/585601cb7ad7417e731454fc856932ab/4c1d5/cms_2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 47.291666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABpUlEQVR42pWRzU8TURTF5x80rt24MzEkJm40GhdEXbBwYYw2fgRtqBpDZKGYGEihChIsjREotB2K0Bl4kxnoDLad9M1H++bnUDTixwJect69ybnv3Hve1TjJSY6C+G5x91mWe7kcomX9t1Q7jeDWnpGKjZF5+SLNG0dUkpxe8NejNUPn5v0MIw8fsdao/eaOQTuM6icOyb87Hp9w09nm1oMMd56OUre3TjbhoFEvvXoMoGJFty2RfohwbPLzC3xcLGI3HeKeot8SyMoUsjpN0rHQasJntuzySW9RN208x2Pqc56ruetcyV3jzdwk5qpA6DahG0HMAIEXsOd1iDY/4D8+g//kLP3tObRsQTA0WuVytsbyNzctjqnqdfLFWWZKBb6urDIzOc/6lxrl9wYTw4uM31igkjfxw4DYqSJfXUC+HiJxN/61rJQi3u/TaXTpGF0iO2anYtFsuKxPG7wbKfH2dhG9sEM7kERiBTl+ETlxCWWX06X8+e8k6XbCg1Rkw8LUdwnckNCLCZoRcTu1HCqSQA3y/YPUsrmEHDtH9/l5+rslfgCXZJnOP/cnCQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/585601cb7ad7417e731454fc856932ab/263a4/cms_2.webp 480w,
/static/585601cb7ad7417e731454fc856932ab/a6361/cms_2.webp 960w,
/static/585601cb7ad7417e731454fc856932ab/0b34d/cms_2.webp 1920w,
/static/585601cb7ad7417e731454fc856932ab/712ca/cms_2.webp 2138w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/585601cb7ad7417e731454fc856932ab/9aebd/cms_2.png 480w,
/static/585601cb7ad7417e731454fc856932ab/a91f8/cms_2.png 960w,
/static/585601cb7ad7417e731454fc856932ab/ac7a9/cms_2.png 1920w,
/static/585601cb7ad7417e731454fc856932ab/4c1d5/cms_2.png 2138w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/585601cb7ad7417e731454fc856932ab/ac7a9/cms_2.png&quot; alt=&quot;CMS 초기 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;CMS 초기 아키텍처&lt;/figcaption&gt;
&lt;h3 id=&quot;콘텐츠-구조-블록-기반&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B5%AC%EC%A1%B0-%EB%B8%94%EB%A1%9D-%EA%B8%B0%EB%B0%98&quot; aria-label=&quot;콘텐츠 구조 블록 기반 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;콘텐츠 구조: 블록 기반&lt;/h3&gt;
&lt;p&gt;콘텐츠는 &lt;strong&gt;블록 기반 구조&lt;/strong&gt;로 저장됩니다.
즉, 단순히 HTML로 서술하는 방식이 아니라, 각 블록이 &lt;strong&gt;타입&lt;/strong&gt;과 &lt;strong&gt;데이터&lt;/strong&gt;를 가진 &lt;strong&gt;배열 형태&lt;/strong&gt;로 나열됩니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 텍스트, 상품, 이미지 등의 요소가 각각 독립된 블록으로 정의됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;78421c00-7e4f-11f0-b7a6-6942e7596f9e&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;text&apos;&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;inputText&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;h2 class=&quot;&lt;/span&gt;fHead1&lt;span class=&quot;token string&quot;&gt;&quot;&gt;안녕하세요&amp;lt;/h2&gt;&amp;lt;p style=&quot;&lt;/span&gt;text&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;align&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;안녕하세요&amp;lt;/p&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;7f5ca6e0-7e4f-11f0-b7a6-6942e7596f9e&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;inputImage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;singleType&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;uid&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;vc-upload-1755754022576-2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;image path&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;92e1c0b0-7e4f-11f0-b7a6-6942e7596f9e&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;productList&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;inputProductList&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;goodsName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;퍼퓸 파우터 팩트&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;imagePath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;image path&apos;&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;판매중&apos;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;두-번째-구현-lit-기반-웹-컴포넌트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%91%90-%EB%B2%88%EC%A7%B8-%EA%B5%AC%ED%98%84-lit-%EA%B8%B0%EB%B0%98-%EC%9B%B9-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8&quot; aria-label=&quot;두 번째 구현 lit 기반 웹 컴포넌트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;두 번째 구현: Lit 기반 웹 컴포넌트&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;Vue에서 얻은 경험을 바탕으로, 지금은 &lt;strong&gt;Lit을 사용한 웹 컴포넌트 기반 CMS&lt;/strong&gt;로 전환했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;선택 이유&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프레임워크 독립적 → React, Vue, Angular 어디서든 재사용 가능&lt;/li&gt;
&lt;li&gt;웹 컴포넌트 표준 기반 → 장기적 유지보수 유리&lt;/li&gt;
&lt;/ul&gt;
  &lt;br&gt;
&lt;p&gt;&lt;strong&gt;구현 방법&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미지/텍스트/상품 모듈을 각각 Lit 컴포넌트로 제작&lt;/li&gt;
&lt;li&gt;모듈 간 통신은 이벤트 기반으로 처리&lt;/li&gt;
&lt;li&gt;상태는 중앙 store에서 관리 (필요 시 공유 가능)&lt;/li&gt;
&lt;li&gt;CMS 모듈을 npm 패키지로 배포 → 다른 프로젝트에서 라이브러리처럼 다운로드 후 사용 가능&lt;/li&gt;
&lt;/ul&gt;
  &lt;br&gt;
&lt;p&gt;&lt;strong&gt;기대 효과&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다양한 프로젝트에 쉽게 붙일 수 있음&lt;/li&gt;
&lt;li&gt;CMS 모듈이 라이브러리처럼 활용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;코드-구현-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BD%94%EB%93%9C-%EA%B5%AC%ED%98%84-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot;코드 구현 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;코드 구현 방식&lt;/h3&gt;
&lt;h4 id=&quot;웹-컴포넌트-정의-customelement-데코레이터&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%B9-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EC%A0%95%EC%9D%98-customelement-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0&quot; aria-label=&quot;웹 컴포넌트 정의 customelement 데코레이터 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;웹 컴포넌트 정의: &lt;strong&gt;@customElement&lt;/strong&gt; 데코레이터:&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;@customElement(&apos;article-editor-container&apos;)는 이 클래스를 브라우저가 인식할 수 있는 사용자 정의 HTML 태그인 &lt;article-editor-container&gt;로 등록하는 역할을 합니다. &lt;br&gt;
이를 통해 &lt;article-editor-container&gt;&lt;/article-editor-container&gt;와 같이 HTML 문서에서 재사용 가능한 UI 요소로 사용할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;상속과-스타일링-extends-basecomponent--static-styles&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%EC%86%8D%EA%B3%BC-%EC%8A%A4%ED%83%80%EC%9D%BC%EB%A7%81-extends-basecomponent--static-styles&quot; aria-label=&quot;상속과 스타일링 extends basecomponent  static styles permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상속과 스타일링: &lt;strong&gt;extends BaseComponent &amp;#x26; static styles&lt;/strong&gt;:&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;static override styles는 부모 클래스의 스타일에 추가로 현재 컴포넌트의 고유한 스타일을 적용하는 부분입니다. &lt;br&gt;
unsafeCSS(styles)는 별도로 작성된 CSS 파일을 가져와 컴포넌트의 캡슐화된 스타일로 만들어 줍니다. &lt;br&gt;
이를 통해 컴포넌트의 스타일이 전역 CSS와 충돌하는 것을 방지합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;반응형-속성-property-데코레이터와-접근자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%98%EC%9D%91%ED%98%95-%EC%86%8D%EC%84%B1-property-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0%EC%99%80-%EC%A0%91%EA%B7%BC%EC%9E%90&quot; aria-label=&quot;반응형 속성 property 데코레이터와 접근자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;반응형 속성: &lt;strong&gt;@property&lt;/strong&gt; 데코레이터와 접근자:&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;이 속성은 컴포넌트의 다양한 설정을 담고 있는 객체입니다. 예를 들어, 어떤 모듈을 보여줄지(visibleModuleList) 등을 제어하는 데 사용됩니다. 이 또한 @property로 정의되어 있어 설정값이 변경되면 UI가 자동으로 갱신됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;    @&lt;span class=&quot;token function&quot;&gt;customElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;article-editor-container&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ArticleEditorContainer&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BaseComponent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; override styles &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;styles&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;unsafeCSS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;styles&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;_moduleList&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModuleItem&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        @&lt;span class=&quot;token function&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Array &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;moduleList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModuleItem&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_moduleList&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;moduleList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token literal-property property&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ModuleItem&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_moduleList&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_moduleList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;requestUpdate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;moduleList&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; oldValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

        @&lt;span class=&quot;token function&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Object &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token literal-property property&quot;&gt;visibleModuleList&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h4 id=&quot;타-프레임워크-환경에서-라이브러리-설치-후-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EC%84%A4%EC%B9%98-%ED%9B%84-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;타 프레임워크 환경에서 라이브러리 설치 후 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;타 프레임워크 환경에서 라이브러리 설치 후 적용&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;html&quot;&gt;&lt;pre class=&quot;language-html&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;article-editor-container&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;editor&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;article-editor-container&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;visibleModuleList&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;IMAGE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;TEXT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;PRODUCT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; editor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;editor&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;DOMContentLoaded&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        editor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;실제-사용-흐름-초기-데모-영상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%82%AC%EC%9A%A9-%ED%9D%90%EB%A6%84-%EC%B4%88%EA%B8%B0-%EB%8D%B0%EB%AA%A8-%EC%98%81%EC%83%81&quot; aria-label=&quot;실제 사용 흐름 초기 데모 영상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실제 사용 흐름 (초기 데모 영상)&lt;/h1&gt;
&lt;hr&gt;
&lt;video autoplay muted loop style=&quot;width: 100%; max-width: 800px;&quot;&gt;
    &lt;source src=&quot;/4686a444a5bdeac4f30a9fe2ff1f7fe9/cms_3.mp4&quot; type=&quot;video/mp4&quot; /&gt;
&lt;/video&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;마치며-배운-점과-앞으로의-계획&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-%EB%B0%B0%EC%9A%B4-%EC%A0%90%EA%B3%BC-%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EA%B3%84%ED%9A%8D&quot; aria-label=&quot;마치며 배운 점과 앞으로의 계획 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며: 배운 점과 앞으로의 계획&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;&quot;처음엔 그냥 빨리 만드는 게 목표였는데, 이제는 정말 쓸만한 도구가 됐네요.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;함께 작업한 기획자분의 이 한마디가 이번 프로젝트를 가장 잘 요약하는 것 같습니다. &lt;br&gt;
저희는 단순히 콘텐츠를 빠르게 만드는 것을 넘어, 사용자의 워크플로우를 근본적으로 개선하는 도구를 만들고자 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;핵심-인사이트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B5%EC%8B%AC-%EC%9D%B8%EC%82%AC%EC%9D%B4%ED%8A%B8&quot; aria-label=&quot;핵심 인사이트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;핵심 인사이트&lt;/h3&gt;
&lt;p&gt;이 과정에서 얻은 가장 중효한 깨달음은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CMS는 단순한 관리자 툴이 아니라 콘텐츠 제작 효율을 혁신하는 도구&lt;/li&gt;
&lt;li&gt;프레임워크 종속적 구조보다 웹 컴포넌트 표준 기반의 독립적 구조가 장기적으로 유리&lt;/li&gt;
&lt;li&gt;개발자에게는 코드를 짜는 것만큼 사용자의 문제를 깊이 이해하는 것이 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;앞으로의-방향&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EB%B0%A9%ED%96%A5&quot; aria-label=&quot;앞으로의 방향 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;앞으로의 방향&lt;/h3&gt;
&lt;p&gt;앞으로는 이번에 구축한 편집기를 단순히 빠른 제작용 툴을 넘어 퀄리티 있는 콘텐츠 제작에 최적화된 도구로 발전시켜 나갈 계획입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;👉 CMS 구축을 고민하고 계신 분들에게 저희의 경험이 조금이라도 도움이 되었으면 좋겠습니다. &lt;br&gt;
그리고 기술이 비즈니스와 만나 어떤 시너지를 낼 수 있는지 계속해서 고민하는 올리브영 테크 조직의 여정에도 많은 관심 부탁드립니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[외부 백엔드 커뮤니티와 함께 한 올리브영의 SpringCamp 2025 참가 후기]]></title><description><![CDATA[안녕하세요. 인벤토리 스쿼드 백엔드 개발자 펭귄대장입니다! 
오늘은 저희 올리브영이 SpringCamp2025에 후원사로 참가한 소식을 전해드리려고 합니다.  SpringCamp 2025 저희 올리브영은 AWS Summit, DASH, FEConf 등 국내외 다양한 개발자 컨퍼런스/커뮤니티에 참여해 왔는데요. 
이번엔 KSUG(한국 스프링 사용자 모임)에서 주최하는, 
국내 대표 백엔드 컨퍼런스 중 하나인 SpringCamp 에 골드🏅 후원사로 참여하여 
물류 시스템 개선기 발표와 홍보 부스 운영을 통해 약 500명의 외부 개발자 분들과 소통할 수 있는 기회를 가졌습니다.  Part 01. 기술, 공유하고 소통하다 : 물류 시스템 개선기 발표 명실상부 대한민국 대표 헬스&뷰티 스토어인 올리브영은 매일 수많은 트래픽과 데이터를 처리하고 있습니다. 
특히 매 올영세일마다 매출 신기록을 갱신하고 있고, 세일 기간의 트래픽과 데이터의 양은 기하급수적으로 증가하고 있는데요. 
이러한 상황 속에서 시스템의 실시간성과 확장성을 위해,   1. 기존의 EAI(Enterprise Application Integration) + 배치 기반 구조로 동작하던 물류 IF를 AWS MSK(Kafka) 기반의 실시간 처리 시스템으로 전환하고,
 2. RDB 중심의 재고 시스템을 Redis 기반으로 개선한 경험을 소개했습니다.  발표는 45분간 진행되었고, 정말 많은 분들이 관심을 가져주셨습니다. 
약 150석의 좌석이 가득 차고도, 자리가 모자라 발표장 뒤쪽에 서서 듣는 분들이 많아 뒷쪽 벽이 보이지 않을 만큼 큰 호응을 얻었습니다. 
행사 종료 후 진행된 설문에서도 '가장 유익했던 세션' 중 하나로 꼽히는 기쁨도 누렸습니다. 🙌  발표 후에는 많은 분들이 홍보 부스를 찾아주셔서 발표 내용에 대한 질문과 의견을 나누는 시간을 가졌습니다. 
다른 발표와 시간이 겹쳐 홍보 부스에서 질문에 대한 답변을 드리지 못한 분들도 계셨는데요. 
그중, 많이 겹쳤던 질문들을 위주로 Q&A를 정리해 보았습니다.  물류 시스템 개선기 관련 Q&A DLQ(Dead Letter Queue) 관련 문답 Q. "DLQ를 사용하면 순서 보장이 어려웠을 텐데, 이런 문제가 없었는지, 어떤 전략이 있었는지 궁금합니다." A. "순서 보장이 중요한 토픽은 DLQ를 사용하는 대신 애플리케이션 수준에서 재처리 전략을 통해 예외 상황을 처리했습니다. 
DLQ에 메시지가 쌓이는 경우에는 우선 오류 원인을 분석하고, 재처리가 필요한 메시지에 한해 순서를 고려하여 처리하도록 설계했습니다. 
또한, 순서 보장이 필요한 경우 메시지 키 기반으로 Kafka 파티션을 구성하여 동일한 키의 메시지는 항상 같은 파티션으로 전송되도록 함으로써 Kafka의 파티션 수준 순서 보장을 활용하고 있습니다." KAFKA 운영 관련 문답 Q. "Kafka를 실무 환경에서 안정적으로 운영하기 위해 고가용성을 확보하는 데 도움이 되었던 방법이나 운영 상의 노하우가 있을까요?" A. "카프카 운영 시 메시지 중복과 유실 문제가 발생할 수 있고, 이를 방지하는 것이 안정적인 카프카 운영의 핵심이라고 할 수 있는데요. 
Kafka 메시지 중복 및 유실 케이스별 해결 방법
포스팅을 참고해 주시면 좋을 것 같습니다."  오프라인 재고 시스템 개선기 관련 Q&A 이런 문제들을 해결하고, 재고 이력 관리의 일관성을 갖기 위해 OpenSearch를 기반으로 EFK스택을 구축하여 재고 변경 이력을 통합 관리하고 있습니다. 
Oracle 에 재고가 변경되는 시점에 Redis 에 데이터를 적재하고 있으며 이때 OpenSearch 에 재고 변경 이력을 함께 저장하고 있는 구조 인데요. 
Fluentbit sidecar pattern 을 사용하여 구축했는데 이는 AWS OpenSearch 기반 EFK STACK 구축기
포스팅을 참고해주시면 좋을 것 같습니다."  발표 자료를 놓치신 분들은 🔗이곳에서 발표 자료와 발표 영상을 확인하실 수 있습니다.  Part02. 사람, 소통하고 연결하다 : 홍보 부스 현장 01. 홍보부스 운영 📢 발표 외에 홍보 부스도 운영하여, 올리브영의 개발 문화와 조직 문화를 소개하는 시간을 가졌습니다.  올리브영의 백엔드 개발자들이 "오늘드림", "올영매장찾기" 등 직접 자신이 개발한 서비스를 소개하고, 
개발하면서 사용한 기술 스택과 개발 환경을 소개하며, 올리브영의 개발 문화를 알리는 시간을 가졌습니다.  올리브영의 개발 문화와 조직에 대해 궁금한 점을 물어보시거나, 
올리브영에서 진행 중인 프로젝트에 대해 질문하시는 분들이 많아 
개발자분들 사이에서 올리브영에 대한 관심이 높다는 것을 느낄 수 있었습니다.🔥  또한 홍보 부스에서는 올리브영과 관련된 간단한 퀴즈를 풀고, 
올리브영의 다양한 상품을 받을 수 있는 뽑기 이벤트 🎁를 진행했습니다. 
역시 많은 분들이 참여해 주셨고, 퀴즈를 통해 올리브영에 대한 이해도를 높일 수 있는 기회가 되었습니다.  발표 세션 사이마다 사람들로 가득 찬 뽑기 홍보 부스 02. 현장 커피챗 ☕️ 홍보 부스 운영과 동시에 커리어에 대해 고민이 있는 주니어 개발자 분들 몇 분을 선정하여 커피챗 시간을 가졌습니다. 
올리브영의 개발 문화와 성장 환경을 소개하고, 현장에서 느낄 수 있는 실질적인 커리어 조언을 나누며 뜻깊은 시간을 보냈습니다.  아래는 커피챗에 참여한 개발자 분들의 소감 💌입니다.  03. 오피스 아워 🏢 홍보 부스와 커피챗을 통해 많은 개발자분들과 소통했지만, 모든 분들과 충분히 대화하기는 어려웠습니다. 
이에 일부 개발자분들을 올리브영 본사로 초청해 오피스아워를 진행했습니다.  사전 신청으로 선정된 참가자들은 점심 식사와 티타임을 함께하며 조직 소개와 커리어 관련 이야기를 나누었고, 시니어 개발자들은 경험과 조언을 전했습니다. 
이어서 테크 조직의 역할과 업무 방식, 그리고 본사 사옥 투어를 통해 실제 근무 환경도 살펴볼 수 있었습니다. 평일 근무 시간에 진행된 행사였기에, 개인 연차를 내고 참석해 주신 열정에 보답하고자 회사 소개 자료와 소정의 기념품을 준비했습니다. 
참가자들은 “연차가 아깝지 않은 유익하고 의미 있는 시간이었다”는 소감을 남기며, 서로에게 영감을 주는 자리로 마무리되었습니다.  마치며.. 🎬 SpringCamp2025는 올리브영이 외부 개발자들과 더 가까워질 수 있었던 의미 있는 행사였습니다. 
참가한 저희도 올리브영에서 일하는 것에 대한 자부심과 긍지를 느낄 수 있었고, 
외부 개발자분들의 열정을 통해 저희도 다시 한 번 열정을 불태울 수 있는 계기가 되었습니다. 

앞으로도 올리브영은 개발자 커뮤니티와의 소통을 지속적으로 이어가며, 
개발자들이 함께 성장할 수 있는 문화를 만들어 나가겠습니다. 
다시 한 번 SpringCamp2025에 참여해 주신 모든 분들께 감사드리며, 
올리브영의 개발 문화와 조직 문화에 많은 관심과 응원 부탁드립니다. 

감사합니다! 그리고 현재 올리브영에서는 다양한 개발 직군의 채용도 진행 중입니다. 
올리브영의 개발 문화와 조직 문화를 직접 경험하고, 함께 성장할 수 있는 기회를 놓치지 마세요! 
많은 지원을 기다리겠습니다! 🙏  👉 올리브영 개발자 채용 바로가기]]></description><link>https://oliveyoung.tech/2025-08-28/springcamp_2025/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-08-28/springcamp_2025/</guid><pubDate>Thu, 28 Aug 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 인벤토리 스쿼드 백엔드 개발자 펭귄대장입니다! &lt;br&gt;
오늘은 저희 올리브영이 SpringCamp2025에 후원사로 참가한 소식을 전해드리려고 합니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;springcamp-2025&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#springcamp-2025&quot; aria-label=&quot;springcamp 2025 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;SpringCamp 2025&lt;/h2&gt;
&lt;p&gt;저희 올리브영은 AWS Summit, DASH, FEConf 등 국내외 다양한 개발자 컨퍼런스/커뮤니티에 참여해 왔는데요. &lt;br&gt;
이번엔 KSUG(한국 스프링 사용자 모임)에서 주최하는, &lt;br&gt;
국내 대표 백엔드 컨퍼런스 중 하나인 &lt;b&gt;&lt;a href=&quot;https://springcamp.ksug.org/2025/&quot;&gt;SpringCamp&lt;/a&gt; &lt;/b&gt;에 골드🏅 후원사로 참여하여 &lt;br&gt;
&lt;b&gt;물류 시스템 개선기 발표&lt;/b&gt;와 &lt;b&gt;홍보 부스 운영&lt;/b&gt;을 통해 약 &lt;b&gt;500명&lt;/b&gt;의 외부 개발자 분들과 소통할 수 있는 기회를 가졌습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center; gap:10px;&quot;&gt;
   &lt;div style=&quot;width: 63%&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 765px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACNklEQVR42iWSyW4aQRRFWXhSAGPADA100wMNTdPzwOCGEIgcKRvbsR0pWWSRLPI5mf0FtiJbduSs4s87qTiLI5XqlZ7Oq/ty6SIkyVzSuU808winPn7i4K8j4tdz/KMJ4XFGer4Q5wTT0dBNDVXrIHfqyO0qHalMq7FHc3+PnJu4DAOToa8zCvv0HYO+rWOKO9nTUcMBZmLTE7Wuo6CoLTqNBu39GlKlIhCNanvUqyVqlRK5YWCTreZ8+Pie6XzM8ekRz1+sCeKQs7NTnq1XTGdTsmyGP3JQbI1aZtCY6dQE9YnKfrVMuVR6JDdwLQ5fHnJ9c83t3S13v+64vLrk4eEPFxc/+PzlE/e/7/n2/Ss3Vz+JjjKKbyxq7zzqb11q5w7lRpViYZdSqUzOi10Goz5PVwuW6yXWyGLk2py8OmEym+B4DpY1YGAOGCcpLaVNs9NC1hQaHYmysNstFsjn84ICOWNooPRUFPEgTCNm8wPiNGYsxlwsl2TzOYHvE0cxjuOKpmN8L8AZeTTrTWFW5Em+8Ngsny+SswMXy3Pp2RZO4GE5DqYwsoZDul0FtSsSVXUUWcFQVZFqG6kp0ZIErbagQ1vRaEoyheK/lEOXaDplkmUsV2sOhFE6O2A8meH6YqVSURtPiOKEIEwIw5ggSoiEcRKnDG0HTTepVGv/R+4Y6uNfGP0ecewzssWa9AwMXUdWunSFnaqZaJqBrqnCrIUkzFRFRm5JPNnZZnNjg52tTXa2t/gLkWsVPG3+RW8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7c139b2d59dcbe5c666c4549e468628c/263a4/springcampinfo.webp 480w,
/static/7c139b2d59dcbe5c666c4549e468628c/db6df/springcampinfo.webp 765w&quot; sizes=&quot;(max-width: 765px) 100vw, 765px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7c139b2d59dcbe5c666c4549e468628c/9aebd/springcampinfo.png 480w,
/static/7c139b2d59dcbe5c666c4549e468628c/e97d8/springcampinfo.png 765w&quot; sizes=&quot;(max-width: 765px) 100vw, 765px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7c139b2d59dcbe5c666c4549e468628c/e97d8/springcampinfo.png&quot; alt=&quot;springcampinfo&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
   &lt;div style=&quot;width: 60%&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 693px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABnElEQVR42n2Sy0/CQBDGt9t2t7UtpVDaQnlE8IHR+AhRPIjvg+DrYohBE41HjF40MZ780z+ni4mYEA+/zOzMfNOZ7TJdFvEfmvBgt1JYjTI0w6VY+G89060QxgxMuwTGffQO+7gZPeC4f42NTg+aGajcLI1qmBVMw8UEXRTAmIPzqyE+Pr/w+DTG0eklTeCr3HSt8tWZNIWoiaTaRqmyhFJ5EVFlGZXGKqrza0jSNopxCwHVROky8sUGYrIx1dea6wiTBeWnVJ/F3aAGZrkJbK9MJAo5F4EbHoRVgG7mCB8GrcLNPOHBED7lXbJ5SCeC5cZKL50YgrSM6062NzQuaUUTUno4ODxFrd7E5tY2urt7qDdayvb2T9DpdJW/092DkK7SME1AI7IPUkObgpwCBlkG3w/w9vaO/tkAw+EtRqN7tNsrOB9c4OXlFePxs+KO4o6TI432o+XQDRpOVz/A/4XWzKblpgtNt5RfCKtwctk6NIWRxWgibk+uZForA2qo3tVfTDuiZ1CaWBGivrKBdGGVRJSje1LQ05ml/QYtvuCkXM3u3AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/883fec6b5aa5754c4adabbe0fe797299/263a4/sponsor.webp 480w,
/static/883fec6b5aa5754c4adabbe0fe797299/a5a71/sponsor.webp 693w&quot; sizes=&quot;(max-width: 693px) 100vw, 693px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/883fec6b5aa5754c4adabbe0fe797299/9aebd/sponsor.png 480w,
/static/883fec6b5aa5754c4adabbe0fe797299/ee2da/sponsor.png 693w&quot; sizes=&quot;(max-width: 693px) 100vw, 693px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/883fec6b5aa5754c4adabbe0fe797299/ee2da/sponsor.png&quot; alt=&quot;sponsor&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;part-01-기술-공유하고-소통하다--물류-시스템-개선기-발표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-01-%EA%B8%B0%EC%88%A0-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B3%A0-%EC%86%8C%ED%86%B5%ED%95%98%EB%8B%A4--%EB%AC%BC%EB%A5%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EB%B0%9C%ED%91%9C&quot; aria-label=&quot;part 01 기술 공유하고 소통하다  물류 시스템 개선기 발표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Part 01. 기술, 공유하고 소통하다 : 물류 시스템 개선기 발표&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;명실상부 대한민국 대표 헬스&amp;#x26;뷰티 스토어인 올리브영은 매일 수많은 트래픽과 데이터를 처리하고 있습니다. &lt;br&gt;
특히 매 올영세일마다 매출 신기록을 갱신하고 있고, 세일 기간의 트래픽과 데이터의 양은 기하급수적으로 증가하고 있는데요. &lt;br&gt;
이러한 상황 속에서 시스템의 실시간성과 확장성을 위해, &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;b&gt; 1. 기존의 EAI(Enterprise Application Integration) + 배치 기반 구조로 동작하던 물류 IF를 AWS MSK(Kafka) 기반의 실시간 처리 시스템으로 전환하고,&lt;/b&gt;&lt;br&gt;
&lt;b&gt; 2. RDB 중심의 재고 시스템을 Redis 기반으로 개선한 경험을 소개했습니다.&lt;/b&gt; &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;figure style=&quot;display:flex; justify-content:center; gap:10px; flex-direction:column;&quot;&gt;
  &lt;div style=&quot;display:flex; gap:10px;&quot;&gt;
    &lt;div style=&quot;width: 40%&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEBf/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/aAAwDAQACEAMQAAABQzNcXEBm/wD/xAAbEAACAgMBAAAAAAAAAAAAAAAAAQITAxESIf/aAAgBAQABBQJYSlIqR1ockoW+f//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAgEBPwFlf//EABsQAAMAAgMAAAAAAAAAAAAAAAABESEyQZGh/9oACAEBAAY/ArfDk1GqxNN5FL2f/8QAGhABAAMBAQEAAAAAAAAAAAAAAQARITFhkf/aAAgBAQABPyFcVDyBBd75sb8+4vObtMvKRXYMLibcf//aAAwDAQACAAMAAAAQkB//xAAXEQEBAQEAAAAAAAAAAAAAAAABABFB/9oACAEDAQE/EHOQl//EABgRAAIDAAAAAAAAAAAAAAAAAAABETFh/9oACAECAQE/EIbs0P/EABsQAQEBAQEAAwAAAAAAAAAAAAERACFRMZGh/9oACAEBAAE/EIq0Da/ritD8XF+tSB5xpW6ktsCcbj8TAuBIzyNySKIl6+7/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/14b3cc5e793fdb66ad5eba0da8854fea/263a4/presentation1.webp 480w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/a6361/presentation1.webp 960w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/0b34d/presentation1.webp 1920w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/da28f/presentation1.webp 2880w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/98b7d/presentation1.webp 3840w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/c13be/presentation1.webp 5712w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/14b3cc5e793fdb66ad5eba0da8854fea/a3e66/presentation1.jpg 480w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/fb816/presentation1.jpg 960w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/aaf92/presentation1.jpg 1920w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/1d134/presentation1.jpg 2880w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/9ad4e/presentation1.jpg 3840w,
/static/14b3cc5e793fdb66ad5eba0da8854fea/00420/presentation1.jpg 5712w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/14b3cc5e793fdb66ad5eba0da8854fea/aaf92/presentation1.jpg&quot; alt=&quot;presentation1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/div&gt;
     &lt;div style=&quot;width: 65%&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEAv/EABUBAQEAAAAAAAAAAAAAAAAAAAEA/9oADAMBAAIQAxAAAAFeZUjWShf/xAAcEAABAwUAAAAAAAAAAAAAAAABAAMTERIxMjP/2gAIAQEAAQUCvCmFJAU7zGgx/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8BV//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABwQAAEDBQAAAAAAAAAAAAAAAAABETECEBIhgf/aAAgBAQAGPwLJtEOxBQLy3//EABwQAQADAAIDAAAAAAAAAAAAAAEAESEQMWFxof/aAAgBAQABPyEIxE0bLLeZsNlT2z63iuqf/9oADAMBAAIAAwAAABA77//EABYRAAMAAAAAAAAAAAAAAAAAAAABIf/aAAgBAwEBPxBWjH//xAAXEQADAQAAAAAAAAAAAAAAAAAAAREh/9oACAECAQE/EHihD//EAB0QAQABBAMBAAAAAAAAAAAAAAERABAhQTGRobH/2gAIAQEAAT8Q2AgYj1mhMlCUJgmguU1DbPk+my//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2971cd990524ee7bb5578e628c653ac0/263a4/presentation2.webp 480w,
/static/2971cd990524ee7bb5578e628c653ac0/a6361/presentation2.webp 960w,
/static/2971cd990524ee7bb5578e628c653ac0/0b34d/presentation2.webp 1920w,
/static/2971cd990524ee7bb5578e628c653ac0/da28f/presentation2.webp 2880w,
/static/2971cd990524ee7bb5578e628c653ac0/98b7d/presentation2.webp 3840w,
/static/2971cd990524ee7bb5578e628c653ac0/d6bb8/presentation2.webp 4000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2971cd990524ee7bb5578e628c653ac0/a3e66/presentation2.jpg 480w,
/static/2971cd990524ee7bb5578e628c653ac0/fb816/presentation2.jpg 960w,
/static/2971cd990524ee7bb5578e628c653ac0/aaf92/presentation2.jpg 1920w,
/static/2971cd990524ee7bb5578e628c653ac0/1d134/presentation2.jpg 2880w,
/static/2971cd990524ee7bb5578e628c653ac0/9ad4e/presentation2.jpg 3840w,
/static/2971cd990524ee7bb5578e628c653ac0/120d2/presentation2.jpg 4000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2971cd990524ee7bb5578e628c653ac0/aaf92/presentation2.jpg&quot; alt=&quot;presentation2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align:center;&quot;&gt;발디딜 곳 없이 꽉 찬 발표장&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;발표는 45분간 진행되었고, 정말 많은 분들이 관심을 가져주셨습니다. &lt;br&gt;
약 150석의 좌석이 가득 차고도, 자리가 모자라 발표장 뒤쪽에 서서 듣는 분들이 많아 뒷쪽 벽이 보이지 않을 만큼 큰 호응을 얻었습니다. &lt;br&gt;
행사 종료 후 진행된 설문에서도 &lt;b&gt;&apos;가장 유익했던 세션&apos;&lt;/b&gt; 중 하나로 꼽히는 기쁨도 누렸습니다. 🙌 &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;발표 후에는 많은 분들이 홍보 부스를 찾아주셔서 발표 내용에 대한 질문과 의견을 나누는 시간을 가졌습니다. &lt;br&gt;
다른 발표와 시간이 겹쳐 홍보 부스에서 질문에 대한 답변을 드리지 못한 분들도 계셨는데요. &lt;br&gt;
그중, &lt;b&gt;많이 겹쳤던 질문들을 위주로 Q&amp;#x26;A를 정리해 보았습니다.&lt;/b&gt; &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h4 id=&quot;물류-시스템-개선기-관련-qa&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%BC%EB%A5%98-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EA%B4%80%EB%A0%A8-qa&quot; aria-label=&quot;물류 시스템 개선기 관련 qa permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;물류 시스템 개선기 관련 Q&amp;#x26;A&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;DLQ(Dead Letter Queue) 관련 문답&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q.&lt;/strong&gt; &quot;DLQ를 사용하면 순서 보장이 어려웠을 텐데, 이런 문제가 없었는지, 어떤 전략이 있었는지 궁금합니다.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; &quot;순서 보장이 중요한 토픽은 DLQ를 사용하는 대신 애플리케이션 수준에서 재처리 전략을 통해 예외 상황을 처리했습니다. &lt;br&gt;
DLQ에 메시지가 쌓이는 경우에는 우선 오류 원인을 분석하고, 재처리가 필요한 메시지에 한해 순서를 고려하여 처리하도록 설계했습니다. &lt;br&gt;
또한, 순서 보장이 필요한 경우 메시지 키 기반으로 Kafka 파티션을 구성하여 동일한 키의 메시지는 항상 같은 파티션으로 전송되도록 함으로써 Kafka의 파티션 수준 순서 보장을 활용하고 있습니다.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KAFKA 운영 관련 문답&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q.&lt;/strong&gt; &quot;Kafka를 실무 환경에서 안정적으로 운영하기 위해 고가용성을 확보하는 데 도움이 되었던 방법이나 운영 상의 노하우가 있을까요?&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; &quot;카프카 운영 시 메시지 중복과 유실 문제가 발생할 수 있고, 이를 방지하는 것이 안정적인 카프카 운영의 핵심이라고 할 수 있는데요. &lt;br&gt;
&lt;a href=&quot;https://oliveyoung.tech/2024-10-16/oliveyoung-scm-oms-kafka/?keyword=kafka&quot;&gt;Kafka 메시지 중복 및 유실 케이스별 해결 방법&lt;/a&gt;
포스팅을 참고해 주시면 좋을 것 같습니다.&quot; &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h4 id=&quot;오프라인-재고-시스템-개선기-관련-qa&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EC%84%A0%EA%B8%B0-%EA%B4%80%EB%A0%A8-qa&quot; aria-label=&quot;오프라인 재고 시스템 개선기 관련 qa permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오프라인 재고 시스템 개선기 관련 Q&amp;#x26;A&lt;/h4&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;b&gt;REDIS 데이터 정합성 관련 문답&lt;/b&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;Q.&lt;/strong&gt; &quot;Oracle과 Redis 간 데이터 정합성이 틀어지면 어떻게 처리하나요?&quot;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; &quot;정합성 이슈에 대비하여 Oracle과 Redis 간 데이터 정합성을 주기적으로 비교·검증하는 배치 작업을 운영하고 있습니다. &lt;br&gt;
이 과정에서 정합성 문제가 발생하면, Datadog을 통해 Slack 알림을 전송하여 실시간으로 대응할 수 있도록 구성되어 있습니다. &lt;br&gt;
문제가 확인된 경우에는 Oracle 데이터를 기준으로 Redis 데이터를 업데이트하여 정합성을 복구합니다. &lt;br&gt;
또한, 매장 운영이 중단되어 재고 변경이 없는 새벽 시간대에 Oracle 데이터를 기준으로 Redis 데이터를 일괄 업데이트 및 생성하여, &lt;br&gt;
정합성 오차가 발생하더라도 새벽에 자동 복구될 수 있도록 설계되어 있습니다.&quot; &lt;br&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;b&gt;재고 이력 관련 문답&lt;/b&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;Q.&lt;/strong&gt; &quot;재고 도메인 특성상 이슈 대응을 위해 특정 시간에 몇개의 재고가 있었는지와 같은 이력 관리도 중요하다고 생각하는데, &lt;br&gt;
재고 이력 관리는 어떻게 하고 있나요?&quot;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; &quot;맞습니다. 과주문 이슈, 입고 이슈 등의 대응을 위해 재고 이력 관리는 매우 중요한데요. &lt;br&gt;
기존에 여러 테이블, 시스템, 백오피스 화면에서 재고 이력을 관리하고 있어 내부 사용자들의 혼란이 많았습니다. &lt;br&gt;
또한 조회 시점의 재고 수량만 알 수 있는 상태였고, 특정 시점의 재고 수량을 조회하려면 수많은 테이블을 조인하고, &lt;br&gt;
너무나도 복잡한 쿼리를 작성해야 해서 이슈 대응이 매우 힘들었습니다.. &lt;br&gt;
&lt;p&gt;이런 문제들을 해결하고, 재고 이력 관리의 일관성을 갖기 위해 &lt;b&gt;OpenSearch를 기반으로 EFK스택&lt;/b&gt;을 구축하여 재고 변경 이력을 통합 관리하고 있습니다. &lt;br&gt;
Oracle 에 재고가 변경되는 시점에 Redis 에 데이터를 적재하고 있으며 이때 OpenSearch 에 재고 변경 이력을 함께 저장하고 있는 구조 인데요. &lt;br&gt;
Fluentbit sidecar pattern 을 사용하여 구축했는데 이는 &lt;a href=&quot;https://oliveyoung.tech/2024-04-02/opensearch-efk/?keyword=efk&quot;&gt;AWS OpenSearch 기반 EFK STACK 구축기&lt;/a&gt;
포스팅을 참고해주시면 좋을 것 같습니다.&quot; &lt;br&gt;&lt;/p&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;b&gt;기타 성능/부하 관련 문답&lt;/b&gt;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;Q.&lt;/strong&gt; &quot;전자라벨의 경우 특정 매장의 모든 상품 재고를 필요로 하여, &lt;br&gt;
Redis Hash 구조에서 매장별 키(store key)를 생성하여 상품코드를 해시 필드로, 상품의 재고 수량을 해시 value로 하여 데이터셋을 구성하셨다고 했는데요. &lt;br&gt;
해당 해시키를 사용한다고 해도, 올리브영의 천 개가 넘는 모든 매장에서 모든 상품의 재고를 조회하게 되면 성능 이슈나 부하 문제가 발생했을 것 같은데, &lt;br&gt;
관련해서 성능 이슈는 없으셨나요?&quot;&lt;/p&gt;
  &lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; &quot;일부 매장의 전 상품 재고를 조회할 경우 부하가 크지 않아 문제가 되지 않지만, &lt;br&gt;
예상하신 것처럼, 전자라벨처럼 주기적으로 모든 매장의 모든 상품 재고 수량을 조회하는 전자라벨의 요청은 서버나 REDIS 입장에서 큰 부하 요소입니다. &lt;br&gt;
그래서 매장이 오픈되기 전인 새벽 시간엔 매장 해시키를 사용하여 모든 매장의 모든 상품 재고를 조회하는 API를 사용하고,&lt;br&gt;
트래픽이 많은 일과 시간엔 변경된 재고 수량만 반환하는 API를 사용하고 있습니다. &lt;br&gt;
관련하여 &lt;a href=&quot;https://oliveyoung.tech/2024-11-15/inventory-changed-stocks-function-with-redis-stream/?keyword=STREAM&quot;&gt;&quot;재고의 변동을 시계열 데이터로?!&quot;&lt;/a&gt;
 포스팅을 참고해주시면 좋을 것 같습니다.&quot;
  &lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;발표 자료를 놓치신 분들은 🔗&lt;b&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Tlh-8msdOM8&quot;&gt;이곳&lt;/a&gt;&lt;/b&gt;에서 &lt;b&gt;발표 자료&lt;/b&gt;와 &lt;b&gt;발표 영상&lt;/b&gt;을 확인하실 수 있습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;part02-사람-소통하고-연결하다--홍보-부스-현장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part02-%EC%82%AC%EB%9E%8C-%EC%86%8C%ED%86%B5%ED%95%98%EA%B3%A0-%EC%97%B0%EA%B2%B0%ED%95%98%EB%8B%A4--%ED%99%8D%EB%B3%B4-%EB%B6%80%EC%8A%A4-%ED%98%84%EC%9E%A5&quot; aria-label=&quot;part02 사람 소통하고 연결하다  홍보 부스 현장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Part02. 사람, 소통하고 연결하다 : 홍보 부스 현장&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;01-홍보부스-운영-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#01-%ED%99%8D%EB%B3%B4%EB%B6%80%EC%8A%A4-%EC%9A%B4%EC%98%81-&quot; aria-label=&quot;01 홍보부스 운영  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;01. 홍보부스 운영 📢&lt;/h3&gt;
&lt;p&gt;발표 외에 홍보 부스도 운영하여, &lt;b&gt;올리브영의 개발 문화와 조직 문화를 소개&lt;/b&gt;하는 시간을 가졌습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;올리브영의 백엔드 개발자들이 &quot;오늘드림&quot;, &quot;올영매장찾기&quot; 등 &lt;b&gt;직접 자신이 개발한 서비스&lt;/b&gt;를 소개하고, &lt;br&gt;
개발하면서 사용한 기술 스택과 개발 환경을 소개하며, 올리브영의 개발 문화를 알리는 시간을 가졌습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;figure style=&quot;display:flex; justify-content:center; gap:10px; flex-direction:column;&quot;&gt;
  &lt;div style=&quot;display:flex; gap:10px;&quot;&gt;
    &lt;div style=&quot;width: 40%&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 616px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.58333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEJElEQVR42g2T6U/TBwCGf25m2ZaYbFMTEaVAQaAVKXKUqy3QQ1pAsNAKHlBaKFfXwkDOtoDlUBgDKwhWWibIuAVxTqNLPNmVJUuWZZ/9sv0N+/asf8H75n2eV/hAEBCLjqDJFGNUSWgoSeV6rYIb9YWM2nW4rToWekz0XS2k9UIOP/pbWbxuZ26wmVmfi/mxDoITXTyY7mNz1oNw8ICAIkWE60I6I3V5rHQX83TYzNZgFUH3Je4P2Xg6086Yy0yPpYQ3y8OszXjYCIywsTDOytwoa4EbbIe+Zm9pCuHjDw+gz05kwKrF22zC02TE33WFWU8DU302fB0W3q1NMNZVz7UGM7/sBXiyOoN/wsvkTS+PNhfYXQ/ww3aIZzuLCIc++QiTVkZ3ZydxmhZEIhEqtR6VRk+JVk3L5XJe7t7D1+ugy2nl3fM1Rn1u1MoC0s6k0tXTwc7OCveCd1haDiDEREVgMWtxusc5JFZw+NODyNNSSYiLJed0PJOeTp7tLTPkbqdYk0ut0cDckI5/91vYHGgk4VQSsXESYqOjMBTmIkhlSqoq1BgtDuQqAzLpKcqK8lFlycjKkJFntNM7Oo3X20OyNJEs2Wlm2tL4eT6P7cVbKIou8sXnEZyVJFBSKEcwVOWiyZVSXHmF/b/+4auOTkp14RDTecyVFXS2NDPU38+gP8Tk8hO+3fmd4Ppv2BwDVLWNYGr1UutyU2q2YjLXIRQ2KagwqKgfCvD4p/dYLHby5TLSMzTItTYy5Gpqzqso0Onwjgd5eHeLR7t/YHWNctnpRWu0UG5rw9U/RV/vBELpcCrOh/GMPXjO/OprMnOV5KlzOJqSTURMHmmqCjwjo+gUUnyjQZ4HH+P3jXPOZKO8roOUbDXHIk9y/HgkUaJYhIuhZL58k0z33Yc0tw9SWm+l4loTRfX1RIqV4V3LePb2e/zuTkasbXS7b3HF7kFZUs25Siux0lRqba3cngzgDoMVasf1tD8q5oJjmNzcfIw1LRitDpKSNRyLzeJspoHdB7dZuDOE16UiURSDTqch1N9KTZi6KF7K4DdBXr2HP//+D+HFq1f4Qxv4ZgI0Ol3oqq8ilqo5ckLG0SNROOsu8d3OLOsvQyy9mKHLeonhxipaS/I4I5GQfFZOeZOHivm33H+yj/D6130WNzZZWg/R4HAQlZRFtKSAE7GZfHY4ngSJjNnNYbbe3GX79QLLKwNkKxREJ6WRnlNISp4Gu97Ilr2HYZMZwdPtwtbsxN47Rul5AxEnZcQkFiA+rSVaqgvvKA8rUo1v2smt5QFm1gfQlOhRasspNBhJVxZTZ69hNeBhb3oQYfWmg+m+RpzNDirNVZxJD7cT5xCToOBkXA6RMRlIEpPCNzuFTqsMu1mMNCy4VJaBqqiMfH01ZTVGrs/ZmVrt4X9eIYsRXEyJUAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e42fa6a0fc2707f11270fe27ca6b564a/263a4/booth.webp 480w,
/static/e42fa6a0fc2707f11270fe27ca6b564a/cec2c/booth.webp 616w&quot; sizes=&quot;(max-width: 616px) 100vw, 616px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e42fa6a0fc2707f11270fe27ca6b564a/9aebd/booth.png 480w,
/static/e42fa6a0fc2707f11270fe27ca6b564a/274fb/booth.png 616w&quot; sizes=&quot;(max-width: 616px) 100vw, 616px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e42fa6a0fc2707f11270fe27ca6b564a/274fb/booth.png&quot; alt=&quot;booth&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/div&gt;
     &lt;div style=&quot;width: 57%&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 879px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAADJElEQVR42hWOS08bBACA+wfMAmw4mSZzHvcQRkC3gJuQaUdbcHXlPVoe1tIHBUFkFGjBzjJeFgt0AzpHgbWFAqUPaHnYlUGobI5sYRoH8aDR2y4evJlPPHzJd/ryCeZmHCzOTxBccLIedvNgwkZ25nnaFFK6qq8zafuanZiHrcgEA0Y1so9zkIoKyTh3BmHGaVQFuXQpZaSdSuH0m8kIFv1ugsFZlpe9RKNB5hemuJx1AYumAqtegc85xPrDMGXFZUjFIrRFV9FXlVMsleDorMZlN2Nt1aApl9Gur0XgC3gILM0RCvuIxpYJLc8jFubS11CDvUWN3+0k/nCEJz4pUz16Ppe8zy11PvJSGdO3KhmzGrl0JY+rEinFNxQIFlZX+J/w5hb3XS4Gu2/ivd/LkEHHSLOS6XEHvrF6tmxn8Y+aMJitXC8QkpqehsNYQeGhJyWfIO3dVD7Iyj483HzBD8//ZGPvLwKPXuD2zBCLOHGY6unXlnNTpya/VE/hjXqqmgfovLdOTr6ck4mvM6KT0aEsJPFIAscSEzh+7CiC6JMDVrZ/JhDbxRveZiG6x1pkDk2pmI7qInraDKh0bUjKtahav0XecJsStQl7XSNx9zBTploahBmcSkkmKekw+Msf/xKJv2QlfoB9bYextV2CKz+RmpFNpSSPvY01Jp3T5BWrqGrqQa78EqPJTIOykjtd9YT69TRJMkk58hpvHU1A8OzgHx7v/832y1cEt39lyr/BXccivbZx7I5hdvcPGJxdQlympVjTiandzHCPiQ+v5PKVtgJXfxNtFUKqcs5wW1WAILbzG8HIU74ZGEWhNVCiqOHT6mu0Wg3ci/nwP41jsvSRV1J7GNXTZ+nGOmShTq0k7DDRoZZxOfM9zI0aulpaEGRdEnL8jXc4d+JtvvjoGi3mbkLPvVhnXPTFYoSfeXngtWG/Y0evqsE71E/APYp/sJGgRU5+voLz4nZSRUbO5mgQfFJWhPw7EQ6zju8V9XSZjUR3/Tz+fYnNfTczj3qZWx1ndSWEZ3KI2bs9/Oix8Cpqpr3uM05erCNdYiRd1E66sJn/AOX0JkAVbvfpAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/661cb04d3803037fa02fb47c4fe0b33c/263a4/booth2.webp 480w,
/static/661cb04d3803037fa02fb47c4fe0b33c/d0a37/booth2.webp 879w&quot; sizes=&quot;(max-width: 879px) 100vw, 879px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/661cb04d3803037fa02fb47c4fe0b33c/9aebd/booth2.png 480w,
/static/661cb04d3803037fa02fb47c4fe0b33c/00c79/booth2.png 879w&quot; sizes=&quot;(max-width: 879px) 100vw, 879px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/661cb04d3803037fa02fb47c4fe0b33c/00c79/booth2.png&quot; alt=&quot;booth2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align:center;&quot;&gt;각 유닛을 대표하여 개발자 분들이 열정적으로 자신이 개발한 서비스와 올리브영의 기술 스택/개발 환경을 소개하고 있는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;올리브영의 개발 문화와 조직에 대해 궁금한 점을 물어보시거나, &lt;br&gt;
올리브영에서 진행 중인 프로젝트에 대해 질문하시는 분들이 많아 &lt;br&gt;
개발자분들 사이에서 올리브영에 대한 관심이 높다는 것을 느낄 수 있었습니다.🔥 &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;또한 홍보 부스에서는 올리브영과 관련된 간단한 퀴즈를 풀고, &lt;br&gt;
&lt;b&gt;올리브영의 다양한 상품을 받을 수 있는 뽑기 이벤트&lt;/b&gt; 🎁를 진행했습니다. &lt;br&gt;
역시 많은 분들이 참여해 주셨고, 퀴즈를 통해 올리브영에 대한 이해도를 높일 수 있는 기회가 되었습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center; gap:10px;&quot;&gt;
  &lt;div style=&quot;width: 45%&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1015px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAELUlEQVR42hWTaUwTBgCFu7klyzRblumyTGWIV0RXFSlCC0iBXvSASg/ogVCgtKUXVMAiLVAtILUcKocUQR0SjyGbLls2dTNzi1nmdE6T/SDbNNkSs2S/tv/fuh/vx3t/3nvJe4IL48N8cWWBVFcLY0EHfU215GRuYPs7b5G9cT3CLVmUFYhQF4sQbt2ETJJHuUSEXlFCSWkZUpmCMoUKqVyJzWpBcHEqSXL0DH2eJo47rQy6jXS12nFYzOSLJeSISxHn7EReIMRh1jLe7eJEZwtDPUGUegNyvRF5pRFFVTXKNATXxk4yO+LHb5UQcbcyGq0hNX+SqVNn6O/twNwUROsYRiGVEmqtJxGwkewPET8SwG06mOZWOhsq0ZUWUizKQTAzPIrX28pIfzuJ+CD9bhOXp+sJGmWsLFekE8uIh5sJucqw6KuwKovo7Q4SCzk4G7cQMhbSYy+np1GD95AVwVA0SqPtED8//4vHv6zwaHmA+9NSurzNxBsO0BtJ8GDByfNvo9y69yN1jQ5MBjOLsQPwTM+5HhESURGJ9gou9OUjmF+6xNzlC6x8/yUfD7UxnzxGZ+AIGzKFyLW1tLjcGGVSFm7e5urcBLcX5vntj3/5eukUH4WFHLEWUR8aJuj3EjbmIrhyY57FxTFSk8eYi3cwG2vDZbewMWMrGnUFRXm5iEvlJOcvodi/i+riXHy1lXhbPYR64tQ7DxNJztPsj6CqtCO4Oa7l2cMYl0b0nGirY8KrwVNVTuamHWjVatoaLUwdizAU7iB367tEbWpMov/Nqgkn5vB1D+LwdCGRanC2diD452EJL57q+Pvufk43i0n1utBVyHl77XqOBj1Mp/c5YNcy6TMQssqQ5e2iUbafjkYbLn8XlpYOKtR6+tw2LnbVIXgys48fFjTcT2xj0JrPjQ8n0GqUrFr1KlfG+zlYvA+nWcH1s/1E7DK89mpCDTXUatSoVBom4lGuj0boTetOT166ckjO7XYhPvl2Rgaj/Pn7d4xPj1AZ9LE0GUMp3ou6vJCr50/wWaqbe4sJ7oyb+fVROwHLTlyVCsoLJezZK0VuLkHQWVvLxRYTBpWC7uhRpoYjzJ46zpOnX/F4OYVBpyQr4z0mx8I8uHuWO0tzXBtQ8eJWET+dFlOYk0fWjt0UlSjRVZkR1Pt6qJAUUibTUVPvp1pjoq3OxOezx/lmKsb5yWE6O9z4XBb608Zhixa7w81sq5TDOgnrN2Wz7YPNbNy8C8uhJgS7s3eTmbGF3DwJigoTNc1dnJuZZeXT8yxHnMwNBDDq5by+Zg1r31xNgTCLyhoHRpsbT1sMgy39qoCTQKCTKnP6KbnCfWzJ2MzenHxUagN79hQw7HbwSdRHslGLQyfmjdWv8crLL5H9/jp27tiG3dtNU1sfodgEDZ4wfq+XmakUmioT/wG3cpy6FnpmnAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7b8f020d442478d37d518d35580ea3cb/263a4/luckydraw.webp 480w,
/static/7b8f020d442478d37d518d35580ea3cb/a6361/luckydraw.webp 960w,
/static/7b8f020d442478d37d518d35580ea3cb/5599d/luckydraw.webp 1015w&quot; sizes=&quot;(max-width: 1015px) 100vw, 1015px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7b8f020d442478d37d518d35580ea3cb/9aebd/luckydraw.png 480w,
/static/7b8f020d442478d37d518d35580ea3cb/a91f8/luckydraw.png 960w,
/static/7b8f020d442478d37d518d35580ea3cb/4d2c8/luckydraw.png 1015w&quot; sizes=&quot;(max-width: 1015px) 100vw, 1015px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7b8f020d442478d37d518d35580ea3cb/4d2c8/luckydraw.png&quot; alt=&quot;luckydraw&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;width: 60%&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1592px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAADCUlEQVR42hWS20+bBQDFv7/AGLNEfdEt4gzgBEpLKQV6YW1hvcDXe2mh0JXevxVGB235YNQWSqXAHGMr4Fh2cWwDlohRHowPGjU+GV9883/5WR/Ow0lO8kvOOYKUSOD3enGKY/jdTlLTYUr5BXY3N7i3sU69VuPe9jZf16rs1r/i0c426yvLLC0usCbL1O6uNP1dZqVbZNNZBKVaw9B1E4N6I53danp6tWg0fegGDdhGrHjcXvL5JW4GgySbMCkaIeD14fP6GROdpDMSmXQGt9OD2+VG0Ov6ae/oxmzUshhzMR/3YLePMOETCTY1NeEnFY/isNmxNgEmiw3LjTEsIzZElw+ny4NN9DDqDaIzmBDMhj6uXL6MaFLxy+EsF3tpnldj/PvDJn+dVvn7ZIvfjjc5e7TK72e7FKQokaCXiydfcr8kEbDp6Vb0YjENYzfpEXTaXjo7O9H0G8iX6tycipKIJ9l/ccZsNk0lm+J2ZJyVwjxnrx6TTUyRnZvn4uSAw9o8fvN1RvvVRMwKiiEjQo+qm2vt7Xz8aRujgRDj4QAaswup9gxvQkacnuXh0bcU88usVSokkynulGsMGzREwmG8Zgdhl71ZmwY5G0NQqRS0tbbScrWLYZuKg9MaW7UisdwKubVDJv0BCstV4hmZQnmHmZkEjRdvkFfSjFjt2N1+FtJuPrqqbEKGEK593o6yu4v3P7iCul/Bxl4OuVJlsf6Ml6c/srf/lGGTg8B4EmeihMNq4UFjl/XN/zNH+KQauoEhrA43N8b8CC0trfQoFbx36UNcIQ/yqgNFRxfKHi2NrSqeWJEOpZ7l9V0i8n0CzY5X91/y9o9/2Pv+T+pvfqX44BhrMMOT47cIA71f0PbZJ7zz7iVGRQPlshXjoA6rxcSWfAt5LkmPeoD1tQ2Onp8QTd4hLR8wHc1w3qjz0/l3vD76hkqp0hwviBAStYyajcw1T7t0e5KcZMRu7CMacvPz4QI7y3EmJ4I0yhmq5VWcFj2PGwdsPz0nN9M8umjkYX6GfVkiVVjjP5xn4WfazNBVAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d642bd4c6f9cd71f4a8172bf537547fd/263a4/luckydraw2.webp 480w,
/static/d642bd4c6f9cd71f4a8172bf537547fd/a6361/luckydraw2.webp 960w,
/static/d642bd4c6f9cd71f4a8172bf537547fd/000a5/luckydraw2.webp 1592w&quot; sizes=&quot;(max-width: 1592px) 100vw, 1592px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d642bd4c6f9cd71f4a8172bf537547fd/9aebd/luckydraw2.png 480w,
/static/d642bd4c6f9cd71f4a8172bf537547fd/a91f8/luckydraw2.png 960w,
/static/d642bd4c6f9cd71f4a8172bf537547fd/44982/luckydraw2.png 1592w&quot; sizes=&quot;(max-width: 1592px) 100vw, 1592px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d642bd4c6f9cd71f4a8172bf537547fd/44982/luckydraw2.png&quot; alt=&quot;luckydraw2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;발표 세션 사이마다 사람들로 가득 찬 뽑기 홍보 부스&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;02-현장-커피챗-️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#02-%ED%98%84%EC%9E%A5-%EC%BB%A4%ED%94%BC%EC%B1%97-%EF%B8%8F&quot; aria-label=&quot;02 현장 커피챗 ️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;02. 현장 커피챗 ☕️&lt;/h3&gt;
&lt;p&gt;홍보 부스 운영과 동시에 커리어에 대해 고민이 있는 주니어 개발자 분들 몇 분을 선정하여 커피챗 시간을 가졌습니다. &lt;br&gt;
올리브영의 개발 문화와 성장 환경을 소개하고, 현장에서 느낄 수 있는 실질적인 커리어 조언을 나누며 뜻깊은 시간을 보냈습니다. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;아래는 커피챗에 참여한 개발자 분들의 소감 💌입니다. &lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;👦🏻 4년차 백엔드 개발자 김*성님:&lt;/strong&gt; &lt;br&gt;
    &quot;아직까지 레거시만 실무에서 다루고 있어 커리어에 대해 고민이 많았는데, &lt;br&gt;
    레거시를 하더라도 어떤 자세와 생각을 가지고 평소에 업무에 임해야 할지에 대한 방향성을 잡을 수 있었습니다. &lt;br&gt;
    레거시라고 커리어에 도움이 아예 안 되는 게 아니라는 말씀을 깊이 새기게 되었습니다. &lt;br&gt;
    커피챗이 끝나고 선물로 주신 서적과, 안에 손글씨로 써져 있는 글귀도 너무 감동입니다. 감사합니다!&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;👧 3년차 백엔드 개발자 이*현님:&lt;/strong&gt; &lt;br&gt;
    &quot;앞으로 어떤 기술 스택을 어떻게 공부해야 할지에 대한 고민이 많이 해소되었습니다. &lt;br&gt;
     올리브영에서 일하는 것에 대한 매력을 느낄 수 있었고, 앞으로도 올리브영과 같은 좋은 기업에서 일하고 싶다는 생각이 들었습니다. &lt;br&gt;
     귀중한 시간을 할애하여 커피챗 해주셔서 감사드립니다.&quot; &lt;br&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;03-오피스-아워-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#03-%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%95%84%EC%9B%8C-&quot; aria-label=&quot;03 오피스 아워  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;03. 오피스 아워 🏢&lt;/h3&gt;
&lt;p&gt;홍보 부스와 커피챗을 통해 많은 개발자분들과 소통했지만, 모든 분들과 충분히 대화하기는 어려웠습니다. &lt;br&gt;
이에 &lt;b&gt;일부 개발자분들을 올리브영 본사로 초청해 오피스아워를 진행&lt;/b&gt;했습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;사전 신청으로 선정된 참가자들은 점심 식사와 티타임을 함께하며 &lt;b&gt;조직 소개&lt;/b&gt;와 &lt;b&gt;커리어 관련 이야기&lt;/b&gt;를 나누었고, 시니어 개발자들은 경험과 조언을 전했습니다. &lt;br&gt;
이어서 &lt;b&gt;테크 조직의 역할과 업무 방식, 그리고 본사 사옥 투어를 통해 실제 근무 환경&lt;/b&gt;도 살펴볼 수 있었습니다.&lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;평일 근무 시간에 진행된 행사였기에, 개인 연차를 내고 참석해 주신 열정에 보답하고자 회사 소개 자료와 소정의 기념품을 준비했습니다. &lt;br&gt;
참가자들은 “연차가 아깝지 않은 유익하고 의미 있는 시간이었다”는 소감을 남기며, 서로에게 영감을 주는 자리로 마무리되었습니다. &lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center; gap:10px;&quot;&gt;
  &lt;div style=&quot;width: 46%&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 456px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 82.45614035087718%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEU0lEQVR42h2T60+TZxjG339iHxb94MyyzGXBbWZORRSdpykCEilSrVIoRWiBQmnfFlpKT9Bz6QEt7YsWaAUDAwFBmSITPE2jw0WiLlsWs1OW7X/47dUPd/Lk+XDdV+7r+glzk2NIcT+9wSDd/jCuUBSjxcLuXV+yd89ODh/aRzzez/fLdxgdzaJSVVFafoySE2Uo1WrO1dcjWrsxmUTsNhHh9aO7vHl2n2tXc2SH0gSicZx9YTSaOpTKKsrLSygrPYJWXY1SUUp11QnUtWcJxRLMzs0SCbox6jVYxXasVhNCpD+KJAtlL2Xeic7Kc3N6ivVnz3i8eo/8pSFS4V48XUZsFgNtbU20GFro83lJJmP0dFsxtOkwW0W6ehwIdrcXo81CnaGV9h4PjkCE4Vye27eWyGUlBuVzjGdTDMb6SCUSpJMphi6k5f8B4sEY41KOCWmUkdQI0d4+hArlSQp2fMGW7VtpCqYIjy1gcbgRTS04HRaSA0kCIT8up5loUGJ1/keeLq2xmMnj7/DwfPk5dycWmJaukOmPIdQ0nmHjxx9QULSLxvRVgnMr2Ny9tOhqcDsM5PMXkaQU8aiTWCjN8uxjVheecjOdJx+QeLj0kjtTK8xmp8kOXETodHbySeFnHFFpqL8wiT07g9svJy5qSUb1XJlIc3kkRTRsZSCSZu3+b/zw3VMezCwykc7y8MYTHi2ucX10nslsDkHbpKX6fD1an4Q6kqfV7iIpZUl4jExlREJRPxaLnktpH/uKiklGJF7eeE7GHEdfXsi90WGyiRzHiw8wkx1GOFjyNadqFFS1iJzrCbG/tARRTqvNamQ4GaKroxmTQUuXqYGacxXo24xMjmSRvC72bd5EZ1MTHtmEIAh4re0IUZ+Z9EAfFrGZROoiQTkAne4UZ5qV2FydeBxmrCYtitI9iB0NBCN+PO423LZmqitKqVN9w+cFH70TrFOdRNB39eALBkjFeokFO3F113JcsZOiI1t56/5tYdv1dagqj8rCBp6szLD+cJZ03EfQbaa28Si7ij/lvQ3vo9RVIHguDGN3u/D2ORHNzZzVKmi312Mw1qJWV2Iz6eQTeFAbRDl1swzAIL+uLfPv61X+fHGb/17flbsYoLB4Gxq9AuHa/JxMxR1+efWEb6cmsNlNNOjU6Fob6PXaSIZdhGWW4+ML5MZycjj9jF5O4nWYsFtbqSzbw96iLRTt/RC15hjCyq1p1h/Ns/74JovXx+UtarQNZ2g8r8KXukxGGiTW72dm4TpXZSRDQS8OWwcWs4HKioNsK9jI9q0bKPxqM6erDyH8/mKZNz8t8c/PD/jj5T2iAZvcwUaZXZnXQB+97k6Z4VYcTgetxnY8Hhc98ttjM1JVcYjDuzdRdmALivKdNKiOI/z1apW/5XnrMjPgJ+J3MjgQYiidYCwnEfR20azTyMlrqK1Voa09zXAmzvLMiFwprdzXJsSmk5xW7Ke8bAf/A7UIKCdhL9PlAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dd49ab9eaefa35887aa6ebb2fb00b4e4/67246/olivecafe.webp 456w&quot; sizes=&quot;(max-width: 456px) 100vw, 456px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dd49ab9eaefa35887aa6ebb2fb00b4e4/b87a0/olivecafe.png 456w&quot; sizes=&quot;(max-width: 456px) 100vw, 456px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dd49ab9eaefa35887aa6ebb2fb00b4e4/b87a0/olivecafe.png&quot; alt=&quot;olivecafe&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;width: 49%&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 805px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEAElEQVR42j2TWUxUZxiGTyqmiRek1TaxprXRuI7W0sVIWiR1bZQYqTWRogEUEwRZTGzoJsgiOEzRQWAczqxn9hnOnFmZYWYYdjVW0cQYeqFp2tT7XjXpTePTH0x68ebk/y+e//ve9z1SPpsgk4osa3xMYyITZ2oiwexUmrtzWfLZGJGwD1X1k0hqpJIR4nGNsDj7PXacNjMWeRhZNmGxjCAtAbLp6P9aemB2KsXd+RzzM1lymRihkAefx4bXLwABK367i6BVwxmcQIndQ0k+RUk/Z9Azg7QEeTXdqymXHpjMjTEzmRbg8WVgWAsQCthxRB30+W8TCcwzdedfso//ZuLpX4w9hYyQTXuIlE6GSSXU5ekmcwnxjTCdzzA7PSOmHVu2QRMrj4YUAhE/ik9BzS0y/xzyz14y+9tLpp9B9M8XGP1BpHgkyFhsFJ9bwdjfL/xSkU0GfE69gMeIRwNowi+PrZdRpZeg04TZI1ZN/44r84Lvrwepb+vE8MCBQfgoRdWAgIRpqD6GZjtKPn4DU08DC85SMmMqyVgAj8fFnUA9j70nUf1O7CM36enuJGavxdhaxKqVBZQdPcJAXzdSyOvA7hTr9FXzT3IdD72VjCdiPMqreJ1maqu+4fD+Uiq/KmPkagtu+zB+l0l46mYxeo5c73Y2b9LR2d4qUr+NNDxopLX1MjWnyrlYvpsjh/bTf62dWc1BxdfHWbt2LR/v+oCtmzdz4ng5A4YOIiGbCC+Exy0zMjTAeCotzinOnz2DZLHKVFRUsLekhJIvDqP78CNKinejbzzN4dJiVr/1NuvXv8eaNWuor6pkOquS1+zMaFZS3gERaJREPMn05ATnz9UgOZ0yR8rK+OzzUqrKv6Tp9DHk3lZyQz9h6bjEG2+uZsXK1ylctQpz52We/3qPvDqCveMi42Gv6GhQFNpGKBhC3ys8dLsdnKqopLm+lnsxmftRmYWMlwXh4cJclIa6Gk4K/75rOMN8xs+ThzkezCeYjNqw26wYB0x0XTNgMskY+nqQFMVJVfVZDh3YJyph5I/FOZ78kuZ+PkAqcAtNuUHM1U9cKKb8zMToEI+mgjReqGLjxi10C9hN420BtNDS3Iik1/fS1NLCpi1beW1FAdWVJ8mp4m9w6NHselSr6N5IN0FzN2GLkNzFZPCGSP0QklRAc9MlXC4vZrOFgwfFXUtTLY1NFyjatZPCwkL27CnCZ+kjYNWjmLpxDXXiHupAGWzHPXgV37BI2XoN2fAtO3Tb0O3YSduVNurqGjlwYD/S1R+aaWqoRafT8c66dykuLsZwvR3HcA+2wS6UoS4Basc5cAWH8cdluW+10S86uWHDRrZu2c6nn2wT67/Pvn17+Q+X6Rxd0h1uiwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/21c62d034d884a5edd536806aa8f7638/263a4/officehour.webp 480w,
/static/21c62d034d884a5edd536806aa8f7638/91236/officehour.webp 805w&quot; sizes=&quot;(max-width: 805px) 100vw, 805px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/21c62d034d884a5edd536806aa8f7638/9aebd/officehour.png 480w,
/static/21c62d034d884a5edd536806aa8f7638/0469b/officehour.png 805w&quot; sizes=&quot;(max-width: 805px) 100vw, 805px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/21c62d034d884a5edd536806aa8f7638/0469b/officehour.png&quot; alt=&quot;officehour&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;마치며-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-&quot; aria-label=&quot;마치며  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며.. 🎬&lt;/h3&gt;
&lt;p&gt;SpringCamp2025는 올리브영이 외부 개발자들과 더 가까워질 수 있었던 의미 있는 행사였습니다. &lt;br&gt;
참가한 저희도 올리브영에서 일하는 것에 대한 자부심과 긍지를 느낄 수 있었고, &lt;br&gt;
외부 개발자분들의 열정을 통해 저희도 다시 한 번 열정을 불태울 수 있는 계기가 되었습니다. &lt;br&gt;
&lt;br&gt;
앞으로도 올리브영은 개발자 커뮤니티와의 소통을 지속적으로 이어가며, &lt;br&gt;
개발자들이 함께 성장할 수 있는 문화를 만들어 나가겠습니다. &lt;br&gt;
다시 한 번 SpringCamp2025에 참여해 주신 모든 분들께 감사드리며, &lt;br&gt;
올리브영의 개발 문화와 조직 문화에 많은 관심과 응원 부탁드립니다. &lt;br&gt;
&lt;br&gt;
감사합니다!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;p&gt;그리고 현재 올리브영에서는 다양한 개발 직군의 채용도 진행 중입니다. &lt;br&gt;
올리브영의 개발 문화와 조직 문화를 직접 경험하고, 함께 성장할 수 있는 기회를 놓치지 마세요! &lt;br&gt;
많은 지원을 기다리겠습니다! 🙏 &lt;br&gt;&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/list.fo?zz_target_1=&amp;#x26;zz_hot_job_yn=&amp;#x26;orderDesc=&amp;#x26;company=F97&amp;#x26;zz_title=back&quot;&gt;올리브영 개발자 채용 바로가기&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;!-- end --&gt;</content:encoded></item><item><title><![CDATA[Visual Studio Code를 Cursor처럼? Amazon Q로 AI 코딩 환경 업그레이드하기]]></title><description><![CDATA[안녕하세요, 올리브영에서 프론트엔드 업무를 진행하고 있는 개발새발자입니다. 최근 개발 환경의 가장 큰 화두는 단연 LLM 기반의 AI 코딩 도우미일 것입니다. 저희 올리브영에서도 개발자들의 생산성을 혁신하기 위해 ChatGPT, Gemini…]]></description><link>https://oliveyoung.tech/2025-08-20/amazonq-vscode/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-08-20/amazonq-vscode/</guid><pubDate>Wed, 20 Aug 2025 18:30:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 올리브영에서 프론트엔드 업무를 진행하고 있는 개발새발자입니다.&lt;/p&gt;
&lt;p&gt;최근 개발 환경의 가장 큰 화두는 단연 LLM 기반의 AI 코딩 도우미일 것입니다. 저희 올리브영에서도 개발자들의 생산성을 혁신하기 위해 ChatGPT, Gemini, Amazon Q 등 다양한 AI 서비스를 적극적으로 검토 및 도입해왔습니다. 특히, &quot;코딩 경험을 바꾼다&quot;는 평가를 받는 Cursor에 대한 기대로 PoC까지 진행 중이나, 아직은 전사 도입 단계는 아니라 대안을 찾는 목소리가 많았습니다.&lt;/p&gt;
&lt;p&gt;그래서 저희는 익숙한 Visual Studio Code 환경을 유지하면서도 Cursor의 강력한 기능을 대체할 방법을 고민했고, 그 해답을 Amazon Q에서 찾았습니다. 특히, 사내에서 쉽게 MCP로 접근할 수 있는 Amazon Q와 Amazon MCP의 뛰어난 연동성을 고려했을 때, Amazon Q는 Cursor보다 더 나은 선택일 수 있습니다.&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Amazon Q의 MCP, Rule, Memory Bank 기능을 활용해 여러분의 Visual Studio Code를 Cursor처럼 강력한 AI 코딩 머신으로 탈바꿈시키는 방법을 공유해 드리고자 합니다. AWS 환경에서 개발하시는 분들에게 이번 글이 매우 유익할 것으로 기대되니, 편안한 마음으로 따라와 주세요!&lt;/p&gt;
&lt;h2 id=&quot;cursor-vs-amazon-q-무엇이-다를까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cursor-vs-amazon-q-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%A4%EB%A5%BC%EA%B9%8C&quot; aria-label=&quot;cursor vs amazon q 무엇이 다를까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Cursor vs Amazon Q, 무엇이 다를까?&lt;/h2&gt;
&lt;p&gt;본격적인 설정에 앞서, 두 도구의 특징을 간단히 짚고 넘어가겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt;는 그 자체로 완벽하게 통합된 개발 환경을 제공합니다. 직관적인 UI, 쉬운 설정, 그리고 코드의 의미 단위(Symbol)까지 정교하게 이해하는 능력은 정말 매력적입니다. 하지만 완성된 제품인 만큼 리소스 사용량이 많고, 유료 플랜을 사용하지 않으면 최소한의 기능만 사용할 수 있다는 점이 아쉬울 수 있습니다.&lt;/p&gt;
&lt;p&gt;반면 &lt;strong&gt;Visual Studio Code에서 사용하는 Amazon Q&lt;/strong&gt;는 익숙한 내 개발 환경을 그대로 유지하면서 강력한 AI 기능을 더할 수 있다는 장점이 있습니다. 특히 AWS 생태계와의 뛰어난 연동성과 높은 자유도를 바탕으로 &lt;code class=&quot;language-text&quot;&gt;내가 원하는 대로 커스터마이징&lt;/code&gt;하는 재미가 쏠쏠합니다. 다만, Cursor처럼 모든 기능이 직관적인 UI로 제공되지는 않아 다소 복잡한 초기 설정과 일부 기능을 직접 구현해야 하는 노력이 필요합니다.&lt;/p&gt;
&lt;p&gt;결론적으로 Cursor가 &lt;code class=&quot;language-text&quot;&gt;잘 차려진 멋진 정식&lt;/code&gt;이라면, Amazon Q는 &lt;code class=&quot;language-text&quot;&gt;내 입맛대로 만드는 최고의 비빔밥&lt;/code&gt;과 같습니다. 이제 그 최고의 비빔밥을 만드는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;본격적으로 Visual Studio Code를 Cursor처럼 업그레이드하기 위해, 우리는 다음 5가지 핵심 단계를 차례대로 진행할 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기본 설정&lt;/strong&gt;: 가장 먼저 Amazon Q 확장 프로그램을 설치합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 기능 구현&lt;/strong&gt;: MCP로 외부 도구를 연동하여 파일 시스템 제어 등 강력한 기능을 추가합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;편의성 높이기&lt;/strong&gt;: 반복적인 확인 작업을 줄이도록 도구 실행을 자동 승인합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;나만의 AI 비서 만들기&lt;/strong&gt;: 사용자 정의 Rule을 추가하여 나만의 코딩 스타일과 규칙을 AI에게 학습시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pro-Tip 적용&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;메모리 뱅크&lt;/code&gt; 기법으로 AI의 할루시네이션을 방지하고 정확도를 높입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;기본-설정-amazon-q-확장-프로그램-설치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EB%B3%B8-%EC%84%A4%EC%A0%95-amazon-q-%ED%99%95%EC%9E%A5-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%84%A4%EC%B9%98&quot; aria-label=&quot;기본 설정 amazon q 확장 프로그램 설치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기본 설정: Amazon Q 확장 프로그램 설치&lt;/h2&gt;
&lt;p&gt;가장 먼저 할 일은 Visual Studio Code에 Amazon Q를 통합하는 것입니다. &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=AmazonWebServices.amazon-q-vscode&quot;&gt;Visual Studio Code 마켓플레이스&lt;/a&gt;에서 &lt;code class=&quot;language-text&quot;&gt;Amazon Q&lt;/code&gt; 확장 프로그램을 설치하세요. 설치가 완료되면 Visual Studio Code 좌측 액티비티 바에 Amazon Q 아이콘이 나타나며, 이 아이콘을 클릭하면 사이드바에 Amazon Q 채팅창이 열리면서 성공적으로 연동된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/16dee233f8cb6830164e88ea11815104/e194f/amazonq-extention.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.041666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACE0lEQVR42l2S327TMBTG+xCrYzuJ7cSO2zrtRlfWDU0CaXCLBG/AM8MdU3gALiE3SLhn38myDXHx6cSW8zvf+bOQpR5qZ0hXVS7rmipryRhDTdNQ27ZT9N5TjHE6T3c4B7zb1hW1UlIjRI5lSRfWfl9cO//jbn9Drzt/2jtHh+DJ4rHDN4uBHJVSVCNhg28DRWuoxd0ZgIWUeYnvUoj7xWe3+fnh+JHebten2z7RdgY9qaqqCSaEIK01tXOCiKTsTkAKwAJvTFHcLypjh/NVon1KJ87s5h9YFiokXCyXJPEDi+8qKPwDlADKF6AZrrpAqe8zA7lHBo+5NI/4JA2HQSt6hf6em5qO6J8vCpJwjbcZM3gE3lVquL1AqanPK0AmAbCeZKjH+XK1oj16u8LZOziDNgC3EkAMo650VgBPwEOww+HNJa27mANPjyc5O2THFi2I6zW1XTc5t/N9N5VcEFpGIWwyKn0Eqrgbju8/Ub9ZZb/ZUdjuyaCcqY88bS4ZiZr/estDaYolhhbpcPUlly6QEWf3i9aH4Xi85lXJtW2oiYn8vG8+BGQPz/vI68Rip3F2KDA0pcqXKZdaDUtMMaWUeS0sGp62O/I3d9QA5OadtPPCc6wRozMzkPewyAyegHU6fHPO/un77W84GeFkXKc0tsd3o9blqJSE1LOQdBSITalHL8SIHRwrKX+Zuv7blfrrA4/KKxIA10saAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/16dee233f8cb6830164e88ea11815104/263a4/amazonq-extention.webp 480w,
/static/16dee233f8cb6830164e88ea11815104/a6361/amazonq-extention.webp 960w,
/static/16dee233f8cb6830164e88ea11815104/0b34d/amazonq-extention.webp 1920w,
/static/16dee233f8cb6830164e88ea11815104/7d7b7/amazonq-extention.webp 1941w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/16dee233f8cb6830164e88ea11815104/9aebd/amazonq-extention.png 480w,
/static/16dee233f8cb6830164e88ea11815104/a91f8/amazonq-extention.png 960w,
/static/16dee233f8cb6830164e88ea11815104/ac7a9/amazonq-extention.png 1920w,
/static/16dee233f8cb6830164e88ea11815104/e194f/amazonq-extention.png 1941w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/16dee233f8cb6830164e88ea11815104/ac7a9/amazonq-extention.png&quot; alt=&quot;Visual Studio Code에서 Amazon Q 확장 프로그램 설치한 모습&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Visual Studio Code에서 Amazon Q 확장 프로그램 설치한 모습&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;h2 id=&quot;핵심-기능-구현-mcp로-외부-도구-연동하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B5%EC%8B%AC-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-mcp%EB%A1%9C-%EC%99%B8%EB%B6%80-%EB%8F%84%EA%B5%AC-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0&quot; aria-label=&quot;핵심 기능 구현 mcp로 외부 도구 연동하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;핵심 기능 구현: MCP로 외부 도구 연동하기&lt;/h2&gt;
&lt;p&gt;Cursor의 강력함 중 하나는 다양한 외부 도구를 통합하는 MCP를 지원한다는 점 입니다. Amazon Q에서도 &lt;strong&gt;MCP(Model Context Protocol)&lt;/strong&gt;를 지원하고 있으며, MCP를 활용하면 파일 생성, 이동, 라이브러리 설치 등 다양한 작업을 AI 명령어 한 줄로 처리할 수 있어 코드 작성의 퀄리티를 한 단계 높일 수 있습니다.&lt;/p&gt;
&lt;p&gt;파일 시스템을 제어하는 &lt;code class=&quot;language-text&quot;&gt;desktop-commander&lt;/code&gt; MCP 서버를 연동해 보겠습니다. &lt;a href=&quot;https://smithery.ai/server/@wonderwhy-er/desktop-commander&quot;&gt;desktop-commander MCP서버를 제공하는 Smithery.ai 웹 페이지&lt;/a&gt;로 이동한 후 &lt;code class=&quot;language-text&quot;&gt;desktop-commander&lt;/code&gt; MCP 서버 정보를 가져옵니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;잠깐!🖐️ Smithery.ai란?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Smithery.ai(이하 Smithery)는 한마디로 &apos;AI를 위한 앱 스토어&apos;입니다. 우리가 스마트폰에 새로운 앱을 설치해 기능을 추가하듯, Smithery는 AI가 웹 검색, 데이터 분석, 특정 서비스 이용 등 새로운 능력을 얻을 수 있도록 도와주는 플랫폼입니다.
이곳에는 AI가 사용할 수 있는 다양한 도구(MCP)들이 목록화되어 있어, 개발자들은 필요한 기능을 손쉽게 찾아 자신의 AI에 연결할 수 있습니다. 즉, Smithery는 AI와 외부 도구를 연결하는 &apos;중앙 허브&apos; 역할을 하여, AI가 더욱 강력하고 실용적인 프로그램으로 발전하도록 돕는 핵심적인 공간입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 840px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b070cc6b6da93765e2a367fe4d301101/2d81a/smithery-connect.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3UlEQVR42mVTSY7bMBDULdZGbZRELaQWayw7tmd3BgHmlAV5Qn6UF+S/lW5aNjDOodBtgqquqqadMQ2x7zUetzP244B1rW5QYmoUBpUjz1IUuYRpG1t1U0PSWadbe9a1NZwiiWFKiSxNEIkQgechDHyEPiHwIKi/IAoDQrggsBBLvfTOfrfDy9PjFW+nV2hVIk9TqygW4ZVEhD4C/zxQLAS3cGqlMA0DDMlvK3WtSSQsQiIIFsRCQBWFdWNdMG4Jh85gO2/Q1JVFISVKyqdYwARlQfnJDEkcwVBeRZ5bsoziSqKI4gjOYMLN3YT748GSztSrsviAiuwzuE9J8aQbzF2LeejITQNDItiRpspCnJpt0lRv9Qmeu4LvufBd91xtv6JFuVaRTBKMRhNpjamtiFhjsx4tKZ/XNNRRZGei59LypP+g0NDAjlTxgthiRxlvN3d4f/uCb+9f8evHd7yQw5FEVUzIF/kN8YesQFLguUxtZfAC+E5K+Uki7Onu1Bs8HPZ42m3xcDxiPU1o29bGYrf8eTdjWp83rRfwkEvlbAQ9H1Y40O+eslsPPU6DwU/K89lUOOkKhtw6LJM/SmLxQZVVSm+R/wlWIZFdLDN6O7DBb5Ph77PCnznCQSb4B0DWGfr9tYxoAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b070cc6b6da93765e2a367fe4d301101/263a4/smithery-connect.webp 480w,
/static/b070cc6b6da93765e2a367fe4d301101/a1057/smithery-connect.webp 840w&quot; sizes=&quot;(max-width: 840px) 100vw, 840px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b070cc6b6da93765e2a367fe4d301101/9aebd/smithery-connect.png 480w,
/static/b070cc6b6da93765e2a367fe4d301101/2d81a/smithery-connect.png 840w&quot; sizes=&quot;(max-width: 840px) 100vw, 840px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b070cc6b6da93765e2a367fe4d301101/2d81a/smithery-connect.png&quot; alt=&quot;desktop-commander MCP서버를 제공하는 Smithery 웹 페이지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
	&lt;/div&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 821px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3b8800d5daa4b5e5a7b4eb3efc3e60cb/809bd/smithery-json.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACgUlEQVR42m1TWXbbMBDTb+NN+2KR1GJtsWwnqdOmuf8h3Je7yCiGsZ2k7QfecChxCGCGTuvPYJTC0LZoqwqV1iiZf4bsqSxDGPiIwgA6XyNmVGvu+R6MVlDcqwsNZx140HGILInhLheY333DinG1mBMLu2ch69XSwvdcu+e7q1uUPY/fnL5r8XjYY9zeYz9usdsOiOQjfxJI4eVfWMxnWM4v8ZK/Yw6nbTboiF7iRlBDU0pEeSJRWMrha7H57I757L8XCZymrlAa/YV64Hn2o0i/yryu27ZBliaY0RrJ/2G434047Hd4oOzt/YCc7LTKrcmGUQyXfJ2ltkAeR0hCHzHZi+8CaYzAd104dVXaw1cGy9ttHxAWokAOPfHSl8OI5/2IyshE5JwChZprReaOsBDZIjegXDkU2Bt9m9u1eMmCEfeOjwe8PB/x8/sTdn2LgZ4Pmwo9a+Rk61znKaekNW8QvK/jWy5zJ00Sf5/HAQ/dxtrTdR0aNjMnqU1ZwvA/R6s1P/a2MXIoicIb5DKJ0YVhwoJHMvrVd3isa/QcesGQUzbznGvLUFikF4NTmn41O4mim+kz+ljR4x9NxZntMFJix0INixzJrFEklKZwqsKg5JMRn6SYILm8nCtyXhjw4oIFO0or9vfQHJ91USJUhoUVWm2QSEEWmAqtJo7LRGlTHIUTWVtQ7pSEoY2u506lu5p6bc7dw/7c7cZzO27P1dCfR2POG22mOMsmpyRDeXJd09jmCHQuMf2UsylkzoLoigKaz7RkUwybkvJ1DcagJeKMTeF7PXFkThLp1Qc810bfd08R45LfN+7y96tSb9sse9tlKZHZ9WuavrVKn+I0Pf0BIDh+emkRHuQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3b8800d5daa4b5e5a7b4eb3efc3e60cb/263a4/smithery-json.webp 480w,
/static/3b8800d5daa4b5e5a7b4eb3efc3e60cb/dfad5/smithery-json.webp 821w&quot; sizes=&quot;(max-width: 821px) 100vw, 821px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3b8800d5daa4b5e5a7b4eb3efc3e60cb/9aebd/smithery-json.png 480w,
/static/3b8800d5daa4b5e5a7b4eb3efc3e60cb/809bd/smithery-json.png 821w&quot; sizes=&quot;(max-width: 821px) 100vw, 821px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3b8800d5daa4b5e5a7b4eb3efc3e60cb/809bd/smithery-json.png&quot; alt=&quot;desktop-commander MCP서버를 제공하는 Smithery 웹 페이지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Smithery에서 제공하는 desktop-commander MCP서버&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;이렇게 가져온 JSON 값은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token property&quot;&gt;&quot;desktop-commander&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
			&lt;span class=&quot;token property&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;-y&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;@smithery/cli@latest&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;@wonderwhy-er/desktop-commander&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;--key&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;여러분의 Smithery 계정 키를 입력하세요&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;--profile&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
				&lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;여러분의 Smithery 프로필 값을 입력하세요&gt;&quot;&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
		&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 이 값을 Visual Studio Code의 Amazon Q 확장 프로그램을 이용하여 사용해보겠습니다. Visual Studio Code를 연 후 Amazon Q 확장 프로그램 인터페이스의 진입하면 우측 상단의 &lt;strong&gt;MCP 도구 아이콘(🛠️)&lt;/strong&gt;을 클릭하면 MCP 관리 인터페이스가 나타납니다. 여기서 우측 상단의 &lt;strong&gt;&apos;Add&apos;&lt;/strong&gt; 버튼을 누르고, 이어지는 추가 양식에 아래 정보를 입력해주세요.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e938ea80034fe164ce8e28b5d6017b8e/d8a90/vscode-amazonq-mcp.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABrUlEQVR42n2SzW7bMBCE9Qg5lL8iKUuyFf1YdlzLaYKkKHoqcuj7v4sBczqUkqBOih5GkJbUt7uzm+XOQ2v9LmttbG7bGLyHzHOccov74M5jc3v5GjwmY/BkNUZD1TX6EFBJgUpJtIxlWqsrIBWHYYj9bofnvsfPzQbGubN37hL48ywmGaSazw7rCqN3aBh7VCIBF5BSapYhUFAV3++khOKZVOrMs8vbHcnYnRA4VSu4eo2iLJGXFYalwgXonINlO7wcC6Xi9hWmlvMzdXm7m4COnZ1qgmiNpzXsAntrkKWMOQM1M1gGc8JaKaNWV1ZcAVMST+DDZo2SFdIOAj2BrxUa9i9yAcMK2WpMbX/w9Z/AQ+HhimKuUFMjY5nko/clXg6/MRYV/ZIJ9l9gajlQx3IFv1qhoofCWmw56UwQ2CqHl/4XJxUgWKEiVNLDv/SpwgScCEwDKdiy1hJjAqYLCXojbiCXFYp9P8RxHNG1LbbDgK7r5il/BO6KABMKes9d7ifsuVLvUzbapJVBqq7bT/H5+w8cT99wvH8A9/ITMHk4yS9Yp4Xmd1M2XHiDP9oTCws4ZocbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e938ea80034fe164ce8e28b5d6017b8e/263a4/vscode-amazonq-mcp.webp 480w,
/static/e938ea80034fe164ce8e28b5d6017b8e/fb5d2/vscode-amazonq-mcp.webp 920w&quot; sizes=&quot;(max-width: 920px) 100vw, 920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e938ea80034fe164ce8e28b5d6017b8e/9aebd/vscode-amazonq-mcp.png 480w,
/static/e938ea80034fe164ce8e28b5d6017b8e/d8a90/vscode-amazonq-mcp.png 920w&quot; sizes=&quot;(max-width: 920px) 100vw, 920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e938ea80034fe164ce8e28b5d6017b8e/d8a90/vscode-amazonq-mcp.png&quot; alt=&quot;Amazon Q에서 MCP를 설정하는 방법&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Visual Studio Code의 Amazon Q에서 MCP를 설정하는 과정&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;각 항목의 역할은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scope&lt;/strong&gt;: MCP 서버를 글로벌로 설치할지, 현재 프로젝트에만 설치할지 지정합니다.
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Global&lt;/code&gt;: 모든 프로젝트에서 사용할 MCP일 경우 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;This Workspace&lt;/code&gt;: 현재 열려있는 프로젝트에서만 사용할 MCP일 경우 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: 설치할 MCP 서버의 별칭 또는 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transport&lt;/strong&gt;: MCP 서버와 메시지를 주고받는 전송방식을 설정하는 항목입니다. 현재는 STDIO만 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;command&lt;/strong&gt;: MCP 서버를 실행할 명령어입니다. &lt;code class=&quot;language-text&quot;&gt;npx&lt;/code&gt;는 Node.js 패키지를 별도 설치 없이 바로 실행해주는 명령어입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;args&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;command&lt;/code&gt; 명령어에 전달될 인자(argument)들의 목록입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;environment&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;args&lt;/code&gt;에서 사용할 환경변수입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;timeout&lt;/strong&gt;: MCP 서버 요청 시, 응답을 기다리는 최대 시간(ms)입니다. 이 시간이 지나면 요청이 중단됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 항목에 맞춰 다음과 같이 입력해주세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scope&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;This workspace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;desktop-commander&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transport&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;STDIO&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;command&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;npx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;args&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;-y&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@smithery/cli@latest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@wonderwhy-er/desktop-commander&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;${SMITHERY_KEY}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;--profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;${SMITHERY_PROFILE}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;environment&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SMITHERY_KEY&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;여러분의 Smithery 계정 키를 입력하세요&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SMITHERY_PROFILE&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;여러분의 Smithery 프로필 값을 입력하세요&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;timeout&lt;/strong&gt;: 0&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
	&lt;div style=&quot;max-width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1800px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/773345347f46a9e106b880c68b2e40bc/28bdc/amazonq-mcp-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABfUlEQVR42m2TSXKEMAxFuUOqwQzdNDONmZKsWCR9g9z/Nko9pUQ5qSx+ydbwJb5FNA693O93KYpCbrebOOckSZIT1+tVY8MwaJwzPs6G8B5V/l38vMiyLLKuq/R9L03TKIGRYfd9l23bNI94SGJ5aZpK9PCzJpRlqc48z9WCLMu0AN80TdJ1nebhw4Ygn6+LtnWR4zi0AGKKrSMwOeZ51hwmH8dR/f8Svu6bfD6fSsRERhQSEoMQMqZEFpra12CBEg59p0nhVH8JaWS6WWMjCIEvmh4Pabv21IaiEPgh4zHquj4n5JHYgsvloqAJuZFp17btuTa8FuBsq+K9V+3IxQ8xPuppQI4ScqmqSrtjw/2y3cQyIWsDaE4dZ1YNUvsanRDBCdruhYSmIdNBBCikOfm2tychToJcKOaMDQmxNLYYpEniJI5jiR02+a0hYzMl1nShkK5GDgmEod76iO5FchdLEU6IfvaLGUINbU2whp97KuXxJc3bh/iRt6jlG4fkZrOPc6fpAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/773345347f46a9e106b880c68b2e40bc/263a4/amazonq-mcp-2.webp 480w,
/static/773345347f46a9e106b880c68b2e40bc/a6361/amazonq-mcp-2.webp 960w,
/static/773345347f46a9e106b880c68b2e40bc/0f190/amazonq-mcp-2.webp 1800w&quot; sizes=&quot;(max-width: 1800px) 100vw, 1800px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/773345347f46a9e106b880c68b2e40bc/9aebd/amazonq-mcp-2.png 480w,
/static/773345347f46a9e106b880c68b2e40bc/a91f8/amazonq-mcp-2.png 960w,
/static/773345347f46a9e106b880c68b2e40bc/28bdc/amazonq-mcp-2.png 1800w&quot; sizes=&quot;(max-width: 1800px) 100vw, 1800px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/773345347f46a9e106b880c68b2e40bc/28bdc/amazonq-mcp-2.png&quot; alt=&quot;Amazon Q에서 MCP를 설정하는 방법&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Visual Studio Code의 Amazon Q에서 MCP를 설정하는 과정&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;모든 정보를 입력한 후 &lt;strong&gt;&apos;Save&apos;&lt;/strong&gt; 버튼을 누르면 &lt;code class=&quot;language-text&quot;&gt;desktop-commander&lt;/code&gt; MCP 서버가 성공적으로 등록됩니다. 그럼 프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;.amazonq/agents/default.json&lt;/code&gt;이라는 파일이 생성되면서 아래의 내용이 추가됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// .amazonq/agents/default.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  ...
  &lt;span class=&quot;token property&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;desktop-commander&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop-commander&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;args&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;-y&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;@smithery/cli@latest&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;run&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;@wonderwhy-er/desktop-commander&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;--key&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;${SMITHERY_KEY}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;--profile&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;${SMITHERY_PROFILE}&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;environment&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;SMITHERY_KEY&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;여러분의 Smithery 계정 키를 입력하세요&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;SMITHERY_PROFILE&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;여러분의 Smithery 프로필 값을 입력하세요&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;timeout&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  ...
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 잘 작동하는지 테스트해 볼까요? 프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;README.md&lt;/code&gt; 파일을 하나 생성한 후, Amazon Q 채팅창에 다음과 같이 요청해 보세요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;프로젝트 루트에 있는 README.md 파일을 &apos;test-mv&apos; 폴더를 만들어 그 안으로 이동시켜 줘.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;요청 후, &lt;code class=&quot;language-text&quot;&gt;desktop-commander&lt;/code&gt;가 파일 이동을 실행해도 되는지 확인하는 팝업이 나타날 것입니다. &lt;strong&gt;Run&lt;/strong&gt;를 클릭하면 &lt;code class=&quot;language-text&quot;&gt;test-mv&lt;/code&gt; 폴더가 생성되고 파일이 이동하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 952px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a84885a6d8cc84ffd549db992ae1ce0c/be2fd/amazonq-prompt.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 211.45833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAqCAYAAACz+XvQAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEZklEQVR42pWXx3IjORBE+Q8yNE1SJEXvrSjKK0LeROg41/2q/dxavGplC8PlKDSHDDQaYKIqy6CZK5WKNhgMbDKZ2HA4tEqlYoVCwYrF4o/A/nq9bkdHR1Yuly1XqjWtPlhYY7iyen9qSblixb8gLJVKVq1WnSxJEsu12m3bnJ3ZaDy2ZrPpi2zCyl2W6p3GXq/nFoJ24MrBCgEjG/L5fEbGs6B38fvtw2q1muUwtd/vu47jYOV0OnUtu92uv2c+m81sNBr5HuYnJydu2fbhrVbLcoi6Xq8dZ8F1xsViYZvNxlarlTUaDRccKSQ8lvC7bcLj4+OU8PT01K1YLpdOdn5+bvP53A/A6k6n43MyAUuxTllxeHiYERIDdzklmjsZFsp1nrES4CYHsJcDIUZ3BScjLJYSdwMoDViMAyUwVzAgOjg4cAtBRJgS4AYWIjxWYKWAtYwECW1xHwsBrvMbDkkJP829uLiw19dXe3p6ssvLS7u7u7Pb21t7eHhw3N/fu84cysgBEEHKQb8RYj7CK31YYK53jKQR0ZUUQFIRWOYZIS9YIDUAFkubGNvJHFdSpiHMlAyW4Qp6YcWuuo2rZRsZoaIF9vf3HdtWxc+a7/LCCXGRBFUkEfn6+tpzDmC1cpB1cvLq6irLADKCdZoDVeUuk8CkAxtJWtxHV4IQayutKUGe9V45nAVFkdOPpFfcbb7DbxpCRFLTKXBdiYzrivh3wfhfUGRhXJM8IzIB2tX3HIWA/BcyQsgQE91UdlhIRSA279S6YlQbQZ5uxSq9FEk1JPbxJyHiUw3kI5UhIAFSqBo0ejDqgbAZ5q2AdiCsRIQKhkpK9wquybp4zKpkl8sQyF3VLAFhxGICtl3X0jpfiBAHJa7J72pWozxJQi9NiqFJlKI8ZEEtnkDEFYKLBEzwaqinY61Vs+owaDkOmk5Cch+VUw3jbgykEwH4U5NwhC+OYjlFIQm5WCx83SlYRqoAoop2aMgthqVYTzrpklKTHfTD1Tsa22a9SS/7Rj0lVB0DujUjbtPF1bkhxG0OUYrxDAgWWeLXKG6pEXAKL3UXY61+hBy6lNTuNFfL8y8HRVaIN8Y//K6O42aSizvxdmeO2/tPm0TuJ60dV8BPSDOXYzdjd/f29lxDgsB8uxdqvw7zoKgPqgT1lUWaANYgVAkKKlN97/gHJ4SkxsvLi49vb2/ZBf/x8WHv7+++Ru4BVZQ+rvgNe7iH/JNYechdQu6xwDP5yDO5yMj85ubG17SuO4hneifp54ScOp9NbflpAW6qcmRRDFWO5jzzzgkrSdE/2JN//rXFah3cew54tcfHR3t+fnYLuDr90D8AUvakFibhCugMLLn6Zf3BMGjzFQx9WfV7Het1Qz/kM7nX9Wch7u6uIaGuVso2aNWt+9lEiSobiBzjcLay/mRh3eHU2v2J9UbT8Dyx/nju5ck+3ZBf7auc3hmkA1aREoB5Y7i0Ov9jBkurDVZW682s1p36PG17pazC/gP/EKL0PjaspgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a84885a6d8cc84ffd549db992ae1ce0c/263a4/amazonq-prompt.webp 480w,
/static/a84885a6d8cc84ffd549db992ae1ce0c/493ca/amazonq-prompt.webp 952w&quot; sizes=&quot;(max-width: 952px) 100vw, 952px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a84885a6d8cc84ffd549db992ae1ce0c/9aebd/amazonq-prompt.png 480w,
/static/a84885a6d8cc84ffd549db992ae1ce0c/be2fd/amazonq-prompt.png 952w&quot; sizes=&quot;(max-width: 952px) 100vw, 952px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a84885a6d8cc84ffd549db992ae1ce0c/be2fd/amazonq-prompt.png&quot; alt=&quot;Amazon Q에서 Desktop Commander MCP 서버 실행 과정&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Amazon Q에서 Desktop Commander MCP 서버 실행 과정&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;h2 id=&quot;편의성-높이기-도구-실행-자동-승인-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8E%B8%EC%9D%98%EC%84%B1-%EB%86%92%EC%9D%B4%EA%B8%B0-%EB%8F%84%EA%B5%AC-%EC%8B%A4%ED%96%89-%EC%9E%90%EB%8F%99-%EC%8A%B9%EC%9D%B8-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;편의성 높이기 도구 실행 자동 승인 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;편의성 높이기: 도구 실행 자동 승인 설정&lt;/h2&gt;
&lt;p&gt;매번 &lt;code class=&quot;language-text&quot;&gt;Run&lt;/code&gt; 버튼을 누르는 것이 번거롭게 느껴질 수 있습니다. Cursor의 &lt;code class=&quot;language-text&quot;&gt;Auto Run&lt;/code&gt; 모드처럼, 신뢰할 수 있는 도구는 자동으로 실행되도록 설정할 수 있습니다. 다시 MCP 도구 아이콘(🛠️)을 클릭하여 MCP 도구를 관리하는 인터페이스로 이동하여 &lt;code class=&quot;language-text&quot;&gt;desktop-commander&lt;/code&gt; 항목 우측의 &lt;strong&gt;도구 아이콘(🛠️)&lt;/strong&gt;을 클릭해 도구 관리 화면으로 들어갑니다. 나타나는 수많은 도구 리스트 중에서 자동 승인하고 싶은 도구(예: &lt;code class=&quot;language-text&quot;&gt;Move file&lt;/code&gt;)를 찾아 &lt;strong&gt;Ask&lt;/strong&gt;로 되어 있는 셀렉터 박스를 &lt;strong&gt;Always Allow&lt;/strong&gt;로 변경해주면 됩니다. 설정 후, 아까와 동일한 프롬프트를 다시 입력해보세요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;test-mv 폴더에 있는 README.md 파일을 프로젝트 루트로 다시 옮겨 줘.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/96dd76f5e0a4685b838fd874102bed30/88ce8/amazonq-autorun.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABtUlEQVR42o1SW3KjMBDkEub9FCBAlgCDTZxNsve/VW+Pakl5q/YjH11CaKanZ6aDvCgQxzGyJIHNMvRth0a1iHhP0tSjrCr0ekDB2IwxcpZlBdVpfz//CQJjDMZxxDRN6Jmo6xqubWHLEgNRsFjJwL7rfEKe5/5M0wTjMEDytdZomSNvwcksCKMYCauppkbFR3O5YI4iKKWw3m5Yl8XDWuuT+773RDVFfCuMmHBCWheE/A7llHsYwjBh3zasAhLPJM2pvqey0UzoSFzWFfKCCk+S/yEhLiSfqObJcQxUMAp4V4SmupGjUCRXTcm5Fj8jdE2DxzzDcV4CQ0Url+ZIJupl1lJICv+I0DJpY7u3v21Lywe3v7LllYu5ilKSWiF8neHrHE9CmefE6o6JjbQm4L2Tdy4t4YxTQr7Fet9bPu2Q8OcracjggS3/oqqN9rrTXjO3XjAukxwWkFMgng4+vn7j/fnEvm94ezs86T8bJ6HYZn88sO077scB6xzNXuN4/8DX5yfu9x3H8aCdrgjccoO5WmjjvA1SzuZVoZCLzxxJxMTeyFxKnKTQ1xnLvNA6jrCMq/AHWjEve/Ml+o0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/96dd76f5e0a4685b838fd874102bed30/263a4/amazonq-autorun.webp 480w,
/static/96dd76f5e0a4685b838fd874102bed30/a6361/amazonq-autorun.webp 960w,
/static/96dd76f5e0a4685b838fd874102bed30/0b34d/amazonq-autorun.webp 1920w,
/static/96dd76f5e0a4685b838fd874102bed30/da28f/amazonq-autorun.webp 2880w,
/static/96dd76f5e0a4685b838fd874102bed30/f3215/amazonq-autorun.webp 3808w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/96dd76f5e0a4685b838fd874102bed30/9aebd/amazonq-autorun.png 480w,
/static/96dd76f5e0a4685b838fd874102bed30/a91f8/amazonq-autorun.png 960w,
/static/96dd76f5e0a4685b838fd874102bed30/ac7a9/amazonq-autorun.png 1920w,
/static/96dd76f5e0a4685b838fd874102bed30/f9c26/amazonq-autorun.png 2880w,
/static/96dd76f5e0a4685b838fd874102bed30/88ce8/amazonq-autorun.png 3808w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/96dd76f5e0a4685b838fd874102bed30/ac7a9/amazonq-autorun.png&quot; alt=&quot;Desktop Commander MCP 서버 Always allow 적용 과정&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Desktop Commander MCP 서버 Always allow 적용 과정&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;이번에는 확인 요청 없이 바로 파일이 이동하는 것을 확인할 수 있습니다. 짠! 한결 더 편리해졌죠?&lt;/p&gt;
&lt;h2 id=&quot;나만의-ai-비서-만들기-사용자-정의-rule-추가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%82%98%EB%A7%8C%EC%9D%98-ai-%EB%B9%84%EC%84%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EC%9D%98-rule-%EC%B6%94%EA%B0%80&quot; aria-label=&quot;나만의 ai 비서 만들기 사용자 정의 rule 추가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;나만의 AI 비서 만들기: 사용자 정의 Rule 추가&lt;/h2&gt;
&lt;p&gt;Cursor의 꽃이라 불리는 기능은 바로 &lt;strong&gt;Rule&lt;/strong&gt;입니다. 개발자가 지정한 규칙에 따라 AI가 답변의 톤, 형식, 준수사항 등을 지키게 만드는 기능이죠. Amazon Q에서도 &lt;code class=&quot;language-text&quot;&gt;.amazonq&lt;/code&gt; 폴더를 통해 이 기능을 구현할 수 있습니다. Amazon Q 채팅창 상단의 &lt;strong&gt;&apos;Rules&apos;&lt;/strong&gt; 버튼을 클릭하고, &lt;strong&gt;&apos;Create a new rule&apos;&lt;/strong&gt; 버튼을 눌러 새로운 규칙의 이름을 입력하세요. &lt;code class=&quot;language-text&quot;&gt;Create&lt;/code&gt;를 클릭하면 프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;.amazonq/rules&lt;/code&gt; 폴더가 생성되며, 그 안에 방금 만든 이름의 마크다운(&lt;code class=&quot;language-text&quot;&gt;.md&lt;/code&gt;) 파일이 열립니다. 이제 이 파일에 원하는 규칙을 자유롭게 작성하면 됩니다. 여러 개의 파일을 만들어 규칙을 세분화할 수도 있습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/59bededeaeb322e31558789e28183c2d/db999/amazonq-rule-1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAABkklEQVR42u2T3XKbQAyFeYb4h99dCAt4wabYbmzHjSdxh+b932Vf4PQIx4wzcS5634tvFiTtkXYkeaayUFrD9/2RJEmQZhkK+sIwHO1B4EMpRTTiRGE+n4+IX2I9ay3qukZRFKiqasCYAppJ2nY12sqyHM48zz8lvyKi0+kUngSKoAg3TYPFYoE4jhFF0Wi/+iTpbcW3zGazi+A95+V5wbd2qWYymXxCxAZBCfhXrs+7x3/B7/E/kO+ZNIWX7zEKyjh8EZIR+SBkhyMKxVIhR8R/eEDA7t7iE+86lBI0TjzPUCriKSIpM//k9uyMQcdN2TBBy8E3jM15TzCk5L9X2wU26zW22y1WbYslh7hfLvH0+IjdssGp+4H39Qa/aDt2Hc7kJYlRMS6haMhEEQlkGViE1zydcO7fcX57w/H4jOf9Hj1puWabU4/f/R+8vL5id9jjQHaHA9Zpim51WcuU34ImSnY51pkrq8rR6UzVuJxkpnBhFDmV5a62ln472MWvUu3qSLsis04r5VjVAHvgdBC4v7G8c2btx7JFAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/59bededeaeb322e31558789e28183c2d/263a4/amazonq-rule-1.webp 480w,
/static/59bededeaeb322e31558789e28183c2d/a6361/amazonq-rule-1.webp 960w,
/static/59bededeaeb322e31558789e28183c2d/0b34d/amazonq-rule-1.webp 1920w,
/static/59bededeaeb322e31558789e28183c2d/62665/amazonq-rule-1.webp 2856w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/59bededeaeb322e31558789e28183c2d/9aebd/amazonq-rule-1.png 480w,
/static/59bededeaeb322e31558789e28183c2d/a91f8/amazonq-rule-1.png 960w,
/static/59bededeaeb322e31558789e28183c2d/ac7a9/amazonq-rule-1.png 1920w,
/static/59bededeaeb322e31558789e28183c2d/db999/amazonq-rule-1.png 2856w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/59bededeaeb322e31558789e28183c2d/ac7a9/amazonq-rule-1.png&quot; alt=&quot;Visual Studio Code에서 Amazon Q 룰 설정&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
	&lt;/div&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/467a5ffab825064efd0f70ede7958330/7f509/amazonq-rule-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 76.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6ElEQVR42qWU2ZLSUBCGeQNLyL4vhywkgTAxIEw5TlmWF44X3vv+xVvk9++IgAxWWXjx10lOTn+9nO5MvCCE43rQdR2GYYwyTROWZSFO5wgTddr7ve/7PtIkwbKuEAYBXMcZKDi2fZgYhhy0/oCJNE1DTKOqqjCfz0dlWYY8z5FwP0oV+t0eq4cORdUMscoQpOowEWPbtk/eRQKeTqdQSqFtW5RliXKxQF3X456cbcIIX7oebZKictyh8QMsXe8w0TR9jOaWrqM+pU21noeXZolveYHOD4b3dLDzAwFqfwVeRn2tKbMqWbeQDt+YxjCzTLw1jfuBoooX6f56HnghsC3rPqApN03lUpIjUO7B+h9gzG+B1PReoBjqjGjUEWifnfw7UIwkLalRyZYp2Yspb9g/R/caOJvNbgKlPUwpNtcN12cCN2GIJwLdY9SvgGEUIYpjGDS61NjsUnymuCQ8Z4vUjK7hxOS0uarrGZipFEuOV8KZjDmjoxiBRVDFb58e1uj7Ht12i1W7xrrr0PHddd3LGp+Bj4GHPk3wwFRWBIlkClY6oypq7D6/4PnjEx73e4LfYbPpsSXc45mbwB/FV3wvP6AxZ1CWjTkPiJSkzb+QykssygJZsYCiA5UvEDFlAVxocFgSroefrweMSmr3zrkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/467a5ffab825064efd0f70ede7958330/263a4/amazonq-rule-2.webp 480w,
/static/467a5ffab825064efd0f70ede7958330/a6361/amazonq-rule-2.webp 960w,
/static/467a5ffab825064efd0f70ede7958330/0b34d/amazonq-rule-2.webp 1920w,
/static/467a5ffab825064efd0f70ede7958330/6a265/amazonq-rule-2.webp 2648w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/467a5ffab825064efd0f70ede7958330/9aebd/amazonq-rule-2.png 480w,
/static/467a5ffab825064efd0f70ede7958330/a91f8/amazonq-rule-2.png 960w,
/static/467a5ffab825064efd0f70ede7958330/ac7a9/amazonq-rule-2.png 1920w,
/static/467a5ffab825064efd0f70ede7958330/7f509/amazonq-rule-2.png 2648w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/467a5ffab825064efd0f70ede7958330/ac7a9/amazonq-rule-2.png&quot; alt=&quot;Visual Studio Code에서 Amazon Q 룰 설정&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;Visual Studio Code에서 Amazon Q 룰 설정&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;h2 id=&quot;pro-tip-ai-기억력-강화와-할루시네이션-방지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pro-tip-ai-%EA%B8%B0%EC%96%B5%EB%A0%A5-%EA%B0%95%ED%99%94%EC%99%80-%ED%95%A0%EB%A3%A8%EC%8B%9C%EB%84%A4%EC%9D%B4%EC%85%98-%EB%B0%A9%EC%A7%80&quot; aria-label=&quot;pro tip ai 기억력 강화와 할루시네이션 방지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Pro-Tip: AI 기억력 강화와 할루시네이션 방지&lt;/h2&gt;
&lt;p&gt;AI를 사용하다 보면 사실이 아닌 정보를 그럴듯하게 만들어내는 &lt;strong&gt;할루시네이션(Hallucination)&lt;/strong&gt; 현상을 겪곤 합니다. 이를 방지하고 AI가 프로젝트의 맥락을 더 잘 이해하게 만들기 위해 &lt;strong&gt;메모리 뱅크&lt;/strong&gt;라는 기법을 적용해 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;방법은 간단합니다. 특정 폴더에 작업 내용을 기록하고, AI가 답변하기 전에 항상 그 내용을 참조하도록 Rule을 만드는 것입니다. 먼저 프로젝트 루트에 &lt;code class=&quot;language-text&quot;&gt;.context-memory&lt;/code&gt;라는 폴더를 생성한 후, 앞서 만든 Rule 파일에 아래와 같은 규칙을 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;markdown&quot;&gt;&lt;pre class=&quot;language-markdown&quot;&gt;&lt;code class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 답변 전 필수 확인 절차&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;컨텍스트 확인&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: 사용자가 질문하면, 답변 전에 반드시 &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`.context-memory`&lt;/span&gt; 폴더 안의 모든 파일을 확인하여 전체 맥락을 파악한 후 답변해야 합니다. 이 절차는 AI가 부정확한 정보를 생성하는 것을 방지하기 위함입니다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 작업 메모리 관리&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;작업 내용 저장&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: 모든 작업 내용은 프로젝트 루트의 &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`.context-memory`&lt;/span&gt; 폴더에 마크다운 문서로 저장합니다.
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;메모리 파일 생성&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: 하나의 기능 단위나 논리적 변경이 완료되면, 해당 작업 내용을 요약하는 마크다운 문서를 생성합니다.
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;파일명 규칙&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: 파일명은 &lt;span class=&quot;token code-snippet code keyword&quot;&gt;`YYYY-MM-DD_HH-MM-SS_작업요약.md`&lt;/span&gt; 형식을 따릅니다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt; 메모리 파일 구조&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 작업 내용을 저장할 때는 다음 구조를 따라주세요.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 📋 작업 개요&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;작업 일시&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: YYYY-MM-DD HH:MM:SS
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token bold&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;작업 유형&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;**&lt;/span&gt;&lt;/span&gt;: [기능 개발/버그 수정/리팩토링 등]

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 🎯 작업 목표&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 이 작업의 목적과 해결하려는 문제를 설명합니다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 🔧 구현 내용&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 주요 변경사항, 추가된 기능, 수정된 로직 등을 상세히 기술합니다.

&lt;span class=&quot;token title important&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;###&lt;/span&gt; 🔗 관련 파일&lt;/span&gt;
&lt;span class=&quot;token list punctuation&quot;&gt;-&lt;/span&gt; 수정되거나 새로 생성된 파일 경로 목록을 기재합니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 규칙을 정해두면, Amazon Q는 답변하기 전 &lt;code class=&quot;language-text&quot;&gt;메모리 뱅크&lt;/code&gt;를 먼저 확인하여 훨씬 더 맥락에 맞고 정확한 답변을 제공하게 될 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리하며&lt;/h2&gt;
&lt;p&gt;지금까지 Visual Studio Code와 Amazon Q를 조합하여 Cursor처럼 강력한 AI 개발 환경을 구축하는 방법을 알아보았습니다.&lt;/p&gt;
&lt;p&gt;LLM 기술이 발전하며 수많은 모델과 서비스가 등장하고 있지만, 그 핵심은 결국 &lt;strong&gt;AI를 어떻게 잘 활용할 것인가&lt;/strong&gt;에 있습니다. 오늘 소개해 드린 MCP와 Rule을 잘 정립해두는 것만으로도 여러분의 곁에 뛰어난 주니어 개발자 한 명을 두는 것과 같은 효과를 누릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;올리브영은 개발자들이 최신 기술을 마음껏 실험하고 실전에 적용할 수 있도록, 도구 지원·샌드박스·사내 바이브 코딩 대회 등 실질적인 성장 장치에 꾸준히 투자하고 있습니다.&lt;/p&gt;
&lt;p&gt;오늘 소개한 Amazon Q 사례처럼, 우리는 스스로 기술을 검토·도입하며 동료와 함께 더 나은 개발 환경을 만들어갑니다. 변화의 흐름에 맞춰 함께 성장하고 싶다면, 올리브영에서 여러분만의 규칙과 도구로 개발 문화를 직접 빚어보세요. 아래 채용 정보를 통해 여러분을 기다리겠습니다.&lt;/p&gt;
&lt;p&gt;[올리브영 채용 페이지 바로가기] (&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/detail.fo?zz_jo_num=8315&quot;&gt;https://recruit.cj.net/recruit/ko/recruit/recruit/detail.fo?zz_jo_num=8315&lt;/a&gt;)&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영은 왜 선물하기를 개편했을까? Part - 2]]></title><description><![CDATA[안녕하세요. 올리브영 전시 영역 백엔드 개발을 담당하고 있는 robi입니다. 오래 기다리셨죠? 작년 11월 28일에 작성된 🎁 올리브영은 왜 선물하기를 개편했을까? Part - 1의 후속편이 드디어 작성되었습니다. 1부에서 말씀드렸던 대로…]]></description><link>https://oliveyoung.tech/2025-08-04/gift-renewal-2/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-08-04/gift-renewal-2/</guid><pubDate>Mon, 04 Aug 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영 전시 영역 백엔드 개발을 담당하고 있는 robi입니다.&lt;/p&gt;
&lt;p&gt;오래 기다리셨죠? 작년 11월 28일에 작성된 &lt;a href=&quot;https://oliveyoung.tech/2024-11-28/gift-renewal/&quot;&gt;🎁 올리브영은 왜 선물하기를 개편했을까? Part - 1&lt;/a&gt;의 후속편이 드디어 작성되었습니다.&lt;/p&gt;
&lt;p&gt;1부에서 말씀드렸던 대로 2부에서는 개편 과정의 세부 기술과 구현 과정을 더 자세히 다뤄볼 예정인데요.&lt;/p&gt;
&lt;p&gt;여러 주제 중, 가장 중요했던 &quot;캐싱&quot;에 관해 여러분에게 소개해 드리고자 합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;-what-is-캐싱&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-what-is-%EC%BA%90%EC%8B%B1&quot; aria-label=&quot; what is 캐싱 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔑 What is 캐싱&lt;/h2&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
   &lt;div style=&quot;width: 50%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1276px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/12d45a6a141e420ecca1b2191d1ca575/5fbd7/01-01-caching.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 98.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAADR0lEQVR42p2U608iSRTF/f8/7oddk52M62zc0clIVlZwRB7DqxERFEYR2ka05SHNo6GbBrsb+G2JoutjkpmtpKqSqsqpc+859y7xE2M2m704eFwex9LPAE4FoDud0W/VaV3KOI7z6rOlH2L18Nis1enm82i5OGMlTqOcpd/TsExj8eT7gNPpFMuy5gFNJlNux7fomRxONsu0UoRWmY4sYXcLaO0TGq3m24AL6o1Gg1KpTKvV4fK6weBUph5OchqIk/sSJROKk01JpJIxjpRjStoVU/HxG4D3oJ12F03rCnaT+3DVJlV/iqxXgIYL5BMl0odlUmcKX9ULFKM312dpodN9ru5BR6MxV1c1XNul1zVEuM78Si9cc5trU890ODnuUWxNSQxsUuJjx3aeh+wIJt2BSbV6Q+H4gvw3lcyJymYoy040g2kY2AJYq+hcljTyZzqHVZOE2qXWNx+je2A4IxQ75cN6En8kR+y0SqKikhd5rHZapKQjjqVDFm/vVtd153u33aHV1J7bpj8c8WFNwrNeYGc3zXlFRj07Jx9JkRPJv97P4d/wYxjmK4O77gRZVlAU5QmwJyivePb521/E8ymFtL2HEpOIbPjY/WubyD9hfJ5d+n1jrqRljsW8FWmw5rm7y7emaU+AurhYfhfCs5ljfS2J908/B9sBJE8Q75oHOZomthWi1xNCKMJOqomiWnwrNoW1roSIo0fmc8ChbfP7uyDvfwmxvBxgbyvATXwfOZRE8gU5i0gi9CS63uOw2SOuDQgK8XyFCunyFbMHaz2FbFr8sRJlYzXNx9UYecGotp9F2okQ9UUIe6Psfd6lo7XJDMcEh7aYDpGRTUEXaXDc54CmNeb9aojNlX08qwlOEweo6QKlRJZi9IC8qJCoPzI3erTr4JFbfD4os3VtkGkPmbn/AVwIpguWXxNFfvvVh299m7R3DzmdoXpWFg2gK2p7wkjUc1Bx2Ex3+BQo4y0Y5Gqi3icvGC5A7zx2ftEQVVDEHOgPXnu6t6wRe9KArfCIzeCQ7bjF4YnObOq+7od3Cr1qoC/ObSFeNNIk4OsR8A/54utwlNOeNdmlt5ro98Dv69ymrhpcyH1u6qbwofv/O/aPjH8B5vrTjn9s2sEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/12d45a6a141e420ecca1b2191d1ca575/263a4/01-01-caching.webp 480w,
/static/12d45a6a141e420ecca1b2191d1ca575/a6361/01-01-caching.webp 960w,
/static/12d45a6a141e420ecca1b2191d1ca575/7974c/01-01-caching.webp 1276w&quot; sizes=&quot;(max-width: 1276px) 100vw, 1276px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/12d45a6a141e420ecca1b2191d1ca575/9aebd/01-01-caching.png 480w,
/static/12d45a6a141e420ecca1b2191d1ca575/a91f8/01-01-caching.png 960w,
/static/12d45a6a141e420ecca1b2191d1ca575/5fbd7/01-01-caching.png 1276w&quot; sizes=&quot;(max-width: 1276px) 100vw, 1276px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/12d45a6a141e420ecca1b2191d1ca575/5fbd7/01-01-caching.png&quot; alt=&quot;데이터베이스와 클라이언트 사이에 있는 캐시(Cache) 저장소의 개념을 나타내는 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;캐싱(Caching)은 자주 사용하는 데이터를 빠른 저장소에 미리 저장해두어, 같은 요청이 들어올 때 더 빠르게 응답할 수 있도록 돕는 기술입니다.&lt;/p&gt;
&lt;p&gt;&apos;숨겨진 보물창고&apos;라는 &apos;Cache&apos;의 본래 의미처럼, 개발에서도 자주 쓰는 데이터를 미리 숨겨두고 꺼내 쓰면 시스템이 훨씬 민첩해질 수 있는데요.&lt;/p&gt;
&lt;p&gt;이러한 캐싱 기술은 비단 웹뿐만 아니라 하드웨어, CPU, 데이터베이스, DNS 등 다양한 분야에서 널리 활용됩니다.&lt;/p&gt;
&lt;p&gt;특히 실시간 트래픽이 많은 전시 영역에서는 DB나 외부 API 호출 같은 느린 작업의 결과를 캐시에 저장해두어, &lt;strong&gt;약간의 정합성을 포기하더라도 빠르게 응답&lt;/strong&gt;할 수 있도록 사용합니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;-올리브영의-캐싱&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%EC%BA%90%EC%8B%B1&quot; aria-label=&quot; 올리브영의 캐싱 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🫒 올리브영의 캐싱&lt;/h2&gt;
&lt;p&gt;전시 서비스에서는 캐싱을 위한 NoSQL로 주로 레디스(Redis)를 사용하며 장애 확산을 방지하고 시스템을 보호하기 위해 서킷 브레이커로 Resilience4j를 함께 사용 중인데, 이를 위해 작성된 Spring Boot에서의 Kotlin 코드는 아래와 같았습니다.&lt;br/&gt;&lt;small&gt;⚠️ 이하 모든 코드는 예시로 작성되었음을 알려드립니다.&lt;/small&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@CircuitBreaker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;commonCircuitBreaker&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    fallbackMethod &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fallbackPagedFooData&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Cacheable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    cacheManager &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;displayCacheManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;display:foo&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{#pageNumber}&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    unless &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#result == null or #result.isEmpty()&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchPagedFooData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageNumber&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;FooDisplayResponseDto&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// ➀ HttpAPI호출, DB조회, 비즈니스 로직 ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fallbackPagedFooData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;FooDisplayResponseDto&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;// ➁ fallback 로직 ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Case 1&lt;/strong&gt; : &apos;commonCircuitBreaker&apos;가 닫혀(CLOSED) 있는 경우 &apos;displayCacheManager&apos;로부터 데이터 획득 시도
&lt;ol&gt;
&lt;li&gt;레디스에서 &apos;display:foo:{#pageNumber}&apos;에 해당하는 값 획득&lt;/li&gt;
&lt;li&gt;값을 획득하지 못했다면 함수 구현체인 ➀을 수행&lt;/li&gt;
&lt;li&gt;➀의 수행 결과가 null이 아니고 &apos;isEmpty() == false&apos;라면 레디스에 저장&lt;/li&gt;
&lt;li&gt;수행 결과를 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Case 2&lt;/strong&gt; : &apos;commonCircuitBreaker&apos;가 열려(OPENED) 있거나 허용되지 않은 예외가 발생한 경우 &apos;fallbackPagedFooData&apos;함수를 수행
&lt;ol&gt;
&lt;li&gt;오류 상황을 로깅&lt;/li&gt;
&lt;li&gt;➁를 통해 DB 데이터를 조회하여 반환하거나, emptyList()를 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;위 코드로 서비스를 개발하는 데 큰 문제는 없었지만, 캐시를 많이 사용하는 선물하기 영역에서는 새로운 기능을 추가할 때마다 아래와 같은 &lt;strong&gt;단점&lt;/strong&gt;을 느꼈습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;써킷과 캐시 적용을 위한 2개의 어노테이션의 개별 사용으로 인한 불편함&lt;/li&gt;
&lt;li&gt;명확한 캐시 key값과 TTL등 설정을 빠르게 확인하기 어려움&lt;/li&gt;
&lt;li&gt;레디스의 hash타입 데이터를 사용할 수 없음 ❌&lt;/li&gt;
&lt;li&gt;@CircuitBreaker의 fallbackMethod속성을 문자열로 지정하여 IDE도움을 받을 수 없음에 의한 실수 유발 가능성 ⬆️&lt;/li&gt;
&lt;li&gt;@Cacheable의 key, unless속성에 SpEL(Spring Expression Language)사용으로 인한 실수 유발 가능성 ⬆️&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;
&lt;p&gt;따라서, 저희는 이러한 불편함을 개선해보고자 별도의 캐시 모듈을 만들었고, 이를 Spring의 AoP에 묶는 작업도 함께 진행했습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;️-개선된-캐시-모듈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EA%B0%9C%EC%84%A0%EB%90%9C-%EC%BA%90%EC%8B%9C-%EB%AA%A8%EB%93%88&quot; aria-label=&quot;️ 개선된 캐시 모듈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🛠️ 개선된 캐시 모듈&lt;/h2&gt;
&lt;p&gt;설계에 앞서 저희는 캐시를 가장 효율적으로 활용할 방안을 고민했으며, 다음과 같은 요구사항을 바탕으로 논의를 시작했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;레디스 연동: 캐시는 레디스에 적재하고, 필요할 때 불러올 수 있어야 합니다.
&lt;ul&gt;
&lt;li&gt;예시: 선물하기 서비스의 전시 데이터 적재 구조
&lt;div style=&quot;display: flex;&quot;&gt;
    &lt;div style=&quot;width: 75%;&quot;&gt;
       &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1296px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3c98131e8c0294096070be4d44933bff/ca756/03-01-redis-key.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 14.791666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAUElEQVR42mMwd/L4b+vp91/fyuG/soH5f1UjC7KwiqH5f20Lu/8M+jaO/10DQv8b27v+1zSzochALXPb/wwgjpWbz39H38D/Zk7uYAlKDAUAJ75fU1rkreEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3c98131e8c0294096070be4d44933bff/263a4/03-01-redis-key.webp 480w,
/static/3c98131e8c0294096070be4d44933bff/a6361/03-01-redis-key.webp 960w,
/static/3c98131e8c0294096070be4d44933bff/a9a75/03-01-redis-key.webp 1296w&quot; sizes=&quot;(max-width: 1296px) 100vw, 1296px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3c98131e8c0294096070be4d44933bff/9aebd/03-01-redis-key.png 480w,
/static/3c98131e8c0294096070be4d44933bff/a91f8/03-01-redis-key.png 960w,
/static/3c98131e8c0294096070be4d44933bff/ca756/03-01-redis-key.png 1296w&quot; sizes=&quot;(max-width: 1296px) 100vw, 1296px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3c98131e8c0294096070be4d44933bff/ca756/03-01-redis-key.png&quot; alt=&quot;레디스(Redis)에 저장된 캐시 키의 구조 예시. &apos;display:foo&apos; 와 같은 접두사와 동적 파라미터로 구성됨&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
 &lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동적 캐시 키: 캐시 키는 문자열로 설정하며, 함수의 매개변수(parameter)를 키 값에 동적으로 포함할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;TTL 설정: 캐시의 유효 시간(TTL, Time-To-Live)을 설정할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;일자별 버전 설정: 일자별 캐시 버전을 키 값을 통해 표현할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;다양한 자료구조 지원: 레디스의 string(value) 및 hash타입을 모두 지원해야 합니다.&lt;/li&gt;
&lt;li&gt;실패 처리 옵션: 캐시 조회에 실패했을 때, 예외(Exception)를 발생시키거나 대체 로직(Fallback)을 수행하는 것 중 선택할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;Fallback 결과 처리: 대체 로직(Fallback)이 수행된 후, 그 결과를 캐시에 다시 적재할지 여부를 선택할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;기존 방식의 단점 개선: 앞서 언급했던 캐시 활용 방식이 가진 문제점들을 개선해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;
&lt;p&gt;위 요구사항들을 바탕으로 논의한 끝에, 저희는 아래와 같은 구조로 모듈을 호출하는 @DisplayCaching 어노테이션을 제공하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;❗️1부에서 언급됐던 @GiftCaching, @GiftCachingKey가 각각 @DisplayCaching, @DisplayCachingKey로 이름이 변경되었습니다.&lt;/small&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// @CircuitBreaker + @Cacheable 어노테이션의 기능을 대체하는 @DisplayCaching&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@DisplayCaching&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; ··· ①
   displayCacheInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DisplayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;FOO_PAGED&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ··· ②
   dateTimeKeySuffix &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DateTimeKeySuffixType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LOCAL_DATE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ··· ③
   throwWhenReadFail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ··· ④
   putDataAfterProceed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; ··· ⑤
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchPagedFooData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token annotation builtin&quot;&gt;@DisplayCachingKey&lt;/span&gt; pageNumber&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long ··· ⑥
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;FooDisplayResponseDto&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// HttpAPI호출, DB조회, 비즈니스 로직&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;① @DisplayCaching : 캐싱을 적용할 함수 위에 어노테이션을 선언하고, 적용할 캐시 속성을 지정합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;details&gt;&lt;summary&gt;DisplayCaching.kt (클릭하여 펼치기)&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;AnnotationTarget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;FUNCTION&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;AnnotationRetention&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;RUNTIME&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCaching&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; displayCacheInfo&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DisplayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dateTimeKeySuffix&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DateTimeKeySuffixType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DateTimeKeySuffixType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NOT_USE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; throwWhenReadFail&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; putDataAfterProceed&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/details&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;② displayCacheInfo : 열거형 상수를 지정합니다. 여기에는 캐시명, TTL, 캐시 타입이 정의되어 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;details&gt;&lt;summary&gt;DisplayCacheInfo.kt&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCacheInfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; key&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; ttl&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;VALUE
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token function&quot;&gt;FOO_PAGED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;display:foo&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 레디스 키값으로 사용&lt;/span&gt;
      ttl &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;900L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 15분&lt;/span&gt;
      redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;VALUE
   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;/details&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;③ dateTimeKeySuffix : 열거형 상수를 지정합니다. 값에 따라 Key의 마지막에 붙을 접미사(suffix)를 현재 시간을 기준으로 생성합니다. (-yyyyMMdd)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;details&gt;&lt;summary&gt;DateTimeKeySuffixType.kt&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; DateTimeKeySuffix &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSuffixString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentTime&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; DateTimeKeySuffixType &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DateTimeSuffix &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

   NOT_USE &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSuffixString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentTime&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

   LOCAL_DATE &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSuffixString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentTime&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
            String&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;%04d%02d%02d&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               currentTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;year&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; currentTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;month&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; currentTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dayOfMonth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// yyyyMMdd&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/details&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;④ throwWhenReadFail : 값이 true인 경우 캐시 읽기 작업이 실패하면 함수 구현부(fallback)을 수행하지 않고 예외를 던집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;⑤ putDataAfterProceed : 값이 true인 경우 fallback을 수행한 결과를 캐시로 저장합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;⑥ @DisplayCachingKey : 캐시 키 값에 사용할 파라미터 앞에 어노테이션을 선언하면 키를 생성할 때 해당 파라미터를 활용합니다. 또한, forHashKey = true로 설정한다면 해당 파라미터는 레디스의 hash타입 데이터의 해시 키 값으로 사용할 수 있습니다.&lt;br&gt;(위 예제의 경우 pageNumber에 1L이 전달된 경우 최종적으로 생성되는 키는 &quot;display:foo:1&quot;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;details&gt;&lt;summary&gt;DisplayCachingKey.kt&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;AnnotationTarget&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;VALUE_PARAMETER&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Retention&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;AnnotationRetention&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;RUNTIME&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCachingKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; forHashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// true인 경우 RedisDataType.HASH의 HashKey로 사용&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;companion&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertParamValueToKeyString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; value
            &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; LocalDateTime &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
               &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;%04d%02d%02d%02d%02d%02d&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;year&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;month&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dayOfMonth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hour&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;minute&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;second
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; LocalDate &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;%04d%02d%02d&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;year&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;month&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dayOfMonth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/details&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;그리고, 어노테이션 속성값에 따라 캐시 모듈이 어떻게 분기 처리되는지를 Excalidraw로 도식화했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
   &lt;div style=&quot;width: 100%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/28a3b23a67d8cad0806ed2c73919df15/d5be6/03-02-cache-module-blueprint.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.20833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB2UlEQVR42oVTi27bMAzM///kgLbrti6OrTdJPa4nxUWyAcMMMLEs8nR3pC74zzPGQO0V0gTKsG7oo6+9Ntraf34uLMGomZEwWLhAuO4WWaEEMWQ1fHc3vBxXRL6Xqgso14I6381WLMDBop5+oftXdA3oBOh5rl8ArmUCWsX7XvC2ZSSpSKaTBvr8VQV6vwPykAupQEqGdweiP+D2K9rxDSO+L8CYI0LKyCkh5wQlgGd+a7RBaQO/L1UPQIVxEWNEM0FhURPHz5SdPeUWxixWpJxRRBGFUglo1ZhbTpvqCTjoSYr48foGkzuLSplWG2r0BAlIh4febhBGcR6O+TYZErDykMmui5yA9MHIIh47QIPVO4xAhoFNoQVC0ELJCPSXOZNJosxORZ2sOwHbttFz/wVInNaQii7qSn8wu93HOkCsUC79Yc5k0sg8LlbKd3aXPv4p+QSM5b5hTFR6OT0TyRwTQZDGURn0rqPYwJYEnt/2XOn96aE9ATYCupihtbOo4hYEG2OPCseDrlFwJMWHLwhU4siw1ukhG1PKo8scn8vXbRB2TylNuTGT13jQlxt9+/n7g/8cK9oR2LQyu1xtKZldnrP41JR/P/OKDXo55uD+dcWe7uYjAHwCjtisS05iSHgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/28a3b23a67d8cad0806ed2c73919df15/263a4/03-02-cache-module-blueprint.webp 480w,
/static/28a3b23a67d8cad0806ed2c73919df15/a6361/03-02-cache-module-blueprint.webp 960w,
/static/28a3b23a67d8cad0806ed2c73919df15/0b34d/03-02-cache-module-blueprint.webp 1920w,
/static/28a3b23a67d8cad0806ed2c73919df15/93c5e/03-02-cache-module-blueprint.webp 2242w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/28a3b23a67d8cad0806ed2c73919df15/9aebd/03-02-cache-module-blueprint.png 480w,
/static/28a3b23a67d8cad0806ed2c73919df15/a91f8/03-02-cache-module-blueprint.png 960w,
/static/28a3b23a67d8cad0806ed2c73919df15/ac7a9/03-02-cache-module-blueprint.png 1920w,
/static/28a3b23a67d8cad0806ed2c73919df15/d5be6/03-02-cache-module-blueprint.png 2242w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/28a3b23a67d8cad0806ed2c73919df15/ac7a9/03-02-cache-module-blueprint.png&quot; alt=&quot;새로 개발한 캐시 모듈의 로직 흐름도. 캐시 조회, 실패 시 분산 락 시도, 폴백 함수 실행, 결과 캐싱 순서로 동작&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;이러한 설계를 바탕으로 DisplayCacheModule을 구현했으며, 그 구조는 다음과 같습니다.&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;DisplayCacheModule.kt&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCacheModule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; circuitBreaker&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CircuitBreaker&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; redisTemplate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisTemplate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; objectMapper&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ObjectMapper
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; logger &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LoggerFactory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getLogger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCacheModule&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;companion&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; SAFE_NULL_OBJECT &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;/**
       * 파라미터 기반의 캐시 키를 생성
       */&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;makeDefaultCacheKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         baseCacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         paramValuesForKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;emptyList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         dateTimeSuffixType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DateTimeKeySuffixType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DateTimeKeySuffixType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NOT_USE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token comment&quot;&gt;// paramValuesForKey 처리&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; parameterCacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; paramValuesForKey&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;joinToString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;separator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; it &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isNotBlank&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;:&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;it&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;:xyz&quot; or &quot;&quot;&lt;/span&gt;

         &lt;span class=&quot;token comment&quot;&gt;// dateTimeKeySuffix 처리&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dateTimeKeySuffix &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dateTimeSuffixType &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; DateTimeKeySuffixType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NOT_USE&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            dateTimeSuffixType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSuffixString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isNotBlank&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;-&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;it&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;-yyyyMMdd&quot; or &quot;&quot;&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt;

         &lt;span class=&quot;token comment&quot;&gt;// 최종적으로 base, parameterCacheKey, dateTimeKeySuffix를 조립하여 반환&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; baseCacheKey &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; parameterCacheKey &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; dateTimeKeySuffix
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;/**
    * 캐시를 읽어옵니다. 캐시를 획득하지 못한 경우 함수 본체를 수행한 결과를 캐싱 후 반환합니다.
    * @param redisDataType 레디스 데이터 타입
    * @param cacheKey 캐시 키
    * @param hashKey 해시 키 ([RedisDataType.HASH]의 경우 사용)
    * @param ttlBySeconds 데이터의 TTL(초)
    * @param putDataAfterProceed fallback로직 수행 후 데이터 적재 수행 여부
    * @param throwWhenReadFail 읽기작업 실패 시 예외 던지기 여부
    * @param fallbackFunction 읽기작업 실패 시 수행할 fallback 함수
    * @param fallbackReturnType fallback함수의 반환 자료형
    */&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAndPut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      hashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      ttlBySeconds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      putDataAfterProceed&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      throwWhenReadFail&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      fallbackFunction&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      fallbackReturnType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Class&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 2.레디스로부터 데이터 읽기 시도&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fetchResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryReadFromRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;token comment&quot;&gt;// 3-3.throwWhenReadFail가 true면 데이터 읽기 실패 예외를 던지고 종료&lt;/span&gt;
         throwWhenReadFail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; throwWhenReadFail&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         returnType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackReturnType
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 3-1.읽기 성공 시 데이터 반환&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fetchResult &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; SAFE_NULL_OBJECT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fetchResult
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 3-4.데이터 획득을 위한 fallback함수 수행&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fallbackResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fallbackFunction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 4-2.fallback함수 수행 결과를 반환&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;putDataAfterProceed &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;isNullOrEmptyData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fallbackResult&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fallbackResult
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 4-1.레디스에 데이터 적재 시도&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;tryWriteIntoRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         ttlBySeconds &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ttlBySeconds&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         writeData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackResult&lt;span class=&quot;token operator&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// isNullOrEmptyData(..)에서 null 검증을 수행&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 7.적재 후 fallback함수 수행 결과를 반환&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fallbackResult
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;/**
    * 빈 데이터인지 검증합니다.
    */&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;isNullOrEmptyData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; Collection&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; Map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isBlank&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;/**
    * Resilience4j적용, 레디스에서 데이터를 읽어옵니다.
    * 데이터 읽기에 실패하거나 데이터가 없는 경우(null) [SAFE_NULL_OBJECT]를 반환합니다.
    */&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryReadFromRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      hashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      throwWhenReadFail&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      returnType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Class&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 생략 ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;/**
    * Resilience4j적용, 레디스에 데이터를 쓰고 예외 발생 시 핸들링을 수행합니다.
    */&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryWriteIntoRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      hashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      ttlBySeconds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      writeData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any
   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* 생략 ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;small&gt;캐시 우선 전략을 구현한 범용적인 데이터 조회 모듈로, 주어진 키로 Redis 캐시에서 데이터를 조회하고, 캐시에 없거나 읽기 실패 시 fallbackFunction을 실행해 데이터를 가져온 후 캐시에 저장&lt;/small&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;그리고 Spring AoP를 활용하여 함수 실행 시 캐시 모듈을 호출하는 DisplayCachingAspect는 아래와 같이 작성되었습니다.&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;DisplayCachingAspect.kt&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Aspect&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Ordered&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LOWEST_PRECEDENCE&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCachingAspect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; circuitBreaker&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CircuitBreaker&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; redisTemplate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisTemplate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; objectMapper&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ObjectMapper&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;/**
    * joinPoint와 method로부터 @DisplayCachingKey가 붙은 모든 매개변수들을 추출하여 키 값을 생성합니다.
    */&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;makeCacheKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProceedingJoinPoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; displayCaching&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DisplayCaching&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Method&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// @DisplayCachingParamKey가 있는 파라미터 획득&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; annotatedParamAndValues &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; method&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parameters
         &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Pair(first: Parameter, second: Actual value)&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;param&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
            param&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isAnnotationPresent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;param&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAnnotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;forHashKey
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 파라미터 접두사 및 값 변환 일괄 처리&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; prefixProcessedParamValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; annotatedParamAndValues&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; param &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;first
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; param&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAnnotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; prefix &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prefix
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; convertedValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DisplayCachingKey&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;convertParamValueToKeyString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;second&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         prefix &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; convertedValue
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 캐시 키 생성&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; DisplayCacheModule&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDefaultCacheKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         baseCacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; displayCaching&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;displayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         paramValuesForKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prefixProcessedParamValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         dateTimeSuffixType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; displayCaching&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dateTimeKeySuffix
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;/**
    * joinPoint와 method로부터 @DisplayCachingKey(forHash = true)인 매개변수를 추출하여
    * 레디스 해시에 사용할 키 값을 생성합니다.
    */&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readHashKeyOrThrow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      joinPoint&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProceedingJoinPoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      method&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Method&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String
   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token comment&quot;&gt;// @DisplayCachingParamKey가 있는 파라미터중 forHashKey = true인 파라미터 획득&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; hashKeyAnnotatedParam &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; method&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parameters
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;first&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;param&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
               param&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isAnnotationPresent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
               param&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAnnotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;forHashKey
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; annotatedValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKeyAnnotatedParam&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;second
               &lt;span class=&quot;token operator&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IllegalArgumentException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
               &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&apos;@&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;simpleName&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.forHashKey = true&apos; parameter value should not be null.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

         &lt;span class=&quot;token comment&quot;&gt;// 해시 키 생성 및 반환&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DisplayCachingKey&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;convertParamValueToKeyString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;annotatedValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hashKey&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isBlank&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IllegalArgumentException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
               &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&apos;@&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;simpleName&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.forHashKey = true&apos; parameter value should not be blank.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; hashKey
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; exceptionMessage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Fail to proper &apos;@&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;DisplayCachingKey&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;simpleName&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.forHashKey=true&apos; &quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
            &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;annotated parameter or value from the &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;. Please check annotation or actual value.&quot;&lt;/span&gt;&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IllegalStateException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exceptionMessage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;/**
    * [@DisplayCaching]어노테이션의 around로 동작합니다.
    */&lt;/span&gt;
   &lt;span class=&quot;token annotation builtin&quot;&gt;@Around&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@annotation(display.module.cache.annotation.DisplayCaching)&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token annotation builtin&quot;&gt;@Throws&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Throwable&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;aroundDisplayCaching&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ProceedingJoinPoint&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 캐시 모듈을 호출하기 위한 전처리: 리플랙션을 통한 fallback함수 속성, 캐시 키 생성, 어노테이션 속성 추출 등 수행&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; method &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;signature &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; MethodSignature&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;method
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; method&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAnnotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;DisplayCaching&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;java&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;makeCacheKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; method&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;displayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;redisDataType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;HASH &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;readHashKeyOrThrow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; method&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// hashKey 는 RedisDataType.HASH일때만 사용&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 캐시 모듈 호출&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCacheModule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         circuitBreaker &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; circuitBreaker&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         redisTemplate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisTemplate&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         objectMapper &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; objectMapper
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchAndPut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;displayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         ttlBySeconds &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;displayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ttl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         putDataAfterProceed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;putDataAfterProceed&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         throwWhenReadFail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;throwWhenReadFail&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         fallbackFunction &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; joinPoint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;proceed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;joinPoint&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         fallbackReturnType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; method&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;returnType
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;small&gt;@DisplayCaching 어노테이션이 붙은 메서드 호출에 DisplayCachingModule의 캐싱 로직을 적용하는 AoP(관점 지향 프로그래밍)의 Aspect 구현체&lt;/small&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;p&gt;최종적으로는 아래와 같은 모습으로 패키지가 구성되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;📂 display
├── 📂 aop
│   └── 📄 DisplayCachingAspect.kt ① Spring AoP 기능을 활용하기 위한 클래스
├── 📂 config
│   └── 📄 DisplayCacheInfo.kt ② 전시영역에서 사용하는 캐시 정보를 담은 열거형(enum) 클래스
└── 📂 module
    └── 📂 cache
        ├── 📂 annotation
        │   ├── 📄 DisplayCaching.kt ③ @DisplayCaching 어노테이션 클래스
        │   └── 📄 DisplayCachingKey.kt ④ @DisplayCachingKey 어노테이션 클래스
        ├── 📄 DateTimeKeySuffixType.kt ⑤ 일자 접미사로 키 버전을 구현하기 위한 정보를 담은 열거형 클래스
        ├── 📄 DisplayCacheModule.kt ⑥ 실질적인 캐시 모듈의 기능을 담당하는 클래스
        └── 📄 RedisDataType.kt ⑦ 지원하는 레디스 데이터 종류를 표현하기 위한 열거형 클래스&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;자체적으로 구현한 커스텀 캐시 모듈 덕분에, 앞서 언급했던 다섯 가지 문제점을 모두 해결할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 프로젝트가 성공적으로 마무리될 무렵, 저희는 예상치 못한 큰 난관에 봉착했습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;️-숨은-문제점-캐시-스탬피드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%88%A8%EC%9D%80-%EB%AC%B8%EC%A0%9C%EC%A0%90-%EC%BA%90%EC%8B%9C-%EC%8A%A4%ED%83%AC%ED%94%BC%EB%93%9C&quot; aria-label=&quot;️ 숨은 문제점 캐시 스탬피드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⚡️ 숨은 문제점: 캐시 스탬피드&lt;/h2&gt;
&lt;p&gt;캐시 모듈에 숨어있던 문제는 서비스 출시를 앞두고 진행한 부하 테스트 과정에서 발견되었습니다.&lt;/p&gt;
&lt;p&gt;특정 캐시가 만료되고 재생성되는 과정에서 서비스 전반의 성능이 저하되는 현상이 발생한 것인데요.&lt;/p&gt;
&lt;p&gt;면밀한 디버깅 끝에 저희는 그 원인이 바로 &lt;strong&gt;캐시 스탬피드(Cache Stampede)&lt;/strong&gt; 현상임을 파악했습니다.&lt;/p&gt;
&lt;p&gt;📒 &lt;strong&gt;캐시 스탬피드&lt;/strong&gt; : 캐시 스탬피드란 캐시 데이터의 유효 기간(TTL)이 만료되는 순간, 수많은 동시 요청이 캐시에서 데이터를 찾지 못하고 한꺼번에 백엔드 데이터베이스(DB)로 몰려들어 과도한 부하를 일으키는 현상을 말합니다.&lt;/p&gt;
&lt;p&gt;아래 그림처럼, 평소에는 대부분의 트래픽이 Redis와 같은 NoSQL 캐시에서 처리되어 DB는 안정적인 상태를 유지합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;일반 케이스&lt;/strong&gt; : 트래픽이 NoSQL(Redis)로 몰려 DB 부하 없음&lt;br/&gt;
&lt;div style=&quot;display: flex;&quot;&gt;&lt;div style=&quot;width: 50%;&quot;&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 890px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8ccb07b1a061b5f79f62881fdfc5269c/f3cf6/04-01-normal-case.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 67.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAACn0lEQVR42pWT7U9SURzH+X9a60Vvmjnn6m2tl71gudx81XowzYepKZSJVgxSppSCgQEXFUwT28CHmeVMfMCllZUPXEBRiBRQEe69387lYuMuW/O3/XbvuTvncz/n/H5HYhlzwz4xD2rUDdPwNAzOKbT2j+PTagB8sByHk4Sk74MHjo9L6B6fS8M6hiah6h3B/A/fsUCOjBkuBQ7H/0iitrmgee2EdnAUbW9G0DYwDJXdBc+qXwCy3B8QH5FEGMp3cvh21zLfWTGwkTKhWivD3SdVKFPVQN6pgEJnhmeRFhmyZCHHcAj92oZpsh2BbRpsktiyjBiooMyoflGP8mY5KjQPUadvQqPBipmpFSQCMUQDu+mJ3ya+4JW0HZZretgKTbAWGmC8+hxjLU7BNLMTicIiAO89k6G8RY5aXSMBUpiZXkEytI+fyyH4x9fguGOD9YIObwtsGCrogbOoD1RuB+zXTQKQ4Y62bEYND1TXpS2PDOcWvEgRYGhpC4c7CcxTbhhyNOi5bITlog69V7rQldsGR6ldbNhktaJWryRQJe6TlL1Uo8nYi1n3CrhYEkxKOHQmxWDj+zY+uxawODCFpaFZeD0+HMQPxGf4yGxCZesDlKrqiKUMVdoGNHRS8CxnVZnjiwJsTPZjc0CJoKMZwUEV/C49Enuxo34SgB2O9zC6SEPz6ZxOv2sdE1hczzQ2KxgmSDG/NUjhvXEa6yU5oG+fxdfi89jdpEVtJYntJxDPSn4c3TvAYYoRTTwk3IC6COHiM9jpLEOk/hL8FXmIh4Jiw//epczEBAHSj6UI3zqFiFKKSHU+6JJziG4FxIb881+ZDeV9fd1PQVfmw1uWC7o8D+uam6Qo0RMaZgVDzjMeCSG+5UMsHEQymfzrLv8GNGnPApwtqIoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8ccb07b1a061b5f79f62881fdfc5269c/263a4/04-01-normal-case.webp 480w,
/static/8ccb07b1a061b5f79f62881fdfc5269c/ffcc1/04-01-normal-case.webp 890w&quot; sizes=&quot;(max-width: 890px) 100vw, 890px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8ccb07b1a061b5f79f62881fdfc5269c/9aebd/04-01-normal-case.png 480w,
/static/8ccb07b1a061b5f79f62881fdfc5269c/f3cf6/04-01-normal-case.png 890w&quot; sizes=&quot;(max-width: 890px) 100vw, 890px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8ccb07b1a061b5f79f62881fdfc5269c/f3cf6/04-01-normal-case.png&quot; alt=&quot;정상적인 캐시 동작 다이어그램. 여러 클라이언트의 요청이 캐시에서 바로 처리되어 데이터베이스 부하가 없음&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 캐시 스탬피드가 발생하면 상황은 달라집니다. 캐시 데이터가 만료되는 짧은 순간, 마치 댐이 터지듯 엄청난 수의 요청이 캐시를 뚫고 DB로 쏟아져 들어옵니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;캐시 스탬피드 발생 케이스&lt;/strong&gt; : 캐시 데이터의 TTL 만료 순간, 수많은 요청이 동시에 DB로 폭주&lt;br/&gt;
&lt;div style=&quot;display: flex;&quot;&gt;&lt;div style=&quot;width: 50%;&quot;&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 890px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0335e5bed70fa88034d51849ce12303b/f3cf6/04-02-bad-case.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 67.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC5ElEQVR42o2T20+ScRjH+W9a3dRVXbS2uqhu6q7LWq01zZvOVsuSsrKTCdnJBARnISrZHFlZaY0yLc1ASLFCCCEK8sDhRUB4P/04NHMd1nd73ufdu2ef9/t7fs+jaLcOc6/PhlnktudvMfYMonvQh8v3lZyyssyvymQyyFmZbCbLn6To7LNzf8CZhxm6B9B09aEy9zAyEVgElIs5EAhgd9hxT7jJZn+HKq50POV651NudT3jpqWXeksP6nu9OL1fCsDsYmByfo7ujx1MRjzFHy6GKs6ZmjnRcJoDNcc5fEVJleECNdomHK5fHMqFnBGOJCnOgPsZwSk/ckb+HVjdcpvjmmoOX63i6PXTVOovodZqcVpHmAtKSOF4vnDcOopxp572kmY6Slowl96hZUcj1oaewgmKJxFA4VBzlkPqkwJ6ikrdBVSNBgF8TdIX5rsrTPCVD0tZO+3rDTzYaqZrWxuPSzsxrdZwd/udArB4SQWHDWcprzvFkWtVVDZepFZn4P3LQeZGx/hm85MIJ3iteYF+ZR2tG3UY12owbzZgWHkVy57WxQ7Pm0zC1WWO1ddQUX8JpV5FbZMJ+/NhUh/HSU2O5zpJMpEk4Agy9mSc4Y4hHF0OPg/5iU1LghYTJdGiQ2MzFTerOFRbSblamXdbq9UzZnNCPERqwkba68wXh8b6mXp5m/ibViL9Rr4OdZNOiMubKYG0vQCst7xA96gf7cNX+ci937BYee/2QipOOuQj/a6buGuQD6oyPpUt51P5Otz7VjF6cAMR1w2IKpGlNuFyHsVMTGImlmA2vhDT4lsyPb8wfymJmK2XyZObCO1bTki1nXDFWvxH1hHzdArgRUi05sdLwb+Un8Fcs2VS4ulX72Jq/woiuv3MVm/GV76GaNAlgHuFw8L4KHIO/hYL4CwZkSablHjLluEpXYJ391I8Z7YgzUZy3YV5bwHI/+jn2oktiXpGiXwYJuoeITEdKhYsbMsPtXjGDGsR4tcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0335e5bed70fa88034d51849ce12303b/263a4/04-02-bad-case.webp 480w,
/static/0335e5bed70fa88034d51849ce12303b/ffcc1/04-02-bad-case.webp 890w&quot; sizes=&quot;(max-width: 890px) 100vw, 890px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0335e5bed70fa88034d51849ce12303b/9aebd/04-02-bad-case.png 480w,
/static/0335e5bed70fa88034d51849ce12303b/f3cf6/04-02-bad-case.png 890w&quot; sizes=&quot;(max-width: 890px) 100vw, 890px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0335e5bed70fa88034d51849ce12303b/f3cf6/04-02-bad-case.png&quot; alt=&quot;캐시 스탬피드 발생 다이어그램. 캐시 만료 시점에 여러 클라이언트의 요청이 동시에 데이터베이스로 몰려 부하를 일으키는 상황&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 문제는 특히 선물하기 기능의 랭킹 데이터 조회와 같이 무거운 쿼리를 수행하는 곳에서 치명적이었습니다.&lt;/p&gt;
&lt;p&gt;평소에도 DB에서 데이터를 가져오는 데 시간이 오래 걸리던 쿼리였기에, 캐시 만료 시점에 요청이 급증하자 서비스 전체의 응답 속도가 현저히 느려지고 장애 발생 위험까지 높아지는 상황에 직면했습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display: flex; justify-content: space-between;&quot;&gt;
   &lt;div style=&quot;width: 100%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1476px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/335584ff7f56e3efee33d2daeaa383cb/8d257/04-00-cache-stampede.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 38.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABiklEQVR42j1S227UMBDd//8kEKKPSBWFF6Cqut12E8eO73aSDXs9nHGBh1GSsefcJpuYMi63G34fVrw9bjEdDrhcrzieTtj3Ci4EnC+XVtJv7+czbgByztj/+IXiHHptsHB2E3yCciP8Swd79x0mRqhBo9QJZrQYrYM2I0JM8DHActg6jzrPuB2PyH2PlXeKCCPhJvPFUIXfKZi7bw3QGItSahu03hPUNkApIXE+oEzvgJHkC0lySjgLoCjs7Qjz9Ar18R47PeD1rYMPEaNY4YBYV7SkCSZnvRpIFAACBqVwEECSNYUxZFp2UI87qA/32NJC16mmRpQorQmg0fVDUycEUqMAno7wXYeZ/f+AtUwItTDDHuPnB2jat7xQJ/ZpQ5R66dG+LLBlyZ5kLArTMGBlLDX/zTDwUHsG/byH+fSVai0GKpimf0ux7SlZvoNHOFkKMxRAT/stQ/Yb4FRnJA5X5eC//ERe+E1lC3+jmAqVVKRSMC8LSea2rMreSrArqz5vsTKWSsUC+AeO7Vt0bHctmQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/335584ff7f56e3efee33d2daeaa383cb/263a4/04-00-cache-stampede.webp 480w,
/static/335584ff7f56e3efee33d2daeaa383cb/a6361/04-00-cache-stampede.webp 960w,
/static/335584ff7f56e3efee33d2daeaa383cb/b87ad/04-00-cache-stampede.webp 1476w&quot; sizes=&quot;(max-width: 1476px) 100vw, 1476px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/335584ff7f56e3efee33d2daeaa383cb/9aebd/04-00-cache-stampede.png 480w,
/static/335584ff7f56e3efee33d2daeaa383cb/a91f8/04-00-cache-stampede.png 960w,
/static/335584ff7f56e3efee33d2daeaa383cb/8d257/04-00-cache-stampede.png 1476w&quot; sizes=&quot;(max-width: 1476px) 100vw, 1476px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/335584ff7f56e3efee33d2daeaa383cb/8d257/04-00-cache-stampede.png&quot; alt=&quot;캐시 스탬피드 발생 시 API 응답 시간 그래프. 특정 시점에 여러 API의 응답 시간이 급격히 느려지는 현상을 보여줌&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;약 3초면 수행되는 API가 캐시 스탬피드가 발생하여 전체적으로 지연되는 상황 발생 😨&lt;/figcaption&gt;
&lt;br/&gt;
&lt;p&gt;이러한 캐시 스탬피드(Cache Stampede) 문제를 해결하기 위해 저희는 여러 대안을 검토했고, 다음과 같은 세 가지 접근법을 찾을 수 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;분산 락 (Distributed Lock)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;캐시가 만료되었을 때, 락(Lock)을 획득한 하나의 요청만 캐시를 재생성하도록 허용하고 락을 얻지 못한 나머지 요청들은 캐시가 재생성될 때까지 기다림&lt;/li&gt;
&lt;li&gt;👍 : DB를 확실하게 보호 가능&lt;/li&gt;
&lt;li&gt;👎 : 락을 획득하지 못한 요청들의 지연 발생 및 락 관리의 복잡함 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;확률적 조기 갱신 (PER: Probabilistic Early Recomputation)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;캐시의 TTL이 만료되기 전 확률적으로 fallback을 호출하여 캐시 갱신을 수행, TTL이 만료에 가까워질수록 fallback 수행 확률을 높임&lt;/li&gt;
&lt;li&gt;👍 : 비동기 처리를 활용하면 별도의 지연시간 발생 없음&lt;/li&gt;
&lt;li&gt;👎 : 낮은 확률이지만 DB에 여러번의 요청이 발생할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;백그라운드 업데이트&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;배치 등을 활용하여 데이터를 주기적으로 미리 생성&lt;/li&gt;
&lt;li&gt;👍 : DB를 확실하게 보호 가능&lt;/li&gt;
&lt;li&gt;👎 : 별도의 배치 서비스 개발 및 운영의 필요성과 실시간 데이터 제공에 제약 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;프로젝트 마감 기간이 얼마 남지 않아있던 저희는 남아있던 개발 기간과 위험성을 고려하여 &lt;strong&gt;분산 락&lt;/strong&gt;을 이용한 방식으로 문제를 해결하기로 결정하여 설계를 시작했고, 위험 부담이 큰 DB에 락을 거는 방식보다 레디스를 활용하여 락을 거는 방식으로 접근했습니다.&lt;/p&gt;
&lt;p&gt;레디스 캐시에 접근했을 때 &quot;display:foo:1&quot;키의 값이 존재하지 않고, 락 옵션을 사용하는 경우 &quot;display:foo:1.lock&quot;을 키로 갖는 데이터를 10초의 TTL로 생성하여 락을 획득한 것을 표현했습니다. (TTL은 프로세스가 비정상 종료되어 영원히 락 해제를 할 수 없는 상황에 대비하기 위해 결정)&lt;/p&gt;
&lt;p&gt;이어서, &quot;.lock&quot;데이터 생성에 실패한 경우, 매 초마다 &quot;display:foo:1&quot; 데이터가 생성되었는지 확인하도록 로직을 변경했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
   &lt;div style=&quot;width: 100%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1822px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ba0edb3909846e3340522c2ad559ca0a/db98b/04-03-lock-blueprint.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.70833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAACQElEQVR42nWU6W7bMBCE8/5P16J/isRBYcdwLMk6SIrX8pgO5djNVQr0oePj7M5QD3gbtdYv37fjv6PdW8p1vo2H9pFzxjzPGIYBWmv4FKCjgU0Wk9NYgoYTD5ccrDj4HGCCgXgLpITiPSoZd6CLkSCDcRw34OgmPuShfcCZ//fqiIM+4YWz/T6tHTp1RpF4FSqCSvAGjLliNo4LXU/EEKG8RqlUbRNCFniqsmHF4hWvKUx+RnCG6vI/4E1hTBWSCyJVekpPvNjKijlRhUBKhrUKlRWUfkB6fUVhJdW5u6qPQMKEK9Va7oaYuG7AcU0EJlhPNVywsPy8zCjOoqwravBfgYkGnc4ddrsnPD490pwF/TKwTGHJGakpFLsplMML5M8eub+gGAK5SHO6hvC+5ALFm1MSREJAReN0ZC81rPOQ5naY6dyMutKIZY+iDqitzyw7dx0KE3KLzgZsD77P4GRGOr9idgqGJljCESbCnvnwE/LwE4Xnth42hR9NIZDxeA+c1wWKOTu7ga5OXHAA7BF1/o06/EAdf6Ha/g75AAw0ZP2kcNATFM9FCjBB0NMApRYsbdIYY6hcaSbi29gUOCoUNrhFp8WmKWwuMwCECgZj0WnGyCT0WnChWZfFI0f5HthmZlMTCaVUusqb67XJmS63rRikMpcRu84yTgLnIrLI152SCGjAixEcp4DTHLFSqUt+278rI5MKwx4KlcZt9jpu2WUUtujUz3u5jcL+5XKd7ShUeJu33n77trnNt7fNXxtOkzB5kbuKAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ba0edb3909846e3340522c2ad559ca0a/263a4/04-03-lock-blueprint.webp 480w,
/static/ba0edb3909846e3340522c2ad559ca0a/a6361/04-03-lock-blueprint.webp 960w,
/static/ba0edb3909846e3340522c2ad559ca0a/87cd1/04-03-lock-blueprint.webp 1822w&quot; sizes=&quot;(max-width: 1822px) 100vw, 1822px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ba0edb3909846e3340522c2ad559ca0a/9aebd/04-03-lock-blueprint.png 480w,
/static/ba0edb3909846e3340522c2ad559ca0a/a91f8/04-03-lock-blueprint.png 960w,
/static/ba0edb3909846e3340522c2ad559ca0a/db98b/04-03-lock-blueprint.png 1822w&quot; sizes=&quot;(max-width: 1822px) 100vw, 1822px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ba0edb3909846e3340522c2ad559ca0a/db98b/04-03-lock-blueprint.png&quot; alt=&quot;분산 락을 이용한 캐시 스탬피드 해결 로직 흐름도. 한 스레드만 락을 획득해 DB에 접근하고, 나머지는 대기 후 캐시를 읽음&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;이러한 설계를 바탕으로 DisplayCacheModule에 기능을 추가했으며 코드는 다음과 같이 작성되었습니다.&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;DisplayCacheModule.kt (Fallback Lock 로직 추가)&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * fallback 기능을 분산락을 사용하여 한 번만 수행할 때,
 * 락의 수명과 다시 읽는 주기 등을 설정하기 위한 열거형 클래스
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;FallbackLockOption&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; maxLockDurationMs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 락 수명(밀리초)&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sleepMsBetweenObtainLock&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long &lt;span class=&quot;token comment&quot;&gt;// 읽기/락 획득 주기(밀리초)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;// 락 미사용&lt;/span&gt;
   &lt;span class=&quot;token function&quot;&gt;DISABLED&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 기본 락 (최대 10초간 락을 걸고, 1초마다 락 해제/데이터 적재여부를 확인)&lt;/span&gt;
   &lt;span class=&quot;token function&quot;&gt;LOCK_10S_CHECK_EVERY_1S&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10000L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;DisplayCacheModule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; circuitBreaker&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CircuitBreaker&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; redisTemplate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisTemplate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; objectMapper&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ObjectMapper
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;companion&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 이전 코드 생략 ...&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;/**
       * Resilience4j적용, 레디스에서 데이터를 읽어오고 예외 발생 시 핸들링 수행합니다.
       * 추가로, 데이터 획득 실패 시 락 획득을 시도하고 획득에 성공한 경우 &apos;[isCurrentThreadHasFallbackLock] = true&apos;로 플래그를 설정합니다.
       */&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryReadFromRedisWithObtainLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         hashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         throwWhenReadFail&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         fallbackReturnType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Class&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         fallbackLockOption&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FallbackLockOption &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; FallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;DISABLED&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         isCurrentThreadHasFallbackLock&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AtomicBoolean
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; isFallbackLockEnabled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fallbackLockOption &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; FallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;DISABLED&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; readRetryCount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isFallbackLockEnabled&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxLockDurationMs &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; fallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sleepMsBetweenObtainLock&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

         &lt;span class=&quot;token function&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;readRetryCount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 1.레디스로부터 데이터 읽기 시도&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; readData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; circuitBreaker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;executeSupplier&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;token function&quot;&gt;tryReadFromRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                     redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                     cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                     hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                     throwWhenReadFail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; throwWhenReadFail&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                     returnType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackReturnType
                  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 2-1.읽기 성공 시 반환&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;readData &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; SAFE_NULL_OBJECT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; readData
               &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;No data &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; in redis. (readRetryCount: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;readRetryCount&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;throwWhenReadFail&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; e
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;// 2-2.읽기 실패 시&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;throwWhenReadFail&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;NoSuchElementException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;No such data &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; in redis.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

            &lt;span class=&quot;token comment&quot;&gt;// 3.락 획득 시도&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isFallbackLockEnabled&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;tryObtainRedisLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                  cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                  fallbackLockOption &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                  isCurrentThreadHasFallbackLock &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; isCurrentThreadHasFallbackLock&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token label symbol&quot;&gt;@repeat&lt;/span&gt;
               &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 락 획득 실패 시 (1)부터 다시 시작&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

         &lt;span class=&quot;token comment&quot;&gt;// 레디스에 데이터가 없고, 락 획득도 실패한 경우 도달&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; SAFE_NULL_OBJECT
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;/**
       * Redis에 .lock 데이터를 생성하여 락 획득을 시도합니다.
       */&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryObtainRedisLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         fallbackLockOption&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         isCurrentThreadHasFallbackLock&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AtomicBoolean
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; lockKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.lock&quot;&lt;/span&gt;&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; lockValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;/span&gt;

         &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; lockObtained &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; circuitBreaker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;executeSupplier&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               redisTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;opsForValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setIfAbsent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lockKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; lockValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxLockDurationMs&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TimeUnit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;MILLISECONDS&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lockObtained&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 4-1.락 획득 성공&lt;/span&gt;
               isCurrentThreadHasFallbackLock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 4-2.락 획득 실패&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 과부하 방지를 위해 일정 시간만큼(최소 1ms) 대기 후 레디스 읽기 재시도&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 대기 후 다음 싸이클에 데이터 읽기가 시도됐을 때,&lt;/span&gt;
               &lt;span class=&quot;token comment&quot;&gt;// 다른 컨테이너/스레드에서 fallback수행 결과를 적재했다면 여기에 다시 진입하지 않음&lt;/span&gt;
               Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sleepMsBetweenObtainLock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;coerceAtLeast&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
               &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Fail to obtain lock &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;lockKey&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; from redis. (&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;/**
       * Resilience4j적용, 레디스에 데이터를 쓰고 예외 발생 시 핸들링을 수행합니다.
       * 추가로, 레디스에 생성되어있던 .lock 데이터의 삭제를 시도합니다.
       */&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryWriteIntoRedisWithReleaseLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
         redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         hashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         ttlBySeconds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         writeData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
         isCurrentThreadHasFallbackLock&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AtomicBoolean
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; isWriteSuccess &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;
         &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 5.레디스에 데이터 적재 시&lt;/span&gt;
            isWriteSuccess &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryWriteIntoRedis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
               redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               ttlBySeconds &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ttlBySeconds&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
               writeData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; writeData
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Fail to write data &apos;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;cacheKey&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; into redis.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;finally&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token comment&quot;&gt;// 6.락 해제&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isWriteSuccess &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; isCurrentThreadHasFallbackLock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
               &lt;span class=&quot;token function&quot;&gt;tryReleaseRedisLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;br/&gt;
&lt;p&gt;이에 따라, DisplayCacheModule의 fetchAndPut(..)함수도 약간의 변화가 생겼습니다.&lt;/p&gt;
&lt;details&gt;&lt;summary&gt;DisplayCacheModule.kt - fetchAndPut(..) (Fallback Lock 로직 추가)&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * 캐시를 읽어옵니다. 캐시를 획득하지 못한 경우 함수 본체를 수행한 결과를 캐싱 후 반환합니다.
 * @param redisDataType 레디스 데이터 타입
 * @param cacheKey 캐시 키
 * @param hashKey 해시 키 ([RedisDataType.HASH]의 경우 사용)
 * @param ttlBySeconds 데이터의 TTL(초)
 * @param putDataAfterProceed fallback로직 수행 후 데이터 적재 수행 여부
 * @param throwWhenReadFail 읽기작업 실패 시 예외 던지기 여부
 * @param fallbackFunction 읽기작업 실패 시 수행할 fallback 함수
 * @param fallbackReturnType fallback함수의 반환 자료형
 * @param fallbackLockOption fallback함수 수행 시 Lock 사용 옵션
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAndPut&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   redisDataType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RedisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   cacheKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   hashKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   ttlBySeconds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   putDataAfterProceed&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   throwWhenReadFail&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   fallbackFunction&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   fallbackReturnType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Class&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   fallbackLockOption&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FallbackLockOption &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; FallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;DISABLED
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; threadHasFallbackLock &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;AtomicBoolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// [Note] 지역 변수는 동시성을 고려하지 않아도 되지만, 락 획득 여부를 참조로 전달하기 위해 사용&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 2.레디스로부터 데이터 읽기 시도 (읽기 실패 시 Lock획득 시도)&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fetchResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryReadFromRedisWithObtainLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 3-3.throwWhenReadFail가 true면 데이터 읽기 실패 예외를 던지고 종료&lt;/span&gt;
      throwWhenReadFail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; throwWhenReadFail&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      fallbackReturnType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackReturnType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      fallbackLockOption &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      isCurrentThreadHasFallbackLock &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadHasFallbackLock
   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 3-1.읽기 성공 시 데이터 반환&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fetchResult &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; SAFE_NULL_OBJECT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fetchResult
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 3-4.데이터 획득을 위한 fallback함수 수행&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fallbackResult &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;fallbackFunction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// fallback수행 중 오류 발생 시 빠른 재시도를 위해 락 해제를 시도하고 예외를 던짐&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;threadHasFallbackLock&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tryReleaseRedisLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; e
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 4-2.fallback함수 수행 결과를 반환&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;putDataAfterProceed &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;isNullOrEmptyData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fallbackResult&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fallbackResult
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 4-1.레디스에 데이터 적재 시도 (적재 후 Lock해제 시도)&lt;/span&gt;
   &lt;span class=&quot;token function&quot;&gt;tryWriteIntoRedisWithReleaseLock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      redisDataType &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; redisDataType&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; cacheKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      hashKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hashKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      ttlBySeconds &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ttlBySeconds&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      writeData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fallbackResult&lt;span class=&quot;token operator&quot;&gt;!!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// isNullOrEmptyData(..)에서 null 검증을 수행&lt;/span&gt;
      isCurrentThreadHasFallbackLock &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadHasFallbackLock
   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

   &lt;span class=&quot;token comment&quot;&gt;// 7.적재 후 fallback함수 수행 결과를 반환&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; fallbackResult
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;br/&gt;
&lt;p&gt;마지막으로 캐시 호출부인 @DisplayCaching에도 FallbackLockOption속성이 추가되어 아래와 같이 호출하도록 변경되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@DisplayCaching&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   displayCacheInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DisplayCacheInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;FOO_PAGED&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   fallbackLockOption &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; FallbackLockOption&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LOCK_10S_CHECK_EVERY_1S &lt;span class=&quot;token comment&quot;&gt;// 기본 락 (최대 10초간 락을 걸고, 1초마다 락 해제/데이터 적재여부를 확인)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchPagedFooData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token annotation builtin&quot;&gt;@DisplayCachingKey&lt;/span&gt; pageNumber&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;FooDisplayResponseDto&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;// HttpAPI호출, DB조회, 비즈니스 로직&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;캐시 모듈에 분산 락을 적용하자, 재실행된 부하 테스트에서 폴백 로직이 정확히 한 번 실행됨을 보여주었습니다.&lt;/p&gt;
&lt;p&gt;폴백에 의해 데이터가 채워지는 동안, 다른 프로세스와 스레드들은 매초 레디스에 데이터 적재 여부를 반복적으로 확인했고, 데이터가 적재된 후에야 정상적으로 읽기를 진행했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; justify-content: space-between;&quot;&gt;
   &lt;div style=&quot;width: 100%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1478px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/08b12bdd3acb3137bc53cd4c8ad8eb88/02a0d/04-04-overcoming-stampede.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 41.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABoElEQVR42i2SiY7aQBBE/f9flmgTJbsbRCIgYHzMPT5hsQ28tGdjqWV1j6b0qnoy1wSGYeTOk94EVF4x3e8sy53L9QOlDcPlwiKzaZ6ZZT4vi5wvqd9VGtV0+BgZxgtZ2Sic8eRa4V+21N+2nI2mKCqcj1jnqZWmrGuKqqaqFca69J/vD3Ir5yIYup5pmshUa2lloGMgvh2w73uU92gh64eBpm3RxibhVWgljk2D94Gb0NpuIAxX2vGayLPclXgXOAqB+bKhfNmwLwp2+4MQBiplOOUFx/yc6nA8Ca3ieMq5TTMH7cjFSWh7bh83MtN7hm7EdS3+xw77tsdIHmWlaMVGiE0SXumM/aRc+1pIH2L5JDv47SrG61UyFcIi1omwdAb9dYP6vkUHL1RnfAhoazFiec1wzW21X2vNuSx5Pp74riF3NbHrmG6Soe4tnWRo24b4usf/+kschCzERNi0Ha3Umttaqf9PLg8D10fyoOjGQZYyk9WtFsFelhJpfu4wrztUWJdi0+UoF5Nd4z6XYkwSW5e0fq44Uv55lyczJsF/ud1ZQ5FUnE8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/08b12bdd3acb3137bc53cd4c8ad8eb88/263a4/04-04-overcoming-stampede.webp 480w,
/static/08b12bdd3acb3137bc53cd4c8ad8eb88/a6361/04-04-overcoming-stampede.webp 960w,
/static/08b12bdd3acb3137bc53cd4c8ad8eb88/d1d4a/04-04-overcoming-stampede.webp 1478w&quot; sizes=&quot;(max-width: 1478px) 100vw, 1478px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/08b12bdd3acb3137bc53cd4c8ad8eb88/9aebd/04-04-overcoming-stampede.png 480w,
/static/08b12bdd3acb3137bc53cd4c8ad8eb88/a91f8/04-04-overcoming-stampede.png 960w,
/static/08b12bdd3acb3137bc53cd4c8ad8eb88/02a0d/04-04-overcoming-stampede.png 1478w&quot; sizes=&quot;(max-width: 1478px) 100vw, 1478px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/08b12bdd3acb3137bc53cd4c8ad8eb88/02a0d/04-04-overcoming-stampede.png&quot; alt=&quot;분산 락 적용 후 캐시 스탬피드 문제가 해결된 API 응답 시간 그래프. 최초 1회만 DB 호출이 발생하고 나머지 요청은 지연 후 캐시를 통해 빠르게 응답받음&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;최초 .lock을 획득한 로직이 3.25초의 수행 시간이 걸렸고 비슷한 시점에 들어온 요청은 DB호출 없이 약 3초간 레디스 호출, 데이터가 레디스에 적재된 후 부터 빠르게 응답받음을 확인&lt;/figcaption&gt;
&lt;br/&gt;
&lt;h2 id=&quot;-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot; 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎁 마치며&lt;/h2&gt;
&lt;div style=&quot;display: flex; justify-content: space-between;&quot;&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 786px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6fc2148e1615c9fae4dcf93fe372815f/d703b/05-01-our-proud-gift-corner1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 219.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAsCAYAAABloJjNAAAACXBIWXMAABYlAAAWJQFJUiTwAAAIlklEQVR42q1XaVNU2Rnu35Cv+QFT+ZBkrMlWNamZmkoqk1hZnIyg0RpHxsIYt0H2XZSlBUSBARHBYXRUlFVAWhhHQdlBWdWGprtv73s39L7ST95ze9VMKl9yut57z3af827nnKcFwWAQLpcL/614PB6IxWIoFAq+zko4HI6Pm81mBAJB6A0G+Hw+CNjDbLbwgzs7O2BTw0kf2e12zMzMYH19Pb5wMqDJZOL7NRot/xawQQYUCoV4ebvOJFbY3B+U6DxWF9hsNkxMTEEi2cTwsAjPnk1gdm4eq6trWFlZxuzs7A+a+naJjfEasg+sNit0Oh0cDgfvC78/AIfbB68/GHUCc0kY5PL4x+wZCvgQ9idiIGAPnV6D/uuNGPq2GSajjh+wSC1QD65COScH4YBhmI1rUHDDsFpV/By/1wGdehPS71ph04oTgOvrLzHcmIfptgKo5JEB6wslLMU3MN/UC6ZjKBSGQf8YEnEaNjdEkQhbjTDQAq8fZUEsWU4A2m1mLE4+wMvJPvhc25HomhzYfLoG+aosKUVMUCoWyQol33a4nNAppmHiHsEX9bMg5mfbtgtOlzf+sc/vh4r51OmM97EAGo0meL2+eCDsDhe5zAS3253QMOi1wakbg9s0h51gZKWQ14WAUY2A3RoHNOhtlA16bG+5ogtYIRrqJd9qyCXBBKBDNwXpk3xwjw7Da4v40KVeh3ZqFLbVWVIlRJEPoGhgBV03h2GZW+PnjA4PYNc7P8bD+7fg87oSgG7zC+hni6AaIUBrBDCgl8Ex2AHL4rPIApQ+X323jpneGXiifp18OoaSjM8x0tMG+7Y1ARgKeMCJx8FtzCAU9PMDzm0bFMvzMKkVcZPVSi0l+ya2oiaz3SER0wZYWkgkdiRBw+R8HznYHR8IUAY73B5KcH8c0EsJ7/a44/7iDw+3l8z1v5nYQaMFzkd3EJx/jHA00gsaG+4tyzCptiHEFqHstq6+gvXZFLycJpoJPqhMMnDGjfjCEZM3ZNBcK4W1pxFhQ8QXlwfGcWTP+2jpHYCDnQ8kz+81Yzrjb9h6NBVxC23TdslplKz9FgabNgEIkxkvBzux+WSAQh7Ju9EVKYq+asWDyVnsRDeudXkaktZ2OJcjUQ5Q5CX6FaxpZuBOjjJTVymXw6Q3IBQ9rnbITIPFRj5LTvYgjNSX8HMARr2ZvrPQIRtIDgpvER+cN46kt4+o6Jxw0lgwKrG2YIeQd3QK+LRqeNRaeFUkSiUvPpWK2kyUcfGplQjR26mXwLBFpmtpX2sVCKo57AT8EHi1KkxduoT1hga8avgD1q58BFllNWTnyyArOwtZUSFJAWTF+fTOhaI4G5NZp/Aw9wvUnP4YNw6nou2zv2MqMw0hAhaINW7U1t2HtKke4qsH8frqHsirL0JeUQ7uUh2U/X1Q9pH0dEPR2w11dydmzuZhJv9LDP3rCO6eOITbxw5iruAoggxQo7bgdf1FcA1CSOsuQ15Zw4PJSTsecHQESpGIZBjK4SEoh/qhFNI2rciGRpgFXdUZ6KuzqO8MvGS2wK9SQFZTCWmNkN5CcLdugmtrA9faCu7KFXCNDeC+uQ55TRW168G1N0N+4yrk1y5Bdq0WsrZabBKYVJgBr4YAPeR8WVUF5EwuCKEgjbjeHii67kIxOADFndtQPBgE19IERU8nuKE+KER94O51gOshudsKWW02ZNWkITPZQxe4pKwMm+fPQ0IiFVZhM9repLqkgt7V1Fd5FpLqc9ioKSMpIcnHxoUcrFdlQlKXiY26M/BQtghcHIeVwkKsFBdDXFyCF2XlWKqswmpBPlZLirBaWoyV/DysFeVjuTQPy+dJKqldX4SHn6bi/vt/wWRJIWYLzsDJSSFwyGSYyc7CWl4BcjLTUVJehummrzFbdg7PCwswT5PHjlOapB/DBGl1t6cI49fKMFNdgJ6UNNx+bzd6dv0ZnT/fDfPya7ropZuY+PI0ejJO4cOj+5BfkIu/nkxDYeYZdOdkYoL6RzOy0Zeeg7HcU7hzKx/dJI8vC/GgqgG3P96Ha+/+Dt/8aR9Mr9YhMG9I8PTYCWSkH8Dhk0fxwaFP8JMTKaisrUJPaREmz53DWGkJnpVXYEJ4FiOt5bjw7WkMtl/EyNl6dP0mBV2ZpeguEcLMqQhQIkbfib3oSv8nykh+lPohTlQW4/uOrzFeUYGxuot4XF2JJ9dbMHD3JkY7GtHTIUR/SyXaM0+i44//QG9uOe4VlsAop7Qxbb5CZ8Ev0FX4a3Se2I/KI2m4fCwdXTnZGKZA3c/LxUBxIfpry9HcfhXnWhpQX56Doi/34vf5v8SetHfReCgNnbnpMMnEEGjFS2j64h00HX4PzZ/vQlvKbrR+shctqalo3heV1BS0HNiHupMHkZaRguPHDyDr4Kf4LPUjfLD/pzj+s1/hUsp+YhESykObHStP7mH54QOsjPRjabQfy6IRElFCHoqwRFtv8UkvFp52YY4Sf1H0PRZHxzE9KMJEZx+e3xfBS+xBgP9zESQTSSS9kdTeiZPPUESSyChf5yVy0gsYH5xnBHNtDYtLS5ifX4BUKsXY+FOefM7RWDKL/Z8aMtLOePLW1jbPp41U396282TcYrGSWOKAMbrMiCfTPERahaJUOA7I2OvCwjwY8Nb2NomdLj4XP8npcsNMl5KT2mzcRovqDYyku4nr+GlxM9/ndnsSgBGfRf3IfuHIirxWScQ8pmWsniDtb/YJ/CEv5rduw+xSw+X0EvfzxgeZZowLsnqQTN22O/hrlTeX5rC6ne5x9k2c2zDAxe1umJ0aeFx+nl4w0s5MYiYz4umnm9FD4A4CZG0GwFzl5ecQUSVSyuaEY2mjVRugUmjifjAajTxTjRWVWg0F7dNY0Wp1/JxYYX96TBTEeB5qdVooVcr/AIyZsbW1xQcs+d8Ty4pYYeMxSvxv8if7Y7J2JeoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6fc2148e1615c9fae4dcf93fe372815f/263a4/05-01-our-proud-gift-corner1.webp 480w,
/static/6fc2148e1615c9fae4dcf93fe372815f/f6aa4/05-01-our-proud-gift-corner1.webp 786w&quot; sizes=&quot;(max-width: 786px) 100vw, 786px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6fc2148e1615c9fae4dcf93fe372815f/9aebd/05-01-our-proud-gift-corner1.png 480w,
/static/6fc2148e1615c9fae4dcf93fe372815f/d703b/05-01-our-proud-gift-corner1.png 786w&quot; sizes=&quot;(max-width: 786px) 100vw, 786px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6fc2148e1615c9fae4dcf93fe372815f/d703b/05-01-our-proud-gift-corner1.png&quot; alt=&quot;올리브영 앱의 개편된 선물하기 메인 화면 스크린샷&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 756px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/589b77bebec3e4471301cc584a0d74e0/68c0a/05-01-our-proud-gift-corner2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 221.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAsCAYAAABloJjNAAAACXBIWXMAABYlAAAWJQFJUiTwAAAI/UlEQVR42oWX+XOdVRnH75/g/6Cj48io+APoD6gjYh0clRnA4gh2BAQK2AoVylCsaVq60IaWUrB0DekSljYJJW3TLG3WJm327d7cLXdf37vv+8fnvCGLgPXMnHnvOe/zfs9znuX7PNdQKBRIxBPoo8bKsNvtmM1mxsbGGB8fZ35+DqfDIXsL8lxckfP7/OTzObw+H4lEAkOxWCSZTK7g1WpLqKFQEJ8I2Ww2rDJ9gSCBYBC320ckmlwBTCRT5EUpLRIhnU5jqFTKVCo1soUKxVJVByyXa7K3qnIumyIXD1KrVqGao1rwUc5HdNlapSRbaarlEhV5b7DbjaS8aZznbmG9NiugNbTwJDbrOTTNQlQLYbdMMdO2H4fpNglnO7aeFwlO7aVSSJKwzeK72UU55MQRlCtbjCPk7Eliu85z68B5MrkqkfAwdvPzOGyX8fg1/IutLPRsYnJ2jKynHXvHs3i6nqWUj5KdGyHYeloAF5l0xTDk0lGS0SzGm0ZsM3b9itFIFKdzlojmJS72tZj6WDR2kkrnyMVMuKcaCVg6l2y4aMI30EE6EiAntzOozZLcX4tFyYm3dCHxViCokcsVdLuFtRj+QETWOTFJRRwUEScsRUY8nSEQiemOUcOgvPinxzfw8CPrudHbp2+mM1lUOJVKJd3wFfGQCo1MJicOK+t76p2Sy2bFSdXKUpTIvsFqtfH7hx7mlw/8ms6u7qVTJS5TqbSAFnWhgh5aKT1ESiUVFVWyom1SZJRcZSkkljRc/lFTIVKTsKmW5SkCNXlWS4gr5VleilGZFaVxrba6FpNkBDwvh1aVhmuSYyVR1u79v1GWqyficYnVjLqzAKa9lKY+oDRznPLMMcqzx8lP/ZvC1FHZP0px4h0q1lZsiQKjoQyTWpYJLfPFzDIdyWFM5JiLZ0gWJbDL/pvkmu+l+Om9BC9tJHDhrwSP3Yt29EcUz91F5vS3KHY8zhVngvdnwxwzhvjcHObWosaoI8onCxHabD1cXuzEkc6LDbUxghcfwHPx54S66iRom7EefBT3W7/FM7yfZOs6iv0v0OVJctqk0WTW6HdKGHmjEvg+LttinJ28yZmxHny5MoZSTJL/+l6cp35ItP0JvMPv4Gu5D9fAWwy1HyP22f3k+zfS7Ulx0hjklDEgICEsZjdjEwtcdSS5f88+7nnjdXxZASzELDg7D+G4egxL84/RznybbM99+PrfxtO/meSVn1AYfI1OT4KzJh+jdg8DNg8L9gAjk2ZaXSkOXenm8MVPCeQrwjb+ftId63Fd2UXy0/vp3/ZNTrxzF8bGH5AbeATriT24+y6JhhHOLARw2X30mO3cnjYzNDzF4GKYgMlKcn5G4jQpVw6OY3z6O3iP/I5gy176LqyjuXUnzu1342rbya1De/B/uJkbgSSNpgCz8zbajTbGZqxMjEzic/uZ7h1gomdIzxpDMTTMyPPfxbbzN7hPvMlg22HOtTTw8cbvM3/kHiybvofvrce4Ec5wSpwyOmum3eRlbNbOzNg0Tk+Q0av93G7pIJUVL1fcPZTa78PT9CjzjZsY/uh9+hr+wYXnH8TacDfRHd8g17qeUbfGlTkH43NmbhntTE0vMDE6wYLRwozZwfSCg5SkpgS2i8rUIUqjB4mPtRAcaiU80oI2cY3YjeMw3SCBfZG0CMfCmk5tcZmhUIiglIVwSCMiUxN2UrlvyApFWR1+TDY3/mAYh9srpBrSf1udHryhGGm5Sjgcxi5FakEKl8VixeP14pXp8Xj02qMoTyeHbCaDQ6qY0+nA5XJhtVr1iqcELRYzsWhEz9GgAHr9fh3I6XThlvcut1t/KtnlQmfI5/PMG42MT06unKIXny/m8ihms6RicTLiSVUps7L+sowOqPgt6g4S8YWFmaM6aSoi/bJgLRCg6HFjEf5UCkxNT+tk++VhUGSZF6YoSGLHEyldg3y++BXAQlIY2xejXCzfkeMM6pSc0H1Orp7N5b/2VPVtXpyXTavDpDR8jcwqoNC3AikKUZbvIKjKgAJSh1ZVwf9fgHdiY6WZ/nFNd7Q+lYnuNHRA9dHyqWs9t9aOqag4zbtIJOAiFvSQCPtlL0AmHiKb0EhG/BQLOeUUVSLzek3WtIhe2ZRjgpIJ8/NGdtVtxWaZ5sJ79dQ98TOOvPJHmuqfou3dl+g8+Qa9Z3fRd34fbYdfxmOeXAXMSFzZXR4p2nEp+gnsTi89Xe08uO6nTIzf5pN369ix4RecqHuSC7s3cf615+g+U0/vud1cb95LV2M9msf8BaCUQZWHCxYbQ/2DdHXfoGN4FJdoXCpkKYlM475X2f30Ok7VP8nVw6/Q8vozdDbtpE8ALzdspev0GkDVYpTFi72Ts2y8dJ0NfVM8NjjLNU9Y1UkKEvwfHthG3V9+xcEt62lv2MylN57i3IG/0350G61bn+b8bunInMY1gBKLI5ZFnmvpZFtHP7s/7ySalpZEbKvC5aP33qT+mYfYv+XP9B3fzSUBadyzhbYPdtK1ZzOfvP06IY99FbBYLDE1M8eh/ft590AD2+vqpSuo6TmrYnRy4CqDLScZ+/AgQ0d2cHX7Cwx8fpaxK+fp3vEiFxu2ExHvrzhFdU8ur5/2jk4+utjKmeaPpb2QLMpl9cNmR3qYv3iM3h1/o33nSzRvXE/T5g30Nh6kd5douHcrYQkpHTCr529e13DaaMYTTeDV4nozpPZ1h00MMHZyH5/960Xa9r5K07N/oOm5x7h88J90vvkS723ZwKJpCoMKaNWaqRmXHkVRmKInNVUqLr0r47PNYBm9zsJwpzSn1zDd6sZ0W7QeusbcYAfj19uE0X2Sy/JBNCpdbEoCWsh2aWaFeRI6S6sD1C1iIhMMBnTZiHT86vBkMiHZtJRlekOvmqWUAM3Nz+u0bjKZ9Gk0mggI/4UEUH2oANUzIrUkJiQbjcX0HjIt3etXcrkqyZ4TBlF2VF2roifl9bVNpJ7rsq6uYfK1jLM2/3VyUNoE5U/N8lCaqOsuD4/HKx2DfWWt3vn9gVV5MYPCWAFURcfhcK4IKPBlATU0TdPttgr432tVoJRtFeB/AEHHSZM/b5rJAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/589b77bebec3e4471301cc584a0d74e0/263a4/05-01-our-proud-gift-corner2.webp 480w,
/static/589b77bebec3e4471301cc584a0d74e0/42545/05-01-our-proud-gift-corner2.webp 756w&quot; sizes=&quot;(max-width: 756px) 100vw, 756px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/589b77bebec3e4471301cc584a0d74e0/9aebd/05-01-our-proud-gift-corner2.png 480w,
/static/589b77bebec3e4471301cc584a0d74e0/68c0a/05-01-our-proud-gift-corner2.png 756w&quot; sizes=&quot;(max-width: 756px) 100vw, 756px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/589b77bebec3e4471301cc584a0d74e0/68c0a/05-01-our-proud-gift-corner2.png&quot; alt=&quot;올리브영 앱의 선물하기 테마 카테고리 화면 스크린샷&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;&lt;/div&gt;
&lt;/div&gt;&lt;br&gt;
&lt;p&gt;많은 고민과 노력 끝에 다사다난했던 선물하기 기능 개편을 성공적으로 마무리할 수 있었는데요. 👏&lt;/p&gt;
&lt;p&gt;이로써 평균 1.74초가 소요되던 페이지 로딩 시간을 5밀리초로 크게 개선시킬 수 있었습니다.&lt;/p&gt;
&lt;p&gt;또한, 새로 개발된 캐시 모듈을 통해 기존의 불편함을 개선하고 &apos;캐시 스탬피드&apos;라는 잠재적 위험 요소까지 해결할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 끝이 아니었습니다. 개인적으로는 아무리 좋은 기능을 만들었더라도 이를 제대로 공유하고 전파하는 과정 또한 매우 중요하다고 생각했기에 새로 개발된 기능에 대해 &lt;strong&gt;팀 내에 공유하고 전파하는 시간을 별도로 마련&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;여러분도 새로운 기능을 개발했다면, 팀원들과 지식을 나누는 시간을 가져보는 것은 어떨까요?&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display: flex;&quot;&gt;
   &lt;div style=&quot;width: 50%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0b66fc8cdf1f4d3e494f64f0783db9cf/42a19/05-02-the-continuing-journey.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAADpElEQVR42kVTaXPiRhDVn0+qkq/5kkptOc61Tnm9XtuAMadAYJA4vDaYZTmFpNEJumamxzjritOiXJWq96E186b79euWVDQgv4ErHS51uFjz8xX/uHzD2fL/+A2rDMj5tIYLHaSaDRUCZQIlC4om3JhQMN6QM+BqA7kD8kZ2VbSyAM/zh1iqEp5bsxuDl0wuOwJRtTOUiKgZVLHSisVLBKpENFxRO1w9hKIXYICVCb9c0ssVy+tM8UTDgaYrqgT6JHnep+meqzat29D0BJ7Lrrj1xSQWo0i0fJD6Pi9uWJ3A5YpicUTTZssg3j/Tf1+YTkXXf6rotGnzbiCGoUBy0eDXBq8RkMYhVCxWtfjVmmpbUTGZESavL/TFcL8loSnA5exx+1Recc3n00TIhDds3nIBJWeyyxbrB4CdKw7ULWZG8es/yWu8e9qzMRXsG12H4mYBWgBtX5RN3vEBG7n1QDqd0z+nVHEwHygu9Hei7/FRQI1tFAS2FYarbdqxWMvmdZufTaLrDbvS+fGEFk0u/T2jx+P0cs0HAVcd1vP5KH5qOWxuBJEzrhO3sNmrdvo53pf19Idr83QSf1iw38bp2ZJJpcOQqiZrmLTn8a5NO4R2HGqQXeo9Ku6uSp77Phv6VDXj/CwuG+xiSU+/0osVk3o+dD1oW/RuCx3C5HWiebyqp+qjcz+6Pxm7JxPWxaQLv/NIuti2zdCtusXxlYRWDbagmCk+vhqH17P0Pkjz6vyorJ82Hn6qLr+7cd4ppLJK2oSi4Q0jvbVZxwMN59x2+K0LXYchSvOwuEg03WOrzmJxX5vMfm07v6vbYy2t6WluFMo6VVxeWSeqx3sBSCipSbhiUVReXyeYtTF33Xl3urnTvOUfvfivfvhLKyrM09w4vpklsgOKndVHvdJwB62DVe8HwfcFC0eibKKH5cwwH7rLr0ft8F0zOupEJ1P6YZLgqmHBJpbZpCrKHgQgb1I0STboxTTOf4nlDZVNPrCju7lxom5/zJHju7DhArJRYMOgLZJtK+5Z9lh1WROLBwLj87tYMZm8igrTULZY12Wn97vcPB3uBHLqevpxtCstEyS/GVazeJsw7FZz+dnnqGWzc2X9vml2HNY00v4242UFLap58HPD+TTeYS6MpZaTbQhajzNDUsfNfgB5vit8ibRAYFL0BlPgnGTcZ4ejz10X/zyOTGkYwHCbvez7cJh5Jn6we8LcuD/4OTwAA9wKPEEy+qwe4v8Ag9EGzx8XoOoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0b66fc8cdf1f4d3e494f64f0783db9cf/263a4/05-02-the-continuing-journey.webp 480w,
/static/0b66fc8cdf1f4d3e494f64f0783db9cf/a6361/05-02-the-continuing-journey.webp 960w,
/static/0b66fc8cdf1f4d3e494f64f0783db9cf/d71bc/05-02-the-continuing-journey.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0b66fc8cdf1f4d3e494f64f0783db9cf/9aebd/05-02-the-continuing-journey.png 480w,
/static/0b66fc8cdf1f4d3e494f64f0783db9cf/a91f8/05-02-the-continuing-journey.png 960w,
/static/0b66fc8cdf1f4d3e494f64f0783db9cf/42a19/05-02-the-continuing-journey.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0b66fc8cdf1f4d3e494f64f0783db9cf/42a19/05-02-the-continuing-journey.png&quot; alt=&quot;선물하기의 계속되는 여정을 나타내는 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;선물하기 캐시 모듈 개발은 단순히 기능을 구현하는 것을 넘어, 안정적인 서비스의 기반을 다지는 여정이었습니다. 그리고 이 여정은 현재 진행형입니다.&lt;/p&gt;
&lt;p&gt;저희는 예측 가능한 문제를 넘어, 아직 수면 위로 드러나지 않은 위험까지 찾아 해결하는 과정을 통해 기술적 깊이를 더해가고 있습니다.&lt;/p&gt;
&lt;p&gt;혹시 저희가 놓치고 있는 부분이 보이시나요? 올리브영은 함께 더 나은 기술적 해답을 찾아낼 동료를 언제나 기다리고 있습니다.&lt;/p&gt;
&lt;p&gt;저희와 함께 다음 기술 블로그의 주인공이 되어 보시는 것은 어떠신가요?&lt;/p&gt;
&lt;p&gt;긴 글 함께해주셔서 진심으로 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영 물류 시스템의 진화 - 고객 경험의 시작과 끝을 함께하다]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-08-01/logistics-system/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-08-01/logistics-system/</guid><pubDate>Fri, 01 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영에서 물류 시스템을 개발하는 올여우🦊입니다.&lt;br&gt;
올리브영의 물류 시스템은 비즈니스의 성장과 발맞춰 끊임없이 진화하고 있습니다. 오늘은 올리브영의 &lt;strong&gt;물류와 재고 그리고 배송&lt;/strong&gt;에 대한 이야기를 들려드리겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;개요--고객-여정의-시작부터-끝까지-함께하는-올리브영-물류&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94--%EA%B3%A0%EA%B0%9D-%EC%97%AC%EC%A0%95%EC%9D%98-%EC%8B%9C%EC%9E%91%EB%B6%80%ED%84%B0-%EB%81%9D%EA%B9%8C%EC%A7%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%AC%BC%EB%A5%98&quot; aria-label=&quot;개요  고객 여정의 시작부터 끝까지 함께하는 올리브영 물류 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개요 : 고객 여정의 시작부터 끝까지 함께하는 올리브영 물류&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;올리브영의 물류 시스템은 &lt;strong&gt;고객의 Discovery(상품 발견)부터 Last Mile(최종 배송)까지 모든 여정&lt;/strong&gt;에 함께 합니다.
고객이 온라인몰(국내/글로벌)이나 매장에서 상품을 탐색하고 주문하는 순간부터 고객에게 전달되는 마지막 순간까지, 물류 시스템은 모든 과정을 유기적으로 연결하고 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;먼저, &lt;strong&gt;올리브영의 주요 물류 시스템&lt;/strong&gt;을 간단하게 소개 드리겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-재고-시스템-inventory-systembr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9E%AC%EA%B3%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C-inventory-systembr&quot; aria-label=&quot;1 재고 시스템 inventory systembr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 재고 시스템 (Inventory System)&lt;br&gt;&lt;/h3&gt;
&lt;p&gt;재고 시스템은 고객이 원하는 상품을 구매할 수 있도록 &lt;strong&gt;정확한 재고 정보를 제공&lt;/strong&gt;합니다.
온라인몰에서는 재고 관리가 곧 주문으로 이어지며, 품절 시 즉각적인 표시와 재입고 알림 기능으로 고객 불편을 최소화합니다. 또한 매장 방문 전 온라인몰에서 재고 현황을 확인하거나, 매장에서 전자라벨로 실시간 재고를 확인할 수 있습니다.
나아가 재고는 정확한 발주, 빠른 배송 등 물류 프로세스 전반의 기초 데이터로 활용됩니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-주문-관리-시스템-oms-order-management-systembr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%A3%BC%EB%AC%B8-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-oms-order-management-systembr&quot; aria-label=&quot;2 주문 관리 시스템 oms order management systembr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 주문 관리 시스템 (OMS, Order Management System)&lt;br&gt;&lt;/h3&gt;
&lt;p&gt;OMS는 고객 주문을 실시간으로 처리하고 통합 관리하는 시스템입니다. 고객이 온라인몰에서 주문을 하면 OMS를 통해 주문 정보가 물류센터(WMS)에 즉시 전달됩니다.
이후 물류센터에서 출고 작업을 거쳐 배송이 시작되면 OMS는 해당 정보를 다시 고객에게 전달합니다. &lt;strong&gt;OMS는 주문과 물류센터 현장 업무를 연결하는 핵심&lt;/strong&gt; 역할을 수행합니다.
또한 OMS는 재고와 발주 등 주요 물류 데이터를 통합 처리하는 시스템이기도 합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-창고-관리-시스템-wms-warehouse-management-systembr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%B0%BD%EA%B3%A0-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-wms-warehouse-management-systembr&quot; aria-label=&quot;3 창고 관리 시스템 wms warehouse management systembr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 창고 관리 시스템 (WMS, Warehouse Management System)&lt;br&gt;&lt;/h3&gt;
&lt;p&gt;WMS는 입고, 보관, 피킹, 출고, 재고, 반품 등 물류센터 업무 전반을 관리하는 핵심 시스템입니다.
OMS에서 전달된 데이터는 &lt;strong&gt;WMS를 통해 물류센터 현장 업무에 반영&lt;/strong&gt;되며, 고객의 주문정보를 바탕으로 상품을 피킹하고 출고 작업이 진행됩니다.
최적의 WMS 구축은 물류 서비스 효율을 높여 고객 배송 서비스 향상으로 이어집니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;4-배송-시스템-delivery-systembr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EB%B0%B0%EC%86%A1-%EC%8B%9C%EC%8A%A4%ED%85%9C-delivery-systembr&quot; aria-label=&quot;4 배송 시스템 delivery systembr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 배송 시스템 (Delivery System)&lt;br&gt;&lt;/h3&gt;
&lt;p&gt;배송시스템은 출고된 상품을 고객에게 신속하고 정확하게 전달합니다. &lt;strong&gt;배송최적화 기능을 통해 주문별 최적의 배송 경로와 방법을 결정하여 배송 리드타임을 최소화&lt;/strong&gt;합니다.
또한 고객 주문 패턴과 배송 데이터를 분석해 지연 요인을 최소화하고 고객 만족도를 높이는 데 기여합니다. 물류센터, MFC, 매장 등 다양한 경로를 활용해 고객이 원하는 시점에 상품을 받을 수 있도록 지원합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;5-발주-시스템-logistics-ordering-systembr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%B0%9C%EC%A3%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-logistics-ordering-systembr&quot; aria-label=&quot;5 발주 시스템 logistics ordering systembr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 발주 시스템 (Logistics Ordering System)&lt;br&gt;&lt;/h3&gt;
&lt;p&gt;고객 구매 상품 재고가 소진되면, 다음 판매를 위해 재고를 확보해야 합니다. 바로 발주가 이루어지는 순간입니다. &lt;strong&gt;자동발주 기능을 통해 고객의 구매 패턴과 재고 현황을 분석하여 최적의 발주를 실행&lt;/strong&gt;합니다.
이를 통해 재고 부족 문제를 최소화하고, 고객에게 필요한 상품을 적시에 제공합니다. 동시에 판매가 부진한 상품의 발주를 제한해서 재고 과잉 문제 또한 예방합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이러한 물류 시스템은 올리브영이 고객에게 최고의 구매 경험을 제공하는 데 중요한 역할을 합니다.&lt;br&gt;
아래는 전체적인 물류 시스템 구성도입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1613px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e48bab407c5e1ced99c7a6fa9b3cbeab/d093b/logistics-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 93.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABibAAAYmwFJdYOUAAAC2UlEQVR42n1U3WsTQRC/P1TffPFFUBD6oIKCFvsgRa1gQYRWa6n6YKnFklYotJXa2AbbpjTJpUmar8v3x93t7e3t/dzbvVySkrpw7OzuzG9mfjNzGin6uPxbw0WijOJxHWaewbMApwr0chSZeEG8GyAFH27Dh98HcocVpA/y4gxwcW6mLJSOG2A1QOMUIH0Ku09ATArPEUZcKIp722IwezYci8IXZ9+DeBDOhF6vbUdnRjxQ2xWCAMSV5Yc7YRwryS7yXVfd++oLVqvFsRmz4br+VfNxQM65/Hxh6VAXf/QKGl1TuvHEvedxqWfbDKnzZuRgIiCXBp4EY4wJ7y486oCJ3RVn5UjpKMdKdyLg4L7ZMVE0WmGKPkbf6q0+KvWOlK2+hXy6KB1PBAzSCdbrDxu4/fj9GOAgzZl3a3gw+1nKZ4k0Ht58KgpmjRMfADI2DL3V6aFsNCJAPlIIo9FGpdYMOSQoFSqRs2AFOFwYaEPjCQyHb6NcSd0wo0EBx1L+sXOMtqwkoBcM7B2lo1RTZYJmX7VN/PQCe4lMGG0HP/eToC6LADf3TqBf1qDduD+HM70kL5fWdnF3elHKjlBej9eRKipnc0sxTM+vSvkwmcWdZwuwbEcSGER67/lHfN9OQHMcIlshWFS0iWWZUfimacr2CVawU0pDmYEQZyzlwa5N4mm0N69ydR2HCmMEcKA0UAgqHM+YMDoqwrXtI3yNHUg5U6hidnEDxHH/Nyl+OAFh/4l9K1FHzlAULK//wtuVLSmfpPKSa5vQYTaeila7rj0kV9QSiix857IAgYoaUR5lNWH0FFCuVEcyrHjQl7UeB6HKKFus4yxbUY1tEeinuTHA/Pkl2mI0tVHyF1d3MPViWRkJfr79biBbteX5zacYHr38IuX0SRZPbs3ImR7Yv5qax+76/niEnW4PlWpt6LVoiNZRgI1mC+XwjToUekrH6P/LEj9i5jL8AxnSrqLCqoOlAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e48bab407c5e1ced99c7a6fa9b3cbeab/263a4/logistics-2.webp 480w,
/static/e48bab407c5e1ced99c7a6fa9b3cbeab/a6361/logistics-2.webp 960w,
/static/e48bab407c5e1ced99c7a6fa9b3cbeab/bf288/logistics-2.webp 1613w&quot; sizes=&quot;(max-width: 1613px) 100vw, 1613px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e48bab407c5e1ced99c7a6fa9b3cbeab/9aebd/logistics-2.png 480w,
/static/e48bab407c5e1ced99c7a6fa9b3cbeab/a91f8/logistics-2.png 960w,
/static/e48bab407c5e1ced99c7a6fa9b3cbeab/d093b/logistics-2.png 1613w&quot; sizes=&quot;(max-width: 1613px) 100vw, 1613px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e48bab407c5e1ced99c7a6fa9b3cbeab/d093b/logistics-2.png&quot; alt=&quot;물류 시스템 구성도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;물류 시스템 구성도&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;p&gt;그럼 이제 각 시스템을 순서대로 살펴볼까요?&lt;br&gt;
시스템이 어떻게 개선되었고 어떤 성과를 냈는지, 또 고객 경험과 운영 효율성에 어떤 영향을 주었는지 함께 알아보겠습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;inventory-api-구축--실시간-재고로-고객-만족도-극대화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#inventory-api-%EA%B5%AC%EC%B6%95--%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9E%AC%EA%B3%A0%EB%A1%9C-%EA%B3%A0%EA%B0%9D-%EB%A7%8C%EC%A1%B1%EB%8F%84-%EA%B7%B9%EB%8C%80%ED%99%94&quot; aria-label=&quot;inventory api 구축  실시간 재고로 고객 만족도 극대화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Inventory API 구축 : 실시간 재고로 고객 만족도 극대화&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;올리브영은 &lt;b&gt;온라인과 오프라인 서비스를 모두 제공하는 O2O(Online to Offline) 기업의 대표 주자&lt;/b&gt;입니다.
특히 온라인몰에서 주문하면 가까운 매장과 MFC에서 당일 배송하는 &lt;strong&gt;오늘드림은 올리브영의 핵심 O2O 서비스&lt;/strong&gt;입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;다양한 O2O 서비스를 원활히 제공하려면 온/오프라인의 정확한 재고를 모두 파악하는 것이 필수적입니다.
하지만 기존에는 &lt;strong&gt;재고 데이터가 온/오프라인 각각의 시스템으로 분리&lt;/strong&gt;되어 있었습니다. 이로 인해 사용성이 떨어지고, 레거시(오래된 시스템) 성능 이슈도 빈번했습니다.
또한 배치와 EAI를 통한 일괄 재고 반영 방식은 처리 지연과 잦은 오류로 이어져 재고 데이터의 정확성(정합성)을 떨어뜨렸습니다. 이는 결국  고객 경험 악화로 직결되었습니다.
예를 들어, 온라인몰에서 주문했는데 재고가 없어서 취소되거나, 매장 재고를 확인하고 픽업을 갔는데 막상 매장에는 재고가 없는 경우가 비일비재했습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이러한 문제를 해결하고자 올리브영은 &lt;strong&gt;온/오프라인 재고를 통합하고 시스템을 모던화하는 프로젝트&lt;/strong&gt;를 진행했습니다. Amazon MemoryDB로 재고 전용 데이터베이스를 구축해서 안정성을 높였고, Amazon MSK(Managed Streaming for Apache Kafka)로 메시지 기반의 실시간 재고 연동을 구현했습니다.
그 결과, 올리브영의 모든 서비스에서 빠르고 간편하며 안정적인 &lt;strong&gt;Inventory API를 통해서 재고 조회가 가능&lt;/strong&gt;해졌습니다.
이 시스템은 온라인몰, 오늘드림, 검색, 매장, MFC, 전자라벨, PDA, POS, 백오피스, 파트너오피스 등 주요 서비스에 모두 적용되었습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;실시간 재고 조회는 고객 경험을 크게 향상&lt;/strong&gt;시켰습니다. 우선 고객이 주문 후에 재고가 부족해서 취소되는 &lt;strong&gt;결품률을 이전 대비 50% 이상&lt;/strong&gt; 낮췄습니다.
또한 고객은 언제든 방문할 매장의 정확한 재고 현황을 확인할 수 있고, 필요한 경우 원하는 상품을 미리 픽업할 수도 있습니다. 이러한 &lt;strong&gt;고객 경험 향상은 곧 매출 증가&lt;/strong&gt;로 이어집니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;내부 사용자들도 큰 변화를 체감하고 있습니다. &lt;strong&gt;통합 대시보드(OpenSearch/Datadog)를 통해 실시간 및 히스토리 재고 데이터를 간편하게 조회&lt;/strong&gt;할 수 있습니다.
기존에는 재고 문제가 발생했을 때 원인 추적에 오랜 시간이 걸렸습니다. 심지어 원인을 찾지 못하고 넘어가는 경우도 다반사였습니다. 이제는 누구나 대시보드를 통해 재고 데이터에 접근할 수 있고 활용할 수 있습니다.
특히, 재고 이슈 발생 시 &lt;strong&gt;5단계 이상의 복잡한 확인 절차를 2단계로 축소&lt;/strong&gt;하여 빠른 원인 분석 및 대응이 가능해졌습니다. 또한 수억 건으로 파편화된 데이터 탓에 &lt;strong&gt;10분 이상 걸리던 대용량 재고 조회 시간을 평균 1~3초로 대폭 개선&lt;/strong&gt;했습니다.
이처럼 통합 대시보드를 통해 전반적인 &lt;strong&gt;재고 운영 효율이 대폭 향상&lt;/strong&gt;되었습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;신규 재고 시스템 구축&lt;/strong&gt;에 대한 더 자세한 이야기는 &lt;a href=&quot;https://oliveyoung.tech/2023-10-04/inventory-project/&quot;&gt;이전 블로그 글&lt;/a&gt;에서 확인하실 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1627px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ffcfa459e9cfc8e26a00ac3674bb95e8/671b0/logistics-5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 38.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABibAAAYmwFJdYOUAAABPElEQVR42m1S2U7DMBDM//8XvPLCA0UIgZqERHVw0thenxnWdk/UlVY+9poZu9m2DWe/tcvdg9h/S2aGlx225NF0fY95XmCtuyakVDw3E8bioA0nx7tht7l6/IDYPQF2QXMQAqvSsM5fUMWY0CkC+YAjDzpyLMV4ie/kgLffoTQrJKLFRjNvEhrPRdY5hFgRBD77EDCuqtxPjFBoKg1zgxgi3uWIV9kjZhZskhl+fu2hDaFRjG5VCu6E0HvPRQGK7/KeuIHKCE/Fj2xeFrRtC7K2IgyhOgrdyGgTiAy0XmB4qiXieKWcEg9VAyL7WYKUgTDBrGyT4R7EVLoXTVCLxklgP35jmmShlNFm2rmxGl5gfp5ZssDOj2IDemngAmuYX5fIFt2uL7cVffJaUaX7r5OIxV4Lo3Lk1cWa+we0InHyGQYmSQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ffcfa459e9cfc8e26a00ac3674bb95e8/263a4/logistics-5.webp 480w,
/static/ffcfa459e9cfc8e26a00ac3674bb95e8/a6361/logistics-5.webp 960w,
/static/ffcfa459e9cfc8e26a00ac3674bb95e8/e7e3c/logistics-5.webp 1627w&quot; sizes=&quot;(max-width: 1627px) 100vw, 1627px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ffcfa459e9cfc8e26a00ac3674bb95e8/9aebd/logistics-5.png 480w,
/static/ffcfa459e9cfc8e26a00ac3674bb95e8/a91f8/logistics-5.png 960w,
/static/ffcfa459e9cfc8e26a00ac3674bb95e8/671b0/logistics-5.png 1627w&quot; sizes=&quot;(max-width: 1627px) 100vw, 1627px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ffcfa459e9cfc8e26a00ac3674bb95e8/671b0/logistics-5.png&quot; alt=&quot;재고 통합 시스템 아키텍처 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;재고 통합 시스템 아키텍처 다이어그램&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h2 id=&quot;oms-order-management-system-구축--주문부터-배송까지-seamless-연결&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#oms-order-management-system-%EA%B5%AC%EC%B6%95--%EC%A3%BC%EB%AC%B8%EB%B6%80%ED%84%B0-%EB%B0%B0%EC%86%A1%EA%B9%8C%EC%A7%80-seamless-%EC%97%B0%EA%B2%B0&quot; aria-label=&quot;oms order management system 구축  주문부터 배송까지 seamless 연결 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OMS (Order Management System) 구축 : 주문부터 배송까지 Seamless 연결&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;기존 올리브영의 물류 시스템은 EAI(Enterprise Application Integration) 방식의 데이터 연동으로 잦은 오류와 지연 처리, 그로 인해 빈번한 시스템 장애를 발생시켰습니다.
이러한 문제를 해결하고 운영 안정화 및 효율성을 높이기 위해, &lt;b&gt;메시지 기반의 데이터 파이프라인으로 구성된 OMS를 구축&lt;/b&gt;했습니다. 이제 물류센터의 주요 데이터는 안정적으로 &lt;strong&gt;준실시간 연동&lt;/strong&gt;됩니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;그럼 물류 데이터의 실시간 연동이 왜 필요할까요? 바로 물류센터 업무 효율화와 재고 데이터 정확성(정합성) 향상이 가능하기 때문입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;물류센터-업무-효율화--물류-공정-시간-단축으로-고객-배송-만족도-향상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%BC%EB%A5%98%EC%84%BC%ED%84%B0-%EC%97%85%EB%AC%B4-%ED%9A%A8%EC%9C%A8%ED%99%94--%EB%AC%BC%EB%A5%98-%EA%B3%B5%EC%A0%95-%EC%8B%9C%EA%B0%84-%EB%8B%A8%EC%B6%95%EC%9C%BC%EB%A1%9C-%EA%B3%A0%EA%B0%9D-%EB%B0%B0%EC%86%A1-%EB%A7%8C%EC%A1%B1%EB%8F%84-%ED%96%A5%EC%83%81&quot; aria-label=&quot;물류센터 업무 효율화  물류 공정 시간 단축으로 고객 배송 만족도 향상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;물류센터 업무 효율화 : 물류 공정 시간 단축으로 고객 배송 만족도 향상&lt;/h3&gt;
&lt;p&gt;온라인몰에서 주문이 들어오거나 매장에서 발주가 이루어지면 해당 데이터는 물류센터(WMS)로 전달됩니다.
그럼 물류센터는 출고 작업을 비롯한 현장 처리 정보를 다시 올리브영(OMS)에 전달합니다. 이외에도 올리브영(OMS)과 물류센터(WMS)는 수많은 데이터를 주고받습니다.&lt;br&gt;
그런데 데이터 연동이 30분마다 된다면? 데이터 처리에 잦은 지연과 오류가 발생한다면? 당연히 물류센터 현장 작업이 지연되고 고객 배송 또한 늦어질 수밖에 없습니다.&lt;br&gt;
올리브영은 시스템 개편을 통해 &lt;strong&gt;물류 전체 공정 시간을 70% 이상 절감&lt;/strong&gt;했으며, 특히 주문량이 폭발적으로 증가하는 &lt;strong&gt;올영세일 기간에도 센터 배송 리드타임을 3일 이내로 단축&lt;/strong&gt;했습니다.
매 분기 진행되는 올영세일은 그동안 엄청난 주문량 때문에 센터 배송 지연이 아쉬웠지만, 이번 개편으로 고객 만족도를 크게 높일 수 있었습니다. 물론 세일 기간에도 오늘드림은 변함없이 빠른 배송을 자랑합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;재고-데이터-정합성-향상--온라인몰-매출-증가로-이어지는-재고-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%AC%EA%B3%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%95%ED%95%A9%EC%84%B1-%ED%96%A5%EC%83%81--%EC%98%A8%EB%9D%BC%EC%9D%B8%EB%AA%B0-%EB%A7%A4%EC%B6%9C-%EC%A6%9D%EA%B0%80%EB%A1%9C-%EC%9D%B4%EC%96%B4%EC%A7%80%EB%8A%94-%EC%9E%AC%EA%B3%A0-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;재고 데이터 정합성 향상  온라인몰 매출 증가로 이어지는 재고 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;재고 데이터 정합성 향상 : 온라인몰 매출 증가로 이어지는 재고 관리&lt;/h3&gt;
&lt;p&gt;물류센터의 재고를 실시간으로 파악할 수 있게 되면서, 온라인몰에서 판매 기회가 크게 증가했습니다. 동시에 주문 후 재고 부족으로 인해 주문이 취소되는 결품률 또한 대폭 줄었습니다.
이처럼 &lt;strong&gt;재고 정합성 향상은 곧 온라인몰 매출 증가와 고객 경험 향상&lt;/strong&gt;으로 이어집니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;올리브영은 &lt;strong&gt;OMS 구축을 통해 물류센터와의 데이터 연동을 개선하고, 물류 프로세스 효율화&lt;/strong&gt;를 이끌어내고 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1677px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c105b9413f31dcfbbcf604c7656153ac/30723/logistics-3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABibAAAYmwFJdYOUAAABJklEQVR42k1Ra0/DMAzs//9T+4CQGAKBpk68hBgvdS1NH0maps/DlykwS5adnH05O8lt+orNNoXrDLquwzAMWNcV0ZjTl2UJfm6xLuKMSfp8wPX9E3rXCaFD3/d/JLSLmwd8ZT/StcD7AfM8S3rCPl4+sdvuMc4jpnEKYhJrDapKYWKhkJSl5NMUNWBztcf7dx5ObPjHgLfHA+4ud+h9LwrXE2HbtqjrGozGmJAfjznyopCHajlXcq+FaIZzDkpVyPMCqqzQNA0a3chUHuM4hpUlWmsU0mytDeOQUGsTmr330pyHNdB4trYLkWpYq2SiuB5yBIVZlgUCLpavxuUzKqXAR0nKWhJF431ZlmEy9jMmlEkgOoHzXyQBnapIev7THJNOnBjzX26THM2fffIjAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c105b9413f31dcfbbcf604c7656153ac/263a4/logistics-3.webp 480w,
/static/c105b9413f31dcfbbcf604c7656153ac/a6361/logistics-3.webp 960w,
/static/c105b9413f31dcfbbcf604c7656153ac/4c7b2/logistics-3.webp 1677w&quot; sizes=&quot;(max-width: 1677px) 100vw, 1677px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c105b9413f31dcfbbcf604c7656153ac/9aebd/logistics-3.png 480w,
/static/c105b9413f31dcfbbcf604c7656153ac/a91f8/logistics-3.png 960w,
/static/c105b9413f31dcfbbcf604c7656153ac/30723/logistics-3.png 1677w&quot; sizes=&quot;(max-width: 1677px) 100vw, 1677px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c105b9413f31dcfbbcf604c7656153ac/30723/logistics-3.png&quot; alt=&quot;OMS 시스템 아키텍처 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;OMS 시스템 아키텍처 다이어그램&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h2 id=&quot;wms-warehouse-management-system-구축--wms-내재화로-글로벌-물류-경쟁력-강화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#wms-warehouse-management-system-%EA%B5%AC%EC%B6%95--wms-%EB%82%B4%EC%9E%AC%ED%99%94%EB%A1%9C-%EA%B8%80%EB%A1%9C%EB%B2%8C-%EB%AC%BC%EB%A5%98-%EA%B2%BD%EC%9F%81%EB%A0%A5-%EA%B0%95%ED%99%94&quot; aria-label=&quot;wms warehouse management system 구축  wms 내재화로 글로벌 물류 경쟁력 강화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;WMS (Warehouse Management System) 구축 : WMS 내재화로 글로벌 물류 경쟁력 강화&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;커머스 서비스의 성장은 물류 역량 강화와 떼려야 뗄 수 없는 관계에 있습니다. 그중에서도 &lt;b&gt;WMS(창고 관리 시스템)는 물류의 핵심 시스템&lt;/b&gt;입니다.&lt;br&gt;
지금까지 올리브영은 외부 WMS에 의존했습니다. 외부 시스템은 도입이 간편한 대신 기능 확장과 프로세스 개선에 한계가 분명합니다.
&lt;strong&gt;올리브영의 성장과 물류 도메인 확장을 위해 WMS 내재화가 필수&lt;/strong&gt;적인 시점입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;글로벌-wms-내재화--성장의-첫-번째-목표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%80%EB%A1%9C%EB%B2%8C-wms-%EB%82%B4%EC%9E%AC%ED%99%94--%EC%84%B1%EC%9E%A5%EC%9D%98-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EB%AA%A9%ED%91%9C&quot; aria-label=&quot;글로벌 wms 내재화  성장의 첫 번째 목표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;글로벌 WMS 내재화 : 성장의 첫 번째 목표&lt;/h3&gt;
&lt;p&gt;올리브영은 WMS 내재화의 첫 번째 목표를 &lt;strong&gt;글로벌 WMS 구축&lt;/strong&gt;으로 정했습니다. 혹시 올리브영에 글로벌몰이 있다는 사실, 알고 계셨나요? 글로벌몰은 해외 고객이 주문하면 한국에서 직접 해외로 배송해 주는 서비스입니다.
기존 글로벌몰 WMS는 외부 시스템에 의존하고 있어, 급변하는 시장 상황에 신속히 대응하기 어려웠습니다. 그 결과, 운영의 비효율성과 물류 생산성 저하가 발생했습니다.
또한 물류센터 내 동선 최적화나 신규 장비 도입과 같은 효율화 작업이 외부 시스템의 제약으로 인해 지연되거나 무산되는 경우도 있었습니다.
반복적인 외부 시스템 투자에도 불구하고 비용은 계속 증가하고, 산출물은 지속적으로 소실되면서 &lt;strong&gt;서비스 자산화의 필요성&lt;/strong&gt;이 시급한 과제로 떠올랐습니다.
올리브영의 넥스트 플랜은 글로벌입니다. 글로벌몰 성장뿐 아니라  미국, 일본 등 글로벌 비즈니스를 확장하고 있는 만큼, &lt;strong&gt;강력한 물류 시스템이 뒷받침&lt;/strong&gt;되어야 합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;올리브영은 &lt;strong&gt;자체 WMS를 성공적으로 구축&lt;/strong&gt;하며, 전사 비즈니스 전략에 맞춘 &lt;strong&gt;확장 가능한 물류 플랫폼을 마련&lt;/strong&gt;했습니다.
이번 시스템 구축으로 주문할당, 피킹지시 등 &lt;strong&gt;주요 공정 시간이 평균 80% 절감&lt;/strong&gt;되었고, 출고 조회를 비롯한 &lt;strong&gt;WMS 주요 기능의 성능이 90% 이상 개선&lt;/strong&gt;되는 놀라운 성과를 이루었습니다.
그 결과, 글로벌 WMS 오픈 이후 처음 진행된 올영세일에서 &lt;strong&gt;출고량이 기존 대비 약 50% 증가하며 역대 최고치를 경신&lt;/strong&gt;했습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이번 올리브영 WMS 구축을 기반으로, 앞으로는 사용 중인 외부 물류 시스템들을 순차적으로 내재화할 계획입니다. 또한 그동안 시스템 제약으로 시도할 수 없었던 고도화 과제들도 하나씩 수행해 나가고 있습니다.
이처럼 물류 시스템 내재화는 올리브영의 물류 서비스 경쟁력을 강화하고, 궁극적으로 &lt;strong&gt;올리브영의 미래 성장을 위한 든든한 밑거름&lt;/strong&gt;이 됩니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1694px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/00b85135dd3cbd30c0229d7b925c183e/cc39a/logistics-4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABibAAAYmwFJdYOUAAABIklEQVR42l2R62rDMAyF8/4vtQ3KGHR0bJTd/owldHGaxE3i2M79zEfMUCYQlo/sT5KdHD9T3B8+4J2FtQ7TNGHbNkSLMddr/b/FfPL8/oXd4yucNeitxTAMWNdVksuy4ObhiFN+xuAd5nmWgszTVFZgf3uA+8uN44jE9j10XYkQaqDWWmIagXf7N5xUCRcmYBFeisA8VXjavQiQGptJuq6D1he0bQtjDJqmwXeaIc0ynMsSpmtFY2csVNU1fvIcqihE1xeNPjRFGFcBFiFJGDviIWN6jAHgvZecUgp1APEMnV0S3rYd8gCPXdvwZAKkE0aRwDgSNR2egF5VlUB5KRrjMkzBzpxzUixhwAQ3BPuwv/45Vue40VkkGmNqPMORGf8Ctc8cC66IkIkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/00b85135dd3cbd30c0229d7b925c183e/263a4/logistics-4.webp 480w,
/static/00b85135dd3cbd30c0229d7b925c183e/a6361/logistics-4.webp 960w,
/static/00b85135dd3cbd30c0229d7b925c183e/ea9c0/logistics-4.webp 1694w&quot; sizes=&quot;(max-width: 1694px) 100vw, 1694px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/00b85135dd3cbd30c0229d7b925c183e/9aebd/logistics-4.png 480w,
/static/00b85135dd3cbd30c0229d7b925c183e/a91f8/logistics-4.png 960w,
/static/00b85135dd3cbd30c0229d7b925c183e/cc39a/logistics-4.png 1694w&quot; sizes=&quot;(max-width: 1694px) 100vw, 1694px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/00b85135dd3cbd30c0229d7b925c183e/cc39a/logistics-4.png&quot; alt=&quot;글로벌 WMS 시스템 아키텍처 다이어그램&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;글로벌 WMS 시스템 아키텍처 다이어그램&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h2 id=&quot;배송최적화-시스템-구축--빠르고-정확한-고객-맞춤-배송&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%EC%86%A1%EC%B5%9C%EC%A0%81%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95--%EB%B9%A0%EB%A5%B4%EA%B3%A0-%EC%A0%95%ED%99%95%ED%95%9C-%EA%B3%A0%EA%B0%9D-%EB%A7%9E%EC%B6%A4-%EB%B0%B0%EC%86%A1&quot; aria-label=&quot;배송최적화 시스템 구축  빠르고 정확한 고객 맞춤 배송 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배송최적화 시스템 구축 : 빠르고 정확한 고객 맞춤 배송&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;보통 온라인에서 구매하면 센터 배송으로 고객에게 전달되지만, &lt;strong&gt;올리브영의 배송은 계속해서 다각화&lt;/strong&gt;되고 있습니다.
물류센터는 기존 양지센터에 더해 경산/안성 센터가 추가되었고, 오늘드림은 매장에서 시작했지만 지금은 MFC에서 더 많은 배송이 이루어지고 있습니다.
물류 시스템이 다각화되고 규모가 커진 만큼 배송도 최적화할 필요가 생겼습니다. 예전처럼 주문하면 센터에서 배송되는 단순한 구조가 아니기 때문입니다.
그래서 올리브영은 &lt;strong&gt;고객이 빠르게 배송받을 수 있는 최적화 시스템을 구축&lt;/strong&gt;하고 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;배송최적화 시스템의 원리는 간단합니다. &lt;strong&gt;고객이 필요한 상품을 적시에 제공하고 빠르게 배송하는 시스템&lt;/strong&gt;입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;예를 들어, 고객 A가 3개의 상품을 구매하고 싶을 때 2개 상품은 1번 물류센터에 재고가 있지만 1개 상품은 재고가 없다고 가정해 보겠습니다.
현재는 주문이 성립하지 않거나 주문 후 결품으로 취소 처리되어 &lt;strong&gt;고객에게 좋지 않은 경험&lt;/strong&gt;을 제공하게 됩니다.&lt;br&gt;
만약 1번 물류센터에 재고가 없더라도 2번 물류센터에 재고가 있다면.. 아니면 인근 매장이나 MFC에 재고가 있다면..&lt;br&gt;
현재는 다른 곳에 재고가 있어도 팔지 못해 &lt;strong&gt;판매 기회 손실이 수시로 발생&lt;/strong&gt;합니다. 현실적으로 모든 센터에 모든 상품 재고를 보관할 수는 없습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;또한 물류센터 CAPA(Capacity, 물류 처리 용량)를 효율적으로 관리하지 못하면 &lt;strong&gt;수시로 배송 리드타임 지연&lt;/strong&gt;이 발생할 수 있습니다.
예를 들어 1번 물류센터 CAPA가 100건인데 150건의 주문이 들어오면, 50건은 배송이 지연될 수밖에 없습니다. 하지만 2번 물류센터 CAPA에 여유가 있다면, 당연히 2번 물류센터에서 주문을 처리하면 배송 리드타임을 단축할 수 있습니다.
현재도 센터별로 CAPA 관리를 하고 있지만, 데이터를 기반으로 더욱 체계화되고 효율적인 프로세스 구축이 필요합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;배송최적화 시스템이 구축되면 고객은 전혀 다른 경험을 하게 됩니다. &lt;strong&gt;재고를 보유한 가까운 곳에서 고객에게 신속히 배송&lt;/strong&gt;합니다.
또한 주문 상품을 분할해서 재고 보유를 기준으로 &lt;strong&gt;여러 곳에서 분리 배송&lt;/strong&gt;도 가능해집니다. &lt;strong&gt;고객에게 판매 기회는 증가하고 배송과 재고로 인한 주문 취소는 감소&lt;/strong&gt;하게 됩니다.
배송최적화 시스템 구축은 이제 막 첫걸음을 떼었습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/90d637f886e4b81b51e6e371cec33ff8/13ce4/logistics-7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABibAAAYmwFJdYOUAAABoUlEQVR42oWS7WvUQBDG8///DQVBKFjBD1XxS4sHV6qnrXi0ldrzTlvvcm952SSbbJLdTX5u4glVWhwYdmdnd+Z5nlnP1ilV1TjXlKVG2xYLZKpGaYML0dr2eWNapCxYbTZY26BrS127O+7SchuiqgovnQ8RGcSRREQFoTQMb255Pjzj8OySpSjIkoogSMld7vpmxuHrNxSFJo4ViShZrAQHRyeMv07x2Fnbtv26kYong488PfnEy/MvjO9W/bnd5WutUUo5VPbPU3JVMv3pI1KJZ+XcdVMOhUDJFD8S7B2PeHb6mf3TMePJFOnP2Hy/Jlu4fZqwDmOapun9X/P08hzjuNdSYJwnWcalHzG4mHCxCFmLhEKEfaGmLpnd3vHqaIAuC8R67oBEtNb0iDuWHg+YMZY8z91qOjHIow1pHP7OuYeqrOgU6AqUmUBGWz5cTZivA1dwp83/TDkkWbAiSVPm/orMMUmjgDIJWPoLXrx9z9W3Hw8jvD+kXUAftYZ3oxF7+wds10sngerpdlpq98UepfxIh91EFWEc/93w3lB+AYSmsBx6wDBLAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/90d637f886e4b81b51e6e371cec33ff8/263a4/logistics-7.webp 480w,
/static/90d637f886e4b81b51e6e371cec33ff8/a6361/logistics-7.webp 960w,
/static/90d637f886e4b81b51e6e371cec33ff8/0b34d/logistics-7.webp 1920w,
/static/90d637f886e4b81b51e6e371cec33ff8/30e03/logistics-7.webp 2006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/90d637f886e4b81b51e6e371cec33ff8/9aebd/logistics-7.png 480w,
/static/90d637f886e4b81b51e6e371cec33ff8/a91f8/logistics-7.png 960w,
/static/90d637f886e4b81b51e6e371cec33ff8/ac7a9/logistics-7.png 1920w,
/static/90d637f886e4b81b51e6e371cec33ff8/13ce4/logistics-7.png 2006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/90d637f886e4b81b51e6e371cec33ff8/ac7a9/logistics-7.png&quot; alt=&quot;배송최적화 프로세스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;배송최적화 프로세스&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h2 id=&quot;자동발주-시스템-구축--효율적인-재고-관리의-시작&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EB%8F%99%EB%B0%9C%EC%A3%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95--%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EC%9E%AC%EA%B3%A0-%EA%B4%80%EB%A6%AC%EC%9D%98-%EC%8B%9C%EC%9E%91&quot; aria-label=&quot;자동발주 시스템 구축  효율적인 재고 관리의 시작 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;자동발주 시스템 구축 : 효율적인 재고 관리의 시작&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;그동안 올리브영의 발주는 엑셀을 통한 수기 계산으로 이루어졌습니다. 발주 담당자들이 기준 데이터를 모아 엑셀에 입력하면 계산되는 방식입니다.
물류 서비스가 확장될수록 담당자 업무가 계속 늘어날 수밖에 없었고, 발주 데이터가 많아질수록 정확도 역시 떨어질 수밖에 없는 구조였습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;발주 정확도 향상과 운영 효율화를 위해 &lt;strong&gt;자동발주 시스템&lt;/strong&gt; 구축이 시급한 상황입니다. 몇 년 전 매장 자동발주 시스템을 우선 구축하여 발주 운영 효율을 개선했지만, 레거시 시스템 기반이라 기능 확장에 제약이 있었습니다.
올리브영의 최종 목표는 매장을 넘어 MFC, 온라인 등 전체 시스템의 통합 자동발주입니다. 이를 위해 기존 수기 계산 중인 개별 시스템을 모던 아키텍처로 전환하는 작업이 필요합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;첫 번째로 &lt;strong&gt;MFC 자동발주 전환&lt;/strong&gt;을 진행하고 있습니다. 수기로 계산 중인 MFC 발주 업무를 전면 자동화합니다.&lt;br&gt;
올리브영의 오늘드림 배송은 어떻게 그렇게 빠를까요? 바로 &lt;b&gt;MFC(Micro Fulfillment Center)&lt;/b&gt; 덕분입니다. 일반 물류센터가 외곽에 있는 반면, 도심형 소규모 물류센터인 MFC는 고객 가까이 위치해서 빠른 배송을 가능하게 합니다.
올리브영은 물류 서비스 경쟁력을 강화하고 퀵커머스를 확대하기 위해 전국 주요 도심에 MFC를 확장하고 있습니다. 실제로 고객 경험이 좋은 &lt;strong&gt;오늘드림의 매출은 꾸준히 상승 중&lt;/strong&gt;이며, 그에 따라 MFC 발주의 최적화 필요성은 점점 커지고 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;올리브영은 자동발주 시스템 구축을 통해 &lt;strong&gt;효율적인 재고 관리와 판매 기회 증가&lt;/strong&gt;를 목표로 합니다. 고객이 필요할 때 해당 상품을 발주해서 재고를 확보하는 것이 핵심입니다.
또한 발주 담당자들의 업무를 경감하고, 과잉 재고로 인한 비용을 최소화할 수 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;나아가 자동발주에 더해 &lt;strong&gt;수요예측을 준비&lt;/strong&gt;하고 있습니다. 최적의 발주는 다양한 조건 고려가 필수입니다.&lt;br&gt;
주요 발주 조건은 판매량/잔여재고/프로모션/상품특성/브랜드/카테고리/요일/지역/날씨/계절/고객성향/구매시간/POG/매장특성/온라인판매여부... 등이 있는데,
이 모든 조건을 사람이 선별하고 계산해서 발주에 반영하는 것은 불가능에 가깝습니다. 그렇기 때문에 저희는 올리브영만의 다양한 데이터를 수집하고 수요예측 모델링을 진행해서 &lt;strong&gt;발주 정확도&lt;/strong&gt;를 더욱 높일 계획입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1447px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/859bb6bdceefcefe4b18788ee6ddadc5/ef92a/logistics-6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.791666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABibAAAYmwFJdYOUAAAB+ElEQVR42qVSXWsTURDdf+UP8cUfIfjqgw9a8AN8EEUUVEQrWInBGqSFglQT0gS3tim2dVM3lE2blmh2N/ud/T7O3E3iFtQHHRju7r13zpw550qgyPIcWZbhXyKlup52hJ19BabtQOLNnAA5+qc6Lt+pIAgjbGx/w+2nq8inhbM7s2/XdeF5Hlw/wPNXVdx//AzqoVYA1j93sdrYwfHQwMXri/CCEB9lBdceLJ9hw1MwyHg8prSIXYokTXF4NIB2fAI3CArAJ9U6rtytzouSJEVOaz6VIaNCx3Ghj3TB7G/ySKwfd+FLnHEc49QwsN7VoA6/I4pCwSgIJr8BIu1JiTglEnSW0o/0cqWFhYdvC4GnwO3dL1i4eQtrzTp8GsOyLNi2LcY0TZPYOmKPV45GW0altoKEyElbXzW8b++Xxo2x2VWw/GaJjJHJoFCMyc3YDF7LybF30MPG5jYiYio0bHVU1Na35oPopoGT0Q+YlimaMBODZGBDym4Xjv96ATy+AFysNXHj0Tux2esPhaauP8FgaM4LkyQRwCMyxikZw2DsQz5NqdCuOOwofZy7cBW2G+D1mozzl+6dYTLT2fd96LoB3/PF3oHaQ+uTLIhI5cv8oDuKJjrpYxd76uCPz6PQOxFv8UOjiRdLFXjUSMJ/Bk83IeM4wyjCT92ojVOx7DEZAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/859bb6bdceefcefe4b18788ee6ddadc5/263a4/logistics-6.webp 480w,
/static/859bb6bdceefcefe4b18788ee6ddadc5/a6361/logistics-6.webp 960w,
/static/859bb6bdceefcefe4b18788ee6ddadc5/a8f0f/logistics-6.webp 1447w&quot; sizes=&quot;(max-width: 1447px) 100vw, 1447px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/859bb6bdceefcefe4b18788ee6ddadc5/9aebd/logistics-6.png 480w,
/static/859bb6bdceefcefe4b18788ee6ddadc5/a91f8/logistics-6.png 960w,
/static/859bb6bdceefcefe4b18788ee6ddadc5/ef92a/logistics-6.png 1447w&quot; sizes=&quot;(max-width: 1447px) 100vw, 1447px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/859bb6bdceefcefe4b18788ee6ddadc5/ef92a/logistics-6.png&quot; alt=&quot;자동발주 프로세스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;자동발주 프로세스&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;&lt;br&gt;
&lt;h2 id=&quot;마치며--진화를-멈추지-않는-올리브영-물류-시스템&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0--%EC%A7%84%ED%99%94%EB%A5%BC-%EB%A9%88%EC%B6%94%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%AC%BC%EB%A5%98-%EC%8B%9C%EC%8A%A4%ED%85%9C&quot; aria-label=&quot;마치며  진화를 멈추지 않는 올리브영 물류 시스템 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며 : 진화를 멈추지 않는 올리브영 물류 시스템&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;오늘은 올리브영의 물류 시스템 전반을 이야기했습니다.&lt;br&gt;
물류 시스템은 PHASE 1을 넘어 PHASE 2가 진행 중입니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHASE 1&lt;/strong&gt;에서는 레거시 시스템의 모던화와 분산형 기술(MSA, AWS, Kafka, OpenSearch, Redis, Datadog 등)에 집중했습니다.&lt;br&gt;
이를 통해 올리브영의 &lt;strong&gt;기술 내실을 다지고 비즈니스 확장에 기여&lt;/strong&gt;했습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHASE 2&lt;/strong&gt;에서는 AI를 중심으로 한 생성형 및 추론형 기술에 집중합니다.&lt;br&gt;
이미 구축된 시스템과 데이터를 기반으로 &lt;strong&gt;효율화와 최적화를 구현&lt;/strong&gt;합니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이러한 노력은 올리브영의 지속적인 비즈니스 확장과 차별화에 기여할 것입니다.&lt;br&gt;
올리브영의 성장과 함께 물류 시스템의 진화는 계속되고 있습니다.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;아직 할 이야기가 많지만, PHASE 2가 완성되면 또 찾아뵙겠습니다~!&lt;br&gt;
긴 글 읽어 주셔서 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[우리 스쿼드에서 같이 일하실래요?]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-07-30/squad-description/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-07-30/squad-description/</guid><pubDate>Wed, 30 Jul 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;반갑습니다! 오늘도 올리브영입니다!&lt;/p&gt;
&lt;p&gt;반갑습니다! 올리브영 백엔드 개발자 유롱롱입니다! 🐶&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;개발-조직의-스쿼드-운영-방식-어떻게-돌아가나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C-%EC%A1%B0%EC%A7%81%EC%9D%98-%EC%8A%A4%EC%BF%BC%EB%93%9C-%EC%9A%B4%EC%98%81-%EB%B0%A9%EC%8B%9D-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%8C%EC%95%84%EA%B0%80%EB%82%98%EC%9A%94&quot; aria-label=&quot;개발 조직의 스쿼드 운영 방식 어떻게 돌아가나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발 조직의 스쿼드 운영 방식, 어떻게 돌아가나요?&lt;/h1&gt;
&lt;p&gt;요즘 개발 조직에서는 빠르게 변화하는 요구에 유연하게 대응하기 위해 다양한 방식의 조직 구조를 도입하고 있습니다.&lt;/p&gt;
&lt;p&gt;그 중 하나가 바로 &apos;&lt;strong&gt;스쿼드(Squad)&lt;/strong&gt;&apos;라는 형태입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 스쿼드란 무엇인지부터 업무를 실제로 어떻게 운영하는지까지, 올리브영의 일하는 방식을 정리해보려 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-스쿼드란-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%8A%A4%EC%BF%BC%EB%93%9C%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;1 스쿼드란 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 스쿼드란 무엇인가요?&lt;/h2&gt;
&lt;p&gt;스쿼드는 다양한 역할의 팀원이 모여 하나의 기능이나 서비스를 중심으로 자율적으로 운영되는 작은 단위 조직입니다.&lt;/p&gt;
&lt;p&gt;기획자, 디자이너, 프론트엔드/백엔드 개발자, QA 등이 모두 한 팀 안에 포함되어 있어 빠른 의사결정과 협업이 가능하죠.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이 개념을 쉽게 게임에 빗대어 보자면, &lt;strong&gt;하나의 게임 파티&lt;/strong&gt;와 유사합니다.&lt;/p&gt;
&lt;p&gt;파티는 보통 특정 레이드를 공략하거나 목표를 위해 각기 다른 직업의 캐릭터들이 모여 협력합니다.&lt;/p&gt;
&lt;p&gt;전사, 힐러, 마법사처럼 역할은 다르지만, 같은 목표를 향해 함께 움직입니다. 스쿼드도 마찬가지입니다.&lt;/p&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;예시 그림 설명:&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c095d473b98309e90c1edf2916183c43/2eb59/squad.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAACWElEQVR42qVU32/SUBTeH+iTPs1nE018MJoZswddYuL2YLI5o3vQGYSIxsVBhixTmLAh5ddayPixjq0tLZQFCl1bWkppe5kXuvBrME38Hprbe853vu+ec9uZiynoDGFazszFdHS6Ja6JTyJbUpJUjwfW0OBbga/2N68jgw7o2QRwfZreeLcyt746l4nZu2RwGRquMkkZmPCJY9831x75P784Srgt8mRlq5imaRkiQ5QIK08QhJDfg0YDkijC17am505Syew+z/N9yoC8fxB0rD9z2BfxQt6qhWIpFEsahqEbwJeIur/cz4XvuXfes7zcY1m2e+Stn84fzkWPcymeRiz7iiKLomA5DMZ9iG/1GPnq2l7m5ZYlOVAm8unwzsdkeKvEUHADCtY4rlqttFotYAKKJjzfHLY3r7PZBOhcObOsNJFIFEumhHOBLlBFhmk0ZEVRikWmQJFaS0uns4HdX7Xa6JktQIVKpcKyJV3X6/UahmGhUAhFUZqmyyzbTVDVhiybpjl1VJcuGg2CII7zeRzHSZIUhPOxhAlkwzTVdhsuYJ9IkqBIkuM46F+SJChIHWLFE7wzcc5KU4l5X+W8j9GIH54fABPSoIVeSD3c27U9mHXM3xXK5bE5d28FTcbCK7fSSzcQ93NVUyVJhN2GLYAuDBO4Xj61z8/aHt5MeDf7F26gLIlnmHd578NCIbttgu4lgdDbba3VnepBwOt4cvvTwh369GiE3AcSCnpcG2yRGWuMtU5EfuO5zHBohMzV6lSBaTabV78BAABbPqvV+X/9GfwV/0X+A6JbWpN2yEgVAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c095d473b98309e90c1edf2916183c43/263a4/squad.webp 480w,
/static/c095d473b98309e90c1edf2916183c43/a6361/squad.webp 960w,
/static/c095d473b98309e90c1edf2916183c43/0b34d/squad.webp 1920w,
/static/c095d473b98309e90c1edf2916183c43/96d48/squad.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c095d473b98309e90c1edf2916183c43/9aebd/squad.png 480w,
/static/c095d473b98309e90c1edf2916183c43/a91f8/squad.png 960w,
/static/c095d473b98309e90c1edf2916183c43/ac7a9/squad.png 1920w,
/static/c095d473b98309e90c1edf2916183c43/2eb59/squad.png 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c095d473b98309e90c1edf2916183c43/ac7a9/squad.png&quot; alt=&quot;스쿼드 이해하기&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;스쿼드 이해하기&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-기능-조직-vs-목적-조직&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EA%B8%B0%EB%8A%A5-%EC%A1%B0%EC%A7%81-vs-%EB%AA%A9%EC%A0%81-%EC%A1%B0%EC%A7%81&quot; aria-label=&quot;2 기능 조직 vs 목적 조직 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 기능 조직 vs 목적 조직&lt;/h2&gt;
&lt;p&gt;전통적인 개발 조직은 기능 조직(Function-based)입니다. (&lt;em&gt;예: 프론트엔드 팀, 백엔드 팀, QA 팀.&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;스쿼드는 목적 조직(Purpose-based)이며, 하나의 기능을 중심으로 다양한 역할이 모여 있는 형태입니다.&lt;/p&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 20px 0;&quot;&gt;
  &lt;thead&gt;
    &lt;tr style=&quot;background-color: #a4d233; color: white;&quot;&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;기능 조직&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;목적 조직(스쿼드)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;구성 기준&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;기술/역할 기반&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;목적/기능 기반&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;커뮤니케이션&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;팀 간 협업 필요&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;팀 내 협업으로 신속함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;장점&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;기술 전문성 강화&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;빠른 실행, 책임 분산&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;단점&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;병목/지연 발생 가능&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;기술 중복/일관성 문제 가능&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-msa와-스쿼드-운영-왜-잘-어울릴까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-msa%EC%99%80-%EC%8A%A4%EC%BF%BC%EB%93%9C-%EC%9A%B4%EC%98%81-%EC%99%9C-%EC%9E%98-%EC%96%B4%EC%9A%B8%EB%A6%B4%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;3 msa와 스쿼드 운영 왜 잘 어울릴까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. MSA와 스쿼드 운영, 왜 잘 어울릴까요?&lt;/h2&gt;
&lt;p&gt;올리브영은 지금 MSA(마이크로서비스 아키텍처) 라는 구조로 서비스를 만들고 있습니다.&lt;/p&gt;
&lt;p&gt;이 방식은 서비스 하나를 여러 개의 작고 독립적인 단위로 나누어 관리하는 구조로, 빠르게 바뀌는 시장에 잘 대응할 수 있는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;여기에 스쿼드(Squad) 조직을 더하면 더 큰 효과를 낼 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;msa와-스쿼드는-아주-잘-맞는-조합입니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#msa%EC%99%80-%EC%8A%A4%EC%BF%BC%EB%93%9C%EB%8A%94-%EC%95%84%EC%A3%BC-%EC%9E%98-%EB%A7%9E%EB%8A%94-%EC%A1%B0%ED%95%A9%EC%9E%85%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;msa와 스쿼드는 아주 잘 맞는 조합입니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MSA와 스쿼드는 아주 잘 맞는 조합입니다&lt;/h3&gt;
&lt;p&gt;올리브영은 이미 MSA 구조를 쓰고 있기 때문에, 스쿼드 운영 방식은 자연스럽게 어울립니다.&lt;/p&gt;
&lt;p&gt;각각의 스쿼드가 하나의 마이크로서비스를 맡아서 책임지고 운영할 수 있기 때문입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;이렇게 되면 좋은 점이 많습니다&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;빠르게 일할 수 있어요&lt;/strong&gt; : 각 스쿼드가 독립적으로 일하고, 직접 배포도 할 수 있어서 속도가 빨라집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;자율성이 커져요&lt;/strong&gt; : 스쿼드가 자기 서비스에 필요한 기술이나 도구를 자유롭게 선택할 수 있어, 더 효율적으로 일할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;책임이 명확해요&lt;/strong&gt; : 어떤 서비스에 문제가 생기면 누가 담당인지 명확하니, 빠르게 대응할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;단점도 있지만, 해결할 수 있어요&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;물론 단점도 있어요. 스쿼드마다 사용하는 기술이나 방식이 다르면 추후 관리가 어려울 수 있습니다.&lt;/p&gt;
&lt;p&gt;상품 조회 기능을 구현하는 경우를 예로 들어볼게요.&lt;/p&gt;
&lt;p&gt;A팀은 빠른 응답 속도를 위해 Redis 캐시를 적극적으로 활용하고, 일부 데이터는 메모리에도 상주시키는 구조로 만들었습니다.&lt;/p&gt;
&lt;p&gt;반면 B팀은 실시간성을 중요하게 여겨, 상품 정보를 매 요청마다 데이터베이스에서 직접 조회하도록 구현했습니다.&lt;/p&gt;
&lt;p&gt;“상품 조회”라는 같은 기능임에도 두 팀의 구현 방식이 다르다 보니, 사용자 경험이나 성능 면에서 차이가 발생할 수 있습니다.
다시 말해, 통합적으로 성능 개선이나 공통 API 설계가 필요할 때 큰 어려움이 생길 수 있죠.&lt;/p&gt;
&lt;p&gt;하지만 이런 문제는 공통 가이드라인이나 기술 리뷰 등을 통해 충분히 조정할 수 있습니다. 자율성을 유지하면서도 기본적인 원칙은 함께 지키는 것이 중요합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;예시 그림 설명:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[MSA와 스쿼드 연결도]
┌──────────────┐
│  스쿼드 A     │ → 마이크로서비스 A
├──────────────┤
│  스쿼드 B     │ → 마이크로서비스 B
├──────────────┤
│  스쿼드 C     │ → 마이크로서비스 C
└──────────────┘
→ 각 서비스는 API, 배포까지 독립적으로 운영&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-2주-스프린트--jira로-스토리포인트-산정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-2%EC%A3%BC-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8--jira%EB%A1%9C-%EC%8A%A4%ED%86%A0%EB%A6%AC%ED%8F%AC%EC%9D%B8%ED%8A%B8-%EC%82%B0%EC%A0%95&quot; aria-label=&quot;4 2주 스프린트  jira로 스토리포인트 산정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 2주 스프린트 &amp;#x26; Jira로 스토리포인트 산정&lt;/h2&gt;
&lt;p&gt;스쿼드는 보통 &lt;strong&gt;2주 단위의 스프린트&lt;/strong&gt;로 업무를 계획하고 실행합니다.&lt;/p&gt;
&lt;p&gt;이 방식은 정해진 시간 안에 해야 할 일을 명확히 정리하고, 팀원 모두가 같은 목표를 바라보며 일할 수 있게 도와줍니다. 업무는 각각 예상되는 &lt;strong&gt;작업량&lt;/strong&gt;과 &lt;strong&gt;난이도&lt;/strong&gt;를 기준으로 &lt;strong&gt;스토리 포인트&lt;/strong&gt;(&lt;strong&gt;Story Point&lt;/strong&gt;) 를 부여해 관리합니다.&lt;/p&gt;
&lt;p&gt;올리브영에서는 각 스쿼드마다 기준이 있는데 제가 소속되어있는 스쿼드는 다음과 같은 기준을 사용하고 있습니다:&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기준&lt;/strong&gt;: 1포인트 = 1일 작업량 (1명이 하루 동안 할 수 있는 일)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도구&lt;/strong&gt;: Jira&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;예시 그림 설명:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[Jira에서 스프린트 구성]
┌────────────────────────────┐
│ Sprint: 2025-07-01 ~ 07-14 │
├────────────────────────────┤
│ ▢ [3pt] 로그인 API 리팩터링    │
│ ▢ [2pt] UI 오류 수정         │
│ ▢ [5pt] 새로운 결제 모듈       │
└────────────────────────────┘
→ 총 10포인트 = 10일 분량&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;이렇게-하면-좋은-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EB%A0%87%EA%B2%8C-%ED%95%98%EB%A9%B4-%EC%A2%8B%EC%9D%80-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;이렇게 하면 좋은 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;이렇게 하면 좋은 이유&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;일정 예측이 쉬워요&lt;/strong&gt; :
각 스프린트가 10포인트라면, 2주 동안 어느 정도 양의 일을 할 수 있는지 감이 생깁니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;업무 우선순위가 명확해요&lt;/strong&gt; :
중요한 일부터 포인트를 할당하고 계획하므로, 어디에 집중해야 할지가 분명해집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;업무 부담 조절이 쉬워요&lt;/strong&gt; :
지나치게 많은 업무가 쌓이지 않도록 조정할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;각 업무 항목에 포인트를 미리 정해두면, 스프린트가 끝날 때 실제 진행 상황과 비교하여 예측 정확도를 점검할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;이를 바탕으로 다음 스프린트 계획이 더 정교해집니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-회고--플래닝-돌아보고-다음을-준비하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%ED%9A%8C%EA%B3%A0--%ED%94%8C%EB%9E%98%EB%8B%9D-%EB%8F%8C%EC%95%84%EB%B3%B4%EA%B3%A0-%EB%8B%A4%EC%9D%8C%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;5 회고  플래닝 돌아보고 다음을 준비하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 회고 → 플래닝: 돌아보고, 다음을 준비하기&lt;/h2&gt;
&lt;p&gt;스프린트가 끝나면 팀은 &lt;strong&gt;회고(Retrospective)&lt;/strong&gt; 시간을 가집니다.&lt;/p&gt;
&lt;p&gt;이 시간은 지난 2주 동안 어떤 점이 잘 됐고, 어떤 점은 개선이 필요했는지를 함께 돌아보는 중요한 과정입니다.&lt;/p&gt;
&lt;p&gt;하지만 회고가 &lt;strong&gt;아쉬운 점 중심&lt;/strong&gt;으로만 흐르면, 팀 분위기가 무거워질 수 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 올리브영에서 저의 스쿼드는 &lt;strong&gt;긍정적인 피드백과 개선점이 균형 있게 나올 수 있도록&lt;/strong&gt; 다음과 같은 방식을 사용합니다:
&lt;br&gt;
&lt;strong&gt;“좋았던 점 하나 + 아쉬운 점 하나”를 함께 말하기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 팀원이 돌아가며 각자 한 가지씩&lt;/li&gt;
&lt;li&gt;자연스럽게 서로를 격려하고&lt;/li&gt;
&lt;li&gt;놓치기 쉬운 피드백까지 챙기고&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;회고를 통해 얻은 인사이트는 곧바로 다음 단계인 &lt;strong&gt;플래닝(Planning)&lt;/strong&gt; 으로 이어집니다.&lt;/p&gt;
&lt;p&gt;플래닝에서는 다음 스프린트 동안 어떤 일을 할지 정리하고, &lt;strong&gt;스프린트 목표&lt;/strong&gt;와 &lt;strong&gt;우선순위&lt;/strong&gt;를 명확히 합니다.&lt;/p&gt;
&lt;p&gt;이 과정을 통해 팀은 매 스프린트마다 작게라도 더 나아지고, 협업 방식도 지속적으로 개선해 나갈 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;스쿼드 운영 방식은 자율성과 책임, 빠른 실행력과 피드백 사이의 균형을 잘 유지해야 효과를 발휘합니다.&lt;/p&gt;
&lt;p&gt;단순히 팀을 나누는 것 이상의 조직 문화와 협업 방식이 필요하기 때문에, 그 안에서의 경험과 학습이 굉장히 중요합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;혹시 스쿼드를 운영 중이거나 준비하고 계시다면, 이 글이 조금이나마 도움이 되었으면 좋겠습니다! 🚀&lt;/p&gt;</content:encoded></item><item><title><![CDATA[프론트엔드 개발자를 위한 5가지 스크롤 복구 시나리오와 실전 코드 팁]]></title><description><![CDATA[안녕하세요! 올리브영 프론트엔드 개발자 hee 입니다‍ 여러분은 혹시 사용자가 이전에 보던 화면 그대로 다시 보여주려다 예상치 못한 스크롤 위치 때문에 당황했던 경험이 있으신가요? SPA 환경에서 '스크롤 복구(Scroll Restoration…]]></description><link>https://oliveyoung.tech/2025-07-30/scroll-restoration/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-07-30/scroll-restoration/</guid><pubDate>Wed, 30 Jul 2025 17:30:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영 프론트엔드 개발자 hee 입니다‍&lt;/p&gt;
&lt;p&gt;여러분은 혹시 사용자가 이전에 보던 화면 그대로 다시 보여주려다 예상치 못한 스크롤 위치 때문에 당황했던 경험이 있으신가요? SPA 환경에서 &apos;스크롤 복구(Scroll Restoration)&apos;는 단순해 보여도 개발자를 끊임없이 괴롭히는 난제 중 하나입니다. 이번 포스팅에서는 올리브영 프론트엔드 개발팀이 다양한 프로젝트에서 마주했던 스크롤 복구 이슈들을 어떻게 해결해왔는지, 그 생존 전략과 실전 팁들을 공유해드리고자 합니다. 이 글이 여러분의 &apos;스크롤 지옥&apos; 탈출에 조금이나마 도움이 되기를 바랍니다!
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;scroll-restoration이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#scroll-restoration%EC%9D%B4%EB%9E%80&quot; aria-label=&quot;scroll restoration이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Scroll restoration이란?&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;Scroll restoration은 사용자가 페이지를 떠났다가 다시 돌아왔을 때, 이전에 보고 있던 스크롤 위치를 자동으로 복구하는 기능입니다. 전통적인 웹사이트에서는 브라우저가 자동으로 이 기능을 제공했지만, 현대의 SPA(Single Page Application) 환경에서는 여러 가지 기술적 제약으로 인해 이 기능이 제대로 작동하지 않는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;이러한 스크롤 위치 복구 실패는 사용자 경험에 심각한 영향을 미칩니다. 특히 긴 목록을 스크롤하다가 상세 페이지로 이동한 후 다시 돌아왔을 때 맨 위로 되돌아가는 현상은 사용자들에게 큰 불편함을 안겨줍니다. 이는 단순히 불편함을 넘어서 사용자의 이탈률 증가와 전반적인 서비스 만족도 저하로 이어질 수 있어, 현대 웹 개발에서 반드시 해결해야 할 중요한 과제 중 하나입니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;spa-환경에서-스크롤-복구가-어려운-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#spa-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EB%B3%B5%EA%B5%AC%EA%B0%80-%EC%96%B4%EB%A0%A4%EC%9A%B4-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;spa 환경에서 스크롤 복구가 어려운 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;SPA 환경에서 스크롤 복구가 어려운 이유&lt;/h3&gt;
&lt;p&gt;SPA에서 스크롤 복구가 어려운 이유를 차근차근 살펴보겠습니다. 전통적인 멀티 페이지 웹사이트에서는 각 페이지가 독립적인 HTML 문서였습니다. 브라우저는 사용자가 페이지를 떠날 때 스크롤 위치를 기억해두었다가, 다시 그 페이지로 돌아올 때 자동으로 복구해주었습니다.&lt;/p&gt;
&lt;p&gt;하지만 SPA의 동작은, 하나의 페이지 안에서 JavaScript가 콘텐츠를 동적으로 바꾸는 방식입니다. 이 때 브라우저 입장에서는 계속 같은 페이지에 머물러 있다고 인식하기 때문에, 기본적인 스크롤 복구 기능이 제대로 작동하지 않습니다.&lt;/p&gt;
&lt;p&gt;더 복잡한 문제는 현대 웹 애플리케이션의 특성에서 나타납니다. 데이터를 비동기적으로 불러오고, 무한 스크롤을 구현하며, 콘텐츠의 높이가 동적으로 변하는 환경에서는 단순히 스크롤 위치만 저장하는 것으로는 해결되지 않습니다. 페이지를 다시 방문했을 때 데이터가 아직 로드되지 않았거나, 동적으로 생성되는 콘텐츠의 높이가 달라질 수 있기 때문입니다.&lt;/p&gt;
&lt;h1 id=&quot;상황별-스크롤-복구-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%ED%99%A9%EB%B3%84-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EB%B3%B5%EA%B5%AC-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;상황별 스크롤 복구 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상황별 스크롤 복구 방법&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;실제 프로젝트에서 마주했던 다양한 상황들을 유형별로 분류해서 살펴보겠습니다. 각 상황마다 최적의 해결책이 다르다는 것을 이해하는 것이 중요합니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-정적인-데이터-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%A0%95%EC%A0%81%EC%9D%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%81%ED%99%A9&quot; aria-label=&quot;1 정적인 데이터 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) 정적인 데이터 상황&lt;/h3&gt;
&lt;p&gt;가장 간단한 경우부터 시작해보겠습니다. 페이지의 모든 데이터가 미리 로드되어 있고, 콘텐츠의 높이가 변하지 않는 상황입니다. 이런 경우에는 브라우저의 History API를 활용하여 비교적 쉽게 해결할 수 있습니다.
&lt;br /&gt;아래 예제에서는 단순하게 History API를 사용해서 scrollY값을 저장했지만 Web Storage를 이용하거나 URL 쿼리를 이용해서 저장할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 기본적인 스크롤 위치 저장 및 복구&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setupBasicScrollRestoration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 페이지를 떠날 때 현재 스크롤 위치를 저장&lt;/span&gt;
  window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;beforeunload&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scrollY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    history&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;pushState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; scrollY &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;?page=1&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 페이지로 돌아왔을 때 저장된 위치로 스크롤&lt;/span&gt;
  window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;popstate&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;정적인 데이터 환경에서는 이 방법이 매우 효과적입니다. 하지만 실제 프로젝트에서는 대부분 동적인 데이터를 다루기 때문에 더 복잡한 접근이 필요합니다.&lt;/p&gt;
&lt;h3 id=&quot;2-동적인-데이터-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%8F%99%EC%A0%81%EC%9D%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%81%ED%99%A9&quot; aria-label=&quot;2 동적인 데이터 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) 동적인 데이터 상황&lt;/h3&gt;
&lt;p&gt;동적인 데이터를 다루는 상황에서는 단순히 스크롤 위치만 저장하는 것으로는 부족합니다. 데이터가 비동기적으로 로드되면서 페이지의 높이가 변하기 때문에, 저장된 스크롤 위치가 의미 없어질 수 있습니다.&lt;/p&gt;
&lt;p&gt;이런 상황에서는 스크롤 위치와 함께 추가적인 정보를 저장해야 합니다. 현재 화면에 보이는 아이템들의 정보를 함께 저장하면, 데이터가 다시 로드되더라도 특정 아이템을 기준으로 스크롤 위치를 복구할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 동적 데이터 환경에서의 스크롤 관리&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DynamicScrollManager&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;saveScrollState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pageKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scrollY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; visibleItems &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getVisibleItemsInfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 스크롤 위치와 보이는 아이템 정보를 함께 저장&lt;/span&gt;
    sessionStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      scrollY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      visibleItems&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;restoreScrollState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pageKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; savedData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sessionStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{}&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;savedData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;visibleItems&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 데이터 로딩이 완료될 때까지 대기&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;waitForDataLoad&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 저장된 아이템을 찾아서 기준점으로 사용&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; targetElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findTargetElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;savedData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;visibleItems&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetElement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      targetElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollIntoView&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 미세 조정을 위해 저장된 정확한 위치로 스크롤&lt;/span&gt;
      window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; savedData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;getVisibleItemsInfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 현재 화면에 보이는 아이템들의 ID와 위치 정보 수집&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; items &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;[data-item-id]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;items&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dataset&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;itemId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;offsetTop&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;offsetTop
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;findTargetElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;savedItems&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 저장된 아이템 중 첫 번째 아이템을 기준점으로 사용&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;[data-item-id=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;savedItems&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;]&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식의 핵심은 절대적인 스크롤 위치보다는 상대적인 기준점을 활용한다는 점입니다. 특정 아이템을 기준으로 위치를 복구하기 때문에, 데이터가 다시 로드되어 페이지 높이가 변하더라도 안정적으로 작동합니다. 중요한점은 데이터로드 이후에 스크롤을 복구해야된다는 점 입니다.&lt;/p&gt;
&lt;h3 id=&quot;3-동적인-데이터-상황---레이지-로딩&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%8F%99%EC%A0%81%EC%9D%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%81%ED%99%A9---%EB%A0%88%EC%9D%B4%EC%A7%80-%EB%A1%9C%EB%94%A9&quot; aria-label=&quot;3 동적인 데이터 상황   레이지 로딩 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3) 동적인 데이터 상황 - 레이지 로딩&lt;/h3&gt;
&lt;p&gt;동적 데이터 상황 중에서도 무한 스크롤이나 레이지 로딩을 사용하는 경우가 가장 복잡합니다. 사용자가 스크롤을 내릴 때마다 새로운 데이터를 불러오는데, 페이지를 벗어나거나 새로고침으로 다시 방문할 때는 그 데이터가 없는 상태이기 때문입니다.&lt;/p&gt;
&lt;p&gt;&quot;스크롤이 해당 지점에 도달했을때 데이터를 불러온다면 어떻게 스크롤 처음 위치를 잡을 수 있을까?&quot;
이 문제를 해결하기 위해서는 두 가지 전략을 조합해야 합니다. 첫째는 필요한 데이터를 미리 로드하는 것이고, 둘째는 스켈레톤 UI를 통해 레이아웃을 미리 확보하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 레이지 로딩 환경에서의 스크롤 복구&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LazyLoadScrollManager&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;restoreScrollWithPreload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pageKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; savedState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSavedState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;savedState&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 1단계: 스켈레톤으로 공간 확보&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createSkeletonPlaceholders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;totalHeight&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;// 2단계: 저장된 지점까지 필요한 데이터를 배치 단위로 미리 로드&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preloadRequiredData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;requiredItemCount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 3단계: 실제 스크롤 위치 복구&lt;/span&gt;
    window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;preloadRequiredData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;itemCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; batchSize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; itemCount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; batchSize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; batch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;loadDataBatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; batchSize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;renderBatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;batch&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;waitForRender&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// DOM 업데이트 대기&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;스켈레톤을-이용한-cls-줄이기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-cls-%EC%A4%84%EC%9D%B4%EA%B8%B0&quot; aria-label=&quot;스켈레톤을 이용한 cls 줄이기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;스켈레톤을 이용한 CLS 줄이기&lt;/h4&gt;
&lt;p&gt;Cumulative Layout Shift(CLS)를 줄이기 위해 스켈레톤 UI를 활용하는 것이 중요합니다. 스켈레톤은 실제 콘텐츠가 로드되기 전에 레이아웃을 미리 잡아주는 역할을 합니다.&lt;/p&gt;
&lt;h4 id=&quot;스켈레톤으로-해당-데이터의-height-크기를-미리잡아놓기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4%EC%9C%BC%EB%A1%9C-%ED%95%B4%EB%8B%B9-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-height-%ED%81%AC%EA%B8%B0%EB%A5%BC-%EB%AF%B8%EB%A6%AC%EC%9E%A1%EC%95%84%EB%86%93%EA%B8%B0&quot; aria-label=&quot;스켈레톤으로 해당 데이터의 height 크기를 미리잡아놓기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;스켈레톤으로 해당 데이터의 height 크기를 미리잡아놓기&lt;/h4&gt;
&lt;p&gt;스켈레톤의 효과를 극대화하려면 실제 콘텐츠와 비슷한 높이를 갖도록 해야 합니다. 이를 위해 과거 데이터를 기반으로 예상 높이를 계산하는 방법을 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 스켈레톤 높이 관리&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SkeletonHeightManager&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;itemHeights &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 실제 아이템 높이들을 기록&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 초기 평균 높이&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 높이 평균값 계산을 위해 각 엘리먼트 순회&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;recordItemHeight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; element&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;offsetHeight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;itemHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 최근 50개 아이템의 평균 높이 계산&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;getItemAverageHeight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; recentHeights &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;itemHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; recentHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;sum&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; h&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; h&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; recentHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 스켈레톤 생성&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;createSkeletonWithEstimatedHeight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;div&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; count&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; skeleton &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;div&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      skeleton&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;className &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;skeleton-item&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      skeleton&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageHeight&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;px&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      container&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;skeleton&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; container&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;4-react-query를-사용하는-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-react-query%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%83%81%ED%99%A9&quot; aria-label=&quot;4 react query를 사용하는 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4) react-query를 사용하는 상황&lt;/h3&gt;
&lt;p&gt;React Query를 활용하면 데이터 캐싱 기능 덕분에 스크롤 복원을 훨씬 수월하게 구현할 수 있습니다. 이미 캐시된 데이터를 즉시 화면에 표시할 수 있어 복잡한 로딩 상태 관리가 불필요해집니다.&lt;/p&gt;
&lt;p&gt;이런 효과를 얻기 위해서는 React Query의 캐싱 메커니즘을 제대로 이해하는 것이 중요합니다. 데이터가 캐시에 저장되어 있으면 별도의 네트워크 요청 없이 UI를 바로 렌더링할 수 있습니다. 이러한 특성은 스크롤 복원 과정에서 끊김 없는, 매끄러운 사용자 경험을 제공합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// React Query 환경에서의 스크롤 복구&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useScrollRestorationWithQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;queryKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; queryClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useQueryClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;saveScrollState&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scrollData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;scrollY&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// React Query 캐시에 스크롤 정보도 함께 저장&lt;/span&gt;
    queryClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setQueryData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;queryKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;scroll&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; scrollData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;restoreScrollState&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 1단계: 캐싱된 데이터 가져옴&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; scrollData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; queryClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getQueryData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;queryKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;scroll&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cachedData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; queryClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getQueryData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;queryKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scrollData &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cachedData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 2단계: 캐싱된 데이터를 이용해서 컴포넌트를 화면에 렌더링&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;componentsRendering&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cachedData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 3단계: 캐싱된 스크롤값을 이용해서 좌표이동&lt;/span&gt;
      window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; scrollData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; saveScrollState&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; restoreScrollState &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 무한 스크롤과 조합한 경우&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useInfiniteScrollRestoration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;queryKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; infiniteQuery &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useInfiniteQuery&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    queryKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;queryFn&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; fetchData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;staleTime&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 캐시 시간을 길게 설정&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;restoreWithInfiniteData&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 1단계: 저장된 pageCount와 스크롤 위치를 가져옴&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; savedScrollData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSavedScrollData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;savedScrollData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// 2단계: 저장된 페이지 수만큼 데이터 복구&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;infiniteQuery&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pages&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; savedScrollData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pageCount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; infiniteQuery&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchNextPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 3단계: 모든 데이터 로딩 완료 후 스크롤 복구&lt;/span&gt;
      window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; savedScrollData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;scrollY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;infiniteQuery&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; restoreWithInfiniteData &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;React Query를 사용할 때의 핵심은 데이터와 스크롤 상태를 함께 관리한다는 점입니다. 캐시된 데이터가 있다면 로딩 시간 없이 즉시 스크롤을 복구할 수 있고, 없다면 데이터 로딩과 스크롤 복구를 순차적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;5-대량-리스트에서-가상화를-이용한-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%8C%80%EB%9F%89-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-%EA%B0%80%EC%83%81%ED%99%94%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%83%81%ED%99%A9&quot; aria-label=&quot;5 대량 리스트에서 가상화를 이용한 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5) 대량 리스트에서 가상화를 이용한 상황&lt;/h3&gt;
&lt;p&gt;가상화(Virtualization)를 사용하는 경우에는 특별한 접근 방식이 필요합니다. 가상화된 리스트는 실제로 화면에 보이는 아이템들만 DOM에 렌더링하기 때문에 기존의 스크롤 복원 방법으로는 한계가 있습니다.&lt;/p&gt;
&lt;p&gt;먼저 가상화의 동작 원리를 살펴보겠습니다. 수천 개의 아이템이 포함된 리스트라고 해도 실제 DOM에 존재하는 것은 현재 화면에 표시되는 10~20개 정도에 불과합니다. 나머지 아이템은 가상으로 존재하며, 사용자의 스크롤에 따라 동적으로 생성되고 제거되는 방식으로 동작합니다.&lt;/p&gt;
&lt;p&gt;그래서 가상화 환경에서 스크롤을 복원하려면 기존과는 다른 접근이 필요합니다. 사용자가 &lt;strong&gt;마지막으로 보고 있던 아이템 데이터를 먼저 로드&lt;/strong&gt;하고, 이후 주변 데이터를 점진적으로 불러오는 방식을 사용해야 합니다. 이렇게 데이터가 준비되면 해당 위치로 스크롤을 이동시켜 자연스러운 복원 경험을 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 가상화 환경에서의 스크롤 복구&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;VirtualizedScrollManager&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  
  &lt;span class=&quot;token function&quot;&gt;saveVirtualizedScrollState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pageKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; virtualList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getVirtualListInstance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 먼저 로드되는 상황을 고려해서 0부터 시작하는 sequence값을 계산해서 저장&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; listSequence &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; virtualList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getSequence&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; virtualList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPageNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pageSize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; virtualList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPageSize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 가상화된 환경에서는 보이는 아이템 순서값과 최신 페이징 데이터값을 저장&lt;/span&gt;
    sessionStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      listSequence&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      pageNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      pageSize&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;restoreVirtualizedScroll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;pageKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; savedState &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sessionStorage&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pageKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{}&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;listSequence&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 1단계: 먼저 보이는 아이템 데이터 호출&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;loadDataRange&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pageNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pageSize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 2단계: 가상 리스트에 저장된 스크롤 오프셋 적용&lt;/span&gt;
    virtualList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;listSequence&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;// 3단계: 보이던 아이템 주변 데이터 로드&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;preloadAroundVisibleRange&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pageNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      savedState&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pageSize
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;가상화 환경에서의 핵심은 필요한 부분만 우선적으로 로드하고, 나머지는 백그라운드에서 점진적으로 로드한다는 점입니다. 이렇게 하면 사용자는 즉시 원하는 위치를 볼 수 있고, 전체 데이터 로딩을 기다릴 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;위 코드는 pseudo 코드를 이용해서 간결하게 작성했습니다. 만약 리액트에서 사용한다면 React-Virtualized, Virtuoso 와 같은 라이브러리 사용을 추천합니다.&lt;/p&gt;
&lt;h1 id=&quot;완벽보단-자연스럽게&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%84%EB%B2%BD%EB%B3%B4%EB%8B%A8-%EC%9E%90%EC%97%B0%EC%8A%A4%EB%9F%BD%EA%B2%8C&quot; aria-label=&quot;완벽보단 자연스럽게 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;완벽보단 자연스럽게&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;지금까지 상황별 스크롤 복구 방법을 살펴봤습니다.&lt;/p&gt;
&lt;p&gt;실제 프로젝트에서는 여러 방법을 조합해 사용하는 경우가 많습니다. 예를 들면 React Query로 데이터를 관리하면서 무한 스크롤을 구현하고, 스켈레톤 UI로 사용자 경험을 개선하는 형태가 있죠. 중요한 것은 각 프로젝트의 특성과 사용자의 사용 패턴을 이해하는 것, 그리고 그에 맞는 최적의 방법을 선택하는 것입니다.&lt;/p&gt;
&lt;p&gt;스크롤 복구을 완벽하게 하기 보다는, 사용자가 불편함을 느끼지 않을 정도의 적절한 수준을 목표로 하는 것이 현실적인 접근입니다. 여러분의 환경에 적합한 꿀조합은 어떤 방법인가요?&lt;/p&gt;</content:encoded></item><item><title><![CDATA[제로베이스 WMS 구축기: Kafka 기반 분산 물류 시스템 설계와 Out-of-Order Events 해결]]></title><description><![CDATA[안녕하세요. 풀필먼트 백엔드 개발을 담당하고 있는 시나브로우입니다. 이번 글에서는 올리브영만의 최초 물류 시스템, GMS(Global Warehouse Management System)를 소개드리고자 합니다. GMS, 최초의 서막 2024년 1…]]></description><link>https://oliveyoung.tech/2025-07-23/gms-open-story/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-07-23/gms-open-story/</guid><pubDate>Wed, 23 Jul 2025 18:00:00 GMT</pubDate><content:encoded>&lt;style type=&quot;text/css&quot;&gt;
    .info-box {
        background: #fcfcfc;
        border: 1px solid #ccc;
        border-radius: 5px;
        color: #333;
        margin: 10px 0 1em 0;
        min-height: 20px;
        padding: 10px 10px 10px 36px;
    }

    .info-box-red {
        background: #fff8f7;
        border: 1px solid #d04437;
        border-radius: 5px;
        color: #333;
        margin: 10px 0 1em 0;
        min-height: 20px;
        padding: 10px 10px 10px 36px;
    }

    .info-box-yellow {
        background: #fffdf6;
        border: 1px solid #ffeaae;
        border-radius: 5px;
        color: #333;
        margin: 10px 0 1em 0;
        min-height: 20px;
        padding: 10px 10px 10px 36px;
    }

    .info-box span, li,
    .info-box-red span, li,
    .info-box-yellow span, li,
    .text-box span {
        font-size: 14px;
    }

    .info-box li:before {
        background-color: #a4d233 !important;
    }

    .info-box p,
    .info-box-red p {
        margin: 0; important;
        font-size: 15px;
    }

    .tg {border-collapse:collapse;border-spacing:0; width: 100%;}
    .tg td {
      border-color:black;border-style:solid;border-width:1px;font-size:14px;
      overflow:hidden;padding:10px 5px;word-break:normal;
    }
    .tg th{
      border-color:black;border-style:solid;border-width:1px;font-size:14px; background-color:#d8e0e7;
      font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal; font-weight: bold
    }
    .tg .tg-0pky { border-color:inherit;text-align:middle;vertical-align:top }
    .tg .tg-0pky:first-child { font-weight: bold }
    .tg tr td:second-child {text-align:middle;}
    figcaption {
        margin: 0 !important;
        padding: 0 !important;
    }
    .side-by-side {
        width: 100%;
        display: flex;
        justify-content: space-between;
        align-items: center;
        flex-wrap: wrap;
        overflow-x: hidden;
    }
    .img-box {
        width: 100%;
        display: inline;
        justify-content:center;
        align-items: center;
        flex-wrap: wrap;
        margin: 0 !important;
        padding: 0 !important;
    }
&lt;/style&gt;
&lt;p&gt;안녕하세요. 풀필먼트 백엔드 개발을 담당하고 있는 시나브로우입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 올리브영만의 최초 물류 시스템, GMS(Global Warehouse Management System)를 소개드리고자 합니다.&lt;/p&gt;
&lt;h1 id=&quot;gms-최초의-서막&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#gms-%EC%B5%9C%EC%B4%88%EC%9D%98-%EC%84%9C%EB%A7%89&quot; aria-label=&quot;gms 최초의 서막 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;GMS, 최초의 서막&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;2024년 10월&lt;/strong&gt;, 올리브영은 글로벌 사업 확장과 자체 물류 인프라 강화를 목표로 &lt;strong&gt;GMS(Global Warehouse Management System) 구축 프로젝트&lt;/strong&gt;를 본격적으로 시작했습니다.
기존의 외부 WMS(Warehouse Management System) 솔루션에 의존하던 방식에서 벗어나, &lt;strong&gt;올리브영만의 독자적인 물류 시스템을 새롭게 설계하고 개발하는 첫 시도&lt;/strong&gt;인데요.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1318px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1d95b983d5b36e029e892cfbcf570957/90694/gms_open_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVR42gGfAmD9AG2ah22ViG+UiHGWjHqalJOrraG1uIiio4KamJahq56suKq5xL/J0cnQ1cvS1sDJz7W/yZyuvmiHlEhwiQB0kZzS2tjk5+Ty9PH5+Pfw7+/n5uXj4uTO0dSQmaRse41qe45sfI+LmaaQobB4h5xseY+BgJJzgYx3gpIAZ32LjJeqf4uapq21pKqzmKCplJqjmJ6npauyv8PI1NfZ3N/fv8LFkZaed36LYWZ1a3F6cnd8gIiHgYWCAEppVoeTfHV5fW51f5KXneTl5PX18/X08/n59/z7+fr59/j49fz79/b28+bl4sTAva6rpcS+sNLIqrKqkgAYNSuJlmKbqVyVjIKFh42xs7Hr6+n29vbw8O/v8PDv7+/v8O7u7+7w8O/29vT+/vv7+ve1saymn4ign4UAGjErJ0QuNlgtdnxQopCGj5GSl5iY1dXT+/r48/Px9PTy9/f29/f29vX16enp293ds7e4bnV5n5iUpqGaADlbNC9UK4SObtHKmJuSZ5+Pi5GUkJOWk7/CvfLy8ebo5dLV0rW4upedoYWOlHiCiV9sdjNFWFFcaXt5dwAaMSMYMh52e1rVy5CNn0CUi2GomJWSlZCAhYGHjY54goVpdXhla3FkbHNmc31gbXlKWWhIV2lCT2Fubm8ADiEZDyUbIUsbg5ozrLhDcY8sjX1wn5GHo6icb3V5YmpzaXR+VV5qVV1rY2dxcG9xY2lqMD1KZGVren5zACM7JyBAIiJQIEl7J6C0N5q0KmB9Nambkq2okpCTilxkblZfal1dYm9ra3Bvampsa1VaXGNjY4KAfFVmUQAeOCEfMxwYMhwWPxllkiSOpjM6axhnekuRhXqEhHqBenRzcGdzc2dvenJsdnFtbmCGhHyQioZUYVKfopEDXXqn204mVQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1d95b983d5b36e029e892cfbcf570957/263a4/gms_open_01.webp 480w,
/static/1d95b983d5b36e029e892cfbcf570957/a6361/gms_open_01.webp 960w,
/static/1d95b983d5b36e029e892cfbcf570957/ccb1e/gms_open_01.webp 1318w&quot; sizes=&quot;(max-width: 1318px) 100vw, 1318px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1d95b983d5b36e029e892cfbcf570957/9aebd/gms_open_01.png 480w,
/static/1d95b983d5b36e029e892cfbcf570957/a91f8/gms_open_01.png 960w,
/static/1d95b983d5b36e029e892cfbcf570957/90694/gms_open_01.png 1318w&quot; sizes=&quot;(max-width: 1318px) 100vw, 1318px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1d95b983d5b36e029e892cfbcf570957/90694/gms_open_01.png&quot; alt=&quot;안성 물류센터 전경&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;안성 물류센터는 사업별 특성에 맞춰 글로벌몰 센터와 자체 브랜드 센터로 이원화하여 운영 중입니다. GMS가 도입된 글로벌몰 전용 센터는 해외 배송에 특화된 시스템으로 설계되었습니다.
국가별로 DHL, LX Pantos, FDX 등의 특송사를 통해서 해외 배송 서비스를 제공하고 있죠.
프로젝트 초기에는 &lt;strong&gt;프론트엔드 개발자 1명, 백엔드 개발자 1명, PO 3명, UX/UI 디자이너 1명&lt;/strong&gt;으로 구성된 소규모 스쿼드로 시작했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;padding: 0 !important; margin:0 !important;&quot;&gt;
    &lt;div style=&quot;align-items: center; padding: 0 !important; margin:0 !important;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1309px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/99da4b7b6a2dd12ec4ea1beef92dd196/3d556/gms_open_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.70833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACtklEQVR42nVUW08TQRTuX/Qv+KL/ABLwwQRfGgTEFx6MiSYIoURiUMFWYgUssVgiUq2UdreX7W13u5fure1ePs9MW9QqJ/kyZ2fO+c5tZmNbW1tIp9PY29tDNpvFwcEBx+7uLgRBwNHREVKpFHK5HHRdh2EYHBN9eo1VKhU0m02Uy2XU63WwbwZRFCHLMmq1GockSdA0jTsyTPTpNbaxsYFkMglFUWDbNizLuoZpmuj1elxn6ySTCaYz5ISMmTn6vo8wDDmiKLrG//YYgiD4ByxwTCdCRe7wKJ2OTJlY6HZV0jtQFZVK0dFVqRxaJamOVqvJ4boupoVVGKtJDdRbbTKqQhSKRMIc2qhWK0TaQkduo6O10Ww1cHlZpH5KKBZLPBsmLNu/CGHLQOsrMu9e4NX2IkLfGB9H6PcB1wzhnhmI+hHfA3xCgP8JJ/Qv9lFduoXNlbuIxxcgU9kTOXxv4/lyDcqKg7ALhFEVjdJjmMr2zYQD8RR6Ygbq8TPoJusLDSAcZZNNm3jySMTlD4c7+P0SxMI8LOXpzYSMAOGAE/zRDi4elewRV+CPvsPARTBoUMDfVTCfYOzICXvEJzo+5PMEQrc7sqLm+aUSfEFEu1RAu5gbbYcW6to5lItlRG7jutf8GkXBiPCLPsRsRsCbDwlgMJ6cqkKNP4T0YBFv78/i9doCH4c2KCNxOodMfomMRoMROgaWjy/wcVOC49I9LFgBVgsaUmowjkfoapCIUJi7B/1TBl1WOx2YXgMnwjpEfQdD54SaWkR8vYjb8ztYm/+MmqwiFrJekfGQDWLci2gwRJ/K7V9dIepZk8roeBTUMw9x9W2GlCRWz/JYuvMT+2syOiYRenTjPdeB6zhwJqA9l56iR8/J9jzem9GZTS9kCMvIw1Bfom+dQ7NsfM83YMj0H7B7+AUfI1whKkVA0gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/99da4b7b6a2dd12ec4ea1beef92dd196/263a4/gms_open_02.webp 480w,
/static/99da4b7b6a2dd12ec4ea1beef92dd196/a6361/gms_open_02.webp 960w,
/static/99da4b7b6a2dd12ec4ea1beef92dd196/e99cd/gms_open_02.webp 1309w&quot; sizes=&quot;(max-width: 1309px) 100vw, 1309px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/99da4b7b6a2dd12ec4ea1beef92dd196/9aebd/gms_open_02.png 480w,
/static/99da4b7b6a2dd12ec4ea1beef92dd196/a91f8/gms_open_02.png 960w,
/static/99da4b7b6a2dd12ec4ea1beef92dd196/3d556/gms_open_02.png 1309w&quot; sizes=&quot;(max-width: 1309px) 100vw, 1309px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/99da4b7b6a2dd12ec4ea1beef92dd196/3d556/gms_open_02.png&quot; alt=&quot;풀필먼트 첫 스프린트 플래닝&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
    &lt;figcaption&gt;풀필먼트 첫 스프린트 플래닝 (혼돈)&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;첫 플래닝부터 정신없는 일정이 이어졌고 혼란스럽게 시작했지만 모두가 힘을 합쳐 &lt;strong&gt;최고의 물류 시스템을 만들겠다는 공동의 목표&lt;/strong&gt;를 달성하기 위해 열심히 달리기 시작했습니다.&lt;/p&gt;
&lt;p&gt;저희 풀필먼트 스쿼드는 글로벌 사업의 성장에 발맞춰 사용자 니즈를 반영하고, 신규 프로세스를 수용할 수 있는 &lt;strong&gt;유연함, 그리고 고가용성과 안정성을 갖춘 물류 시스템 구축&lt;/strong&gt;을 목표로 삼았습니다.&lt;/p&gt;
&lt;p&gt;WMS 내재화를 위한 여정은 말 그대로 &lt;strong&gt;제로베이스&lt;/strong&gt;에서 출발했습니다.&lt;/p&gt;
&lt;p&gt;WMS에 대한 배경 지식이나 경험이 전무한 상태였기에, 실무 개발과 함께 WMS 도메인 학습을 병행해야 했습니다.&lt;/p&gt;
&lt;p&gt;‘입고’, ‘적치’, ‘할당’, ‘피킹’, ‘검수’, ‘출고’, ‘재고보충’, ‘기준정보’ 등 전반적인 WMS 비즈니스 로직을 빠르게 이해하고 직접 설계/구현하며,&lt;/p&gt;
&lt;p&gt;DB 설계부터 서버 인프라 구축까지 모든 과정을 종횡무진으로 수행해 나갔습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그 결과, &lt;strong&gt;2025년 2월 중순부터 본격적인 QA를 시작&lt;/strong&gt;할 수 있었고,&lt;/p&gt;
&lt;p&gt;개발과 테스트, 버그 수정이 동시에 돌아가는 ‘전쟁 같은’ 일정을 보내며 시스템의 완성도를 높여갔습니다.&lt;/p&gt;
&lt;p&gt;2~4월 동안 진행된 체계적이고 전문적인 QA 과정을 통해,&lt;/p&gt;
&lt;p&gt;GMS는 단순한 WMS를 넘어 올리브영 물류의 중심축이 될 준비를 마칠 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;side-by-side&quot;&gt;
    &lt;div style=&quot;display: inline-flex; width:100%; align-items: flex-end;&quot;&gt;
        &lt;div style=&quot;width:35%; height:100%;&quot;&gt;
            &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 678px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5b11f88539dc0cf258d7a24721a81864/dc057/gms_open_03_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 93.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAACt0lEQVR42pWTW08TQRTH+WB4e/DBdxPfkTc+BAYNr0ZBaAvFcBOJIT4IQaIYItDSFhCQll5oC2Xp/cZ2t+1e/56ZbaENLeok/8zunDm/OZeZnoHVKTz7/AYDq070fbGhb9mG/hU7+pfteE7/TxaH8WD+JR7OD+P+7BB6ZwZxb+bFtXqnB/F4/hUezQ3h6dJr9AhiAQkxD6FShCAyFXBB/ym5iLNyDp7EJTzR39gXfAjkYoiW84iU0ogUk9ZMipYyOCXFSln0GKaJTiORrWFtL4PFzTz29t3wnzpwHF9AuuCDaWodfQzDtIAMyWRwto6jszQmvyUwuxHHzEYSWwTcjzjgDk5gy/+W7EtQNIlDTNz4600gNzTmTNmP7cAklnePMUcw57qAdZ8L3tAIdkIT8IWnyD6CoLDaPcKbBQ0H0Y/YCY5iLzKFFZ8P3w8LSBXyuMi54Qk7CeoguBPuk3GUZaEtmBagtSBWU/AEmw6jCF2ugZlNGNyeLvnhOhmDl8Cuk3c4z3ruBhYqsRaHMQIE+LpuWE2oqyKl/B4eSp0BI8n1BtDoDCxJCSq8rSUCd5uDKCepHHayT3J7LP2zG9AarHO7kWlycvAoWOoZilLVqrwch/FPbQdmr0LdUr5ZjKc3eRe9vAF2uILj1iHUDNaIZkN+RT9QKZRGKGanCK1Z1as4oki2A6M8NXZNvCE2O/khbjqAZVCSzq/vYddr04Sy1EPCV0rLxsGsQSxFNh/EFnitW/ffAbSGpun81cj1Swh5D06TP3gpclfhW2n+EzB8XsRFRqJXY6JYMW+5m13ef1dgtV5CMi+jKKrQdI02GiSdi8FqtRokSfo7sPmVEysQclVkSyqlaDSAlhiwXq9DluX/iDBfgJCWkCqqHNBJiqJ0BiqKBlXV26TUFYpCRY1sitpdt/wUDX8AeUp6YldF1UIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5b11f88539dc0cf258d7a24721a81864/263a4/gms_open_03_01.webp 480w,
/static/5b11f88539dc0cf258d7a24721a81864/e0546/gms_open_03_01.webp 678w&quot; sizes=&quot;(max-width: 678px) 100vw, 678px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5b11f88539dc0cf258d7a24721a81864/9aebd/gms_open_03_01.png 480w,
/static/5b11f88539dc0cf258d7a24721a81864/dc057/gms_open_03_01.png 678w&quot; sizes=&quot;(max-width: 678px) 100vw, 678px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5b11f88539dc0cf258d7a24721a81864/dc057/gms_open_03_01.png&quot; alt=&quot;QA 진행 대시보드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;width:65%; height:100%;&quot;&gt;
            &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1692px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ee2ea9752a7f4f41c4f3b3580a24850e/0f94e/gms_open_03_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 22.291666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAsUlEQVR42m2N6wqCQBBGff/HCirK+lN0MSxKS6PAtHbXnb34tVpZYAOHOcM3zHjz9IDZOcLyesTiEjsiTJItpukOg/0aw0PQuJ+E6MdBk41PIXrRqvEa382jeINZuodHpYS1Ff6VtUD1juquTdV6nX1KkgFxBas0PO4OklJu2XQgraHerrRBSbp1Ui831oAJiVtxBxcCXl48kGU5pKQOpXv2dQLjsnXxkzFeIi8YOBd4AiyMMrmyGYVgAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ee2ea9752a7f4f41c4f3b3580a24850e/263a4/gms_open_03_02.webp 480w,
/static/ee2ea9752a7f4f41c4f3b3580a24850e/a6361/gms_open_03_02.webp 960w,
/static/ee2ea9752a7f4f41c4f3b3580a24850e/f4fa0/gms_open_03_02.webp 1692w&quot; sizes=&quot;(max-width: 1692px) 100vw, 1692px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ee2ea9752a7f4f41c4f3b3580a24850e/9aebd/gms_open_03_02.png 480w,
/static/ee2ea9752a7f4f41c4f3b3580a24850e/a91f8/gms_open_03_02.png 960w,
/static/ee2ea9752a7f4f41c4f3b3580a24850e/0f94e/gms_open_03_02.png 1692w&quot; sizes=&quot;(max-width: 1692px) 100vw, 1692px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ee2ea9752a7f4f41c4f3b3580a24850e/0f94e/gms_open_03_02.png&quot; alt=&quot;QA 진행 대시보드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;QA 진행 대시보드&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;QA total 이슈가 409건&lt;/strong&gt;이 있었지만,  저희 WMS 전문 시니어 QA 님이 이 정도는 적은 편이라고 하셨습니다. 👍&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;side-by-side&quot;&gt;
    &lt;div style=&quot;display: inline-flex; width:100%;&quot;&gt;
      &lt;div style=&quot;width:54%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e497eed428919b9db191ac7b46f45c0a/2a0d8/gms_open_04_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.541666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAADoElEQVR42h2S2VPbVRiGf/+BF944Ol5Yta1QRUuxdBRECi0SKAxlwDilZNqCdCe2BaUglEAgYZMAWdjCGrZC2AIhYUsJSSAQwtJG6lanMzr+A3rjzOMZL755z3xz5nnf73xH0hlMHDzdJXR4wL5Q3+aa0CC7+wH8u5vsPNsh9FuIw98P8WyvM24bY9phZdnjxLWxzPrWE3w767i3XLg3VpF+aDOy4ffwLLRHIOhnzbNCcC8gTIIEDgLsHe4R+jXEM1Fr/jVsS9Mse53/AzyBNRbdDirUKspU5VRrapAaWw2suJYEYBvvhpvVJ062BXhXJNs+EL0dH8FQkO19Yba5woxINzxhYWndiX/fi9li5tiJkxwNjyAsIhKpocXAnN3Gj4dB/AEPXjFCV6eBnp4OAk/9aIX7omuBB9+VUl5dRbupFaOhg6FxC2tbq9Tr6nnnvRO8fTycI8fCkDRNOqxT47R3mrial09njwlFjoKO1ibajG0kxMtQKpW8fzKWs+dT0GrUdNZrMDWo0LfUMT45SFVtGbHxn4s7UUhaXTsDYxZUD2+Q/UUSn8bF8/pbYcizssnKyCAtWYZOXUFpcTG386/weNDMkFlPQ80j1FWPmJ4ao7tXT2R0NGeTziM1D86jMZbSrPsaS18Ld+7cQHFRxiuvvkZE5Me0VRXhGG5hytyIoV5FS6MWtbqG8opKNLX1TFpn0Bs7Sc/8kiTZBaSb36uoVBegaSynTltMTcktbt+8RPjxd6m6lYN31op9aB59g466unqqqmtQi9I16wXMxqLTxezsAv2WUZp0eqTE7GsU3JUzNGKiSf0tpq5MmqzpFFdeQVumZL5ngpWRX/BM/cyAyUhPh1iYuV+MahcgJ3b7Ejabg6nJObELG1J8Ri7pF9NISoklUXaKLkcyLUN55BXmoyisZH7Zj893gG3QhmuiF4e1m7lhM/OjFhwjk/SaBjC2dTM4MMroxDRSjCwT5YNcziVGcyHzNEXlCi7nyUnJEr10BXWtfQR/eimS92HvaiDkXcI9Kc597fzz4k/+fvEHz90BFoatjDyeRDojvsW1AjmVqqtUa3N5qMoh7lwyCSmpfCQ2l5KdR1FlGx26ZjZnLWzbJ4T2U3pfSYmyhA3bIv++/Ivn6z7G+jqQUlJTSZYlcO++HGWhnG/ufUXUmdOkyWOI/uwob7x5hFMffoBBdRffTD8bAjakrxXPdImo6DhiPonnuuI6fXoDLtsI/wEX4b2e8TiduwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e497eed428919b9db191ac7b46f45c0a/263a4/gms_open_04_01.webp 480w,
/static/e497eed428919b9db191ac7b46f45c0a/a6361/gms_open_04_01.webp 960w,
/static/e497eed428919b9db191ac7b46f45c0a/0b34d/gms_open_04_01.webp 1920w,
/static/e497eed428919b9db191ac7b46f45c0a/da28f/gms_open_04_01.webp 2880w,
/static/e497eed428919b9db191ac7b46f45c0a/98b7d/gms_open_04_01.webp 3840w,
/static/e497eed428919b9db191ac7b46f45c0a/0ca69/gms_open_04_01.webp 7088w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e497eed428919b9db191ac7b46f45c0a/9aebd/gms_open_04_01.png 480w,
/static/e497eed428919b9db191ac7b46f45c0a/a91f8/gms_open_04_01.png 960w,
/static/e497eed428919b9db191ac7b46f45c0a/ac7a9/gms_open_04_01.png 1920w,
/static/e497eed428919b9db191ac7b46f45c0a/f9c26/gms_open_04_01.png 2880w,
/static/e497eed428919b9db191ac7b46f45c0a/5da7e/gms_open_04_01.png 3840w,
/static/e497eed428919b9db191ac7b46f45c0a/2a0d8/gms_open_04_01.png 7088w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e497eed428919b9db191ac7b46f45c0a/ac7a9/gms_open_04_01.png&quot; alt=&quot;안성물류센터 war room&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
      &lt;div style=&quot;width:46%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fd8e2b80f8b562a8c935ab6c2c71d409/1ce76/gms_open_04_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEHUlEQVR42i2R6U/TBxjHf1tCplU5HK03KmMITKmIB5BpEYoIpS20lI4KWqylQKEHh0CF0gNBEY+Cms45UMQKUwGpHIK6ZGbTLNmRJe4PWfb+s59mL548V/LJ9/s8wszcBM+iESJT3zL77AHPF6eYm5/8GB96R1M9LQ1WLgX7CN+8QUerHb2uDFO1gUZrHY4GC73n3Vz0dnKh040QXXjE8soTZmbvE30e4cXqYxaXp8V4zMLSNF0OOx32Bm5cGcBk1KAtKaC6XEVD7TcEuttwO+sx1+io0JygSq9CmI9OiwofiWomiS6IoNUoz5fnPsJerM7g7XFx1qxj+HKAZqsJm7mSWmMpdSYNF9w21Kp8SopzMddqaao3IczORpgXLX+ALS/c4e93IX57M8LbX1f45d0q10c7CQzWMz11l6DPJtZmPH4zgf4mRq958QYa6PFaCQYd+HxO0XL0KUv/W3y59B3O+iJG+ivp7XUTvjPC2L0Qo+EhRm+HuDLQzKX+s5jr9BirtTjsZpwtRhprCnG1WOjr9yG8eLnE65+WmFt9xtj312k0lFOen0vKtq0YdSrsbe2oKqqoMp3heEEBp+usWGzNaMWZVqtBpy5G+fU+yjWl+DxehMnJ+/wQXaF7ZJDbV9w89FkJXxrgls9FX7MJdXY65qoqzro6cVnUFBco2Lc3k+ISNdXmenTaIrLz8hi2KPnnaglC6NYwLXaH+KVjPBztYnzMxupfN7nxoJOmczpWnDn0VmYz35jJct0eKuRfsEsmJWtvOg1NVmx15ZSV5hGtSQaLgPAo5KT6jJ6iE7ncu9bBj+/O8/u/NobCrbT7PAT9NpaVcbzfL6BKjuVQ2k6OpKaSsTsJg16Nx9ONu9lIv/4ATw+uQbDXFHCyTMHBw3JUpSmEhs8x9aqXrkArfUN+eq76GSxK43KswKHdO8g6mEbKlkS+TJKiUGRhb+3G0+PHZq1io1SGkKc8Rk5OBoW5cuR58XQM5zPzpx3/HQOG00Y0FYXsS93NDkkMsi3JNLbaaO/X0xrUU6w6weDQMJEHU6iPyDkqihKMyu3I0xM58NV2dmyNQ1GWyfgbE10jRRwvOUaGfCMSSSxr1q1lTUwCjX4lT95bGV3UkFMoKnS0c3PkGk5TBRHvKQTb8U/YvyeOrdtkJMbHkJaRyvUJD90X2zCaTSiKs9mYsIGEhDg+i5Ggtxzl4VsXgfFKknZt5khWFufbugiPTTARDiGY8iWk75SwWRZLarKUhPj1WJubuCouddU6knZuIy5e3It3+wD9PDGBw0fTyFfswaxMx2U4iU5/ir5AkPuRKYTa0kxStm9gs3QdackyERgrWlzLpi1SJOskrF+/FqksjqSkTR+zIHxKTvomLhiS8bWc4unCzyy+/oPH86+4Oz7Gf9W7j9iDqVEbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fd8e2b80f8b562a8c935ab6c2c71d409/263a4/gms_open_04_02.webp 480w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/a6361/gms_open_04_02.webp 960w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/0b34d/gms_open_04_02.webp 1920w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/da28f/gms_open_04_02.webp 2880w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/98b7d/gms_open_04_02.webp 3840w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/d6bb8/gms_open_04_02.webp 4000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fd8e2b80f8b562a8c935ab6c2c71d409/9aebd/gms_open_04_02.png 480w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/a91f8/gms_open_04_02.png 960w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/ac7a9/gms_open_04_02.png 1920w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/f9c26/gms_open_04_02.png 2880w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/5da7e/gms_open_04_02.png 3840w,
/static/fd8e2b80f8b562a8c935ab6c2c71d409/1ce76/gms_open_04_02.png 4000w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fd8e2b80f8b562a8c935ab6c2c71d409/ac7a9/gms_open_04_02.png&quot; alt=&quot;안성물류센터 war room&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;안성물류센터 war room&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;GMS 오픈을 앞두고, 안성 물류센터에 War Room을 구축하고 밤낮없이 오픈 준비에 매진했습니다.&lt;/p&gt;
&lt;p&gt;수많은 시련과 인내의 시간을 거쳐 마침내, &lt;strong&gt;2025년 5월 8일, GMS를 Grand Open&lt;/strong&gt; 할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;떨리는 그 순간, &lt;strong&gt;올리브영은 사상 처음으로 자사 개발 WMS 시스템을 성공적으로 런칭&lt;/strong&gt;하게 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;gms-architecture&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#gms-architecture&quot; aria-label=&quot;gms architecture permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;GMS Architecture&lt;/h2&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e0ab1a821d206aa3469ebad255e58784/41da6/gms_open_05.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAC4jAAAuIwF4pT92AAAClUlEQVR42m1UW2sTURDOT/TVV5/8BUJB8MVXEfRJfNCnPikFq9BqxQtFrCWlsWhTmvRiNmmy2Ut2k909e26fM7OJFfHA4cyezHxnvm9m0lK1hnUOvMIoxn67g6pSKGkrutbGwvy1reXT0OmQzDJ8ax8iz+fQWmMaz9CqtcFq1XRZlqXYtlogfr0Gm/bh6dt7B+c8+qcZpmH1x79Y+vOKkgwtzmC1vPeyBVCVmGw/goovwE8aoyWzfj9GOCnER9O3I3bLEEQpAf486SEIhpJBFKc4PDqG9fy6gaHT0T0HsiyWIoWuI9rWiA9Td0tEAZxMY6LZUCiKEuPJGHkcwDiiXyjMkhKWSM+zKe0JWR4V6a5qI6C8y3QAb+aIZnO0uChglSgTprYIjjF+coOsDO3PETYeDuQ+fvsA6dY90XM0zHHUiaAJTCmF4dObqLuvkFBeLSKF/e0Iuy8TAZ7PIowOPxANhWAwRWf/DPxkfHmM6LwD7odZvsA4zKB0jZq0DX/1UBUzJNmiATzYHeDL1iXY5kouNZYWsZSFp3tNumnWkc5KVQRWChhTZmD2EQ2ZshORa+kzrtqqbGxbcSbB3z9G+u6+3Kdf1xG/WUNNj6uqwtX6bej+NuLSN4Acz5lxX0nb+AaUq+ds06fT758QH+2InXT3ELY3peo1PTja20QV9pDMVdPYHMhZZFmOYHglOjE9fsDQybsmn7PgivTLoej3iuTg6ZAW8u7vxr6elJRGqXcRYHGyg3p40EzD+Bj1+Uexu6dnyJIIxY9NuGJKiUDk8ivA9B9Aa0zTFi/uIt971lDdXUe4cUey5oZ3ZY7R81swYRdWCmeuJ+V/o7dqcP6D4KXoXNDw809cdV5G66ZoS51XcXGa4zcS8y9s0rA0qQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e0ab1a821d206aa3469ebad255e58784/263a4/gms_open_05.webp 480w,
/static/e0ab1a821d206aa3469ebad255e58784/a6361/gms_open_05.webp 960w,
/static/e0ab1a821d206aa3469ebad255e58784/0b34d/gms_open_05.webp 1920w,
/static/e0ab1a821d206aa3469ebad255e58784/da28f/gms_open_05.webp 2880w,
/static/e0ab1a821d206aa3469ebad255e58784/98b7d/gms_open_05.webp 3840w,
/static/e0ab1a821d206aa3469ebad255e58784/f569e/gms_open_05.webp 4018w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e0ab1a821d206aa3469ebad255e58784/9aebd/gms_open_05.png 480w,
/static/e0ab1a821d206aa3469ebad255e58784/a91f8/gms_open_05.png 960w,
/static/e0ab1a821d206aa3469ebad255e58784/ac7a9/gms_open_05.png 1920w,
/static/e0ab1a821d206aa3469ebad255e58784/f9c26/gms_open_05.png 2880w,
/static/e0ab1a821d206aa3469ebad255e58784/5da7e/gms_open_05.png 3840w,
/static/e0ab1a821d206aa3469ebad255e58784/41da6/gms_open_05.png 4018w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e0ab1a821d206aa3469ebad255e58784/ac7a9/gms_open_05.png&quot; alt=&quot;GMS Architecture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;GMS 시스템은 WMS 핵심 비즈니스 기능을 유연하고 효율적으로 처리할 수 있도록 최적화되어 있으며,&lt;/p&gt;
&lt;p&gt;외부 시스템과의 안정적인 연동 구조와 실시간 대량 데이터를 처리할 수 있는 분산 처리 아키텍처를 갖추고 있는 것이 특징입니다.&lt;/p&gt;
&lt;br&gt;
&lt;h4&gt;유기적 시스템 통합을 위한 API &amp; Kafka Message Event 중심 설계&lt;/h4&gt;
&lt;p&gt;GMS 는 오프라인 코어 백오피스 시스템, 글로벌몰 시스템, CBT(Cross Border Trade) 시스템, WCS(Warehouse Control System) 등 다양한 외부 시스템과의 상호 운용성을 확보하고, 실시간 데이터 연계에 최적화된 통합 아키텍처를 기반으로 설계되었습니다.&lt;/p&gt;
&lt;p&gt;외부 시스템과의 연동 방식은 사용 목적에 따라 구분하였습니다.&lt;/p&gt;
&lt;p&gt;응답(Response)이 필요한 API 기반 연동이나 레거시 시스템과의 통합은 REST API 중심으로 구축했으며,&lt;/p&gt;
&lt;p&gt;그 외 대부분의 시스템 간 데이터 흐름은 Kafka(MSK) 를 활용한 비동기 Pub/Sub 메시징 방식으로 설계하여 확장성과 실시간성을 모두 확보했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;Kafka 메시지 중복 및 유실 방지 설정을 적용하여 물류 데이터의 신뢰성을 확보했습니다.&lt;/p&gt;
&lt;p&gt;자세한 설정 방법은 이전 블로그 글에서도 다뤘으니 참고하시면 도움이 될 것입니다.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://oliveyoung.tech/2024-10-16/oliveyoung-scm-oms-kafka/&quot;&gt;
Kafka 메시지 중복 및 유실 케이스별 해결 방법
&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;out-of-order-events-trouble-shooting&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#out-of-order-events-trouble-shooting&quot; aria-label=&quot;out of order events trouble shooting permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Out-of-Order Events Trouble Shooting&lt;/h2&gt;
&lt;p&gt;Kafka(MSK)를 기반으로 한 비동기 Pub/Sub 메시징 구조를 통해 여러 시스템 간 유기적인 데이터 연계가 가능해졌으며,&lt;/p&gt;
&lt;p&gt;대량의 데이터를 효과적으로 처리하고 분산 처리 체계를 구현했지만,&lt;/p&gt;
&lt;p&gt;이러한 비동기 메시징 구조가 순기능한 것은 아니었습니다.&lt;/p&gt;
&lt;p&gt;Message Queue 방식의 고질적인 문제인 이벤트 순서를 보장하지 못하는 이슈가 있습니다.&lt;/p&gt;
&lt;p&gt;Queue 의 순서를 엄격하게 지정하더라도 이벤트를 생성하는 서비스 처리 과정에서 지연이 발생하는 경우 &lt;strong&gt;&apos;Out-of-Order Events&apos;&lt;/strong&gt;  가 발생하게 됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;GMS 시스템에서도 처리 지연과 네트워크 지연으로 인해 메시지 순서가 뒤바뀌는 문제가 발생했으며, 이에 대한 해결 과정을 소개드리겠습니다.&lt;/p&gt;
&lt;p&gt;출고 프로세스는 GMS, WCS, Global Mall System, CBT(Cross Border Trade) System, 특송사 시스템 등 여러 시스템이 연계된 구조로, 네트워크 경로가 복잡하게 구성되어 있습니다.&lt;/p&gt;
&lt;p&gt;시스템마다 AWS 계정이 서로 다르고, 각각의 MSK도 독립적으로 운영되고 있습니다. 특히, Global 시스템은 AWS Virginia 리전(us-east-1) 에 위치하고, GMS는 Seoul 리전(ap-northeast-2) 에 위치해 있어 리전 간 네트워크 지연 또한 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이처럼 복잡한 네트워크 구간과 시스템 간 통신의 비동기성으로 인해, 각 구간에서 메시지 순서가 어긋날 가능성이 존재했습니다.&lt;/p&gt;
&lt;p&gt;아래는 GMS 출고 프로세스의 구조를 도식화 한 것입니다.&lt;/p&gt;
&lt;div class=&quot;img-box&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d1d5d82db26686fe1ea26e3e2ac46164/28ce7/gms_open_06.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 44.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABC0lEQVR42n2Q63LEIAiFff/37OxsY+IliqggxZjdtj/azBkHCcjHMczCNJYASk6ZOqt6p+ucop+iQauexTCPwTKuJ0ou6UyDhPsIm00h3r86zxks1Ghmhqy8SfHEUnXgJdDmnsr+uRUfMePjw/ba4fDeORmyb363vrcevK/YDAIqTKutApaYIRcFzjG3qtwDselZsYISsZTSVFo/R2JVbLlJaAS7u8Mp2/Z0kBELHtY2bG/UwwbvTkU44+mdN2sZfq3NpB4I0TRG7VhLqhdnyJe1DAlf5tF3893/ClY8Bw5BqM+HpfnxvoW5y2WheZf+J31AFFXtBI0VcBH9mvyXdAUtjT7FmNaVaOa/AF+OD5TMTOOHAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d1d5d82db26686fe1ea26e3e2ac46164/263a4/gms_open_06.webp 480w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/a6361/gms_open_06.webp 960w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/0b34d/gms_open_06.webp 1920w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/da28f/gms_open_06.webp 2880w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/f25f5/gms_open_06.webp 3237w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d1d5d82db26686fe1ea26e3e2ac46164/9aebd/gms_open_06.png 480w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/a91f8/gms_open_06.png 960w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/ac7a9/gms_open_06.png 1920w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/f9c26/gms_open_06.png 2880w,
/static/d1d5d82db26686fe1ea26e3e2ac46164/28ce7/gms_open_06.png 3237w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d1d5d82db26686fe1ea26e3e2ac46164/ac7a9/gms_open_06.png&quot; alt=&quot;GMS 출고 프로세스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;GMS 출고 프로세스&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;검수와 패킹이 완료된 후, 운송장 채번 요청이 이루어지면 실물이 물류 설비를 통해 이동하게 됩니다.&lt;/p&gt;
&lt;p&gt;이후 과정부터는 외부 시스템과의 연동이 서로 독립적으로 진행됩니다.&lt;/p&gt;
&lt;p&gt;CBT(Cross Border Trade) 시스템은 특송사를 통해 운송장을 채번한 뒤, Virginia 리전에 위치한 OYG MSK 로 운송장 정보를 발행(Produce) 합니다.&lt;/p&gt;
&lt;p&gt;이후 GMS External Consumer가 해당 데이터를 수신하여, 타 리전의 운송장을 Seoul 리전의 GMS MSK로 전달(Forwarding) 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;WCS(Warehouse Control System) 는 출고 요청 데이터를 수신한 후, 물류 설비를 통해 특송사별 출고 라인으로 소팅하고, 배송 상자에 운송장 라벨링 작업을 수행합니다.&lt;/p&gt;
&lt;p&gt;이후 WCS 출고 확정 정보를 API를 통해 GMS External API로 전송합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;최종 단계에서는 GMS External API가 주문 상태를 변경하고, 출고 확정 정보를 GMS MSK의 stockout-internal(가칭) 토픽으로 다시 발행(Produce)합니다.&lt;/p&gt;
&lt;p&gt;GMS Internal Consumer 는 GMS MSK의 stockout-complete 토픽을 소비(Consume) 하여 출고 수량을 확정하고 재고 이력을 기록함으로써, GMS 출고 프로세스를 마무리합니다.&lt;/p&gt;
&lt;p&gt;출고 완료 결과는 다시 stockout-complete(가칭) 토픽으로 발행되며, Global Mall 시스템이 해당 데이터를 수신하여 주문 상태를 &apos;배송 중&apos;으로 변경하고, 정산 용 데이터로 활용하게 됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;서술한 순서대로 진행되면 문제 없이 출고 완료 데이터를 Global Mall 에 전송할 수 있겠지만,&lt;/p&gt;
&lt;p&gt;한 구간이라도 지연되면, &apos;Out-of-Order Events&apos; 가 발생하여, 운송장 번호가 누락된 상태로 출고확정 정보가 Global Mall 에 전송되는 문제가 발생합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이러한 지연이 주로 발생하는 구간을 설명하기 위해, 위 그림에 번호를 부여해 대표적인 지연 구간들을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;
    &lt;p&gt;지연 구간&lt;/p&gt;
    &lt;span&gt;1.CBT 시스템에서 GMS MSK 를 Consume 하는 구간&lt;/span&gt;&lt;br&gt;
    &lt;ul&gt;
        &lt;li&gt;region 간 consume 지연 이슈&lt;/li&gt;
        &lt;li&gt;대량 요청에 의한 병목 현상 (Bottleneck)&lt;/li&gt;
    &lt;/ul&gt;
    &lt;br&gt;
    &lt;span&gt;2.CBT 에서 특송사 API 를 호출하는 구간&lt;/span&gt;&lt;br&gt;
    &lt;ul&gt;
        &lt;li&gt;특송사 API 응답 지연&lt;/li&gt;
        &lt;li&gt;특송사 정기 PM 및 정기 배포 시간으로 인한 특송사 시스템 다운타임 시 응답 지연&lt;/li&gt;
    &lt;/ul&gt;
    &lt;br&gt;
    &lt;span&gt;3.GMS External Consumer 시스템에서 OYG MSK 를 Consume 하는 구간&lt;/span&gt;&lt;br&gt;
    &lt;ul&gt;
        &lt;li&gt;region 간 consume 지연 이슈. MB 단위 이상의 데이터를 수신할 경우 region 간 consume 지연 이슈가 발생함&lt;/li&gt;
        &lt;li&gt;GMS External Consumer 는 Seoul 리전, OYG MSK 는 Virginia 리전에 위치함.&lt;/li&gt;
    &lt;/ul&gt;
    &lt;br&gt;
    &lt;span&gt;4.GMS 작업자의 실수에 의한 수기 확정 처리&lt;/span&gt;&lt;br&gt;
    &lt;ul&gt;
        &lt;li&gt;작업자가 실수로 확정을 할 경우, 운송장 발행이 없는 상태로 출고확정 처리 됨.&lt;/li&gt;
    &lt;/ul&gt;
    &lt;br&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;결국, 각 서비스가 독립된 환경에서 상호배타적으로 비동기 처리되기 때문에, 다양한 원인으로 인해 운송장 번호가 수신 순서가 가장 마지막이 되는 경우가 발생하게 되는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/@prabhu.seshadri/handling-out-of-order-events-in-a-event-driven-systems-93349bd20c26&quot;&gt;https://medium.com/@prabhu.seshadri/handling-out-of-order-events-in-a-event-driven-systems-93349bd20c26&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;물론 네트워크 구간을 줄이고, 복잡한 메시징 구조를 피하는 것이 가장 좋은 방법입니다만,&lt;/p&gt;
&lt;p&gt;저희 GMS 시스템은 여러 외부 시스템과 연동되어 있고, 서버 자원을 각자 관리하고, 각 시스템이 서버 자원을 독립적으로 운영하기 때문에 &apos;Out-of-Order Events&apos; 를 방지할 수 없는 구조입니다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 메시지의 순서 및 정합성을 보장하기 위해 &lt;strong&gt;&apos;Delayed Queue&apos; 패턴을 적용하여, 지연 메시징과 지연 재처리 아키텍처&lt;/strong&gt;를 구축하게 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;img-box&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/adee3b184e191a86d01c61ebe8a98209/b7c46/gms_open_07.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABRklEQVR42k1R2W7DMAzL/3/anjegQ9ut6BV0iRvHcXxJtkfHaRGAEORDJCU1Vs9KCAocOTHFAi6xHDnFmNdkwTYhis2s1Pn7C1mK+dH2P/srEwcfnr36PbbTZECUU8ZrwSaJMTV4cz5ExlXyjqxxy4/krBe9RASXdb4Keut4kTGztsY2uFr5QMaRApEHQvc3nI73nLOU4+dh511A2eWwU3LA5Xn/8bjuS3FtDKxAKFKlYTPbUSp4Qx48VbcBDMSZUyvlc54W5ZjRldHucnroyWJgOCppB2G8p+KzdFhQPHKCshYCc16LAaYkOimlAj2erfGiG6CPRZSatE6rsuQ6sLftqh/LqhJhgsR1cvCMejtpNLWpx7biS3mJlfV+6dpbX2sQlRydC+3pMIoejt5OkTTxtfc3sB49aRDXI8QhiEssYvuNKf4DTZ9H911BqXoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/adee3b184e191a86d01c61ebe8a98209/263a4/gms_open_07.webp 480w,
/static/adee3b184e191a86d01c61ebe8a98209/a6361/gms_open_07.webp 960w,
/static/adee3b184e191a86d01c61ebe8a98209/0b34d/gms_open_07.webp 1920w,
/static/adee3b184e191a86d01c61ebe8a98209/d30f9/gms_open_07.webp 2162w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/adee3b184e191a86d01c61ebe8a98209/9aebd/gms_open_07.png 480w,
/static/adee3b184e191a86d01c61ebe8a98209/a91f8/gms_open_07.png 960w,
/static/adee3b184e191a86d01c61ebe8a98209/ac7a9/gms_open_07.png 1920w,
/static/adee3b184e191a86d01c61ebe8a98209/b7c46/gms_open_07.png 2162w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/adee3b184e191a86d01c61ebe8a98209/ac7a9/gms_open_07.png&quot; alt=&quot;재처리 Streams Topology Architecture&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption style=&quot;text-align: center;&quot;&gt;재처리 Streams Topology Architecture&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;지연 메시징과 지연 재처리 아키텍처는 &lt;strong&gt;Kafka Streams&lt;/strong&gt; 를 활용해서 구축하였습니다.&lt;/p&gt;
&lt;p&gt;지연 재처리 위한 Streams Topology 구성하기 위해,&lt;/p&gt;
&lt;p&gt;기존의  운송장 수신 토픽 (tracking-number-internal topic) 외에도&lt;/p&gt;
&lt;p&gt;유효하지 않은 출고를 저장할 토픽 (stockout-invalidated topic) 과 출고 재처리를 위한 토픽 (stockout-retry topic) 을 추가 생성하였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;운송장 수신 토픽 (tracking-number-internal topic) 수신이 지연된 경우에는,&lt;/p&gt;
&lt;p&gt;출고 확정 토픽(stockout-complete) 으로 발행(Produce) 을 막고,&lt;/p&gt;
&lt;p&gt;대신 유효하지 않은 출고 토픽 (stockout-invalidated topic) 으로 메시지를 발행(Produce) 하여 Kafka Streams 을 처리할 수 있게 합니다.&lt;/p&gt;
&lt;p&gt;이후 지연된 운송장을 운송장 수신 토픽 (tracking-number-internal topic) 을 통해 수신되면,&lt;/p&gt;
&lt;p&gt;Kafka Streams 는 이를 감지하고 출고 재처리를 위한 토픽 (stockout-retry topic) 으로 메시지를 생성하여,&lt;/p&gt;
&lt;p&gt;출고 확정 정보를  자동으로 재연동합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;지연 재처리 아키텍처를 통해서 재처리된 출고 데이터 프로세스 정보는 Slack 으로 단계별로 전송됩니다.&lt;/p&gt;
&lt;div class=&quot;img-box&quot;&gt;
    &lt;div style=&quot;width: 450px !important; max-width: 100%;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1572px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bb0596ead7a898b04fcd5f9ad0515a0f/dc8ac/gms_open_08.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 106.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAADLAAAAywAEoZFrbAAAD/UlEQVR42mVUy47jVBDNH6Bh6CZxXnYSO7bzcGLHifN+d9Ld9PRqJNbMAoYNrAAhkBCDxEO9nxUg8WfsaIkdFhES+HDqJunQYlGq6Ob61Klzqm7q7u7LP35+/R2m04tke3WTbK5uk+X6KrnY3jBvk6vr22R7eZNEg3EShP3ErXlJqVJNyqbN/BD/lE0HPP8tNZmP4neXK3T9PoaTGdbrJeazCVbLBaLBBLPFBSazJcbTBfrDKeqNNgp6BXrJglGuEsSWSATQqFTvU+c5I25oFgIvhNduY9zRMevpiHwDBcNCkSEfC0jVaSjgXjSCW2/BsutH4BOgbtpxZHqwKy4M0+ahVHQOoapj346tQKbzFVmvmdfoErjlh6g3/RNgiYCuVUOr1YFTb8N1GyiVzAMYW2JbUuTYXsVyFVOzWlMMnVpT8gkwrRXidqeHV998jXdun6FQC2E0QlhOXX1ou00VjkTNU/nI+BiPWqZz8Wb7DC8/eB+byw0sf4Rad4ygE6Ed9NDpDcA7ClQYyYe5YhnZQgnZvAGNUTQqyV5n8z5198Pn8S8/vUKj1cWEumwvrzGjThwZjOnuenutfotevhRhN26jpfRsUCaPGhI8EWAtp9+nTNuN5xMPnW7IyxGa7RC1pq8u1jgiwtLzuyqErZghwALY4n9S4BHgmabHXrPCPzto+j3o1ESnAW+l8zjP5PG2VlBxls5xjEyCDlm0gyJbVMEz5kSjBArw6XkuLup5TAZtBAFZBCGaTRrgVOloXWmUzhZVzutlVKqu0rJCl8t03LRrUujEkG3Ew/EMm+UI/f4Q3W4Pvu8j6oWcsYhtt2DTXWEnRghrYSusMzldFWNOskeGd99/Fr/+8Vt44RgXNGC+2mC+3GC62NCkJbjfWG2ulCkS4njdCyiLRaC9y4wToGXZsTMI4LYDhDIi3T7CaKAM8GiQOCkmCYhPsMFohibPBCBzkOIRYEYrxtk69XDr+00QbThrFoc6WyzhjC1KW5L3pgyUKTY3RDZI2n4E+ORNLX5arkAzyqz4oMnx4n6A5TJ/ly0H0XCyb78/IvtAseWIJVUOfrZg3Kc++vhl/MVXn6A7mGI0mWMwnvPymHmmPhzyrD+aKmPE1Txfnf8WO2xNIlkxfPHe8/jDT1+gEURYrS9pxlqZIu/gbHnBLbnEYrVVwDLsxw4O2qkQlx/G5o0nmfhJuQzHa3H5uVLcDlkt0alELYWFsBIG4qzDDRGm6cPI/G9ssnxgC60GCmb1MPWmEv84d5kDC/lQDFtwr0fyelMGWUfZKhY8MST671nT+itTKO3Smr5LZ/UdzdnnnLHTDiFnecPc8Unb8S1U4dS9HTdlR8A/ifO3ltd//Rc+8H0vrIKCGQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bb0596ead7a898b04fcd5f9ad0515a0f/263a4/gms_open_08.webp 480w,
/static/bb0596ead7a898b04fcd5f9ad0515a0f/a6361/gms_open_08.webp 960w,
/static/bb0596ead7a898b04fcd5f9ad0515a0f/c9b4b/gms_open_08.webp 1572w&quot; sizes=&quot;(max-width: 1572px) 100vw, 1572px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bb0596ead7a898b04fcd5f9ad0515a0f/9aebd/gms_open_08.png 480w,
/static/bb0596ead7a898b04fcd5f9ad0515a0f/a91f8/gms_open_08.png 960w,
/static/bb0596ead7a898b04fcd5f9ad0515a0f/dc8ac/gms_open_08.png 1572w&quot; sizes=&quot;(max-width: 1572px) 100vw, 1572px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bb0596ead7a898b04fcd5f9ad0515a0f/dc8ac/gms_open_08.png&quot; alt=&quot;Slack 내 지연 재처리 알림 메시지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;Slack 내 지연 재처리 알림 메시지&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;자동으로 재처리 된 출고 데이터를 Slack 을 통해 실시간으로 간편하게 확인할 수 있어서, 개발자의 수동 확인 및 재처리 작업을 크게 줄일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;참고로, 주문 단위로 메시지를 단건 전송하는 방식이 아니라, &apos;Batching&apos; 방식으로 일정 시간 동안 메시지를 모은 뒤 Slack에 묶어서 전송되도록 구현되어 있습니다.
이는 Slack 알림의 과도한 빈도로 인한 피로도를 줄이고, 동일한 이슈에 대한 메시지를 한눈에 파악할 수 있도록 하기 위한 목적입니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;gms-핵심-업무와-기술-기반-개선-사례&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#gms-%ED%95%B5%EC%8B%AC-%EC%97%85%EB%AC%B4%EC%99%80-%EA%B8%B0%EC%88%A0-%EA%B8%B0%EB%B0%98-%EA%B0%9C%EC%84%A0-%EC%82%AC%EB%A1%80&quot; aria-label=&quot;gms 핵심 업무와 기술 기반 개선 사례 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;GMS 핵심 업무와 기술 기반 개선 사례&lt;/h2&gt;
&lt;div class=&quot;side-by-side&quot;&gt;
    &lt;div style=&quot;display: inline-flex; width:100%;&quot;&gt;
      &lt;div style=&quot;width:52%;height:100%;&quot;&gt;
          &lt;h3&gt;GMS 입고&lt;/h3&gt;
      &lt;/div&gt;
      &lt;div style=&quot;width:48%;height:100%;&quot;&gt;
          &lt;h3&gt;GMS 출고&lt;/h3&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display: inline-flex; width:100%;&quot;&gt;
      &lt;div style=&quot;width:52%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 947px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c94c045340589ce09c0add522c6b10e1/22121/gms_open_09_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 76.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAACBElEQVR42l1TiZKbMAzl/3+unU67SbPHbNNsiG3A94Hx0WezZaZlxMMYPUt6EsN4v337+uV8err8PL2+XE4/vt+uv7QSSvL/TEthlLBGOqsDbiWH+8ftcn56fT6/vz0zMr69XB7jR62llASsNdeaOu7rfOxYqwfBl7RF56w1xjuLZylbSaEk4NoXwNixLWpaa15r2axRw8wIHlyIx4NwIZFe3lzNW83wC931LwGLT4wgGy0bGcd7lCBmb9Xqzefnw7V0MoIfzIM8MYIXZyQZb1rOKfqKanPEQTBnlFU8d++cwrb6vAEdSmjkZaJWyxACYwyItFenZzoaa7TWUkqlFJ/oFix5jHO/KGVGq572RFdvheCUEqUkmoFSg0MjsLfgkgJNWlavjRaM4oQ7Y9Q7A8+edk0pupIjUsqb38Vsi64WNpvD6vhEcFwzwSFSJ1OQc/lHodYkFN/Oih5ZOCO8EQgzPsj1+huInA9yKbuqdQMH4sF2wYLViAlBkUgMVoplmSlfprhCEjEs8wR502qDVWJhiINoewn7YByFtOYDa8cSDcYTjICGSAzJOE8MAwwCIqM2JSXnTTD0Bt7HwDUVcmxq40aiKSWtVc4ZhOANn5n33lqLxgBjDH2k97Cxx0+Qe5B8QZ8ZI3Prw11yzBkUUk2e+w2/DSUjps98GrdawN/jhc9/AA2/XE7aRh53AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c94c045340589ce09c0add522c6b10e1/263a4/gms_open_09_01.webp 480w,
/static/c94c045340589ce09c0add522c6b10e1/0a256/gms_open_09_01.webp 947w&quot; sizes=&quot;(max-width: 947px) 100vw, 947px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c94c045340589ce09c0add522c6b10e1/9aebd/gms_open_09_01.png 480w,
/static/c94c045340589ce09c0add522c6b10e1/22121/gms_open_09_01.png 947w&quot; sizes=&quot;(max-width: 947px) 100vw, 947px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c94c045340589ce09c0add522c6b10e1/22121/gms_open_09_01.png&quot; alt=&quot;GMS 입고 Flow&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
      &lt;div style=&quot;width:48%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1240px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/65eb15855f451c9132a4621e8299547a/09504/gms_open_09_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 82.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAIAAACZeshMAAAACXBIWXMAAAsTAAALEwEAmpwYAAACEElEQVR42nWTx24cMRBE5/+/zYZvtgUBNnzSYoc5NMMkP3IkWBcvGlyGTlVds/z68/vLt68vry8/X39+f/kRckySo6Tb0m0lT5NcJTeRVurWfArLatRDPVe9Gm+V0/t17tfxP9v+rSfpFhd8FnHOJmrU0s+tnb2N9ZMd96bz2ueRFDHFxQZX9669jSW3o2/Xfgf0z3aNG54wjtu5H9cZc1q0M7U36x2HCakAm31IMfdCOjaBk+TSa6zZBaeNzkUGZmW0FDHWxhhJ5AXXaIzx3pvgXObfa61TTi55zAf/fD45Um8xzrS9pyrS6VvIbWEOd6tTk37tXKaa6eLew2vAa28+BoItAHgAGGuQ0bCjBiiafODcZauhxPR+cxzXNdrWH8Fc4cSm7A0n+id49pKoTF910tlnLkJGZXDCCt1COAG42uRMsC6HIYa9TTiFdQzi2N5Fsg8ih0hyyWBc15VOiAkx8INFFBZKgsicM0efg4s+pmThUSbbEEMmSCLT02oq08+qVmONogNJkLIqBa/jKQel1dvjjeFhC+yhNfqZbEUTHc+MavAdHcFujso5BzScH4+HUuqpRpvI06E1YEMDIitbpQiVmT/wuKeRKaHIRLnhiKIpA6gl5AR127XdwiTfKKvGpBHcGMQ5BnHPbH4Vg+3xYUhegEpKPjHUxyqt3qXGqJFkqwWqp5VpMpkfIknhL8gJl13BRtoAAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/65eb15855f451c9132a4621e8299547a/263a4/gms_open_09_02.webp 480w,
/static/65eb15855f451c9132a4621e8299547a/a6361/gms_open_09_02.webp 960w,
/static/65eb15855f451c9132a4621e8299547a/53f84/gms_open_09_02.webp 1240w&quot; sizes=&quot;(max-width: 1240px) 100vw, 1240px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/65eb15855f451c9132a4621e8299547a/9aebd/gms_open_09_02.png 480w,
/static/65eb15855f451c9132a4621e8299547a/a91f8/gms_open_09_02.png 960w,
/static/65eb15855f451c9132a4621e8299547a/09504/gms_open_09_02.png 1240w&quot; sizes=&quot;(max-width: 1240px) 100vw, 1240px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/65eb15855f451c9132a4621e8299547a/09504/gms_open_09_02.png&quot; alt=&quot;GMS 출고 Flow&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;GMS 핵심 업무&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;OliveYoung GMS (Global WMS) 시스템의 입고는 양지센터에서 출고되는 상품을 매입 처리하는 방식으로 진행됩니다.&lt;/p&gt;
&lt;p&gt;입고 경로는 양지센터 외에도 다양하지만, 전체 입고 물량의 약 85%가 양지센터로부터 공급되고 있습니다.&lt;/p&gt;
&lt;p&gt;양지에서 출고된 데이터는 본사 DB에 저장된 후, GMS DB로 연동되며,&lt;/p&gt;
&lt;p&gt;GMS 에서 입고 확정 처리가 완료되면, 해당 데이터를 기반으로 매입 확정 정보를 실시간 반영하는 것이 핵심 업무입니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;매입 정보를 실시간으로 반영해야 했지만, 본사 코어 백오피스 시스템과의 MSK 기반 통신이 불가능한 상황이었습니다.
이에 따라 차선책으로 본사 DB와 GMS DB 간의 연동을 Two-Phase Commit 방식으로 구현해, 매입 정보를 실시간으로 반영할 수 있도록 했습니다.&lt;/p&gt;
&lt;p&gt;본사 DB와 GMS DB는 배타적으로 접근이 제어되며, 각 트랜잭션은 서로 분리된 상태로 처리됩니다.&lt;/p&gt;
&lt;p&gt;GMS 입고 확정은 PDA 와 PC 를 통해 실시간으로 이루어지며,&lt;/p&gt;
&lt;p&gt;이때 Spring의 ApplicationEventPublisher를 활용해 이벤트를 발행하고, TransactionalEventListener 를 통해서 본사 DB 에 입고 확정 정보를 반영합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;또한 &apos;적치&apos;, &apos;재고 이동&apos;, &apos;재고 보충&apos;, &apos;할당&apos;, &apos;피킹지시&apos; 등 주요 물류 프로세스에서 다수의 PC 와  PDA 작업할 경우 동시성 문제를 효과적으로 해소하기 위해,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis 분산 락&lt;/strong&gt;을 도입해서 다수의 PDA 및 PC 사용자가 동시에 작업을 수행하더라도 재고 데이터의 충돌을 방지하고, 데이터의 일관성과 시스템의 신뢰성을 확보할 수 있도록 설계했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;다음으로 OliveYoung GMS(Global WMS) 시스템의 출고 프로세스를 소개해드리겠습니다.&lt;/p&gt;
&lt;p&gt;이 중에서도, WMS 내재화 이후 가장 눈에 띄게 변화한 영역은 바로 ‘할당 및 피킹 지시’ 파트입니다.&lt;/p&gt;
&lt;div class=&quot;img-box&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/59d57e691d6dc7c18a790cad83d2bb76/c7a69/gms_open_10.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 38.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABW0lEQVR42q2Q3U7CQBCF+6K+kO9gvDTxhhsTUWNMBEUqELnQGKxAC2Ipv+22u1u23ePsCkZNvLPJl86cOTOzGUdKiaIo8F+fs44TzOYLLJYrmJgLgYxz+0+zzGJiq2ccCUu/6oaEMbCUPFJAaw0nomHPPQ+j8QSMzKZomlJqDqczhNGMBgirm4WTcIqY6gnLrOc9jOxjTFyWNLBQGmpTEgWUKn+Q5wq5VNZstc2n9tuz2fbSA+HEagZWLqBR2huQDQXorsgpzlHoHIwnUFraXBvtG3rnI0CZ8xhd4tarwA0quA+P4aVX8HkTvmhgsGyjTwTyjrQG/ITyaRd+2sKQ8oC78MIO+VoYCdcud7rjKi7cI5z09nE42MPt/ACBaGIo6+i+neNhfIYBr1te1tdoD07xymroZzUMxQ06wyqe6FFmqTIDpY6RW5hFlGvwcrVlueWvfEX+nba2Z/sAr7dhEfh9br0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/59d57e691d6dc7c18a790cad83d2bb76/263a4/gms_open_10.webp 480w,
/static/59d57e691d6dc7c18a790cad83d2bb76/a6361/gms_open_10.webp 960w,
/static/59d57e691d6dc7c18a790cad83d2bb76/0b34d/gms_open_10.webp 1920w,
/static/59d57e691d6dc7c18a790cad83d2bb76/d5269/gms_open_10.webp 2560w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/59d57e691d6dc7c18a790cad83d2bb76/9aebd/gms_open_10.png 480w,
/static/59d57e691d6dc7c18a790cad83d2bb76/a91f8/gms_open_10.png 960w,
/static/59d57e691d6dc7c18a790cad83d2bb76/ac7a9/gms_open_10.png 1920w,
/static/59d57e691d6dc7c18a790cad83d2bb76/c7a69/gms_open_10.png 2560w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/59d57e691d6dc7c18a790cad83d2bb76/ac7a9/gms_open_10.png&quot; alt=&quot;출고 프로세스 개선 성과 지표&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;출고 프로세스 개선 성과 지표&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;출고량을 극대화하기 위해 출고 프로세스를 중점적으로 개선 하였으며,&lt;/p&gt;
&lt;p&gt;GMS 오픈 이전까지는 외부 WMS 솔루션을 사용해왔지만,&lt;/p&gt;
&lt;p&gt;자체 GMS를 통해 성능을 개선하고, 기존 외부 솔루션 대비 더 높은 출고 효율을 달성하는 것을 목표로 삼았습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;할당로직에서 과할당 없이 정확한 재고 할당을 보장하면서 성능까지 개선하는 작업은 결코 쉽지 않았습니다.&lt;/p&gt;
&lt;p&gt;우리는 차수 내에서 동일 Transaction 을 유지하여, 중복 할당 없이 &apos;Concurrency-safe&apos; 한 처리를 보장하도록 개선 하였고,&lt;/p&gt;
&lt;p&gt;할당 결과는 Bulk Commit 방식으로 처리하여 성능을 최적화하였습니다.&lt;/p&gt;
&lt;p&gt;또한, 할당 시 재고가 부족한 경우에는 Kafka 기반의 비동기 분산 처리 구조를 통해,&lt;/p&gt;
&lt;p&gt;할당 Transaction 과 분리된 보충 지시 프로세스가 자동으로 실행되도록 구성하였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;피킹 지시 단계에서는 피킹 동선에 따라 그룹을 분리하고, 이를 병렬 비동기(Parallel Async) 방식으로 처리하여 대량의 피킹 지시를 빠르게 수행할 수 있도록 최적화하였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;스쿼드원들의 적극적인 개선 시도 덕분에, 우리는 &lt;strong&gt;할당부터 피킹 지시까지의 소요 시간을 기존 대비 81% 단축&lt;/strong&gt;할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;주문 1,000건 기준으로 AS-IS 외부 솔루션과 GMS의 성능을 비교한 결과,
주문 할당은 280초 → 40초,
피킹리스트 생성은 40초 → 12초,
피킹라벨 출력은 40초 → 14초로
전반적인 출고 퍼포먼스가 개선되었습니다.&lt;/p&gt;
&lt;p&gt;이러한 성과로 &lt;strong&gt;3월 올영세일 대비 6월 올영세일의 출고 처리량(CAPA)을 50% 확대&lt;/strong&gt;하는 데 기여를 할 수 있었습니다! 👏🏻👏🏻👏🏻&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;gms-모니터링-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#gms-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;gms 모니터링 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;GMS 모니터링 소개&lt;/h2&gt;
&lt;p&gt;GMS(Global WMS) 대시보드는 &lt;strong&gt;올리브영 표준 모니터링 도구인 DataDog&lt;/strong&gt; 기반으로 구축되었습니다.&lt;/p&gt;
&lt;p&gt;서버 자원, API, Kafka(MSK) 모니터링뿐만 아니라, WMS 전반에서 발생하는 주요 이벤트들을 가능한 한 시각화하여 반영했습니다.&lt;/p&gt;
&lt;div class=&quot;side-by-side&quot;&gt;
    &lt;div style=&quot;display: inline-flex; width:100%;&quot;&gt;
      &lt;div style=&quot;width:52%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/29c937ec882c98fa1552c57a17a5caa9/53e1c/gms_open_11_01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 94.16666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAADLAAAAywAEoZFrbAAADxklEQVR42k1TW29cNRA+v5LLO5cHHrgIiaZBSPCARAstbZpQnqBAgYomKUWVsqW7XUFIgEhAGkgDaRKyCZtsNrs5Z4/v9vHHjM+mqqWR7fH4m/m+sbMVtS+WVKdaVp3ws9oLS3I3LEta671wf/BPuPvfn+Fu96/Hc7P3MHAsxyQ7Xau9iu7KbE8M9a4cYFccY6fso2sL7KkT2g+wM+pj66SX9mmd97BdHKXYDt3pyOET6wEO3Mhk3ngdXYQ3AV4H8LpytDYG3lo4mhEqVGRSmGSi1FDSIjiODwDPPnAMAfqgqxDhnEfgixWBe0/mYAnQOgKlPZ8b66GNhbZ0xkmdS4mtlKi4AKNNprTVQiiUooTSOoEYspKChJDQ2iQwrcknBJRSMHS5qipYOiuLgvySfJTcWJNpGtY4ctSXpDQJRBGgoQRSMoikRA794QiHxwWOBgWGuaBKbTJOaFNSM6bsSQbLFFgrphxqykSX51CFRNsQKDEicD+m7BEJkGnHEBAsV2icllJDyJL0MUk/S9qUUj2m7Ck4yUB7JWvKMcbUMMGUyzL5nHMmk5KUY4qKOkjBvOZDpqxJr0Td6ES5GAkUpcRJUUJSHBfAunPl3CzLgEw5Vkh0k/kqVenGlE+NZXA+pMs1ZZ98kdgEAo3MTiuTUUM0VydIfGpS0itRVqqmSIFhTFnzK6BqAyXjwZTlaESUBfVg3GWmzGVLAmBjMH4Cp5Q1ycDvjZPuHtCPOhiiQ3Y0LFMCQXH1c3P8rKgpzmlLnVVE49QkVXosC/RFnmygCgyoitnFv3Hjhw3cXt7EwsoWBMW5QAU4AvTEgB82PRlSFYj87ein8Ij0Wyp6l5V26RsmH2kbHYsdATqvOdMZfcEoSQIqCqRM9u5uW0/828CZRwt4Y3OB5gYmd2qbeNRI+zPrdzCxQbZdn9dxC5jcruPYd3b7e0xsNUw2013U5/abmOq38PHoR0wP23h/v4VznXu4dNTE9EkbU4dNnCeA5Os3UwzbhW4L5zstXOrR/eMWLvdbJltbNGqtbXFzSsZvZ6p466qLv7d0fNDW8caHIt68bOP8VR//aNu4el/Hry+W8Rv2feTirw0d13+ycXZaxK8+kJi9onS2sRpIS+C91x0mnzd49VmD1V/4CwLvvOTw1gsWrzxtsLZSgePeZt+LDi8/ZdC+U+t74azHm88FvPYM/eXNddfrdUNx+7rJ56+Z/PqMzh+uurx/GPK5T3U+/5nJv5gm3wOX97o+n7um81vsu6Lz35Zsng9D/t2XOp/7xBSfT+ne/1JCfCxI23poAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/29c937ec882c98fa1552c57a17a5caa9/263a4/gms_open_11_01.webp 480w,
/static/29c937ec882c98fa1552c57a17a5caa9/a6361/gms_open_11_01.webp 960w,
/static/29c937ec882c98fa1552c57a17a5caa9/0b34d/gms_open_11_01.webp 1920w,
/static/29c937ec882c98fa1552c57a17a5caa9/41513/gms_open_11_01.webp 2200w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/29c937ec882c98fa1552c57a17a5caa9/9aebd/gms_open_11_01.png 480w,
/static/29c937ec882c98fa1552c57a17a5caa9/a91f8/gms_open_11_01.png 960w,
/static/29c937ec882c98fa1552c57a17a5caa9/ac7a9/gms_open_11_01.png 1920w,
/static/29c937ec882c98fa1552c57a17a5caa9/53e1c/gms_open_11_01.png 2200w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/29c937ec882c98fa1552c57a17a5caa9/ac7a9/gms_open_11_01.png&quot; alt=&quot;GMS 자원 모니터링&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
      &lt;div style=&quot;width:48%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 965px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/753ba2b236aee8d39d515b9427cd9814/e7a62/gms_open_11_02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 101.45833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAADhElEQVR42nVUbY7kNBTM3TgAF+EoXAD2D1dA/ANpWISEVuxKI80PBjEjzXR3Op1PJ3YcJ7Hj1NZzM7Msgu6pcZy2y/XqvecsTL8h2hviZ2zj54j2LZz6CfnT9yiPP8B2P2Kf3iKMN/+LzOgCqn1KcPaCfVOIoXvFTtjxgr47cM0zenXAtrZ8rz4DCGdLZMsaMBNhQ8IWr4j/gMwD/8UY03zf/xuyjoQrmnlE3GVl5N8V8V8IwWNfqXjfsMs3kcgYXxHjhmwm4cH1cHGF58tpWxC4KcTAQ/Z00LoxAr/Sv5w2eMresHmfSLZwlbxtgohs9QH3xwL3zyUe8w4PpxrnSqHTGm2voUeLwWgsy4Li9ITT8YxLWaI4X9B2HYbeQBsDayxGMyKz3qGsWhg9phd64AJtoPqeGKBUj4G/hdHgXJSo2o4EY1pjxjGRpVFf92XBKFRlDTfPfKHRD8N1JGGnVBoNVXqtqKpA3XRXZXrAwLXy/AIhz4bJoigbOOe4gESKqqiy6z8tHkSNGlEUVXrfUbUcPPDgjgemfZyPPDgzk0sbJAxRNNMr+cS4p2yL4fKVBC3LCmsnSGXgNct7ms/zkqzJpmlGU7cYlGbYFjY3+P0QUHHBTBsG/m6o3pO76gxOeU7Pa0bRJ5+ttSkKEdPS38xxQ1srTCTrHwLyb1t8/W7El3cR1TijrDucCe+poK9I1qQs5/TzXFzQtC0JVQpfPM0kNClo+Ux3v6K7vcFEoq/eK7w3Uv4ebvXw24aSm8uyQnEpcSHkWfwUpZJIsS6bZ5dOdnPAZn8Bnr7Amz+/wze371IC/joe8Xg80duVSnqquxK+QDFUSYooFNLMuQlNU3DSwDJ8r+9w233AH/RICjqvWehNA79M6OsTK6JO6l4Iex4q5TK8KByZtYEZXhiWZNOzlSLbyFnHUKQcdMoiuxUL229ySwqzZfg1DwpswUg7tsD2JDKV6kqlOpSSUarF4UJVncXMzUbPmCYpi4Bl3ohAezyzT/JF/I0YZt4BLINp2a6ER5aCeDO5mTlYcJ8P+HCcpRrTrSKJi39fX1J3cpU9q4DHNhIbHpqAB465JqFhuHXTpp51JJTibZRJfs7019qBBxmq1HwmJpPq0zN8ua5klKvt+uzZemyzgnUlpBPDFr+k9aQjOqnDjpb0FlWjodgJk1vhloBmsOkykEoQr1eSravHRyMr+CT3dmh9AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/753ba2b236aee8d39d515b9427cd9814/263a4/gms_open_11_02.webp 480w,
/static/753ba2b236aee8d39d515b9427cd9814/a6361/gms_open_11_02.webp 960w,
/static/753ba2b236aee8d39d515b9427cd9814/d655e/gms_open_11_02.webp 965w&quot; sizes=&quot;(max-width: 965px) 100vw, 965px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/753ba2b236aee8d39d515b9427cd9814/9aebd/gms_open_11_02.png 480w,
/static/753ba2b236aee8d39d515b9427cd9814/a91f8/gms_open_11_02.png 960w,
/static/753ba2b236aee8d39d515b9427cd9814/e7a62/gms_open_11_02.png 965w&quot; sizes=&quot;(max-width: 965px) 100vw, 965px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/753ba2b236aee8d39d515b9427cd9814/e7a62/gms_open_11_02.png&quot; alt=&quot;GMS MSK 모니터링&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;side-by-side&quot;&gt;
    &lt;div style=&quot;display: inline-flex; width:100%;&quot;&gt;
      &lt;div style=&quot;width:48%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 965px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3a57f3d6b69a69d369cddcc8bfe7b072/e7a62/gms_open_11_03.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 85.83333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAACnElEQVR42qWUyW7UQBCG/Y6Q8AIcOCfhDBLPBEcOgIQ4RglJJstkmcV7t9vtpe2x/fNXTxKQQOSQkb6prqpeytW/HYyrH8DdV2DxDROt5/aLR+Lu8jOiw4+IDz+h5Rh/5P9FUOdLmPAKZTRHqxZAnWCq40c72gibMsTGhH48VfEj2zmJt2hSOLXihm2PiohtuwEjgG4Y4foRwzjhf79xmrDh3N7PHzyBKUtYaxFGMbI8R9s2UErRj6C1RlVVqOva26qu0DQNxnHAMAxcZ3C+UoiU5Zqc8wsEhSmRZrknSTOPJB58OeghHsWJj8sBTdui0AqzkAe5DfrOoWRhQa4M4jjDOoy5OPHjNGWFYeJZriJEUepz4ivON8ay4oZPpLGINA9wrJZPpksEtrIwVkMbhcJomLKArct7X3FSjqLUW8zWNm3FDg7se8W1ln1khRvHeI1gfq5xepTiTDjOMPuZ4fwk92NBcg95ycl4xvjNpcX8nD08Ubi+KDGfGVycagQf3lxh/+UMb3dntGd/cbB7hnevL/GeHLwSf+bZY27vxSn2d848BzuM0wY3c4XjwyUWNwY62SCLO+Rx/0gWiU+SjmOH9Z3FikTLCjlz8arxvnB9xQoL9iyMV+yNog55e66Co23F9o2o7Z7BY6sCcc7HjqzX4sD+ydpcp77/gdxYlivKIkecZJ6MtywxRfnUTQvnek/XbbwOk9zgaN1xswkTxV3XlFBBPfPmA1m0poiFVUjWoddenKTcrEHfy0adR8bWlrhdp/h+XbO+ySMvh+jUFAaB5p8IViqShPiCCF7E2/kNf9O2DrqsEBcO47B9YxrGKord8m0KZJG17Jdzvvzn4FyHYHziA7Bt/EDhbp6cJx+LYOCXYnxmZQ+M44hf64P9jaXFiVkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3a57f3d6b69a69d369cddcc8bfe7b072/263a4/gms_open_11_03.webp 480w,
/static/3a57f3d6b69a69d369cddcc8bfe7b072/a6361/gms_open_11_03.webp 960w,
/static/3a57f3d6b69a69d369cddcc8bfe7b072/d655e/gms_open_11_03.webp 965w&quot; sizes=&quot;(max-width: 965px) 100vw, 965px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3a57f3d6b69a69d369cddcc8bfe7b072/9aebd/gms_open_11_03.png 480w,
/static/3a57f3d6b69a69d369cddcc8bfe7b072/a91f8/gms_open_11_03.png 960w,
/static/3a57f3d6b69a69d369cddcc8bfe7b072/e7a62/gms_open_11_03.png 965w&quot; sizes=&quot;(max-width: 965px) 100vw, 965px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3a57f3d6b69a69d369cddcc8bfe7b072/e7a62/gms_open_11_03.png&quot; alt=&quot;GMS API 모니터링&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
      &lt;div style=&quot;width:52%;height:100%;&quot;&gt;
          &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 965px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/87353ad08bbcba81a84bc1efba122ff6/e7a62/gms_open_11_04.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 117.08333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEeklEQVR42n1UyY4jRRD1l/AHfAfighAXxIUL0hyA4cJIwwUhFiHEBX6DExoagTRMj7u9tZf2vrvbbVd5Kdfirewql10uP15kDyM1ElgKZ2RE5MuIF1EZi3ojnFpDnNoa0NZfrdrrvehWtqYEHf21798i9gMltnUWWBk2/PkKu8Va6euZjYC6azpYGhY21hwbe6F0sZ28HeAFD8UPELoeYv5uh/F0iunMRHg8otfvo9ZsUg+xXK9RqTew3myw9X0USmVMjBn+6xeGR8Q8Ag50HcPRGMcoQoVg6XxB6TPbxovLJFYbF97Op57AHWP/FzC0Vzg5a0RcQ2sJf2IpOVI/zOZKd3UDm7EJb2whMBwVGzmvznGNFMYKB56JHW+YWUeDNAfURaKu/lpwO8Gy3IFVaODEmKjLJt4wVnSek7On7v3ZkPaYu/Vg2g5WaxfB4YCQpW5omy+WWKzW2B9CRNGJti0c2kzHgdAklLi0zVcr6JMpgv1excbWBCqWK7hIpJBIZTDUR3DdDQrFkrIlM1lo+hgLguUKRVwkU0jRNiZIRFBZ//zrHDb59vwdu8y/KTs3GGoYaDpsh7wxg4lhYMi9xibM5wt4no/J9N4msmRmAujM5yhVanCZmL8LWPKGaS9XCIIAO4qUu+K4hGEIyd4iHRKzochlnucxdk+gBRzupdQlz4tPzsYWvKnWaKLZ7igee7e3qHL2luRvsVzy9iq2BBEO44mkqiQ6Rshfl1Cu1nAiv7f9OyTSGaxJVUz46t8NGKirg6PxhPshdZ9ZbdC7ueVQe8qXyRWg03/kB1Cp1dFotXE6nRTvueuiqiS23frYMlXhTcpW3eYqXVSdZpbCs2QsfEkWUq74hBaZAAGV30G6LACd3g1L7asbpKRWu6sADNNEs9Vh0wzsSHid1Ag9RdIg5Q61kcr2wZcixLe7PQXqssS74VCBSBcNft/1RkuNhpDfZoyA5TlSVQLf3t0RMHoIKCVLZuLY7w+qZClfRkLKkwtlvjbuFi6z3pHbgC/Lip1dcx+yzIePA4PzxTIz09QcyetyXaooYMO0kLrKqcMbw0Q2noDe7mHPJ6+czqKeu0ZI/k+vAA/qcTjskbm6Qrfbhej1Rh2FQoEgS06/hZcXF9i6a/jk8fzZGQbkjp8Nci9eonRxCbb/YYbjdYi+7WO4CKCvQgzmAWUHbXmg7HFjcqxsD33DRVdfYGB60J0d+lNXyWixx9g9KhkxPtaxTmhbQNOEWrs20PhnT72k+YhXprhs2CgOPbSde7+sLYk172OVGMwwrmn4pdXC5WSMhDnD+czAha7ht04H8fEYVyw77dh4ybizXg9Jlp61aONIPefrfkaqErQlLXJMW+zddAJv/vEMT4t5vH3+HG/8/ise5TL4JJvGk0IOnxeyeCv+Ah9mktxn8Zi+j+l7LxHHBzz76CqFT2n7iP5HV2nEnlRKeD91iZ+bdTzOZ/EO9W9qVfxQr+In2r6vVXhBFl9Wy/iRtm+5flEs4LPrPJ6WiviqUlS2rynfVSv4G7qGliElX39aAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/87353ad08bbcba81a84bc1efba122ff6/263a4/gms_open_11_04.webp 480w,
/static/87353ad08bbcba81a84bc1efba122ff6/a6361/gms_open_11_04.webp 960w,
/static/87353ad08bbcba81a84bc1efba122ff6/d655e/gms_open_11_04.webp 965w&quot; sizes=&quot;(max-width: 965px) 100vw, 965px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/87353ad08bbcba81a84bc1efba122ff6/9aebd/gms_open_11_04.png 480w,
/static/87353ad08bbcba81a84bc1efba122ff6/a91f8/gms_open_11_04.png 960w,
/static/87353ad08bbcba81a84bc1efba122ff6/e7a62/gms_open_11_04.png 965w&quot; sizes=&quot;(max-width: 965px) 100vw, 965px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/87353ad08bbcba81a84bc1efba122ff6/e7a62/gms_open_11_04.png&quot; alt=&quot;GMS 물류 업무 모니터링&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display: flex; width:100%; justify-content: center;&quot;&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;GMS 모니터링 대시보드&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;입고부터 출고까지의 처리량 및 실패 건수 등 주요 지표를 시각화하여, WMS 핵심 업무의 진행 상황을 한눈에 파악할 수 있도록 구성했습니다. GMS 뿐만 아니라 CBT(Cross Border Trade), WCS, Global Mall 등과의 연동 흐름도 함께 시각화하여 통합 대시보드로 제공하고 있습니다.
이처럼 WMS 주요 업무를 직관적이고 실시간으로 모니터링할 수 있도록 구성함으로써, 세일 기간과 같은 트래픽 집중 상황에서도 안정적으로 운영할 수 있었습니다.&lt;/p&gt;
&lt;div class=&quot;img-box&quot;&gt;
    &lt;div style=&quot;width: 550px !important; max-width: 100%;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 732px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/36e67e658817d3fbdfa105bbd853c5f5/61a54/gms_open_12.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.95833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABoUlEQVR42o1S2W7bMBD0V8RKLFsWRVKkeIg6HF9xgzRFgAYF+tb//5PJkrLaPBR1H0azw10tZ8FdqEpi7wa48zeI3ReI/Suq4UJ4SjrGx9MruA7YbBmKkv8Ti7Jg+CUNvj8+wY5H9OMej4cTht0BY8ThjPfzBUpqaljdbsiKCu+swM+jwstJwe1P8F9/wByeoS5v0L4HVwa81jebpYZiw+DrGjY4eKOhXUCpHAqusCXnBRPYluTsP9xNDWlkz2vIxoMJBaUNmsYRW+jICZSLFzA5obqCfWK6+LfDlksYZ2GaBsaYCdbARlDsrE1szcTeuVQz56Kua4V8U04OQ8VhXYNgNTqnESj2duKWuL2ez7G3cxzrFFqqq6XA+k/Dim6ksQUH5xyakroWqUgShBD0yqSJJYEnLSlHEBPn6y01ZNPIA72g63qs8gL3D2tkhOV9joww67vlCsssT+czss/IVleH9OnqBh3tnPUB2nj4tkcbeoR+JO3SuVRN+iE6+RtW0SFtwqKikTW9lOtGdP0OFb20odVpSfswgNNCzzrmbi33B+qnP++256WdAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/36e67e658817d3fbdfa105bbd853c5f5/263a4/gms_open_12.webp 480w,
/static/36e67e658817d3fbdfa105bbd853c5f5/02596/gms_open_12.webp 732w&quot; sizes=&quot;(max-width: 732px) 100vw, 732px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/36e67e658817d3fbdfa105bbd853c5f5/9aebd/gms_open_12.png 480w,
/static/36e67e658817d3fbdfa105bbd853c5f5/61a54/gms_open_12.png 732w&quot; sizes=&quot;(max-width: 732px) 100vw, 732px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/36e67e658817d3fbdfa105bbd853c5f5/61a54/gms_open_12.png&quot; alt=&quot;Slack 내 DataDog 알림 메시지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption style=&quot;text-align: center;&quot;&gt;Slack 내 DataDog 알림 메시지&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;대시보드에서 수집한 Custom Metric 지표와 로그는 Slack 알림과 연동하여 자원 상태 및 에러 상황을 빠르게 감지할 수 있습니다. GMS의 안정적 운영에 크게 기여하고 있지요.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;끝으로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%81%9D%EC%9C%BC%EB%A1%9C&quot; aria-label=&quot;끝으로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;끝으로&lt;/h2&gt;
&lt;p&gt;지금까지 올리브영 GMS(Global WMS)의 오픈 과정과 핵심 업무, 기술 개선 사례, 그리고 모니터링 방식에 대해 소개드렸습니다.&lt;/p&gt;
&lt;p&gt;이번 글은 올리브영 최초의 WMS 내재화 프로젝트인 GMS를 소개하기 위해 작성되었습니다.
GMS 는 올리브영 WMS의 시작점이며, 앞으로도 지속적으로 고도화되어 나갈 예정입니다.&lt;/p&gt;
&lt;p&gt;올리브영 WMS와 풀필먼트가 성장할 수 있도록 많은 관심부탁드립니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이전 글도 많은 관심부탁드립니다.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://oliveyoung.tech/2024-10-16/oliveyoung-scm-oms-kafka/&quot;&gt;
Kafka 메시지 중복 및 유실 케이스별 해결 방법
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;👉 &lt;a href=&quot;https://oliveyoung.tech/2023-10-04/oliveyoung-b2b-msk-connect-introduction&quot;&gt;
AWS MSK Connect 효과적으로 운영하기
&lt;/a&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;지금까지 올리브영 풀필먼트 스쿼드의 시나브로우였습니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[개발자가 알면 좋은 Redis 꿀팁 모음]]></title><description><![CDATA[들어가며 Redis는 현대 백엔드 시스템 아키텍처에서 빼놓을 수 없는 핵심 구성 요소입니다. 인메모리 데이터 구조 저장소로서 캐싱뿐 아니라 세션 관리, 메시지 브로커 등 다양한 케이스에 유연하고 쉽게 적용할 수 있습니다. 특히 StackOverflow…]]></description><link>https://oliveyoung.tech/2025-07-23/redis-tips-for-developer/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-07-23/redis-tips-for-developer/</guid><pubDate>Wed, 23 Jul 2025 16:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;들어가며&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;Redis는 현대 백엔드 시스템 아키텍처에서 빼놓을 수 없는 핵심 구성 요소입니다. 인메모리 데이터 구조 저장소로서 캐싱뿐 아니라 세션 관리, 메시지 브로커 등 다양한 케이스에 유연하고 쉽게 적용할 수 있습니다. 특히 StackOverflow &lt;a href=&quot;https://survey.stackoverflow.co/2024/technology#most-popular-technologies-database-prof&quot;&gt;2024년 가장 인기있는 데이터베이스 기술&lt;/a&gt;에서 6위를 차지하며(NoSQL중 2위), 많은 개발자들이 애정하는 데이터베이스로 자리매김했습니다.&lt;/p&gt;
&lt;p&gt;저 역시 백엔드 개발자로 다양한 서비스를 운영하며 Redis와 함께해왔는데요. 지금까지 많은 도움을 얻었지만, 돌이켜보니 Redis 초기 도입 과정에서 &quot;이걸 미리 알았더라면 더 수월했을 텐데...&quot;라는 아쉬움이 남는 순간들이 꽤 있었습니다. 🤔
이번 포스트에서는 그때 겪었던 시행착오를 바탕으로, Redis를 도입하거나 활용도를 높이려는 개발자분들께 도움이 될 만한 실전 팁들을 공유해보려 합니다. 제가 겪은 어려움을 반복하지 않으시길 바라는 마음으로 정리한 Redis 사용 꿀팁, 함께 살펴보시죠!&lt;/p&gt;
&lt;h1 id=&quot;반드시-ttltime-to-live을-설정하세요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%98%EB%93%9C%EC%8B%9C-ttltime-to-live%EC%9D%84-%EC%84%A4%EC%A0%95%ED%95%98%EC%84%B8%EC%9A%94&quot; aria-label=&quot;반드시 ttltime to live을 설정하세요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;반드시 TTL(Time-To-Live)을 설정하세요&lt;/h1&gt;
&lt;p&gt;Redis에 데이터를 저장할 때 TTL(Time-To-Live, 데이터 유효 시간)을 설정하는 것은 단순한 선택 사항이 아닌 필수적인 설정입니다.
많은 개발자들이 이 사실을 알고 이행합니다.
그렇다면 왜 TTL을 설정해야할까요? 두가지 관점으로 설명해보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;소중한-메모리-자원-낭비-방지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8C%EC%A4%91%ED%95%9C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%9E%90%EC%9B%90-%EB%82%AD%EB%B9%84-%EB%B0%A9%EC%A7%80&quot; aria-label=&quot;소중한 메모리 자원 낭비 방지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;소중한 메모리 자원 낭비 방지&lt;/h2&gt;
&lt;p&gt;Redis는 빠른 속도를 자랑하는 인메모리(In-Memory) 데이터 저장소입니다. 하지만 메모리는 용량이 한정적이고 비용이 비싼 자원이기 때문에 효율적인 관리가 매우 중요합니다.
TTL이 없는 데이터는 메모리에 계속 쌓이게 됩니다.&lt;/p&gt;
&lt;p&gt;이렇게되면 결국 물리적인 메모리 한계를 초과하여 메모리 고갈(OOM) 현상을 야기할 수도 있습니다. OOM이 발생하면 Redis 서버는 더 이상 새로운 데이터를 쓰지 못하거나, 심각한 경우 서버가 다운되어 전체 시스템 장애로 이어질 수 있습니다.
OOM이 발생하지 않더라도 불필요한 데이터가 메모리를 계속 점유하고 있다면 비싼 메모리 자원을 낭비하는 것과 같습니다. 더 많은 메모리를 확보하기 위해 스케일업(Scale-up)을 하는 상황을 피할 수 있게 해줍니다.
OOM이 발생하면 Redis 서버는 더 이상 새로운 데이터를 쓰지 못하거나, 심각한 경우 서버가 다운되어 전체 시스템 장애로 이어질 수 있습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;서비스-운영에도-도움이-돼요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9A%B4%EC%98%81%EC%97%90%EB%8F%84-%EB%8F%84%EC%9B%80%EC%9D%B4-%EB%8F%BC%EC%9A%94&quot; aria-label=&quot;서비스 운영에도 도움이 돼요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;서비스 운영에도 도움이 돼요&lt;/h2&gt;
&lt;p&gt;인프라 자원 외에도 개발 관점에서도 TTL 있다는 것은 복잡도를 낮추고 데이터의 정합성을 유지하는 데 큰 장점을 가집니다.&lt;/p&gt;
&lt;h3 id=&quot;데이터의-최신성-보장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%B5%9C%EC%8B%A0%EC%84%B1-%EB%B3%B4%EC%9E%A5&quot; aria-label=&quot;데이터의 최신성 보장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터의 최신성 보장&lt;/h3&gt;
&lt;p&gt;많은 경우 데이터는 특정 시간 동안만 유효합니다. 예를 들어, &apos;오늘만 보이는 팝업&apos;, &apos;1시간 동안 유효한 인증번호&apos;, &apos;5분간 유지되는 세션 정보&apos; 등이 대표적입니다.
만약 TTL을 사용하지 않는다면, 개발자는 직접 데이터의 생성 시간을 기록하고, 조회 시마다 현재 시간과 비교하여 데이터의 유효성을 검사하고, 별도의 스케줄러를 통해 기간이 만료된 데이터를 삭제하는 로직을 모두 구현해야 합니다.
TTL을 설정하면 이러한 복잡한 로직을 Redis에 위임하여 애플리케이션 코드를 단순하고 명료하게 유지할 수 있으며, 데이터가 항상 최신 상태로 유지되는 것을 보장할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;캐시-데이터의-정합성-유지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BA%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%A0%95%ED%95%A9%EC%84%B1-%EC%9C%A0%EC%A7%80&quot; aria-label=&quot;캐시 데이터의 정합성 유지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;캐시 데이터의 정합성 유지&lt;/h3&gt;
&lt;p&gt;Redis는 주로 데이터베이스의 부하를 줄이기 위한 캐시(Cache) 서버로 많이 활용됩니다.
캐시 데이터는 원본 데이터베이스(Source of Truth)의 복사본이므로, 시간이 지나면 원본 데이터와의 정합성이 깨질 수 있습니다.
TTL을 설정하면 캐시 데이터가 일정 시간 후에 자동으로 삭제되도록 하여, 주기적으로 원본 데이터베이스에서 새로운 데이터를 가져오게끔 유도할 수 있습니다. 이를 통해 사용자는 오래된 캐시 데이터가 아닌, 정합성이 보장된 데이터를 제공받을 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;장애-상황에서의-데이터-자동-정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%95%A0-%EC%83%81%ED%99%A9%EC%97%90%EC%84%9C%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%90%EB%8F%99-%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;장애 상황에서의 데이터 자동 정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;장애 상황에서의 데이터 자동 정리&lt;/h3&gt;
&lt;p&gt;특정 작업 중에만 일시적으로 사용되는 데이터(예: 작업 진행 상태를 나타내는 플래그)가 있다고 가정해 보겠습니다.
만약 애플리케이션에 예기치 않은 장애가 발생하여 정상적으로 데이터를 삭제하는 로직을 수행하지 못하고 종료된다면, 이 임시 데이터는 &apos;쓰레기 데이터&apos;가 되어 메모리에 영원히 남게 됩니다.
TTL을 설정해두면 애플리케이션에 장애가 발생하더라도 정해진 시간 후에 Redis가 알아서 데이터를 정리해주므로, 시스템의 안정성과 데이터의 일관성을 높일 수 있습니다.&lt;/p&gt;
&lt;p&gt;이처럼 Redis에서 TTL을 설정하는 것은 단순히 메모리를 아끼는 차원을 넘어, 시스템의 안정성을 담보하고, 개발의 복잡성을 낮추며, 데이터의 정합성을 보장하는 좋은 습관입니다.
따라서 특별한 이유가 없는 한, Redis에 저장되는 모든 데이터에는 반드시 적절한 TTL을 설정하는 습관을 들이는 것이 좋습니다.&lt;/p&gt;
&lt;h1 id=&quot;하나의-키에-사이즈가-큰-데이터를-저장하지마세요big-key-problem&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%98%EB%82%98%EC%9D%98-%ED%82%A4%EC%97%90-%EC%82%AC%EC%9D%B4%EC%A6%88%EA%B0%80-%ED%81%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%A0%80%EC%9E%A5%ED%95%98%EC%A7%80%EB%A7%88%EC%84%B8%EC%9A%94big-key-problem&quot; aria-label=&quot;하나의 키에 사이즈가 큰 데이터를 저장하지마세요big key problem permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;하나의 키에 사이즈가 큰 데이터를 저장하지마세요(Big Key Problem)&lt;/h1&gt;
&lt;p&gt;Redis에 데이터를 저장할 때 사이즈가 너무 큰 데이터를 저장하지 않도록 주의해야합니다.
(일반적으로 크기가 1MB를 초과하는 문자열 유형 값이나 10,000개가 넘는 요소를 포함하는 컬렉션 유형 키를 사이즈가 큰 데이터로 간주합니다.)&lt;/p&gt;
&lt;p&gt;부끄러운 일화 하나를 소개할게요. 어느날 SRE 파트에서 특정 노드의 송/수신 바이트 수가 이상하게 많다는 제보가 있었습니다. 알고 보니 그 원인은 저였습니다🫣. 하나의 String 자료형에 10MB가 넘는 데이터를 저장했기 때문이었죠😂&lt;/p&gt;
&lt;p&gt;왜 하나의 키에 사이즈가 큰 데이터를 저장하면 안될까요? 다음과 같은 이유가 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;메모리-소비-및-단편화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%86%8C%EB%B9%84-%EB%B0%8F-%EB%8B%A8%ED%8E%B8%ED%99%94&quot; aria-label=&quot;메모리 소비 및 단편화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;메모리 소비 및 단편화&lt;/h2&gt;
&lt;p&gt;대용량 String은 상당한 양의 RAM을 직접적으로 소비합니다. 큰 연속 블록을 저장하면 메모리 단편화(fragmentation)를 유발하여, 사용 가능한 전체 메모리가 충분하더라도 Redis가 효율적으로 메모리를 할당하기 어렵게 만들 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;성능-저하&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5-%EC%A0%80%ED%95%98&quot; aria-label=&quot;성능 저하 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;성능 저하&lt;/h3&gt;
&lt;p&gt;많은 양의 메모리를 차지하여 메모리 단편화를 심화시키고 Redis 성능에 영향을 미칩니다. 읽기, 쓰기, 삭제와 같은 큰 키 관련 작업은 더 많은 CPU 시간과 메모리 리소스를 소모하여 시스템 성능을 더욱 저하시킵니다.&lt;/p&gt;
&lt;h3 id=&quot;과도한-네트워크-리소스-유발&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%BC%EB%8F%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%9C%A0%EB%B0%9C&quot; aria-label=&quot;과도한 네트워크 리소스 유발 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;과도한 네트워크 리소스 유발&lt;/h3&gt;
&lt;p&gt;하나의 키에서 대량의 네트워크 트래픽이 발생하여 네트워크 대역폭이 포화되고 서비스에 영향을 미칠 수 있습니다. 예를 들어, 큰 키의 크기가 1MB이고 초당 1,000회 액세스되는 경우 1,000MB(1GB)의 트래픽이 발생합니다.&lt;/p&gt;
&lt;h3 id=&quot;동기화-지연&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%99%EA%B8%B0%ED%99%94-%EC%A7%80%EC%97%B0&quot; aria-label=&quot;동기화 지연 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;동기화 지연&lt;/h3&gt;
&lt;p&gt;사이즈가 너무 큰 데이터는 동기화 지연을 발생시킬 수 있습니다. 예를 들어, RDB 스냅샷과 AOF 파일의 크기를 증가시켜 영속화 작업의 속도를 늦추고 더 많은 리소스를 소모할 수 있습니다&lt;/p&gt;
&lt;h3 id=&quot;데이터-불균형&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%88%EA%B7%A0%ED%98%95&quot; aria-label=&quot;데이터 불균형 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터 불균형&lt;/h3&gt;
&lt;p&gt;Redis 클러스터 모드에서 특정 샤드에 데이터가 편중되어 다른 샤드보다 메모리를 훨씬 많이 사용하는 메모리 불균형이 발생할 수 있습니다.
이 상태에서 특정 샤드의 메모리 사용량이 maxmemory에 설정된 임계값에 도달하면, 메모리 정책에 따라 키를 제거하게 되면 중요한 데이터가 의도치 않게 삭제될 위험이 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c23443ae9bfbaeab7ad4660c05d5c327/c4c6f/big-key.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.874999999999996%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+UlEQVR42o1R2XKEIBD0//8xieKJCCKLoIJ0BnaT2sftqq5hrh4GKmMMYoz4FCmlf+LtXHxCdV0X7vtG+mt4MeM4DtiH/WhQCKGwyrdLiQSzaMw24SabYfSGtm4Qr4CTxMN5QgmJsRsgxVLyq1SYRw72XWPqh6egPU4Ms8BMSa5WTIuEMg+MFOs5x0/D0FJT3faoWYduohhr0Q4TvmoGRkJN14PTkArOwUsJuWkYa7FqDSFWaG0hKO68x6IUNq2wLDPMvsN6V3IXrWipP9J22ebaylmPgY3YSFDRDRUVKkmCSpc1dxJwu4c1nDjD+4P8vcQDvf878l/8Au3b0BlXFrxbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c23443ae9bfbaeab7ad4660c05d5c327/263a4/big-key.webp 480w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/a6361/big-key.webp 960w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/0b34d/big-key.webp 1920w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/da28f/big-key.webp 2880w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/4411a/big-key.webp 3060w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c23443ae9bfbaeab7ad4660c05d5c327/9aebd/big-key.png 480w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/a91f8/big-key.png 960w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/ac7a9/big-key.png 1920w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/f9c26/big-key.png 2880w,
/static/c23443ae9bfbaeab7ad4660c05d5c327/c4c6f/big-key.png 3060w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c23443ae9bfbaeab7ad4660c05d5c327/ac7a9/big-key.png&quot; alt=&quot;당시의 그래프&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;특정 노드의 Byte 지표가 유독 많은걸 확인할 수 있는 당시의 그래프😂&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;따라서 Redis에 데이터를 저장할 때는 데이터의 사이즈가 과하게 크지 않나 항상 주의해야합니다.&lt;/p&gt;
&lt;h1 id=&quot;redis의-다양한-data-type을-활용해보세요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#redis%EC%9D%98-%EB%8B%A4%EC%96%91%ED%95%9C-data-type%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%84%B8%EC%9A%94&quot; aria-label=&quot;redis의 다양한 data type을 활용해보세요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Redis의 다양한 Data Type을 활용해보세요&lt;/h1&gt;
&lt;p&gt;Redis를 캐싱과 세션 관리 용도로 사용하는 경우가 많은데, 이 외에도 활용 범위가 넓습니다.
그중에서 Data Type을 활용한 몇가지 사례를 소개합니다. 서비스 운영에 인사이트를 얻을 수 있을 거에요.&lt;/p&gt;
&lt;h3 id=&quot;정렬이-필요하면-sorted-set을-고려해보세요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%A0%AC%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%98%EB%A9%B4-sorted-set%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%B4%EB%B3%B4%EC%84%B8%EC%9A%94&quot; aria-label=&quot;정렬이 필요하면 sorted set을 고려해보세요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;정렬이 필요하면 Sorted Set을 고려해보세요&lt;/h3&gt;
&lt;p&gt;저의 사례를 소개하자면 저는 올리브영의 커뮤니티 서비스 &apos;셔터&apos;를 개발하면서 Redis의 Sorted Set을 활용하고 있습니다.
Sorted Set은 Redis가 지원하는 Data Type으로 하나의 key에 여러 Score와 Value로 구성된 자료구조입니다. value는 중복되지 않으며 score 기준으로 정렬될기 때문에 랭킹과 같은 정렬이 필요할 때 매우 유용한 Redis의 Data Type입니다.&lt;/p&gt;
&lt;p&gt;셔터에서는 유저의 최근 행동 데이터를 근거로 추천 피드를 구성합니다. Sorted set은 유저가 동작한 시간을 timestamp로 변환해 score에 저장하는 방식으로 활용됩니다.
유저의 행동 데이터를 시간순으로 정렬할 수 있기 때문에 유저의 최근 행동을 기반한 추천 게시물을 피드로 노출할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/647885e3a5bde51e78bd970db17c8ff1/8c899/shutter-feed.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACY0lEQVR42o2Sy08TURSHR/8Bt/oHaNCoUYMbXJgYnyujCSGC6MJXYly4IMaYYAhBQaMgCxBYABpEA6SQKO+WFgKCUKCAQK1t6Ys2mPCaPuhr5nNmAOmChb/ky7k3d+4593fOCP4IzKzCTwXbOiRldiRJxK3zxLzuzb0sbwUZ3edG6t9X0t3ainFqgL5JE5OWcYSL3TL7GmROtUnsbwL93CLtumY6DX04igoxn81g4OQxRKeD76Oj1NfV8Wdpifx7uVxPP8ylE0c5dzWDrOIblLaUItwfkknTyVzplkj/BrOBILbZGebsduzVVXSkHWT41k02Vlbw+f38GBlhdW2NwbIC3mWeJ/f0EQ7s3cPtnGz0Q3oE1UU0CeEEJKQdt/KWPVF5TSIWI1WJRIIFXS2Db5/TU/SEa8cPcTcnk3Z9JwK7KKYkUInH48QU5JTeSSqSTNTUhs/YRmvjR6rfFFNa/JQPDbUIktJ49XIkEvkXl5eXEUVRW0ejUUKhkBbVb7WCyQTm8pc4P1VSU/iMvIcPuHD5DC9KChCCQZHp6UkmJsawWmdx2H9hMhkY6DficTs11LNFn1srqGojnqC95jUTFfn0vsqj6lE2d7IyKSuvQFCru112vN4FPB4nPiUaDD0afr8Hl3JmsZjx+VxKwuimdYWFsa+MDDdi1DfQW/AYe78Jt9OHkFQaHA6HFMIaaoFAIKCh7oPBkNYCdb1tWZXXNYrDqsNi/EJfST5THV1Yxqd3H8r/yG7uxzrUxJLbRmB+hnFDF81NLbsnVKe5PdnUXyhVosdGWAwSl2Tt5Yvu36yvrfIXdPD7xteq0y0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/647885e3a5bde51e78bd970db17c8ff1/263a4/shutter-feed.webp 480w,
/static/647885e3a5bde51e78bd970db17c8ff1/a6361/shutter-feed.webp 960w,
/static/647885e3a5bde51e78bd970db17c8ff1/0b34d/shutter-feed.webp 1920w,
/static/647885e3a5bde51e78bd970db17c8ff1/77152/shutter-feed.webp 2240w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/647885e3a5bde51e78bd970db17c8ff1/9aebd/shutter-feed.png 480w,
/static/647885e3a5bde51e78bd970db17c8ff1/a91f8/shutter-feed.png 960w,
/static/647885e3a5bde51e78bd970db17c8ff1/ac7a9/shutter-feed.png 1920w,
/static/647885e3a5bde51e78bd970db17c8ff1/8c899/shutter-feed.png 2240w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/647885e3a5bde51e78bd970db17c8ff1/ac7a9/shutter-feed.png&quot; alt=&quot;셔터 피드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;Sorted set을 이용한 시간순 정렬&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;객체를-그대로-저장하기-보단-hash를-사용하세요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0-%EB%B3%B4%EB%8B%A8-hash%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%84%B8%EC%9A%94&quot; aria-label=&quot;객체를 그대로 저장하기 보단 hash를 사용하세요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;객체를 그대로 저장하기 보단 Hash를 사용하세요&lt;/h3&gt;
&lt;p&gt;Redis에 데이터를 저장할 때 객체를 직접 직렬화(Serialization)하여 String 형태로 저장하시나요?
그럴 경우 새로운 필드가 추가/삭제 될 경우 곤란해질 수도 있습니다. 예를 들어보겠습니다.&lt;/p&gt;
&lt;p&gt;상황 1: 객체를 직접 직렬화한 경우&lt;/p&gt;
&lt;p&gt;초기 버전 (V1): User 클래스에 id와 name만 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Serializable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 V1 객체를 직렬화하여 user:1 키에 저장했습니다. 이 데이터 안에는 id와 name에 대한 정보만 바이너리 형태로 들어있습니다.&lt;/p&gt;
&lt;p&gt;요구사항 변경 (V2): email 필드를 추가해야 하는 상황이 발생했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;User&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Serializable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; email&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 필드 추가!&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;문제 발생:
새로운 V2 코드가 Redis에서 user:1 데이터를 읽어옵니다.&lt;/p&gt;
&lt;p&gt;V2 코드는 이 데이터를 V2 User 객체로 역직렬화하려고 시도합니다.
하지만 데이터 안에는 email 필드에 대한 정보가 없으므로, 역직렬화 과정에서 InvalidClassException과 같은 심각한 오류가 발생하며 애플리케이션이 중단될 수 있습니다.
이처럼 데이터의 구조가 코드의 클래스 구조와 강하게 결합(Tightly Coupled)되어 있어, 코드 변경이 데이터의 호환성을 깨트리는 결과를 낳습니다.
이러한 문제를 해결하는 좋은 방법은 Redis의 해시(Hash) 데이터 타입으로 저장하는 것입니다.
해시는 하나의 키(Key) 아래에 여러 개의 필드(Field)와 값(Value)을 가질 수 있는 자료구조로, 객체를 표현하기에 좋습니다.&lt;/p&gt;
&lt;p&gt;상황 2: 해시(Hash)를 사용한 경우
초기 버전 (V1): User 클래스에 id와 name만 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;HSET user:1 id &quot;1&quot; name &quot;Alice&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;요구사항 변경 (V2): email 필드를 추가해야 하는 상황이 발생했습니다.&lt;/p&gt;
&lt;p&gt;새로운 사용자를 등록하는 V2 코드는 이제 email 필드도 함께 저장합니다.
기존 사용자 정보에 이메일을 추가해야 한다면, 간단히 HSET 명령만 실행하면 됩니다&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;HSET user:1 email &quot;alice@example.com&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;V2 코드가 user:1 데이터를 읽으면 id, name, email 세 필드를 모두 정상적으로 가져와 사용합니다.&lt;/p&gt;
&lt;p&gt;만약 아직 업데이트되지 않은 V1 코드가 user:1 데이터를 읽더라도, V1 코드는 자신이 아는 id와 name 필드만 사용하고 모르는 email 필드는 그냥 무시합니다. 오류가 발생하지 않습니다.&lt;/p&gt;
&lt;p&gt;이렇게 Hash를 이용하면 필드가 추가/삭제되어도 유연하게 대응할 수 있습니다.
이 외에도 다양한 Data Type의 활용 사례들이 많습니다. 서비스 개발에 앞서 사용할 만한 &lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/&quot;&gt;Redis Data Type&lt;/a&gt;이 있을지 살펴보시길 권장합니다.&lt;/p&gt;
&lt;h1 id=&quot;핫키hot-key-만료시-주의가-필요해요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%AB%ED%82%A4hot-key-%EB%A7%8C%EB%A3%8C%EC%8B%9C-%EC%A3%BC%EC%9D%98%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%B4%EC%9A%94&quot; aria-label=&quot;핫키hot key 만료시 주의가 필요해요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;핫키(Hot Key) 만료시 주의가 필요해요&lt;/h1&gt;
&lt;p&gt;많은 분들이 Redis를 사용할 때 Redis는 메인 데이터베이스가 아닌 원천 데이터베이스의 캐싱을 위해 많이 사용합니다
앞서 말했듯이 캐싱이 목적이라면, Redis에 데이터를 저장할 때 TTL을 설정해야 합니다.
그리고 캐싱하려 저장한 데이터의 TTL이 만료되면, 일반적으로 원천 데이터베이스를 조회합니다.&lt;/p&gt;
&lt;p&gt;요청이 집중되는 캐시 키를 흔히 &apos;핫키(Hot Key)&apos;라 부릅니다. 만약 핫키의 TTL이 만료되면 어떻게 될까요?
Redis로 관리하던 수많은 요청이 원천 데이터베이스로 몰리게 됩니다.
원천 데이터베이스는 Redis만큼 빠르게 처리할 수 없으니 부하가 발생합니다. 결국 전체 서비스의 장애로 이어질 수도 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1204px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d90d3c8ff189e9b0d78c8d43a91a60f4/1ea01/hot-key.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 124.79166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAYAAAAxFw7TAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC80lEQVR42rWVSWgTURzGBz2LouJVcUNQ6naQqtiK4sGDKG4Xxd0evIhIFUQPggt48C4uB8WDUqhr69K6UKzGWlRwSVu0aSemTZo0yWSZmcy8n/+ZGDdUNMEH33szA+97/+V732iUO5TyFzvUSybwFCeX9d+1vAM722BNK6x7oFh5D46/8jY4mJZFX18foVCInp4eYrEYruNQENj5PL0nj/Hp0AGC61fTvryWTPA92rAJY84rqhoUS28pxl5U1F5TmHJiOp0mKiTRaJTBwUESiQQZw8DIZhmORLg7p4qWGdN4PH82jaNHEWluQssVYG2roqZJseq+olpID3YU01F/SFW5Lm9OnqCpahY3p08lULebjN6PVtroyuSo4qr+oYa5/jDpN++/ftZ+FYn66764WNjkZHal5t4hWlkN/nLkcCHGkd7N1AVraU/f+RbhvxO6/tprBoVwE0dD27gWP185oW5+YFfXEja8m8n1+IXyCUtVLiibttRtbicuEbXDlRD+flRM6OL4HS41qjJCT4sicB9K/aeUPV7DFk1ZkBSkBAX3p7QkAkcMQakfJa/SKTItd0k0XMH69KUpL4Zg0lXFHDGEuYLxlxWnXnsbbUy7QF5cJRwOEwwGGRgY8InlTpDs6uLlxrV83LWFZ/Nm8XBxNbaYh9aqw4gzikU3FAsFI88q9rZJiE6WpJEhkykiKw7jrwLDNBno7OTGhHE0T57oo2n6FPLiSJouvljfAfsCsP857GmHe7qLmzewJEIvolQqRTwe9+3LEPvyhiORd587x4MVy3i8ehWhxkbfgbRfN09h27YfkUfkGayu637KnkeW7MuvbzaHMq0fm1Kyre/xM7mXsgfv2fWlAqZor7/hNP3NZ6TiRQ/U/l5y36IeSgxJiRUd+UfUt1VTH6jhrdlZvg7tguVf54fDjRwOb+eIvoPnqZbyCX0zlRFI32dr9wLqPtTwLtdR+U2xHYuXkac86W4lmUpWTug1x0hmiUWGRJ/F//Jn7j9YC/yo/TUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d90d3c8ff189e9b0d78c8d43a91a60f4/263a4/hot-key.webp 480w,
/static/d90d3c8ff189e9b0d78c8d43a91a60f4/a6361/hot-key.webp 960w,
/static/d90d3c8ff189e9b0d78c8d43a91a60f4/dbf17/hot-key.webp 1204w&quot; sizes=&quot;(max-width: 1204px) 100vw, 1204px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d90d3c8ff189e9b0d78c8d43a91a60f4/9aebd/hot-key.png 480w,
/static/d90d3c8ff189e9b0d78c8d43a91a60f4/a91f8/hot-key.png 960w,
/static/d90d3c8ff189e9b0d78c8d43a91a60f4/1ea01/hot-key.png 1204w&quot; sizes=&quot;(max-width: 1204px) 100vw, 1204px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d90d3c8ff189e9b0d78c8d43a91a60f4/1ea01/hot-key.png&quot; alt=&quot;핫키&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;핫 키 만료가 원본 DB 장애로 이어지는 과정&lt;/figcaption&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;이러한 현상을 해결하기 위해 백그라운드에서 주기적으로 캐시를 갱신하거나 매 요청마다 일정 확률로 캐시를 갱신하는 PER 알고리즘을 적용하는 방식을 고려해보시길 권장합니다.&lt;/p&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h1&gt;
&lt;p&gt;개발자로서 Redis를 사용할 때 도움이 될만한 몇가지를 소개해봤습니다.
잘 알려진 내용이라 생각되는 문항들도 있겠지만, Redis를 잘 아시는 분들에게는 리마인드되는 좋은 기회가 되었으면 좋겠습니다.
Redis를 처음 사용하시거나 도입을 검토하는 분들에게 실용적인 팁이 되길 바랍니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 1. 마이크로프론트엔드 너 뭐야?]]></title><description><![CDATA[안녕하세요! 방탈출과 보드게임을 좋아하는 올리브영의 프론트엔드 개발자 애플🍎입니다.

디플롯 서비스에 마이크로프론트엔드 아키텍처를 적용할지 검토하면서 MFE(Micro Frontend…]]></description><link>https://oliveyoung.tech/2025-07-22/what-is-MFE-part1/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-07-22/what-is-MFE-part1/</guid><pubDate>Tue, 22 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 방탈출과 보드게임을 좋아하는 올리브영의 프론트엔드 개발자 &lt;strong&gt;애플&lt;/strong&gt;🍎입니다.
&lt;br /&gt;&lt;br /&gt;
디플롯 서비스에 마이크로프론트엔드 아키텍처를 적용할지 검토하면서 MFE(Micro Frontend, 마이크로프론트엔드)에 대해 본격적으로 깊이 파고들게 되었습니다.
이 시리즈는 그 과정에서 정리한 내용을 바탕으로 마이크로프론트엔드의 개념부터 구조, 장단점, PoC까지 한 번에 담아보려고 합니다.
이어지는 Part 2에서는 Module Federation PoC를, Part 3에서는 Nx를 활용한 마이크로프론트엔드 과정을 생생하게 소개해드릴 예정이니 많은 기대 부탁드립니다.&lt;br/&gt;
이번 Part 1에서는 마이크로프론트엔드 개념과 구조, 장단점에 대해 살펴보겠습니다!&lt;/p&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#1-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EB%9E%80&quot;&gt;마이크로프론트엔드란?&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;1.1 &lt;a href=&quot;#11-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%9D%98-%EB%93%B1%EC%9E%A5-%EB%B0%B0%EA%B2%BD&quot;&gt;마이크로프론트엔드의 등장 배경&lt;/a&gt;&lt;br/&gt;&lt;/li&gt;
&lt;li&gt;1.2 &lt;a href=&quot;#12-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8B%9C---%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%86%B5%ED%95%A9&quot;&gt;구현 예시&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#2-%EC%BD%94%EB%93%9C-%EB%B6%84%EB%A6%AC%EB%A1%9C%EB%8F%84-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EC%A7%80-%EC%95%8A%EB%82%98%EC%9A%94&quot;&gt;코드 분리로도 해결할 수 있지 않나요?&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;2.1 &lt;a href=&quot;#21-%EC%BD%94%EB%93%9C-%EB%B6%84%EB%A6%AC%EC%9D%98-%ED%95%9C%EA%B3%84&quot;&gt;코드 분리의 한계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2.2 &lt;a href=&quot;#22-%EC%A7%84%EC%A0%95%ED%95%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%9D%98-%EC%A1%B0%EA%B1%B4&quot;&gt;진정한 마이크로프론트엔드의 조건&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#3-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%86%B5%ED%95%A9-%EB%B0%A9%EC%8B%9D&quot;&gt;마이크로프론트엔드 통합 방식&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;3.1 &lt;a href=&quot;#31-%ED%86%B5%ED%95%A9-%EC%8B%9C%EC%A0%90-%EC%96%B8%EC%A0%9C-%EC%A1%B0%EB%A6%BD%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80&quot;&gt;통합 시점: 언제 조립할 것인가?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;3.2 &lt;a href=&quot;#32-%ED%86%B5%ED%95%A9-%EC%9C%84%EC%B9%98-%EC%96%B4%EB%94%94%EC%84%9C-%EC%A1%B0%EB%A6%BD%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80&quot;&gt;통합 위치: 어디서 조립할 것인가?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;h1 id=&quot;1-마이크로프론트엔드란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EB%9E%80&quot; aria-label=&quot;1 마이크로프론트엔드란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 마이크로프론트엔드란?&lt;/h1&gt;
&lt;p&gt;마이크로프론트엔드는 웹 애플리케이션을 독립적으로 개발하고 배포하며 운영 가능한 작은 단위로 분할하는 아키텍처 패턴입니다. 이는 백엔드의 마이크로서비스 아키텍처(MSA) 개념을 프론트엔드 영역에 적용한 것으로 하나의 거대한 단일 페이지 애플리케이션(SPA) 대신 여러 개의 작은 애플리케이션들이 유기적으로 조합되어 전체 서비스를 구성하는 방식입니다.&lt;/p&gt;
&lt;h2 id=&quot;11-마이크로프론트엔드의-등장-배경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#11-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%9D%98-%EB%93%B1%EC%9E%A5-%EB%B0%B0%EA%B2%BD&quot; aria-label=&quot;11 마이크로프론트엔드의 등장 배경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1.1 마이크로프론트엔드의 등장 배경&lt;/h2&gt;
&lt;p&gt;기존의 모놀리식 프론트엔드 아키텍처는 애플리케이션의 규모가 커지면서 다음과 같은 문제점들을 나타내기 시작했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;배포 의존성&lt;/strong&gt;: 작은 변경사항도 전체 애플리케이션을 다시 빌드하고 배포해야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기술 스택 제약&lt;/strong&gt;: 모든 팀이 동일한 프레임워크와 버전을 사용해야 함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;코드 충돌&lt;/strong&gt;: 여러 팀이 동시에 작업할 때 머지 충돌이 빈번하게 발생&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;느린 빌드 시간&lt;/strong&gt;: 코드 베이스가 커질수록 빌드와 테스트 시간이 점점 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 문제들을 해결하고 백엔드에서 이미 검증된 마이크로서비스의 장점을 프론트엔드에도 적용하고자 마이크로프론트엔드가 등장하게 되었습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;12-구현-예시---컨테이너-애플리케이션-기반-마이크로프론트엔드-통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#12-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8B%9C---%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EA%B8%B0%EB%B0%98-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%86%B5%ED%95%A9&quot; aria-label=&quot;12 구현 예시   컨테이너 애플리케이션 기반 마이크로프론트엔드 통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1.2 구현 예시 - 컨테이너 애플리케이션 기반 마이크로프론트엔드 통합&lt;/h2&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1506px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bb5f26fa153e2ae83300ece4f3139c70/1235c/mfe-example.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 93.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAADb0lEQVR42m1UiY7bNhD177comvT4hd5IiyIp0EXSK90CLRa13V1nD9eXLMmSTB0UD4mSLb0OKe+mbULgaURy+DgcvuFI6woi2iLONgh3EVarFRaLhQNLU5RlCc75A2z//5BSQmvt7EgpjbZSaFqyjUFFE1pJsgqHtkXXdYQjerIWx0P7XxwPOFgcDgOh/dhWaoNUGjBRO2SqwTKIcbPY4HbpYXa3cDYpFPZlBVZqpORnYZrWcdgoR+JEGGcCfqbhp9IhoP+7bYLJzRKX8w1+n15jfL3Altl5BY98tplyfalrx+EiFEK4TtM2kBUd1VTkICCUgCToWqM7dmjqGpVSkJQzTWs0LTaVRk1R9X3/mvD+yLku4IsA2yKAx30s2Zr+fcQ6ceQ6zSDCEDKKUBJSz0Ph+9CMoaX8vZUwUDvcRXNceTNMl39hU2wR6RiF4KizDDqOSBHkMx5jv15DEbHJc8ph8xZCWSBRqUMk9oglQ9EKpDqnNNRorBpKAUWRyn0KnRcwvIQie6TbfzOHtYQSjHLEnNUyA0t8JFajkYfdboAQKZQiUrIWWuVOPv+KcCCUPCQ9+oQQRgdo6x38zRSz6UvMb/7A+OJH3Mx+Qy23MMonbNHocOgb9Vo2VVW5zoF2aemmLRoSuKkrJ/TmBGOs3gxqGr+HHWtb48RvW01KGNmPKz9BepJqgBpQUs5s5Vg4iRBsiuzRFM1bDGtIbsQzELYdKkKUFhA1lY85OqimQ7gnKWU1gsJgxSqEZPc5XUzdwhx6Qof22CPnEpbHYiRrqlNberTzseudQ0POZOBFOV4FEtc7hYknyGpsYpIQlRqtJf31IE5wOklHax2hJVAUUZRxF6Fujif0FE15+h92r2gs5crVfdX2bs6eLqG1ZdXSI9FjhFPLM/ZQQvctZW+OiZLThQ21ez/HefFwMSNGL0zIG9yGOQJu69mA6wZFdYC3Y4h5BU6726giXmMRMKzj3J1G1Q2S0mC+jdyDYl+o0adTjscvE3x0HuPDc4bvrnN8Msnx6MUKH/+0xnvPV3hyxfHsVYF3vl/g0dkc754t3diLuwIf0Jr3z27x+OcQn12SsH9dK+f8bMbwlOxkK/D8JsXnFyG+uUzx5XiPX/7OcUGX8tWU4ckkwRd/xjhfFrgKBL6epnh6yfDtrMAPc45/AKscnix6MH6eAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bb5f26fa153e2ae83300ece4f3139c70/263a4/mfe-example.webp 480w,
/static/bb5f26fa153e2ae83300ece4f3139c70/a6361/mfe-example.webp 960w,
/static/bb5f26fa153e2ae83300ece4f3139c70/96f28/mfe-example.webp 1506w&quot; sizes=&quot;(max-width: 1506px) 100vw, 1506px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bb5f26fa153e2ae83300ece4f3139c70/9aebd/mfe-example.png 480w,
/static/bb5f26fa153e2ae83300ece4f3139c70/a91f8/mfe-example.png 960w,
/static/bb5f26fa153e2ae83300ece4f3139c70/1235c/mfe-example.png 1506w&quot; sizes=&quot;(max-width: 1506px) 100vw, 1506px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bb5f26fa153e2ae83300ece4f3139c70/1235c/mfe-example.png&quot; alt=&quot;마이크로프론트엔드 구현 예시 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;온라인 쇼핑몰을 예로 들면, 각 마이크로프론트엔드는 서로 다른 기술 스택을 사용할 수 있으며 독립적인 팀이 각각의 앱을 담당할 수 있습니다&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;헤더&lt;/strong&gt;: React + TypeScript (내비게이션팀)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상품 영역&lt;/strong&gt;: Vue.js (상품팀)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장바구니&lt;/strong&gt;: Angular (주문팀)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결제&lt;/strong&gt;: Svelte (결제팀)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이때 컨테이너 애플리케이션이 모든 마이크로프론트엔드를 통합하고 라우팅을 관리하게 됩니다. 각 팀은 자신들의 영역을 독립적으로 개발하고 배포할 수 있으면서도, 사용자 입장에서는 하나로 자연스럽게 연결된 애플리케이션처럼 인식되도록 구성할 수 있습니다. &lt;br/&gt;
이것이 마이크로프론트엔드가 지향하는 핵심 가치입니다.&lt;/p&gt;
&lt;br /&gt;
&lt;h1 id=&quot;2-코드-분리로도-해결할-수-있지-않나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%BD%94%EB%93%9C-%EB%B6%84%EB%A6%AC%EB%A1%9C%EB%8F%84-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EC%A7%80-%EC%95%8A%EB%82%98%EC%9A%94&quot; aria-label=&quot;2 코드 분리로도 해결할 수 있지 않나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 코드 분리로도 해결할 수 있지 않나요?&lt;/h1&gt;
&lt;p&gt;맞습니다. 하나의 코드베이스 내에서도 영역별로 코드를 잘 분리하고 팀 간 협업 구조를 마련하면 어느정도 유사한 구조를 만들 수는 있습니다.&lt;br/&gt;
하지만 이런 방식은 여전히 빌드, 배포, 기술 스택 통일 등의 제약이 존재하고 팀 간 완전한 독립성과 유연성은 확보하기 어렵습니다.&lt;/p&gt;
&lt;h2 id=&quot;21-코드-분리의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#21-%EC%BD%94%EB%93%9C-%EB%B6%84%EB%A6%AC%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;21 코드 분리의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.1 코드 분리의 한계&lt;/h2&gt;
&lt;p&gt;단순히 폴더 구조를 나누고 모듈을 분리하는 것만으로는 진정한 마이크로프론트엔드라고 할 수 없습니다. 이러한 수준의 분리는 잘 구조화된 모놀리식 애플리케이션에서도 충분히 구현할 수 있기 때문입니다.&lt;br /&gt;
잘 정리된 모놀리식 애플리케이션도 다음과 같은 특징을 가질 수 있어요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;명확한 폴더 구조와 모듈 분리&lt;/li&gt;
&lt;li&gt;컴포넌트 기반의 재사용 가능한 아키텍처&lt;/li&gt;
&lt;li&gt;팀별로 담당 영역이 구분된 코드베이스&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
하지만 이러한 모놀리식 구조는 여전히 다음과 같은 한계를 가져요.
&lt;br /&gt;
&lt;br /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;단일 빌드 프로세스&lt;/strong&gt;: 전체 애플리케이션이 함께 빌드되어야함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단일 배포 파이프라인&lt;/strong&gt;: 일부분만 변경되어도 전체를 배포함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기술적 의존성&lt;/strong&gt;: 모든 팀이 동일한 기술 스택을 사용해야함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;팀 간 의존성&lt;/strong&gt;: 한 팀의 작업이 다른 팀에 영향을 미칠 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h2 id=&quot;22-진정한-마이크로프론트엔드의-조건&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#22-%EC%A7%84%EC%A0%95%ED%95%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%9D%98-%EC%A1%B0%EA%B1%B4&quot; aria-label=&quot;22 진정한 마이크로프론트엔드의 조건 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2.2 진정한 마이크로프론트엔드의 조건&lt;/h2&gt;
&lt;p&gt;진정한 마이크로프론트엔드를 구현하려면 각 단위가 독립적인 생명주기(개발, 빌드, 배포)를 가질 수 있도록 하는 &lt;b&gt;&quot;통합 방식&quot;&lt;/b&gt;이 필요합니다.&lt;br/&gt;
즉, 코드 분리뿐만 아니라 이 분리된 코드들을 &lt;b&gt;언제, 어떻게 하나의 애플리케이션으로 통합할 것인지&lt;/b&gt;에 대한 전략이 있어야 비로소 마이크로프론트엔드의 진정한 가치를 실현할 수 있어요.&lt;/p&gt;
&lt;br/&gt;
&lt;h1 id=&quot;3-마이크로프론트엔드-통합-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%86%B5%ED%95%A9-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot;3 마이크로프론트엔드 통합 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 마이크로프론트엔드 통합 방식&lt;/h1&gt;
&lt;p&gt;앞서 단순한 코드 분리만으로는 진정한 마이크로프론트엔드를 구현할 수 없다는 점을 살펴보았습니다. 그렇다면 여기서 말하는 &lt;b&gt;&quot;통합 방식&quot;&lt;/b&gt;이란 구체적으로 무엇일까요? &lt;br /&gt;
통합 방식은 간단히 말해 작은 마이크로프론트엔드 코드 조각들을 언제, 어디서 하나의 애플리케이션으로 묶을지를 결정하는 전략입니다.&lt;br /&gt;
이 전략은 크게 두 가지 관점을 고려해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;언제 통합하는가?&lt;/strong&gt; 개발 시점에 미리 조립할 것인가, 아니면 사용자가 접속할 때 실시간으로 조립할 것인가?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;어디서 통합하는가?&lt;/strong&gt; 서버에서 조립할 것인가, 브라우저에서 조립할 것인가, 아니면 그 중간 지점에서 조립할 것인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;h2 id=&quot;31-통합-시점-언제-조립할-것인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#31-%ED%86%B5%ED%95%A9-%EC%8B%9C%EC%A0%90-%EC%96%B8%EC%A0%9C-%EC%A1%B0%EB%A6%BD%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;31 통합 시점 언제 조립할 것인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.1 통합 시점: 언제 조립할 것인가?&lt;/h2&gt;
&lt;h3 id=&quot;311-빌드-타임-통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#311-%EB%B9%8C%EB%93%9C-%ED%83%80%EC%9E%84-%ED%86%B5%ED%95%A9&quot; aria-label=&quot;311 빌드 타임 통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.1.1 빌드 타임 통합&lt;/h3&gt;
&lt;p&gt;빌드 타임 통합은 개발할 땐 따로따로 작업하지만 배포할 땐 하나로 합쳐서 내보내는 방식입니다. &lt;br /&gt;
개발자들이 마이크로 앱을 독립적으로 만들어도 빌드 단계에서 하나의 번들로 코드가 통합됩니다. 이 덕분에 사용자는 앱이 제각각 만들어졌다는 사실을 알 수 없고, 완성된 하나의 앱 형태로 자연스럽게 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;빌드-타임-통합-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%8C%EB%93%9C-%ED%83%80%EC%9E%84-%ED%86%B5%ED%95%A9-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;빌드 타임 통합 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;빌드 타임 통합 방법&lt;/h3&gt;
&lt;h3 id=&quot;1-npm-패키지-방식-의존성으로-관리하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-npm-%ED%8C%A8%ED%82%A4%EC%A7%80-%EB%B0%A9%EC%8B%9D-%EC%9D%98%EC%A1%B4%EC%84%B1%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1 npm 패키지 방식 의존성으로 관리하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) npm 패키지 방식: 의존성으로 관리하기&lt;/h3&gt;
&lt;p&gt;가장 간단한 빌드타임 통합 방법은 각 마이크로 앱을 npm 패키지로 만드는 것입니다. 메인 애플리케이션의 package.json에 다른 마이크로 앱들을 의존성으로 추가하면 빌드 시 번들러가 모든 코드를 가져와 하나의 결과물을 만들어냅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// host-app/package.json&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@app/header-app&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^1.2.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@app/product-app&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^2.1.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@app/checkout-app&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^1.5.0&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 기존 JavaScript 생태계의 패키지 관리 시스템을 그대로 활용할 수 있다는 점에서 장점이 있습니다. npm의 버전 관리나 의존성 해결 같은 기능도 그대로 사용할 수 있어 새로운 도구를 학습해야 하는 부담이 적습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;h3 id=&quot;2-모노레포-기반-통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%EA%B8%B0%EB%B0%98-%ED%86%B5%ED%95%A9&quot; aria-label=&quot;2 모노레포 기반 통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) 모노레포 기반 통합&lt;/h3&gt;
&lt;p&gt;좀 더 정교한 방법은 Nx나 Turborepo 같은 모노레포 도구를 활용하는 방법입니다. 여러 마이크로 앱을 하나의 저장소에서 관리하면서도 각각을 독립적인 라이브러리로 취급할 수 있습니다.
모노레포 환경에서는 마이크로프론트엔드끼리 통합하는 과정이 직관적입니다.&lt;br/&gt;
예를 들어, 각 마이크로 앱이 &lt;code class=&quot;language-text&quot;&gt;libs&lt;/code&gt; 폴더에 독립 패키지로 존재하고 &lt;code class=&quot;language-text&quot;&gt;main-app&lt;/code&gt; 에서는 이들을 일반 npm 패키지처럼 가져다 쓸 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;workspace/
├─ apps/
│  └─ main-app          // 메인 호스트 애플리케이션
├─ libs/
│  ├─ header-app        // 헤더 마이크로 앱
│  ├─ product-app       // 상품 마이크로 앱
│  └─ checkout-app      // 결제 마이크로 앱
└─ nx.json
└─ package.json&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// main-app/package.json&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// main-app의 package.json에서 각 마이크로 앱들을 의존성으로 등록해줌&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;main-app&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@workspace/header-app&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@workspace/product-app&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;@workspace/checkout-app&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^18.0.0&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;react-dom&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;^18.0.0&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// main-app/product/page.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ProductComponent &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@workspace/product&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ProductPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 상품 페이지에 product-app 프로젝트 컴포넌트 렌더링&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;상품 페이지&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;h1&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ProductComponent &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 설정하면 각 마이크로 앱은 독립적으로 개발되지만 최종 빌드 시에는 모든 코드가 하나의 번들로 통합됩니다. 이때 각 앱에서 공통으로 사용하는 라이브러리는 자동으로 중복 제거되어 최적화됩니다.&lt;br /&gt;
예를 들어, &lt;code class=&quot;language-text&quot;&gt;header-app&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;product-app&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;checkout-app&lt;/code&gt;이 모두 React를 사용하더라도 최종 번들에는 React가 한 번만 포함됩니다.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;빌드 타임 통합의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;코드 공유와 일관성 확보&lt;/li&gt;
&lt;li&gt;초기 로딩 성능 우수&lt;/li&gt;
&lt;li&gt;타입 안정성 확보 (전체 코드가 같은 빌드 시점에 컴파일되기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;단점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전체 소스 코드를 함께 빌드해야 하므로 빌드 시간이 증가
&lt;blockquote&gt;
&lt;p&gt;물론, 모노레포 도구들의 원격 캐싱 기능을 사용하여 변경된 부분만 재빌드하는 시스템을 사용하여 빌드시간을 단축시킬 수 있긴 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;모든 앱이 동일한 라이브러리 버전을 사용해야 하는 제약 (Single Version Policy)
&lt;blockquote&gt;
&lt;p&gt;Nx 공식 문서에서도 &quot;&lt;strong&gt;Single Version Policy&lt;/strong&gt;&quot; 전략을 통해 모든 의존성 정의를 루트 package.json에 중앙화해서 코드베이스 전반에 걸쳐 일관된 버전을 보장해야 한다고 명시하고 있어요.&lt;br/&gt;
실제로 Nx GitHub 이슈에서도 개발자들이 &quot;&lt;strong&gt;서로 다른 팀이 서로 다른 프로젝트를 소유하고 있을 때 그 중 하나가 breaking change가 있는 버전으로 업그레이드하면 모든 프로젝트에 강제 업그레이드가 발생한다. 이는 훨씬 큰 조직에서는 확장되지 않는다&lt;/strong&gt;&quot;라고 지적했어요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;312-런타임-통합-실시간-조립-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#312-%EB%9F%B0%ED%83%80%EC%9E%84-%ED%86%B5%ED%95%A9-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A1%B0%EB%A6%BD-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;312 런타임 통합 실시간 조립 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.1.2 런타임 통합: 실시간 조립 전략&lt;/h3&gt;
&lt;p&gt;런타임 통합은 각 마이크로 앱을 독립적으로 배포하고 사용자가 접속할 때 실시간으로 조합하는 방식입니다. &lt;br/&gt;
이 방식에서는 각 팀이 자신의 마이크로 앱을 독립적으로 개발하고 배포할 수 있습니다. 새로운 기능을 추가하거나 수정할 때 다른 팀의 영향을 받지 않는다는 장점이 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;런타임-통합-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%9F%B0%ED%83%80%EC%9E%84-%ED%86%B5%ED%95%A9-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;런타임 통합 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;런타임 통합 방법&lt;/h3&gt;
&lt;h3 id=&quot;1-module-federation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-module-federation&quot; aria-label=&quot;1 module federation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) Module Federation&lt;/h3&gt;
&lt;p&gt;Webpack 5의 Module Federation 기능은 런타임 통합을 대표하는 기술입니다. 이 방식에서는 호스트 애플리케이션이 리모트 애플리케이션을 런타임에 불러와 하나의 앱처럼 통합합니다.
Module Federation을 이해하려면 먼저 두 가지 핵심 개념을 알아야 합니다. 바로 호스트(Host) 애플리케이션과 리모트(Remote) 애플리케이션인데요.&lt;br/&gt;
호스트 애플리케이션은 말 그대로 다른 마이크로프론트엔드들을 불러와서 통합하는 메인 애플리케이션입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// host-app/webpack.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ModuleFederationPlugin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;webpack/lib/container/ModuleFederationPlugin&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ModuleFederationPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;host&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;remotes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;productApp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;productApp@http://localhost:3001/remoteEntry.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;cartApp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;cartApp@http://localhost:3002/remoteEntry.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;반면 리모트 애플리케이션은 자신의 기능을 외부에 노출해서 다른 애플리케이션에서 사용할 수 있도록 하는 독립적인 마이크로프론트엔드 입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// product-app/webpack.config.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ModuleFederationPlugin &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;webpack/lib/container/ModuleFederationPlugin&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ModuleFederationPlugin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;productApp&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;filename&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;remoteEntry.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;// productApp이 자신의 컴포넌트들을 다른 앱들이 찾아서 사용할 수 있도록 하는 진입점 역할&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;exposes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&apos;./ProductList&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/components/ProductList&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&apos;./ProductDetail&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/components/ProductDetail&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br /&gt;
&lt;p&gt;Module Federation의 장점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 팀의 기술적 독립성 보장&lt;/li&gt;
&lt;li&gt;코드 충돌 방지&lt;/li&gt;
&lt;li&gt;장애 격리
&lt;blockquote&gt;
&lt;p&gt;한 모듈에서 문제가 발생해도 다른 모듈들은 정상적으로 동작할 수 있어서 장애 격리 효과가 있어요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;점진적 현대화 가능
&lt;blockquote&gt;
&lt;p&gt;비즈니스 관점에서는 여러 팀이 병렬로 개발할 수 있어서 새로운 기능을 훨씬 빠르게 출시할 수 있고 레거시 시스템을 점진적으로 현대화할 수 있어요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
&lt;p&gt;단점은 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;라이브러리 버전 충돌 위험
&lt;blockquote&gt;
&lt;p&gt;예를 들어, 호스트 애플리케이션에서는 React 17을 사용하고 있는데 리모트 애플리케이션에서 React 18을 사용한다면 런타임에서 두 버전의 React가 충돌하면서 예상치 못한 오류들이 발생해요. Context API가 제대로 동작하지 않거나, Hook이 이상하게 작동하거나 심지어 메모리 누수까지 발생할 수 있어요.&lt;br/&gt;
이러한 문제를 해결하기 위한 방법 중 하나가 바로 &quot;&lt;strong&gt;싱글톤(singleton) 설정&lt;/strong&gt;&quot;입니다. 싱글톤 설정은 여러 애플리케이션 간에 하나의 라이브러리 인스턴스만 공유하도록 강제하는 설정입니다. 이를 통해 각 애플리케이션이 서로 다른 버전의 라이브러리를 사용하더라도 런타임에서는 하나의 버전만 로드되어 충돌을 방지합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;성능 저하 가능성 (네트워크 지연, 런타임 오버헤드)&lt;/li&gt;
&lt;li&gt;복잡한 배포 및 의존성 관리&lt;/li&gt;
&lt;li&gt;타입 안정성 확보 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-nextjs-multi-zone-경로-기반-통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-nextjs-multi-zone-%EA%B2%BD%EB%A1%9C-%EA%B8%B0%EB%B0%98-%ED%86%B5%ED%95%A9&quot; aria-label=&quot;2 nextjs multi zone 경로 기반 통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) Next.js Multi-zone: 경로 기반 통합&lt;/h3&gt;
&lt;p&gt;Next.js를 사용하는 조직이라면 Multi-Zone 아키텍처를 고려해볼 수 있습니다. 이는 여러 개의 독립적인 Next.js 애플리케이션을 경로 기반으로 통합하는 방식입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 호스트 앱의 next.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rewrites&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/product/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// /product/ 경로 하위는 모두 상품 앱으로&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;http://localhost:3001/:path*&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 상품 앱 포트로 프록시&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 상품 앱의 next.config.js&lt;/span&gt;
module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;basePath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/product&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 정적 리소스 및 라우팅의 base 경로 설정&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사용자가 &lt;code class=&quot;language-text&quot;&gt;/product&lt;/code&gt; 경로에 접속하면 내부적으로 상품 앱으로 라우팅됩니다.&lt;/p&gt;
&lt;p&gt;Next.js의 기본 기능만으로 구현할 수 있어 설정이 간단하지만 앱 간 이동 시 전체 페이지가 새로 로드되는 하드 탐색이 발생한다는 단점이 있습니다. (next/link를 사용해도 소프트 탐색이 되지않음)&lt;/p&gt;
&lt;br /&gt;
&lt;h3 id=&quot;어떤-방식을-선택해야-할까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;어떤 방식을 선택해야 할까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떤 방식을 선택해야 할까요?&lt;/h3&gt;
&lt;p&gt;그렇다면 빌드타임 통합과 런타임 통합 중 어떤것을 선택해야할까요?&lt;/p&gt;
&lt;p&gt;어느 방식이 더 적합한지는 정해진 정답이 없다고 생각합니다. 프로젝트의 규모, 팀의 구조, 기술적 요건 등에 따라 다르게 결정되어야 합니다.
&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;빌드타임 통합이 적합한 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소규모 팀이나 단일 팀에서 모든 마이크로 앱을 관리하는 경우&lt;/li&gt;
&lt;li&gt;초기 로딩 성능이 중요한 경우&lt;/li&gt;
&lt;li&gt;간단한 배포 및 설정을 선호하는 경우&lt;/li&gt;
&lt;li&gt;마이크로 앱 간의 강한 결합이 가능한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;런타임 통합이 적합한 경우&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;여러 팀이 독립적으로 개발하고 배포해야 하는 경우&lt;/li&gt;
&lt;li&gt;각 마이크로 앱의 배포 주기가 다른 경우&lt;/li&gt;
&lt;li&gt;기술적 독립성이 필요한 경우&lt;/li&gt;
&lt;li&gt;대규모 조직에서 팀 간 의존성을 최소화하려는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;br /&gt;
결국 마이크로프론트엔드의 통합 방식 선택은 기술적 고려사항과 조직적 요구사항의 균형을 맞추는 것입니다. 현재의 팀 구조와 프로젝트 요구사항을 분석한 후 가장 적합한 방식을 선택해야합니다.
&lt;br /&gt;&lt;br /&gt;
&lt;h2 id=&quot;32-통합-위치-어디서-조립할-것인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#32-%ED%86%B5%ED%95%A9-%EC%9C%84%EC%B9%98-%EC%96%B4%EB%94%94%EC%84%9C-%EC%A1%B0%EB%A6%BD%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;32 통합 위치 어디서 조립할 것인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.2 통합 위치: 어디서 조립할 것인가?&lt;/h2&gt;
&lt;p&gt;마이크로프론트엔드 아키텍처를 도입할 때 또 하나의 중요한 결정은 &lt;b&gt;“어디서 통합을 수행할 것인가”&lt;/b&gt; 입니다. 이 선택에 따라 애플리케이션의 성능과 사용자 경험, 그리고 인프라 비용이 크게 달라집니다.&lt;/p&gt;
&lt;p&gt;통합 위치에 따라 크게 세 가지 방식으로 나눌 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;321-서버-측-구성ssr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#321-%EC%84%9C%EB%B2%84-%EC%B8%A1-%EA%B5%AC%EC%84%B1ssr&quot; aria-label=&quot;321 서버 측 구성ssr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.2.1 서버 측 구성(SSR)&lt;/h3&gt;
&lt;p&gt;서버 측 구성은 사용자가 페이지를 요청하면 서버에서 각 마이크로프론트엔드 조각들을 가져와 하나로 조립한 다음 완성된 HTML을 브라우저로 보내는 방식입니다.&lt;/p&gt;
&lt;p&gt;Podium이나 Tailor 같은 도구들이 지원합니다.&lt;/p&gt;
&lt;p&gt;이 방식의 장점은 빠른 초기 로딩과 SEO 최적화입니다. 사용자는 이미 완성된 페이지를 받기 때문에 첫 화면을 빠르게 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 모든 조립 작업이 서버에서 일어나다 보니 서버 부하가 커지고 캐싱 전략도 복잡해집니다. 실시간 인터랙션이 많은 앱에서는 한계가 있을 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;322-클라이언트-측-구성csr&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#322-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%B8%A1-%EA%B5%AC%EC%84%B1csr&quot; aria-label=&quot;322 클라이언트 측 구성csr permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.2.2 클라이언트 측 구성(CSR)&lt;/h3&gt;
&lt;p&gt;클라이언트 측 구성은 브라우저에서 필요한 마이크로 앱을 동적으로 로드하고 조합합니다.&lt;/p&gt;
&lt;p&gt;Module Federation 같은 도구들이 지원합니다. Module Federation은 런타임에 모듈을 동적으로 공유할 수 있게 해줍니다.&lt;/p&gt;
&lt;p&gt;이 방식의 장점은 동적이고 인터랙티브한 UX를 제공할 수 있습니다. 필요한 부분만 선택적으로 로드할 수 있어 효율적이고 서버 부하도 줄일 수 있어요.&lt;/p&gt;
&lt;p&gt;하지만 초기 로딩이 느릴 수 있고 SEO 최적화도 별도 작업이 필요합니다.&lt;/p&gt;
&lt;h3 id=&quot;323-에지-측-구성edge-computing&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#323-%EC%97%90%EC%A7%80-%EC%B8%A1-%EA%B5%AC%EC%84%B1edge-computing&quot; aria-label=&quot;323 에지 측 구성edge computing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3.2.3 에지 측 구성(Edge Computing)&lt;/h3&gt;
&lt;p&gt;에지 측 구성은 여러 개의 독립적인 마이크로 앱을 에지(Edge) 네트워크에서 조합하여 사용자에게 최종 페이지를 전달하는 방법입니다.&lt;/p&gt;
&lt;p&gt;Cloudflare Workers나 Lambda@Edge 같은 플랫폼이 지원합니다. 이들은 전 세계에 분산된 서버 네트워크를 활용해 사용자와 가까운 지점에서 코드를 실행할 수 있게 해줍니다.&lt;/p&gt;
&lt;p&gt;이 방식의 장점은 전 세계 어디서든 빠른 응답 속도를 제공할 수 있다는 점입니다. 서버 부하도 분산되고 클라이언트 성능에 덜 의존적입니다.&lt;/p&gt;
&lt;p&gt;하지만 에지 컴퓨팅 인프라가 필요하고 분산된 환경에서의 디버깅과 모니터링이 복잡해집니다. 또한 상대적으로 높은 인프라 비용이 발생할 수 있습니다.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;지금까지 마이크로프론트엔드 개념과 구조, 장단점에 대해 함께 살펴보았습니다!&lt;/p&gt;
&lt;p&gt;마이크로프론트엔드는 분명 대규모 프론트엔드 애플리케이션의 복잡성을 효과적으로 관리하고 팀의 자율성을 높이는 데 유용한 강력한 아키텍처입니다. 독립적인 배포, 기술 스택 선택의 자유, 팀 간 의존성 최소화 등 다양한 장점을 갖추고 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 동시에 새로운 복잡성과 고려해야 할 요소들도 수반됩니다. 버전 관리의 어려움, 런타임 오류 위험, 증가된 인프라 복잡도 그리고 무엇보다 조직 전체의 기술적 성숙도가 뒷받침되어야 한다는 점은 현실적인 제약으로 작용합니다. 결국 모든 문제를 해결해주는 만병통치약은 아니라는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;특히 도입을 검토할 때는 현재 겪고 있는 구체적인 문제부터 명확히 정의하는 것이 중요합니다. 단순히 최신 기술이라서 다른 회사에서 성공했다는 이유만으로 도입한다면 예상보다 훨씬 큰 비용을 치를 수 있습니다. 팀 규모, 도메인 복잡성, 조직 문화, 인프라 역량 등을 종합적으로 고려한 후 점진적으로 접근하는 것이 현명한 전략이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;마이크로프론트엔드를 처음 접하시는 분들이나 도입을 고민하고 계신 분들에게 조금이라도 도움이 되었기를 바랍니다.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;&lt;b&gt;시리즈&lt;/b&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;b&gt;1. 대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 1. 마이크로프론트엔드 너 뭐야?&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-11-06/what-is-MFE-part2/&quot;&gt;2. 대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 2. 모듈 페더레이션 PoC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-11-10/what-is-MFE-part3/&quot;&gt;3. 대규모 프론트엔드 아키텍처의 새로운 패러다임 - Part 3. Nx를 활용한 마이크로프론트엔드&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br /&gt;
&lt;p&gt;이어서 공개할 Part 2에서는 Module Federation PoC를, Part 3에서는 Nx를 활용한 마이크로프론트엔드 내용을 자세히 다룰 예정입니다. PoC처럼 시리즈물도 팀원들이 모여 단단히 준비했으니 많은 관심과 기대 부탁드립니다.&lt;/p&gt;
&lt;br /&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다!☺️&lt;/p&gt;</content:encoded></item><item><title><![CDATA[비동기 요청-응답 패턴으로 풀어낸 발주 서비스 개발기]]></title><description><![CDATA[안녕하세요.
올리브영 SCM…]]></description><link>https://oliveyoung.tech/2025-06-30/purchase-order-service/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-06-30/purchase-order-service/</guid><pubDate>Mon, 30 Jun 2025 15:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.
올리브영 SCM 백엔드 개발을 담당하고 있는 &lt;strong&gt;승쭈누&lt;/strong&gt;입니다. &lt;br&gt;
매일 반복되는 백오피스 업무 속에서, 우리는 종종 시스템의 한계에 부딪히곤 합니다. 특히 대량의 데이터를 처리해야 하는 경우, 느린 응답 속도는 사용자들의 업무 피로도를 높이고 비효율을 초래하죠. 하지만 백오피스 개발은 단순히 기능을 구현하는 것을 넘어, 현업의 고충을 해결하고 비즈니스 효율을 극대화하는 가치 있는 작업입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 올리브영의 핵심 도메인 중 하나인 &apos;발주 서비스&apos;를 개선하며 겪었던 아키텍처적 도전과제와 이를 해결하기 위한 여정을 다룹니다. 기존 동기식 처리 방식의 한계를 넘어, Kafka 기반의 비동기 요청-응답 아키텍처를 도입하며 마주했던 고민들, 그리고 최종적으로 올리브영 환경에 최적화된 솔루션을 찾아가는 과정을 상세히 소개하고자 합니다. 이 글이 백오피스 시스템의 성능과 안정성 고민에 대한 실질적인 해답을 찾는 데 도움이 되기를 바랍니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;발주&lt;/strong&gt;란?&lt;br&gt; 고객이 온라인 몰에서 상품을 주문하듯, 올리브영이 협력사 또는 자사 물류센터로 상품을 주문하는 B2B 개념의 주문입니다. &lt;br&gt;
대표적으로, 협력사에 상품을 주문하여 센터로 입고시키는 &lt;code class=&quot;language-text&quot;&gt;협력사 발주&lt;/code&gt;와 센터에 입고된 상품을 주문하여 각 매장으로 입고시키는 &lt;code class=&quot;language-text&quot;&gt;매장 발주&lt;/code&gt; 등이 있습니다.&lt;/p&gt;
&lt;p&gt;이를 통해, 전국 올리브영 매장으로 상품을 원활히 공급하고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;h1 id=&quot;-신규-발주-서비스-왜-개발했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%8B%A0%EA%B7%9C-%EB%B0%9C%EC%A3%BC-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%99%9C-%EA%B0%9C%EB%B0%9C%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot; 신규 발주 서비스 왜 개발했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📦 신규 발주 서비스 왜 개발했나요?&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;기존 발주 시스템의 느린 처리 속도 문제와 더불어 비즈니스 구조의 다각화로 인해 새로운 요구사항이 등장했습니다.&lt;/p&gt;
&lt;h2 id=&quot;기존-문제-동기식-발주-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A1%B4-%EB%AC%B8%EC%A0%9C-%EB%8F%99%EA%B8%B0%EC%8B%9D-%EB%B0%9C%EC%A3%BC-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;기존 문제 동기식 발주 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기존 문제: 동기식 발주 처리&lt;/h2&gt;
&lt;p&gt;올리브영의 발주 업무는 ‘올리브원’이라 하는 백오피스 서비스에서 처리되고 있습니다. 발주를 담당하는 각 사용자가 올리브원에 접속하여 아래와 같은 프로세스로 발주 처리를 진행합니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 972px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6b5789a838be4bb9a2b66d4ef34f7f52/84a58/po-process.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 21.041666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAuUlEQVR42k1PWwrCMBDs/U/iBcRPwR/PICioKLVpkzSbPtI26dhdiDgwZDK7md0U+MO6rqCB4AIhzAHLsiDGiHmehayzl1L6aUb2Ctu22J32sOTgyUNphdJ8QH4LHQN85zEMA0II6LoO4zgKWfd9L7VpmnC53nB/PFFUSuFwPkJbI4G1qkGOYMx29x5aazRNIwHWWvGY7baIc04G8Xavd4myUih4zYwYkzTzRJ7Omojk4f/Jtcz85YwvUYE0Pj/aTO4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6b5789a838be4bb9a2b66d4ef34f7f52/263a4/po-process.webp 480w,
/static/6b5789a838be4bb9a2b66d4ef34f7f52/a6361/po-process.webp 960w,
/static/6b5789a838be4bb9a2b66d4ef34f7f52/3300c/po-process.webp 972w&quot; sizes=&quot;(max-width: 972px) 100vw, 972px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6b5789a838be4bb9a2b66d4ef34f7f52/9aebd/po-process.png 480w,
/static/6b5789a838be4bb9a2b66d4ef34f7f52/a91f8/po-process.png 960w,
/static/6b5789a838be4bb9a2b66d4ef34f7f52/84a58/po-process.png 972w&quot; sizes=&quot;(max-width: 972px) 100vw, 972px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6b5789a838be4bb9a2b66d4ef34f7f52/84a58/po-process.png&quot; alt=&quot;올리브영 기존 발주 프로세스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;올리브영 기존 발주 프로세스&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;ol&gt;
&lt;li&gt;담당자가 발주 요청 Excel 파일을 올리브원으로 업로드합니다.&lt;/li&gt;
&lt;li&gt;파일을 DTO 리스트로 변환합니다.&lt;/li&gt;
&lt;li&gt;DTO 리스트를 순회하며 유효성 검증을 수행합니다.&lt;/li&gt;
&lt;li&gt;유효한 발주 건에 대해 발주 처리합니다.&lt;/li&gt;
&lt;li&gt;발주 처리 결과에 대해 이력을 저장합니다.&lt;/li&gt;
&lt;li&gt;처리된 발주 전표번호와 함께 결과를 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;기존 서비스는 발주를 동기식으로 처리했기 때문에, &lt;strong&gt;발주량이 많아질수록 응답이 지연되어 다른 작업을 수행하기 어려웠습니다.&lt;/strong&gt; 처리에 최대 수 분이 소요되기도 하여 사용자의 업무 생산성이 크게 저하되었어요. 또한 서버 메모리 자원도 장시간 점유되면서, 간헐적인 발주 처리 장애가 발생하는 등 성능과 안정성 측면 모두 개선이 필요한 상황이었습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;새로운-요구사항-신규-백오피스-오픈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EC%8B%A0%EA%B7%9C-%EB%B0%B1%EC%98%A4%ED%94%BC%EC%8A%A4-%EC%98%A4%ED%94%88&quot; aria-label=&quot;새로운 요구사항 신규 백오피스 오픈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;새로운 요구사항: 신규 백오피스 오픈&lt;/h2&gt;
&lt;p&gt;올리브영의 백오피스 서비스는 업무 영역에 따라 점차 세분화되고 신규 개발되고 있으며, 올해 상반기에는 PB 상품 관리 전용 백오피스인 ‘&lt;strong&gt;브랜드원&lt;/strong&gt;’이 새롭게 런칭되었는데요.&lt;/p&gt;
&lt;br&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 878px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/751ae2516c1756bc483d56ea56bb46c6/7bc11/new-bo.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.708333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABMklEQVR42p1S0U4DIRDk/39B3/XV+BtGk6ZR0/ig7Wnag9KDIhxwvfF2K7U1NjG3l8ku5GZYdhA4ipgiXHDwbUCKCW3bIsaIlNIJcs4Mqvu+/2bvs3h4eYT2lhcb02CuFlB2jdhGeO9ZkFDEKZc6hMCiXddhUX1AqjXE5e017mdTOGOxXC5hh2waA2MMpJTQWqNpGgbtWWs50z7VdKhzDpPpE17fFhDeeW6fWifSdrvln+h0ItKaCEQmUE0o+9QdBXWdcwfRY//xFAZR/+kha4m6rhlVVWG1Wp1c+3iOv0OUgsRCDNBWQ2oFN3RABBL4i3guDqbs8g5qozDX75BW8ZV+HPx/iIubK9w9T6AHh5RSB1OoHiVIM+M3lRMPubhGhowJcbwgx8bM7axgMWfMVYvgFw9wWjfZG8hnAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/751ae2516c1756bc483d56ea56bb46c6/263a4/new-bo.webp 480w,
/static/751ae2516c1756bc483d56ea56bb46c6/8fd2c/new-bo.webp 878w&quot; sizes=&quot;(max-width: 878px) 100vw, 878px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/751ae2516c1756bc483d56ea56bb46c6/9aebd/new-bo.png 480w,
/static/751ae2516c1756bc483d56ea56bb46c6/7bc11/new-bo.png 878w&quot; sizes=&quot;(max-width: 878px) 100vw, 878px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/751ae2516c1756bc483d56ea56bb46c6/7bc11/new-bo.png&quot; alt=&quot;브랜드원에서 발주 처리&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;브랜드원에서 발주 처리&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;브랜드원 측에서는 기존 백오피스인 ‘올리브원’처럼 자체 화면에서 직접 발주를 처리할 수 있기를 원했습니다.
이 요구는 향후 새롭게 도입될 다른 백오피스들에서도 반복될 가능성이 높다고 판단했기 때문에, 브랜드원 전용 발주 API를 따로 개발하기보다는, &lt;strong&gt;올리브원을 포함한 모든 백오피스에서 공통으로 활용할 수 있는 신규 발주 서비스를 구축&lt;/strong&gt;하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;다만 기존 발주 API의 구조를 그대로 계승할 경우, 과거의 문제점들이 신규 서비스로 그대로 전파될 수 있기 때문에, 이러한 한계를 근본적으로 해결할 수 있도록 아키텍처를 새롭게 설계하고자 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-기존-문제를-어떻게-해결하지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B8%B0%EC%A1%B4-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%98%EC%A7%80&quot; aria-label=&quot; 기존 문제를 어떻게 해결하지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧑🏻‍💻 기존 문제를 어떻게 해결하지?&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;병목-지점-찾기-유효성-검증검증검증&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%91%EB%AA%A9-%EC%A7%80%EC%A0%90-%EC%B0%BE%EA%B8%B0-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%A6%9D%EA%B2%80%EC%A6%9D%EA%B2%80%EC%A6%9D&quot; aria-label=&quot;병목 지점 찾기 유효성 검증검증검증 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;병목 지점 찾기: 유효성 검증검증검증&lt;/h2&gt;
&lt;p&gt;먼저 기존 문제를 해결하기 위해 프로세스 내 병목 지점을 파악해보았습니다. 5,000개의 상품을 발주 처리하는 상황을 가정했을 때, 각 단계별 처리 시간은 다음과 같았습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 310px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ede84b09ab4887c40a286a0627b6cec5/56e1b/po-process-duration-per-step.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.2258064516129%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAACAklEQVR42nVT23LaMBD1/39IH/vSl06YZlLShgDphBYGCAm3cgtgbsaWb4LTPbKV4MxUzKKVtdKePWfleEeF/cE3Nl2ssXb3WK53WCw3Zna3B8xfXeMfvCx2s/VM3Hqzx0q+r8SnBSqCc/QVjoHC0t3h0+cSKg8tXN/+wpernyjd1NHqDvD1uopKvQUVxtjtj8anPTS6+FH9g7taE4/NZ8RxAicIQsl6RBjFSJIUcW70IwlgkAojKMnueb5YAF8A+HKOCbjHO3wBxtk5SND5fEavP5EsT4JoiEarh05vKDZC7bGNNE3BofUJ+nQyPs8shAqCses01XCUCk3gdueh/TTEdL7CZLbC62qD8WSB7vMYSX4hD6RaG/8kFy/XW4PQrhnnsDwl5ZpgsSQ/xJlBZ/kl+XdSQLNrng2j5M3nnjMSFBzNzgu+letCdAe31d+4uqkZ/mw5pmRJwGqy8jXK9w20hZpCyfxjFgZkBzJkUfyO2lqci8QYipZc7FkRHVd6iRv90QytzgBEzPkSnUFIBDqjwyKmIJHQVeDQ3e7NYvh3bozrwXhmMn4cpoJcZQ6CsaK8lyycsJ/IjRWDviX+0j6KorUuCGRKJhre3h9OUa40pBd7KH2vY3fRXwWEuSisqtnpm5dT7EOBzJuNOIKUYgRKGQ7/J4ptL76WOEneROHT+wfzveTviSevAgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ede84b09ab4887c40a286a0627b6cec5/028b8/po-process-duration-per-step.webp 310w&quot; sizes=&quot;(max-width: 310px) 100vw, 310px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ede84b09ab4887c40a286a0627b6cec5/56e1b/po-process-duration-per-step.png 310w&quot; sizes=&quot;(max-width: 310px) 100vw, 310px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ede84b09ab4887c40a286a0627b6cec5/56e1b/po-process-duration-per-step.png&quot; alt=&quot;발주 프로세스 단계별 처리 시간&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;발주 프로세스 단계별 처리 시간&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;분석 결과, 가장 오래 지연되는 단계는 유효성 검증 프로세스였습니다. 매장 발주 처리 과정에서는 상품 하나당 수십 개의 유효성 검증을 거쳐야 하는데요, 이러한 구조적 특성으로 인해 전체 처리 시간이 길어질 수밖에 없었습니다. &lt;br&gt;&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1470px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/05d2024db04e1c1f48a15649a7a4026a/3d705/validation-list.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 58.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB5UlEQVR42lVSCY7bMAz0/z9ZoNhmdxNbF3X74M7QaYEGEByR0nAOLUmK9jF17oe2PrSWpgWr83/tWlvXOXcdOFNqxX5avaF+30Nv3t8da1m3oCkXPc7TQOPm1DuvjQAA3lwE+NT9ODQl1FvWJKIuiA26VO3uiXXgzLL5iGayqWyI95pC1Aww1lYASq64cGBA1NEdvqIhZtSLsbuuyxZBFw8wAlLScV4qzmn0AYD9ZoiBtIUXszid3atIQj3ZUMom0D9AhwYlc7O/AVMIJr/WpuzT2/M6jeEx0Reo8oIaBev/DL9fDrKCRrDYj1tyAMNSBxhUkxxShl8Tg732FiA3mLdUVzD0egMb4Auh+EhPxJLdW9Xn96qfWJRMufSQkisC6fMyO3jemexq4Qi+YyBlHuZigx6WktV7TheNYEaG9CtJtoQbAFu/nw4Z8cd0aREtWF4rUpX6fhqn1iwAvVltBhYt0T4GWESA8UUUY065BKW/fFZUsfz6/VDKjmBwADAjEKZMT/mo+TSYJi/UHADcTQ0BDRQ93uNAJr78+VzNw5jKnWwS9Zs3ya0NY2opg0mtyVhwT1B+uf/7YzgAfCoX3yEPhO1D3fqFRKu+VqePr5eFxZRL2CzxJ+ofj+ftO5j33s1vnvsBj2KhvsaXYxcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/05d2024db04e1c1f48a15649a7a4026a/263a4/validation-list.webp 480w,
/static/05d2024db04e1c1f48a15649a7a4026a/a6361/validation-list.webp 960w,
/static/05d2024db04e1c1f48a15649a7a4026a/71c23/validation-list.webp 1470w&quot; sizes=&quot;(max-width: 1470px) 100vw, 1470px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/05d2024db04e1c1f48a15649a7a4026a/9aebd/validation-list.png 480w,
/static/05d2024db04e1c1f48a15649a7a4026a/a91f8/validation-list.png 960w,
/static/05d2024db04e1c1f48a15649a7a4026a/3d705/validation-list.png 1470w&quot; sizes=&quot;(max-width: 1470px) 100vw, 1470px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/05d2024db04e1c1f48a15649a7a4026a/3d705/validation-list.png&quot; alt=&quot;유효성 검증 리스트&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;유효성 검증 리스트&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;개선-1-요청-그룹화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0-1-%EC%9A%94%EC%B2%AD-%EA%B7%B8%EB%A3%B9%ED%99%94&quot; aria-label=&quot;개선 1 요청 그룹화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선 1: 요청 그룹화&lt;/h2&gt;
&lt;p&gt;처리 시간을 단축하기 위해 유효성 검증 항목을 점검하던 중, 불필요한 중복 검증이 발생하고 있다는 점을 발견했습니다.
문제의 원인은 엑셀 양식의 구조에 있었는데, 동일한 매장 코드나 발주 일자 등의 값이 모든 상품 행마다 반복 입력되면서, 같은 값에 대한 검증이 매번 반복되고 있었던 것입니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 요청 DTO 구조를 개선했습니다. 기존의 &apos;단일 레코드 기반&apos; 구조에서 &apos;그룹 기반&apos; 구조로 변경하여, 헤더 레벨(물류센터, 발주일자 등)과 상품 리스트 레벨로 분리하고, 헤더 레벨 단위로 그룹화하여 중복 검증을 최소화하는 방향으로 변경했습니다. 아래 그림은 엑셀 데이터를 물류센터와 발주일자 기준으로 그룹화하여 DTO로 변환하는 개념적인 예시를 보여줍니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 796px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9fbfd741ba29e059d420c52d394b9d77/c0214/divided-request.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.75000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABk0lEQVR42pVSW27iQBD0yfcOuURugpawLEJiSUywsUN4GCLb857xi8r0ECRW2o9NS6WW7XZ1VXdH+GY0TQNtNVzrQraNDdBG43K5IOqG/r+IqJjCOYtslyPZptid90jfN5CNgrAyNIt+PD7gVH5ASQmtNcqyhBAioKoqKKXQtu0XK9B2LdLtBk+/J1i+PiPerPB23KLk1ZXwNUvxZ7lEHMfI8xyz2QxpmiLLMkynUxwOBxhjAh/nHDVnqHSNoj6jqE44sw8cy8KLOqHrOkRSSKxWq4D1eo3JxHf2DYhoPB6DMRaUE0ipaxy44rCthbIK0nhnjYFxBsMwICJr8/kci8UCSZJgNBoFwqIoglpr7V+2KQsloYyfmxKekDL3C7q6CISkgmZH+V4RzYQIiLDv+zBX5d9n+xwvSYy3wzbk2rCwmGCZim8/3uCcC3Mj0HdSeVNHtbtij5+/xti8Z2HTpawg3dVFRIXknUCn8S/ch/HWau0Xo2q4vkGHwaOHuif8Tlh/h7VgYQlMcg8Gof1i/DMd9id/Df2nBOLmxQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9fbfd741ba29e059d420c52d394b9d77/263a4/divided-request.webp 480w,
/static/9fbfd741ba29e059d420c52d394b9d77/c6463/divided-request.webp 796w&quot; sizes=&quot;(max-width: 796px) 100vw, 796px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9fbfd741ba29e059d420c52d394b9d77/9aebd/divided-request.png 480w,
/static/9fbfd741ba29e059d420c52d394b9d77/c0214/divided-request.png 796w&quot; sizes=&quot;(max-width: 796px) 100vw, 796px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9fbfd741ba29e059d420c52d394b9d77/c0214/divided-request.png&quot; alt=&quot;그룹화된 발주 요청 DTO 변환 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;그룹화된 발주 요청 DTO 변환 예시&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;이러한 그룹화 구조 개선을 통해, 예를 들어 물류센터 유효성 검증이나 발주일자 유효성 검증처럼 헤더 레벨에서 반복되던 유효성 검증을 그룹당 한 번만 수행하도록 변경할 수 있었습니다. 덕분에 불필요한 중복 검증을 줄이고 리드타임을 단축하는 데는 성공했지만, 전체 발주 처리 시간이 여전히 길다는 근본적인 문제는 해결하기 어려웠습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;개선-2-kafka-기반-비동기화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0-2-kafka-%EA%B8%B0%EB%B0%98-%EB%B9%84%EB%8F%99%EA%B8%B0%ED%99%94&quot; aria-label=&quot;개선 2 kafka 기반 비동기화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선 2: Kafka 기반 비동기화&lt;/h2&gt;
&lt;p&gt;이에 따라 서비스 아키텍처 자체를 변경하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;핵심은, 유효성 검증부터 발주 처리까지의 과정을 비동기 프로세스로 분리한 것입니다.
요청이 들어오면 Kafka를 통해 메시지를 발행하고, 발주 처리는 후속 단계에서 별도로 진행되도록 설계했습니다. 요청 시점에는 곧바로 응답을 반환하므로, 사용자 입장에서는 지연 없이 작업을 계속 이어갈 수 있습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/65b9465069f80ad7f5cc14f4c9b117d1/f1ea5/po-async-process.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.666666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA7klEQVR42jWQ6U7EMAyE+/4vyA+QQFoB7aZp7qM5OjsJNJKVw/aX8Szf+wbtLKy3UE6hlIKxemvIZ0LOCeU8Z1TmWm0z17m3WmdUxr2Wj8cXNingnYNjhBCQUkLwHspKpOgANqN3gKCaM988cgwoMaKyNrPnuq4/4H1IVOOD5+99qtyFgBArvNbIhMvnBqc0tJJ47iuOQyBaO/P6OCgk4nfdsLx9vuOx/UAZhV1LeDZnqgjOw0iJYAwKVVilCOAEhIx3x/v4wBFoCBxja2OxGBakMyMnQnyYsOkhR7w9asOzcec+1A8v6793d/728AV8NoOHMlChIAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/65b9465069f80ad7f5cc14f4c9b117d1/263a4/po-async-process.webp 480w,
/static/65b9465069f80ad7f5cc14f4c9b117d1/a6361/po-async-process.webp 960w,
/static/65b9465069f80ad7f5cc14f4c9b117d1/0b34d/po-async-process.webp 1920w,
/static/65b9465069f80ad7f5cc14f4c9b117d1/43f21/po-async-process.webp 2168w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/65b9465069f80ad7f5cc14f4c9b117d1/9aebd/po-async-process.png 480w,
/static/65b9465069f80ad7f5cc14f4c9b117d1/a91f8/po-async-process.png 960w,
/static/65b9465069f80ad7f5cc14f4c9b117d1/ac7a9/po-async-process.png 1920w,
/static/65b9465069f80ad7f5cc14f4c9b117d1/f1ea5/po-async-process.png 2168w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/65b9465069f80ad7f5cc14f4c9b117d1/ac7a9/po-async-process.png&quot; alt=&quot;올리브영 비동기 발주 서비스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;올리브영 비동기 발주 서비스&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;또한, 발주 담당자가 엑셀 파일을 업로드하면, 발주 데이터를 요청 DTO로 변환하는 과정까지는 각 백오피스에서 처리하도록 했습니다. 이후 비동기 프로세스를 거치면서, &lt;strong&gt;발주 요청 즉시 응답을 받을 수 있도록 하여 기존의 지연 문제를 효과적으로 해소&lt;/strong&gt;할 수 있었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📮 근데, 데이터 파이프라인으로 왜 Kafka를 선택했나요?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;메시지 유실 방지&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;- Kafka는 replication과 log 기반 메시지 저장 방식을 통해 메시지의 내구성과 고가용성을 보장합니다. 발주 서비스는 단 한 건의 메시지 유실도 치명적인 비즈니스 문제로 이어질 수 있기에, Kafka의 강력한 메시지 내구성 보장이 매우 중요했습니다. 덕분에 발주 시스템에서 가장 중요한 문제인 메시지 유실을 원천 차단할 수 있었습니다&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;재처리 자동화&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;- Kafka는 메시지 오프셋을 조정함으로써 특정 시점으로 되돌아가서 메시지를 다시 처리할 수 있습니다. 예를 들어 발주 처리 중 시스템 장애가 발생하면, 장애 발생 직전 오프셋부터 다시 재처리를 시작해 누락된 발주 건들을 복구할 수 있습니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;분산 처리를 통한 처리 성능 제고&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;- Kafka는 메시지를 여러 partition으로 분산 저장하는 수평 확장 구조라, 각 partition에 독립적인 Consumer를 할당해 병렬 처리할 수 있습니다. 이를 통해 발주량이 증가해도 Consumer 수를 늘려 처리 성능을 선형적으로 확장할 수 있습니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 이 설계에는 한 가지 근본적인 문제가 존재했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;비동기로 처리된 발주 결과를 사용자에게 어떻게 전달할 수 있을까요?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;API는 요청을 수신한 시점에 바로 응답을 반환하기 때문에, 이후 비동기적으로 처리된 결과를 함께 전달할 방법이 없었습니다.
그러나 사용자 입장에서는 발주가 언제 처리되었는지, 그리고 성공적으로 완료되었는지 확인할 수 있는 수단이 반드시 필요했습니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;비동기-요청-응답-아키텍처를-적용해보자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9A%94%EC%B2%AD-%EC%9D%91%EB%8B%B5-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90&quot; aria-label=&quot;비동기 요청 응답 아키텍처를 적용해보자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧑🏻‍💻비동기 요청-응답 아키텍처를 적용해보자&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;요청-응답-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%94%EC%B2%AD-%EC%9D%91%EB%8B%B5-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;요청 응답 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;요청-응답 아키텍처&lt;/h2&gt;
&lt;p&gt;이에 대한 해결책은 Enterprise Integration Patterns(EIP)에서 찾을 수 있었습니다. &lt;br&gt;
EIP는 기업 시스템 간 데이터 및 기능 통합을 위한 표준화된 아키텍처 패턴을 정리한 것이라고 볼 수 있는데요. &lt;br&gt; 소개된 수많은 아키텍처 중 &lt;em&gt;요청-응답 아키텍처&lt;/em&gt;를 참고하여 문제의 해결 실마리를 찾을 수 있었습니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enterprise Integration Patterns : &lt;a href=&quot;https://www.enterpriseintegrationpatterns.com/patterns/messaging/RequestReply.html&quot;&gt;&lt;strong&gt;request-reply 아키텍처&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Microsoft Azure Architecture : &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/async-request-reply&quot;&gt;&lt;strong&gt;async-request-reply 아키텍처&lt;/strong&gt;&lt;/a&gt;
&lt;br&gt;&lt;br&gt;
&lt;center style=&quot;overflow: hidden;&quot;&gt;
  	&lt;div style=&quot;width: 45%; display:block; float:left; padding: 0 2% 0 2%;&quot;&gt;
  		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 372px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/916fc5e0ea1e85af2261d30ec4fd160b/448ef/eip-request-reply.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.387096774193544%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElEQVR42mP49Cnx47t3Ce8+f074e/Vq3GYGIKisTBZPT0/v9fHx4aqvr2cpLy/nb2trU4FitWnT2gUZGCS5nj2LvfDxY+LP9+8T3nz4EP//4sXY5Qz//2f+//s348///7n/791LPSIoWCS7f8oqnrBaA9O6XQ7NmZmZgtXV9Q49PT2nJkyYcLS/v/9qV1dHlKiovcT792nP/v/P/v//f8av//9z/t+6lbIVaGD6/z9/0v6AJO7dS9oPcqEWAwObXaS0atJkwwgglw2ImTg5OaW4uLgkxMXFxf7/X8UMEvv6Nenm//9ZIP2/QPSNG4kQA//+hRh4/37SAZCB9vYMPEBKAoiFgZgdiFmAWA1ooBGQVmaAABaYgUD9YANv3cIwMBls4P//DIxAf4AxVDMTEHMDMZc4hCbOwAcPIF6GAkYGNJCVlZWZnR0rTNDAP39S/4IErl9Pu8jAUCjU09OaDwz8XZMmTdrQ19e3rCw/XzM3t54vNirKZ0uJvsHEjnoZS8so+S9fku9gGPjjR/rfr1/Tf4FcePVq8lEGhnqm8vJ0fWDshmdnZwcWFRW55+bm8oWuWsUc4yrOvadAJWRmeSg/A4Mx17u3yTdABn3/nv4dRF++nLSVobQ01K+sLNyzoiLUr74+wQjmHSBWAGIZaPiBATc3NzAIGeSgEcXQ3h5lXF4e7F9UFODT2Bjl2dGRpgsSF4fGqCjUIAZRUVEeYDKRYWdnVwSxkcNx1SoGZrTwFYZawgXiAQDPE0xTcTdG1AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/916fc5e0ea1e85af2261d30ec4fd160b/242af/eip-request-reply.webp 372w&quot; sizes=&quot;(max-width: 372px) 100vw, 372px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/916fc5e0ea1e85af2261d30ec4fd160b/448ef/eip-request-reply.png 372w&quot; sizes=&quot;(max-width: 372px) 100vw, 372px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/916fc5e0ea1e85af2261d30ec4fd160b/448ef/eip-request-reply.png&quot; alt=&quot;Request-Reply Pattern (Enterprise Integration Patterns)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  		&lt;figcaption&gt;Request-Reply Pattern (Enterprise Integration Patterns)&lt;/figcaption&gt;
  	&lt;/div&gt;
  	&lt;div style=&quot;width: 45%; display:block; float:left;&quot;&gt;
  		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 900px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d69c79792aab8a0d45c0ef03d6f77544/e7448/azure-request-reply.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 27.916666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAABB0lEQVR42lWOy07EMAxF+6d8Dt8CGzYsWLACCYbFLFgiDSrTRzpt4ubhNE2TJiG0EtIcWZZl697r4uPwBhlt1ezonMb2jg+fFFeGMyh7qNTtSzOMEpTJG6ISdE+SvseUCUVZtaz+7oU2LhifGIJmFWfDqDTn/DToxy8h+4qPAEKBQDRcz5Nz3i1LcanL6gItFU0PVUepsumaGK8mI1iW1C05n38KbX2KIeZD/GvBsRjmf6UPUcxhk+2V0Hrr1s0qFgMDAqobkYDsMDHyQNtjB6aF/KF9Pk0396QWHm3gJmQjOoXsuFsXHhmi0pNG1M6vPjiHYFDqybjFNtK/lkbakPbcPXEjpfQLy6RUC6te1jMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d69c79792aab8a0d45c0ef03d6f77544/263a4/azure-request-reply.webp 480w,
/static/d69c79792aab8a0d45c0ef03d6f77544/d0742/azure-request-reply.webp 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d69c79792aab8a0d45c0ef03d6f77544/9aebd/azure-request-reply.png 480w,
/static/d69c79792aab8a0d45c0ef03d6f77544/e7448/azure-request-reply.png 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d69c79792aab8a0d45c0ef03d6f77544/e7448/azure-request-reply.png&quot; alt=&quot;Request-Reply Pattern (Microsoft Azure)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  		&lt;figcaption&gt;Request-Reply Pattern (Microsoft Azure)&lt;/figcaption&gt;
  	&lt;/div&gt;
  &lt;/center&gt;
  &lt;br&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;요청-응답 아키텍처&lt;/strong&gt;란, 요청과 응답을 각각의 채널로 분리하여 처리하는 구조를 의미합니다.
이 패턴을 발주 서비스에 적용하면, 비동기 처리를 하면서도 요청자가 발주 결과를 수신할 수 있을 거라 기대했습니다.&lt;/p&gt;
&lt;p&gt;그래서 이 아키텍처를 실제 서비스에 도입해 보기로 했고, 이후 여러 번의 설계 시도를 거쳤습니다.
두 번의 실패를 딛고, 최종적으로 올리브영 발주 서비스에 가장 잘 맞는 형태의 요청-응답 아키텍처를 완성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이제부터 그 과정을 순서대로 소개하려고 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫 번째 설계: API Polling 기반 아키텍처
&lt;ul&gt;
&lt;li&gt;첫 번째 설계의 한계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;두 번째 설계: ReplyingKafkaTemplate 기반 아키텍처
&lt;ul&gt;
&lt;li&gt;두 번째 설계의 한계&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최종 설계: API + Kafka를 결합한 하이브리드 요청-응답 아키텍처&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;첫-번째-설계-api-polling-기반-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EC%84%A4%EA%B3%84-api-polling-%EA%B8%B0%EB%B0%98-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;첫 번째 설계 api polling 기반 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;첫 번째 설계: API Polling 기반 아키텍처&lt;/h2&gt;
&lt;p&gt;첫 설계안은 Microsoft Azure의 아키텍처를 참고하여, API Polling 기반 비동기 아키텍처로 설계했습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ca526096b5c7697cd72f9e0537bb89ce/e249b/first-archi.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA/ElEQVR42j1Q127EMAzL/39iURRo+9Di7hJ5KI7jxaOdYYCwJilpwvlSySi1XC7W4GFVkPcdhagpoeWCwj+GFSEo9m1DjpE1Ea210Td9/HxhcQbiBNZb7GyOLHK0jczYvEcgdpKoMYheIcsTIi/GHVQM1LKPQob/5HoxSZSFnnZHT2y6jmJ/NnRiO8/DtvMCR3gRLM/HqNk47cJ/8lSOJDRq4VeFqsI5h0Dbq0EKAY3qKDxHqcfa3OBCz42T1Hqs/Pn7jcf8ghiB4UqFjamPT/XH/9+YrPaGE4M85wNDpIz73oTXMSvVe/DyE9X77RJXqecUN7o/YunOtZPwDb750oEn0stVAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ca526096b5c7697cd72f9e0537bb89ce/263a4/first-archi.webp 480w,
/static/ca526096b5c7697cd72f9e0537bb89ce/a6361/first-archi.webp 960w,
/static/ca526096b5c7697cd72f9e0537bb89ce/0b34d/first-archi.webp 1920w,
/static/ca526096b5c7697cd72f9e0537bb89ce/57a23/first-archi.webp 2192w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ca526096b5c7697cd72f9e0537bb89ce/9aebd/first-archi.png 480w,
/static/ca526096b5c7697cd72f9e0537bb89ce/a91f8/first-archi.png 960w,
/static/ca526096b5c7697cd72f9e0537bb89ce/ac7a9/first-archi.png 1920w,
/static/ca526096b5c7697cd72f9e0537bb89ce/e249b/first-archi.png 2192w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ca526096b5c7697cd72f9e0537bb89ce/ac7a9/first-archi.png&quot; alt=&quot;API Polling 기반 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;1차 설계: API Polling 기반 아키텍처&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;ol&gt;
&lt;li&gt;클라이언트가 발주 API를 호출합니다.&lt;/li&gt;
&lt;li&gt;서버는 &lt;strong&gt;요청 이력&lt;/strong&gt;을 생성하고, 이력 ID를 포함한 메시지를 Kafka로 발행합니다. (컨슈머에 있던 이력 적재 단계를 앞으로 배치함)&lt;/li&gt;
&lt;li&gt;서버는 클라이언트에게 요청 이력 ID를 응답합니다.&lt;/li&gt;
&lt;li&gt;발주 처리 Consumer가 요청 메시지를 수신하여 발주 처리를 수행합니다.&lt;/li&gt;
&lt;li&gt;발주 처리 결과를 요청 이력에 업데이트합니다. (예외 발생 시, 데드레터 메시지 처리 및 알림)&lt;/li&gt;
&lt;li&gt;클라이언트는 &lt;strong&gt;요청 이력 결과 조회 API&lt;/strong&gt;를 통해 처리 결과를 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;이 구조를 택한 이유는 &lt;strong&gt;요청자로 하여금, 최소한의 의존성을 갖도록 하기 위함&lt;/strong&gt;이었습니다.&lt;br&gt;
EIP는 메시징 기반 아키텍처를 소개하는데, 이는 별도 메시지 파이프라인이 필요한 구성입니다. 반면 Azure에서 제시하는 아키텍처는 API만으로도 구성 가능해 큰 장점으로 다가왔습니다. 아키텍처 구조도 복잡하지 않아, 브랜드원 개발자에게 발주 서비스 구조를 설명하기가 쉬웠습니다.&lt;/p&gt;
&lt;p&gt;물론, 이 구조는 클라이언트가 결과가 나올 때까지 주기적으로 API를 호출(polling) 해야 하는 단점이 있었습니다. 호출 주기에 따라 반영 속도가 지연되거나 발주 서비스에 부하를 줄 수도 있었습니다.&lt;/p&gt;
&lt;p&gt;그럼에도 불구하고, 트래픽을 감안했을 때, 실행 불가능한 구조는 아니었기에 충분히 운용 가능한 설계라고 생각했습니다.&lt;/p&gt;
&lt;h3 id=&quot;️-첫-번째-설계의-한계-중복-메시지-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EC%84%A4%EA%B3%84%EC%9D%98-%ED%95%9C%EA%B3%84-%EC%A4%91%EB%B3%B5-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;️ 첫 번째 설계의 한계 중복 메시지 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⚠️ 첫 번째 설계의 한계: 중복 메시지 처리&lt;/h3&gt;
&lt;p&gt;하지만, Kafka의 at-least-once 전달 보장 특성 때문에 구조 변경이 불가피해졌습니다.&lt;/p&gt;
&lt;p&gt;Kafka는 배포, 스케일 인/아웃 등으로 Consumer Group의 리밸런싱이 발생할 수 있고, 이로 인해 요청 메시지가 재처리될 수 있습니다.&lt;br&gt; 발주 처리는 멱등(idempotent)하지 않아서 동일한 메시지가 두 번 처리되면, &lt;strong&gt;같은 상품이 두 번 발주&lt;/strong&gt;되어 중대한 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;보통, 중복 처리를 방지하려면 두 가지 선택지가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;처리를 재수행해도 멱등적인 결과를 낼 수 있도록 발주 처리 로직을 수정한다.&lt;/li&gt;
&lt;li&gt;중복 자체를 차단, 한 번 처리한 요청은 다시 처리되지 않도록 메시지를 필터링한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;첫 번째 방식은, 발주 처리 로직 자체를 수정해야 하는데, 기존 구조를 깨야하기 어려움이 있었습니다. 이에, 중복을 차단하는 두 번째 방식을 택하게 됩니다.&lt;/p&gt;
&lt;p&gt;두 번째 방법의 차단 방법은 대표적으로 Database의 Unique 제약 조건을 사용한 것인데요. 발주 처리의 경우 구조상 이 제약 조건을 사용할 수 없었습니다.
이를 위해, 이력 적재 단계를 다시 Producer(API 호출부) → Consumer로 이전했습니다. &lt;br&gt;
요청 메시지의 토픽(구분자), 파티션, 오프셋 값을 기준으로 Unique Index 기준을 설정하고, 이를 구성한 요청 이력을 생성합니다.
이렇게 구성하게 되면, 동일한 Index를 가진 메시지가 처리될 경우, 예외가 발생하여 커밋되지 못하도록 처리할 수 있습니다. 이를 통해, 동일 요청 메시지가 재처리되지 않도록 하였습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1394px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/303443d01a7fe39fe3526c94f92e2511/02f69/first-archi-fix.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.458333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAABBElEQVR42n1RyU7EMBTr/38hB5AQBxjaZt+3GiczDDcqWWkSPz/7ZcPja71hjPG7RUgBxim0UtCJUQuunNGtRdIKOTq0nDB4V1PC9ajdXj7eoIyGNBKaa29tiUgloLRA2g/EY2dRgNca2TlIefD+gCMnWQOrNMqq0dg0DzI7e+fhSB78n0WWxY4Ed7vBHweyD1BcAx1aqRb0ecJIGhFiaQghsXl2zoxjvIGPHjUm9BjhvaXDEyVFoHeiATMWE/TC6PmOq9YVeTwjv79iF4zATkrJRaD/1fX763M5mnP6QyWnLeF7o45K4afgdV33R5mzmyTu51mms+Q9cggcfv4XZT7KQ+cHD2TRujWCu7UAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/303443d01a7fe39fe3526c94f92e2511/263a4/first-archi-fix.webp 480w,
/static/303443d01a7fe39fe3526c94f92e2511/a6361/first-archi-fix.webp 960w,
/static/303443d01a7fe39fe3526c94f92e2511/2e4e1/first-archi-fix.webp 1394w&quot; sizes=&quot;(max-width: 1394px) 100vw, 1394px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/303443d01a7fe39fe3526c94f92e2511/9aebd/first-archi-fix.png 480w,
/static/303443d01a7fe39fe3526c94f92e2511/a91f8/first-archi-fix.png 960w,
/static/303443d01a7fe39fe3526c94f92e2511/02f69/first-archi-fix.png 1394w&quot; sizes=&quot;(max-width: 1394px) 100vw, 1394px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/303443d01a7fe39fe3526c94f92e2511/02f69/first-archi-fix.png&quot; alt=&quot;변경된 1차 설계&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;변경된 1차 설계&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;그러나, 이 구조 변경은 요청 이력 ID를 API 응답에서 반환할 수 없게 만들었고, 그에 따라 기존에 사용하던 &lt;strong&gt;요청 이력 조회 API를 통한 결과 확인도 불가능&lt;/strong&gt;해졌습니다. 즉, 클라이언트 입장에서 &lt;strong&gt;요청과 응답을 연결하는 식별자가 사라진 상황&lt;/strong&gt;이 되었습니다.&lt;/p&gt;
&lt;p&gt;위 이유로, API 기반 폴링 구조는 포기하고 다시 설계를 진행했습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;두-번째-설계-replyingkafkatemplate-기반-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%84%A4%EA%B3%84-replyingkafkatemplate-%EA%B8%B0%EB%B0%98-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;두 번째 설계 replyingkafkatemplate 기반 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;두 번째 설계: ReplyingKafkaTemplate 기반 아키텍처&lt;/h2&gt;
&lt;p&gt;두 번째 설계에서 검토한 기술은 Kafka의 공식 템플릿인 &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;입니다.
이 템플릿은 request-reply 시맨틱을 지원하며, Kafka 기반의 요청-응답 구조를 간편하게 구현할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p&gt;특히 &lt;code class=&quot;language-text&quot;&gt;sendAndReceive&lt;/code&gt; 메서드를 사용하면, 마치 WebClient로 API를 호출하듯 요청에 대한 응답을 &lt;code class=&quot;language-text&quot;&gt;RequestReplyFuture&lt;/code&gt; 형태로 수신할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendAndReceive&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProducerRecord&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendAndReceive&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProducerRecord&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token annotation punctuation&quot;&gt;@Nullable&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Duration&lt;/span&gt; replyTimeout&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이를 활용하면 Kafka 기반의 요청-응답 아키텍처를 손쉽게 구성할 수 있을 것으로 기대했습니다. 실제로 아래와 같이 아키텍처를 설계해보았습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f67c18e2c8c7b575cc7f6ea9bf776084/725bf/second-archi.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 24.583333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA80lEQVR42k1QyU7EMAzt/38dHBASGjjAgaFtVmd10j6cdJCI5NiW7Ld42ZxGrRWGLCgGlNyQYsF5njO4MZgremtoldGY0bldPT96qcfseMvT2wuMNbDWwnmC3TuMiogpTiIrhOt+h1UKRylCRnBkQORQYwSnhCK5934Bjm+gl1pQuUw1LKzHcSCEAL3t+P78wsftHUkIndHQ+wYvOTg3w2mNnDPuPyuW59srrHdQosQFj+gFyGckUVhygRflY3mX4USEIiRkLKK9wMiYmZvYDjFhWbUSewlBhgMFWa4gl8RCmxaGlf/3GupZTlEfMfo/u8PyL9fRgq6ZKkiuAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f67c18e2c8c7b575cc7f6ea9bf776084/263a4/second-archi.webp 480w,
/static/f67c18e2c8c7b575cc7f6ea9bf776084/a6361/second-archi.webp 960w,
/static/f67c18e2c8c7b575cc7f6ea9bf776084/0b34d/second-archi.webp 1920w,
/static/f67c18e2c8c7b575cc7f6ea9bf776084/ad61f/second-archi.webp 1998w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f67c18e2c8c7b575cc7f6ea9bf776084/9aebd/second-archi.png 480w,
/static/f67c18e2c8c7b575cc7f6ea9bf776084/a91f8/second-archi.png 960w,
/static/f67c18e2c8c7b575cc7f6ea9bf776084/ac7a9/second-archi.png 1920w,
/static/f67c18e2c8c7b575cc7f6ea9bf776084/725bf/second-archi.png 1998w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f67c18e2c8c7b575cc7f6ea9bf776084/ac7a9/second-archi.png&quot; alt=&quot;ReplyingKafkaTemplate 기반 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;2차 설계: ReplyingKafkaTemplate 기반 아키텍처&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이 방식은 첫 번째 설계보다 훨씬 단순하고 명확해 보였습니다.
다만, 각 백오피스가 이 템플릿을 사용하려면 Kafka 관련 의존성과 설정을 직접 포함해야 하며, 무엇보다 &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;의 동작 방식에 대한 충분한 이해 없이 도입하는 것은 위험할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;따라서, 실제로 이 설계가 현실적인지, 그리고 안정적으로 동작할 수 있는지 직접 동작 방식부터 살펴보며 검토해보기로 했습니다.&lt;/p&gt;
&lt;h3 id=&quot;replyingkafkatemplate-톺아보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#replyingkafkatemplate-%ED%86%BA%EC%95%84%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;replyingkafkatemplate 톺아보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;ReplyingKafkaTemplate 톺아보기&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;가 어떻게 요청-응답 패턴을 구현할 수 있는지, 하나씩 살펴보겠습니다. &lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;1-요청-메시지-발행---replyingkafkatemplate-사용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9A%94%EC%B2%AD-%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B0%9C%ED%96%89---replyingkafkatemplate-%EC%82%AC%EC%9A%A9&quot; aria-label=&quot;1 요청 메시지 발행   replyingkafkatemplate 사용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 요청 메시지 발행 - ReplyingKafkaTemplate 사용&lt;/h4&gt;
&lt;p&gt;먼저, 해당 템플릿을 활용해 요청을 발행하는 &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaProducer&lt;/code&gt; 서비스를 구성해보겠습니다.
이 서비스는 &lt;code class=&quot;language-text&quot;&gt;sendAndReceive&lt;/code&gt; 메서드를 통해 요청을 전송하고, 그에 대한 응답을 &lt;code class=&quot;language-text&quot;&gt;RequestReplyFuture&lt;/code&gt; 형태로 비동기 수신이 가능합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* ReplyingKafkaProducer */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReplyingKafkaTemplate&lt;/span&gt; replyingKafkaTemplate&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ConsumerRecord&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendAndReceive&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

	&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

	&lt;span class=&quot;token class-name&quot;&gt;RecordHeaders&lt;/span&gt; headers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RecordHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;// 응답 토픽 설정&lt;/span&gt;
	headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;KafkaHeaders&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;REPLY_TOPIC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; replyTopic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getBytes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;StandardCharsets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;UTF_8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// 요청 메시지 생성&lt;/span&gt;
	&lt;span class=&quot;token class-name&quot;&gt;ProducerRecord&lt;/span&gt; producerRecord &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ProducerRecord&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; replyingKafkaTemplate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sendAndReceive&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;producerRecord&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ofSeconds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;REPLY_TIMEOUT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;sendAndReceive&lt;/code&gt; 메서드를 통해 요청 메시지를 전송할 때, 반드시 메시지 헤더에 다음 두 가지 항목을 설정해야 합니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;KafkaHeaders.REPLY_TOPIC
&lt;ul&gt;
&lt;li&gt;응답 메시지를 받기 위한 응답 토픽입니다.&lt;/li&gt;
&lt;li&gt;이 토픽은 &lt;code class=&quot;language-text&quot;&gt;replyContainer&lt;/code&gt; 빈을 설정할 때에도 동일하게 구독 설정이 되어 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;KafkaHeaders.CORRELATION_ID  (⭐️ 아주 중요!)
&lt;ul&gt;
&lt;li&gt;요청과 응답을 연결하는 식별자입니다.&lt;/li&gt;
&lt;li&gt;이 값은 템플릿 내부에서 기본적으로 UUID로 자동 설정되며, &lt;strong&gt;요청과 응답의 연결 고리를 담당하는 핵심 요소&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-응답-메시지-발행---sendto-사용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%9D%91%EB%8B%B5-%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B0%9C%ED%96%89---sendto-%EC%82%AC%EC%9A%A9&quot; aria-label=&quot;2 응답 메시지 발행   sendto 사용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 응답 메시지 발행 - @SendTo 사용&lt;/h4&gt;
&lt;p&gt;요청 메시지를 컨슈머가 수신하면, 해당 메시지에 대한 처리를 수행한 후 응답 메시지를 전송해야 합니다.
이 과정은 &lt;code class=&quot;language-text&quot;&gt;@SendTo&lt;/code&gt; 어노테이션을 활용해 아래와 같이 간단하게 구현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* PurchaseOrderListener */&lt;/span&gt;

&lt;span class=&quot;token annotation builtin&quot;&gt;@KafkaListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	topics &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;purchase-order.topic&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	containerFactory &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@SendTo&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ReplyMessage &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;요청 메시지 헤더에 담긴 &lt;code class=&quot;language-text&quot;&gt;CORRELATION_ID&lt;/code&gt; 값은 &lt;strong&gt;응답 메시지로 복제&lt;/strong&gt;된 후, 응답 토픽으로 함께 전송되게 됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;&lt;br&gt;&lt;code class=&quot;language-text&quot;&gt;Message&amp;lt;?&gt;&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;Collection&amp;lt;Message&amp;lt;?&gt;&gt;&lt;/code&gt;를 응답 포맷으로 설정한 경우, 응답 메시지 헤더를 명시적으로 설정해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;&lt;br&gt; &lt;code class=&quot;language-text&quot;&gt;@SendTo&lt;/code&gt;가 동작하기 위해선 &lt;code class=&quot;language-text&quot;&gt;KafkaListenerContainer&lt;/code&gt;에 &lt;code class=&quot;language-text&quot;&gt;ReplyKafkaTemplate&lt;/code&gt; 설정이 추가되어 있어야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;3-응답-메시지-수신-및-연결---reply-consumer&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%9D%91%EB%8B%B5-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%88%98%EC%8B%A0-%EB%B0%8F-%EC%97%B0%EA%B2%B0---reply-consumer&quot; aria-label=&quot;3 응답 메시지 수신 및 연결   reply consumer permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 응답 메시지 수신 및 연결 - Reply Consumer&lt;/h4&gt;
&lt;p&gt;응답 메시지가 발행되면, &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt; 내부의 replyContainer가 해당 메시지를 수신합니다.
이때 메시지의 &lt;code class=&quot;language-text&quot;&gt;CORRELATION_ID&lt;/code&gt;를 기준으로, 요청 당시 생성한 &lt;code class=&quot;language-text&quot;&gt;RequestReplyFuture&lt;/code&gt;를 찾아 응답 데이터를 설정함으로써 요청-응답 흐름이 완성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* ReplyingKafkaTemplate */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;onMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ConsumerRecord&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
       &lt;span class=&quot;token class-name&quot;&gt;Header&lt;/span&gt; correlationHeader &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lastHeader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;correlationHeaderName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

		&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

       &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// 요청 future 가져옵니다.&lt;/span&gt;
          &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; future &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;futures&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;correlationId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

          &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;future &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
             &lt;span class=&quot;token function&quot;&gt;logLateArrival&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; correlationId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	         &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
             future&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
       &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;details&gt;
	&lt;summary&gt;🎁🎁🎁 ReplyingKafkaTemplate에 대해 조금 더 알고싶다면 펼쳐보세요. 🎁🎁🎁&lt;/summary&gt;
&lt;br&gt;
&lt;h3 id=&quot;replyingkafkatemplate-구성-시-고려사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#replyingkafkatemplate-%EA%B5%AC%EC%84%B1-%EC%8B%9C-%EA%B3%A0%EB%A0%A4%EC%82%AC%ED%95%AD&quot; aria-label=&quot;replyingkafkatemplate 구성 시 고려사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;ReplyingKafkaTemplate 구성 시 고려사항&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;의 내부 &lt;code class=&quot;language-text&quot;&gt;replyContainer&lt;/code&gt;에 다음과 같은 설정을 권장합니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;서버 인스턴스별 고유한 group.id 설정&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버 인스턴스마다 고유한 group.id(예: UUID)를 부여해야 리밸런싱을 방지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;동일한 group.id를 사용하는 경우, 배포나 오토 스케일링 시 컨슈머 리밸런싱이 발생하게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;auto.offset.reset=latest&lt;/code&gt; 설정&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;earliest로 설정 시, 서버 구동 이전의 응답 메시지까지 모두 컨슘하게 되어 불필요한 처리 비용이 발생합니다.&lt;/li&gt;
&lt;li&gt;latest로 설정하면, 구동 이후에 도착한 응답 메시지만 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Transactional Sementic&lt;/code&gt;과 병행 사용 불가&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;위 시맨틱을 위해 &lt;code class=&quot;language-text&quot;&gt;isolation.level=read_committed&lt;/code&gt;로 설정되어 있는 경우, 요청을 전송했지만 커밋되지 않은 메시지이므로 수신할 수 없습니다.&lt;/li&gt;
&lt;li&gt;위 시맨틱을 같이 사용해야 한다면, &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt; 외 다른 방식으로 구현해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;공유-응답-토픽-shared-reply-topic&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%B5%EC%9C%A0-%EC%9D%91%EB%8B%B5-%ED%86%A0%ED%94%BD-shared-reply-topic&quot; aria-label=&quot;공유 응답 토픽 shared reply topic permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;공유 응답 토픽 (Shared Reply Topic)&lt;/h4&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;은 요청자 서버 내에서 요청과 응답을 모두 처리하는 구조입니다.
따라서 요청자 서버 수가 늘어날수록 요청 및 응답 토픽에 붙는 프로듀서와 컨슈머 수도 증가하게 됩니다. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;그렇다면 만약, 여러 요청자 서버가 동작하는 상황에서 각 요청에 대한 응답이 정확히 해당 요청자 서버로 전달되도록 하려면 어떻게 해야 할까요? 예를 들어, 아래와 같은 상황을 보겠습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1770px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7753653c87a6d1d3c4186107852db625/d77a7/rkt-inf.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 34.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABMElEQVR42oVSaUvDQBDd//8viqjFFsQv4lHoD9BSj9KaqsGkieQoLSTNtUdeZ7ZE6Ad14bGzw7w314o8z1HXNYqigJQSv522bS3+O2Kz2SAIAsRxbEVZnIWZHCcpFs4SzvIdYfhtY9hmjjHmJ5FSykJrDVGWpRXa7XbIyhzKaLCPRZ9eZuidXqA/vMJk+oyPTxe9swG+PA+yaSyHO8yyDNvt1tqCK2KBhgI0ialWw7SH7Ma05K/h+x7Bx2rlEyk7GkFVVVYwiiJrC350UI3kSEgtSVyhpMrnCwdv1PZ6vcbDZIpHwutsbsmMrqDuFkmS0HxCpESQ5ADPj0HzYMGb+zHuRmO4rouT/hDng0tc345IrLQzO3RiLLhi0S2Bs3UBVObRdnkJnDhNE6g/fgJveQ/lABYsvlo3kQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7753653c87a6d1d3c4186107852db625/263a4/rkt-inf.webp 480w,
/static/7753653c87a6d1d3c4186107852db625/a6361/rkt-inf.webp 960w,
/static/7753653c87a6d1d3c4186107852db625/2bc88/rkt-inf.webp 1770w&quot; sizes=&quot;(max-width: 1770px) 100vw, 1770px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7753653c87a6d1d3c4186107852db625/9aebd/rkt-inf.png 480w,
/static/7753653c87a6d1d3c4186107852db625/a91f8/rkt-inf.png 960w,
/static/7753653c87a6d1d3c4186107852db625/d77a7/rkt-inf.png 1770w&quot; sizes=&quot;(max-width: 1770px) 100vw, 1770px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7753653c87a6d1d3c4186107852db625/d77a7/rkt-inf.png&quot; alt=&quot;2개의 요청자 서버가 요청하는 경우&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;2개의 요청자 서버가 요청하는 경우&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;위와 같이, &lt;code class=&quot;language-text&quot;&gt;A 요청자&lt;/code&gt; 서버에서 발주 요청 메시지를 발행한 경우, &lt;code class=&quot;language-text&quot;&gt;응답자&lt;/code&gt; 서버에선 발주 처리를 수행할 것이고, 처리 결과가 담긴 응답 메시지는 응답 토픽으로 발행될 것입니다. &lt;br&gt;
응답 토픽을 구독하는 &lt;code class=&quot;language-text&quot;&gt;replyContainer&lt;/code&gt;는 &lt;code class=&quot;language-text&quot;&gt;A 요청자&lt;/code&gt; 서버 뿐만 아니라, &lt;code class=&quot;language-text&quot;&gt;B 요청자&lt;/code&gt; 서버도 있기 떄문에, 메시지는 &lt;code class=&quot;language-text&quot;&gt;B 요청자&lt;/code&gt;에게도 갈 수 있습니다. &lt;br&gt; 그러면, &lt;code class=&quot;language-text&quot;&gt;A 요청자&lt;/code&gt; 서버는 응답 메시지를 못 받게 되지 않을까요?&lt;/p&gt;
&lt;p&gt;이 문제는 서버 인스턴스별 &lt;code class=&quot;language-text&quot;&gt;group.id&lt;/code&gt; 를 동일하게 구성한 여부에 따라 나뉘게 됩니다.&lt;/p&gt;
&lt;h5 id=&quot;1-동일한-groupid-로-구성한-경우&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%8F%99%EC%9D%BC%ED%95%9C-groupid-%EB%A1%9C-%EA%B5%AC%EC%84%B1%ED%95%9C-%EA%B2%BD%EC%9A%B0&quot; aria-label=&quot;1 동일한 groupid 로 구성한 경우 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 동일한 &lt;code class=&quot;language-text&quot;&gt;group.id&lt;/code&gt; 로 구성한 경우&lt;/h5&gt;
&lt;p&gt;이 경우, 응답 메시지가 엉뚱한 요청자에게 갈 수 있습니다. 이를 방지할 수 있는 방법은 &lt;code class=&quot;language-text&quot;&gt;TopicPartitionOffset&lt;/code&gt; 설정을 통해 각 인스턴스 별로 서로 다른 파티션을 각각 할당해주는 것입니다.
&lt;center&gt;
&lt;div style=&quot;width: 70%; display:block; padding:20px 0 20px 0;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 670px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6c057a81a8e48e411e1dabf377c138e1/6bbca/rkt-inf-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.04166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAABaElEQVR42qVS72vCMBDt//9XyJBtDgd+8INM1M2pKxuCMgb77WrLWm1rta02Nk3fctkqjDkYLCHckdy9vHd3WpIk2G63YIwp+9+lRVEEy7LgOA6WyyVYwsA5R5ZlWK1CvBlTGFMTzmwG13XxOjHk/Qp5nisAskKI3dGIYRzHWK/XCOMIjLMvtgzPLxOUyhWUDk/QG+i4f3jEwVEVpmkqNUSG8haLBTzPU75GTMjZbDaKFc+zb79znmLm2FKBrZTEEqR4o3gC9n1fARIRjYDCMFSH2NLiQkoWmZSQwfUDPJlzZd35HLbtqGSKL+pOGOQrwCAIdjUMoxCpSD8ZyE2y290BjmsNGOY7Tmt1lCtVNJotpGn6o4bka0WHiR0FySfFkCxjCfThGPWODsvx0O1fSbAO+vo1hJS7t8tFLYofCnZq5wLj2ztcXPZgyUY0mm2ctc5xMxyp+L2Af5ktGhPbtmXt3J3U3+bwA6l9sAK+dDKSAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6c057a81a8e48e411e1dabf377c138e1/263a4/rkt-inf-2.webp 480w,
/static/6c057a81a8e48e411e1dabf377c138e1/32193/rkt-inf-2.webp 670w&quot; sizes=&quot;(max-width: 670px) 100vw, 670px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6c057a81a8e48e411e1dabf377c138e1/9aebd/rkt-inf-2.png 480w,
/static/6c057a81a8e48e411e1dabf377c138e1/6bbca/rkt-inf-2.png 670w&quot; sizes=&quot;(max-width: 670px) 100vw, 670px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6c057a81a8e48e411e1dabf377c138e1/6bbca/rkt-inf-2.png&quot; alt=&quot;요청자 수만큼 토픽 파티션을 각각 구독합니다&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;요청자 수만큼 토픽 파티션을 각각 구독합니다&lt;/figcaption&gt;
&lt;/div&gt;
&lt;/center&gt;&lt;/p&gt;
&lt;h5 id=&quot;2-서로-다른-groupid를-구성한-경우&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%84%9C%EB%A1%9C-%EB%8B%A4%EB%A5%B8-groupid%EB%A5%BC-%EA%B5%AC%EC%84%B1%ED%95%9C-%EA%B2%BD%EC%9A%B0&quot; aria-label=&quot;2 서로 다른 groupid를 구성한 경우 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 서로 다른 &lt;code class=&quot;language-text&quot;&gt;group.id&lt;/code&gt;를 구성한 경우&lt;/h5&gt;
&lt;p&gt;이 경우, 응답 메시지는 모든 요청자에게 수신됩니다. 따라서, 요청자는 요청에 대한 응답 메시지를 반드시 수신받을 수 있습니다.&lt;/p&gt;
&lt;center&gt;
       &lt;div style=&quot;width: 70%; display:block; padding:20px 0 20px 0;&quot;&gt;
           &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1772px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/64e577c5ff3e5d7736c3d0020bfb08eb/e670e/rkt-inf-3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABJklEQVR42qVR20rDQBDN/3+EhXojCCKCRRR8FfGxlrbmKaYxN5uE3NNscpyzssUXn9xlmB125lx2raqqcDgc0HUd+r7Hf5eVZRl830cQBCA4gUkwTROS5AvrzRbbd0d6PhFFETZy5sw4jkcQpZSuGVZd12jbFsxlI2rVgLZpBbTHcrXG7NTG/OIKzy+vWL6tMDuz4XmedkMBjDzPkaapPlu8aJpGK1OjwiBBdWaRNQxDUZsgiWNN/lsZZ4ui0Pe8swwLw7whQcdp1NZpMwhCreJDlHneDmEUoypLDUAhzJzVgLGw8g1jNtGygBmltdR3949YPDxpm+f2NU7ml7i5XYi64ejEvB9ry3wC8zBIE/c0/WRp7oR5J4Su68JxHOz3qdhs//zlbyvcFvIJFY/YAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/64e577c5ff3e5d7736c3d0020bfb08eb/263a4/rkt-inf-3.webp 480w,
/static/64e577c5ff3e5d7736c3d0020bfb08eb/a6361/rkt-inf-3.webp 960w,
/static/64e577c5ff3e5d7736c3d0020bfb08eb/98657/rkt-inf-3.webp 1772w&quot; sizes=&quot;(max-width: 1772px) 100vw, 1772px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/64e577c5ff3e5d7736c3d0020bfb08eb/9aebd/rkt-inf-3.png 480w,
/static/64e577c5ff3e5d7736c3d0020bfb08eb/a91f8/rkt-inf-3.png 960w,
/static/64e577c5ff3e5d7736c3d0020bfb08eb/e670e/rkt-inf-3.png 1772w&quot; sizes=&quot;(max-width: 1772px) 100vw, 1772px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/64e577c5ff3e5d7736c3d0020bfb08eb/e670e/rkt-inf-3.png&quot; alt=&quot;단일 공유 응답 토픽으로 모든 요청 서버 인스턴스가 구독합니다&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
           &lt;figcaption&gt;단일 공유 응답 토픽으로 모든 요청 서버 인스턴스가 구독합니다&lt;/figcaption&gt;
       &lt;/div&gt;
&lt;/center&gt;
그러나, 요청하지 않은 요청자도 불필요한 응답 메시지를 수신받기 때문에, 이를 무시 처리하기 위해 처리가 필요한데요.
이런 무시 처리를 하기 위해 사용하는 것이 `공유 응답 토픽` 설정입니다.
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt; 빈을 설정할 때, &lt;code class=&quot;language-text&quot;&gt;sharedReplyTopic&lt;/code&gt; 설정을 활성화 하면 공유 응답 토픽을 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* KafkaConfig */&lt;/span&gt;

&lt;span class=&quot;token annotation punctuation&quot;&gt;@Bean&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReplyingKafkaTemplate&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;replyingKafkaTemplate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   &lt;span class=&quot;token class-name&quot;&gt;ProducerFactory&lt;/span&gt; producerFactory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
   &lt;span class=&quot;token class-name&quot;&gt;ConcurrentMessageListenerContainer&lt;/span&gt; replyContainer
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token class-name&quot;&gt;ReplyingKafkaTemplate&lt;/span&gt; template &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ReplyingKafkaTemplate&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;producerFactory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; replyContainer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   template&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setSharedReplyTopic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; template&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;sharedReplyTopic&lt;/code&gt; 활성화 시, 해당 메시지는 Debug 로그만 남기고 무시됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* ReplyingKafkaTemplate */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;logLateArrival&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ConsumerRecord&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CorrelationKey&lt;/span&gt; correlationId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sharedReplyTopic&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	   &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;debug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;missingCorrelationLogMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; correlationId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	   &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;missingCorrelationLogMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; correlationId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
   &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;br&gt;
&lt;h3 id=&quot;️-두-번째-설계의-한계-메시지-유실로-인한-재처리-가능성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%84%A4%EA%B3%84%EC%9D%98-%ED%95%9C%EA%B3%84-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%9C%A0%EC%8B%A4%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EC%9E%AC%EC%B2%98%EB%A6%AC-%EA%B0%80%EB%8A%A5%EC%84%B1&quot; aria-label=&quot;️ 두 번째 설계의 한계 메시지 유실로 인한 재처리 가능성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;⚠️ 두 번째 설계의 한계: 메시지 유실로 인한 재처리 가능성&lt;/h3&gt;
&lt;p&gt;ReplyingKafkaTemplate은 요청 메시지 발행 시, 요청 Future를 내부 Map에 저장하고, 응답 메시지가 도착하면 Map으로부터 해당 Future를 가져와 처리하는 구조입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* ReplyingKafkaTemplate.java */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ConcurrentMap&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CorrelationKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; futures &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ConcurrentHashMap&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sendAndReceive&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ProducerRecord&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token annotation punctuation&quot;&gt;@Nullable&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Duration&lt;/span&gt; replyTimeout&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

	&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

    &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;K&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;V&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;R&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; future &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RequestReplyFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 요청 future를 내부 Map에 저장&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;futures&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;correlationId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; future&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; future&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이는 요청 정보를 메모리에 올려놓고 사용하는 것이기 때문에 유실 가능성이 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;응답 메시지가 &lt;code class=&quot;language-text&quot;&gt;replyTimeout&lt;/code&gt; 이내 수신되지 못한 경우
&lt;ul&gt;
&lt;li&gt;내부 스케쥴러에 의해 Map에서 요청 future는 삭제됩니다.&lt;/li&gt;
&lt;li&gt;발주 서비스 특성 상, 발주량이 큰 요청을 처리 시, 타임아웃을 넘길 가능성이 있기 때문에 요청을 유실할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;배포 또는 스케일인과 같은 이벤트 시, Graceful Shutdown에도 응답 메시지를 수신하지 못한 경우
&lt;ul&gt;
&lt;li&gt;요청 future는 결국 삭제되고, 동일한 리스크를 갖습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 상황이 발생되면, 클라이언트는 요청에 대한 응답을 수신받지 못했기 때문에 다시 같은 발주 요청을 수행할 가능성이 있습니다.&lt;br&gt;
이는 곧 중복 발주가 될 수 있어, 2차 아키텍처도 도입하지 않는 것으로 결정했습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;최종-설계-api--kafka를-결합한-하이브리드-요청-응답-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%A2%85-%EC%84%A4%EA%B3%84-api--kafka%EB%A5%BC-%EA%B2%B0%ED%95%A9%ED%95%9C-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C-%EC%9A%94%EC%B2%AD-%EC%9D%91%EB%8B%B5-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;최종 설계 api  kafka를 결합한 하이브리드 요청 응답 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;최종 설계: API + Kafka를 결합한 하이브리드 요청-응답 아키텍처&lt;/h2&gt;
&lt;p&gt;최종적으로 설계한 아키텍처는 요청은 API로 응답은 Kafka Consumer로 분리하여 아래와 같이 구현했습니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4a260740b2edb5291201332fe186c1e3/3004a/third-archi.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA9ElEQVR42mWRjZKFIAiFe/+33Nm9zR1Lw0RTy86Cdfe3mS9wBA7IMJNDSAwKBB88zvPslFpQlaK2/mLf9wvxD7HtOPD6hvfnCEsLluVCE0rJIG8RecUp51OTxKJeNqfYqXnDkQvKtn0X1J925MljXdeuzCHAzgbsCZkjvHN4Ph6wZsLGAbM1cG5GlPgkkNwXmcRMMwbrF7CoEXtw5H6hBTUweo+aEpp0XKJ0xdzPRURyYKQ7joXWGpJ0OryNH5hEzYmKW1zvcJMkshbPccRsjIyVsQs6ekfGbjfq79LEv5H1/YioF1Q1tX0JEvzl/13Ozc+lfAIjR9I8o3ghuwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4a260740b2edb5291201332fe186c1e3/263a4/third-archi.webp 480w,
/static/4a260740b2edb5291201332fe186c1e3/a6361/third-archi.webp 960w,
/static/4a260740b2edb5291201332fe186c1e3/0b34d/third-archi.webp 1920w,
/static/4a260740b2edb5291201332fe186c1e3/ea3cd/third-archi.webp 2222w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4a260740b2edb5291201332fe186c1e3/9aebd/third-archi.png 480w,
/static/4a260740b2edb5291201332fe186c1e3/a91f8/third-archi.png 960w,
/static/4a260740b2edb5291201332fe186c1e3/ac7a9/third-archi.png 1920w,
/static/4a260740b2edb5291201332fe186c1e3/3004a/third-archi.png 2222w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4a260740b2edb5291201332fe186c1e3/ac7a9/third-archi.png&quot; alt=&quot;API + Kafka 기반 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;최종 설계: API + Kafka 기반 아키텍처&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;대체적으로 &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;의 구조를 답습하였고, 일부 커스터마이징을 진행했습니다. 요청-응답 간 연결 역할을 하던 &lt;code class=&quot;language-text&quot;&gt;CORRELATION_ID&lt;/code&gt;를 참고하여, 응답 메시지 내 요청 메시지의 식별자를 두어 연결했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;requestKey&lt;/code&gt; : 요청 키&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;source&lt;/code&gt;: 출처 (e.g. 브랜드원, 올리브원)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 식별자는 브랜드원 DB에도 저장되어 있기 때문에 영속적입니다. 2차 설계처럼 휘발되지 않고 응답 메시지별로 요청에 대한 처리 결과를 연결하여 반영할 수 있습니다.&lt;/p&gt;
&lt;p&gt;응답 메시지 컨슈머 컨테이너에 대한 설정은 &lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;의 &lt;code class=&quot;language-text&quot;&gt;replyContainer&lt;/code&gt;처럼 제약이 있지 않습니다.
동일한 Consumer Group을 사용할 수 있고, 오프셋을 &lt;code class=&quot;language-text&quot;&gt;earliest&lt;/code&gt;로 설정해 처리 유실을 방지할 수 있으며, &lt;code class=&quot;language-text&quot;&gt;Transactional Sementic&lt;/code&gt; 또한 자유롭게 사용할 수 있습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;예외-처리-전략-dltlistener--reply-message&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EC%A0%84%EB%9E%B5-dltlistener--reply-message&quot; aria-label=&quot;예외 처리 전략 dltlistener  reply message permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;예외 처리 전략: DltListener + Reply Message&lt;/h2&gt;
&lt;p&gt;메시지 처리 과정에서 예외가 발생할 경우, Kafka에서는 보통 죽은 메시지로 판단하여 DLT(Dead Letter Topic)로 전송하고, 그 다음 오프셋의 메시지를 처리합니다.
그런데, 요청-응답 패턴을 사용하는 이 구조에서 발주 처리 시 예상치 못한 예외가 발생하면 어떻게 될까요? &lt;br&gt;
요청자는 API로 요청해서 성공 응답을 이미 받은 상태이기 때문에, 응답이 올 것을 기대하지만, 실제로 응답은 오지 않아 무한 대기하게 되는 상황이 발생할 수 있습니다.
이를 방지하기 위해서는 어떻게 할 수 있을까요?&lt;/p&gt;
&lt;h3 id=&quot;timeout-설정하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#timeout-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;timeout 설정하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Timeout 설정하기&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;ReplyingKafkaTemplate&lt;/code&gt;은 &lt;code class=&quot;language-text&quot;&gt;replyTimeout&lt;/code&gt;을 두어 요청 별로 무한 대기를 방지할 수 있는 기능이 있습니다.
현재 설계에서도 요청 타임아웃을 설정할 수 있지만, 발주 처리 프로세스가 요청 발주량에 선형적으로 처리 시간이 늘어나다보니,
발주량이 많은 요청의 경우, 처리가 잘 수행되더라도 설정한 타임아웃으로 인해 실패된 것으로 오인할 수 있기에 좋은 방법은 아니라고 생각했습니다.
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;dlt-구독하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#dlt-%EA%B5%AC%EB%8F%85%ED%95%98%EA%B8%B0&quot; aria-label=&quot;dlt 구독하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;DLT 구독하기&lt;/h3&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2b118efce7cf410c7d9cc58e48d74b86/d9ac3/third-archi-with-dlt-subscription.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA/klEQVR42k2Ri66EIBBD/f//vPsQeYsCAr0d4m6WpCEwzJlWFxc9fAyQ3QWPMQZ6awjRIqWAXirGdX3VSkE+09RVMuuFe8HoHbKWp3rjrRWcc9DG4GJTyQXr+oI3GjUlRGth1hV6Vcg8O6enjhCQY0Rkb6MJax0WoYqrwKJIpqUJcUjeT0f1PHHMRo+y74jGYmddBokCVWuF4/vFMq7fI+URU5zAU1zQrVYKRm1oBLbjQKcg8e74tDUlQ/sn8oORX9tKu5aRNePm6chvG8zfA+rxxCBo8G4IrN4wgdKVqFFf4CeyfENHaJdHPIvTQlAm6OLdryRelZ9x700G3Osfwi3Rj4hnFA0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2b118efce7cf410c7d9cc58e48d74b86/263a4/third-archi-with-dlt-subscription.webp 480w,
/static/2b118efce7cf410c7d9cc58e48d74b86/a6361/third-archi-with-dlt-subscription.webp 960w,
/static/2b118efce7cf410c7d9cc58e48d74b86/0b34d/third-archi-with-dlt-subscription.webp 1920w,
/static/2b118efce7cf410c7d9cc58e48d74b86/9c950/third-archi-with-dlt-subscription.webp 2036w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2b118efce7cf410c7d9cc58e48d74b86/9aebd/third-archi-with-dlt-subscription.png 480w,
/static/2b118efce7cf410c7d9cc58e48d74b86/a91f8/third-archi-with-dlt-subscription.png 960w,
/static/2b118efce7cf410c7d9cc58e48d74b86/ac7a9/third-archi-with-dlt-subscription.png 1920w,
/static/2b118efce7cf410c7d9cc58e48d74b86/d9ac3/third-archi-with-dlt-subscription.png 2036w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2b118efce7cf410c7d9cc58e48d74b86/ac7a9/third-archi-with-dlt-subscription.png&quot; alt=&quot;DLT 구독해서 실패 메시지 수신하기&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;DLT 구독해서 실패 메시지 수신하기&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;정말 간단한 접근으로는 DLT도 요청자 서버에서 구독하면 됩니다. 그럼 예외가 발생하더라도 DLT로 인입된 경우, 요청이 실패됨을 판단할 수 있습니다.&lt;br&gt;
다만, 이 방법은 요청자가 모든 API에 대해서 응답 토픽과 DLT 따로 구독해야만 하기 때문에, 관리 포인트가 증가하는 문제가 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;h3 id=&quot;실패-응답-메시지-발행하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%ED%8C%A8-%EC%9D%91%EB%8B%B5-%EB%A9%94%EC%8B%9C%EC%A7%80-%EB%B0%9C%ED%96%89%ED%95%98%EA%B8%B0&quot; aria-label=&quot;실패 응답 메시지 발행하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실패 응답 메시지 발행하기&lt;/h3&gt;
&lt;p&gt;그래서 설계한 전략은 DLT Listener에서 실패 응답 메시지를 만들어 응답 토픽으로 발행하는 것입니다.&lt;/p&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/285b5922909f775a6f783740dacbb8e9/f6d08/third-archi-with-fail-strategy.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABMUlEQVR42j1RSXLDMAzz///XSw5N21OT2NqtXUJAO4lmOLAoEgToRTmDlDOMN4gpYs6J1hq03xBjwKj1iMncaBW9FLScXphPZLzPcvn9hrYGSis45w6yEALW9YboHWqMSN5D3e+IzqNyqNIPWKNQ9h2FtZHv0ueIi7CKqm3b4JmoVBOIXuujsKZEIgf1IIlSx7dhrV5XBGNgiZ6YqXJTGovfw3HRtJxKRk4ZOydLszSZdcOsp1WxPWkzk8BzgPq/Aa2jMPex/HW9YDMamooMCwsbE21mkorVREWTd8ieZI9UXK1FlXe+jVJRmJMzhPBjeT0tjzEO27N3VowzSARRIc2yDu7T0balRdltInlg/PxdsQiZhJB0IeHpJOgkaK94/0lBUW7p5r0S2W2mQulN3uEJGWseXGesocQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/285b5922909f775a6f783740dacbb8e9/263a4/third-archi-with-fail-strategy.webp 480w,
/static/285b5922909f775a6f783740dacbb8e9/a6361/third-archi-with-fail-strategy.webp 960w,
/static/285b5922909f775a6f783740dacbb8e9/0b34d/third-archi-with-fail-strategy.webp 1920w,
/static/285b5922909f775a6f783740dacbb8e9/5ada6/third-archi-with-fail-strategy.webp 2030w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/285b5922909f775a6f783740dacbb8e9/9aebd/third-archi-with-fail-strategy.png 480w,
/static/285b5922909f775a6f783740dacbb8e9/a91f8/third-archi-with-fail-strategy.png 960w,
/static/285b5922909f775a6f783740dacbb8e9/ac7a9/third-archi-with-fail-strategy.png 1920w,
/static/285b5922909f775a6f783740dacbb8e9/f6d08/third-archi-with-fail-strategy.png 2030w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/285b5922909f775a6f783740dacbb8e9/ac7a9/third-archi-with-fail-strategy.png&quot; alt=&quot;실패 응답 메시지를 발행해서 Reply Topic으로 수신하기&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;실패 응답 메시지를 발행해서 Reply Topic으로 수신하기&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
Kafka는 Dead Letter 발생 시, 예외에 대한 정보와 Origin 정보, 그리고 기존 요청 메시지 헤더 정보까지 복사하여 Dead Letter 메시지 헤더에 담아 같이 전송해줍니다. 이를 활용해서 응답 메시지를 구성하고, 응답 토픽으로 발행하는 것이 가능합니다.&lt;br&gt;&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;KafkaHeaders.REPLY_TOPIC (from. 요청 메시지 헤더)&lt;/li&gt;
&lt;li&gt;KafkaHeaders.DLT_ORIGINAL_TOPIC: 예외 발생 토픽&lt;/li&gt;
&lt;li&gt;KafkaHeaders.DLT_EXCEPTION_CAUSE_FQCN: 예외 사유 FQCN(Full Qualified Class Name)&lt;/li&gt;
&lt;li&gt;KafkaHeaders.DLT_EXCEPTION_MESSAGE: 예외 사유 메시지&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
바로 아래 Slack 메시지는 위 정보를 활용하여 만든 것입니다. 아래 메시지를 통해서, 실시간으로 어떤 이슈로 Dead Letter가 발생했는지 알 수 있고, 메시지 하단 버튼을 통해 Kafka Admin 페이지 또는 서비스의 데이터독 로그 페이지로 이동하여 빠르게 대응할 수 있습니다. &lt;br&gt;&lt;br&gt;
&lt;center&gt;
	&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1236px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a5def020ffe1a20f7392e1325e269ed8/baa93/slack-error-message.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 96.25000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACW0lEQVR42p1U2XLbMAzUXyRxLNmyZd33fTt2OpPp/38QigXFOH2r+4CRSIGLxWIp4/P2ReNyp2FcaZyv1PYzLddPipKC3t4P9G7aT4Wx/Fooi13ywpS8IBGgMM7p5PiSsLdO/xwC+PH7Tk3hkuvHEklWMmBG9tmj3d5+nmFVDxQkFVlHh17ezC0sBjs+DSaATTdSWbVkHs7fsT8o+iY/Aay1xLsuhBb1+ue+UTUDt1mRzxrmZUtYZ0VNcVpQmte818h+nJYSQZSJxtjLioaOJ4/DZc096dKoW2ZY91RUHaUMXNadgGKNAc3rjXp2QN2qwkWliuJMNy7sikkIwB2YAbc8yUcE3jGUlBPW613AuoFdwCwrAAyz5IDEytaalxsN0yoB4PMlUIBoTQOmWS3rNK+4vZSlSKTyxYvZVjEf8iUgESTAExJcvEh0NGDiXqjPlDAQPqKtKMkFyGdvhqwbDqJlgMCvAJBCfiTfQAKFDOgDvXIOHEAiqD+eITluqMA387t+JIexjw6QB9/CIUbLbYpGTSfMoBkmDKEb1gW6YZodF+64C00AubplTPp1p6xlQNCSE3CPp+VDDoApQDBZAIt9GAAWwhogAIUjkBfzIOGIE7M01GQroR6xfvou4xB8pU0Lc+v7qk2uDf7XTcEwHNYAmuCHYJ9d0cNxA9EIWuJd6fbYU5N/6AttYXAxNlpDW0jUAYbqhxH9AIi2CQMk2QYUfwMe7AsYTmIZJL/uTHWXrccdfibEh9DP4YqqciwaJpvXdv/zt8HocTfl/iYZucy0Z+tM40im9TzgHz82MWzk+57UAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a5def020ffe1a20f7392e1325e269ed8/263a4/slack-error-message.webp 480w,
/static/a5def020ffe1a20f7392e1325e269ed8/a6361/slack-error-message.webp 960w,
/static/a5def020ffe1a20f7392e1325e269ed8/0c34c/slack-error-message.webp 1236w&quot; sizes=&quot;(max-width: 1236px) 100vw, 1236px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a5def020ffe1a20f7392e1325e269ed8/9aebd/slack-error-message.png 480w,
/static/a5def020ffe1a20f7392e1325e269ed8/a91f8/slack-error-message.png 960w,
/static/a5def020ffe1a20f7392e1325e269ed8/baa93/slack-error-message.png 1236w&quot; sizes=&quot;(max-width: 1236px) 100vw, 1236px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a5def020ffe1a20f7392e1325e269ed8/baa93/slack-error-message.png&quot; alt=&quot;실패 메시지 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
		&lt;figcaption&gt;실패 메시지 예시&lt;/figcaption&gt;
	&lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-신규-발주-서비스-도입-후&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%8B%A0%EA%B7%9C-%EB%B0%9C%EC%A3%BC-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%8F%84%EC%9E%85-%ED%9B%84&quot; aria-label=&quot; 신규 발주 서비스 도입 후 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍷 신규 발주 서비스 도입 후&lt;/h2&gt;
&lt;p&gt;이번 신규 서비스 개발을 통해, 아래와 같은 개선 성과를 낼 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;신규 발주 서비스의 평균 처리 속도는 0.1초 수준이며, 백오피스 별 요청 응답 속도가 98.7% (218초 → &lt;strong&gt;2.8초&lt;/strong&gt;, 5,000건 기준) 향상되었습니다.
&lt;center&gt;
    &lt;div style=&quot;width: 90%; display:block;&quot;&gt;
 		&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 565px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a8929308ad306c48037803151ce8e743/a1277/datadog.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABXUlEQVR42qWTWW6EMBBEfY4oCZh9hxn2dTYl979Rp6sRUr5mHOWjhMGt52pXo+bpQst8pSI/k7Z9cp3wX1JxlFGWluS5IQM9/uiTo31+D1jRS4CjAzFymFFZWggwYbDLQNtyRNp2pci2vKfyvZiiMKMwSMSIGoeZ6nNLeAZ+xEUub3gi61M/FQ7Os5LapqdTVUt3qu9m6ruJtvUmd7mtd7psD9Zd2nnVMlzCYduM4lYNHRz2hHC6dmRNfwoHh6ZJQU097MBxWOQF0Gnc6Hb94oLSGApgHOVUVc0OBASqT52Mzqlq+YJTo3YPYBLvDgM/IVXz4sywqmxkdFCEDZOROYAwUBb10fJKPd/byoGsC3QTxWFOjkHbh0MEKw6rsma7PS0cCpL6eNf0/maRtszvEM5+OVw4kI4wPhgbpP24f/N8VUbBAIixGfp5d4j+sYCwxiZawGkoNhHA+58S0A/6Nn6hZul7hwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a8929308ad306c48037803151ce8e743/263a4/datadog.webp 480w,
/static/a8929308ad306c48037803151ce8e743/01742/datadog.webp 565w&quot; sizes=&quot;(max-width: 565px) 100vw, 565px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a8929308ad306c48037803151ce8e743/9aebd/datadog.png 480w,
/static/a8929308ad306c48037803151ce8e743/a1277/datadog.png 565w&quot; sizes=&quot;(max-width: 565px) 100vw, 565px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a8929308ad306c48037803151ce8e743/a1277/datadog.png&quot; alt=&quot;신규 발주 서비스 평균 처리 시간&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
 		&lt;figcaption&gt;신규 발주 서비스 평균 처리 시간&lt;/figcaption&gt;
    &lt;/div&gt;
 &lt;/center&gt;
&lt;/li&gt;
&lt;li&gt;브랜드원을 비롯하여, 향후 생겨날 백오피스 서비스에 발주 기능을 유연하게 제공할 수 있게 되었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-정리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A0%95%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot; 정리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✍🏻 정리하며&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;이번 발주 서비스 개선 프로젝트를 통해, 기존 동기식 처리의 한계를 극복하고 Kafka 기반의 비동기 요청-응답 아키텍처로 성공적인 전환을 이뤄냈습니다. 이 과정에서 저희는 단순히 기능을 구현하는 것을 넘어, 중복 발주와 같은 치명적인 비즈니스 리스크를 차단하고, 향후 다양한 백오피스 환경에서도 안정적으로 발주를 처리할 수 있는 높은 서비스 확장성을 확보하고자 끊임없이 고민했습니다.&lt;/p&gt;
&lt;p&gt;물론, 모든 문제를 해결할 수 있는 🚄은총알은 없다는 것을 다시 한번 느꼈습니다. 이번에 적용한 설계가 언제나 정답일거라 생각하지 않으며, 앞으로도 더 좋은 기술이나 방법이 있다면 적극적으로 적용하며 올리브영의 핵심 비즈니스를 뒷받침하는 안정적이고 효율적인 서비스를 만들도록 노력할 계획입니다.&lt;/p&gt;
&lt;p&gt;이 글이 대규모 백오피스 시스템의 성능 및 안정성, 그리고 확장성 문제로 고민하는 다른 분들께도 작은 도움이 되었으면 좋겠습니다.
긴 글 읽어주셔서 감사합니다!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[전국 3,500대 POS 실시간 모니터링 구축기]]></title><description><![CDATA[안녕하세요. 올리브영에서 POS 시스템을 개발하고 있는 호두마루🐯입니다. 저는 지난 9년간 리테일 시스템, 그중에서도 매장에서 고객과 가장 가까이 맞닿아 있는 POS(Point of Sale…]]></description><link>https://oliveyoung.tech/2025-06-27/adding-datadog-to-pos-system/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-06-27/adding-datadog-to-pos-system/</guid><pubDate>Fri, 27 Jun 2025 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영에서 POS 시스템을 개발하고 있는 &lt;strong&gt;호두마루🐯&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;저는 지난 9년간 리테일 시스템, 그중에서도 매장에서 고객과 가장 가까이 맞닿아 있는 &lt;strong&gt;POS(Point of Sale) 시스템의 운영과 개발&lt;/strong&gt;을 담당해왔습니다. 그리고 지금도 “더 안정적이고 유연한 시스템”을 만들기 위해 매일 고민하고 있습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;strong&gt;POS 시스템에 Datadog을 도입하면서 로그 수집과 모니터링 체계를 개선해 온 과정&lt;/strong&gt;을 공유해 보려고 합니다. 단순한 도구 도입을 넘어, 실제로 &lt;strong&gt;현업 문제를 기술적으로 해결해 나간 여정&lt;/strong&gt;이자, &lt;strong&gt;운영 패러다임을 바꿔 간 이야기&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/cff70c493c74449c31528e3bba5c8ef2/98e2c/pos1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACVklEQVR42mPQ0DHS0DEGk0aausZABOdCRFS1DG1t7Fwc7dXVVH38AyRk5MTEJS5eOHvr5mUGoAo1TV1VLT01LT0VTR1VTR11LX24ZiBS1zbUNzLNysny9vV38Qn1jEjzDk1euXbxyp0LGDR1Taxsg0wtvc0tfZ2c4pycYs0sPIFmqWuDtGnoGqtp6pnbe3nElvtltYZXT09oXRSa2+TiE7x7/xaQzea2/mY2viZW3uZ2QaY2Afomjpq6hlp6JppgZyuoakdExqY0zg5vWxtSNskxOMUlOEHX2L6ioJxBXdvAwtrO0cXD3snN2NxKUVXD0NTSxd3X0dXb0dVL39hCXkU7Ni66vqnBt6DfLafdJiglKaNmWnv39oOLgZoNrexc3LwCnNx8LGyc1bQMjMxsXD39gfptHdzNrBzklLUyMtMP7V2XUlCWUzVlw+rNZ2/sPXB75cFr60CaDU2szCztjc1t9YwstPVNNHWNdA3NdAxMgY4CktIK6jW1lddvnJkzZ/L6bfMOPl08aVXVhk2Lj+zfywC0ysrW2dMnyMMnyN0rwNHFy8DESt/IQt/YEmiWjoGZlJxKY1vPu+//L169unbz4qVr58yePmHNmoWbNi0HBZi2vilQKdB7emA9QIcYmloDEZCta2guJavc2jPl3bd/J86dX7V89uL5k5cvm7F+7YKdO1YxIEcpJFaRkAHQF9LyaoUlZbt2bVqxbNaGdQuB2latmL1l89KTJ3aha0ZOWzYOri5uPpa2LsXF+UcPbdq3Z/3enWu3b1m+b/e6s2f23bh2AgBxoeC1YKEUbwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cff70c493c74449c31528e3bba5c8ef2/263a4/pos1.webp 480w,
/static/cff70c493c74449c31528e3bba5c8ef2/a6361/pos1.webp 960w,
/static/cff70c493c74449c31528e3bba5c8ef2/0b34d/pos1.webp 1920w,
/static/cff70c493c74449c31528e3bba5c8ef2/548e7/pos1.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cff70c493c74449c31528e3bba5c8ef2/9aebd/pos1.png 480w,
/static/cff70c493c74449c31528e3bba5c8ef2/a91f8/pos1.png 960w,
/static/cff70c493c74449c31528e3bba5c8ef2/ac7a9/pos1.png 1920w,
/static/cff70c493c74449c31528e3bba5c8ef2/98e2c/pos1.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cff70c493c74449c31528e3bba5c8ef2/ac7a9/pos1.png&quot; alt=&quot;POS 시스템 개요&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h1 id=&quot;pos-시스템은-무엇이고-올리브영의-환경은-어떠한가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pos-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%ED%99%98%EA%B2%BD%EC%9D%80-%EC%96%B4%EB%96%A0%ED%95%9C%EA%B0%80&quot; aria-label=&quot;pos 시스템은 무엇이고 올리브영의 환경은 어떠한가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;POS 시스템은 무엇이고, 올리브영의 환경은 어떠한가?&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;POS는 고객이 결제를 진행하고, 포인트를 적립하거나 쿠폰을 사용하는 등 &lt;strong&gt;판매 시점 전반의 흐름을 책임지는 핵심 시스템&lt;/strong&gt;입니다.&lt;br&gt;
저희는 POS 단말기뿐 아니라 셀프계산대(SCO)도 함께 관리하고 있으며, 이러한 장비들은 &lt;strong&gt;전국 약 1,370개 매장에 총 3,500여 대&lt;/strong&gt;가 설치되어 있습니다.&lt;/p&gt;
&lt;p&gt;문제는, 이 장비들이 대부분 &lt;strong&gt;오프라인 매장에 설치되어 있어 네트워크가 항상 안정적으로 연결되기 어렵다&lt;/strong&gt;는 점입니다.&lt;br&gt;
즉, 네트워크가 &lt;strong&gt;간헐적으로 끊기거나 통신이 불안정한 상황이 빈번히 발생&lt;/strong&gt;합니다. 이런 특성 때문에 기존의 서버 기반 모니터링 솔루션을 그대로 적용하기 어려웠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;올리브영처럼 일평균 수백~수천 명의 유동 고객이 방문하고, 결제가 실시간으로 이루어져야 하는 리테일 환경에서는 운영 안정성이 매우 중요합니다.&lt;/strong&gt; &lt;br&gt;
작은 장애 하나가 곧 매장 운영에 직접적인 영향을 미치기 때문입니다.&lt;/p&gt;
&lt;p&gt;이 글은 바로, &lt;strong&gt;그런 문제의식에서 출발한 여정&lt;/strong&gt;을 담고 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/70ec9a9e1e4824956f947f8144968995/98e2c/pos2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACSElEQVR42kWQy2sTURTGx/pYKApFEcRFWpKYzCvzvvO+k0kmxunLWJIm1jSYRNCKJGhaxCStLaghkIoxVqEoUmhWVQuuK24EN4K4y8I/xpuHejmLc7nn933fuViv12s+PrzmbBeSXVspHzs+NnbkqGeCxGnej1N+nB6Wb9RTviCJmgDJBSkeq9Ye/vzx+1a6W0x9AMwNDMNwbwRqeZrTbHvJtDK6kTTNtOPkrXA6Fiu47nLYXkSvAZLFGEJ72X5VKTciRk4Dlwmcg3LO46E4YMlmwoguQCcNoynNXuCVKRkmVfu6oE4TDAhSHEZIKe+kKDAwEAQBwpL5q2fHLxK4zogyTrHQduLurDuTUHTIijIvKZwo05w0it3a+TrpJSulZ9VCueQY66sNU51LJ+/7CYpiACtqogJ5YGimI8gQp0WSkciQhMg+XKs07+RWVh+879ztfKlvdV/sl+5tzM3eDFAszcmqEbFj01bUjU8lTDuOVATZHJJ9eL7cdvObU7CuJd8tLT7drT5qPu/4vCiahpxVM4pgoFpotO85qP9wtvQ6Hq5l4+Wtemutvv1mo80xnGfCF+IVnBaQlWJEOElH+RlBRasOtx3BZ06Nr9x+m137WNzc2Wt9zhefJBN1U02RLI9i69BBzjByRbdiFAuQ3D+yDwMRHux+UwsNsL4vqxlRnNfEDE3ZBMOj7wFaGK2Kwsu6jVIguYH5X/ig+/3w0y9ruanUuidOnsYG59z5CyQD0NwlgvUFQ36cQc3wykt62HGd+IxhOX8A/PawOJ1DCUgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/70ec9a9e1e4824956f947f8144968995/263a4/pos2.webp 480w,
/static/70ec9a9e1e4824956f947f8144968995/a6361/pos2.webp 960w,
/static/70ec9a9e1e4824956f947f8144968995/0b34d/pos2.webp 1920w,
/static/70ec9a9e1e4824956f947f8144968995/548e7/pos2.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/70ec9a9e1e4824956f947f8144968995/9aebd/pos2.png 480w,
/static/70ec9a9e1e4824956f947f8144968995/a91f8/pos2.png 960w,
/static/70ec9a9e1e4824956f947f8144968995/ac7a9/pos2.png 1920w,
/static/70ec9a9e1e4824956f947f8144968995/98e2c/pos2.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/70ec9a9e1e4824956f947f8144968995/ac7a9/pos2.png&quot; alt=&quot;POS 시스템 현황&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h1 id=&quot;매장-현장에서-들려오던-sos-신호들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%ED%98%84%EC%9E%A5%EC%97%90%EC%84%9C-%EB%93%A4%EB%A0%A4%EC%98%A4%EB%8D%98-sos-%EC%8B%A0%ED%98%B8%EB%93%A4&quot; aria-label=&quot;매장 현장에서 들려오던 sos 신호들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 현장에서 들려오던 SOS 신호들&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;실제 매장에서 POS나 셀프계산대(SCO)에 오류가 발생하면, 고객의 결제가 지연되고 직원은 곧바로 대응에 나서야 합니다. &lt;br&gt;
하지만 문제를 바로 인지하고 대응하는 일은 전혀 쉽지 않았습니다.&lt;/p&gt;
&lt;p&gt;기존에는 장애가 발생해도 &lt;strong&gt;&apos;POS 전담 기술 지원팀&apos;을 통해 제보가 들어온 뒤에야&lt;/strong&gt; 문제가 인지되었습니다. &lt;br&gt;
이후 담당자가 원격 접속해 로그를 수집하고 분석하는 방식으로 대응했는데, &lt;strong&gt;장비 한 대를 확인하는 데만 평균 5분 이상&lt;/strong&gt;이 소요됐습니다. &lt;br&gt;
복수 매장에서 동시에 장애가 발생하면, &lt;strong&gt;즉각적인 대응은 사실상 불가능&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;p&gt;무엇보다 장비가 꺼져 있거나 로그가 삭제되면 문제 확인조차 할 수 없는 상황도 종종 발생했습니다.&lt;br&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“왜 항상 매장에서 연락이 오고 나서야 문제를 인식할 수 있을까?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;“대응하기 위해 때론 너무 많은 자원을 비효율적으로 쓰고 있는 건 아닐까?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이러한 반복적인 한계 속에서, 우리는 지금의 방식으로는 더 이상 대응할 수 없다는 결론에 도달했습니다.&lt;br&gt;
&lt;strong&gt;더욱 근본적인 변화가 필요하다&lt;/strong&gt;는 판단 아래, &lt;strong&gt;로그 수집과 모니터링 방식 자체를 다시 설계하기로 했습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div style=&quot;display:block;&quot;&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/313136fa05c74c0f5aba532a9f8f489f/98e2c/pos3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABX0lEQVR42n1S2WrDMBDMx7SpL0m2Duv0EduNS6B9SQikkLf+/zd0YjmBQlsYCe3s7uys8SYpOGGWlLZguqA6LTiYnKgbyUyyhH9hg9P2l368+vbou7MfPsN4de1pnL+a/pIU1X/NKRHSdFRYUmmuGyYDFZ5wu8CALKUD8KBL+MA6uVIeiZe8QlFtO2VaKEZtkDlTAqLC4g1EPhbcmrPl9ZwyWmmpG5Qi3GYlALIo69DvIcrrgEow4KPKrVnZLmpjpnY9gLroFiTC6e3DtRMgTQsmpjAcO3PEsP0rsBuWMs2AO6MKLh6pdXIpHBJPCeXKh25vw4gw2ou2wZgwMGmYMOni/G6bcCUHKXvCtdXz5M+jOxu1p0JDlAojVbdzx2AORo+1HuIujJt0/drSA0xaNI/u5PTM1bozwFVT14NSO9xS9Qtp0b/ahpM7JJAU4if5+CuqbV4KHXavh2l+D930DUsShIsBaiA1AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/313136fa05c74c0f5aba532a9f8f489f/263a4/pos3.webp 480w,
/static/313136fa05c74c0f5aba532a9f8f489f/a6361/pos3.webp 960w,
/static/313136fa05c74c0f5aba532a9f8f489f/0b34d/pos3.webp 1920w,
/static/313136fa05c74c0f5aba532a9f8f489f/548e7/pos3.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/313136fa05c74c0f5aba532a9f8f489f/9aebd/pos3.png 480w,
/static/313136fa05c74c0f5aba532a9f8f489f/a91f8/pos3.png 960w,
/static/313136fa05c74c0f5aba532a9f8f489f/ac7a9/pos3.png 1920w,
/static/313136fa05c74c0f5aba532a9f8f489f/98e2c/pos3.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/313136fa05c74c0f5aba532a9f8f489f/ac7a9/pos3.png&quot; alt=&quot;문제 해결을 위한 모니터링 솔루션 비교&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;문제 해결을 위한 모니터링 솔루션 비교&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h1 id=&quot;왜-datadog이었을까-솔루션-검토-과정과-선택-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-datadog%EC%9D%B4%EC%97%88%EC%9D%84%EA%B9%8C-%EC%86%94%EB%A3%A8%EC%85%98-%EA%B2%80%ED%86%A0-%EA%B3%BC%EC%A0%95%EA%B3%BC-%EC%84%A0%ED%83%9D-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;왜 datadog이었을까 솔루션 검토 과정과 선택 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 Datadog이었을까? 솔루션 검토 과정과 선택 이유&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;문제를 해결하기 위해, 위 이미지 자료처럼 다양한 로그 수집 솔루션을 검토했습니다.&lt;br&gt;
자체 서버 구축부터 여러 도구를 비교한 끝에,
최종적으로 &lt;strong&gt;Datadog을 선택&lt;/strong&gt;하게 되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이미 일부 사내 시스템에서 효과적으로 사용 중이었으며&lt;/li&gt;
&lt;li&gt;운영 로그 모니터링, 매출 흐름 파악에 유리하다는 것이 입증된 상태였고&lt;/li&gt;
&lt;li&gt;뛰어난 &lt;strong&gt;확장성&lt;/strong&gt;, &lt;strong&gt;통합성&lt;/strong&gt;, &lt;strong&gt;대시보드 구성 능력&lt;/strong&gt; 이 주요 강점으로 작용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 이 선택은, 단순히 적절한 도구를 고른 것을 넘어
&lt;strong&gt;전국 수천 개 매장과 수천 대의 장비가 분산된 오프라인 환경에서 Datadog을 운영한 드문 사례&lt;/strong&gt;라는 점에서도 의미가 있었습니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;pos-특성에-맞춘-구성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pos-%ED%8A%B9%EC%84%B1%EC%97%90-%EB%A7%9E%EC%B6%98-%EA%B5%AC%EC%84%B1&quot; aria-label=&quot;pos 특성에 맞춘 구성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;POS 특성에 맞춘 구성&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;Datadog을 POS 환경에 적용하기 위해서는 해결해야 할 기술적 과제가 많았습니다.&lt;/p&gt;
&lt;p&gt;우선, 전국에 분포한 &lt;strong&gt;3,500여 대의 POS 장비에 Datadog Agent를 설치하고&lt;/strong&gt;, 로그를 Datadog 서버로 수집하는 구조를 만드는 것이 중요했습니다.&lt;br&gt;
하지만 POS의 특수한 운영 환경 때문에 단순한 설치만으로는 해결되지 않았습니다.&lt;/p&gt;
&lt;h2 id=&quot;폐쇄망에서-public-망으로-네트워크-아키텍처-상세&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8F%90%EC%87%84%EB%A7%9D%EC%97%90%EC%84%9C-public-%EB%A7%9D%EC%9C%BC%EB%A1%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%83%81%EC%84%B8&quot; aria-label=&quot;폐쇄망에서 public 망으로 네트워크 아키텍처 상세 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;폐쇄망에서 Public 망으로: 네트워크 아키텍처 상세&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[POS 장비] → [매장 내부망] → [프록시 서버] → [Datadog 서버]
     ↓            ↓              ↓              ↓
   3,500대    폐쇄망 환경     보안 게이트웨이    Public 클라우드&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 구조에서 가장 중요했던 것은 &lt;strong&gt;보안을 유지하면서도 실시간 데이터 전송&lt;/strong&gt;을 보장하는 것이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;폐쇄망 구조&lt;/strong&gt;
&lt;br&gt;POS는 외부 인터넷과 단절된 &lt;strong&gt;전용 폐쇄망&lt;/strong&gt;에서 운영되고 있으며, 반면 Datadog 서버는 &lt;strong&gt;Public 망&lt;/strong&gt;에 자리 잡고 있습니다.
&lt;br&gt;이를 해결하기 위해 &lt;strong&gt;중간에 프록시 서버를 구성&lt;/strong&gt;, POS → 프록시 서버 → Datadog 서버 간 통신이 가능하도록 네트워크 구조를 설계했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;비표준 로그 포맷 표준화&lt;/strong&gt;
&lt;br&gt;기존 로그는 단순 텍스트 형태였기 때문에, Datadog에서 효과적으로 집계·분석이 어려웠습니다.
&lt;br&gt;이에 따라 &lt;strong&gt;로그 포맷을 JSON 기반으로 정비&lt;/strong&gt;하고, 파싱 및 시각화가 가능하도록 구조를 개선했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;선별 수집 구조 적용&lt;/strong&gt;
&lt;br&gt;모든 로그를 수집하면 오히려 노이즈가 많아지고 Agent 리소스 부담이 커질 수 있습니다.
&lt;br&gt;그래서 &lt;strong&gt;사용자 이벤트와 오류 로그 중심으로 수집 대상을 선별&lt;/strong&gt;하고,
&lt;strong&gt;당일 로그만 수집 경로에 남기도록 조정&lt;/strong&gt;해 불필요한 데이터 처리 비용을 줄였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 일련의 최적화 과정을 거쳐,
&lt;strong&gt;Agent의 CPU 사용률을 평균 0.5% 이하, 메모리 사용량을 120MB 이하로 유지하며 안정적인 로그 수집이 가능한 구조&lt;/strong&gt;를 완성할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/921b3364a610aad0cdeb3462262219e7/98e2c/pos4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABMElEQVR42pVRaU+DQBTsbzEC5V6OZdlyLMcWqNBDjIkmfvD//wqHbq3GGGOT4YXdN/PeDKzWTviJ6Izw/1jh0UxXtzzNdPByqzhifGTpmOdzyqe1E9wgNl1atG8sPwn5zstnwyaGHShcSdebHy2IIxIlou0q2Q3jIRNb3Ng+BdB2A7bdHeVwkP0+YgUuXZI4JAHnktnyaNkMtRwpr+K0DGimoBjQADErwiQn8UYdMRT7l81ewNBgmyrhwvJizfQVoMQSTAHbjzgq5QL8O91RzpfNaDTd1PZ7OEyzGkealhinbMPzMD4O03x6ej3OL6LdgYMdFzHGKIfnCLEKrDIDLKvTvEEt6x7pAF60cLTY/vUffP+kukUQAfV+7QEq0ZftvwFTyheq4YSIU8kH2U+5kB+xe4EkkcLa0AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/921b3364a610aad0cdeb3462262219e7/263a4/pos4.webp 480w,
/static/921b3364a610aad0cdeb3462262219e7/a6361/pos4.webp 960w,
/static/921b3364a610aad0cdeb3462262219e7/0b34d/pos4.webp 1920w,
/static/921b3364a610aad0cdeb3462262219e7/548e7/pos4.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/921b3364a610aad0cdeb3462262219e7/9aebd/pos4.png 480w,
/static/921b3364a610aad0cdeb3462262219e7/a91f8/pos4.png 960w,
/static/921b3364a610aad0cdeb3462262219e7/ac7a9/pos4.png 1920w,
/static/921b3364a610aad0cdeb3462262219e7/98e2c/pos4.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/921b3364a610aad0cdeb3462262219e7/ac7a9/pos4.png&quot; alt=&quot;전국 수천 대의 POS에 적용하기 위한 자동화 설치 방안&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;전국 수천 대의 POS에 적용하기 위한 자동화 설치 방안&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h2 id=&quot;전국-3500대-pos에-한-번에-점진적-확산-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%84%EA%B5%AD-3500%EB%8C%80-pos%EC%97%90-%ED%95%9C-%EB%B2%88%EC%97%90-%EC%A0%90%EC%A7%84%EC%A0%81-%ED%99%95%EC%82%B0-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;전국 3500대 pos에 한 번에 점진적 확산 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;전국 3,500대 POS에 한 번에? 점진적 확산 전략&lt;/h2&gt;
&lt;p&gt;다음으로 고민한 부분은 &lt;strong&gt;설치 자동화&lt;/strong&gt;였습니다.&lt;/p&gt;
&lt;p&gt;전국 수천 대의 POS 장비에 수작업으로 Agent를 설치하는 것은 현실적으로 불가능했기 때문에, 기존에 사용 중이던 &lt;strong&gt;POS 원격 관리 솔루션의 스크립트 실행 기능&lt;/strong&gt;을 활용하기로 했습니다.&lt;/p&gt;
&lt;p&gt;설치 자동화를 위해 다음과 같은 작업을 진행했습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Datadog Agent 설치 스크립트 작성&lt;/strong&gt;
&lt;br&gt;Agent를 자동으로 설치하고, 서비스 시작/중지를 제어할 수 있는 스크립트를 구현했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;설정 파일(.yaml) 원격 자동 배포 구성&lt;/strong&gt;
&lt;br&gt;로그 수집 대상이나 구성 변경 시 POS 원격 관리 솔루션을 이용해 각 장비의 설정 파일을 자동으로 변경하도록 구성했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;매장별 POS 장비 식별 체계 정립&lt;/strong&gt;
&lt;br&gt;각 매장의 POS 장비에서 수집되는 Datadog 로그를 정확히 식별할 수 있도록,
&lt;strong&gt;Host 값을 매장 내 각 POS를 고유하게 구분할 수 있는 키&lt;/strong&gt;로 설정했습니다.
&lt;br&gt;이를 통해 &lt;strong&gt;어느 매장의 어떤 POS에서 발생한 로그인지 명확하게 구분&lt;/strong&gt;할 수 있게 되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이후 &lt;strong&gt;테스트 장비 → 일부 매장 → 전국 확산&lt;/strong&gt;이라는 점진적 흐름을 따라 적용 범위를 넓혀 나갔고,
결과적으로 약 &lt;strong&gt;3,000대의 장비에 설치를 완료&lt;/strong&gt;할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;설치 과정에서는 네트워크 과부하를 방지하기 위해 &lt;strong&gt;매장이 영업하지 않는 새벽 시간대에 분산 설치&lt;/strong&gt;를 진행했습니다.
&lt;br&gt;일부 구간에서 &lt;strong&gt;네트워크 임계치에 근접하는 순간도 있었지만&lt;/strong&gt;,
영업시간 중에는 전혀 영향을 주지 않도록 안전하게 마무리할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c336137c5db11994f0561a9d71d80c1c/98e2c/pos5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3UlEQVR42o2Sa0/aYBTH+SbTaSm1F9pCbxTFKkqWLC7bK18YP5GamMx4CRpinGaZUdEiogJqmUQkXkhMGPsIC96AIkrBg52E+Mrk16fnOed/zov/eSw2grURjhbY92OBD8EIBCOboDj93maMdEqur4I0JEhfAMn1Dbe7EIxCMBrB7CbWLhrFGWsjaGBmXpopzjM46ukfVgZHFN8InCyvwESC5nGaJxmBYkWc4lDCAUob6YQSbue6KGejmWL4wMJiJBrbjWk7US2ZvpgNLPsD/rh2GNyJRQ8TmcvM6trGzNTsUeJ3MnV2dpHJZnOJxLHdIVloRrw6Ob69/ndf0O/yN/V6PZlKRQ+2KuVSsVgs3N8ZRjX7Jxff3y/rRaNWMwwDNPn8NcvJFpqVrs7Tul6ovRZOTtNHyXTpoVyqPFaqNcjkcn/jmlZ5etIfqw/VlmaKEeb8S0u/QqsbIVXdDoUjYxOTE5Pf1XAkuBVeD8J/b3pufmx8XFU31e3w+qYKmh8rPylWALcdvKyI7j6pux9M+tCBdaCkKCvdvT6Wd3+0Eu0I3taJO8Ue2TMA/oEAMgD439gz+G4CMcwCIIZ9QNm8AihkWgTA/0fSvJupN4omsB4Yx3Byr/eT1/fZ7fE+AzldAbqPRlHJAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c336137c5db11994f0561a9d71d80c1c/263a4/pos5.webp 480w,
/static/c336137c5db11994f0561a9d71d80c1c/a6361/pos5.webp 960w,
/static/c336137c5db11994f0561a9d71d80c1c/0b34d/pos5.webp 1920w,
/static/c336137c5db11994f0561a9d71d80c1c/548e7/pos5.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c336137c5db11994f0561a9d71d80c1c/9aebd/pos5.png 480w,
/static/c336137c5db11994f0561a9d71d80c1c/a91f8/pos5.png 960w,
/static/c336137c5db11994f0561a9d71d80c1c/ac7a9/pos5.png 1920w,
/static/c336137c5db11994f0561a9d71d80c1c/98e2c/pos5.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c336137c5db11994f0561a9d71d80c1c/ac7a9/pos5.png&quot; alt=&quot;운영 성과 지표&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h2 id=&quot;데이터가-증명하는-변화-운영-패러다임의-전환&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A6%9D%EB%AA%85%ED%95%98%EB%8A%94-%EB%B3%80%ED%99%94-%EC%9A%B4%EC%98%81-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84%EC%9D%98-%EC%A0%84%ED%99%98&quot; aria-label=&quot;데이터가 증명하는 변화 운영 패러다임의 전환 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;데이터가 증명하는 변화: 운영 패러다임의 전환&lt;/h2&gt;
&lt;h3 id=&quot;-before--after-한눈에-보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-before--after-%ED%95%9C%EB%88%88%EC%97%90-%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot; before  after 한눈에 보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📊 Before &amp;#x26; After 한눈에 보기&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;도입 전&lt;/th&gt;
&lt;th&gt;도입 후&lt;/th&gt;
&lt;th&gt;개선률&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;문제 탐지 시간&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4분 30초&lt;/td&gt;
&lt;td&gt;59초&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76% 단축&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;사전 대응 건수&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;월 0건&lt;/td&gt;
&lt;td&gt;월 3건+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;∞% 증가&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;대응 주체&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;매장 제보 후&lt;/td&gt;
&lt;td&gt;시스템 선감지&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;패러다임 전환&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;문제 인지 방식&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;사후 대응형&lt;/td&gt;
&lt;td&gt;예방 중심형&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;운영 철학 변화&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;pos-전담-기술-지원팀보다-먼저-아는-실시간-알람-시스템&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pos-%EC%A0%84%EB%8B%B4-%EA%B8%B0%EC%88%A0-%EC%A7%80%EC%9B%90%ED%8C%80%EB%B3%B4%EB%8B%A4-%EB%A8%BC%EC%A0%80-%EC%95%84%EB%8A%94-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%95%8C%EB%9E%8C-%EC%8B%9C%EC%8A%A4%ED%85%9C&quot; aria-label=&quot;pos 전담 기술 지원팀보다 먼저 아는 실시간 알람 시스템 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&apos;POS 전담 기술 지원팀&apos;보다 먼저 아는, 실시간 알람 시스템&lt;/h3&gt;
&lt;p&gt;이제는 &lt;strong&gt;우리가 먼저 문제를 인지&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오류가 발생하면 Datadog에서 Slack으로 &lt;strong&gt;알람이 즉시 전송&lt;/strong&gt;되어, &apos;POS 전담 기술 지원팀&apos; 제보 없이도 문제를 빠르게 인지하고 &lt;strong&gt;선제적으로 대응&lt;/strong&gt;할 수 있게 되었습니다.&lt;/li&gt;
&lt;li&gt;그뿐만 아니라, &lt;strong&gt;텍스트 로그 기반의 모니터링 구조&lt;/strong&gt;를 활용해
오류 발생 이후 &lt;strong&gt;정상 복구 여부까지 자동으로 추적&lt;/strong&gt;할 수 있도록 구성했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제는 단순히 오류를 감지하는 것을 넘어,
&lt;strong&gt;발생부터 복구까지 전 과정을 하나의 흐름으로 모니터링&lt;/strong&gt;할 수 있는 구조가 마련된 셈입니다.&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3af776f90065d317f33ebc657252f22b/98e2c/pos6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.458333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABg0lEQVR42n2S0UrDMBSG9yii29ombZqkbdZkyZq11W5q0d2JoIii4LzR24HMe59AfAUvfAsRX8qzVXE4FE4gPTlf//+cpOX6nDDN+CDkmkYZjyxk/goviLyANwGfLS+IpZ7Y4lQPj0x+bIuTf+AuostgDmYLGBamieMziC6mbYRdn/0FZ8V4kFeZVUINgG/BorFCJHExR0Fycfkcp1XHC1w/WvfMhElkJqTxqXAaZYAdDGYI/OLh4V2ZSdvFULpKOnjR5+7+flHV0pTNKSjzgPW4yH0qof/Z7A3gjod/KYMOIrwa7WT5iCX9LxgARMjt3evBZLbR7s7nH3Gv3uyiFc2fESQyV6ZMde6HyaJnUPApOzt/up8/1od2evNydT3VVnNhQIHwlAsNGxwmYHuY22K7iFPjoO9pgwdEYqE0jQUOhS33tB1ABUwF8sAHVCzLeK+fpdpKnUOyUf7y1vGaC6RbDlnu6S/boKG0kn1FI9nceeufJ7EeDg4j0R+W4+1RbezOJ+fpiBbt8lL+AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3af776f90065d317f33ebc657252f22b/263a4/pos6.webp 480w,
/static/3af776f90065d317f33ebc657252f22b/a6361/pos6.webp 960w,
/static/3af776f90065d317f33ebc657252f22b/0b34d/pos6.webp 1920w,
/static/3af776f90065d317f33ebc657252f22b/548e7/pos6.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3af776f90065d317f33ebc657252f22b/9aebd/pos6.png 480w,
/static/3af776f90065d317f33ebc657252f22b/a91f8/pos6.png 960w,
/static/3af776f90065d317f33ebc657252f22b/ac7a9/pos6.png 1920w,
/static/3af776f90065d317f33ebc657252f22b/98e2c/pos6.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3af776f90065d317f33ebc657252f22b/ac7a9/pos6.png&quot; alt=&quot;성과 지표 시각화&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;숫자로-보는-운영-혁신&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%AB%EC%9E%90%EB%A1%9C-%EB%B3%B4%EB%8A%94-%EC%9A%B4%EC%98%81-%ED%98%81%EC%8B%A0&quot; aria-label=&quot;숫자로 보는 운영 혁신 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숫자로 보는 운영 혁신&lt;/h3&gt;
&lt;p&gt;이러한 변화는 &lt;strong&gt;데이터로도 확실히 증명&lt;/strong&gt;되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;⏱️ &lt;strong&gt;문제 탐지 시간&lt;/strong&gt;
&lt;br&gt;기존에는 평균 4분 30초가 걸렸던 문제가,
이제는 &lt;strong&gt;59초 이내에 감지&lt;/strong&gt;되며 &lt;strong&gt;약 76% 단축&lt;/strong&gt;되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🧹 &lt;strong&gt;로그 품질 향상&lt;/strong&gt;
&lt;br&gt;로그가 중앙으로 집계되면서, 의미 없는 로그 패턴을 식별하고 제거할 수 있었고,
&lt;br&gt;그 결과 &lt;strong&gt;오류 로그의 정확도가 높아지고&lt;/strong&gt;, &lt;strong&gt;문제의 원인을 더 빠르게 파악&lt;/strong&gt;할 수 있게 되었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;📶 &lt;strong&gt;사전 대응 문화 정착&lt;/strong&gt;
&lt;br&gt;과거에는 네트워크, Database, OS 관련 문제는 대부분 사후에야 알 수 있었지만,
&lt;br&gt;지금은 &lt;strong&gt;사전에 이상 징후를 감지하고 대응&lt;/strong&gt;할 수 있게 되었고,
실제로 &lt;strong&gt;월평균 3건 이상의 선제적 조치&lt;/strong&gt;가 이뤄지고 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제는 단순히 문제가 발생한 뒤 대응하는 것이 아니라,
&lt;strong&gt;발생 전에 감지하고, 감지된 문제의 원인을 빠르게 식별해 대응하는 구조&lt;/strong&gt;를 갖추게 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ec9b913d5ab18d5036516e9060610b32/98e2c/pos7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACX0lEQVR42kVRS08aURTmpwhVBovyEpsoGhLbWgoilRlfiQmLvhMXXXTRdTdddaWlMXXVuKu2Whfgq49tF2r6EgrCMCIz2BmZGQfmce/M9A4uevLl5NycfOc73z02V0+fyzPc7R3p9oS7nF6n09fp9HR0uTs63VbGfB1Ynx3z2bu9drff7vLY260rWK+rJ2Bze/vniIn7k7HUFD6YiPjHwuFENJmcIYhZnJghokP4qBePhCaHRieD4eT12Dg+TUzP3YjEMbff5g8Eq+/j5ucku0c8z6ef5ReWyXdc+YTMFXWJ1z7NgK94awMvzT/JP3h8upCGphXbO3sOZ4/NHxys1urobei6oRumYUKoyypoyqqiarTInQhSWWafHi0+Onq5RK6bukXO7uy2yf0hpm6RAQQIEFHbU1BGpaqIUMgbTRJKpC5V4EUZCAXTYLIbKw7MawtcC5GVKkur9InUYBWeVbi6zDKtv7XWWU2G8pnW2Af8AeQPAP8d8IdqY9+ExczmWwfms8gUVSPzQqUoUMdCpcBTRQSxUhBqpKiqClCBgUxdBloQWK6z2+21e30Dyy9+f3xzvvqK/rBU31rh1tLM2mtmdfHsywantVqA45CTtifLlNpo/Cf7g6GDb6XcjzpSLv9p5H7SDCUf57hfhzTLyIqiaAAiRaSqt3GpnNnaRae21i6RJeqUasoXosQXy0Woa7x4XqWrGlDRnwtNRWjKYlO5aFnFuSgB01zfzDi63Larnr7RaAIBnR/h5u3xsQQRjSdvxe7EUDGOR+O4la0uERmbmJpN3Xs4P5e6OzA88g+R37VUoocijgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ec9b913d5ab18d5036516e9060610b32/263a4/pos7.webp 480w,
/static/ec9b913d5ab18d5036516e9060610b32/a6361/pos7.webp 960w,
/static/ec9b913d5ab18d5036516e9060610b32/0b34d/pos7.webp 1920w,
/static/ec9b913d5ab18d5036516e9060610b32/548e7/pos7.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ec9b913d5ab18d5036516e9060610b32/9aebd/pos7.png 480w,
/static/ec9b913d5ab18d5036516e9060610b32/a91f8/pos7.png 960w,
/static/ec9b913d5ab18d5036516e9060610b32/ac7a9/pos7.png 1920w,
/static/ec9b913d5ab18d5036516e9060610b32/98e2c/pos7.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ec9b913d5ab18d5036516e9060610b32/ac7a9/pos7.png&quot; alt=&quot;POS 시스템의 실시간 운영 데이터를 한눈에 보여주는 Datadog 대시보드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;POS 시스템의 실시간 운영 데이터를 한눈에 보여주는 Datadog 대시보드&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h2 id=&quot;모두가-함께-보는-운영-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%91%90%EA%B0%80-%ED%95%A8%EA%BB%98-%EB%B3%B4%EB%8A%94-%EC%9A%B4%EC%98%81-%EC%A7%80%ED%91%9C&quot; aria-label=&quot;모두가 함께 보는 운영 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모두가 함께 보는 운영 지표&lt;/h2&gt;
&lt;p&gt;Datadog 도입은 &lt;strong&gt;개발팀만을 위한 변화가 아니었습니다.&lt;/strong&gt;
&lt;br&gt;운영, 마케팅, CS, 영업, QA 등 여러 부서에서도
&lt;strong&gt;공통된 실시간 데이터를 기반으로 문제를 인지하고, 함께 대응할 수 있는 환경&lt;/strong&gt;이 만들어졌습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;매출 추이&lt;/strong&gt;, &lt;strong&gt;결제 수단별 비율&lt;/strong&gt;, &lt;strong&gt;오류 발생 현황&lt;/strong&gt;, &lt;strong&gt;POS 온라인 상태&lt;/strong&gt; 등
다양한 정보를 실시간 대시보드로 시각화하여 제공하고 있고,&lt;/li&gt;
&lt;li&gt;그 결과, &lt;strong&gt;부서 간 커뮤니케이션은 더 원활해지고&lt;/strong&gt;,
&lt;strong&gt;의사결정 속도도 훨씬 빨라졌습니다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제는 특정 팀만 보는 지표가 아닌,
&lt;strong&gt;모두가 함께 보고 함께 움직이는 운영 체계&lt;/strong&gt;로 바뀌고 있습니다.&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/31920ef1e901979386615095ad0b71b3/98e2c/pos8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACKElEQVR42l2PW2sTQRTH9wMoWnz0wdpkd2dmd2azm70kzSabTXdz2Whzq23T5mLc0JBWrORBEaG0WogWFd+CWB8EBZ8sPllQ8UmEfqqeEilF+M3hP+f855wznDhXE+fqKNKgQlPm1xTS4WdrGDWZFt7gK8juXzeayvI+ax3MJDszqc4Va+WysXxJvwNwFhnadNtRRrXEeLPypRVMkvKDzuJht/NJVQdudbzQmzi9D2rrDbv7Vmi/ml3dv1Z+dLUwArhnnePd9e/j9s+n1a871aOHwccXzeN3WydPwqPH977VliZLG583dn+xxp7ROxgcnvTf/12f/Fl8/Tt4+YPrVvZq3qjmbheTYdEMPa27mh3dtoeO1fadgZ3pm25oL95HzlosCLNrIz7TmF/Zzg+e53o73E0RR5HEY1mUFIFQQaIRIguSAleeUEQYwpRHskiYgGhEwCJmUUGKRFGUxxymmpfv50vDYrCZ83sZd933w5RdLwZb2VzbLw7dfJ/HDMkaonFM44j+EwAHWVE2RNkUqYEViwDMwlQnsQRmpmo58YTLtIQcM0VJPWtxAU4gsUwuv1Aop7M+00yRxKA3rENYnEe0VK5U6yv5UiWV8cB5NklSz7twoPREJueXXS9IOX7CzkFMphc004YyDAQNSSvlxq10TJ/XrTRwtj88hgMKTEo8Caj6PDhATHtDiTD9IpJiQJyuwE1N02Uu8t/3zm2qkcp6Ja9wy3a8U4WSp3N+W8MRAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/31920ef1e901979386615095ad0b71b3/263a4/pos8.webp 480w,
/static/31920ef1e901979386615095ad0b71b3/a6361/pos8.webp 960w,
/static/31920ef1e901979386615095ad0b71b3/0b34d/pos8.webp 1920w,
/static/31920ef1e901979386615095ad0b71b3/548e7/pos8.webp 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/31920ef1e901979386615095ad0b71b3/9aebd/pos8.png 480w,
/static/31920ef1e901979386615095ad0b71b3/a91f8/pos8.png 960w,
/static/31920ef1e901979386615095ad0b71b3/ac7a9/pos8.png 1920w,
/static/31920ef1e901979386615095ad0b71b3/98e2c/pos8.png 2400w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/31920ef1e901979386615095ad0b71b3/ac7a9/pos8.png&quot; alt=&quot;향후 계획&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;지금까지도 많은 변화가 있었지만,
저희는 여기서 멈추지 않고 &lt;strong&gt;다음 단계로 나아갈 준비를 하고 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;결제 흐름 로그 분석 고도화&lt;/strong&gt;
&lt;br&gt;전체 POS/SCO에서 수집된 로그를 바탕으로
&lt;strong&gt;결제 지연의 원인을 타임라인 기반으로 정밀 분석&lt;/strong&gt;하고,
&lt;br&gt;병목 구간을 정확히 찾아 개선 포인트를 도출하려 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;지능형 알람 시스템&lt;/strong&gt;
&lt;br&gt;단순히 오류 발생 여부만 알리는 데서 벗어나,&lt;br&gt;
&lt;strong&gt;오류 발생의 추세나 빈도를 기반으로 이상 징후를 감지하는 시스템&lt;/strong&gt;으로
알람 체계를 한층 더 고도화할 계획입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a756bff04557163e500a64d020b59a9e/81f49/pos_krug3.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQBAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAABAAL/2gAMAwEAAhADEAAAAVIcvlzhsn//xAAaEAACAwEBAAAAAAAAAAAAAAAAAQISEwMi/9oACAEBAAEFArkpuifQ8qWdDOR//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGxABAAICAwAAAAAAAAAAAAAAAQARIjECQWH/2gAIAQEABj8CNRQ73MSLQeVHkmFTHU//xAAbEAEBAAIDAQAAAAAAAAAAAAABEQAhMUFhUf/aAAgBAQABPyFNh28cSS6YIbJ4YtjeKoxg27OrcpXkfuf/2gAMAwEAAgADAAAAEG/P/8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERUf/aAAgBAwEBPxBXSM//xAAXEQADAQAAAAAAAAAAAAAAAAAAAREx/9oACAECAQE/EKshUf/EABwQAQEAAwEAAwAAAAAAAAAAAAERACExQVGBof/aAAgBAQABPxCYkW3YfuKZyZoT75mhhqtYvuPo3UqkNhuOKJwCPRsl58YPUqEnO9dnkz//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a756bff04557163e500a64d020b59a9e/263a4/pos_krug3.webp 480w,
/static/a756bff04557163e500a64d020b59a9e/a6361/pos_krug3.webp 960w,
/static/a756bff04557163e500a64d020b59a9e/0b34d/pos_krug3.webp 1920w,
/static/a756bff04557163e500a64d020b59a9e/da28f/pos_krug3.webp 2880w,
/static/a756bff04557163e500a64d020b59a9e/98b7d/pos_krug3.webp 3840w,
/static/a756bff04557163e500a64d020b59a9e/d182c/pos_krug3.webp 4032w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a756bff04557163e500a64d020b59a9e/a3e66/pos_krug3.jpg 480w,
/static/a756bff04557163e500a64d020b59a9e/fb816/pos_krug3.jpg 960w,
/static/a756bff04557163e500a64d020b59a9e/aaf92/pos_krug3.jpg 1920w,
/static/a756bff04557163e500a64d020b59a9e/1d134/pos_krug3.jpg 2880w,
/static/a756bff04557163e500a64d020b59a9e/9ad4e/pos_krug3.jpg 3840w,
/static/a756bff04557163e500a64d020b59a9e/81f49/pos_krug3.jpg 4032w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a756bff04557163e500a64d020b59a9e/aaf92/pos_krug3.jpg&quot; alt=&quot;Datadog 한국 사용자 모임 발표 현장 📸&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;Datadog 한국 사용자 모임 발표 현장 📸&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;2025년 5월, 이 사례를 Datadog 한국 사용자 모임에서 직접 공유했습니다.&lt;br&gt;
POS라는 오프라인 환경에서 로그를 수집하고 모니터링 체계를 구축한 과정은 쉬운 도전은 아니었지만, &lt;br&gt;함께 고민하고 실행해 준 팀원들이 있었기에 가능했습니다.&lt;br&gt;
&lt;strong&gt;🙌이 자리를 빌려 운영을 함께 만들어간 모든 팀원분께 진심으로 감사의 마음을 전합니다!&lt;/strong&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;마무리하며-더-똑똑한-pos-시스템을-향한-변화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0-%EB%8D%94-%EB%98%91%EB%98%91%ED%95%9C-pos-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%ED%96%A5%ED%95%9C-%EB%B3%80%ED%99%94&quot; aria-label=&quot;마무리하며 더 똑똑한 pos 시스템을 향한 변화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리하며: 더 똑똑한 POS 시스템을 향한 변화&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;“Datadog을 POS에 붙여보면 어떨까?” 하는 단순한 아이디어에서 시작했지만,
이 프로젝트를 통해 저희는 &lt;strong&gt;운영 방식 자체를 한 단계 끌어올릴 수 있었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존의 반응형 운영에 머무르지 않고, &lt;strong&gt;예방 중심의 운영 체계로 나아가기 위한 기반을 마련&lt;/strong&gt;했으며,&lt;/li&gt;
&lt;li&gt;실시간 데이터 분석을 통해 문제를 빠르게 인지하고 대응할 수 있는 운영 체계를 정립했습니다.&lt;/li&gt;
&lt;li&gt;또한, &lt;strong&gt;영업, 인프라, 보안팀과의 협업&lt;/strong&gt;을 통해 시스템을 더 넓은 관점에서 바라보는 시야도 키울 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이러한 변화들이, &lt;strong&gt;리테일 환경에서 비슷한 고민을 하고 계신 분들께 작은 아이디어나 시작점이 되어줄 수 있다면 참 좋겠습니다.🙌&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;올리브영 POS는 지금, 이 순간에도 &lt;br&gt;전국 수천 개 매장에서 발생하는 데이터를 실시간으로 수집하고, &lt;br&gt;장애를 사전에 감지하면서
&lt;strong&gt;더 빠르고, 안정적인 리테일 시스템&lt;/strong&gt;으로 계속 발전해 나가고 있습니다.&lt;/p&gt;
&lt;p&gt;앞으로 저희가 만들어갈 변화들도 기대해 주세요!&lt;/p&gt;
&lt;p&gt;감사합니다. 🙇🏻‍♀️&lt;/p&gt;</content:encoded></item><item><title><![CDATA[AWS Summit Seoul 2025 발표 후기 - 소중한 우리의 시간을 위한 클라우드 스케일링 자동화]]></title><description><![CDATA[안녕하세요.
올리브영 플랫폼엔지니어링을 담당하는 무스타파🌙입니다. 지난 5월 15일부터 16일까지 코엑스에서 개최된 AWS Summit Seoul 2025에서 "소중한 우리의 시간을 위한 클라우드 스케일링 자동화"라는 주제로 Community…]]></description><link>https://oliveyoung.tech/2025-06-25/aws-summit-recap/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-06-25/aws-summit-recap/</guid><pubDate>Wed, 25 Jun 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.
올리브영 플랫폼엔지니어링을 담당하는 무스타파🌙입니다.&lt;/p&gt;
&lt;p&gt;지난 5월 15일부터 16일까지 코엑스에서 개최된 &lt;a href=&quot;https://aws.amazon.com/ko/events/summits/seoul/&quot;&gt;AWS Summit Seoul 2025&lt;/a&gt;에서 &quot;소중한 우리의 시간을 위한 클라우드 스케일링 자동화&quot;라는 주제로 Community Track에서 발표를 진행했습니다. 정말 많은 분들이 세션에 참석해주셔서 감사했습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 발표에 사용된 슬라이드와 함께 주요 내용을 정리하여 공유하고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C&quot;&gt;올리브영의 클라우드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%ED%8A%B8%EB%9E%98%ED%94%BD-%ED%8C%A8%ED%84%B4%EA%B3%BC-auto-scaling%EC%9D%98-%ED%95%9C%EA%B3%84&quot;&gt;실제 트래픽 패턴과 Auto Scaling의 한계&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9D%B8-%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%A7%81-%EC%A0%84%EB%9E%B5&quot;&gt;효과적인 스케일링 전략&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#sre%EC%9D%98-%ED%95%B5%EC%8B%AC-%EC%9B%90%EC%B9%99&quot;&gt;SRE의 핵심 원칙&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EC%A4%91%EC%8B%AC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81bom&quot;&gt;비즈니스 중심 모니터링&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%98%A4%EB%8A%98%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-3%EA%B0%80%EC%A7%80&quot;&gt;오늘부터 시작할 수 있는 3가지&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h1 id=&quot;올리브영의-클라우드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C&quot; aria-label=&quot;올리브영의 클라우드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영의 클라우드&lt;/h1&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7dd6612843d86ec005f02f7ab174ca29/09658/slide3.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIFBv/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/9oADAMBAAIQAxAAAAGPWj6AzccmD//EABsQAAEEAwAAAAAAAAAAAAAAAAEAAgMRIjFB/9oACAEBAAEFAuXjGbamgGIa/8QAFhEAAwAAAAAAAAAAAAAAAAAAAhAh/9oACAEDAQE/ATi//8QAGBEAAwEBAAAAAAAAAAAAAAAAAQIRAAP/2gAIAQIBAT8B7sFhOY03f//EABoQAAICAwAAAAAAAAAAAAAAAAACECERQlH/2gAIAQEABj8CEvUuEzyP/8QAGhAAAwADAQAAAAAAAAAAAAAAAAERECFBsf/aAAgBAQABPyHQcKpKOkGjhIFi+Yv/2gAMAwEAAgADAAAAEHjv/8QAFxEBAQEBAAAAAAAAAAAAAAAAAQAhEf/aAAgBAwEBPxAnGS7f/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAERITFRsf/aAAgBAgEBPxBrczwn9mf/xAAbEAEBAQACAwAAAAAAAAAAAAABEQAhMUFhgf/aAAgBAQABPxCK/P3VxFYILPWouSoK9l4yAtNigXARv//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7dd6612843d86ec005f02f7ab174ca29/263a4/slide3.webp 480w,
/static/7dd6612843d86ec005f02f7ab174ca29/a6361/slide3.webp 960w,
/static/7dd6612843d86ec005f02f7ab174ca29/0b34d/slide3.webp 1920w,
/static/7dd6612843d86ec005f02f7ab174ca29/96d48/slide3.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7dd6612843d86ec005f02f7ab174ca29/a3e66/slide3.jpg 480w,
/static/7dd6612843d86ec005f02f7ab174ca29/fb816/slide3.jpg 960w,
/static/7dd6612843d86ec005f02f7ab174ca29/aaf92/slide3.jpg 1920w,
/static/7dd6612843d86ec005f02f7ab174ca29/09658/slide3.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7dd6612843d86ec005f02f7ab174ca29/aaf92/slide3.jpg&quot; alt=&quot;올리브영 클라우드 아키텍처 개요&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;저희가 AWS 클라우드를 적극적으로 활용하게 된 주요 이유는 &lt;strong&gt;계절성 트래픽 대응&lt;/strong&gt;입니다. 올리브영은 3월, 6월, 9월, 12월, 연 4회 대규모 세일을 진행하는데, 이때 평소 대비 약 10배의 트래픽이 발생합니다.&lt;/p&gt;
&lt;p&gt;이러한 급격한 트래픽 증가에 대응하기 위해 AWS 클라우드를 선택했고, 현재는 대부분의 워크로드가 AWS 클라우드 위에서 운영되고 있습니다. 아마도 많은 분들이 비슷한 이유로 클라우드 서비스를 활용하고 계실 것으로 생각됩니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/153e52135f1adbb91da06b908d90660c/09658/slide4.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMBBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB5kNaTNQH/8QAGBAAAgMAAAAAAAAAAAAAAAAAAAERIEH/2gAIAQEAAQUCxkU//8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/AbGv/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABsQAAEEAwAAAAAAAAAAAAAAAAEAEBEhMVGB/9oACAEBAAE/IReRQztnSpb/2gAMAwEAAgADAAAAEGw//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8Qgp//xAAVEQEBAAAAAAAAAAAAAAAAAAARAP/aAAgBAgEBPxAY3//EABwQAQACAgMBAAAAAAAAAAAAAAEAESExEEFxkf/aAAgBAQABPxAUOw6CA7q2ssc5aGPpLO1fXj//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/153e52135f1adbb91da06b908d90660c/263a4/slide4.webp 480w,
/static/153e52135f1adbb91da06b908d90660c/a6361/slide4.webp 960w,
/static/153e52135f1adbb91da06b908d90660c/0b34d/slide4.webp 1920w,
/static/153e52135f1adbb91da06b908d90660c/96d48/slide4.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/153e52135f1adbb91da06b908d90660c/a3e66/slide4.jpg 480w,
/static/153e52135f1adbb91da06b908d90660c/fb816/slide4.jpg 960w,
/static/153e52135f1adbb91da06b908d90660c/aaf92/slide4.jpg 1920w,
/static/153e52135f1adbb91da06b908d90660c/09658/slide4.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/153e52135f1adbb91da06b908d90660c/aaf92/slide4.jpg&quot; alt=&quot;E-커머스 마케팅 트래픽 패턴&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;E-커머스 업계에서는 마케팅이 매우 중요한 요소입니다. 마케팅 없이는 E-커머스 비즈니스를 제대로 운영하기 어렵습니다.&lt;/p&gt;
&lt;p&gt;마케팅 활동으로 인한 트래픽 증가는 매우 빈번하게 발생하며, &lt;strong&gt;매달 약 10회 정도 트래픽 스파이크&lt;/strong&gt;가 발생합니다. 인플루언서 협업이나 아이돌 굿즈 판매 시에도 트래픽이 급격히 증가합니다.&lt;/p&gt;
&lt;p&gt;이러한 예측 불가능한 트래픽들을 어떻게 효과적으로 관리할 것인가 하는 문제가 우리의 핵심 과제였습니다.&lt;/p&gt;
&lt;h1 id=&quot;실제-트래픽-패턴과-auto-scaling의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%ED%8A%B8%EB%9E%98%ED%94%BD-%ED%8C%A8%ED%84%B4%EA%B3%BC-auto-scaling%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;실제 트래픽 패턴과 auto scaling의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실제 트래픽 패턴과 Auto Scaling의 한계&lt;/h1&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5decc40b00f4623dfa3057474fea3c4e/09658/slide5.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEAv/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB3FAaCxgT/8QAGBABAAMBAAAAAAAAAAAAAAAAAgEDBCD/2gAIAQEAAQUCGMoaaIqUxx//xAAWEQEBAQAAAAAAAAAAAAAAAAAAARH/2gAIAQMBAT8BrX//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwGI/8QAGhABAAEFAAAAAAAAAAAAAAAAAREAAhIgIf/aAAgBAQAGPwK1zelBKzr/AP/EABkQAAMBAQEAAAAAAAAAAAAAAAABESEQMf/aAAgBAQABPyFp1ycwXqdSLwrnrLz/2gAMAwEAAgADAAAAECM//8QAFhEBAQEAAAAAAAAAAAAAAAAAABEB/9oACAEDAQE/EMRT/8QAFhEBAQEAAAAAAAAAAAAAAAAAABFh/9oACAECAQE/ENJf/8QAHRABAQEAAAcAAAAAAAAAAAAAAREAECExQWFxgf/aAAgBAQABPxBmsiCClxBxaQ5RnbGpU86CgfXU9Vfbw//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5decc40b00f4623dfa3057474fea3c4e/263a4/slide5.webp 480w,
/static/5decc40b00f4623dfa3057474fea3c4e/a6361/slide5.webp 960w,
/static/5decc40b00f4623dfa3057474fea3c4e/0b34d/slide5.webp 1920w,
/static/5decc40b00f4623dfa3057474fea3c4e/96d48/slide5.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5decc40b00f4623dfa3057474fea3c4e/a3e66/slide5.jpg 480w,
/static/5decc40b00f4623dfa3057474fea3c4e/fb816/slide5.jpg 960w,
/static/5decc40b00f4623dfa3057474fea3c4e/aaf92/slide5.jpg 1920w,
/static/5decc40b00f4623dfa3057474fea3c4e/09658/slide5.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5decc40b00f4623dfa3057474fea3c4e/aaf92/slide5.jpg&quot; alt=&quot;실제 광고 유입 트래픽 패턴&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이 슬라이드는 실제로 저희가 자주 경험하는 광고 유입 트래픽 패턴입니다.&lt;/p&gt;
&lt;p&gt;이 패턴을 살펴보면, 광고가 시작되는 순간 트래픽이 급격히 증가합니다. 예를 들어, 오후 7시 59분부터 8시 1분 사이에 트래픽이 두 배로 증가하는 경우가 있습니다. &lt;strong&gt;단 2분만에 2배로 증가&lt;/strong&gt;하는 상황입니다.&lt;/p&gt;
&lt;p&gt;이런 급격한 변화에 대응하기 위해 일반적으로 Auto Scaling을 활용합니다만, 현실은 그리 단순하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9fb20fbe1988ef107b931e660c03cbce/09658/slide6.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMCBf/EABcBAAMBAAAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAWuG0KplP//EABoQAAICAwAAAAAAAAAAAAAAAAIDAAQBEBT/2gAIAQEAAQUCQpJp56+ZZAQbv//EABYRAQEBAAAAAAAAAAAAAAAAAAARMf/aAAgBAwEBPwHFf//EABYRAQEBAAAAAAAAAAAAAAAAAAAREv/aAAgBAgEBPwGMv//EAB4QAAAEBwAAAAAAAAAAAAAAAAABAgMRICIxMnGi/9oACAEBAAY/AiNaqtjLoQbtJ//EABsQAQACAgMAAAAAAAAAAAAAABEAARAhMUGh/9oACAEBAAE/ISKbNGpfI8Iuk12wz//aAAwDAQACAAMAAAAQ9y//xAAWEQADAAAAAAAAAAAAAAAAAAAQIUH/2gAIAQMBAT8QUj//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPxAh/8QAHBABAAMAAgMAAAAAAAAAAAAAAQARITFBYXGR/9oACAEBAAE/EAPp4AUM9R0TquJLdHD3puygKGw3mvkqf//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9fb20fbe1988ef107b931e660c03cbce/263a4/slide6.webp 480w,
/static/9fb20fbe1988ef107b931e660c03cbce/a6361/slide6.webp 960w,
/static/9fb20fbe1988ef107b931e660c03cbce/0b34d/slide6.webp 1920w,
/static/9fb20fbe1988ef107b931e660c03cbce/96d48/slide6.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9fb20fbe1988ef107b931e660c03cbce/a3e66/slide6.jpg 480w,
/static/9fb20fbe1988ef107b931e660c03cbce/fb816/slide6.jpg 960w,
/static/9fb20fbe1988ef107b931e660c03cbce/aaf92/slide6.jpg 1920w,
/static/9fb20fbe1988ef107b931e660c03cbce/09658/slide6.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9fb20fbe1988ef107b931e660c03cbce/aaf92/slide6.jpg&quot; alt=&quot;Auto Scaling의 현실적 문제점&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 핵심 포인트&lt;/strong&gt;
Auto Scaling은 만능 도구가 아니라 &apos;장애 시간을 줄여주는 보험&apos; 정도로 이해하는 것이 적절합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;현실에서 Auto Scaling만으로는 효과적인 대응이 어려운 이유는 다음과 같습니다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;반응 지연&lt;/strong&gt;: 트래픽이 급증한 후에야 Auto Scaling 조건이 충족됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장애 시점&lt;/strong&gt;: 조건 충족 후 증설이 시작되지만, 이미 장애는 시작된 상태입니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복구 시간&lt;/strong&gt;: 증설 완료까지 약 3-5분이 소요됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사용자 경험&lt;/strong&gt;: 결과적으로 첫 5분 동안의 트래픽은 모두 장애를 경험하게 됩니다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;총 장애 시간&lt;/strong&gt;: 시스템 회복까지 약 10분 정도 장애 상태가 지속됩니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 되면 광고를 집행했음에도 장애만 발생하고, 효과는 없으며, 비용만 낭비하는 결과를 초래합니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/aba1862e9b6cbd8a09f93baabb899afb/09658/slide7.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAEDAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAACAQP/2gAMAwEAAhADEAAAAeO8PUzKBn//xAAYEAACAwAAAAAAAAAAAAAAAAABEQACIP/aAAgBAQABBQIQ1CWP/8QAFhEAAwAAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/AWoU/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABcQAQEBAQAAAAAAAAAAAAAAAAERACD/2gAIAQEAAT8hhY4FR5f/2gAMAwEAAgADAAAAEGQ//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8QCn//xAAWEQADAAAAAAAAAAAAAAAAAAAAARH/2gAIAQIBAT8QapJ//8QAHBABAAIBBQAAAAAAAAAAAAAAAQARECExUXGB/9oACAEBAAE/ECOyniOOjaEdcoBlrur7j//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/aba1862e9b6cbd8a09f93baabb899afb/263a4/slide7.webp 480w,
/static/aba1862e9b6cbd8a09f93baabb899afb/a6361/slide7.webp 960w,
/static/aba1862e9b6cbd8a09f93baabb899afb/0b34d/slide7.webp 1920w,
/static/aba1862e9b6cbd8a09f93baabb899afb/96d48/slide7.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/aba1862e9b6cbd8a09f93baabb899afb/a3e66/slide7.jpg 480w,
/static/aba1862e9b6cbd8a09f93baabb899afb/fb816/slide7.jpg 960w,
/static/aba1862e9b6cbd8a09f93baabb899afb/aaf92/slide7.jpg 1920w,
/static/aba1862e9b6cbd8a09f93baabb899afb/09658/slide7.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/aba1862e9b6cbd8a09f93baabb899afb/aaf92/slide7.jpg&quot; alt=&quot;Auto Scaling 한계 요약&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;따라서 우리는 Auto Scaling의 한계를 인정하고 새로운 접근 방법을 모색해야 합니다.&lt;/p&gt;
&lt;h1 id=&quot;효과적인-스케일링-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9D%B8-%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%A7%81-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;효과적인 스케일링 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;효과적인 스케일링 전략&lt;/h1&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7eef9d21ed856b4e3f4e6eeea904612a/09658/slide8.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAwAC/8QAFgEBAQEAAAAAAAAAAAAAAAAABAED/9oADAMBAAIQAxAAAAE0wbKtuBv/AP/EABkQAAIDAQAAAAAAAAAAAAAAAAECABARMf/aAAgBAQABBQJcMKiBNDUnP//EABkRAAIDAQAAAAAAAAAAAAAAAAABAgMUUf/aAAgBAwEBPwHJFsy18P/EABkRAAIDAQAAAAAAAAAAAAAAAAECAAQRIf/aAAgBAgEBPwHd7HsOGIE//8QAFhAAAwAAAAAAAAAAAAAAAAAAACAx/9oACAEBAAY/Air/AP/EABcQAQEBAQAAAAAAAAAAAAAAAAEAETH/2gAIAQEAAT8hC4uQjUE2xyActZJf/9oADAMBAAIAAwAAABA3L//EABkRAQACAwAAAAAAAAAAAAAAAAEAETGx0f/aAAgBAwEBPxDCKQMrZ7P/xAAYEQADAQEAAAAAAAAAAAAAAAAAESEBMf/aAAgBAgEBPxCS8N4Iz//EABsQAQACAgMAAAAAAAAAAAAAAAEAESFBMWGh/9oACAEBAAE/EFAHodzrxi+fZQsyrMNsUwQyykihbP/Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7eef9d21ed856b4e3f4e6eeea904612a/263a4/slide8.webp 480w,
/static/7eef9d21ed856b4e3f4e6eeea904612a/a6361/slide8.webp 960w,
/static/7eef9d21ed856b4e3f4e6eeea904612a/0b34d/slide8.webp 1920w,
/static/7eef9d21ed856b4e3f4e6eeea904612a/96d48/slide8.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7eef9d21ed856b4e3f4e6eeea904612a/a3e66/slide8.jpg 480w,
/static/7eef9d21ed856b4e3f4e6eeea904612a/fb816/slide8.jpg 960w,
/static/7eef9d21ed856b4e3f4e6eeea904612a/aaf92/slide8.jpg 1920w,
/static/7eef9d21ed856b4e3f4e6eeea904612a/09658/slide8.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7eef9d21ed856b4e3f4e6eeea904612a/aaf92/slide8.jpg&quot; alt=&quot;스케일링 자동화 전략 개요&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;그렇다면 어떻게 더 효과적으로 스케일링을 자동화하고 안전하게 할 수 있을까요?&lt;/p&gt;
&lt;p&gt;저희는 운영자들이 밤잠을 설치지 않고 편안하게 쉴 수 있도록 &lt;strong&gt;4단계 접근법&lt;/strong&gt;을 제안합니다.&lt;/p&gt;
&lt;h2 id=&quot;1단계-트래픽-카테고리화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EB%8B%A8%EA%B3%84-%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%B9%B4%ED%85%8C%EA%B3%A0%EB%A6%AC%ED%99%94&quot; aria-label=&quot;1단계 트래픽 카테고리화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1단계: 트래픽 카테고리화&lt;/h2&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/de9f98e1a5db4d11d124ec839a04f7cd/09658/slide9.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMCBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB49YNDCgn/8QAGBAAAwEBAAAAAAAAAAAAAAAAAAERAyD/2gAIAQEAAQUClNEmTj//xAAWEQADAAAAAAAAAAAAAAAAAAAAARH/2gAIAQMBAT8BahT/xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFH/8QAGhAAAQUBAAAAAAAAAAAAAAAAIQABAhESIP/aAAgBAQAGPwJNmNA8/wD/xAAaEAACAgMAAAAAAAAAAAAAAAABEQAgMUFR/9oACAEBAAE/IQWAmv8ARC2e1f/aAAwDAQACAAMAAAAQeD//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAwEBPxAKf//EABYRAAMAAAAAAAAAAAAAAAAAAAABEf/aAAgBAgEBPxBqkn//xAAaEAEAAgMBAAAAAAAAAAAAAAABACEQEVFB/9oACAEBAAE/EC7HryUnQAq9obzQDDH/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/de9f98e1a5db4d11d124ec839a04f7cd/263a4/slide9.webp 480w,
/static/de9f98e1a5db4d11d124ec839a04f7cd/a6361/slide9.webp 960w,
/static/de9f98e1a5db4d11d124ec839a04f7cd/0b34d/slide9.webp 1920w,
/static/de9f98e1a5db4d11d124ec839a04f7cd/96d48/slide9.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/de9f98e1a5db4d11d124ec839a04f7cd/a3e66/slide9.jpg 480w,
/static/de9f98e1a5db4d11d124ec839a04f7cd/fb816/slide9.jpg 960w,
/static/de9f98e1a5db4d11d124ec839a04f7cd/aaf92/slide9.jpg 1920w,
/static/de9f98e1a5db4d11d124ec839a04f7cd/09658/slide9.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/de9f98e1a5db4d11d124ec839a04f7cd/aaf92/slide9.jpg&quot; alt=&quot;트래픽 카테고리 분류의 필요성&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/813b1194d2c9c4a9cecc96b8a271d0a6/09658/slide10.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAIBAwX/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAActUjQQsCf/EABcQAAMBAAAAAAAAAAAAAAAAAAABESD/2gAIAQEAAQUCGiY//8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/AbGv/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABkQAQEBAAMAAAAAAAAAAAAAAAEhABARgf/aAAgBAQABPyEYVxNe3NTevP8A/9oADAMBAAIAAwAAABC4P//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/EIKf/8QAFREBAQAAAAAAAAAAAAAAAAAAEQD/2gAIAQIBAT8QGN//xAAeEAACAQMFAAAAAAAAAAAAAAABEQAhMWEQUYHB4f/aAAgBAQABPxBzrO3kfkVx1AtUsxkClDMZNyTp/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/813b1194d2c9c4a9cecc96b8a271d0a6/263a4/slide10.webp 480w,
/static/813b1194d2c9c4a9cecc96b8a271d0a6/a6361/slide10.webp 960w,
/static/813b1194d2c9c4a9cecc96b8a271d0a6/0b34d/slide10.webp 1920w,
/static/813b1194d2c9c4a9cecc96b8a271d0a6/96d48/slide10.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/813b1194d2c9c4a9cecc96b8a271d0a6/a3e66/slide10.jpg 480w,
/static/813b1194d2c9c4a9cecc96b8a271d0a6/fb816/slide10.jpg 960w,
/static/813b1194d2c9c4a9cecc96b8a271d0a6/aaf92/slide10.jpg 1920w,
/static/813b1194d2c9c4a9cecc96b8a271d0a6/09658/slide10.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/813b1194d2c9c4a9cecc96b8a271d0a6/aaf92/slide10.jpg&quot; alt=&quot;자동차 구매 비유를 통한 카테고리 설명&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;트래픽이 어떤 형태로 들어오는지 카테고리를 나누는 것이 첫 번째 단계입니다.&lt;/p&gt;
&lt;p&gt;트래픽을 카테고리로 나누는 것은 마치 자동차를 구매할 때 티코를 살 것인지, 아반떼를 살 것인지, 소나타를 살 것인지, 그랜저를 살 것인지 명확히 정하는 것과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;명확한 목표와 경계를 정해두지 않으면&lt;/strong&gt; 티코를 사려고 하다가 그랜저를 사려고 하는 상황이 발생할 수 있습니다. 트래픽 카테고리를 명확히 구분한다면, 대응 체계를 수립하는 데 훨씬 효율적입니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d01e5ae200ae288b278834aadc82c2ab/09658/slide11.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAgABBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB5ZG6glSf/8QAFhABAQEAAAAAAAAAAAAAAAAAEBEB/9oACAEBAAEFAqaU/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/Aa1//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABgQAAIDAAAAAAAAAAAAAAAAAAARIDEy/9oACAEBAAY/AjJSh//EABwQAQABBAMAAAAAAAAAAAAAAAEAEBEhMUGBof/aAAgBAQABPyEx17Gzwe2EXBjUup//2gAMAwEAAgADAAAAEGgv/8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERMf/aAAgBAwEBPxBJhR//xAAVEQEBAAAAAAAAAAAAAAAAAAARAP/aAAgBAgEBPxBY3//EAB0QAQEBAAEFAQAAAAAAAAAAAAERACExQXGBsdH/2gAIAQEAAT8QJIlgcqM53tiftuiQJBX7hhgey4BpDwGVXlu//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d01e5ae200ae288b278834aadc82c2ab/263a4/slide11.webp 480w,
/static/d01e5ae200ae288b278834aadc82c2ab/a6361/slide11.webp 960w,
/static/d01e5ae200ae288b278834aadc82c2ab/0b34d/slide11.webp 1920w,
/static/d01e5ae200ae288b278834aadc82c2ab/96d48/slide11.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d01e5ae200ae288b278834aadc82c2ab/a3e66/slide11.jpg 480w,
/static/d01e5ae200ae288b278834aadc82c2ab/fb816/slide11.jpg 960w,
/static/d01e5ae200ae288b278834aadc82c2ab/aaf92/slide11.jpg 1920w,
/static/d01e5ae200ae288b278834aadc82c2ab/09658/slide11.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d01e5ae200ae288b278834aadc82c2ab/aaf92/slide11.jpg&quot; alt=&quot;올리브영 5단계 트래픽 분류&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;올리브영에서는 트래픽을 &lt;strong&gt;5단계로 구분&lt;/strong&gt;합니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Low&lt;/strong&gt;: 야간 시간대 (비용 절감 목적)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Normal&lt;/strong&gt;: 평상시 트래픽&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High&lt;/strong&gt;: 일반적인 광고 트래픽&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Very High&lt;/strong&gt;: 인플루언서 마케팅, 아이돌 굿즈 판매 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ultra High&lt;/strong&gt;: 올영세일과 같은 대규모 이벤트&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 카테고리는 &lt;strong&gt;분당 결제 건수&lt;/strong&gt;와 같은 핵심 비즈니스 KPI를 기준으로 정의합니다. 저희는 E-커머스 기업이므로 실제 판매량이 가장 중요한 지표입니다.&lt;/p&gt;
&lt;p&gt;다른 업계는 각자의 핵심 KPI에 맞게 스케일링 기준을 설정해야 합니다. 각 회사의 특성을 잘 반영하는 지표를 개발하고, 그에 맞는 카테고리를 설정하는 것이 중요합니다.&lt;/p&gt;
&lt;h2 id=&quot;2단계-증설-대상-선정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EB%8B%A8%EA%B3%84-%EC%A6%9D%EC%84%A4-%EB%8C%80%EC%83%81-%EC%84%A0%EC%A0%95&quot; aria-label=&quot;2단계 증설 대상 선정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2단계: 증설 대상 선정&lt;/h2&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4911a66807d1e3fe611680b38c0e5676/09658/slide12.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMCBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB49ItDCgn/8QAGRAAAQUAAAAAAAAAAAAAAAAAAAIDERIg/9oACAEBAAEFAhyqiMf/xAAWEQADAAAAAAAAAAAAAAAAAAAAARH/2gAIAQMBAT8BahT/xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFH/8QAGRAAAgMBAAAAAAAAAAAAAAAAASEAAhIg/9oACAEBAAY/AoMgVTfP/8QAGhABAAMAAwAAAAAAAAAAAAAAAQARMSAhUf/aAAgBAQABPyELNCDLFNLfY9+H/9oADAMBAAIAAwAAABB4P//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/EAp//8QAFhEAAwAAAAAAAAAAAAAAAAAAAAER/9oACAECAQE/EGqSf//EABsQAAMAAgMAAAAAAAAAAAAAAAABERBRITFx/9oACAEBAAE/EE21o2dEa5G0IMrfCJJix//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4911a66807d1e3fe611680b38c0e5676/263a4/slide12.webp 480w,
/static/4911a66807d1e3fe611680b38c0e5676/a6361/slide12.webp 960w,
/static/4911a66807d1e3fe611680b38c0e5676/0b34d/slide12.webp 1920w,
/static/4911a66807d1e3fe611680b38c0e5676/96d48/slide12.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4911a66807d1e3fe611680b38c0e5676/a3e66/slide12.jpg 480w,
/static/4911a66807d1e3fe611680b38c0e5676/fb816/slide12.jpg 960w,
/static/4911a66807d1e3fe611680b38c0e5676/aaf92/slide12.jpg 1920w,
/static/4911a66807d1e3fe611680b38c0e5676/09658/slide12.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4911a66807d1e3fe611680b38c0e5676/aaf92/slide12.jpg&quot; alt=&quot;증설 대상 선정 기준&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8acf8b2c10a8683f30dc6fa97fbb9abd/09658/slide13.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIDBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAABzEQ0JlAn/8QAGBAAAgMAAAAAAAAAAAAAAAAAAAERIEH/2gAIAQEAAQUCxoin/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/AbGv/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABoQAQACAwEAAAAAAAAAAAAAAAEAERAxUSH/2gAIAQEAAT8hNPWEm1jNvWW9x//aAAwDAQACAAMAAAAQKD//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAwEBPxCCn//EABURAQEAAAAAAAAAAAAAAAAAABEA/9oACAECAQE/EBjf/8QAHBABAAICAwEAAAAAAAAAAAAAAQARITEQYXHh/9oACAEBAAE/ELKOocExlRor5KGUGAruXFKT3j//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8acf8b2c10a8683f30dc6fa97fbb9abd/263a4/slide13.webp 480w,
/static/8acf8b2c10a8683f30dc6fa97fbb9abd/a6361/slide13.webp 960w,
/static/8acf8b2c10a8683f30dc6fa97fbb9abd/0b34d/slide13.webp 1920w,
/static/8acf8b2c10a8683f30dc6fa97fbb9abd/96d48/slide13.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8acf8b2c10a8683f30dc6fa97fbb9abd/a3e66/slide13.jpg 480w,
/static/8acf8b2c10a8683f30dc6fa97fbb9abd/fb816/slide13.jpg 960w,
/static/8acf8b2c10a8683f30dc6fa97fbb9abd/aaf92/slide13.jpg 1920w,
/static/8acf8b2c10a8683f30dc6fa97fbb9abd/09658/slide13.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8acf8b2c10a8683f30dc6fa97fbb9abd/aaf92/slide13.jpg&quot; alt=&quot;고객 행동 패턴 분석&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;카테고리로 트래픽을 정의했다면, 다음은 &lt;strong&gt;증설할 자원을 선정&lt;/strong&gt;하는 작업입니다.&lt;/p&gt;
&lt;p&gt;어떤 자원을 증설할지 정하는 것도 중요합니다. 모든 자원을 다 증설하는 것이 아니라, 전략적으로 선택해야 합니다.&lt;/p&gt;
&lt;p&gt;예를 들어 웹/애플리케이션 서버(WAS)는 증설이 필요하지만, 백오피스 시스템은 증설하지 않아도 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증설 대상을 선정할 때 가장 중요한 것은 고객의 행동 패턴&lt;/strong&gt;입니다. 고객들이 우리 서비스에서 무엇을 원하는지 파악해야 합니다. 고객의 행동 패턴은 비즈니스와 밀접하게 연결되어 있고, 고객들이 자주 이용하는 서비스에 트래픽이 가장 많이 몰리기 때문입니다.&lt;/p&gt;
&lt;p&gt;또 하나 중요한 것은 &lt;strong&gt;데이터 기반 의사결정&lt;/strong&gt;입니다. 개발을 시작하고 서비스를 런칭할 때 &quot;서버 몇 대 필요해?&quot;라는 질문에 &quot;많이 필요합니다!&quot;라고 대답하는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;하지만 운영을 한 달, 두 달, 6개월, 1년 해보면 분명히 데이터가 축적됩니다. &lt;strong&gt;반드시 실제 데이터를 기반으로 판단&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/84d501864c2aad0cf95974b4eb479364/09658/slide14.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMCBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB5LDQmoJ//8QAGBAAAgMAAAAAAAAAAAAAAAAAAhEgIjH/2gAIAQEAAQUCddFQ/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/Aa1//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABcQAAMBAAAAAAAAAAAAAAAAAAAgMYH/2gAIAQEABj8CJq//xAAaEAEAAgMBAAAAAAAAAAAAAAABABEQIUHx/9oACAEBAAE/Iem7uANe1+MOQUx//9oADAMBAAIAAwAAABAIP//EABYRAQEBAAAAAAAAAAAAAAAAAAARAf/aAAgBAwEBPxDEU//EABYRAAMAAAAAAAAAAAAAAAAAAAABEf/aAAgBAgEBPxBqkn//xAAcEAEBAAICAwAAAAAAAAAAAAABEQAhEEExodH/2gAIAQEAAT8QKaes3U+4MMkJW5OnvHfJFlyPnTx//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/84d501864c2aad0cf95974b4eb479364/263a4/slide14.webp 480w,
/static/84d501864c2aad0cf95974b4eb479364/a6361/slide14.webp 960w,
/static/84d501864c2aad0cf95974b4eb479364/0b34d/slide14.webp 1920w,
/static/84d501864c2aad0cf95974b4eb479364/96d48/slide14.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/84d501864c2aad0cf95974b4eb479364/a3e66/slide14.jpg 480w,
/static/84d501864c2aad0cf95974b4eb479364/fb816/slide14.jpg 960w,
/static/84d501864c2aad0cf95974b4eb479364/aaf92/slide14.jpg 1920w,
/static/84d501864c2aad0cf95974b4eb479364/09658/slide14.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/84d501864c2aad0cf95974b4eb479364/aaf92/slide14.jpg&quot; alt=&quot;모니터링 도구 비교&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;데이터 수집을 위해서는 적절한 도구가 필요합니다. 마치 길이를 재려면 좋은 자가 있어야 하는 것과 같습니다.&lt;/p&gt;
&lt;p&gt;대표적으로 &lt;strong&gt;CloudWatch&lt;/strong&gt;는 기본적으로 많이 사용하실 것입니다. 저희는 &lt;strong&gt;Datadog&lt;/strong&gt;을 주로 사용하고 있습니다. 유료이지만 대시보드 구성 등이 잘 되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Grafana&lt;/strong&gt; 같은 오픈소스 툴도 좋은 선택입니다. 비용 부담은 없지만 직접 구축하고 관리해야 하므로 관리 인력이 필요할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/dc1897e34bb19a196a70824a7b8879a0/09658/slide15.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAUCAwQG/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAHfTzstjUXkf//EABkQAAMBAQEAAAAAAAAAAAAAAAECAwARE//aAAgBAQABBQLyhykpDOJA5HZCb0O//8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/Aa1//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BiP/EAB0QAAICAQUAAAAAAAAAAAAAAAABAhEDECEiM2H/2gAIAQEABj8CXCIqxRfh0VpcXTN5s//EABwQAAICAgMAAAAAAAAAAAAAAAABEVEhMUFx0f/aAAgBAQABPyFXH0KnzOkC7Va9OCh4lDuWGf/aAAwDAQACAAMAAAAQVx//xAAWEQADAAAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8QCJ//xAAWEQADAAAAAAAAAAAAAAAAAAAQEVH/2gAIAQIBAT8Qej//xAAcEAACAwADAQAAAAAAAAAAAAABEQAhMUFRYXH/2gAIAQEAAT8QuTwFouPsLhDZImjbcEjMJTHtwWwZXTjCVHwHYnoUCVk//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dc1897e34bb19a196a70824a7b8879a0/263a4/slide15.webp 480w,
/static/dc1897e34bb19a196a70824a7b8879a0/a6361/slide15.webp 960w,
/static/dc1897e34bb19a196a70824a7b8879a0/0b34d/slide15.webp 1920w,
/static/dc1897e34bb19a196a70824a7b8879a0/96d48/slide15.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dc1897e34bb19a196a70824a7b8879a0/a3e66/slide15.jpg 480w,
/static/dc1897e34bb19a196a70824a7b8879a0/fb816/slide15.jpg 960w,
/static/dc1897e34bb19a196a70824a7b8879a0/aaf92/slide15.jpg 1920w,
/static/dc1897e34bb19a196a70824a7b8879a0/09658/slide15.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dc1897e34bb19a196a70824a7b8879a0/aaf92/slide15.jpg&quot; alt=&quot;비즈니스 대시보드 구성&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;앞서 언급한 비즈니스 기반 모니터링을 위해 저희는 각종 비즈니스 지표에 대한 &lt;strong&gt;전용 대시보드&lt;/strong&gt;를 구축했습니다.&lt;/p&gt;
&lt;p&gt;이 비즈니스 대시보드를 통해 트래픽이 높은지 낮은지, 다음 상황이 어떻게 될지를 매일 모니터링하고 패턴을 파악합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;📚 참고자료&lt;/strong&gt;
&lt;a href=&quot;https://oliveyoung.tech/2024-07-05/dash-2024-slide&quot;&gt;올리브영 DASH 2024 발표자료&lt;/a&gt;에서 비즈니스 대시보드 구성 사례를 확인할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/26b725907183e86bf7ad6ca88cfdffa1/09658/slide16.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAgADBf/EABcBAAMBAAAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAeVkhohKk//EABcQAQEBAQAAAAAAAAAAAAAAABEAECH/2gAIAQEAAQUCeRM5/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQMBAT8Bh//EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAgEBPwGsf//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABgQAAMBAQAAAAAAAAAAAAAAAAABESHB/9oACAEBAAE/IaETtHopKcKTwtP/2gAMAwEAAgADAAAAEEAP/8QAFhEAAwAAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/EHDKf//EABURAQEAAAAAAAAAAAAAAAAAABAx/9oACAECAQE/EIH/xAAcEAEBAQACAwEAAAAAAAAAAAABEQAxgSFBYXH/2gAIAQEAAT8QkUF8damAr7edCMQEh8ZSoOjKVefzf//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/26b725907183e86bf7ad6ca88cfdffa1/263a4/slide16.webp 480w,
/static/26b725907183e86bf7ad6ca88cfdffa1/a6361/slide16.webp 960w,
/static/26b725907183e86bf7ad6ca88cfdffa1/0b34d/slide16.webp 1920w,
/static/26b725907183e86bf7ad6ca88cfdffa1/96d48/slide16.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/26b725907183e86bf7ad6ca88cfdffa1/a3e66/slide16.jpg 480w,
/static/26b725907183e86bf7ad6ca88cfdffa1/fb816/slide16.jpg 960w,
/static/26b725907183e86bf7ad6ca88cfdffa1/aaf92/slide16.jpg 1920w,
/static/26b725907183e86bf7ad6ca88cfdffa1/09658/slide16.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/26b725907183e86bf7ad6ca88cfdffa1/aaf92/slide16.jpg&quot; alt=&quot;증설 대상 자원 분류&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;어떤 자원을 증설해야 할까요?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;증설 필요 자원:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WAS 계열 (웹 애플리케이션 서버)&lt;/li&gt;
&lt;li&gt;웹 서버 계열&lt;/li&gt;
&lt;li&gt;DB 계열&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;증설 불필요 자원:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버리스(Serverless) 자원 - 자동 스케일링&lt;/li&gt;
&lt;li&gt;CDN 서비스 - 관리형 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 실용적 팁&lt;/strong&gt;
서버리스 자원을 많이 활용하면 직접 프로비저닝하고 증설해야 하는 자원의 양이 줄어들어 훨씬 효율적으로 운영할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;프로비저닝이 필요한 자원들에 대해서는 명확한 원칙이 있어야 합니다. 예를 들어, &apos;&lt;strong&gt;5분 이내에 무조건 증설을 시작할 수 있어야 한다&lt;/strong&gt;&apos;와 같은 원칙입니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/46af138e66baf0a3e8c48ff43e002d80/09658/slide17.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUE/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAHFVib9k8R//8QAGRAAAwADAAAAAAAAAAAAAAAAAQIDAAQU/9oACAEBAAEFAtcCleWObM1naTFH6a5Zi7//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAwEBPwFX/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAES/9oACAECAQE/Aay//8QAHhAAAQMEAwAAAAAAAAAAAAAAAAECEQMSIWExMqH/2gAIAQEABj8CRrpjRxULWTGy5uFO3hLsqf/EABsQAQACAwEBAAAAAAAAAAAAAAEAESExYaHR/9oACAEBAAE/ITBVXqW79IwAVOofWgczL8ovfBuf/9oADAMBAAIAAwAAABAXz//EABYRAQEBAAAAAAAAAAAAAAAAAAEQEf/aAAgBAwEBPxDAn//EABYRAQEBAAAAAAAAAAAAAAAAAAEAIf/aAAgBAgEBPxAK5Av/xAAcEAEAAgIDAQAAAAAAAAAAAAABABEhMUFx0eH/2gAIAQEAAT8QQ0Ga8DGolTJ4rxDVRgvkmdypCgoO+4gEF7v5y5qgaBrqf//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/46af138e66baf0a3e8c48ff43e002d80/263a4/slide17.webp 480w,
/static/46af138e66baf0a3e8c48ff43e002d80/a6361/slide17.webp 960w,
/static/46af138e66baf0a3e8c48ff43e002d80/0b34d/slide17.webp 1920w,
/static/46af138e66baf0a3e8c48ff43e002d80/96d48/slide17.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/46af138e66baf0a3e8c48ff43e002d80/a3e66/slide17.jpg 480w,
/static/46af138e66baf0a3e8c48ff43e002d80/fb816/slide17.jpg 960w,
/static/46af138e66baf0a3e8c48ff43e002d80/aaf92/slide17.jpg 1920w,
/static/46af138e66baf0a3e8c48ff43e002d80/09658/slide17.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/46af138e66baf0a3e8c48ff43e002d80/aaf92/slide17.jpg&quot; alt=&quot;DB 커넥션 풀 모니터링 사례&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;특별히 주의해서 모니터링해야 할 지표가 있습니다.&lt;/p&gt;
&lt;p&gt;특히 &lt;strong&gt;Oracle DB를 사용하는 경우&lt;/strong&gt;, WAS의 DB 커넥션 풀(Connection Pool)을 주의 깊게 모니터링해야 합니다. 커넥션 풀이 가득 차면 시스템이 즉시 다운됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 주의사항&lt;/strong&gt;
DB 커넥션 풀이 가득 차면 바로 장애로 이어지며, 실제로 이로 인한 장애 비율이 매우 높습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;시스템 CPU만 모니터링할 것이 아니라, 장애를 유발할 수 있는 다른 핵심 지표들도 선정해서 경험적으로 데이터를 축적하고, 그에 맞춰 증설 대상을 정의해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;3단계-증설-방법-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EB%8B%A8%EA%B3%84-%EC%A6%9D%EC%84%A4-%EB%B0%A9%EB%B2%95-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;3단계 증설 방법 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3단계: 증설 방법 구현&lt;/h2&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9875e1b1f17fbb33f6cd61ed3cd29c34/09658/slide19.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIDBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAABy0U0JlAn/8QAGBAAAgMAAAAAAAAAAAAAAAAAAAECESD/2gAIAQEAAQUCJIrH/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/AbGv/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABoQAQABBQAAAAAAAAAAAAAAAAEAEBEhMVH/2gAIAQEAAT8hNGWYdrRd6y71p//aAAwDAQACAAMAAAAQ+D//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAwEBPxCCn//EABURAQEAAAAAAAAAAAAAAAAAABEA/9oACAECAQE/EBjf/8QAGxABAAMAAwEAAAAAAAAAAAAAAQARIUFR4ZH/2gAIAQEAAT8QY3Fhx5LilDsjuUGfSHuR1t1n/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9875e1b1f17fbb33f6cd61ed3cd29c34/263a4/slide19.webp 480w,
/static/9875e1b1f17fbb33f6cd61ed3cd29c34/a6361/slide19.webp 960w,
/static/9875e1b1f17fbb33f6cd61ed3cd29c34/0b34d/slide19.webp 1920w,
/static/9875e1b1f17fbb33f6cd61ed3cd29c34/96d48/slide19.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9875e1b1f17fbb33f6cd61ed3cd29c34/a3e66/slide19.jpg 480w,
/static/9875e1b1f17fbb33f6cd61ed3cd29c34/fb816/slide19.jpg 960w,
/static/9875e1b1f17fbb33f6cd61ed3cd29c34/aaf92/slide19.jpg 1920w,
/static/9875e1b1f17fbb33f6cd61ed3cd29c34/09658/slide19.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9875e1b1f17fbb33f6cd61ed3cd29c34/aaf92/slide19.jpg&quot; alt=&quot;증설 방법 구현 원칙&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이제 &apos;어떻게&apos; 증설할지 구현 방법을 찾아야 합니다.&lt;/p&gt;
&lt;p&gt;효과적인 오퍼레이션을 위한 핵심 원칙:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기존 도구 활용&lt;/strong&gt;: 팀에서 이미 사용하던 도구를 잘 활용&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;멀티 플랫폼&lt;/strong&gt;: PC와 모바일 양쪽 모두 지원&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;간편성&lt;/strong&gt;: 복잡한 도구보다는 간편한 도구 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AWS 콘솔도 좋지만, 앞서 말한 간편함의 원칙에는 다소 부족하고 모바일에서 사용하기 어려운 측면이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4b33d643fe7680dd711d8d463f6225fd/09658/slide20.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAQBAv/EABUBAQEAAAAAAAAAAAAAAAAAAAED/9oADAMBAAIQAxAAAAHmaXKAB//EABwQAAICAgMAAAAAAAAAAAAAAAIDAQQAEhETFP/aAAgBAQABBQLy8VqqIdL1dTdy1EyDCMin/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEx/9oACAEDAQE/AcV//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BiP/EABsQAAMAAgMAAAAAAAAAAAAAAAABEQISIkFh/9oACAEBAAY/Am9vR1yDwXRNnDi4XLJs/8QAHBABAQEAAgMBAAAAAAAAAAAAAREAIUExUWGR/9oACAEBAAE/IZTtDxCfurL0Qu52J2cDMPW8ZiuvmrgfXf/aAAwDAQACAAMAAAAQo8//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAwEBPxBBR//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAECAQE/EFJP/8QAGRABAQEBAQEAAAAAAAAAAAAAAREAITFR/9oACAEBAAE/EKTIoYksnrWZ54Kt9uBBOgReZXJEl8fJnjNxXNLJkGkPm//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4b33d643fe7680dd711d8d463f6225fd/263a4/slide20.webp 480w,
/static/4b33d643fe7680dd711d8d463f6225fd/a6361/slide20.webp 960w,
/static/4b33d643fe7680dd711d8d463f6225fd/0b34d/slide20.webp 1920w,
/static/4b33d643fe7680dd711d8d463f6225fd/96d48/slide20.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4b33d643fe7680dd711d8d463f6225fd/a3e66/slide20.jpg 480w,
/static/4b33d643fe7680dd711d8d463f6225fd/fb816/slide20.jpg 960w,
/static/4b33d643fe7680dd711d8d463f6225fd/aaf92/slide20.jpg 1920w,
/static/4b33d643fe7680dd711d8d463f6225fd/09658/slide20.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4b33d643fe7680dd711d8d463f6225fd/aaf92/slide20.jpg&quot; alt=&quot;Slack과 Google Calendar를 활용한 자동화&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;저희는 &lt;strong&gt;Slack과 Google Calendar&lt;/strong&gt;를 활용합니다. 대부분 회사에서 사용하고 있는 도구들입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google Calendar 활용:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;매달 발생하는 이벤트 스케줄 등록&lt;/li&gt;
&lt;li&gt;&apos;(H)&apos; 프리픽스와 함께 이벤트 등록&lt;/li&gt;
&lt;li&gt;프리픽스를 보고 자동으로 해당 레벨로 증설&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Slack 활용:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예정에 없던 갑작스러운 증설 필요 시&lt;/li&gt;
&lt;li&gt;슬랙봇을 이용해서 &apos;증설 카테고리: (V)Very High&apos; 입력&lt;/li&gt;
&lt;li&gt;바로 증설 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;4단계-위험-대응&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4%EB%8B%A8%EA%B3%84-%EC%9C%84%ED%97%98-%EB%8C%80%EC%9D%91&quot; aria-label=&quot;4단계 위험 대응 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4단계: 위험 대응&lt;/h2&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a2c9c9988c14d25adda2e7c533bdc222/09658/slide21.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMCBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB41YtDCgn/8QAGhAAAQUBAAAAAAAAAAAAAAAAAAEDERIgMf/aAAgBAQABBQLo5VSMf//EABYRAAMAAAAAAAAAAAAAAAAAAAABEf/aAAgBAwEBPwFqFP/EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/AUf/xAAYEAEBAAMAAAAAAAAAAAAAAAABIQASIP/aAAgBAQAGPwLDUCW8/wD/xAAaEAEAAgMBAAAAAAAAAAAAAAABETEAIFFx/9oACAEBAAE/IQ6D3BXmFkur/9oADAMBAAIAAwAAABD4P//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/EAp//8QAFhEAAwAAAAAAAAAAAAAAAAAAAAER/9oACAECAQE/EGqSf//EABoQAQADAAMAAAAAAAAAAAAAAAEAESEQYXH/2gAIAQEAAT8QKw+xA1S+jtR3KAYcf//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a2c9c9988c14d25adda2e7c533bdc222/263a4/slide21.webp 480w,
/static/a2c9c9988c14d25adda2e7c533bdc222/a6361/slide21.webp 960w,
/static/a2c9c9988c14d25adda2e7c533bdc222/0b34d/slide21.webp 1920w,
/static/a2c9c9988c14d25adda2e7c533bdc222/96d48/slide21.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a2c9c9988c14d25adda2e7c533bdc222/a3e66/slide21.jpg 480w,
/static/a2c9c9988c14d25adda2e7c533bdc222/fb816/slide21.jpg 960w,
/static/a2c9c9988c14d25adda2e7c533bdc222/aaf92/slide21.jpg 1920w,
/static/a2c9c9988c14d25adda2e7c533bdc222/09658/slide21.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a2c9c9988c14d25adda2e7c533bdc222/aaf92/slide21.jpg&quot; alt=&quot;위험 관리의 필요성&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e078c22e94818765738400d9d5aede97/09658/slide22.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBf/EABYBAQEBAAAAAAAAAAAAAAAAAAECA//aAAwDAQACEAMQAAAB5cQ0mGgP/8QAFxABAAMAAAAAAAAAAAAAAAAAAAERIP/aAAgBAQABBQJKsf/EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAwEBPwGxr//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/AUf/xAAUEAEAAAAAAAAAAAAAAAAAAAAg/9oACAEBAAY/Al//xAAZEAEAAgMAAAAAAAAAAAAAAAABADEgIYH/2gAIAQEAAT8hHVwjavcX/9oADAMBAAIAAwAAABB8P//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/EIKf/8QAFREBAQAAAAAAAAAAAAAAAAAAEQD/2gAIAQIBAT8QGN//xAAaEAEAAgMBAAAAAAAAAAAAAAABABEQIUGh/9oACAEBAAE/ELRfJ0hngGO8oBhj/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e078c22e94818765738400d9d5aede97/263a4/slide22.webp 480w,
/static/e078c22e94818765738400d9d5aede97/a6361/slide22.webp 960w,
/static/e078c22e94818765738400d9d5aede97/0b34d/slide22.webp 1920w,
/static/e078c22e94818765738400d9d5aede97/96d48/slide22.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e078c22e94818765738400d9d5aede97/a3e66/slide22.jpg 480w,
/static/e078c22e94818765738400d9d5aede97/fb816/slide22.jpg 960w,
/static/e078c22e94818765738400d9d5aede97/aaf92/slide22.jpg 1920w,
/static/e078c22e94818765738400d9d5aede97/09658/slide22.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e078c22e94818765738400d9d5aede97/aaf92/slide22.jpg&quot; alt=&quot;위험 대응 전략&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;앞의 3단계를 완료했다고 완벽한 것은 아닙니다. &lt;strong&gt;위험은 항상 존재&lt;/strong&gt;하며, 이에 대응하는 자세가 필요합니다.&lt;/p&gt;
&lt;p&gt;저희가 모든 이벤트를 예측할 수는 없습니다. 갑자기 트래픽이 폭증하는 경우도 있습니다. 따라서 &lt;strong&gt;얼마나 빠르고 기민하게 대응할 수 있는가&lt;/strong&gt;가 핵심입니다.&lt;/p&gt;
&lt;p&gt;위험성을 감지하는 방법은 앞서 강조한 &lt;strong&gt;체계적인 모니터링&lt;/strong&gt;과 직결됩니다. 위험이 감지되었을 때 신속한 증설이 가능해야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 대응 원칙&lt;/strong&gt;
장애를 겪는 것보다는 과잉 대응으로 장애를 예방하는 것이 더 나은 선택입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/48cb421099665b682405b231b792aa70/09658/slide23.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMFAgb/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAv/aAAwDAQACEAMQAAABy+Dftz48T//EABsQAAICAwEAAAAAAAAAAAAAAAIDAQQAEhMj/9oACAEBAAEFAgOroyuDEkuRLElPFs+v/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8Biv/EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAgEBPwGsf//EAB0QAAEDBQEAAAAAAAAAAAAAAAEAAgMQESExcRL/2gAIAQEABj8CHppurwx5Osog7FG8Tur/xAAcEAEAAwACAwAAAAAAAAAAAAABABEhEDFRgeH/2gAIAQEAAT8h07qDj3LuMlP1CipKeBjnoRl3yn//2gAMAwEAAgADAAAAEAPP/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEB/9oACAEDAQE/ENhT/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEB/9oACAECAQE/EMRL/8QAGxABAAMBAAMAAAAAAAAAAAAAAQARITFBUcH/2gAIAQEAAT8QJrSTQG1sKlBTWXvfSWdwAbBIFadgiaBDb5KlLfET/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/48cb421099665b682405b231b792aa70/263a4/slide23.webp 480w,
/static/48cb421099665b682405b231b792aa70/a6361/slide23.webp 960w,
/static/48cb421099665b682405b231b792aa70/0b34d/slide23.webp 1920w,
/static/48cb421099665b682405b231b792aa70/96d48/slide23.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/48cb421099665b682405b231b792aa70/a3e66/slide23.jpg 480w,
/static/48cb421099665b682405b231b792aa70/fb816/slide23.jpg 960w,
/static/48cb421099665b682405b231b792aa70/aaf92/slide23.jpg 1920w,
/static/48cb421099665b682405b231b792aa70/09658/slide23.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/48cb421099665b682405b231b792aa70/aaf92/slide23.jpg&quot; alt=&quot;신속한 위험 대응 사례&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;위험이 감지되면 빠르게 대응해야 합니다. &lt;strong&gt;장애 발생 후 대응보다 장애 예방이 우선&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;예를 들어, DB 커넥션 풀이 갑자기 증가하면 즉시 모든 가용 자원을 투입하여 증설합니다. 이는 이러한 상황이 90% 이상의 확률로 장애로 이어지기 때문입니다.&lt;/p&gt;
&lt;p&gt;증설 후에 상황이 안정되면 다시 감축할 수 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;구현의-단순함&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B5%AC%ED%98%84%EC%9D%98-%EB%8B%A8%EC%88%9C%ED%95%A8&quot; aria-label=&quot;구현의 단순함 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;구현의 단순함&lt;/h1&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/276c743a3a5586f710dbf484f051639b/09658/slide24.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAED/9oADAMBAAIQAxAAAAHJlKhJYH//xAAYEAADAQEAAAAAAAAAAAAAAAABESEAEP/aAAgBAQABBQJxYi9//8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/AbGv/8QAFREBAQAAAAAAAAAAAAAAAAAAABL/2gAIAQIBAT8BS//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EAB0QAAICAQUAAAAAAAAAAAAAAAERACEQMUFRYXH/2gAIAQEAAT8hFOsLGy/IFk13Nlxn/9oADAMBAAIAAwAAABC4L//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/EIKf/8QAFhEAAwAAAAAAAAAAAAAAAAAAAAER/9oACAECAQE/EGqQf//EABsQAQEBAAIDAAAAAAAAAAAAAAERACExUXGB/9oACAEBAAE/EAQSo81+Y2e3PJxCIrg7HvKpSyoXrGeWu//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/276c743a3a5586f710dbf484f051639b/263a4/slide24.webp 480w,
/static/276c743a3a5586f710dbf484f051639b/a6361/slide24.webp 960w,
/static/276c743a3a5586f710dbf484f051639b/0b34d/slide24.webp 1920w,
/static/276c743a3a5586f710dbf484f051639b/96d48/slide24.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/276c743a3a5586f710dbf484f051639b/a3e66/slide24.jpg 480w,
/static/276c743a3a5586f710dbf484f051639b/fb816/slide24.jpg 960w,
/static/276c743a3a5586f710dbf484f051639b/aaf92/slide24.jpg 1920w,
/static/276c743a3a5586f710dbf484f051639b/09658/slide24.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/276c743a3a5586f710dbf484f051639b/aaf92/slide24.jpg&quot; alt=&quot;구현 아키텍처의 단순함&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;사실 이 전체 시스템 구성은 매우 단순합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;구현 복잡도:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발 기간: 2-3일 정도&lt;/li&gt;
&lt;li&gt;기술 스택: AWS Lambda, SQS, 기존 Chat/Calendar API&lt;/li&gt;
&lt;li&gt;핵심: 여러분이 이미 가지고 있는 경험 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;캘린더나 슬랙에서 이벤트를 받아 SQS 같은 큐에 넣고, Lambda 함수가 이를 처리해서 스케줄링하는 구조입니다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;strong&gt;앞서 설명한 전략적 사고과정이 기술적 구현보다 훨씬 중요&lt;/strong&gt;합니다. 어떻게 트래픽을 분류하고, 무엇을 모니터링하며, 어떤 기준으로 증설할지 정하는 것이 핵심입니다.&lt;/p&gt;
&lt;h1 id=&quot;sre의-핵심-원칙&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#sre%EC%9D%98-%ED%95%B5%EC%8B%AC-%EC%9B%90%EC%B9%99&quot; aria-label=&quot;sre의 핵심 원칙 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;SRE의 핵심 원칙&lt;/h1&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/29266b60c5f183b51f9a070ebb917b13/09658/slide25.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAEDBf/EABYBAQEBAAAAAAAAAAAAAAAAAAIBA//aAAwDAQACEAMQAAABzoNamRQM/8QAGRABAAIDAAAAAAAAAAAAAAAAAQARAhAS/9oACAEBAAEFAikoYk6yIa//xAAXEQEAAwAAAAAAAAAAAAAAAAAAAREh/9oACAEDAQE/AZxb/8QAFhEBAQEAAAAAAAAAAAAAAAAAABES/9oACAECAQE/AYy//8QAFxAAAwEAAAAAAAAAAAAAAAAAARAhsf/aAAgBAQAGPwK64X//xAAbEAACAgMBAAAAAAAAAAAAAAAAAREhMUFRgf/aAAgBAQABPyHWpC7PC2hKQ1IwGf/aAAwDAQACAAMAAAAQ9/8A/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/9oACAEDAQE/EINP/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEh/9oACAECAQE/ENIf/8QAGxABAQEBAQADAAAAAAAAAAAAAREAMSFBYZH/2gAIAQEAAT8QkesRqH8mMad/CmAZU++6bk4D5gNPcC7/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/29266b60c5f183b51f9a070ebb917b13/263a4/slide25.webp 480w,
/static/29266b60c5f183b51f9a070ebb917b13/a6361/slide25.webp 960w,
/static/29266b60c5f183b51f9a070ebb917b13/0b34d/slide25.webp 1920w,
/static/29266b60c5f183b51f9a070ebb917b13/96d48/slide25.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/29266b60c5f183b51f9a070ebb917b13/a3e66/slide25.jpg 480w,
/static/29266b60c5f183b51f9a070ebb917b13/fb816/slide25.jpg 960w,
/static/29266b60c5f183b51f9a070ebb917b13/aaf92/slide25.jpg 1920w,
/static/29266b60c5f183b51f9a070ebb917b13/09658/slide25.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/29266b60c5f183b51f9a070ebb917b13/aaf92/slide25.jpg&quot; alt=&quot;SRE 핵심 원칙&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;SRE에는 &lt;a href=&quot;https://medium.com/@jsakyle/the-foundational-pillars-of-site-reliability-engineering-d2249b192538&quot;&gt;업무를 지탱하는 몇 가지 기본 원칙&lt;/a&gt;들이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SRE의 3대 핵심 영역:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가용성(Availability)&lt;/li&gt;
&lt;li&gt;장애 대응(Incident Response)&lt;/li&gt;
&lt;li&gt;관찰 가능성(Observability)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그중에서도 &lt;strong&gt;가용성을 높이려면 &apos;관찰&apos;을 잘해야 한다&lt;/strong&gt;고 생각합니다. 앞서 데이터 기반 의사결정의 중요성을 강조한 것과 같은 맥락입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🔍 핵심 인사이트&lt;/strong&gt;
관찰하지 못하면 가용성을 확보할 수 없습니다. 적절한 관찰은 우리에게 더 높은 가용성을 보장해 줍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;옵저버빌리티(Observability)가 확보되어야 자동화도 제대로 할 수 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;비즈니스-중심-모니터링bom&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EC%A4%91%EC%8B%AC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81bom&quot; aria-label=&quot;비즈니스 중심 모니터링bom permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;비즈니스 중심 모니터링(BOM)&lt;/h1&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e73a8db4b61d13a6eeda5e7d60deb1ad/09658/slide26.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAIBAwX/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAcmazSUHA//EABkQAAEFAAAAAAAAAAAAAAAAAAEAEBEhQv/aAAgBAQABBQLMIi3/AP/EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAwEBPwGxr//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/AUf/xAAWEAEBAQAAAAAAAAAAAAAAAABBACD/2gAIAQEABj8CZz//xAAaEAACAgMAAAAAAAAAAAAAAAAAIQEQEUFR/9oACAEBAAE/IWwaKDRme1//2gAMAwEAAgADAAAAEEw//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8Qgp//xAAWEQADAAAAAAAAAAAAAAAAAAAAARH/2gAIAQIBAT8QapJ//8QAHBABAAIDAAMAAAAAAAAAAAAAAQAhETFRQWGR/9oACAEBAAE/ENTQOnFPjccXDY5UfLOVOx9mtXDo+xtuf//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e73a8db4b61d13a6eeda5e7d60deb1ad/263a4/slide26.webp 480w,
/static/e73a8db4b61d13a6eeda5e7d60deb1ad/a6361/slide26.webp 960w,
/static/e73a8db4b61d13a6eeda5e7d60deb1ad/0b34d/slide26.webp 1920w,
/static/e73a8db4b61d13a6eeda5e7d60deb1ad/96d48/slide26.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e73a8db4b61d13a6eeda5e7d60deb1ad/a3e66/slide26.jpg 480w,
/static/e73a8db4b61d13a6eeda5e7d60deb1ad/fb816/slide26.jpg 960w,
/static/e73a8db4b61d13a6eeda5e7d60deb1ad/aaf92/slide26.jpg 1920w,
/static/e73a8db4b61d13a6eeda5e7d60deb1ad/09658/slide26.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e73a8db4b61d13a6eeda5e7d60deb1ad/aaf92/slide26.jpg&quot; alt=&quot;비즈니스 중심 모니터링의 필요성&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&apos;관찰을 하자&apos;고 하면 CPU와 메모리만 보는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;&apos;CPU랑 메모리만 잘 보면 되는 거 아니야?&apos;라고 생각하시기보다, &lt;strong&gt;한 걸음 더 나아가는 것&lt;/strong&gt;을 권장합니다. 단순히 시스템 지표만 보는 것이 아니라, 우리 비즈니스가 운영 시스템에 어떤 영향을 미치는지 함께 살펴보는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;비즈니스 지표는 우리 시스템이 앞으로 어떻게 나아가야 할지 알려주는 나침반&lt;/strong&gt;과 같은 역할을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;br/&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bc728c4f9dbcd0916b629f734ad96391/09658/slide27.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAEDAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgP/2gAMAwEAAhADEAAAAeM8vQmUCf/EABcQAAMBAAAAAAAAAAAAAAAAAAABESD/2gAIAQEAAQUCGiY//8QAFhEBAQEAAAAAAAAAAAAAAAAAABEx/9oACAEDAQE/AcV//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABkQAAMAAwAAAAAAAAAAAAAAAAEQEQAhMf/aAAgBAQABPyEdwU1GpX//2gAMAwEAAgADAAAAEOg//8QAFxEBAQEBAAAAAAAAAAAAAAAAAQARMf/aAAgBAwEBPxBHi1f/xAAWEQEBAQAAAAAAAAAAAAAAAAABABH/2gAIAQIBAT8QQbN//8QAGRABAAMBAQAAAAAAAAAAAAAAAQARITFh/9oACAEBAAE/EGUsHxlXrYKnTyO5QDCoLaOTus//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bc728c4f9dbcd0916b629f734ad96391/263a4/slide27.webp 480w,
/static/bc728c4f9dbcd0916b629f734ad96391/a6361/slide27.webp 960w,
/static/bc728c4f9dbcd0916b629f734ad96391/0b34d/slide27.webp 1920w,
/static/bc728c4f9dbcd0916b629f734ad96391/96d48/slide27.webp 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bc728c4f9dbcd0916b629f734ad96391/a3e66/slide27.jpg 480w,
/static/bc728c4f9dbcd0916b629f734ad96391/fb816/slide27.jpg 960w,
/static/bc728c4f9dbcd0916b629f734ad96391/aaf92/slide27.jpg 1920w,
/static/bc728c4f9dbcd0916b629f734ad96391/09658/slide27.jpg 2048w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bc728c4f9dbcd0916b629f734ad96391/aaf92/slide27.jpg&quot; alt=&quot;BOM 개념 정의&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;이러한 접근법을 &quot;&lt;strong&gt;비즈니스 오리엔티드 모니터링(Business Oriented Monitoring, BOM)&lt;/strong&gt;&quot;이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BOM + Automation = 안정적인 시스템&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여러분들이 더 좋고 안정적인 시스템을 구축하는 데 이러한 접근법을 활용하시기를 추천합니다.&lt;/p&gt;
&lt;h1 id=&quot;오늘부터-시작할-수-있는-3가지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%EB%8A%98%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-3%EA%B0%80%EC%A7%80&quot; aria-label=&quot;오늘부터 시작할 수 있는 3가지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오늘부터 시작할 수 있는 3가지&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;트래픽 카테고리 정의&lt;/strong&gt;: 우리 서비스의 5단계 트래픽 레벨 설정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 지표 선정&lt;/strong&gt;: 주요 KPI 기반 모니터링 지표 정의&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;간단한 자동화&lt;/strong&gt;: Slack/Calendar 연동 스케일링 봇 구축&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-07-05/dash-2024-slide&quot;&gt;올리브영 DASH 2024 발표자료&lt;/a&gt; - 비즈니스 대시보드 구성 사례&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@jsakyle/the-foundational-pillars-of-site-reliability-engineering-d2249b192538&quot;&gt;SRE 기본 원칙 가이드&lt;/a&gt; - Site Reliability Engineering의 핵심 개념&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line.html&quot;&gt;Amazon Q Developer CLI 가이드&lt;/a&gt; - AI 기반 개발 도구 활용법&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;📝 이 포스트는 Amazon Q Developer CLI를 활용해 작성되었으며, Claude 4를 통한 리뷰로 마무리했습니다.&lt;/p&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[iOS 개발자가 올리브영 QA 엔지니어가 되기까지, 5개월간의 리얼 온보딩]]></title><description><![CDATA[iOS 개발자가 올리브영 QA 엔지니어가 되기까지, 5개월간의 리얼 온보딩 안녕하세요! 25년 1월에 CJ올리브영 QA팀 QA 엔지니어로 합류한 구망🐧 입니다. "QA…]]></description><link>https://oliveyoung.tech/2025-06-19/journey-to-joining-oliveyoung-qa/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-06-19/journey-to-joining-oliveyoung-qa/</guid><pubDate>Thu, 19 Jun 2025 15:30:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;ios-개발자가-올리브영-qa-엔지니어가-되기까지-5개월간의-리얼-온보딩&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ios-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-qa-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EA%B0%80-%EB%90%98%EA%B8%B0%EA%B9%8C%EC%A7%80-5%EA%B0%9C%EC%9B%94%EA%B0%84%EC%9D%98-%EB%A6%AC%EC%96%BC-%EC%98%A8%EB%B3%B4%EB%94%A9&quot; aria-label=&quot;ios 개발자가 올리브영 qa 엔지니어가 되기까지 5개월간의 리얼 온보딩 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;iOS 개발자가 올리브영 QA 엔지니어가 되기까지, 5개월간의 리얼 온보딩&lt;/h2&gt;
&lt;p&gt;안녕하세요! 25년 1월에 CJ올리브영 QA팀 QA 엔지니어로 합류한 구망🐧 입니다.&lt;/p&gt;
&lt;p&gt;&quot;QA팀에 신입이 있다고?&quot; 아마 많은 분들이 고개를 갸우뚱하거나, 한편으로는 놀라실지도 모르겠습니다. 사실 QA 직무는 서비스에 대한 깊은 이해와 다채로운 테스트 경험을 요구하기에, 대부분의 기업에서 경력직 채용이 일반적인 것으로 알고 있습니다. 신입 QA 엔지니어를 육성하는 데 따르는 시간과 비용 부담 탓에, 신입 채용 자체가 흔치 않은 것이 업계의 현실입니다. 🥲&lt;/p&gt;
&lt;p&gt;하지만 올리브영은 단순히 버그를 찾아내는 &apos;검사자&apos;의 역할을 넘어, &lt;strong&gt;개발의 시작부터 고객 경험의 마지막까지 품질을 &apos;설계&apos;하고 &apos;책임지는&apos; QA 엔지니어&lt;/strong&gt;의 가치를 높이 평가합니다. 그리고 그 가치에 투자하며, 잠재력 있는 신입 인재를 육성하는 데도 적극적입니다!&lt;/p&gt;
&lt;p&gt;코드 한 줄이 미치는 영향력을 고민하던 개발자였던 제가, 이제는 올리브영의 서비스 품질을 책임지는 &apos;신입 QA 엔지니어&apos;라는 새로운 도전을 시작하게 된 이유도 바로 여기에 있습니다.&lt;/p&gt;
&lt;p&gt;올리브영에 합류한 지 벌써 5개월. 개발 백그라운드를 가진 제가 어떻게 올리브영 QA팀의 차별화된 온보딩과 실무 문화에 스며들며 성장하고 있는지, 그 생생한 여정을 여러분께 소개하려 합니다. 🙌 국내 대표 H&amp;#x26;B 트렌드 리딩 플랫폼을 넘어 글로벌로 나아가는 올리브영에서, 품질 최전선을 지키는 QA 엔지니어의 역할과 매력은 무엇인지, 저의 솔직한 경험담을 통해 함께 알아가 보시죠!&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;개발자였던 내가 왜, QA 엔지니어를 선택하게 되었을까?&lt;/li&gt;
&lt;li&gt;왜 하필 CJ올리브영 QA팀이었을까?&lt;/li&gt;
&lt;li&gt;입사 후 깨달은 QA의 진짜 세계: 이론과 실무의 간극을 넘어&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;꼼꼼함만으로는 부족했던 세계&lt;/li&gt;
&lt;li&gt;온보딩을 통해 배운 것들 – 문서로 체계화된 학습 여정&lt;/li&gt;
&lt;li&gt;책에서 본 QA vs 실제 QA – 일하는 방식의 진짜 차이&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;첫 2주 온보딩 리얼 후기&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;개발자였던-내가-왜-qa-엔지니어를-선택하게-되었을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%98%80%EB%8D%98-%EB%82%B4%EA%B0%80-%EC%99%9C-qa-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%98%EA%B2%8C-%EB%90%98%EC%97%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;개발자였던 내가 왜 qa 엔지니어를 선택하게 되었을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발자였던 내가 왜, QA 엔지니어를 선택하게 되었을까?&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;면접에서나 일상에서나 제가 최근에 가장 많이 들은 질문인데요!
&lt;/br&gt;&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #000000; padding-left: 16px; font-style: italic;&quot;&gt;
  &lt;span style=&quot;color:rgb(20, 18, 18); font-weight: bold;&quot;&gt;&quot;개발자에서 QA 엔지니어로 전환하는 이유는 무엇인가요?&quot;
&lt;/blockquote&gt;
&lt;/br&gt;
&lt;p&gt;가장 큰 이유는 2가지 입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;정적 테스팅: 리뷰의 중요성을 깨닫다&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;내가 가장 뿌듯한 순간: 남들이 발견하지 못한 것을 찾아낼 때&lt;/strong&gt; 🦅👀&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-정적-테스팅-리뷰의-중요성을-깨닫다-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A0%95%EC%A0%81-%ED%85%8C%EC%8A%A4%ED%8C%85-%EB%A6%AC%EB%B7%B0%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1%EC%9D%84-%EA%B9%A8%EB%8B%AB%EB%8B%A4-&quot; aria-label=&quot; 정적 테스팅 리뷰의 중요성을 깨닫다  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 정적 테스팅: 리뷰의 중요성을 깨닫다 ]&lt;/h3&gt;
&lt;center&gt;
    &lt;div style=&quot;display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 447px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c92189ac35d5236cc4f5df8140560309/74a2b/spec_review.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 78.52348993288591%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEdElEQVR42h2UeVDUZRjHd8ZpJo/pj5qxJo8xzFJLBwNRZ5ppEi80U0wKJTOLxPtG07S0ISsVUQktYXBAJQbWXYKFBRZYlmV32ZNDWC532YPlkoUUHfuZ+emFP56Zd9555/k+3+N5ZdYGp3QxJVWSy5XSPZdXCgT6pZGRJ9KDB4/HCpBqDDbpbrtb8vX2SXUWi2SqM0vqcq2Ul6eQLl5IlkpKSiStVisNBoclWcc9N4OD9wkEAuTn5aPIl6Ov0eP1Behy+0lMPElY+BISj53A4/Vis1rp6OggOyuLznse/hOIz57D03+f81QcZD7xyGAwUFlZRXV1Ned+PU9trRG3y0vC9v2EvBnKlGkhhM5fQHOzU1Qz7e3taMrLsJotPBIkujx+urq6CPi8yFwuF62trXhF44yMDGQyGXt37cSk0xG28H1iYneya88JTNZ6/IEePJ4u/H4/NQK8rKSItrZ2uru7sdpsNN11IktNTaO3txe3201DQwPjx7/IuHEvEBm5ilVRH7NrazyXz5zG5XLibHVisVhxOOpx1Ncjz8+js7MDo9FIqVpNZbUe2fr1G8YajaKOTrsoYjEH4uM5kvg9O/ccpNFhp/BWOtoKpaBZTFVVJT1Cb6NehzInUwzTIyYM0NMTYPXqtchmvTWXTZviuHNHQerVP4iK+ojD278hYVsCZ5N+pFpXRdaNdOS5mRTIb6MqUAqNTUSvXcPsWTOx2e3Ui2mHgkGiN2xElpWjYPJrM5g46WWSr1wnGPwbrz/A2Z+TuZh8mZu3lVz7PZvbN3MouJOPSqkgOjp2TOvRSk9PFzIIc0ZGOHj4GLIyrYlp099m9pz5NDW34fX4+C0tg/37j3L02zPs3neMQ0dOkrDjAKdOJbHti21Mf2MuEye8xJHjp1FX6jGbzUI2Bzm5uchy5ComTHqFrYJiS0vrmJ6ZmZlsjt3Mh0tXErHkAyKXrWZ11FrWRK1j4YJF4n4V69ZvpERdTo2uFo2mCrujkbvNLcjUlQZmvxvO3n2HGRi4T/9APz6fj5SUFLbEfclnMXEsX7acefPmM3fOO6xYEUWdyYrdbEer0Yj8ainX6DEY7Tga20QO/QPEfv4VJ8Qm9Pf1jcVhlMK588lsifmU3VviWLNyOcuWRhISMpPwsAgBliai46SxqUO4rqewqBSFohCTAJF1egJ8ErOZosIievv7RXA9DA0NkZT0E1OnTCUiPIzFEQt5LzSUGdOn8fqrk/k6fgdGkwNDrYU6sxjAYqfd3Y2vdxiZsriCfYeOi6TbsYg97ezsHMtjgVLJL2INtTVm1OU6YdR1vjv5A1dSr3E97SqXLpxHVVKJy99PYPAx/cP/0NQqVi8zO5c6e8sYYp78L9HUJkLaQ7GqWNBvQaFUcSP7T+qbnLgEG3/vIJqyCi6lXEJXa8PVPUSV3kqFzoJGWydczlPQG3yISlVKkUpNrdFMtfht9AYLpaWllJZrsDW0ikZB4WQDRfm3xI/joC/4iOGHT3j05BmuLh+W+ja674/wP4kfqkLGH98HAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c92189ac35d5236cc4f5df8140560309/17ebd/spec_review.webp 447w&quot; sizes=&quot;(max-width: 447px) 100vw, 447px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c92189ac35d5236cc4f5df8140560309/74a2b/spec_review.png 447w&quot; sizes=&quot;(max-width: 447px) 100vw, 447px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c92189ac35d5236cc4f5df8140560309/74a2b/spec_review.png&quot; alt=&quot;spec review&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;기획리뷰 중인 펭귄들 - 오늘도 ???가 안 된다고 했다.&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;저는 약 1년 반 정도 iOS를 개발한 경험이 있습니다. 개발자로 일할 때는 단순히 &apos;개발만 잘하면 되지&apos;라는 생각이 강했습니다.
보통 신규 기능이나 기존 기능이 변경될 때, 개발 단계 전에 기획서를 리뷰하는 시간이 있는데요.
처음에는 마감 일정을 지키기 위해 내 개발 업무에만 집중하는 것이 최선이라 생각했고, 다 같이 모여 리뷰하는 시간이 오히려 개발 시간을 뺏는 것 같다고 생각했습니다. 기획서에 명시된 대로만 구현하면 된다는 생각으로, 기획에서 놓친 부분이나 잘못된 부분에 대해 언급은 했지만, &lt;code class=&quot;language-text&quot;&gt;기획자의 생각이 있겠지&lt;/code&gt; 라며 넘겨짚기도 했습니다.&lt;/p&gt;
&lt;p&gt;하지만 이러한 태도는 결국 기획 의도와 다른 개발로 이어졌고, 이는 더 많은 업무와 스트레스를 초래했습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;blockquote style=&quot;border-left: 4px solid #000000; padding-left: 16px;&quot;&gt;
  &lt;span style=&quot;color:rgb(20, 18, 18);&quot;&gt;
  예를 들어, 특정 화면에서 어떤 버튼을 눌렀을 때 A처럼 동작해야 하는 상황이 있었는데, 기획 문서에는 명확히 정의되어 있지 않았습니다.
  해당 화면은 이전에 작업했던 화면과 구조가 거의 동일했기 때문에, 당연히 그전과 같은 방식으로 동작하면 될 거라 판단하고 개발을 진행했습니다.
  하지만 실제로는 이전과는 다르게 동작했어야 했고, iOS / Android / WEB 모두 다시 수정해야 했습니다.🥹
  &lt;/br&gt;
  &lt;/br&gt;
  만약 기획 리뷰 단계에서 세부 동작에 대해 한 번만 더 질문하고 확인했다면, 이런 비효율은 발생하지 않았을 것입니다.
  이 경험을 통해 아무리 익숙한 기능이라도 기획을 추정해서는 안 되며, 왜❓라는 생각을 가지고 작더라도 모호한 부분은 반드시 리뷰를 통해 해소해야 한다는 점을 깨달았습니다.
&lt;/blockquote&gt;
&lt;/br&gt;
&lt;p&gt;이때 비로소 일은 혼자 잘하면 되는 것이 아니라, &lt;strong&gt;각각의 담당자(PO, 디자이너, 개발자, QA 등)는 단순한 구현자가 아닌, 자신의 전문성을 바탕으로 당면한 문제를 함께 검토하고 개선해 나가며 결과물을 완성해 나가는 역할&lt;/strong&gt;임을 깨닫게 되었습니다. 이후에는 요구사항을 단순히 따르는 것에 그치지 않고, 해당 요구사항이 왜 생겨났는지, 최선의 방법인지를 함께 고민하게 되었습니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 &lt;strong&gt;리뷰의 중요성&lt;/strong&gt;을 새롭게 인식하게 되었고, SW 테스팅 관련한 서적(개발자도 알아야할 소프트웨어 테스팅 실무)에서도 리뷰는 단순한 문서 검토가 아닌, 동적 테스팅 전에 &lt;strong&gt;결함을 조기에 발견&lt;/strong&gt;하고, &lt;strong&gt;잘못된 방향으로 프로젝트가 진행되는 것을 사전에 방지&lt;/strong&gt;하는 중요한 단계임을 인식하게 되었습니다. 그 결과 점점 품질에 대한 관심이 높아지게 되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-내가-가장-뿌듯한-순간-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%82%B4%EA%B0%80-%EA%B0%80%EC%9E%A5-%EB%BF%8C%EB%93%AF%ED%95%9C-%EC%88%9C%EA%B0%84-&quot; aria-label=&quot; 내가 가장 뿌듯한 순간  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 내가 가장 뿌듯한 순간 ]&lt;/h3&gt;
&lt;p&gt;다음으로 &lt;code class=&quot;language-text&quot;&gt;내가 뿌듯했던 때는 언제였을까&lt;/code&gt; 인데요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기획 리뷰 때 놓친 것이나 잘못된 것을 언급하는 것&lt;/li&gt;
&lt;li&gt;QA도 발견하지 못한 결함을 찾아내는 것&lt;/li&gt;
&lt;li&gt;문제의 원인을 정확하게 알아내는 것&lt;/li&gt;
&lt;li&gt;비효율을 제거하고 개선안을 제안하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;center&gt;
    &lt;div style=&quot;display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 179px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4125510ce04d85c2c8d3f93f1d7572d8/a0b6e/smells_defect.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 178.21229050279328%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAkCAYAAACJ8xqgAAAACXBIWXMAAAsTAAALEwEAmpwYAAAIC0lEQVR42k1W53eT5x19tSxZ3hbykC1r7z1tS56S5SHJU8Zgs9xA7RCbjBJCTxIgLSkEU4JzEiDskTByajBJzin9H/qlX/r33N73kUnz4Z7nlY6e+977m5KiIR8S8TBikSAiIb84k7GwQCLK53gIkXAQfp8HHo8LLpcdTqcVDkcPesxdsNsssFp7YOnpFpCi4QBkBAMeXnLB73Ui6HcjwGf5DAc98PE7t8tGIhsc9h44bGZxWq3m3whthN1mhZRMRqkwIlS+Rfx3z1EqDvjccLsdVCUTEg4LbCSVSax7sFjMAlI6FYOM3lQcfb0J9Kbj1c88U+JlYar0wefz7tklXFVyu70K255KGVI2k0KGGOgneGb30E/y/r4kFQZohfHq6RJkXo9TELuce6QOK2x2iyCVIY0M9mJ0KIPcSBa5YZ7DWYHhgT74ebnDaEDnvlY0NtRBq62Blbb8jK1bEDv5EpnYTlipljEcEOqSGMxUMTSQ5plCLOSFucMIL204qc7cboS2RiNInU4LCR0kdAj7As4qqTQ61I8RQj5lhXkqHc6m4aJNPy0kme2o2wqX2YQmqlQqJXR2trEi3IJQViqTVS3vEY6NDhCDGM8PY2p8BEOZBFV1I+F1oS/kQX/YgwhJDc0NUCmVaGlupG0PvF5P1b4oKTsrwQlJkOUGMTE2jOJEDsXxUfQmw3BbupCN+DCcCGAg5oOzux2GxjoSKqDX65h1l8i8j8RySclqZUgzpQJmigUszExhcaGMudIY+kgYdJiRT4cxmY0j4XdCpVJCq1GjgWRyLD2MYZBdFgh4qdTFz9UQSFOFEZQmRjBbymFmahSl8UFkkiHEvTaUhlIoD6eESrupDZZOI+prtSRUs2xsCLG7ArTsJ6FPkLpkwlFMjg1hMk/b+SwtDyEV8zIZdizkMwJjVJoMOGDv6kC9XgudVsOOsYg2dbsZP5ddlI7cmtIUY1aaHKHtMUxPVZVGA+xnezcW+YIDE0MYSUUxHDQj2tmAetrWktBDkrCwzL6X54Bftu6GVJwkIS/NFP9/ehi/NmY0lwqjzDI6XvDj1nwd1ny0qlaikba9jGE07GccvQgEmRyfR0wkabqYQ3lyGHPlvCCbnx6DpbuDCVDC0tWJwYgbD5cNeHlQj0sDNZhoVsOk1cFPVTGOt0jYh1BQVkow4yybDK2O0mpeYGG2gG6TESqFBF1tLUbdDbg7W4+n83rcmdLgFFUGNVp2kh9pDo94NFSdpRyBoVBAVlhAeSovarA0zsIuDMBoaIaShGq1ClGDGh/GdLgxocPD6RqeGiy1qxA0mRCLyUPZJ2L4NtvS/oUZLM4XsTA9icrcBOtxHIaWKqGG8TLVKNFPm2s+La7ldXh2UIetUTV6WxoQpTo5MUHW4tsBLS3MFrE4V8R8uYD9JC4xSfJkUbFn1SxmQ40CPRoFQrVKVMwabER0WLKrEarTIMKYhZkYmTQUrJJSYRkHKiUqK2BpociaHIFOpxGEcpvVUWU3yXq0Sri0KgR0ajiUKliZ6RjJ5Hkpo6qUhKuH57FSmcJ+ElbKOeSG+oRVtUoBNQnl6SKTtuuU6CJxJ8+eFj2SLJd47O2aCAiFMqRUgiOeAY3xDYVcFv3pmFCnkQkJ+VmpUAjU8AUallNra7MoFZlAFDfvv32WlPKlvca3dLYjRtlq9R6ZIKyqVO6FQMPfGg0tYt/EY1yxsl1aj0Vl21So19WImCnki5KEfU31HKT63ywrmG0Fv1cpFEK1rNhAhdlMGkm6SyYiXHAxkWkzh4dkZPrrSCiXio7jXatRoZYvkUe9noFvN7aim6ugUadFPVGn1YoqaG1pEmu0h4PYZOpAc3MTZHFSk74GldlpvHq5g8XKAh4/fkQLIZw/fw7Xrv0d9+/fx+rqMXz2+aeoq9Nj89Qm+tJct8kYdl/tYPPkGmRRne2t6Olqg9TWXIul+TLufH8TJ9fX8Osvv+Levbt48+afePjwAR7cv4dsNoPt7esMhx7Xr17GTGEQs+yonRdPsHXxPNeEE+mQA5kIy8a8r57lMo7HvPj9rZs4ffpPuHnzBi5cuIBLl/6Gp09/xMryQfzw+CGuX/4C9299gxdP7uHc6Q3sPHuMNz/vYGFiEOVcPyaH0pB8lg44Ta2Ic67V6/Wo44hXqVTcG7WoZQxNpk6EfU5O7xg2j85z4KawvlLG6lwOH504gNXKGFbnC1g/NIeVuXFIAVsH5bpR5CpoZWLa24y02C/+VkRZuPISGu6N4cz6Mir5BE6vHcSfTx7Bp+8dxvbFs/jr2Q2cXC7hwz8sYuPYIiR3ZzPOffIRHjH4Zz4+ja2tK/juu2+xsnIQu7uvcPTIIfy8+w88uf01rv7lDN7sPsPLH27jxpXP8d///BtP725j81AR7x8p471DM5A8phYG9hxnYQkfM34vnj9ndq9iaX8Fr1/vYnKigNe7L3Fn+0s8u/cNrl88gzMnKvjkjxX8i+QXPjiCDw5P4rN3l3D6OBWGre3YeOcQXrx4hnffOYr3WQbfbl/D+vFjOLq8BL1KwrWty7jx9VfYPDKDX54/wM6jWzgxk8GD7Yu4sLGM25fO4scblwS51B/k34mufbCxynu52UbiXO7EsdIAvjq1gHMnilify3JhJZB0dyFIAd5uA1x0Zm9rhM+8D3FPD0vHwX8YnIcpnxXubiNc3CP53igKvRHMDsXx09Yp/HRlDV+eLOPoZAJpXzd3swHthiYYWxvZWY2oYf9LbMvf438wE5JtzcQuOgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4125510ce04d85c2c8d3f93f1d7572d8/5f0f1/smells_defect.webp 179w&quot; sizes=&quot;(max-width: 179px) 100vw, 179px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4125510ce04d85c2c8d3f93f1d7572d8/a0b6e/smells_defect.png 179w&quot; sizes=&quot;(max-width: 179px) 100vw, 179px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4125510ce04d85c2c8d3f93f1d7572d8/a0b6e/smells_defect.png&quot; alt=&quot;smells defect&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;결함 냄새를 맡은 펭귄&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이 중에 제가 가장 뿌듯한 순간은 &lt;strong&gt;남들이 발견하지 못한 것을 찾아낼 때 🦅👀&lt;/strong&gt; 입니다.
개발자로 일할 당시, 문자 인증 기능에서 인증번호 자동완성이 특정 단말에서만 작동하지 않는 이슈를 겪은 적이 있습니다. 사용자 입장에서는 너무나 당연한 기능이었기에, 원인을 조속히 파악할 필요가 있었죠.&lt;/p&gt;
&lt;p&gt;QA 담당자와 함께 분석해봤지만, 초기에는 뚜렷한 단서를 찾지 못했습니다. 그러던 중 최근에 리뉴얼한 &lt;code class=&quot;language-text&quot;&gt;TextField(입력창)&lt;/code&gt; 쪽에 문제가 있을지도 모른다는 생각이 들어, 관련 코드를 중심으로 점검을 시작했습니다.&lt;/p&gt;
&lt;p&gt;하지만 소스 코드상 특별한 이상은 없었고, 이후에는 외부 요인을 의심해보게 됐습니다. 문제 발생 시점과 그 이전을 비교하던 중, 인증 문자 메시지의 형식이 일부 변경되었음을 확인했습니다. 혹시나 싶어 이전 형식으로 테스트를 진행해보니, 문제 단말에서도 자동완성이 정상 동작하는 것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;사실 정확한 원인을 파악하기까지는 예상보다 시간이 많이 걸렸습니다. 그 과정에서 ‘만약 이슈의 본질을 좀 더 빨리 짚어냈더라면, 해결 시점도 앞당길 수 있지 않았을까?’ 하는 아쉬움이 남았어요.&lt;/p&gt;
&lt;p&gt;이 경험을 통해 문제를 파악할 때 단순히 &lt;strong&gt;기능적인 요소만이 아니라,  비기능적인 관점(성능, 보안, 사용성 등)까지 함께 살펴보는 시야&lt;/strong&gt;가 중요하다는 걸 배우게 되었습니다.&lt;/p&gt;
&lt;p&gt;이런 과정을 겪다 보니, 개발자로서 기능을 구현하고 문제를 해결하는 것도 흥미로웠지만, 이전 경험을 바탕으로 더 넓은 시각에서 문제를 바라보고 싶다는 확신이 들었습니다. 그렇게 저는 QA 엔지니어라는 새로운 역할에 도전하게 되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;왜-하필-cj올리브영-qa팀이었을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-%ED%95%98%ED%95%84-cj%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-qa%ED%8C%80%EC%9D%B4%EC%97%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;왜 하필 cj올리브영 qa팀이었을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 하필 CJ올리브영 QA팀이었을까?&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-국내-대표-옴니채널을-넘어-글로벌로-나아가는-올리브영-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B5%AD%EB%82%B4-%EB%8C%80%ED%91%9C-%EC%98%B4%EB%8B%88%EC%B1%84%EB%84%90%EC%9D%84-%EB%84%98%EC%96%B4-%EA%B8%80%EB%A1%9C%EB%B2%8C%EB%A1%9C-%EB%82%98%EC%95%84%EA%B0%80%EB%8A%94-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-&quot; aria-label=&quot; 국내 대표 옴니채널을 넘어 글로벌로 나아가는 올리브영  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 국내 대표 옴니채널을 넘어, 글로벌로 나아가는 올리브영 ]&lt;/h3&gt;
&lt;p&gt;현재 올리브영은 국내를 넘어 글로벌로 도약하고 있습니다. 이 올리브영의 끊임없는 성장 여정에 QA팀의 일원으로 함께할 수 있다는 것은 저에게 매우 뜻깊은 기회라고 생각했습니다. 단순히 품질을 검증하는 것을 넘어, 글로벌 고객을 대상으로 한 서비스 품질 향상과 더 높은 기준의 테스트 환경을 경험하며 저 역시 한층 성장할 수 있을 것이라 기대했습니다. 급변하는 시장과 기술 환경 속에서 QA 엔지니어로서 기여할 수 있는 범위가 더욱 넓어지고 있는 지금, 올리브영에서 다양한 도전과 경험을 통해 역량을 키워 나가고 싶다고 생각했습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-대규모-트래픽-경험-기회-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%8C%80%EA%B7%9C%EB%AA%A8-%ED%8A%B8%EB%9E%98%ED%94%BD-%EA%B2%BD%ED%97%98-%EA%B8%B0%ED%9A%8C-&quot; aria-label=&quot; 대규모 트래픽 경험 기회  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 대규모 트래픽 경험 기회 ]&lt;/h3&gt;
&lt;p&gt;&apos;올영세일, 페스타, 라이브 등&apos;과 같은 대규모 트래픽이 발생하는 이벤트에 QA 엔지니어로 직접 참여한다는 것은, 단순한 테스트를 넘어 실제 서비스 운영 환경에서의 품질 안정성과 시스템 대응력을 경험할 수 있는 매우 특별한 경험이라고 생각했습니다.&lt;/p&gt;
&lt;p&gt;수많은 사용자가 동시에 몰리는 상황에서 발생할 수 있는 다양한 이슈를 예측하고 선제적으로 대응하는 과정은, 어디서도 쉽게 얻을 수 없는 실전 경험이자 QA로서의 역량을 빠르게 성장할 수 있는 발판이 된다고 느꼈습니다.&lt;/p&gt;
&lt;p&gt;이 좋은 경험을 1년에 4번이상 경험할 수 있다니! 어디에서도 경험하지 못할 큰 기회라고 생각했습니다. 🙌&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-높은-품질-인식-feat-a-hrefhttpsoliveyoungtech2024-01-23incident인시던트-관리-프로세스a-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%86%92%EC%9D%80-%ED%92%88%EC%A7%88-%EC%9D%B8%EC%8B%9D-feat-a-hrefhttpsoliveyoungtech2024-01-23incident%EC%9D%B8%EC%8B%9C%EB%8D%98%ED%8A%B8-%EA%B4%80%EB%A6%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4a-&quot; aria-label=&quot; 높은 품질 인식 feat a hrefhttpsoliveyoungtech2024 01 23incident인시던트 관리 프로세스a  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 높은 품질 인식 (feat. &lt;a href=&quot;https://oliveyoung.tech/2024-01-23/incident/&quot;&gt;인시던트 관리 프로세스&lt;/a&gt;) ]&lt;/h3&gt;
&lt;p&gt;종종 플랫폼 서비스들의 기술 블로그를 구경하는데, 올리브영 테크 블로그에 기억에 남는 글이 있었습니다. 그것은 바로 인시던트 관리 프로세스입니다.&lt;/p&gt;
&lt;p&gt;국내 대표 옴니채널 서비스를 제공하는 만큼 다양한 문제들이 동시 다발적으로 발생하게 되는데, 이런 다양한 문제들에 어떻게 대응하는지 궁금했습니다. 인시던트 관리 프로세스는 인시던트 발생뿐만 아니라 재발 방지 대책 관리를 통해 후속 프로세스까지도 체계적으로 관리하고 있는 점이 인상 깊었습니다. 또한, 이슈를 투명하게 공개하고 같이 해결해 나가는 과정에서 개발 조직의 품질에 대한 인식이 매우 높다고 판단 되어 저도 꼭 같이 일해보고 싶다고 생각하게 되었습니다.&lt;/p&gt;
&lt;p&gt;저 또한 입사 후 인시던트 알림 전화를 받아보았는데요.&lt;/br&gt;
이 전화 알림을 통해 장애에 빠르게 대응할 수 있는 점이 매우 좋다고 느껴졌습니다.&lt;/p&gt;
&lt;center&gt;
    &lt;div style=&quot;display:block;&quot;&gt;
        &lt;img src=&quot;/2b5297bdf2668ce436169b015e4f42c8/incident_call.png&quot; width=&quot;30%&quot;/&gt;
        &lt;figcaption&gt;실제 인시던트 알림 전화&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;입사-후-깨달은-qa의-진짜-세계-이론과-실무의-간극을-넘어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%85%EC%82%AC-%ED%9B%84-%EA%B9%A8%EB%8B%AC%EC%9D%80-qa%EC%9D%98-%EC%A7%84%EC%A7%9C-%EC%84%B8%EA%B3%84-%EC%9D%B4%EB%A1%A0%EA%B3%BC-%EC%8B%A4%EB%AC%B4%EC%9D%98-%EA%B0%84%EA%B7%B9%EC%9D%84-%EB%84%98%EC%96%B4&quot; aria-label=&quot;입사 후 깨달은 qa의 진짜 세계 이론과 실무의 간극을 넘어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;입사 후 깨달은 QA의 진짜 세계: 이론과 실무의 간극을 넘어&lt;/h2&gt;
&lt;hr&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-꼼꼼함만으로는-부족했던-세계-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%BC%BC%EA%BC%BC%ED%95%A8%EB%A7%8C%EC%9C%BC%EB%A1%9C%EB%8A%94-%EB%B6%80%EC%A1%B1%ED%96%88%EB%8D%98-%EC%84%B8%EA%B3%84-&quot; aria-label=&quot; 꼼꼼함만으로는 부족했던 세계  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 꼼꼼함만으로는 부족했던 세계 ]&lt;/h3&gt;
&lt;p&gt;입사 전, QA는 &lt;code class=&quot;language-text&quot;&gt;꼼꼼함이 생명&lt;/code&gt;인 일이라고만 생각했습니다. 기능이 제대로 작동하는지만 확인하면 되는, 비교적 명확한 일이라는 이미지였습니다.&lt;/p&gt;
&lt;p&gt;그런데 막상 실무에 들어가 보니, 그건 QA라는 일의 아주 일부에 불과했습니다.
&lt;strong&gt;제품의 신뢰도를 높이는 설계를 하고, 사용자 흐름을 지키며, 정책과 서비스의 일관성을 관리하는 일&lt;/strong&gt;이었습니다.&lt;/p&gt;
&lt;p&gt;그 인식의 전환은 온보딩 과정에서부터 차근차근 시작되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-온보딩을-통해-배운-것들--문서로-체계화된-학습-여정-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%98%A8%EB%B3%B4%EB%94%A9%EC%9D%84-%ED%86%B5%ED%95%B4-%EB%B0%B0%EC%9A%B4-%EA%B2%83%EB%93%A4--%EB%AC%B8%EC%84%9C%EB%A1%9C-%EC%B2%B4%EA%B3%84%ED%99%94%EB%90%9C-%ED%95%99%EC%8A%B5-%EC%97%AC%EC%A0%95-&quot; aria-label=&quot; 온보딩을 통해 배운 것들  문서로 체계화된 학습 여정  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 온보딩을 통해 배운 것들 – 문서로 체계화된 학습 여정 ]&lt;/h3&gt;
&lt;p&gt;입사 첫 주는 QA팀에서 마련한 온보딩 체크리스트를 따라가며, 실무에 필요한 지식과 툴을 익히는 시간이었습니다.
&lt;/br&gt;
단순히 시스템 계정을 세팅하거나 툴을 설치하는 걸 넘어, 아래와 같은 실무형 항목들이 포함되어 있었는데요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;QA 표준 프로세스 및 정책&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jira와 Confluence를 활용한 이슈 관리 및 문서 구조&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSP와 인시던트 관리 프로세스&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;트라이브/스쿼드 체계&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
&lt;p&gt;이를 통해 자연스럽게 팀의 일하는 방식을 익힐 수 있었습니다.&lt;/p&gt;
&lt;p&gt;특히 인상 깊었던 점은, &lt;strong&gt;문서는 혼자 보기 위한 게 아니라, 모두가 함께 이해하기 위한 수단이다&lt;/strong&gt;라는 관점이었습니다.
&lt;/br&gt;
테스트 케이스 하나를 작성할 때도 협력사, 개발자, 기획자가 함께 보고 행동할 수 있도록 의도를 명확히 표현하고, 불필요한 중복을 줄이는 구조를 고민하게 되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-책에서-본-qa--실제-qa--일하는-방식의-진짜-차이-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%B1%85%EC%97%90%EC%84%9C-%EB%B3%B8-qa--%EC%8B%A4%EC%A0%9C-qa--%EC%9D%BC%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%98-%EC%A7%84%EC%A7%9C-%EC%B0%A8%EC%9D%B4-&quot; aria-label=&quot; 책에서 본 qa  실제 qa  일하는 방식의 진짜 차이  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[ 책에서 본 QA 🆚 실제 QA – 일하는 방식의 진짜 차이 ]&lt;/h3&gt;
&lt;p&gt;입사 전, QA 직무에 대해 공부하며 접한 대부분의 자료는 테스트 기법, 버그 리포팅 요령, 결함 추적 방식 같은 이론적인 내용이 많았습니다.
또 책을 통해 제가 생각한 QA는 &lt;code class=&quot;language-text&quot;&gt;테스트 케이스를 얼마나 꼼꼼하게 작성했는지&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;이슈를 얼마나 잘 찾았는지&lt;/code&gt;에 초점이 맞춰져 있었습니다.&lt;/p&gt;
&lt;/br&gt;
그런데 실제 팀에 배치되어 본 QA는 그보다 훨씬 더 빠르고, 유연하며, 넓은 시야를 요구하는 역할이었습니다.
&lt;/br&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;구분&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;책/이론에서 본 QA&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;실제 실무 QA (올리브영 기준)&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;업무 개입 시점&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;개발 완료 후 테스트에 참여&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;기획 단계부터 리뷰에 참여하여 요구사항과 정책 검토&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;주요 역할 인식&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;버그를 찾고 리포트하는 사람&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;리스크를 선제적으로 예방하고, 품질을 설계하는 사람&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;테스트 관점&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;기능이 정상 동작하는지 확인&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;정책 준수 여부, 예외 흐름, 사용자 경험까지 고려&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;테스트 케이스(TC)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;문서로만 정리, 개인 작업 중심&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;협업 문서로 작성, 내부 팀 + 협력업체와 함께 리뷰 진행&lt;/td&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;문서의 역할&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;테스트 결과 기록용&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;팀 간 커뮤니케이션 도구, 이해관계자 모두가 보는 기준점&lt;/td&gt;
  &lt;/tr&gt;
    &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;커뮤니케이션&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;QA 내부 공유 중심&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;Slack을 통한 실시간 이슈 공유 및 대응 프로세스 확보&lt;/td&gt;
  &lt;/tr&gt;
    &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;협력업체와의 관계&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;외주에 일방적으로 테스트 위임&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;TC 리뷰를 포함한 실질적 파트너십 구조로 협업&lt;/td&gt;
  &lt;/tr&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;문제 해결 방식&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;테스트 결과를 개발자에게 전달&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;테스트 설계 → 실행 → 이슈 식별 → 공유 → 조치까지의 사이클을 QA가 주도&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;/br&gt;
&lt;p&gt;올리브영 QA팀은 개발이 시작되기 전 단계인 기획 리뷰부터 적극적으로 참여합니다.&lt;/p&gt;
&lt;p&gt;서비스 정책이나 예외 흐름을 QA의 시선으로 검토하며, 설계 단계에서부터 리스크를 줄이는 것이 일의 시작이었습니다. 단순히 기능 명세서를 보고 테스트하는 것이 아니라, &apos;이 기능이 실제로 어떻게 사용될까?&apos;, &apos;이 흐름은 정책에 부합할까?&apos;와 같은 질문을 던지며 전체 맥락을 파악하려는 시도들이 당연한 문화로 자리 잡고 있었습니다.&lt;/p&gt;
&lt;p&gt;또한, 외부 협력업체와도 함께 테스트 업무를 수행하고 있습니다. 단순히 테스트를 맡기는 구조가 아니라, 테스트 케이스를 함께 리뷰하고, 정책 기준을 명확히 맞추는 체계적인 협업 프로세스를 운영합니다.
덕분에 실제 테스트를 수행하는 외부 인력도 제품에 대한 이해도가 높고, 정책 변경이나 기능 업데이트가 있을 경우 빠르게 정리된 내용을 서로 공유해 일관된 품질 검증이 가능합니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;이슈가 발생했을 때의 대응 속도도 인상적이었습니다.
올리브영은 Slack을 통해 실시간으로 이슈가 공유되고, QA뿐 아니라 개발자, 기획자까지 함께 참여하는 스레드가 금방 만들어집니다.
누구든 문제를 먼저 발견하면 빠르게 알리고, 문서화하거나 Jira 티켓으로 연결하여 놓치지 않도록 체계화합니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;이런 구조 덕분에, 신입인 저도 “무언가 이상하다”고 느끼는 순간 바로 공유할 수 있고, 투명하게 공유하는 습관이 형성되는 것 같습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;첫-2주-온보딩-리얼-후기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-2%EC%A3%BC-%EC%98%A8%EB%B3%B4%EB%94%A9-%EB%A6%AC%EC%96%BC-%ED%9B%84%EA%B8%B0&quot; aria-label=&quot;첫 2주 온보딩 리얼 후기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;첫 2주 온보딩 리얼 후기&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;입사하고 한 동안은 실감이 잘 안났습니다.
하지만 부서배치를 받고 나니 점점 &apos;이제 정말 QA엔지니어가 됐구나&apos; 라는 실감이 들었는데요.
&lt;/br&gt;
부서배치 근 2주간 어떤 일이 있었는지 소소하고 가볍게 소개해보려 합니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-첫날-신입사원을-환영해주는-책상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%B2%AB%EB%82%A0-%EC%8B%A0%EC%9E%85%EC%82%AC%EC%9B%90%EC%9D%84-%ED%99%98%EC%98%81%ED%95%B4%EC%A3%BC%EB%8A%94-%EC%B1%85%EC%83%81&quot; aria-label=&quot; 첫날 신입사원을 환영해주는 책상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📚 첫날, 신입사원을 환영해주는 책상&lt;/h3&gt;
&lt;center&gt;
    &lt;div style=&quot;display:block;&quot;&gt;
        &lt;img src=&quot;/28a6c817d1a080018c3de004db2d1e60/day1_2.jpg&quot; width=&quot;20%&quot;/&gt;
        &lt;img src=&quot;/91e08a947b78322cc4d9bd8a9ce16748/day1_3.jpg&quot; width=&quot;20%&quot;/&gt;
        &lt;figcaption&gt;바쁘신 와중에도 한껏 환영해주시는 QA팀원분들!&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;/br&gt;
&lt;p&gt;자리에 도착하니 웰컴 키트와 각종 필요 물품들, 온보딩 가이드 문서가 놓여 있었습니다.
누군가가 제 첫날을 준비해 줬다는 부분이 든든하면서도 감사하게 느껴졌습니다.
동기분들에게 자랑했더니 최고라고 들었는데요 😎
덕분에 저에게는 어깨가 으쓱해진 첫 날이 되었습니다!&lt;/p&gt;
&lt;p&gt;그리고 CTO님께서 환영의 꽃 풍선과 업무 적응에 도움이 되는 책 2권을 선물해주셨는데요.
점점 실감이 나면서도 의욕을 다지게 된 하루가 되었습니다.🔥&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-2일차-신입사원의-이슈-제보&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-2%EC%9D%BC%EC%B0%A8-%EC%8B%A0%EC%9E%85%EC%82%AC%EC%9B%90%EC%9D%98-%EC%9D%B4%EC%8A%88-%EC%A0%9C%EB%B3%B4&quot; aria-label=&quot; 2일차 신입사원의 이슈 제보 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📢 2일차, 신입사원의 이슈 제보!&lt;/h3&gt;
&lt;center&gt;
    &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c8d1b2ac0edaba83991b9bcca27d10b4/84ac2/issue_report.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.416666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACW0lEQVR42m2SW0jTYRjGd9HhqnDmdOeTfzc35wG1OZ1WSNFFl10J4iG3JtpFWRGBYnZlzs3/FDpSmpplWQhRUAgZYWmpCzxEofchQSpBZrpfX7swii6e73vh+/jxPO/7KiY/j70b/TDCtcHLm539MqHuNoI3Lwm1EuoJMvxmiOtDVwh3Bwn3tMcVEnX7rTahIB23Q7TdaN18NHaf6NLEmOLj9xkamhqwmNKw2TOQ0hyYTBJqtYHkFC3NoSaO+QI4M3ORpHRM5jQMBgmjVchkEX/07Nyxm8q6Kj6tzaKYW43GKuorsVrteIqKSXdm4nRlY3e4kOwOLkaaqfD52XfoIMXeAhwZWUg2BxbJjtGcGldCQjK+k35ml6diCnEQOFPLk8dPWVhcJN9dyB6VGo3OiN5ooUVu5mhZOaWHj1DiyRIObZitNvFmRas3C6C0BZxbmSYOrBXAiYkp1n9uCJclf4Ai0gUBrKrxkZ3nJiffQ05OrogqoRNAncHyl8MtYOB0gNHnL1hZXsVd6P0PsIZCj4eCklKRoIhU0ctUyRZPYPgXOPN1MlZ3tp7p6BRrG9/weveTqEoRcUxxYEtnC/7jPtzCefZeLxmuLGyOTFwZDuHUitEioVSm4D/1u4fTMcXi+jzVJ6ppPHeeDlkWk3aRlKRFpdKKSetoCjZSVl6J030AmzMPvd6KRmNCmaghQakmSaVj+7ZdVIgpL/yYjymiS+PjD18NMjRyN3bnWT/hPpmugYjYr3YifWGGXz9AFvsZvneVyEAXHb0h5N4wsniL1+IWexvfw/df3r78BQnWgwFYJKfyAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c8d1b2ac0edaba83991b9bcca27d10b4/263a4/issue_report.webp 480w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/a6361/issue_report.webp 960w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/0b34d/issue_report.webp 1920w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/da28f/issue_report.webp 2880w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/35549/issue_report.webp 3006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c8d1b2ac0edaba83991b9bcca27d10b4/9aebd/issue_report.png 480w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/a91f8/issue_report.png 960w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/ac7a9/issue_report.png 1920w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/f9c26/issue_report.png 2880w,
/static/c8d1b2ac0edaba83991b9bcca27d10b4/84ac2/issue_report.png 3006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c8d1b2ac0edaba83991b9bcca27d10b4/ac7a9/issue_report.png&quot; alt=&quot;issue report&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;이슈 공유방에 제보한 모습&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;올리브영 Slack에는 어떤 이슈든 자유롭고 투명하게 공유하는 채널이 있는데요.
&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;사소한 오타 하나를 발견해서 제보했더니 생각보다 핫한(?) 반응을 해주셔서 매우 뿌듯했습니다.
비록 매우 작은 이슈이지만, 신입도 이런 방식으로 품질에 기여할 수 있다 라는 자신감을 얻게 되었습니다.
&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;제 주변 동기분들도 이걸 발견했다고? 하며 감탄해주셨는데, 이런 부분은 제 직무 전환 계기인 &lt;strong&gt;남들이 발견하지 못한 것을 찾는 것&lt;/strong&gt; 🦅👀 과도 자연스레 이어지는 모습이네요!&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;-2주차-점심-어떻게-먹지---팀내-밥봇-워크플로로-점심-공유하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-2%EC%A3%BC%EC%B0%A8-%EC%A0%90%EC%8B%AC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A8%B9%EC%A7%80---%ED%8C%80%EB%82%B4-%EB%B0%A5%EB%B4%87-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EB%A1%9C-%EC%A0%90%EC%8B%AC-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0&quot; aria-label=&quot; 2주차 점심 어떻게 먹지   팀내 밥봇 워크플로로 점심 공유하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🍚 2주차, 점심 어떻게 먹지? - 팀내 밥봇 워크플로로 점심 공유하기&lt;/h3&gt;
&lt;center&gt;
    &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/39968cecbdb7e242726b5d07fde5908c/84ac2/babbot.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.416666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACaUlEQVR42k2SW0iTYRjHvyAIAukwNzxsuu+bc99yzdN0F67sQARRmQd0pgRKYnWhF0HdVlDZTUSUUIiVQQQJWuo0jcpw07Z52GblTZAXaaVmp1v3690m0cWP9+Hh5f/+n+f/SoFFbyC8EiC46FsTEGfhPz4nehPzbxibG+HFrIfnoX6GwwOCfkYiHoZDA2u++dHYfa8UWZ1k/KuX6RU/MeHIapBYL0GQ8Pcgs7+nedzbydnDx1FzcjGZVUxZMSwoAq02ncr6Kt6Le9Lo3HD0YdsVWs+10Nhykrqmeqob3Lgba+P1keoyHng66ei5g9lkxbYzH7tAtdrJVm3ISjZbt+g4VlvBzNLbqDQs7B917MJktGC22FDNOVhFnWJQSM9Q2LhhMxdvXuBefwcmk4rTWUJufjEWdQd6o4LeIKPRpFJRV8nMsh9pKNRHiixjFmM4CooodJVitxdgzbORYckmKWk7bXev0iVc5trycbh2UyhEHYVFyHIWevGoRpMiBKsSgoMzfeh0aWiMMqm2HHQGI1q9TEaqjuRtyUjSJi7fusSjwfsowrmh2IUqRjYas+LuMo2mdYfrgq/mnkfdDTXUtDbiPt9MeVMtFafrKT9zgrLmOpyH9nOjp53b3e0cyCsRe1QxCKG09ExxKiIYKzqtXuxQjLzkj0rvfk4R+SESXRYJL/mZjaW6IhD98K+pePpjy+Nc67rOqYPllO7dh6t0DyWx0Yuc2PMKUEwWakSQH/6EolJwwTsRsxpY8EX//cP4X/QmEHXo2wTjn17T7X/C4PQzhsSahkSYsdoz9ZSByd61sY8vCX7xjf4Fevux/0diZBUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/39968cecbdb7e242726b5d07fde5908c/263a4/babbot.webp 480w,
/static/39968cecbdb7e242726b5d07fde5908c/a6361/babbot.webp 960w,
/static/39968cecbdb7e242726b5d07fde5908c/0b34d/babbot.webp 1920w,
/static/39968cecbdb7e242726b5d07fde5908c/da28f/babbot.webp 2880w,
/static/39968cecbdb7e242726b5d07fde5908c/35549/babbot.webp 3006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/39968cecbdb7e242726b5d07fde5908c/9aebd/babbot.png 480w,
/static/39968cecbdb7e242726b5d07fde5908c/a91f8/babbot.png 960w,
/static/39968cecbdb7e242726b5d07fde5908c/ac7a9/babbot.png 1920w,
/static/39968cecbdb7e242726b5d07fde5908c/f9c26/babbot.png 2880w,
/static/39968cecbdb7e242726b5d07fde5908c/84ac2/babbot.png 3006w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/39968cecbdb7e242726b5d07fde5908c/ac7a9/babbot.png&quot; alt=&quot;babbot&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;밥봇 워크플로&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;p&gt;조금 익숙해지던 2주차, QA팀의 경우 점심 스타일이 자유로운 편인데요!
그래서 매 점심시간마다 서로 어떻게 먹을 건지 논의하는 경우가 많았습니다.
이때, 서로의 점심 유형을 미리 공유하고 필요할 때만 논의하는게 어떨까?🧐 하는 생각에서 간단한 &lt;strong&gt;밥봇 워크플로&lt;/strong&gt;를 만들게 되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;이모지로 서로의 점심 유형을 공유하고, 만일 배달 시켜 먹고 싶은 경우엔 &lt;strong&gt;배달 논의&lt;/strong&gt; 버튼을 누르면 해당 스레드에 댓글이 생겨 배달 논의를 편하게 할 수 있도록 해보았습니다.
매우 작은 기능이지만, 서로의 점심 타입을 말하지 않아도 알 수 있다는 점에서 보다 편리해진 것 같습니다.🤩&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;여기까지 저의 입사 여정을 남겨보았습니다. 직무 전환 계기부터 온보딩까지 꽤 긴 글이 되었는데요.
&lt;/br&gt;
작은 것부터 천천히, 팀 안에 스며드는 중이다 하는 느낌이 드는 요즘입니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;입사 전엔 QA가 뒤에서 ‘검사하는 사람 🔍’이라 생각했다면,
&lt;/br&gt;
지금은 &lt;strong&gt;제품을 함께 만들어가는 동료&lt;/strong&gt;👩🏻‍🔧 라는 감각에 더 가까워졌습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;&lt;strong&gt;문서 한 줄을 작성할 때에도 누군가의 행동 흐름을 바꾸게 될 수 있다는 책임감&lt;/strong&gt;,&lt;/br&gt;
단순한 오류 발견이 아니라 &lt;strong&gt;고객의 경험을 지키는 중요한 역할이라는 사명감&lt;/strong&gt;,
&lt;/br&gt;
이 모든 걸 매일 조금씩 배우고 있습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;앞으로도 팀의 일하는 방식과 철학을 깊이 이해하며,
&lt;/br&gt;
서비스에 가장 먼저 의심을 제기할 수 있는 QA,
&lt;/br&gt;
기술과 정책 사이의 간극을 메울 수 있는 QA엔지니어로 성장해가고 싶습니다.&lt;/p&gt;
&lt;p&gt;다음엔 더 성장한 모습으로 나타나 보겠습니다! 긴글 읽어주셔서 감사합니다. 🙇🏻‍♀️&lt;/p&gt;
&lt;/br&gt;</content:encoded></item><item><title><![CDATA[버그가 아니라 장애를 잡아라!! QA와 카오스 엔지니어링의 만남]]></title><description><![CDATA[카오스 엔지니어링 도입 배경 24시간 내내 운영되는 이커머스 서비스에서는 언제든지 장애가 발생할 수 있습니다.
그 원인도 매우 다양합니다. 인프라 문제 코드 문제 설정 문제 데이터 문제 휴먼 에러 이처럼 다양한 원인으로 인해, QA…]]></description><link>https://oliveyoung.tech/2025-06-10/chaos/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-06-10/chaos/</guid><pubDate>Tue, 10 Jun 2025 18:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;카오스-엔지니어링-도입-배경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B9%B4%EC%98%A4%EC%8A%A4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%8F%84%EC%9E%85-%EB%B0%B0%EA%B2%BD&quot; aria-label=&quot;카오스 엔지니어링 도입 배경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;카오스 엔지니어링 도입 배경&lt;/h2&gt;
&lt;p&gt;24시간 내내 운영되는 이커머스 서비스에서는 언제든지 장애가 발생할 수 있습니다.
그 원인도 매우 다양합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;인프라 문제&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;코드 문제&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설정 문제&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 문제&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;휴먼 에러&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이처럼 다양한 원인으로 인해, QA 팀이 배포 코드를 아무리 철저히 테스트하더라도 그것만으로는 서비스의 완전한 안정성을 보장할 수는 없습니다.&lt;/p&gt;
&lt;p&gt;장애가 발생하면 이를 해결하기 위해 많은 인력이 투입되고, 서비스 중단이나 고객 불편으로 이어질 수 있습니다. 복구가 지연되거나 데이터가 꼬이는 등 예기치 못한 상황이 발생할 가능성도 있습니다. 예측 가능한 장애라 하더라도, 사전 테스트를 하지 않으면 실제 문제로 이어질 수 있습니다.&lt;/p&gt;
&lt;p&gt;이에 따라, 올리브영은 최소한 예측 가능한 장애에 대해서는 서비스 안정성을 확보하기 위해 &lt;strong&gt;카오스 엔지니어링&lt;/strong&gt;을 도입하기로 결정했습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;카오스-엔지니어링이-무엇이길래-필요한가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B9%B4%EC%98%A4%EC%8A%A4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B8%B8%EB%9E%98-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80&quot; aria-label=&quot;카오스 엔지니어링이 무엇이길래 필요한가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;카오스 엔지니어링이 무엇이길래 필요한가?&lt;/h2&gt;
&lt;p&gt;위에서 계속 카오스 엔지니어링에 관해서 이야기 했는데 그렇다면 카오스엔지어링이 무엇이길래 서비스의 안정도를 높이는 데 도움이 된다고 이야기하는 걸까요?&lt;/p&gt;
&lt;p&gt;앞서 카오스 엔지니어링을 언급했는데, 이것이 왜 서비스 안정성을 높이는 데 도움되는 걸까요?
간단히 말하면 &lt;strong&gt;일부러 장애를 일으켜 시스템이 잘 버티는지 실험하는 것&lt;/strong&gt;입니다. 예를 들어, DB를 일시적으로 꺼보거나 네트워크 지연을 인위적으로 발생시켜 서비스가 이를 어떻게 처리하는지 관찰하는 방식입니다.
가령, 하나의 DB가 다운되었을 때 그 DB에 의존하는 서비스는 어떻게 될까요?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서비스가 완전히 중단될까요?&lt;/li&gt;
&lt;li&gt;성능 저하가 발생할까요?&lt;/li&gt;
&lt;li&gt;아니면 정상적으로 동작할까요?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 상황은 직접 장애를 발생시켜보기 전까지는 추측에 불과합니다.&lt;/p&gt;
&lt;p&gt;만약 실제 장애 상황에서 서비스가 중단된다면, 이는 사용자 경험에 치명적인 영향을 미칠 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 사전에 카오스 엔지니어링을 통해 테스트를 진행했다면 이중화를 구성한다든지, 장애 대응책을 마련해둘 수 있었을 것입니다.&lt;/p&gt;
&lt;p&gt;카오스 엔지니어링은 단순히 장애를 일으키는 것이 아닙니다. &lt;strong&gt;시스템의 복원력(Resilience)&lt;/strong&gt; 을 높이고 취약점을 사전에 발견해 개선하는 것을 목표로 하는 방법론입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;실험-종류&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%ED%97%98-%EC%A2%85%EB%A5%98&quot; aria-label=&quot;실험 종류 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실험 종류&lt;/h2&gt;
&lt;p&gt;그렇다면 카오스 엔지니어링 실험은 어떤 종류가 있을까요? 대표적인 유형은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Application Level&lt;/strong&gt;: 애플리케이션 내부에서 발생할 수 있는 예외나 오류 상황을 유도합니다. 애플리케이션의 비정상적인 동작을 유도하여 시스템의 안정도와 복원력을 테스트합니다. &lt;br&gt;
예: API 호출 실패, null 반환 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host Level&lt;/strong&gt;: 시스템이 실행되는 호스트 서버 자체에 영향을 주는 실험입니다. 서버의 리소스나 상태를 조작해 복원력을 테스트합니다. &lt;br&gt;
예: DB 다운, 프로세스 종료 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Attack&lt;/strong&gt;: 서버 리소스를 고갈시키거나 과부하 상태를 만드는 실험입니다. 서비스 병목 현상이나 자원 부족 문제 재현에 유용한 방법이죠.&lt;br&gt;
예: CPU 100% 사용, 메모리 누수 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Attack&lt;/strong&gt;: 네트워크 연결 문제나 성능 저하를 유도합니다. 네트워크 장애를 시뮬레이션하여 복원력을 테스트합니다. &lt;br&gt;
예: 네트워크 단절, 패킷 손실 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AZ (Availability Zone) Attack&lt;/strong&gt;: 특정 AZ 또는 리전의 장애 상황을 시뮬레이션합니다. 이 역시 시스템의 복원력을 테스트합니다. &lt;br&gt;
예. AZ 단절, 리전 단절 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Region Attack&lt;/strong&gt;: 대규모 리전 장애 상황을 시뮬레이션합니다. 시스템 복원력을 테스트하는 또다른 유형입니다.&lt;br&gt;
예. 리전 단절, 대규모 장애 등&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;People Attack&lt;/strong&gt;: 사람의 실수나 잘못된 행동을 시뮬레이션합니다. &lt;br&gt;
예: 잘못된 설정, 오배포 등&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 외에도 다양한 유형이 존재하지만, 올리브영에서는 &lt;strong&gt;Application Level, Host Level, Resource Attack&lt;/strong&gt;을 우선적으로 사용하기로 했습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 이 중에서도 &lt;strong&gt;Application Level&lt;/strong&gt; 테스트에 대해 다루겠습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-level-테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#application-level-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;application level 테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Application Level 테스트&lt;/h2&gt;
&lt;p&gt;올리브영에서 Application Level 테스트를 1순위로 진행한 이유는 이전에 발생한 장애 유형 중에 &lt;strong&gt;null exception&lt;/strong&gt;이 있었기 때문입니다.&lt;/p&gt;
&lt;p&gt;API 응답이 &quot;key&quot;:&quot;value&quot; 형태로 전달될 때 일부 필드의 value가 null인 상황은 흔히 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;머리로는 &quot;특정 key의 value만 null이라면, 그 부분만 안 나오고 나머지는 정상적으로 보이겠지?&quot;라고 생각할 수 있지만, 실제로는 &lt;strong&gt;null exception으로 인해 전체 페이지가 렌더링되지 않는&lt;/strong&gt; 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이런 경우를 방지하기 위해서 카오스 엔지니어링을 도입하여 API의 key에 null 값을 넣어주는 실험을 진행하게 되었습니다.&lt;/p&gt;
&lt;h3 id=&quot;테스트-목표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%AA%A9%ED%91%9C&quot; aria-label=&quot;테스트 목표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;테스트 목표&lt;/h3&gt;
&lt;p&gt;테스트를 시작하기에 앞서 이 테스트의 목표를 명확하게 정하는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;그래야 문제를 발생시켰을 때 생각하는 기대결과 수준에 따라서 수정 여부가 결정되기 때문입니다.&lt;/p&gt;
&lt;p&gt;예를들어 null값이 넘어왔을때&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;해당 API를 사용하는 페이지가 정상적으로 보여야 한다. default 데이터로 null 값이 넘어온 필드가 보여져야 한다.&lt;/li&gt;
&lt;li&gt;해당 API를 사용하는 페이지가 정상적으로 보여야 한다. 단, null 값이 넘어온 필드에 대해서는 보여지지 않아야 한다.&lt;/li&gt;
&lt;li&gt;해당 페이지는 표시할 수 없으므로 다른 페이지로 이동시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;등의 목표를 세울 수 있을 것입니다. 따라서 목표에 따라서 수정 방향도 달라질 것이 보일것입니다.&lt;/p&gt;
&lt;p&gt;올리브영은 **해당 API를 사용하는 페이지가 정상적으로 보여야 한다. 단, null 값이 넘어온 필드에 대해서는 보여지지 않아야 한다.**는 목표로 설정하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;테스트-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;테스트 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;테스트 방법&lt;/h3&gt;
&lt;p&gt;Application Level 테스트를 진행하기 앞서 목표를 정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;API의 value에 null이 넘어오더라도 해당 정보를 사용하는 영역만 노출되지 않고 서비스는 정상적으로 이용 가능해야 한다.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이런 목표를 달성할 실험이라면 어떻게 진행할 수 있을까요? 가장 먼저 생각난 방법은 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;google overwrite contents (&lt;a href=&quot;https://developer.chrome.com/docs/devtools/overrides?hl=ko&quot;&gt;https://developer.chrome.com/docs/devtools/overrides?hl=ko&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;charles, fiddler와 같은 프록시 툴을 사용하여 API의 필드에 null 값을 넣어주는 방법&lt;/li&gt;
&lt;li&gt;proxy를 실제로 만드는 방법 (mitmproxy - &lt;a href=&quot;https://mitmproxy.org/&quot;&gt;https://mitmproxy.org/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;실제 데이터를 입력하는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;간단한 테스트라면 1, 2번 방법이 적합합니다. 하지만 필드 값이 많으면 하나하나 수동으로 수정하며 테스트하는 번거로움이 있습니다.&lt;/p&gt;
&lt;p&gt;일회성 테스트에는 이 방법이 유용할 수 있으나, 올리브영에서는 이번 테스트뿐만 아니라 향후 Application Level 테스트를 프로세스화해 진행할 계획이었습니다. 이러한 이유로 매번 수동 작업이 필요한 1, 2번 방식은 리소스가 과도하게 소모될 것으로 판단했습니다.&lt;/p&gt;
&lt;p&gt;4번 방식의 경우 API가 어떨 때 null 값을 넘겨주는지까지는 정의되지 않았기 때문에 데이터 수정으로 null을 넘겨주기는 어렵다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;그래서 3번, &lt;strong&gt;mitmproxy를 이용해 proxy를 만드는 방법&lt;/strong&gt;으로 테스트 진행을 결정했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;mitmproxy-구성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mitmproxy-%EA%B5%AC%EC%84%B1&quot; aria-label=&quot;mitmproxy 구성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;mitmproxy 구성&lt;/h2&gt;
&lt;p&gt;일단 mitmproxy를 이용한 간단한 플로우를 보면 아래와 같습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f0bf9df419b2ff9bfc24bdc9824ae26f/95bc7/proxy_flow.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAC4jAAAuIwF4pT92AAAB6klEQVR42pVSTW/TQBD1v+A/g4SQUA8IcUCCIwKJAxdUGiIMaaEkpKW4aZzEJk2cxLE3tvfT3sfuJpbgguBJs+P1vn0zOzOe1hp1XUNKiaZpoFQNzvff1qTZW9ztFbhztETvVrh9bc70gc8OfAvPLnZjha0XRrii3BFV3aDkClpJ+DHHPT/HT6LcRS5rw/2dr5yGd/b5C54+e44XL1+BkJ0j2wMLm12Z3iKZ3WAbh8jnI6yiGyTRGNliiqrYQR/4OSFgjMN73/2Ar4MznHROMA6nTigrFcK1BKkEZJlB0S2idYCiSEC3M9A8MtEKcEYdP4rn+Hh6jourAF5/MMAm3RixEKNxCCEkklygP62MZyipBG8UAjZFqRlq9s0IDZ0QFfv6Xhqh12/e4rjjw/vUO0UcxxgOLzCdRY6QljUmJkOuNJa5RLhgWKYKm0K7WgmpXFOo2DdikazRH/5AMJrAO37Xwf0HD/Ho8RMjPHd1iFOJ3jVBUTEECw7/isAfLPF9zpAShv71CpeTDITua21X+7KmMU3JssxlmCSJi94SLEStseN7K4Q2GRmTQLyqMFvuQHjLhBs712X8BbbZTWtmab2VoZS5p1sRm4gVdHNof7T2r2iZnHP3ujRN/xzs/0UbPM9zdLtdV64WvwAQpEgS2sWR2gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f0bf9df419b2ff9bfc24bdc9824ae26f/263a4/proxy_flow.webp 480w,
/static/f0bf9df419b2ff9bfc24bdc9824ae26f/a6361/proxy_flow.webp 960w,
/static/f0bf9df419b2ff9bfc24bdc9824ae26f/0b34d/proxy_flow.webp 1920w,
/static/f0bf9df419b2ff9bfc24bdc9824ae26f/f7a21/proxy_flow.webp 1933w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f0bf9df419b2ff9bfc24bdc9824ae26f/9aebd/proxy_flow.png 480w,
/static/f0bf9df419b2ff9bfc24bdc9824ae26f/a91f8/proxy_flow.png 960w,
/static/f0bf9df419b2ff9bfc24bdc9824ae26f/ac7a9/proxy_flow.png 1920w,
/static/f0bf9df419b2ff9bfc24bdc9824ae26f/95bc7/proxy_flow.png 1933w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f0bf9df419b2ff9bfc24bdc9824ae26f/ac7a9/proxy_flow.png&quot; alt=&quot;proxy flow&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(proxy flow)&lt;/figcaption&gt;
&lt;p&gt;mitmproxy는 HTTP(S) 트래픽을 가로채고 수정할 수 있는 강력한 도구입니다. 이를 통해 API 요청과 응답을 조작하여 null 값을 주입하는 테스트를 수행할 수 있습니다.&lt;/p&gt;
&lt;p&gt;mitmproxy에서 제공하는 기본 함수는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔹 request(flow: HTTPFlow)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/// 클라이언트 → 서버로 가는 요청을 가로챌 때 호출
def request(flow: http.HTTPFlow):
    if flow.request.pretty_url.endswith(&quot;/api/data&quot;):
        flow.request.headers[&quot;X-Test-Header&quot;] = &quot;Test&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;🔹 response(flow: HTTPFlow)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/// 서버 → 클라이언트로 가는 응답을 가로챌 때 호출
def response(flow: http.HTTPFlow):
    if &quot;example.com&quot; in flow.request.pretty_url:
        flow.response.headers[&quot;X-Debug&quot;] = &quot;modified&quot;
        flow.response.text = flow.response.text.replace(&quot;Hello&quot;, &quot;Hi&quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;🔹 error(flow: HTTPFlow)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/// 네트워크 오류, 처리 실패 등 에러가 발생했을 때 호출
def error(flow: http.HTTPFlow):
    print(f&quot;Error occurred: {flow.error}&quot;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;🔹 clientconnect / clientdisconnect&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/// 클라이언트가 연결/종료할 때 호출됨
def clientconnect(connection):
    print(f&quot;Client connected: {connection}&quot;)

def clientdisconnect(connection):
    print(f&quot;Client disconnected: {connection}&quot;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 외에도 다양한 유형의 함수를 기본적으로 제공합니다.&lt;/p&gt;
&lt;p&gt;null exception 테스트를 위해서는 응답(Response)을 변환해야 했기 때문에, response(flow: HTTPFlow) 함수를 중심으로 테스트를 진행했습니다. 우선 테스트할 API를 선별해 목록 파일로 만들었습니다. 그리고 mitmproxy를 실행할 때 이 파일을 불러와 테스트했습니다. 각 API의 응답 값에서 필드를 하나씩 골라가며 null 값을 자동으로 주입해 테스트가 진행되도록 구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;response code중 일부를 발췌&lt;/strong&gt;하여 흐름을 설명해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;def response(flow: http.HTTPFlow):
    if api_list[i] in flow.request.url and flow.response.content:
            print(f&quot;🌐 요청 URL: {flow.request.url}&quot;)
            # 응답 데이터 파싱 (bytes → str 변환 후 JSON 로드)
            data = json.loads(flow.response.content.decode(&quot;utf-8&quot;))
            # &quot;data&quot; 필드가 딕셔너리인지 확인
            if isinstance(data, dict) and isinstance(data.get(&quot;data&quot;), dict):
                data_fields = list(data[&quot;data&quot;].keys())  # &quot;data&quot; 하위 필드 리스트 가져오기

                # API에 있는 data를 모두 확인한다
                if field_index &amp;lt;= len(data_fields)-1:
                    key_to_null = data_fields[field_index]  # 현재 필드를 `null`로 설정
                    data[&quot;data&quot;][key_to_null] = None&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;///api/data의 응답값
{
  &quot;name&quot;: &quot;Alice&quot;,
  &quot;age&quot;: 30,
}
///api/data2의 응답값
{
  &quot;city&quot;: &quot;seoul&quot;,
  &quot;company&quot;: &quot;oliveyoung&quot;,
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 두 API가 호출된다고 가정하면, 이들을 테스트대상 API 목록 파일에 선언합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/api/data
/api/data2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이후 proxy를 실행하여 모바일 기기와 proxy를 연결하면, 대상 API가 호출될 때 proxy가 가로채서 응답 값을 변환하게 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;/api/data 처음 호출 시&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;{
&quot;name&quot;: null,
&quot;age&quot;: 30,
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;두 번째 호출 시&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;{
&quot;name&quot;: &quot;Alice&quot;,
&quot;age&quot;: null,
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;을 반환하게 됩니다. 이렇게 /api/data의 마지막 value까지 null로 변환 되면 /api/data2도 동일하게, value 값을 null로 순차 변환합니다.&lt;/p&gt;
&lt;p&gt;이렇게 /api/data의 마지막 value까지 null로 변환하면 /api/data2도 동일하게, value 값을 null로 순차 변환합니다. Proxy를 만든 후 테스트를 진행하니 반자동 테스트가 되었고, 시간을 아끼면서 다른 영역들로 확장하기에 유용했습니다.&lt;/p&gt;
&lt;p&gt;실제 올리브영도 테스트 도중에, 에러 바운더리 작업을 1차 진행했음에도 문제가 발견되었는데요.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b00b3d67f2cba2555633d7860956a17c/dface/proxy_log.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 34.583333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABUUlEQVR42kWRWXKDMBBEuYhZJHYtSMKAwQ7YqUp8/xN1GkIlH696RoKHBkVh2aB1B20dylpDtity9YBoZuTtgzV7vUE2C/s7CvNCuWN3Ptlvv+t6hah6RD5MGKYFZdVCygpF0SAnVa2YLYqSVKyZmSgg8xqCz11igTiRJ+Ivo/n5Rtf16K8TjA1Q3Q2tHWD9DENUN0G7GbobkVcdGjOiJmmukRWWGNYKGftU1Ihuz28KPUI/QhkP5Vco90HR48D2L5j+ScmMohm4v6HUM2Q98jdMzOEYdc8sN4im7QvO9RTyJDyhCc9DqP0vlrJd2tqFwhGa+//C25GHsDqFy+sN768I+8gc3foHjLvDhjvcdWWu/MiKsrlC2RsakhUeogxkT0cRR2edyhaRCwMchZ3jC7xtH3reukXdtEjTDEJIZJlAkqTsxbF2ucQkOfOsY2ac4gesjdQr5k0SEQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b00b3d67f2cba2555633d7860956a17c/263a4/proxy_log.webp 480w,
/static/b00b3d67f2cba2555633d7860956a17c/411c4/proxy_log.webp 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b00b3d67f2cba2555633d7860956a17c/9aebd/proxy_log.png 480w,
/static/b00b3d67f2cba2555633d7860956a17c/dface/proxy_log.png 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b00b3d67f2cba2555633d7860956a17c/dface/proxy_log.png&quot; alt=&quot;proxy log&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(proxy log)&lt;/figcaption&gt;
&lt;p&gt;검출된 부분들은 QA팀에서 리포팅해 조치하도록 처리하였습니다. 앞으로 전시 영역은 에러 바운더리 작업을 진행하여 null exception이 발생하지 않도록 하기 위해 정기 검증 및 프로젝트 프로세스화를 진행할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;* 여기서 잠깐! 에러 바운더리란 무엇일까요?&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;React 앱에서 컴포넌트 중 하나라도 오류가 발생하면 전체 앱이 중단될 수 있습니다. 이 때 React 컴포넌트인 에러 바운더리를 사용하면 문제가 있는 부분만 격리하여 사용자에게는 대체 UI를 보여주고, 앱의 나머지 부분은 계속 작동할 수 있도록 합니다.&lt;/em&gt;&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 250px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4efd1bfb77ef06d1f7ee960056472619/bce2d/error.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 215.20000000000002%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAArABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAECBAb/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHoBIoQTN4zaZA2CBiD/8QAGxAAAQQDAAAAAAAAAAAAAAAAIAECAxEEEBL/2gAIAQEAAQUCC9KEjcjtiT0X/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwFf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwFf/8QAGxAAAgIDAQAAAAAAAAAAAAAAAQIAIBASMSL/2gAIAQEABj8CtycydGQLPbKTf//EAB0QAAEDBQEAAAAAAAAAAAAAAAEAESEQIEFRYZH/2gAIAQEAAT8hsZuhsRAp+faF+Ig2MAiUMc8hhi//2gAMAwEAAgADAAAAEDDCPDDP/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPxBf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPxBf/8QAHhABAAICAgMBAAAAAAAAAAAAAQARITEQcSBBYZH/2gAIAQEAAT8Q5UEFMwUUFDW+EodhqoOk257fkNQJKaexIqYhqjWbwwssFWLA+d+f/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4efd1bfb77ef06d1f7ee960056472619/23cfa/error.webp 250w&quot; sizes=&quot;(max-width: 250px) 100vw, 250px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4efd1bfb77ef06d1f7ee960056472619/bce2d/error.jpg 250w&quot; sizes=&quot;(max-width: 250px) 100vw, 250px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4efd1bfb77ef06d1f7ee960056472619/bce2d/error.jpg&quot; alt=&quot;error&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(페이지를 표시할 수 없습니다가 뜨면 안돼요😢)&lt;/figcaption&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;QA에서 하는 업무는 주로 정상적인 동작을 가정하지만, 카오스 엔지니어링은 반대로 비정상적인 동작을 가정하기 때문에 어떻게 보면 &lt;strong&gt;기존 QA 업무와 반대의 경우를 발생&lt;/strong&gt;시킨다고 볼 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;예측할 수 없는 장애에도 흔들리지 않는 시스템을 만들기 위해&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;정상 조건뿐만 아니라 비정상 상황에서도 품질을 보장하기 위해&lt;/li&gt;
&lt;li&gt;개발자, SRE와 함께 서비스 안정성, 회복력을 검증하고 강화하기 위해&lt;/li&gt;
&lt;li&gt;실제 서비스 중 장애가 발생해도 사용자에게 최소한의 영향을 주도록 사전 대비하기 위해&lt;/li&gt;
&lt;li&gt;그리고 무엇보다, 문제가 터졌을 때 놀라지 않기 위해&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;카오스 엔지니어링을 도입해 보는 건 어떨까요?&lt;/p&gt;
&lt;p&gt;이번에는 카오스 엔지니어링의 개념과 Application Level 테스트를 다뤘습니다. 다음에는 &lt;strong&gt;Host Level 테스트&lt;/strong&gt;를 다루는 포스트로 다시 찾아오겠습니다!&lt;/p&gt;
&lt;hr/&gt;
&lt;div style=&quot;text-align: center; margin-top: 30px; padding: 20px; border: 2px solid #f0f0f0; border-radius: 10px; background-color: #fafafa;&quot;&gt;
&lt;p style=&quot;font-size: 1.2em; font-weight: bold; margin-bottom: 15px;&quot;&gt;우리와 함께 일할 동료를 열린 마음으로 기다리고 있습니다.🤩&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250807033067&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;코어플랫폼 QA 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033063&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;SCM QA 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033551&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;Back Office QA 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title><![CDATA[올리브영 사용자 행동 데이터로 학습한 상품 유사도 언어 모델: 전통적 속성 기반 추천을 넘어선 의미론적 유사도 모델링]]></title><description><![CDATA[안녕하세요! 올리브영에서 머신러닝으로 다양한 문제를 풀고 있는 데이터 사이언티스트 입니다 😃
최근에 개발 했던 추천을 위한 Language Modeling에 대해서 공유하고자 합니다. 올리브영 AI…]]></description><link>https://oliveyoung.tech/2025-06-02/itemlm/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-06-02/itemlm/</guid><pubDate>Mon, 02 Jun 2025 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영에서 머신러닝으로 다양한 문제를 풀고 있는 데이터 사이언티스트 입니다 😃
최근에 개발 했던 추천을 위한 Language Modeling에 대해서 공유하고자 합니다.&lt;/p&gt;
&lt;p&gt;올리브영 AI 플랫폼의 최대 강점은 바로, 상품 구매 요인과 직결된 방대한 속성·성분 데이터를 보유하고 있다는 점입니다. 이러한 강점은 고객의 구매 패턴, 선호, 니즈를 더욱 정교하게 파악할 수 있게 해주며, 이를 통해 신규 상품 기획, 협력사 제안, 마케팅 캠페인의 차별화를 가능하게 합니다. 나아가 단순히 ‘많은 상품’을 추천하는 것을 넘어, 올리브영 고객이 원하는 맞춤형 상품을 제안하여 고도화된 고객 경험을 창출한다는 점에서 중요한 의미를 가집니다.&lt;/p&gt;
&lt;h2 id=&quot;추천-모델의-핵심-구조-query-x-value&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B6%94%EC%B2%9C-%EB%AA%A8%EB%8D%B8%EC%9D%98-%ED%95%B5%EC%8B%AC-%EA%B5%AC%EC%A1%B0-query-x-value&quot; aria-label=&quot;추천 모델의 핵심 구조 query x value permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;추천 모델의 핵심 구조: Query x Value&lt;/h2&gt;
&lt;p&gt;올리브영의 추천 시스템은 사용자가 필요해서 찾는 아이템, 혹은 관심 있게 둘러보는 카테고리를 UX 흐름에 적합한 모델으로 예측해 제공합니다.&lt;/p&gt;
&lt;p&gt;추천 모델링의 목적(&lt;code class=&quot;language-text&quot;&gt;Object&lt;/code&gt;)은 &lt;code class=&quot;language-text&quot;&gt;Query&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;Value&lt;/code&gt;의 관계를 &lt;code class=&quot;language-text&quot;&gt;Label&lt;/code&gt;에 맞게 모델링하는 것입니다. 이 때 Query는 조건이나 사용자 같은 상황(Context)을, Value는 추천 대상을 의미하며, Label은 Query와 Value를 기반으로 한 목적을 말합니다.
이는 어떤 모델을 사용하든 동일하게 적용됩니다.&lt;/p&gt;
&lt;p&gt;또한, 모델이 필요한 상황(Context)에 따라 Query는 키워드(Keyword), 사용자(User), 브랜드(Brand), 아이템(Item) 등 다양한 형태가 될 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1050px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/04f6c42b28a52ae79109a6f8559650d2/dd14e/1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 73.33333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAABi0lEQVR42p1UuVLDMBD1D/KJNHQUdFTMUNDQpGGoGI6QkDjBiXzEp7SS89DKGU98kIEUa+vYfW935608rTXVdQ1jTGv27N/GcURkPKUUZgsfL++feJ3OsPDXKIqyA0xk7Y/AHjtXVQWlKkjJJpmp47SvNWozDHZEPTLPLUwNP9JISwMuv1uKxkoQgohGytRQxPFHgMy+SQg3E4m7Z4miIpuNQZbl1oFQSo3bR4nre4kgVMDeILV32Cs8LTUuLhWuJgRAQ9IhQ07bt85fQjnW5qzJyFgTMeFtqSAVub0ici2IC42HD415qIG6wfHafihpndVoo4kskZbDO8PAZSfOATLjzA/wLWJ7OJTNehtjvtq4ktr+WStKiekyQJhkrSoaQCsdIQSiKBqA8V2SJAPZGNfnDNvN1sqsGAJyEEtmDJCJWFrH2uR1mqbY7XZdHfKHgcIwHO0fA3Jgn4wB8zxHWXaHwOMNO3PJ/B8bvVPj2L9rS+bUfwM8Zf2p8g5DfdaD8Msskzz3hRl5begHxuWSWPGHeB4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/04f6c42b28a52ae79109a6f8559650d2/263a4/1.webp 480w,
/static/04f6c42b28a52ae79109a6f8559650d2/a6361/1.webp 960w,
/static/04f6c42b28a52ae79109a6f8559650d2/5e9fb/1.webp 1050w&quot; sizes=&quot;(max-width: 1050px) 100vw, 1050px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/04f6c42b28a52ae79109a6f8559650d2/9aebd/1.png 480w,
/static/04f6c42b28a52ae79109a6f8559650d2/a91f8/1.png 960w,
/static/04f6c42b28a52ae79109a6f8559650d2/dd14e/1.png 1050w&quot; sizes=&quot;(max-width: 1050px) 100vw, 1050px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/04f6c42b28a52ae79109a6f8559650d2/dd14e/1.png&quot; alt=&quot;추천 모델의 핵심 구조: Query x Value = Label&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;유사-상품-추천-어떻게-접근할까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9C%A0%EC%82%AC-%EC%83%81%ED%92%88-%EC%B6%94%EC%B2%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%91%EA%B7%BC%ED%95%A0%EA%B9%8C&quot; aria-label=&quot;유사 상품 추천 어떻게 접근할까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;유사 상품 추천, 어떻게 접근할까?&lt;/h2&gt;
&lt;p&gt;오늘 소개할 내용은 사용자가 상품을 둘러보며 고민할 때, 비슷한 상품을 추천하거나 리마인드·탐색 연속성을 돕는 추천 방식에 대한 이야기입니다.
추천은 &lt;strong&gt;유사상품 모델&lt;/strong&gt;을 기반으로, 최근에 본 상품과 유사한 제품을 제안하거나 다양한 화면과 목적에 맞춰 활용됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;즉, 상품(item) 간 관계를 수치화(score; similarity) 하는 &lt;code class=&quot;language-text&quot;&gt;Item&lt;/code&gt; × &lt;code class=&quot;language-text&quot;&gt;Item&lt;/code&gt; 모델이라고 할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f0d2f9522114fc5b3db1c71ca509ebea/2ac49/2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 78.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAADmklEQVR42l2Uy2/cRBzHIyFxosqRLRIXuHHupUJbXoIDDSDg1sAFjgj+CUSkSrxSARWolAst0AhBQ6FpEUpJeCTppnkvm5Dd7K49ttdez3r93PF45svYSbcJI40s219/fq/veGRELQD3Wa49rZudzz3PO5Gm6ak0jctxHJdt2y7X6/XRkYOltPebtjPVbBst6vlf9V33ZOzTU77vl33qlwtRGIbHLdqsWabFgyAUSZJIBRNJHEsVAI7jPH0I+ECXug3dMqJUIEgGXKpnkvFUbS73RWFY6nk+GXCOIIoE9pcUUiITAipAAXzsiedLl3+8eYJqOm0srWNt/gcZ0XUwXwcPGWKtjZFHy889fOv2nWc9TdfdjRquX5kS5ybew8cTZ+V25TswbxP67uIzBwmO/vLH0mt+tTawbizAmjsrteV3Qf+9hn5VgzdbgdI8Mrq0tfOGX9sm6ew8rn06KU48dBwvnnxK3rjwMtzal9hd+GJY8sJ69XRH07lrevDJnGwtv4/e3gzongW/bqIQ3fln5ww1THPQdbFSWRZvvvoKpr6ektt/fQCyNolm5dwQuFjZGDO1Nu92HfR6PWkYOlgSo2OY8INgH7iyuTNuW5bZpRRezxOtZgumZUlCNBikqT68N5TF1Y0xTevxhubBpv18IMhSjs2VVWj13X3g5k59POq6Zr/v5y+F3/MwYExyniHLBDjn94Ab1bGZ+W0+eXEav/35u9RaOtqtNr79aAJTn3x4kGF1+zRjqVFEE0IwFTGf8sG0h1PO17rSrm7t8p+vz8Jo70mTmDA0DSvT32Dx7zkMfehSSsIwyr8f2kbmtskyBEEwBFZUhqSlcV03EHie6qGJ0Peh7TWUX+0hsBR4lCDjeTbi6tWfQAgpMkxTfiTD2xu1x6O+H2fKsyLLZBLFUEUhZWkR/MD9Yen8bI18ttZHk1iiurWFdluTruuCpewIUJ2aY6qndh5MXVWfeV7L3e7cBaJ0aZOSC9U+fNcT1KVIBkyqM1108v9A27RsgxBomiajKMrBRXZM6YfAuV+/J4szlxF7rvDUtKM4UuUwNIIFNIPKISCOOc22bbf2oOu6VAes8B9j7HDJKN26cp7cvDSpgI6gtKd6l0p1mOGnysCBeSRD9eOwk2RQGFv1H51OB6ZpFrsQqSk+SNqthkcdRFGYdF2XKeFAbaZuU8fpPXn4b2PbjvJsPzd/pqDirbffEWMvvCTOjL8u/gMlHrNi5BakIwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f0d2f9522114fc5b3db1c71ca509ebea/263a4/2.webp 480w,
/static/f0d2f9522114fc5b3db1c71ca509ebea/a6361/2.webp 960w,
/static/f0d2f9522114fc5b3db1c71ca509ebea/0b34d/2.webp 1920w,
/static/f0d2f9522114fc5b3db1c71ca509ebea/4cd1a/2.webp 2336w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f0d2f9522114fc5b3db1c71ca509ebea/9aebd/2.png 480w,
/static/f0d2f9522114fc5b3db1c71ca509ebea/a91f8/2.png 960w,
/static/f0d2f9522114fc5b3db1c71ca509ebea/ac7a9/2.png 1920w,
/static/f0d2f9522114fc5b3db1c71ca509ebea/2ac49/2.png 2336w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f0d2f9522114fc5b3db1c71ca509ebea/ac7a9/2.png&quot; alt=&quot;유사 상품 모델을 사용하고 있는 추천 화면 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;기존-접근법-속성-기반-유사도&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A1%B4-%EC%A0%91%EA%B7%BC%EB%B2%95-%EC%86%8D%EC%84%B1-%EA%B8%B0%EB%B0%98-%EC%9C%A0%EC%82%AC%EB%8F%84&quot; aria-label=&quot;기존 접근법 속성 기반 유사도 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;기존 접근법: 속성 기반 유사도&lt;/h2&gt;
&lt;h3 id=&quot;상품-속성-one-hot-encoding-기반-유사도-계산&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%ED%92%88-%EC%86%8D%EC%84%B1-one-hot-encoding-%EA%B8%B0%EB%B0%98-%EC%9C%A0%EC%82%AC%EB%8F%84-%EA%B3%84%EC%82%B0&quot; aria-label=&quot;상품 속성 one hot encoding 기반 유사도 계산 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상품 속성 one-hot encoding 기반 유사도 계산&lt;/h3&gt;
&lt;p&gt;올리브영 AI 플랫폼의 가장 큰 자산중 하나는 상품의 구매 요인과 관련된 속성과 성분 데이터를 보유하고 있다는 점입니다.
이러한 풍부한 데이터를 기반으로, 상품 간 유사도를 계산할 때 속성과 성분 정보를 one-hot encoding한 뒤 cosine similarity를 적용하는 방식을 사용해왔습니다.&lt;/p&gt;
&lt;h3 id=&quot;속성-가중치의-함정-모든-속성이-같을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8D%EC%84%B1-%EA%B0%80%EC%A4%91%EC%B9%98%EC%9D%98-%ED%95%A8%EC%A0%95-%EB%AA%A8%EB%93%A0-%EC%86%8D%EC%84%B1%EC%9D%B4-%EA%B0%99%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;속성 가중치의 함정 모든 속성이 같을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;속성 가중치의 함정: 모든 속성이 같을까?&lt;/h3&gt;
&lt;p&gt;이 방법론은 상품의 객관적인 특징을 비교해 교집합을 도출한다는 점에서 의미 있는 접근이었습니다. 그러나 모든 속성에 동일한 가중치를 적용하면서, &lt;strong&gt;중요도가 낮은 속성의 일치가 유사도 계산에 과도하게 반영되는 문제&lt;/strong&gt;가 발생했습니다. 그 결과, 실제로는 관련성이 낮은 상품 간에도 유사도가 과대 평가되는 현상이 나타났습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1714px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/8cd37/3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.62500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABg0lEQVR42nWSzWoVQRCF874+guAzZOcrCOLGRRARsktWSYjExUUwRpEQc++d6em/quq+Hk/3zEgWceBQMz30V6dO91HOue6GAfvRYZw8Rjf9k+P3fnDIYrBSUWtFKeVZtX/G50hV+2bvqWnsdXCOsLlOrDhk4I9BTblJubl0PQcl0FCL4XGq+Pw94mEs2A2EJyUs4G6rOD71eHsl2I6BUwQkOm4SK2ywwBZoB4pk7H3Bj+0BYywA+HNx8DgVfNxUnN9WZK0QLayztMnauyGkPAM5NtrYyhq9w6+94tOXCZt74fjM0Asubj2+Pmh3N4XYIc2driJYRGegEBZiQhR2yhln3xSvPhje3zAKyx308sTw+swIE8REZetKeYZZh9oMXANtDp0PKAw9Bg9TOqSbLIKDBRyqYPSJ0PzkEGZgItyHtGa4OGS3lBJON4oX7wRvLgnmuouKnUv47RTXd4abn2wc6TAZm83AfurrKTPDst6vVlsmaQm/PFlvtd3F9VT/cw/1L6nntd5ZFUYGAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/263a4/3.webp 480w,
/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/a6361/3.webp 960w,
/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/84262/3.webp 1714w&quot; sizes=&quot;(max-width: 1714px) 100vw, 1714px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/9aebd/3.png 480w,
/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/a91f8/3.png 960w,
/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/8cd37/3.png 1714w&quot; sizes=&quot;(max-width: 1714px) 100vw, 1714px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e26c2c0ba5c14c7447fc2f6357a5f6dc/8cd37/3.png&quot; alt=&quot;중요도를 고려하지 않는 one-hot encoding 방식의 문제 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;위 샘플에서는 상품 A와 B, B와 C가 각각 0.5로 동일한 유사도를 보입니다. 하지만 각 속성이나 속성값의 중요도가 모두 동일하게 취급되기 때문에, 단순히 교집합의 수만으로 유사도를 계산하는 방식에는 한계가 있습니다. 사용자의 판단에 자연스럽게 영향을 주는 주요 기준이 반영되지 않기 때문입니다.&lt;/p&gt;
&lt;h2 id=&quot;개선된-접근법-사용자-관점-속성-가중치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0%EB%90%9C-%EC%A0%91%EA%B7%BC%EB%B2%95-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B4%80%EC%A0%90-%EC%86%8D%EC%84%B1-%EA%B0%80%EC%A4%91%EC%B9%98&quot; aria-label=&quot;개선된 접근법 사용자 관점 속성 가중치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선된 접근법: 사용자 관점 속성 가중치&lt;/h2&gt;
&lt;h3 id=&quot;상품마다-다른-속성-가중치-모델링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%ED%92%88%EB%A7%88%EB%8B%A4-%EB%8B%A4%EB%A5%B8-%EC%86%8D%EC%84%B1-%EA%B0%80%EC%A4%91%EC%B9%98-%EB%AA%A8%EB%8D%B8%EB%A7%81&quot; aria-label=&quot;상품마다 다른 속성 가중치 모델링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상품마다 다른 속성 가중치 모델링&lt;/h3&gt;
&lt;p&gt;따라서 새로운 방법은 상품 간 속성을 비교할 때, &lt;strong&gt;사용자의 비교 관점에 기반한 중요도&lt;/strong&gt;에 따라 가중치를 다르게 반영하도록 해야 했습니다.
그러므로 같은 속성이라도 상품 카테고리에 따라 그 비중이 달라져야 합니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 틴트 제품에서는 색상 속성이 중요한 반면, 스킨 제품에서는 상대적으로 덜 중요할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✏️ 여기서 다음과 같은 도전 과제가 있었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;레이블&lt;/strong&gt;: 사용자의 비교 관점 및 비교 상품 카테고리간 어떤 상품과 어떤 상품이 얼마나 유사한지의 데이터 취득&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정량적인 검증 방법&lt;/strong&gt;: 모델의 loss외에 실질적인 성능을 검증할 수 있는 정량적인 방법&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;콜드 스타트&lt;/strong&gt;: 신규 상품 및 속성 미보유 상품 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;레이블-설계-사용자의-비교-관점에서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%EC%9D%B4%EB%B8%94-%EC%84%A4%EA%B3%84-%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%EB%B9%84%EA%B5%90-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C&quot; aria-label=&quot;레이블 설계 사용자의 비교 관점에서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;레이블 설계: 사용자의 비교 관점에서&lt;/h2&gt;
&lt;h3 id=&quot;레이블의-중요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%EC%9D%B4%EB%B8%94%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1&quot; aria-label=&quot;레이블의 중요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;레이블의 중요성&lt;/h3&gt;
&lt;p&gt;아무리 강력한 모델을 사용하더라도, 학습에 쓰이는 레이블이 목적(Objective)에 적합하지 않으면 의미가 없습니다.
도메인 지식이 있더라도 임의로 관계를 정의해 데이터를 만들고 학습시키는 것은 정확도가 떨어지며, 이는 결국 수많은 &lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt; 문을 직접 설계하는 것과 다를 바 없어 AI 활용의 본질에도 맞지 않습니다.&lt;/p&gt;
&lt;p&gt;그래서 모집단의 분포를 그대로 모방할 수 있는 방법을 찾기 위해, &lt;strong&gt;간접적인 레이블 데이터를 획득하는 방안&lt;/strong&gt;을 모색하였습니다.&lt;/p&gt;
&lt;h4 id=&quot;탐색형-행동-로그로부터의-레이블-생성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%90%EC%83%89%ED%98%95-%ED%96%89%EB%8F%99-%EB%A1%9C%EA%B7%B8%EB%A1%9C%EB%B6%80%ED%84%B0%EC%9D%98-%EB%A0%88%EC%9D%B4%EB%B8%94-%EC%83%9D%EC%84%B1&quot; aria-label=&quot;탐색형 행동 로그로부터의 레이블 생성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;탐색형 행동 로그로부터의 레이블 생성&lt;/h4&gt;
&lt;p&gt;상품 간 유사성 판단의 핵심은 사용자가 실제로 상품을 비교하고 선택하는 관점에 있습니다. 이러한 인사이트를 바탕으로, 우리는 목적 지향적인 사용자의 검색 행동에서 해답을 찾았습니다.&lt;/p&gt;
&lt;p&gt;사용자가 검색하는 과정은 특정 목적을 향한 탐색 경로라 할 수 있습니다. 예를 들어, 동일한 키워드로 검색한 결과 내에서 클릭된 상품들은 해당 키워드의 관점에서 일정한 관계를 맺고 있다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;물론 유사 관계에 대한 신뢰도 높은 레이블을 확보하기 위해, 일정량 이상의 교집합(여러 사용자가 동일한 키워드에서 동일한 상품을 클릭한 경우)에 해당하는 데이터만 학습에 활용했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1548px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f946444889c916a363b17542eccccf19/600f6/4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 82.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB5UlEQVR42p1Ua1PbMBD0//+DBYYWSBMHP2PL1uMkOds7yRhDmU7ohxudbGl1u7dS4YiWtr/gMoxouh7GGMQYEUL4Vsge730oPE/OVZ0ABdhYexMg7/0StFiWBaNS6PoBkl+v1/TTOffX4nTQGnm+QAraRyGLZ+sxTC6NznsYR2ApdtX4JIW1hg+ysJwbo6H1jBgI1yVgiXltojzODr3SuDDoZHI+m/cKBfxYDThUI8pmwrEe8VT2eDp16EaHYQ5QOsuQKlSaGMzyD0pAA+faZsCFqRkXcH+OeKw87suAH6eIX3XAXRk5PB7OhLuTh3UroCOmJHQpwJL88OkbhYimqdNhh9ZDmxn14HDsWB6t0thyhdM04XdL6eCsoSGoWegKGPFG1pBBE2XPVduAx1diMIOXxuHnq0u5jAeeV73mfAUU3iNTbYcZF+WYvkOnzAcNidec+sBVBq4kx0vznj8z/bLP6zYNe/WmIWGUjvMY2BZN2zI9/aU3pbNv3d26nCtgvVabiHYSfmeZFJ/8tjc47UyeAEW32djkP8U6SqWi5y1XjujjrVmbIt4zqTHiyXaYkid9WNCulD13nNZq9vH5Cm4VTtwMY2nzpIx7yrc+EoWInXWjtHHLg/+vF4dt4+N3N/7r+foDd1Ld+RxF6OkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f946444889c916a363b17542eccccf19/263a4/4.webp 480w,
/static/f946444889c916a363b17542eccccf19/a6361/4.webp 960w,
/static/f946444889c916a363b17542eccccf19/e77b3/4.webp 1548w&quot; sizes=&quot;(max-width: 1548px) 100vw, 1548px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f946444889c916a363b17542eccccf19/9aebd/4.png 480w,
/static/f946444889c916a363b17542eccccf19/a91f8/4.png 960w,
/static/f946444889c916a363b17542eccccf19/600f6/4.png 1548w&quot; sizes=&quot;(max-width: 1548px) 100vw, 1548px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f946444889c916a363b17542eccccf19/600f6/4.png&quot; alt=&quot;동일한 검색어로 클릭된 상품들간의 유사성에 대한 설명&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1548px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a442b62e44cb29a6f595af3b08e70f9f/600f6/5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAzklEQVR42n2R3Q6CMAyF9/5vaEz0woiKgvyMja2DerpiJBG9+MJ21p2eMhNjTHXTcmctExHHqMj6F1s1KSVoMRrAxbXk6tnkA57B9N9wnrQukbI2NSK4kdgHgO/hRnwsdd9az9aNPKFw8GPeByQ7VcT7C7H1ifvBQ3cw09RGnAcfYBayUDbE91Y7DtAckOIxxFwn+qMjLmrRKN9764KRmB26CJJwd9bukrBB5x4JxVBHWkZOn9+SVnoeGYtJunsQIPZOkbVocvb1KAsbj0IvJy7VBW95vakAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a442b62e44cb29a6f595af3b08e70f9f/263a4/5.webp 480w,
/static/a442b62e44cb29a6f595af3b08e70f9f/a6361/5.webp 960w,
/static/a442b62e44cb29a6f595af3b08e70f9f/e77b3/5.webp 1548w&quot; sizes=&quot;(max-width: 1548px) 100vw, 1548px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a442b62e44cb29a6f595af3b08e70f9f/9aebd/5.png 480w,
/static/a442b62e44cb29a6f595af3b08e70f9f/a91f8/5.png 960w,
/static/a442b62e44cb29a6f595af3b08e70f9f/600f6/5.png 1548w&quot; sizes=&quot;(max-width: 1548px) 100vw, 1548px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a442b62e44cb29a6f595af3b08e70f9f/600f6/5.png&quot; alt=&quot;동일한 검색어로 클릭된 상품들간의 유사성을 기반으로 유사 관계 를 구성하는 방법&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이 상품들은 특정 검색 키워드라는 관점에서 &lt;code class=&quot;language-text&quot;&gt;유사하다&lt;/code&gt;고 볼 수 있으며, 이것이 바로 사용자가 상품을 바라볼 때 &lt;code class=&quot;language-text&quot;&gt;비슷한 상품&lt;/code&gt;으로 정의할 수 있는 레이블입니다.
또한 다수의 통계 데이터를 통해 사용자들이 상품을 바라보는 관점을 일반화함으로써, 상품의 중요한 속성을 효과적으로 학습시킬 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 820px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/774ea5868395ed69420892a3693f25cf/0bbcd/6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAABiElEQVR42p1TiW6CQBDl///PpLY2VjwQ5BL2gGHhdQawFQP22ORlr5m3b471iChveACo27YlMPoZN4D4mn4YNUOMcq/rOhhjsTsE8I8B9qczDkEI/3DisxPWmy20NvxOC+fcU4iN51zTKXb42B8npFveC1brDYpSsbGDBPIMrLDzZCHsovQGeel+L/czzj0ez71Hgzk8OnFU42OchiXC36KqG1yV4bSEnOsLtK2eE9IMSU0DkcBUDUpd4RwliOIMVUXzhOKUFDVyPczKcgdxaHYksSOh5XV61fC5cLvjGVGST0RMCKNUs1GIIM4RF4RMNUw83N0gxMpUiLMclyRDofQCIRtGmcEhTBAmBaskzs9AKKrkvg+dWhzZZvW25bYKsONcyvlMlRvkilAYDqkkDpPQueH8Fm7dzw7XUiNOM2TXsl8TLRRF2uF+7tdjob6L4nBJS7y8+1i9buEH0VShdPdchWnpNwg5qzfcLtbWnI76q1f7nzL3C/4L4RKFSr7avdK/QnxHDvUJXtCRaPn83HIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/774ea5868395ed69420892a3693f25cf/263a4/6.webp 480w,
/static/774ea5868395ed69420892a3693f25cf/d9db4/6.webp 820w&quot; sizes=&quot;(max-width: 820px) 100vw, 820px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/774ea5868395ed69420892a3693f25cf/9aebd/6.png 480w,
/static/774ea5868395ed69420892a3693f25cf/0bbcd/6.png 820w&quot; sizes=&quot;(max-width: 820px) 100vw, 820px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/774ea5868395ed69420892a3693f25cf/0bbcd/6.png&quot; alt=&quot;한개의 상품 기준으로 봤을때 상품이 가지는 유사 비교의 다양한 관점들&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h2 id=&quot;item-similarity-language-model-설계와-학습-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#item-similarity-language-model-%EC%84%A4%EA%B3%84%EC%99%80-%ED%95%99%EC%8A%B5-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot;item similarity language model 설계와 학습 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Item Similarity Language Model: 설계와 학습 방식&lt;/h2&gt;
&lt;h3 id=&quot;모델과-방법론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%8D%B8%EA%B3%BC-%EB%B0%A9%EB%B2%95%EB%A1%A0&quot; aria-label=&quot;모델과 방법론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모델과 방법론&lt;/h3&gt;
&lt;p&gt;Language Model을 사용해서 두 상품 문서의 embedding의 cosine similarity값이 label값이 되도록 모델이 추론하는 embedding값을 학습시키는 방법론을 선택했습니다.
이 목적을 위해서 Predicted similarity와 Ground truth similarity를 최소화하도록 학습하고자 Sentence Transformer구조와
Loss function으로 MultipleNegativesRankingLoss를 사용 했습니다. 이 Loss function은 positive sample(유사 관계)만 있는 상태일때 negative sample(비유사 관계)을 in batch implicit하게 sampling함으로써 &quot;비유사 관계&quot; 데이터 없이 &quot;유사 관계&quot; 데이터만 있어도 학습을 할 수 있는 특징이 있습니다.&lt;/p&gt;
&lt;p&gt;학습을 위한 모델로는 다양한 언어로 사전학습된 Sentence Transformer 구조의 paraphrase-multilingual-MiniLM-L12-v2(118M 파라미터 크기)를 사용 했습니다.&lt;/p&gt;
&lt;p&gt;🤔  Language model을 선택한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;데이터의 형태&lt;/strong&gt;: 상품을 구성하는 속성이나 상품명 등은 수치가 아니라 텍스트 형태로서 의미를 지님&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의미의 이해 필요&lt;/strong&gt;: 수치 비교가 아니라 상품을 설명하는 다양한 요소들을 통해 상품을 이해하고 연결하기를 목표함&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의미론적 이해&lt;/strong&gt;: 동일(equal)할 뿐 아니라 비슷한 표현에서 오는 차이나 유사함을 처리하도록 목표함 (LM의 지식도 활용)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1220px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3ce1fae1cb01dc4bc0366f323cd2ad89/1e677/7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 126.45833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAYAAAAxFw7TAAAACXBIWXMAABYlAAAWJQFJUiTwAAACqklEQVR42o1VyXbUMBCc//8DvgAu8IATyx1ChgQegUd4gZBkNs/mZbxJLdlFt2Ubz9iT5CBba6laVWqNjDHEBZoIWuvqL22q21S3HypFUcjcfFRyZeuHmMw8TOYLzL0Vbqdz3ExmVYl2MU+2jwK21mLUsOkWrV0xdfsxDGucciSocZJhulgh2CUHE3jXISZmuF9KDZhi6q0RREm7IMgMZrFBovqLNwlhyUXTAKB88lwjihNkmXIDPDHTBlsGVdq1qV7MCsJPDbwakOq+PUBrTXXw8peFKA0WPuH0DyFM3HjDsiwM5jw22VC1mYz1Qk7SHLPlBouVX4mR88SPV4RnJxrvf+iqLsCy2ToyuJgQxteEn1PC3Xo/9FZl8Z9isIJ3HDOzF58V3lwQXp1rPGXgZcihxm7s6y3h2x3h7K+r5x2mo0NVBfDLDeHlmcZrBns+1vjEIDmLIyy/M7tzHj/9TbicszghtefbA2wU3onCfE4nvOgts5S2bCShpbmp2ErYm53r76ncsNu7SjwxYjVXER3cBvePc3PcNsKqLAsGsa01KmVZ0aIcNrA+YvqRLExZZW/tww93zjYMdLMlPPmg8Wvp2k0EMl/EmW6dV+0QQ6U0Qr52SZq14QYc7rtLwvX6AFDCzZx91DFAB2L3TCqgwP3hDobczWdyW+TfClWLpTpXr1s/mhzSLMeCz3C58bHm3FilrgPFQz6CZdS3ySBD2TFXij2WI+My5E05r1TZvTCrYzJH7rIwa7LuUCaOeE6wE9H+W8vbhoi5v7umVVkGjmVnYZFxioszvdcfxjlHpoeNLWIMseveEF0L1NSJ7LBtJNOkWVaJ89AbIqEGnCQkwwyKIswSBvI2onJQZW9778sm91sE6ifX6tVjRiQV50H7uDfY9k0tGAyl/gHyaZttQcxhCAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3ce1fae1cb01dc4bc0366f323cd2ad89/263a4/7.webp 480w,
/static/3ce1fae1cb01dc4bc0366f323cd2ad89/a6361/7.webp 960w,
/static/3ce1fae1cb01dc4bc0366f323cd2ad89/f6961/7.webp 1220w&quot; sizes=&quot;(max-width: 1220px) 100vw, 1220px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3ce1fae1cb01dc4bc0366f323cd2ad89/9aebd/7.png 480w,
/static/3ce1fae1cb01dc4bc0366f323cd2ad89/a91f8/7.png 960w,
/static/3ce1fae1cb01dc4bc0366f323cd2ad89/1e677/7.png 1220w&quot; sizes=&quot;(max-width: 1220px) 100vw, 1220px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3ce1fae1cb01dc4bc0366f323cd2ad89/1e677/7.png&quot; alt=&quot;Sentence Transformer를 사용한 유사도 학습 모델 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;학습 과정에서 얻어지는 것은 단순히 검색어와 관련된 상품이 아니라, 상품 문서 간 유사성에 영향을 미치는 텍스트의 중요도입니다.&lt;/p&gt;
&lt;p&gt;즉, A라는 상품이 B, C, D, E, F와 유사하다고 묶여 있을 때, A 상품을 기준으로 B, C, D, E, F와의 유사성은 최대화하고, 그 외 다른 샘플과의 유사성은 최소화하는 과정을 통해 어떤 단어(vocab)가 중요한 정보를 제공하는지, 또는 무시해야 하는지를 학습하게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;상품-문서의-구성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%ED%92%88-%EB%AC%B8%EC%84%9C%EC%9D%98-%EA%B5%AC%EC%84%B1&quot; aria-label=&quot;상품 문서의 구성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;상품 문서의 구성&lt;/h3&gt;
&lt;p&gt;모델에 들어가는 상품의 문서는 예시처럼 속성 외에도 다양한 정보를 포함하도록 하고, 섹션을 구분해 구성했습니다. 아래에서는 구조적으로 표현했지만 실제로는 텍스트 기반 markdown 형태로 작성되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 560px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a8002dafff4c2dbac9445a303fa70412/6d7b8/8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 80.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAADY0lEQVR42o1TXWgcVRSeVDRFCmpBFB+kFEEQUQRfxJda0KJQoeiDIvgiWERQX5SigtS+9ME+VHyRpamY1qShMQ/SCEWaaNJIY5MGN5vNzmTn/39mZ+7835m7u9dzl01JIVUPfMydc+8599zvfIcrSVfoxBlduFnvumFMSY+2z8/N7c2q6v0upVTUnZ6gmtTyQupHKTU8RMBNi5Kc5sBgeQ+30+IUf255wfSWZl4wnc7PaY6/ZP4kKZ8J42zCC+MJ0wsnTS8Y0x2/ppneObj4Ypri14YJ99yRMMPVr6Ju2X8sr0mK6TppXl6dnZ0dzTB5U1AMa3Wdl29tCLYXRgsQfP8wyQi3iw38cV6edYP4elNQfnN9tBTn+Du2mef5C5Dld1m1r0mqtRhn+EcI2PtvCQeGkry2ttleWdsQFptbymoQZ2PMXxB6SNLt5c22ulRviX+2RHVBVK2/mm3l1rogrq4L8qogGwNsKeZKg5fqbgfNcFGaT7ckXYCguiAZbbhgBiq4F578ump6vKDojYYgNXhR31AN15I0ywYq7C2ArDu2bAB02xI1u+OHyRIX5fjTKC0meVE7D128BE05MWzK00GSj8mGPWY4/nhBeqfYRdx/WZQVax2UYuA+tH1UQsK/IfC+rKiOM9lott91UUrzkhhhGD64g8MR9t3J52ANT3xbdzpnVds/DU34NkDJO9ubgIemarX963Nz+7j/a7jqgrQQXa7zPcXyoJJKRyh7nuf5UaaxmV+ufHbl6rVTloc+tN3gY4TofkLI4bIkXztecALWJ+HcodsVRhF+khD6CnOmaXEEFcUTRVEc8DzvsZKWz4G4saBYtCXrfZAYZfqEs6xxVLXckn1pj07vKvJtPlgVQRA8ztZTU5ffm59f/AL23sKYHhu8CuOn4P8TwAeAjwAvAR6hX0HCIVd7tsEC+v3+qOOkj5YlfdbyUbIparTZVvvQNBphfBTOTYVJRnlR6bZljWqW22eFEkpe5e42QgGlD8B3X6PZOtPk2z+htGAT9U0c04fB/0YYx+OW438fhNHFEKXjFSE18B+8a7OYdAAH5q/fuLx4Y2W+JWmTIN6JIcfH0zRjs/3irtztOuRs/Ap6UDV9C0ar3xK1Poi8h5Li5aoiP7Angk7fZeduguCHlI38A1mNJPsu5IdbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a8002dafff4c2dbac9445a303fa70412/263a4/8.webp 480w,
/static/a8002dafff4c2dbac9445a303fa70412/f4853/8.webp 560w&quot; sizes=&quot;(max-width: 560px) 100vw, 560px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a8002dafff4c2dbac9445a303fa70412/9aebd/8.png 480w,
/static/a8002dafff4c2dbac9445a303fa70412/6d7b8/8.png 560w&quot; sizes=&quot;(max-width: 560px) 100vw, 560px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a8002dafff4c2dbac9445a303fa70412/6d7b8/8.png&quot; alt=&quot;상품 문서의 시각화 표현 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h3 id=&quot;속성-없는-상품까지-학습시키는-augmentation-전략&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8D%EC%84%B1-%EC%97%86%EB%8A%94-%EC%83%81%ED%92%88%EA%B9%8C%EC%A7%80-%ED%95%99%EC%8A%B5%EC%8B%9C%ED%82%A4%EB%8A%94-augmentation-%EC%A0%84%EB%9E%B5&quot; aria-label=&quot;속성 없는 상품까지 학습시키는 augmentation 전략 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;속성 없는 상품까지 학습시키는 Augmentation 전략&lt;/h3&gt;
&lt;p&gt;실제 운영 환경에서는 속성 정보가 부족하거나 아예 없는 상품들이 불가피하게 존재합니다. 특히 신규 출시 상품의 경우 속성 데이터가 완비되지 않은 경우가 많아, 기존 속성 기반 유사도 계산으로는 적절한 추천이 어렵습니다. 그러나 사용자 관점에서는 이런 상품들에 대해서도 관련성 높은 다른 상품들이 추천되어야 합니다.&lt;/p&gt;
&lt;p&gt;그래서 이런 상황을 대비해 &lt;strong&gt;학습시의 레이블 데이터를 추가로 보강&lt;/strong&gt;하는 방식을 사용했습니다. 방법은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;속성이 모두 있는 유사 상품 쌍 중에서 상품 쌍 a  = b를 무작위로 뽑습니다.&lt;/li&gt;
&lt;li&gt;그 중 상품 a의 속성 정보를 일부러 제거해서 속성이 없는 상태로 만듭니다.&lt;/li&gt;
&lt;li&gt;속성이 없는 a 기준에서 상품이 있는 b와 관련있는 상태로 레이블을 만들게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 만든 쌍을 학습 데이터셋에 추가하여 속성이 없는 상품도 관계를 학습할 수 있도록 했습니다.
이 외에도 실제의 데이터 분포에서 얻기 힘들지만 기획적으로 추가되어야 하는 관계성을 augmentation을 통해 레이블을 생성해서 추가 했습니다.
결과적으로 1년의 검색 로그로부터 약 300만개 정도의 학습 레이블을 확보할 수 있었고, 이 중 20%를 검증용 데이터로 사용 했습니다.&lt;/p&gt;
&lt;h2 id=&quot;모델-성과-정량적-검증&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%8D%B8-%EC%84%B1%EA%B3%BC-%EC%A0%95%EB%9F%89%EC%A0%81-%EA%B2%80%EC%A6%9D&quot; aria-label=&quot;모델 성과 정량적 검증 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모델 성과: 정량적 검증&lt;/h2&gt;
&lt;h3 id=&quot;정량적-측정-방법-개발&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%9F%89%EC%A0%81-%EC%B8%A1%EC%A0%95-%EB%B0%A9%EB%B2%95-%EA%B0%9C%EB%B0%9C&quot; aria-label=&quot;정량적 측정 방법 개발 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;정량적 측정 방법 개발&lt;/h3&gt;
&lt;p&gt;모델 학습의 성과를 평가할 때, 일반적으로 loss 값을 확인합니다. 그렇지만 loss는 모델이 주어진 레이블에 대해 얼마나 잘 학습했는지를 나타내는 지표일 뿐, &lt;strong&gt;실제 우리가 원하는 목적에 맞게 학습되었는지&lt;/strong&gt;는 다른 관점의 평가 지표가 필요합니다.&lt;/p&gt;
&lt;p&gt;그래서 다음과 같은 정량적 측정 방법을 도입하여 모델이 목적에 맞게 학습되었는지 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 모델의 집중 영역(Attention) 분석&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모델이 상품 A와 상품 B 간의 관계(유사/비유사)를 학습하는 과정에서, 주어진 텍스트(vocab) 내에서 모델이 &lt;code class=&quot;language-text&quot;&gt;어느 부분에 집중&lt;/code&gt;하는지를 파악합니다.&lt;/li&gt;
&lt;li&gt;학습이 잘 되었다면, 같은 카테고리 내에서는 모델이 집중하는 토큰(단어)들이 유사해야 하며, 이는 모델이 해당 카테고리에서 중요한 속성을 잘 파악하고 있다는 의미입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 카테고리별 집중 영역 차이&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;반면, 서로 다른 카테고리 간에는 모델이 집중하는 부분이 달라야 합니다.&lt;/li&gt;
&lt;li&gt;예를 들어, 헤어 스프레이는 향취가 더 중요한 반면 헤어 왁스의 경우에는 향취보다 고정력을 더 중요하게 보는 편입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;예시&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C&quot; aria-label=&quot;예시 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;예시&lt;/h3&gt;
&lt;p&gt;각 토큰별 중요도를 시각화하면 &lt;strong&gt;카테고리별, 상품별로 상이&lt;/strong&gt;합니다. 학습 과정에서 중요도가 달라지기 때문입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1568px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ecaedd79d3a15b2a235a1c1d1770539b/00f4c/9.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 33.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB00lEQVR42mN4/ur1KsPO11KCqQetDJOWSZnGd0nox88XYACC////M4Ho79//q9x/8vLli1fv6hjUj/BqZ67TNE7bxMWADTx++nyaZfc7ecm8w662BWsl3ZJ7hUIrJ4oap81kBRrICDVQ4db9J2devHpTwBC6RcK4cL2uc/ZkYYPEiaJa9avYUAx89fbdfKHcE3wMhnOkjI1nsjLU17MAhRmRXfjh+3elpy/f3Hr99mMJSE7LPosHZKFxWhorTC0cPHj2sn/P+UfSu07eSLl2/1nQ//9fDP7/eRMPNEwUpubdt29ydx+92Pv05bucs9cfSp27fCsLKC+rUfVK0rL7uY79/v8sUKWMDE+fPuX68OGD8t2HT4++ffu2uX//j4C+/d8P9h/4oQl1JSPIpTt3XuC+desf+5MXr52Bam/8//3bhSHxppJt2zU3oDwLiiu/fPkicffRsz1AOoHB/K44f9atEKXyd/wQ2f8w74Ppx8/fmD968erEh6+/zD5//hC18czrZruW23EmLXeDVesfuoC1AF0o+PLth43vPn+L+v/rl83vXz8WAw1QQg7H+noI/ez1a+M3Hz7tef8FGDT//xR//v5nVfr8+woGrbdEfXqeigAAgbADRhNcrj4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ecaedd79d3a15b2a235a1c1d1770539b/263a4/9.webp 480w,
/static/ecaedd79d3a15b2a235a1c1d1770539b/a6361/9.webp 960w,
/static/ecaedd79d3a15b2a235a1c1d1770539b/7ac34/9.webp 1568w&quot; sizes=&quot;(max-width: 1568px) 100vw, 1568px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ecaedd79d3a15b2a235a1c1d1770539b/9aebd/9.png 480w,
/static/ecaedd79d3a15b2a235a1c1d1770539b/a91f8/9.png 960w,
/static/ecaedd79d3a15b2a235a1c1d1770539b/00f4c/9.png 1568w&quot; sizes=&quot;(max-width: 1568px) 100vw, 1568px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ecaedd79d3a15b2a235a1c1d1770539b/00f4c/9.png&quot; alt=&quot;상품 문서에서 모델이 학습에 따라서 중요한 부분에 집중한 영역에 대한 시각화 표현&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;예시처럼, 카테고리별로 속성 섹션 내에서의 중요도 수치를 평균 내면 다음과 같이 차이를 볼 수 있습니다.
카테고리 내에서는 집중하는(중요도) 속성이 비슷하고, 카테고리 간에는 다른 현상은 모델이 상품 특성을 잘 학습했다는 중요한 증거 입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/cfb23623acae055978ffa89a3afc4385/d8a90/10.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 70.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAACH0lEQVR42o2TzW/TQBDF869zQ9w4tXBClQoc2rRCCAkJBQlxQGohVNCWpm3c9IMmsRN/Zr2za+fx1o4VJVQIS7/srr375s3MpiUieVnOMZ+XpB7r9ZKiKFA2lGW1ttau4N4ZY/KWOzCNYhyfXeDXhcfxEr3+AKfnHs4ur9C/ukUwjTAJY0yJd3OHME4q4YdEW26iRRByc5SkSNIUuU4xUzGyWQSlIh7OISaHyjXi1H1TKy6bOR3Wgi7aTOV0MIU/KdB+b0mBnXcWL3YMnr8SnPctgDpt/mBe2IqCOBPOVEGtVhMhZ3SlZphEBvsfM+x1Mrz5JGh3NHY/aBx7zEIZjBMD35FyTjJtkWZZJWgbwdolG0CnSixORyF+DkN4PDDIanpTg+6t4NtNTZccXAvuaUBrVaW7IpikGWuWsUYGRycFvpOvPwRfujUHR0InhkHrNBssybVeFTRkOA5YwwnuR4KtPbJvsNUWbGzrimcvNX6PDDB3xV/tbpwsalg8kLKryaEX4nMvgE9HAIsPW48Ucw4bKodOcL0pTcvFCD8Y3PmCwVDDDw3CyPKe1oTxEreeqVpUa1lN2XXZvRz5Y9aS6e7yqrw22NwWPN7Qf/FkU+PRU423Hamcu/rLepcrGGXGlE8GKbzrCOOAVyOwDzLy3XVp7mG2dIi1x5ZApFyjpPof/8/jmtrsbVF5SCYkaCitDRgtEM5F/sFiv9u70Bj+AefKK2IgAE0AAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cfb23623acae055978ffa89a3afc4385/263a4/10.webp 480w,
/static/cfb23623acae055978ffa89a3afc4385/fb5d2/10.webp 920w&quot; sizes=&quot;(max-width: 920px) 100vw, 920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cfb23623acae055978ffa89a3afc4385/9aebd/10.png 480w,
/static/cfb23623acae055978ffa89a3afc4385/d8a90/10.png 920w&quot; sizes=&quot;(max-width: 920px) 100vw, 920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cfb23623acae055978ffa89a3afc4385/d8a90/10.png&quot; alt=&quot;학습된 모델이 헤어스프레이와 헤어왁스간에 다른 내용에 집중하고 가중치를 주는 예시&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;결론과-향후-방향&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0%EA%B3%BC-%ED%96%A5%ED%9B%84-%EB%B0%A9%ED%96%A5&quot; aria-label=&quot;결론과 향후 방향 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결론과 향후 방향&lt;/h2&gt;
&lt;p&gt;이번 유사상품 추천 Language Model 프로젝트는 기존의 단순 매칭 방식에서 벗어나, 상품 간 가변적인 유사성 기준을 데이터 기반으로 유연하게 학습할 수 있는 AI 모델을 설계하고 적용했다는 점에서 중요한 진전을 이뤘습니다.&lt;/p&gt;
&lt;p&gt;특히 다양한 속성과 사용자 선호도를 포괄할 수 있는 모델 구조, 모델 학습에 최적화된 과학적이고 합리화된 레이블링 방식, 속성 미보유 상품과 카테고리에 대한 추천 커버리지를 100%로 확대 그리고 관련된 추천 화면의 실질적인 비즈니스 성과(기존 모델 대비 CTR 약 50% 개선)를 모두 달성했다는 점은 이번 프로젝트의 핵심 성과로 꼽을 수 있습니다.&lt;/p&gt;
&lt;p&gt;또한, 카테고리 구체화와 같은 예상치 못한 파생 효과는 향후 모델의 확장성과 AI 플랫폼의 경쟁력을 더욱 높일 중요한 단서가 되었습니다.
앞으로도 올리브영 AI 플랫폼은 방대한 상품·고객 데이터를 기반으로, 더 진화된 추천 경험과 더 정교한 개인화 전략을 통해 비즈니스 임팩트를 창출하는 AI 기술 고도화에 계속 도전할 예정입니다.&lt;/p&gt;
&lt;p&gt;앞으로의 행보가 기대되는 가운데 많은 응원과 관심 부탁드립니다. 감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[HLS 기반 숏폼 스트리밍 구현기: iOS/Android 호환성 대응 사례]]></title><description><![CDATA[안녕하세요! 올리브영 리뷰와 커뮤니티 FE개발을 담당하고 있는 뽀야미입니다! 이번 포스팅에서는 커뮤니티 서비스 셔터에 숏폼 콘텐츠를 도입한 전체 과정을 공유드리려고 합니다.
셔터는 올리브영의 SNS…]]></description><link>https://oliveyoung.tech/2025-05-29/short-form-content/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-05-29/short-form-content/</guid><pubDate>Thu, 29 May 2025 19:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영 리뷰와 커뮤니티 FE개발을 담당하고 있는 &lt;strong&gt;뽀야미&lt;/strong&gt;입니다!&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 커뮤니티 서비스 셔터에 숏폼 콘텐츠를 도입한 전체 과정을 공유드리려고 합니다.
셔터는 올리브영의 SNS 커뮤니티 서비스로, 사용자들이 자유롭게 메이크업에 대한 정보나 사용 후기를 나누는 공간입니다.
개발 초기 단계부터 성능 최적화까지, 프론트엔드 개발자 관점에서 마주한 기술적 도전과 해결책을 담아보았습니다!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;숏폼-콘텐츠-왜-중요할까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%8F%ED%8F%BC-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%99%9C-%EC%A4%91%EC%9A%94%ED%95%A0%EA%B9%8C&quot; aria-label=&quot;숏폼 콘텐츠 왜 중요할까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숏폼 콘텐츠, 왜 중요할까?&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;요즘 SNS는 관계 형성보다는 추천 콘텐츠 중심으로 변화하고 있어요. 사용자는 짧은 시간 안에 원하는 정보를 얻고, 더 나은 추천 경험을 기대하죠.
이런 흐름에서 숏폼은 빠른 몰입과 직관적인 정보 전달이 가능해 최적의 콘텐츠 포맷이 됩니다.&lt;/p&gt;
&lt;p&gt;셔터에도 숏폼을 통해 뷰티, 상품 정보를 효과적으로 제공하고,
사용자 경험을 확장하면서 브랜드와의 연결을 자연스럽게 만들어갈 수 있도록 개발을 진행했습니다!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;숏폼-콘텐츠-어떻게-만들까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%8F%ED%8F%BC-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B9%8C&quot; aria-label=&quot;숏폼 콘텐츠 어떻게 만들까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숏폼 콘텐츠, 어떻게 만들까?&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;셔터 내에서 콘텐츠 경험을 더욱 다양화하기 위해 숏폼 콘텐츠 도입을 결정하였습니다.
단순히 영상을 추가하는 것에 그치지 않고, 빠르고 끊김 없는 스트리밍을 통해 최상의 사용자 경험을 제공하기 위해 다양한 기술적 요소들을 검토했습니다.&lt;/p&gt;
&lt;p&gt;MP4, DASH, HLS 등 여러 포맷을 비교 분석한 결과, 호환성, 스트리밍 속도, 적응형 비트레이트(ABR) 지원 측면에서 M3U8(HLS) 포맷이 가장 우수하다고 판단했습니다. 특히, iOS에서 네이티브 지원이 가능하고, 다양한 네트워크 환경에서도 안정적인 재생이 가능한 점이 큰 장점이었습니다.&lt;/p&gt;
&lt;p&gt;그 결과, 여러 기준에 부합하며 저희가 추구하는 UX에 가장 적합한 형태로 M3U8(HLS) 포맷을 채택하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;m3u8-왜-선택했을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#m3u8-%EC%99%9C-%EC%84%A0%ED%83%9D%ED%96%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;m3u8 왜 선택했을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;M3U8, 왜 선택했을까?&lt;/h2&gt;
&lt;p&gt;숏폼 콘텐츠에서 가장 중요한 부분은 빠른 재생과 끊김 없는 시청 경험입니다!
기존 MP4 방식은 파일을 처음부터 끝까지 다운로드해야 재생이 원활하지만 네트워크 상태에 따라 지연이 발생합니다. 이는 사용자 경험을 해칠 수 있는 문제로 부각되었죠.
그래서 &lt;strong&gt;M3U8(HLS)&lt;/strong&gt; 포맷을 선택하게 되었습니다. 그 이유는:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;세그먼트 로드&lt;/strong&gt;: 영상이 작은 조각으로 나뉘어 필요한 부분만 불러와 빠른 재생이 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;어댑티브 스트리밍&lt;/strong&gt;: 네트워크 상태에 따라 화질을 자동 조정해 끊김 없는 재생을 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디스크 캐시&lt;/strong&gt;: 로드된 세그먼트는 캐시되어 재시청 시 데이터 소모 없이 빠르게 재생됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 장점 덕분에 MP4보다 훨씬 빠르고 안정적인 스트리밍 환경을 제공할 수 있었어요. 그래서 최종적으로 M3U8 형식을 채택했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;테스트 환경&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;네트워크: PC 환경에서 네트워크 쓰로틀링 적용 (3G/4G 시뮬레이션)&lt;/li&gt;
&lt;li&gt;디바이스: Chrome DevTools 기반 테스트&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 20px 0;&quot;&gt;
  &lt;thead&gt;
    &lt;tr style=&quot;background-color: #a4d233; color: white;&quot;&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;지표&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;MP4&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;M3U8 (HLS)&lt;/th&gt;
      &lt;th style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;개선율&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;초기 재생 시간&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;2-5초&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;0.5-1.5초&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;&lt;strong&gt;60-70% 단축&lt;/strong&gt;&lt;br&gt;&lt;small&gt;네트워크 및 디바이스 환경에 따른 차이 있음&lt;/small&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;버퍼링 발생률&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;높음 (네트워크 변동 시)&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;낮음&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;&lt;strong&gt;UX 개선&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;네트워크 적응성&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;고정 비트레이트&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;동적 품질 조절&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;&lt;strong&gt;끊김 현상 최소화&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;메모리 사용량&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;전체 파일 버퍼링&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;세그먼트 단위 처리&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;&lt;strong&gt;메모리 최대로 절약 가능&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;CDN 캐싱 효율성&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;파일 단위&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;세그먼트 단위 분산&lt;/td&gt;
      &lt;td style=&quot;border: 1px solid #ddd; padding: 12px; text-align: center;&quot;&gt;&lt;strong&gt;캐시 히트율 향상&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;aws-media-converter를-활용한-mp4---m3u8-변환-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#aws-media-converter%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-mp4---m3u8-%EB%B3%80%ED%99%98-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;aws media converter를 활용한 mp4   m3u8 변환 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;AWS Media Converter를 활용한 MP4 -&gt; M3U8 변환 과정&lt;/h3&gt;
&lt;p&gt;앞서 설명한 것처럼, 빠르고 안정적인 스트리밍 환경을 위해 M3U8(HLS) 포맷을 도입하게 되었습니다.
하지만 사용자들이 업로드하는 원본 영상은 대부분 MP4 형식이기 때문에, 이를 실시간 스트리밍에 최적화된 M3U8으로 변환하는 과정이 필수적이었습니다.
이를 자동화하고 효율적으로 처리하기 위해 AWS Media Converter를 도입하게 되었고, 전체 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;영상 전처리: 네이티브 환경에서 영상의 상태에 따라 압축 및 인코딩을 진행합니다.&lt;/li&gt;
&lt;li&gt;파일 업로드: 5MB 이상 크기의 파일을 분할한 후, Presigned URL을 사용해 S3에 업로드합니다.&lt;/li&gt;
&lt;li&gt;HLS 변환 트리거: 업로드 완료 시 Lambda가 트리거되어 AWS Media Converter를 활용한 HLS 변환을 수행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 하면 MP4 파일이 M3U8(HLS) 포맷으로 변환되어 스트리밍에 최적화됩니다!&lt;/p&gt;
&lt;p&gt;변환이 완료되면 하나의 MP4 파일이 해상도별로 분리된 M3U8 플레이리스트로 생성됩니다. 예를 들어:
&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;plaintext&quot;&gt;&lt;pre class=&quot;language-plaintext&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;eb5b465246a94534b6686a08664dc55f_233647_240p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1318256,AVERAGE-BANDWIDTH=1088734,CODECS=&quot;avc1.64001e,mp4a.40.2&quot;,RESOLUTION=360x640,FRAME-RATE=30.000
eb5b465246a94534b6686a08664dc55f_233647_360p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2740288,AVERAGE-BANDWIDTH=2226457,CODECS=&quot;avc1.64001f,mp4a.40.2&quot;,RESOLUTION=480x854,FRAME-RATE=30.000
eb5b465246a94534b6686a08664dc55f_233647_480p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5130896,AVERAGE-BANDWIDTH=4306140,CODECS=&quot;avc1.64001f,mp4a.40.2&quot;,RESOLUTION=720x1280,FRAME-RATE=30.000
eb5b465246a94534b6686a08664dc55f_233647_720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8619424,AVERAGE-BANDWIDTH=7202333,CODECS=&quot;avc1.640028,mp4a.40.2&quot;,RESOLUTION=1080x1920,FRAME-RATE=30.000
eb5b465246a94534b6686a08664dc55f_233647_1080p.m3u8&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 생성된 M3U8 플레이리스트는 사용자의 네트워크 환경에 따라 적절한 해상도를 자동 선택하여 끊김 없는 스트리밍 경험을 제공합니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1378px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c6e512c35b4690e2c2c0c23e35d55cbc/d234e/m3u8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 65.20833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB7klEQVR42n1Ty3LTQBD0iQtURdJKK6319EOyFNux/AgYleMqOzGcuFN8AcUv8PHNzMSylRA4TM1otdvb3TPbc10X3fA8D1rrv8L3/Rf/uH59lqPXBeLMm5MkQZZl2Gw2WK/XWC6XUjdNg6qqEEWRRPfcBZAXlFKwLAu2bcuGIAgkjDGSW4btd7v2JkMGC8MI9aLGeDwW8PZWBkjiGNPbW+T5GH7g/xPoAsisyqrE49MjVqulLLag8/kc9x+3OD59xcP+SN93SNMUjuNcLuW6K72njUY8izGoB0juYpg0QNgPxSNDOa4+I6p2iKd7RKMpMfSIaSCH+/2++N1l3fOMh+FqiPVhhfxTjv7AYJgNRWKSDZDNHlDvviFfnZAWC7rMCBAHW1QUhXjaqpKmuB5RV7ZkqUkGWyGbfAKIh9AmJgbPDWwlhgTqE7uu77226OZ23nhjUUxwOn3BdtsQE3PZo5QLy1HQxO7m7KNuGb6O62wq5KmN9TzBrNAIA0tYusyOxqiZFDguazTlBKkh2d3BfvFauNN00PccLEbvcLj3sZ2+xyj6QIxLFGWJmsB+7nf4/eM7fh32mCURbFL0JsOrbE9YRYYiuEGgeVyen50hhpMoRE5N4hxy51ny/4ZU5kxpYStZXe1gf9k765zVWdkfLG5VO4VoxbkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c6e512c35b4690e2c2c0c23e35d55cbc/263a4/m3u8.webp 480w,
/static/c6e512c35b4690e2c2c0c23e35d55cbc/a6361/m3u8.webp 960w,
/static/c6e512c35b4690e2c2c0c23e35d55cbc/640c8/m3u8.webp 1378w&quot; sizes=&quot;(max-width: 1378px) 100vw, 1378px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c6e512c35b4690e2c2c0c23e35d55cbc/9aebd/m3u8.png 480w,
/static/c6e512c35b4690e2c2c0c23e35d55cbc/a91f8/m3u8.png 960w,
/static/c6e512c35b4690e2c2c0c23e35d55cbc/d234e/m3u8.png 1378w&quot; sizes=&quot;(max-width: 1378px) 100vw, 1378px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c6e512c35b4690e2c2c0c23e35d55cbc/d234e/m3u8.png&quot; alt=&quot;m3u8으로 영상 변환 흐름도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;
 [변환 과정 흐름도]
&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;업로드-페이지-네이티브-vs-하이브리드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%85%EB%A1%9C%EB%93%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-vs-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9C&quot; aria-label=&quot;업로드 페이지 네이티브 vs 하이브리드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;업로드 페이지, 네이티브 vs 하이브리드&lt;/h2&gt;
&lt;p&gt;업로드 페이지를 구성하기 전에 웹과 네이티브 각각의 방식에 대한 한계와 장점을 고민했습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-웹web-기반-구현의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9B%B9web-%EA%B8%B0%EB%B0%98-%EA%B5%AC%ED%98%84%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;1 웹web 기반 구현의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 웹(Web) 기반 구현의 한계&lt;/h3&gt;
&lt;p&gt;웹 환경에서는 브라우저의 네트워크 자원과 실행 환경만으로 영상 인코딩 및 업로드를 처리해야 하는데, 다음과 같은 한계가 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영상 인코딩 성능 제한:
&lt;ul&gt;
&lt;li&gt;FFmpeg.js 같은 WebAssembly 라이브러리는 CPU 사용률이 높고, 멀티스레딩 지원이 부족해 인코딩 속도가 느림.&lt;/li&gt;
&lt;li&gt;모바일 브라우저에서는 배터리 소모가 심하고, 일부 기기에서 인코딩이 원활하지 않음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대용량 파일 업로드 제한:
&lt;ul&gt;
&lt;li&gt;네트워크 중단 시 자동 재시도 기능 부족, 업로드 실패 가능성 높음.&lt;/li&gt;
&lt;li&gt;백그라운드에서 업로드가 종료될 수 있어 안정적인 업로드 어려움.&lt;/li&gt;
&lt;li&gt;브라우저 메모리 제한으로 업로드 도중 중단될 가능성 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;2-네이티브native-전환-고려&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8Cnative-%EC%A0%84%ED%99%98-%EA%B3%A0%EB%A0%A4&quot; aria-label=&quot;2 네이티브native 전환 고려 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 네이티브(Native) 전환 고려&lt;/h3&gt;
&lt;p&gt;웹 기반의 한계를 보완하기 위해, 우리는 영상을 네이티브 앱에서 처리하는 방안을 검토했습니다. 네이티브 환경에는 다음과 같은 장점이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;더 빠른 인코딩 성능:
&lt;ul&gt;
&lt;li&gt;iOS VideoToolbox, Android MediaCodec 활용해 GPU 가속 가능 → 빠르고 전력 효율적.&lt;/li&gt;
&lt;li&gt;웹은 CPU 연산만 사용 → 속도 느리고 배터리 소모 많음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;안정적인 업로드:
&lt;ul&gt;
&lt;li&gt;멀티파트 업로드 + 중단 후 재개 지원으로 대용량 파일도 안정적.&lt;/li&gt;
&lt;li&gt;백그라운드 업로드 지원으로 앱 종료 후에도 업로드 지속.&lt;/li&gt;
&lt;li&gt;네트워크 상태에 따라 속도 조절 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h3 id=&quot;3-하이브리드hybrid-선택&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%ED%95%98%EC%9D%B4%EB%B8%8C%EB%A6%AC%EB%93%9Chybrid-%EC%84%A0%ED%83%9D&quot; aria-label=&quot;3 하이브리드hybrid 선택 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 하이브리드(Hybrid) 선택&lt;/h3&gt;
&lt;p&gt;그러나 한 가지 문제점이 있었습니다. 셔터 커뮤니티의 서비스 특성상 UI 변경 및 A/B 테스트 등을 자주 수행해야 하다 보니, 100% 네이티브로 구현할 경우 배포와 업데이트에 있어 유연성이 떨어지는 아쉬움이 있었어요.&lt;/p&gt;
&lt;p&gt;따라서, 네이티브의 강점과 웹의 유연성을 결합한 하이브리드 방식을 선택했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;영상 전처리(압축, 인코딩)는 네이티브에서 수행하여 성능을 확보하고,&lt;/li&gt;
&lt;li&gt;업로드 및 UI 관련 요소는 웹에서 처리하여 서비스 운영 및 업데이트를 원활하게 진행할 수 있도록 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 함으로써 성능과 유연성을 모두 고려한 최적의 업로드 경험을 제공할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;img src=&quot;/fb6ba7a456daabfce4984d05f3cc5429/upload.gif&quot; alt=&quot;숏폼 업로드 과정&quot; style=&quot;width: 200px&quot;&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;스트리밍-구현은-hlsjs로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%EA%B5%AC%ED%98%84%EC%9D%80-hlsjs%EB%A1%9C&quot; aria-label=&quot;스트리밍 구현은 hlsjs로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;스트리밍 구현은 HLS.js로&lt;/h2&gt;
&lt;p&gt;HLS.js 라이브러리는 HLS 기반 스트리밍을 웹에서 쉽게 구현할 수 있도록 도와줍니다. 이 라이브러리의 장점은 이렇습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;네이티브 HLS 미지원 브라우저에서도 재생 가능
&lt;ul&gt;
&lt;li&gt;Chrome, Firefox 등 기본적으로 HLS를 지원하지 않는 브라우저에서도 MSE를 활용해 HLS 스트리밍이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;세밀한 화질 제어 및 어댑티브 스트리밍 최적화
&lt;ul&gt;
&lt;li&gt;기본적인 HLS 어댑티브 스트리밍 외에도, 개발자가 화질 전환 로직을 직접 조정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;특정 네트워크 조건에서 화질 선택을 미세하게 튜닝 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실시간 이벤트 핸들링 및 오류 복구 기능
&lt;ul&gt;
&lt;li&gt;네트워크 끊김, 버퍼링 문제 등을 감지해 자동으로 복구하는 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;특정 오류 발생 시 커스텀 복구 로직을 적용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;디버깅 및 개발 편의성
&lt;ul&gt;
&lt;li&gt;hls.js는 다양한 디버깅 로그를 제공해 문제 해결이 용이합니다.&lt;/li&gt;
&lt;li&gt;개발자가 재생 상태를 세밀하게 제어할 수 있도록 다양한 API를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;img src=&quot;/0579e6346c205a1eccc3c7ddc6dd3abf/streaming.gif&quot; alt=&quot;숏폼 스트리밍 예시&quot; style=&quot;width: 200px&quot;&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;숏폼-콘텐츠-문제-해결의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%8F%ED%8F%BC-%EC%BD%98%ED%85%90%EC%B8%A0-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;숏폼 콘텐츠 문제 해결의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숏폼 콘텐츠, 문제 해결의 여정&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;숏폼 콘텐츠를 구현하면서 예상했던 문제부터, 예상 못한 다양한 이슈까지 마주했습니다. 그중에서도 특히 고민이 많았던 몇 가지 이슈와 해결 과정을 공유해 보려고 합니다.&lt;/p&gt;
&lt;h2 id=&quot;ios에서-스트리밍이-안-된다니&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ios%EC%97%90%EC%84%9C-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D%EC%9D%B4-%EC%95%88-%EB%90%9C%EB%8B%A4%EB%8B%88&quot; aria-label=&quot;ios에서 스트리밍이 안 된다니 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;iOS에서 스트리밍이 안 된다니!&lt;/h2&gt;
&lt;p&gt;HLS.js를 사용해 HLS 스트리밍을 구현했지만, iOS 15~16 버전에서 정상적으로 재생되지 않는 문제가 발생했습니다.&lt;/p&gt;
&lt;p&gt;처음에는 해당 OS 버전에서도 MSE가 지원된다고 명시되어 있어 예상치 못한 문제였습니다. 하지만, 실제 테스트 결과 일부 iOS 환경에서는 MSE 기반 재생이 원활하게 동작하지 않는다는 사실을 확인했습니다.&lt;/p&gt;
&lt;h3 id=&quot;원인&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%90%EC%9D%B8&quot; aria-label=&quot;원인 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;원인&lt;/h3&gt;
&lt;p&gt;iOS의 MSE 지원이 공식적으로 명시되어 있음에도 불구하고, 특정 버전에서 정상적으로 동작하지 않는 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;MSE 지원이 불완전하거나 제한적
&lt;ul&gt;
&lt;li&gt;iOS의 MSE는 기존 데스크톱 브라우저와 완전히 동일하게 구현되지 않았으며, 일부 기능이 제한적이거나 특정 환경에서 예외적으로 동작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사파리의 네이티브 HLS 우선 정책
&lt;ul&gt;
&lt;li&gt;iOS 사파리는 자체적으로 HLS를 지원하며, 네이티브 HLS를 기본적으로 사용하려는 경향이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h3 id=&quot;해결-네이티브-hls-고마워&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EB%84%A4%EC%9D%B4%ED%8B%B0%EB%B8%8C-hls-%EA%B3%A0%EB%A7%88%EC%9B%8C&quot; aria-label=&quot;해결 네이티브 hls 고마워 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결: 네이티브 HLS, 고마워!&lt;/h3&gt;
&lt;p&gt;이 문제를 해결하기 위해 iOS 환경에서는 사파리의 네이티브 HLS를 활용하도록 분기 처리를 추가했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일반적인 브라우저(Chrome, Firefox 등) → HLS.js를 사용하여 MSE 기반으로 재생&lt;/li&gt;
&lt;li&gt;iOS 사파리(특정 버전 포함) → 네이티브 HLS를 활용해 직접 재생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 구현함으로써 iOS에서도 안정적인 HLS 스트리밍을 제공할 수 있었습니다&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;android-색상을-왜-그렇게-뽑아&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#android-%EC%83%89%EC%83%81%EC%9D%84-%EC%99%9C-%EA%B7%B8%EB%A0%87%EA%B2%8C-%EB%BD%91%EC%95%84&quot; aria-label=&quot;android 색상을 왜 그렇게 뽑아 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Android, 색상을 왜 그렇게 뽑아?&lt;/h2&gt;
&lt;p&gt;Android 기기로 영상 스트리밍 테스트 중, 밝은 영상 일부에 백화(화이트아웃) 현상이 발생했습니다.
영상이 제대로 표현되지 않아 원인을 조사한 결과, 다음과 같은 사실을 확인할 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;원인-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%90%EC%9D%B8-1&quot; aria-label=&quot;원인 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;원인&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;HDR 영상 촬영 증가: 최신 디바이스는 Dolby Vision(HDR) 촬영을 지원하며, 특히 iOS는 기본적으로 HDR로 촬영됩니다.&lt;/li&gt;
&lt;li&gt;Android 기기의 색상 표현 한계: HDR은 SDR보다 넓은 색상 범위를 가지지만, Android 기기에서는 완벽히 지원되지 않아 색상이 정상적으로 노출되지 않을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h3 id=&quot;초기-접근--hdr--sdr-변환&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B4%88%EA%B8%B0-%EC%A0%91%EA%B7%BC--hdr--sdr-%EB%B3%80%ED%99%98&quot; aria-label=&quot;초기 접근  hdr  sdr 변환 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;초기 접근 : HDR → SDR 변환 &lt;/h3&gt;
&lt;p&gt;초기에는 이 문제를 해결하기 위해, HDR 영상을 SDR로 변환하는 방식을 채택했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS MediaConvert와 Lambda에서 FFmpeg을 활용하여 업로드된 HDR 영상을 SDR로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 인코딩 파이프라인을 개선한 결과, Android 기기에서도 색상 왜곡 없이 영상을 정상적으로 표시할 수 있었습니다.
하지만 변환 시간이 길어, 영상의 퀄리티나 해상도(크기)에 따라 인코딩 소요 시간이 달라지고, 일부 영상은 5분 이상 걸리는 경우도 발생하며 사용자 경험에 부정적인 영향을 줄 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;해결-클라이언트-기반의-동적-톤-조정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B8%B0%EB%B0%98%EC%9D%98-%EB%8F%99%EC%A0%81-%ED%86%A4-%EC%A1%B0%EC%A0%95&quot; aria-label=&quot;해결 클라이언트 기반의 동적 톤 조정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결: 클라이언트 기반의 동적 톤 조정&lt;/h3&gt;
&lt;p&gt;최종적으로는 AWS MediaConvert 인코딩 과정에서 영상의 HDR 여부만 사전 판별한 뒤,
Android 기기에서는 해당 정보를 기반으로 클라이언트 측에서 동적으로 톤을 보정하는 방식으로 전환했습니다.&lt;/p&gt;
&lt;p&gt;이를 통해 SDR로 영상 타입 자체를 변환하지 않더라도, 디바이스 특성에 맞춰 색상을 최적화할 수 있었고,
추가적인 인코딩 과정 없이도 영상의 화이트아웃 현상을 효과적으로 완화할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;display:flex; justify-content:center; gap:10px;&quot;&gt;
  &lt;div style=&quot;width: 200px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 792px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7b889681a7eea602602fdb0a1022f1c5/59f54/before.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 181.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAkCAYAAACJ8xqgAAAACXBIWXMAABYlAAAWJQFJUiTwAAAI50lEQVR42l2W+U+VVxrHbyaZzA+TNJmZ/hHzwyQzTlusVVQWRdmURWSRRUAREaxVUFxAKzsKxYWLgLRSpVoF3EAREBVEQFa5LBcumyBw2SmyiHA/89yXmkz6Jt8879m+51nPOaqAvb54e3rg7uaCk+M2bKytsDDbyNerTfjaxIT169ZisXEDmy3NsbPZgrurEx6C7fY22NtaC7Yq0mGbnQJV2JEQgoP24b/bS0h3sH27HZs3W/LNN2tYt3YNm8w3sFXadtZbcHHajo+XG54eLsq/Ec6ixA6Bq4sTO50dUcVFRxJx8ihBgf747vZUBqxlsanpOjZsMMXaahP2opnTNltZsB33nc54uu9QSHe5C8QyY5/3Lle8PHaiSjkfw5nIoxwRTQP3+Smd9va2bNy4HnOz9diLCxy32eAiWnjIQl/RMMDfiz27d7Hbyx1fbw9Feu9yw8fTHVX2j2pSkmKIPB3O4UMH2OPnhaMsNjPbgIWYu912C87bbXEV7TzdjISuCvx9PITUU4G/kBvb/j5eqO7l5/BT1iWSz0eL6WEE7ffDWXxjLmSbLDfgZG8thDa47XDAS0z1FW0CfD3Z57eCAMFeIQ30342/9y5URYV3uHkjk9SL5zh75jjBBwJwcLAX/60Tk01xsDP6b6sCB2sLnGw34epoi5ebk7jHVcEu1x14uTri4eKMqvxFEXl3fuaKOpnoqAhCggPFh9YSlG8kyl9jsX41lgJjsCKjzxOblE5y5l3OJN3gZGwWx2N+5FhMJqEnYnEQ36veNNVQVFRAdnYm8QlRHAwJkihvxeSrVXz5n3+xJyCE9F9KKH4zxjPtLBn5tcReuo1f4GG8ffezO+AQB8OTeKFbIDDkW1SdnS08f15Cfn4uycnnOHBgP+tN1+C00wN1zjOKNdMUaWY4Fp2Blc02TEy+wHTtakn2tWy1NGOzuaTWFmte6cZJSruGavBdnxAWc//hQ9Rpar49GIytnT2pv7ykZhDUN1/g7OKJyRf/Zu2ar7AUEnPzjZiZG7NgI5YKLHjZMUROQRmq8fERSksKKSh8wJXMDFx3uhAYGsvPT3qJE9OOHTlCZPgh4s5G4CepYfLlKqmi1UrQrK0s2bLJDJcdLnRMzHGr8AWqDx/mqaur4smTR2RmZmIn2p04e4Vrd2qw2OzIX/78Jz7/22f87bO/8vk//o6Ptw//XbVKyL1JTojmxk+p5OXn0T0L1V2TqAyGZTo72ygpKeHChQtY224jLk3Mz2skp6yPlBtPcfb5jn+uMuXk9/EYv6HhYQbfvaOqqprGmnLq62romjOgnTKgMk7of9tLaWmpRDmBtaZmpFx/wePmeU6kPCA5I5fZj9DaO8bI5CyG5WUMBgPLIo3f9OQEL16+oks07J5ZXiHU64coK3vKqZOnMN/iTE5pPxU9UNAwzJ0ndcpCoyUGVj4j2SfC5zVtPKhsZ3gJuqZ/J5yamKSh6jV3fvmVS5cyeVbTS3XTANrOPnreDtPbO8DQwCD6IT39vX28e9u/0h4corFBQ90bHSNzC+iFVCGcn5ujs01Le3MrNc9fUVtRRX1FNU119XRo2nhZVk57WzvlT59TV11LS5OG5vommusa6WhpQ9fVw8jsApqxjyuEC3PztMhAZXklj+8V0VBTT2N1PW2aVjq1HWgammiX8c72Ttplg6b6RnqFpE2IK2QznVbHxCJkPaxfIfwoztGVPKPvVSXpKWquZhfRpZ9iePw3dH0TihwcmWTi/QLD+gn0oxNMTv3G3Pw870WZ/neDFNX2sedgpBAurzh7oegBhtJCo0fp6uimuGGE++W1ZGRmo866Tad+nlmJw4JE2Dh/WZEGxueXeN2/yI1SLVGJV4TQ8HvsxgYwPMjBMD8KSx+4mV/FvkOR+Lg74+jgTENbH+JzhqcXmf+wxAfRbOH9DIO/GXjcMkNmgYb8wooVkw3Lxr1E0/5Wloa7YWKQ7LhLuLj54bpti1wDthQUVyr7NnaOMv1+kdnpKeamJ6hpG+V6WS8RP9zkcUm5sVI+ZZfIpVkxZRx6q6nPz8fJ2RU3IbQyN6eiplmZ1dk3zpB+lvmpcdFylrwSDQfPXMN//1HybucZNTTwcWmJxQ8LfJztkn/J6I8TzM5MyqEQiofcLw5yCz4sKFIIu3snGRmdEtfMMDkxxQ8/PSEkLIqLKRc4Hx2FqqenhzS1mrS0NJEX5UxMpKr6tWi7wNPCe/jv2omPqzNlpSUKYc3rVvp6+pXxEf0odx88prOjRWq6kv0yVzUwMEBh4SMKCh5xPy+XvNzbtGoa+TgzgeZNE6FBewgL3icV0s3U1DQ93b2Mj44wMznO5PgYsxKYhcVFXpcWcVpuTZW2rY3im9eZlCKf0Q8zNaZnZETP2OiY1LiexvpatG2tDPT3K+2RkVGGpeyGpOwG+vswKjQic+9mpnMx7gyqdk07r57l09L9WnZ/i06no6enl94+qVk5ovoH3smCUTnidIIuReqkenRarZiqpVtc9qahgYvRZ7ggjwaVpvkNnV06tFopq/Z2kR20y+T2di1t0jaipaVVGauvKJGyq6W5pUX6NGg7jHM7KC58yOljh7manoLqedljnhTm8aL8JU+flinnYmnpUzlwS/+AEkqKHlFSXKy0y8qeKfMrX1WRlZFGxIkw7uZeR5V/9y5Hwk9gZW1DcupV4uPjiY2NJSEhkYTE/8c54uITiImJVXD8xAkiIiK5Itnx3aEgEuOjKCrMRfXjtSwSU1KJCT9MevavJCUlc/nyZUmfZFkYI+RxskmCgu+/P8upUxGEhobi7u6qvL5ORxznQNBeks9Fc//eLVSXUy+SlnWdW7dyycy4quwcLURhR48SHBxCSMhBDgQHy319QJHB0g4KCmKHPJ48pM6trCw4eTKc/Ds5ZF+7iupcYrQQRBObcF4xy2hOdLRoFhevmPgJRg2NfbFxcYIYub+D2CfPv4C9fkRFRZGVJe+jyxdQJSclcDXrKunpGVxONVZMugJ12hXU6j8g7RPUREmaHAsPE4vCiRG3pKrTuHjhB/4HpVAWh/HCvkkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7b889681a7eea602602fdb0a1022f1c5/263a4/before.webp 480w,
/static/7b889681a7eea602602fdb0a1022f1c5/60a83/before.webp 792w&quot; sizes=&quot;(max-width: 792px) 100vw, 792px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7b889681a7eea602602fdb0a1022f1c5/9aebd/before.png 480w,
/static/7b889681a7eea602602fdb0a1022f1c5/59f54/before.png 792w&quot; sizes=&quot;(max-width: 792px) 100vw, 792px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7b889681a7eea602602fdb0a1022f1c5/59f54/before.png&quot; alt=&quot;hdr 톤 보정 전&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
   &lt;div style=&quot;width: 200px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 792px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8e81533e0ef8c6dd52f8a10a2854eaf2/59f54/after.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 181.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAkCAYAAACJ8xqgAAAACXBIWXMAABYlAAAWJQFJUiTwAAAJZElEQVR42lWWCUyUZxrHJ7ubbNLN7naTzabptrXKKQy33CAIHqCoIKcohzAMCMM1w3CpSLEV8JYCVZTbCxWlcoyIgIBUqYIgHuDRAopHUaumXaOov31nTDbZSf55v3nyvv/v/xzf+zwSj3luuLo44+TogJ2tDZYW5pjONkFfTw/9WbMwNjTE1GQ2ZlJTrCwtcHSwE3vtmGNjhbWV5f8wx8ZaB4mPjxeLFs7Hba4Ljo722IiNUnFYX18fAwN9pCbGmEuluhdpX+jq7Iizk73uWQvbOR/gYG+LvZ0tktCVQQQF+OHp4cZcV2dhnIOFuRkGhgYYGRlibiaUWZhhY20pCKxxEIecHbUq7XHSqhXQ2lycHAUctISB+C1fgrfXQrTuazdqXTMUZMYCluamWFuaYyuUO9jPEQodcHdzxs3VCRfxrFWsXV2Eah1hTHQ4oSsDWO7rg5dwfa6LE9ZCjZGRgSA0wMpcio3VB0JHQfjhoJ3Y5/h/0BLPdXZCkpQoJzpqNSHB/iz18RYqXXXuGRrqY2xsgLWFVCg0w05LaPeBUEvgpoXrh1X7312ES6dQlZpAnDySsFXB+AqVnp5uWAoX9Q1mYWSgJ1w2ETGU6oitpMZYm8/G1spMkFuLjNsK1XbYi+Q4ivjai+RIMjNVKBLkRISvZMWKZYJwHuYiCXp6M5k180uM9GdgrDdDuGzNshUh+AdHEhKZiG+IHB//aJYILPaPxHuJvy55kvzNeSiVKcTErCEwcAWe8+dhKsrmi8//zeeffoLrXA8iYtWo8/aQmV9JpCIX/1XxuLgvxMl5Hs5zFzB/cTBZBbW4eS5CUlmxj7S0VBITFaxcGYyHxzxR0DOxsrEjXJ5O2qb9AuV4+0UyW2rBjBmfozdLKBchMRUxNjHSx8zUhHUFVQSHxSPRtDShTE0iRZlKeEQY8xd4YGZuTlhsNht2HiUsLhtrGwe++OxTZs78gtnGH8rJUFcFhrpK0CK7oBx5Si6S/v5LpKYqhNupRKxZI6p+Du5eK4hRbSNAuOa9yItlixcS4LtMl+FPP/nXB5UixtqvyHS2EXOsrckvOUGs8mskDx5MsnHjeuG2kjWC0Fyo8/GNQJaQi7GJFX/64x/4218/4i8f/Zl/fPwxwcHBgkwPD3c3ggN8iQoPIVGhoHDvKdYX1iB59eoVO3duQ6VSEbpqFVIzCwLCkglXfEVsxnZCZOlY2Hvy939+RpQ8Ae3vwYMH3Lt/j66uLop2FAhBORSWNbK59Hsk73lHbU21cFtJkO7thoTGZJL2zX68AuOJV+Zy8844Ta3nuDZyk+npl7x7/563b6d15MND/agzstlS1iRUNiLRGptbmklQJLB4yWJmGpgRrdpCekE1cVklZG0u4fX0a93h9+/fCbLXguwNb9+9FZZptu0uRab6hp3VHRTuaUby+s1Lhq9c4fCucnZm5bMhOYft+dWUFR3nVG07LbXdtB7r4ecro0xcv8ud/hvc6bvOxNAdxodvcbqmifr9TZTWdrK9qgPJ46kxnj18zOCpHxho6OV02UnaKxvp2NdI39FOrjb20bL/BAMtfWj21NMq3LpY30X3oTbaKpsYFGe6TvTw3YFucksakIxN3OS3J8+4dPIHNFXNHNhaw7kDZ9B8d4rL9ecZbP2RH0900V/fy8VjvVwSBBfqOrnW3E/PwTOcLD7MkX0ayo72EZmUh2RifISXL6Y4W1zNwLdlfBUegzI+h9L9bRw42EtVdTcNjcPUHR/gWMdPHPt+iJOnrnPm7M80dE5S13KL+kM9rM/bh7OHD5KH9+7w/D+PGS3ayt2cVKgooj8vF/WmfaxdX0x0QjZha9dTUNXDgbZ71HY9pPvmb4xP/c6l0acca79G+vY6ZOpt+AWFI3nx9CGTj+4w3XOa8ahw3pdshfoqslM24uK5DFc7K9GgzNhffpLRvhFK6i7TdWGE508e8uvUI85dGCYxp4yo1M0oUrNFll/9yt1bV3j30xDTW/N4V7qdtwe/I3XFKqzsXLCzlGJsYExZxX6eT46wofAI7V0XRCKv8uzRJBUHG4hMKWR5aDxKdZaowzcv+On2IL/fvwENNVD1LW/y0ilTpWIpbhwHKymzZnwpYnhIkNwlv7SVzo52nk38yMsnE2zcvBs3n0ic3b1RxCcIwrcvmbg1yM/DF5iu3M3boq9531ZHW9NBli72wtHGEhPRUg8dquSXe2Pk76inr6eNV79c5eHYDSLkahYsCSQ2NpaVAQFIbl/vZ0fhJvJysij+Zj3b8jK5eKmRoXMNrBP3pJvogrZWVnx/pJyx64OoM3fRd+4I714OMjqoIV6RwpYt+Wwv3IyHaAmSq/3nqTtYwd7ibVQUb+bw3gKGanfx7EIXGk0z80SHW+DuylBHC09vj9HT2svdgTZeTPRwpfsEhw5Uc+ToITZlZ7BM3KWSiz3taGrKuTZ0nomrF5nSHGfyTCNTjx7z6NEjujs7uTRwmfEzrUzVHOFpUyf3uwaYunKRs8eqaNE00dbWzLqkBFYH+iE5f66Ns42V9F9rY3x8ktt37zI2OcnY+Dj370/yYPIBT54/5/aFi9yqPcxo6V5ubirkel0VjYLw8kAfzU31xEWsJiw0SLSA5uNomo7yQ3c7gwOXuH5tmJs3bjAyMsrI6C1GBW7cuMno7VsMiGQMdXQw3NDEUG83vT1nGbp6mT2lReJWX0T0mtVI9hUXULBRRXV5CbUVJRyo2sPh2nKOHKzhWN1hTp44TlNTI6c1pznbqqG9s4OO7h56envRNJ6gpfEkapG8YNExU5PXItm59StxE8fiJuaaTYXbdcaUpDjSlIkok+NJVsSSnChsyQphV5CalIhapSQ5KUmsKWzKXaebOOTyKDLUyaLRZyRSsKOYbRuz+HZftWhWyeRsyCYzI40kxVqSBJl2TYiXExkeSkiQnyDwwkXMQFoE+vuyYL4HYatDhAgFkpREGbtKy6mrO05R0W7RqMJJTk4iIjKcoKAAAX8CxLi3fLkPfn7L8PNdirf3Qt08qJ26pFIT3Z7cnHWiWcUiiYpcSWhoCLJYOTFymZggZETLopDFRCOTRevWuLVxyGNjiI2TExcfR1jYKt2Q6jnfnflCXYwsUjfOyMTlIokVI4hKxCJNrdT15vSMdDKzMlCr00hPV5OVlUlGZgZK8W2r0lRin4r4hLWEiCnDT7gbKLxISk4Qw0Iictka/gs1LNvg7Qvu9AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8e81533e0ef8c6dd52f8a10a2854eaf2/263a4/after.webp 480w,
/static/8e81533e0ef8c6dd52f8a10a2854eaf2/60a83/after.webp 792w&quot; sizes=&quot;(max-width: 792px) 100vw, 792px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8e81533e0ef8c6dd52f8a10a2854eaf2/9aebd/after.png 480w,
/static/8e81533e0ef8c6dd52f8a10a2854eaf2/59f54/after.png 792w&quot; sizes=&quot;(max-width: 792px) 100vw, 792px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8e81533e0ef8c6dd52f8a10a2854eaf2/59f54/after.png&quot; alt=&quot;hdr 톤 보정 후&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;
 [톤 조정 전, 후 비교✨]
&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;숏폼-콘텐츠마치면서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%8F%ED%8F%BC-%EC%BD%98%ED%85%90%EC%B8%A0%EB%A7%88%EC%B9%98%EB%A9%B4%EC%84%9C&quot; aria-label=&quot;숏폼 콘텐츠마치면서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숏폼 콘텐츠, 마치면서..&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;숏폼 콘텐츠 도입부터 스트리밍 최적화, 디바이스 이슈 대응까지 쉽지 않은 과정이었지만, 결국 사용자 경험을 최우선으로 두며 하나씩 풀어낼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;특히 기술적으로는 M3U8(HLS) 포맷 도입을 통해 보다 빠른 영상 로딩 속도를 제공할 수 있었고, 세그먼트 단위 캐싱 구조를 활용해 CDN 캐싱 효율성 또한 크게 향상되었습니다.
이러한 기술적 기반 위에서, 실제 서비스 내 활성 사용자 수(AU)는 약 21% 증가, 콘텐츠 기반 전환율도 5% 개선되는 등 비즈니스 측면에서도 의미 있는 성과를 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;물론, 아직도 해결해야 할 과제들이 남아있습니다.
페이지 렌더링 최적화, 콘텐츠 반복 노출 최소화를 위한 정렬 로직 개선, 숏폼 컴포넌트 공통화 등 더 나은 서비스 경험을 위해 계속 고민하고 있습니다.&lt;/p&gt;
&lt;p&gt;앞으로도 이러한 고민을 지속하며, 더 나은 서비스 경험을 제공할 수 있도록 끊임없이 개선해 나가겠습니다.
셔터의 다음 변화도 기대해 주세요. 🙇‍♀️&lt;/p&gt;</content:encoded></item><item><title><![CDATA[1인 QA의 품질 관리 프로세스 구축 이야기]]></title><description><![CDATA[안녕하세요. 라이프스타일 플랫폼인 디플롯 서비스를 전담하고 있는 QA 엔지니어 콩🫘입니다. QA…]]></description><link>https://oliveyoung.tech/2025-05-29/dplot-qa-docs/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-05-29/dplot-qa-docs/</guid><pubDate>Thu, 29 May 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 라이프스타일 플랫폼인 디플롯 서비스를 전담하고 있는 QA 엔지니어 콩🫘입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/be5f3d75404414e9bbc2f46eda98ebfe/3db4c/dplot-4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAACnElEQVR42k2S7U/SURTHf/9VL3Jra5mbTst6Yz4VCaUooGCgggL95EEEV4JP2JDCB9Z8qLm2xEz0lVvaWvWubK6WQ4eamQk4+XRl1jrbd+fee879np3vOZK+qYmG+nqUNTU06jQEg0MEAr34ex8QCoWIRqOMj48zNRnl5ewYy3MR3i2Hib8YxulyY7PbsbSbaTMZ6JQ7kVqMRpr1TSiVtUQG2ngdm8bX8xCn08nU1BSnljk+JpPJcPg7xc+DI/YPUuz9+EVoJITH4yHg9yPbO5BtFiSdVotO24BCoWTIp+fDWpy5uXlmZ58RDoeYGH/MzMxTFhZe8fbtGp8/f2Jd4OOH9/T19ecKu90uHA4HTZpapLrau6JdBZVV1bjtOmanx3g0PMSTcBCbpYXCC3l4u2QmoqMsLy/lSFffrLKysoLP68VsNmOz2bBarVjEWaosv0F1dYUgrMBsVDPg9/Kgx0O3WyYyGqbsSgkPfV6eT04yPx8jFouxuBgnHl9ClmUMBgNGo0nAiMlkQiq9UkxVZTklxUVUlF3H2t5Ky71Guj0ObFYLN0XBYCDAxNgYI0KzgcFB/AE/o6MR2lpbqatTo9FoBLRoBaSC/EsUFxWSd/4cBfkXMYoBKSrKqLujpNmgxydE9zldBPoDuLu6cJxqJrzLJaMTmlUJqRQKRQ41YlOkgsuXuFZ6ldKrJahUNWJSVuT7nbSJNu7bbXgFoUsI3iFWw+3pxmIRvsuDt9sliqu5pbiNWq2mXqyeSqVCSiaTnCKdTvPXTjK/2EluifcdviU2OUqnOEqlQPjdnR2+ft9kb3eXk5MTstls7k9KxDc2NpD4386CR7tfSB/ucXr7tP6e7W1BsH9Advs7ya0t1je3ztKz/wgTiURuYH8A45M8KiM2tzoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/be5f3d75404414e9bbc2f46eda98ebfe/263a4/dplot-4.webp 480w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/a6361/dplot-4.webp 960w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/0b34d/dplot-4.webp 1920w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/da28f/dplot-4.webp 2880w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/e74a8/dplot-4.webp 3828w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/be5f3d75404414e9bbc2f46eda98ebfe/9aebd/dplot-4.png 480w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/a91f8/dplot-4.png 960w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/ac7a9/dplot-4.png 1920w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/f9c26/dplot-4.png 2880w,
/static/be5f3d75404414e9bbc2f46eda98ebfe/3db4c/dplot-4.png 3828w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/be5f3d75404414e9bbc2f46eda98ebfe/ac7a9/dplot-4.png&quot; alt=&quot;dplot 4&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;디플롯 서비스 홈 화면&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;QA엔지니어로 일을 하다 보면 다양한 환경을 경험할 수 있습니다. 각기 다른 환경을 겪다 보면 때로는 게임 속에서 퀘스트를 풀어나가는 과정같다는 생각이 들 때가 있는데요. 이번 포스트에서는 제가 경험한 여러 퀘스트 중 하나를 이야기해보려 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;당신은 품질 전담 인력이 없는 팀에 혈혈단신 홀로 합류하게 되었다. 이 때 가장 먼저 할 일은?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;당장 눈앞에 있는 작업부터 테스트한다. &lt;br&gt;&lt;/li&gt;
&lt;li&gt;기능을 하나하나 확인하기 전 서비스 전체 흐름을 먼저 익힌다. &lt;br&gt;&lt;/li&gt;
&lt;li&gt;자동화 시스템을 먼저 만들고 시작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;여러분의 선택은 무엇인가요? 사실 정답이 있는 문제는 아니었습니다(찡끗). &lt;br&gt;
왜냐하면 팀의 상황, 서비스의 성격, 그리고 리소스 조건에 따라 모두 정답이 될 수 있으니까요. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;품질 담당이 전무한 디플롯 개발팀에 1인 QA 체제로 합류한 후, 제가 중점적으로 고려한 것은 세 가지였어요.&lt;/p&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;왜 해야하는지 &lt;br&gt;
판단 기준은 무엇인지 &lt;br&gt;
지금 우리 팀에 필요한 프로세스인지&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;무엇을 했는지가 아니라 왜 그렇게 했는지에 초점을 맞춰 ‘품질 관리’에 대해 함께 고민하고 체계를 만들어간 과정을 공유드리려 합니다. 품질 관리 체계가 없거나 리소스가 부족한 QA 조직의 실무자분들에게 도움이 되길 바랍니다. 🫢🫢&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;step-1-서비스-익히기---단순-기능이-아닌-사용자-관점에서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-1-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9D%B5%ED%9E%88%EA%B8%B0---%EB%8B%A8%EC%88%9C-%EA%B8%B0%EB%8A%A5%EC%9D%B4-%EC%95%84%EB%8B%8C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C&quot; aria-label=&quot;step 1 서비스 익히기   단순 기능이 아닌 사용자 관점에서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 1. 서비스 익히기 - 단순 기능이 아닌 사용자 관점에서&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;새롭게 합류한 QA 엔지니어로서 가장 먼저 한 일은 도구를 고르는 것도, 테스트 케이스를 만드는 것도 아니었어요.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;‘이 서비스는 어떤 흐름으로 움직이는가?’&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;즉, 서비스를 사용자 관점에서 이해하고 따라가 보는 것이 먼저였습니다. 그래야 어떤 문제가 사용자 경험에 치명적인 영향을 줄지 판단할 수 있으니까요. 🤔&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그리고 사용자 관점에서 파악한 흐름을 기준으로 &lt;strong&gt;해피패스&lt;/strong&gt;를 정의했습니다.&lt;br&gt;
사람들이 이커머스 서비스를 이용하는 이유는 무엇일까요? 당연히 &lt;em&gt;&lt;strong&gt;구매&lt;/strong&gt;&lt;/em&gt; 하기 위해서겠지요?
&apos;구매&apos;라는 목적을 달성하기 위한 해피패스는 이렇습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;앱/웹 실행 → 로그인 → 상품 탐색 → 상품 상세 진입 → 장바구니 담기/바로구매 → 주문서&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이를 뿌리로 삼아 해피패스의 각 단계를 세분화했어요.
그리고 마지막으로 우선순위 기준을 설정했는데요, 바로 &lt;strong&gt;사용자 비중&lt;/strong&gt;과 &lt;strong&gt;리스크&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1838px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6d153232d2d1a2ee9e98163f8c08c45a/687ee/dplot-1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACN0lEQVR42nWTaZOaQBRF+f+/KV9SqdS4ZxQVdwXBcQXBHZcbTic4JpVY1UJ3vz7v3tcPqzX09b1iq1R31HUXao9m+vKtaObDINRwulCaXnQ+ncx74Yejr29V2X1P7fGH3rJ5qdHTaB6pZHdlrZcLfQS+dkmsJI6UbCM17YaCqafz8aA4m9/vd12vV22jUN5krHC9+hWf7TlOS81GI9vbyJ+6subzuVzXUxRFOp/POmVKWq2WWD8ej9put3o8HrrdbgrDUKPRSPPFQscs7nJJ1e8PNBwOdchiPW8qa5Ftep5ngi+XiwFWq1UFQWDeX4EkHY/Hz2Sobjab6vV6Zt91fyvMgfzYqNfrWea+9vv9vxVmZ9I0NfHdble2betwOGg6zRTOZjNDJjuHCGxkNanVaorj2AyA1HGz2Rh7uMIN6yQuFAomme/7srAGECVYAAiMOpI1VwhwvV5rMBgYIPVmrdPpqFKpfCqEmivkRxBAVO52uz+Aq9XqCcwtO46jYrFoYg2QP4BIJojMr8BXywCxCJALo0TtdlvlcllJknwCJ5OJUQLsFfh3DXPgcrk0t4yAXCHnTQ1zIJZzhe/v76Yu7P0PiCLqBrBUKplYWsqiZQBimSwM1DG40bydAGKVNgFIIqD0II5gkOwJRCF9x+CGUciTwzmQdwAoJQ7b9CVtg23AFheCVIAEMOh+Aggk2atC2oQndikRc+JQhytTQwZAbg4gG9QRu6y/1pAy0OCUBtvM+bI4y0fyE+mGWnGiBikkAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6d153232d2d1a2ee9e98163f8c08c45a/263a4/dplot-1.webp 480w,
/static/6d153232d2d1a2ee9e98163f8c08c45a/a6361/dplot-1.webp 960w,
/static/6d153232d2d1a2ee9e98163f8c08c45a/c6318/dplot-1.webp 1838w&quot; sizes=&quot;(max-width: 1838px) 100vw, 1838px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6d153232d2d1a2ee9e98163f8c08c45a/9aebd/dplot-1.png 480w,
/static/6d153232d2d1a2ee9e98163f8c08c45a/a91f8/dplot-1.png 960w,
/static/6d153232d2d1a2ee9e98163f8c08c45a/687ee/dplot-1.png 1838w&quot; sizes=&quot;(max-width: 1838px) 100vw, 1838px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6d153232d2d1a2ee9e98163f8c08c45a/687ee/dplot-1.png&quot; alt=&quot;dplot 1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;해피패스별 우선순위를 나눈 표&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;검증 항목은 0순위, 1순위, 2순위로 나누었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;가장 먼저 확인해야 할 중요 항목은 0순위였습니다. 1순위는 무조건 수행하는 기본적 테스트 범위이고, 2순위는 리소스 상황에 따라 유동적으로 수행 여부를 판단했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;예를 들어, 로그인 기능이라면&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이메일 로그인&lt;/li&gt;
&lt;li&gt;카카오 로그인&lt;/li&gt;
&lt;li&gt;네이버 로그인&lt;/li&gt;
&lt;li&gt;Apple 로그인&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;네 가지 로그인 수단 중 어떤 걸 선택하더라도 로그인되어야 서비스를 이용할 수 있습니다. 그래서 이 경우 전부 해피패스로 간주합니다. 단, 리소스가 한정적이기 때문에 사용자 점유율과 장애 리스크를 기준으로 카카오 로그인을 0순위로 가장 먼저 검증하고,  나머지 수단은 1순위로 설정했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;이렇게 테스트 대상에 사용자 점유율과 장애 리스크, 두 가지 기준을 우선순위로 설정해 관리했더니 &lt;strong&gt;제한된 리소스로도 핵심 기능의 안정성을 보장&lt;/strong&gt;할 수 있었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;step-2-기반-다지기---부딪히기-전에-기초부터&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-2-%EA%B8%B0%EB%B0%98-%EB%8B%A4%EC%A7%80%EA%B8%B0---%EB%B6%80%EB%94%AA%ED%9E%88%EA%B8%B0-%EC%A0%84%EC%97%90-%EA%B8%B0%EC%B4%88%EB%B6%80%ED%84%B0&quot; aria-label=&quot;step 2 기반 다지기   부딪히기 전에 기초부터 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 2. 기반 다지기 - 부딪히기 전에 기초부터&lt;/h1&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;고객이 일상을 건강하고 아름답게 가꿀 수 있도록 끊임없는 에너지와 영감을 제공하는 올리브영에서는 라이프스타일 커머스 플랫폼 &quot;디플롯&quot;을 함께 운영하고 있습니다. 국내 뷰티&amp;#x26;헬스 트렌드 리딩 플랫폼을 넘어 고객에게 더 다양한 웰니스 경험을 선사하기 위함인데요, 올리브영 앱과는 완전히 분리된 별도의 서비스로 운영되고 있습니다. 🙃 서비스 구조도 다르고, 유입량과 트래픽, 인시던트가 미치는 비즈니스 임팩트도 전혀 다르죠. 그렇기 때문에 디플롯 서비스에 맞는 QA 기준이 필요했습니다!
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;-테스트-환경을-분리하고-정의했습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD%EC%9D%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B3%A0-%EC%A0%95%EC%9D%98%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; 테스트 환경을 분리하고 정의했습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 테스트 환경을 분리하고 정의했습니다.&lt;/h2&gt;
&lt;p&gt;개발을 위한 DEV 환경은 있었지만, 디플롯 서비스에서는 개발과 자체 테스트가 동시에 진행하고 있었어요. &lt;br&gt;
그래서 검증 중에 이슈가 발생해도 원인을 정확히 파악하기 어려웠습니다. 테스트 결과 역시 신뢰하기 어려운 경우가 있었지요.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e4511a99c28b3362abf9f2b164936e98/b7c46/dplot-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 23.333333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAv0lEQVR42m1QiwqDMBDb///m2BC179qX1uxSYQyccPQeuSTnw7iAXOqIWhsWZTEtBuxrG2BDxGta0PZD3hXP9wznI5Tx2FJBkR1GbQ0pFzzO88TvxwEXrBAycq6ymLELYZLch03imtME+zTS2v6fkCCl3XA6L1pc+lGv2opQGLlhz7DnxksRJ8HZjZAqPIcgK05DTAPImjmFSK5GXMJ02IWn934nLLUibulbHwIiEcFVzgrxOpm94UxE6Zj/kdgP6hyDc9H1nbYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e4511a99c28b3362abf9f2b164936e98/263a4/dplot-2.webp 480w,
/static/e4511a99c28b3362abf9f2b164936e98/a6361/dplot-2.webp 960w,
/static/e4511a99c28b3362abf9f2b164936e98/0b34d/dplot-2.webp 1920w,
/static/e4511a99c28b3362abf9f2b164936e98/d30f9/dplot-2.webp 2162w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e4511a99c28b3362abf9f2b164936e98/9aebd/dplot-2.png 480w,
/static/e4511a99c28b3362abf9f2b164936e98/a91f8/dplot-2.png 960w,
/static/e4511a99c28b3362abf9f2b164936e98/ac7a9/dplot-2.png 1920w,
/static/e4511a99c28b3362abf9f2b164936e98/b7c46/dplot-2.png 2162w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e4511a99c28b3362abf9f2b164936e98/ac7a9/dplot-2.png&quot; alt=&quot;dplot 2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;테스트 환경별 목적/사용자/배포 대상 정리 표&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;그래서 &lt;strong&gt;QA 전용 환경&lt;/strong&gt;을 새로 구성했습니다. 개발자가 자체 테스트를 마친 작업만 QA 환경에 반영해 검증하는 흐름을 만들었어요.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;검증을 위한 환경이 따로 만들어져서 &lt;strong&gt;안정된 테스트가 가능&lt;/strong&gt;해졌습니다! 테스트 일정 또한 예측 가능한 범위로 운영할 수 있었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-이슈-티켓의-우선순위-기준을-만들었습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4%EC%8A%88-%ED%8B%B0%EC%BC%93%EC%9D%98-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EA%B8%B0%EC%A4%80%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; 이슈 티켓의 우선순위 기준을 만들었습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 이슈 티켓의 우선순위 기준을 만들었습니다.&lt;/h2&gt;
&lt;br&gt;
&lt;p&gt;이슈 티켓의 우선순위 기준이 필요했던 가장 큰 이유는, 서비스에서 또는 프로젝트 진행 중 발견된 이슈를 명확하게 판단하기 위해서였습니다. &lt;strong&gt;무엇까지 수정하고, 어디부터 다음 배포로 넘길 수 있을지&lt;/strong&gt;를 구분해야 했어요. 🤔 &lt;br&gt;
왜냐구요? PO, 개발, 디자인, QA. 모두가 같은 기준 위에서 이슈의 우선순위를 판단하고 조율할 수 있어야 하니까요!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1558px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e87b4f31bf7501d8307b5d0089d52b7a/3a95e/dplot-3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 37.291666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAxUlEQVR42l2RCQqFMAxEe/+TirVal1pwi7zAFP8PDNZ0ZrI0bNtmKSUjYox2HId943ke/17XZX3fW631J09uXdfGD/M8W9d1Tsg5OyjAdxgGO8+zGU7TZOM4ttx937Ysi2uFgHspxQmcKQAJIcBEBrqjmDpFA8QJjAyRwJgzBESYIVR1zACcfd9dwz98JmiGCGUogjoREUN1w91/h9q9G+qSyhpT+8QQM+1Qe0RHiEceTqArCKrGg/DaEih4AAoCDL4jC3BeYGQfbSP4xiMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e87b4f31bf7501d8307b5d0089d52b7a/263a4/dplot-3.webp 480w,
/static/e87b4f31bf7501d8307b5d0089d52b7a/a6361/dplot-3.webp 960w,
/static/e87b4f31bf7501d8307b5d0089d52b7a/e1260/dplot-3.webp 1558w&quot; sizes=&quot;(max-width: 1558px) 100vw, 1558px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e87b4f31bf7501d8307b5d0089d52b7a/9aebd/dplot-3.png 480w,
/static/e87b4f31bf7501d8307b5d0089d52b7a/a91f8/dplot-3.png 960w,
/static/e87b4f31bf7501d8307b5d0089d52b7a/3a95e/dplot-3.png 1558w&quot; sizes=&quot;(max-width: 1558px) 100vw, 1558px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e87b4f31bf7501d8307b5d0089d52b7a/3a95e/dplot-3.png&quot; alt=&quot;dplot 3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;이슈 우선순위 기준 예시 표&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;프로젝트의 QA를 진행하기 위해 팀에서 정의한 예시를 보여드릴게요. &lt;strong&gt;이슈의 영향도, 발생 범위, 재현 빈도&lt;/strong&gt;를 기준으로 우선순위를 정했습니다.
실전에서는 아래 사진처럼 사용되었어요!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1166px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bb4b71af2caf0c31dba894c5476cfd0b/5ba5e/dplot-5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAABnElEQVR42o2TW1OyUBSG9///F9103W1TV9U0NfZpiIBnBRTMA0pYpsZ+v3eBzdSE5MUzazF773V8UbOwg5ZVgVm9Q8O4x8P9Ner0a5UbeH0T2C2QbueF6AJU4Nlwuwamky5Cz4Hx7xaSBJ/Lo4/KUD6rsMxHXJ2fwXi6QdN5wmrhAukK6Y6X9ovT2B0CJpGHxHfQu7xA4lrAyoeedArBS7cYOZd3HE/WctCuYtqvY2g+IGbbWIfQ8Qg48OXrpVdM5EInQR7wZdxCs/HIOT7D5lLEyixDJhoNGghcGyE7mJDS+X3MoDdTqPfXMdKwDT1yMlLPwn7YwKeb29zPv8HzcppQWyk1aAPygFUdx/njPEdtWOFXQH3Cg9MCiu44Kylb02q2+QNJdvCzTkrIAm58G1tefuem9zKLTArfZBF+k0zQKkUNuVX55UxSpbCHvTrWlEhMKUTzQWbfmHRFK/5fYldr6ijmFpOBiYRVxj0DUaeWMbUrWNK+svKoXcOCek2lylm/mHkf6kO2TNnAt3/LYNwkTn4mVvCs44xs/AeM+xQeCURqZAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bb4b71af2caf0c31dba894c5476cfd0b/263a4/dplot-5.webp 480w,
/static/bb4b71af2caf0c31dba894c5476cfd0b/a6361/dplot-5.webp 960w,
/static/bb4b71af2caf0c31dba894c5476cfd0b/a92a4/dplot-5.webp 1166w&quot; sizes=&quot;(max-width: 1166px) 100vw, 1166px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bb4b71af2caf0c31dba894c5476cfd0b/9aebd/dplot-5.png 480w,
/static/bb4b71af2caf0c31dba894c5476cfd0b/a91f8/dplot-5.png 960w,
/static/bb4b71af2caf0c31dba894c5476cfd0b/5ba5e/dplot-5.png 1166w&quot; sizes=&quot;(max-width: 1166px) 100vw, 1166px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bb4b71af2caf0c31dba894c5476cfd0b/5ba5e/dplot-5.png&quot; alt=&quot;dplot 5&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;실제 프로젝트에서 적용된 이슈 우선순위 분류 사례&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;심각도(Severity)와 우선순위(Priority)를 함께 고려해 정의해두니 &lt;strong&gt;팀 전체가 같은 언어로&lt;/strong&gt; 이슈를 판단하고 결정할 수 있게 됐습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;step-3-흐름-만들기---언제-테스트하고-어떻게-종료할-것인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-3-%ED%9D%90%EB%A6%84-%EB%A7%8C%EB%93%A4%EA%B8%B0---%EC%96%B8%EC%A0%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B3%A0-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A2%85%EB%A3%8C%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;step 3 흐름 만들기   언제 테스트하고 어떻게 종료할 것인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 3. 흐름 만들기 - 언제 테스트하고, 어떻게 종료할 것인가&lt;/h1&gt;
&lt;hr&gt;
&lt;br&gt;
&lt;p&gt;리소스가 한정된 상황에서 ‘테스트를 한다’는 기준만으로는 제품의 품질을 보증하기가 어렵습니다.&lt;br&gt;
그래서 테스트를 언제 시작하고, 어떻게 종료하며, 어떤 기준으로 sign-off 할지를 정의했어요.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;-테스트-시작-조건을-정의했습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%9C%EC%9E%91-%EC%A1%B0%EA%B1%B4%EC%9D%84-%EC%A0%95%EC%9D%98%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; 테스트 시작 조건을 정의했습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 테스트 시작 조건을 정의했습니다.&lt;/h2&gt;
&lt;p&gt;테스트를 단순히 “개발 완료”만으로 시작하면 여러 문제가 발생할 수 있습니다. &lt;br&gt;
(다들 한 번씩 겪어보셨을 거 같네요 코쓱…)&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;❗UI 작업이나 디자인 검토가 끝나지 않은 상태라 테스트 도중 요구사항이 변경될 수 있습니다.&lt;/li&gt;
&lt;li&gt;❗배포가 완료되지 않은 상태라 기능이 아직 동작하지 않는데도 이를 이슈로 오인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;❗개발자의 자체 테스트가 진행되지 않았다면 기본 동작 오류가 반복해서 발견되기도 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;그래서 아래처럼 테스트 시작 조건을 설정했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;✔️ 개발, PO, 디자인 담당자의 자체 테스트가 완료되었는가? &lt;br&gt;
✔️ 테스트 환경에 작업이 배포되었는가? &lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BE : 테스트 서버 배포 완료&lt;/li&gt;
&lt;li&gt;FE : 테스트 서버 배포 완료&lt;/li&gt;
&lt;li&gt;APP : 테스트 빌드 버전 공유 완료&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;이렇게 &lt;strong&gt;최소한의 조건을 명확히&lt;/strong&gt; 하고, 반복 테스트나 불필요한 이슈 등록을 배제했어요. 그렇게 늘 &lt;strong&gt;신뢰할 수 있는 기준&lt;/strong&gt; 위에서 검증을 시작할 수 있었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-테스트-종료-조건을-정의했습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A2%85%EB%A3%8C-%EC%A1%B0%EA%B1%B4%EC%9D%84-%EC%A0%95%EC%9D%98%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; 테스트 종료 조건을 정의했습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 테스트 종료 조건을 정의했습니다.&lt;/h2&gt;
&lt;p&gt;테스트를 마쳤다고 판단하는 기준이 명확하지 않으면 혼란이 시작됩니다. 검증 단계인지, 완료인지, 혹은 특정 기능의 테스트만 완료된 건지, 함께 일하는 모두가 혼란스러울 수 있어요. 🤔🤔🤔 &lt;br&gt;
특히 서버 작업이 포함된 경우 버전 간의 호환성이나 돌발 상황으로 인한 롤백에 대한 대응까지 고려해야 하죠.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그래서 우리는 아래 조건을 모두 충족한 경우에만 테스트가 종료된 것으로 판단하도록 기준을 정했습니다!&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;✔️ 테스트 케이스 또는 체크리스트 수행이 100% 완료되었는가? &lt;br&gt;
✔️ 탐색적 테스팅 수행이 완료되었는가? &lt;br&gt;
✔️ 하위 호환 테스트 및 롤백 테스트가 수행되었는가? (서버 작업이 있는 경우) &lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;하위 호환 테스트 : 서버 ON 상태에서 앱 또는 FE 이전 버전이 정상 동작하는지 확인&lt;/li&gt;
&lt;li&gt;롤백 테스트 : 서버 OFF 상태에서 앱 또는 FE 작업 버전이 정상 동작하는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;테스트 케이스 수행 외에 탐색적 테스팅까지 완료 조건에 추가한 이유는!
실제 운영 환경에서 발생할 수 있는 &lt;strong&gt;예외 상황&lt;/strong&gt;까지 확인하기 위함입니다.&lt;/p&gt;
&lt;p&gt;(사용자들은 문서화된 테스트 케이스대로만 행동하진 않으니까요 ㅎㅅㅎ)&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;단순히 즉흥적으로 눌러보는 애드혹 테스팅이 아니었습니다.
사전에 목적을 설정하고 실제 사용자의 흐름을 따라가는 &lt;strong&gt;탐색적 테스팅&lt;/strong&gt;이 리스크 확인에 알맞다고 판단했어요!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;div style=&quot;width: 60%; display:block;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1148px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9923acbf3936ecaadba47ba1fa09537f/6452e/dplot-6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.583333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB5klEQVR42oWTWZOiQBCE+///rn3YI2LWVWdcHeUW5BYRUDlyKxuIGSMmYh8ymqaa6qovCzU8UgyPDL2s3S1G14xqRf0tQXX2YL4v4BorOMYajrmGuf+DwHlDeLJQlwGGO8+OUknwDks+8ORgdNziHJuoi6NORNXFrOd31dl9is8xVSQWsvCAIrHR1hGGNsPQ5WDlEM03/0/3a4Ayc6DuVSiV7ZDqpJau0pH2eEl62uNRndDfE42BCAY+S4K2Dp/Ec0yq2IqxfcFm9R3WfoHd608sX75hK+t29QNJsNcYqFjwnNwNitSSCyKd/LPwSKAaSZhFBsjymrsacnXxcZHyr8KEbTBBKTEmZTfspGvCL1uXCn3EkxnkxvYo7f58e5tqthC26M8jZz0d07lJZK6a0pcRiYB2MqL9CI77DPXliDw2NOfI32m2gbPRKzGEU0GX1IbiB4ScS9ue9QpbZowj5ByWOMres9Z6Dvl8ENZ8Jl9ztxD91u92b7/EyCVuUpx2mfwoutXRNal4ZkL4vHBuq5+wjHg+EPE9kSga4Xp7HOwDgkh4JkednH8CHc0jE4Gsvi1776+O0URf4hTbjQVDKDGOmuLt4clAlsmkVzHKMsRVXC7F/SL3dBufxVmjGuFKzXuOH/34B+Y/h5vuIF8OAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9923acbf3936ecaadba47ba1fa09537f/263a4/dplot-6.webp 480w,
/static/9923acbf3936ecaadba47ba1fa09537f/a6361/dplot-6.webp 960w,
/static/9923acbf3936ecaadba47ba1fa09537f/e5a25/dplot-6.webp 1148w&quot; sizes=&quot;(max-width: 1148px) 100vw, 1148px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9923acbf3936ecaadba47ba1fa09537f/9aebd/dplot-6.png 480w,
/static/9923acbf3936ecaadba47ba1fa09537f/a91f8/dplot-6.png 960w,
/static/9923acbf3936ecaadba47ba1fa09537f/6452e/dplot-6.png 1148w&quot; sizes=&quot;(max-width: 1148px) 100vw, 1148px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9923acbf3936ecaadba47ba1fa09537f/6452e/dplot-6.png&quot; alt=&quot;dplot 6&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figcaption&gt;실제 프로젝트에서 적용된 테스트 종료 조건&lt;/figcaption&gt;
    &lt;/div&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;완료 기준을 통해 QA 담당자 뿐만 아니라 팀 전체가 &lt;strong&gt;테스트 누락 없이 수행 범위를 명확히 확인&lt;/strong&gt;할 수 있었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-sign-off-기준을-정의했습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-sign-off-%EA%B8%B0%EC%A4%80%EC%9D%84-%EC%A0%95%EC%9D%98%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot; sign off 기준을 정의했습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ sign-off 기준을 정의했습니다.&lt;/h2&gt;
&lt;p&gt;테스트가 종료되었다고 해서 곧바로 배포할 수 있는 건 아닙니다.&lt;/p&gt;
&lt;p&gt;배포는 단순히 코드를 반영하는 작업이 아니라 이 기능을 &lt;strong&gt;사용자에게 제공해도 된다&lt;/strong&gt;는 &lt;strong&gt;팀의 합의&lt;/strong&gt;이니까요!&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;발견된 치명적인 이슈가 아직 남아있거나, 배포 후 돌발 상황에 대한 대응 방안이 준비되지 않았다면 “테스트가 완료되었다”는 사실만 기준으로 배포해선 안 되겠지요?&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그래서 우리는 아래 조건을 모두 충족한 경우에만 sign-off 상태로 판단하기로 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;✔️ 테스트 종료 조건을 충족했는가?&lt;br&gt;
✔️ 테스트 결과가 팀 내부에 공유되었는가?&lt;br&gt;
✔️ 발견된 이슈가 우선순위에 따라 처리되었는가?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;매우 높음 / 높음 → 반드시 배포 전 수정&lt;/li&gt;
&lt;li&gt;보통 / 낮음 / 매우 낮음 → 프로젝트 구성원과 협의 후 처리 여부 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✔️ 롤백 플랜이 논의되었는가?&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;blockquote&gt;
&lt;p&gt;그 결과 &lt;strong&gt;배포 전의 판단이 감이 아닌 기준&lt;/strong&gt;에 따라 이뤄질 수 있었고, 팀 전체가 품질 상태를 함께 바라보며 결정을 내릴 수 있었습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;품질을-지킨다는-것의-의미&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%92%88%EC%A7%88%EC%9D%84-%EC%A7%80%ED%82%A8%EB%8B%A4%EB%8A%94-%EA%B2%83%EC%9D%98-%EC%9D%98%EB%AF%B8&quot; aria-label=&quot;품질을 지킨다는 것의 의미 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;품질을 지킨다는 것의 의미&lt;/h1&gt;
&lt;hr&gt;
&lt;br&gt;
품질을 지킨다는 건 무엇일까요?
&lt;p&gt;사실 아직도 저한테는 명확히 이것입니다!라고 말하기 어려운 주제예요. 🫣 하지만 한 가지 분명한 건, 단순히 테스트를 수행하는 것만으로는 부족하다는 점입니다.&lt;/p&gt;
&lt;p&gt;테스트가 의미있게 작동하려면 그 테스트가 &lt;strong&gt;언제, 무엇을, 어떤 기준으로 수행돼야 하는지를 정하는 과정&lt;/strong&gt;이 꼭 있어야 한다고 생각해요.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;위 과정들을 통해 우리는 서비스에 맞는 QA 기준을 세웠고, 불확실한 판단 없이 팀원들 모두 같은 기준 위에서 품질을 바라보고 있답니다.&lt;/p&gt;
&lt;p&gt;앞으로 더 나은 문화를 만들어갈 수 있도록 노력하겠습니다.&lt;/p&gt;
&lt;p&gt;글 읽어주셔서 감사합니다. 🙇&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;</content:encoded></item><item><title><![CDATA[유저의 소리를 듣는 법: 앱 리뷰 수신 시스템 개발기]]></title><description><![CDATA[안녕하세요! 올리브영에서 백엔드 개발을 담당하고 있는 silverbell…]]></description><link>https://oliveyoung.tech/2025-05-23/app-review-system/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-05-23/app-review-system/</guid><pubDate>Fri, 23 May 2025 19:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영에서 백엔드 개발을 담당하고 있는 &lt;strong&gt;silverbell&lt;/strong&gt;입니다.&lt;br&gt;
오늘은 &quot;유저의 소리를 듣는 법: 앱 리뷰 수신 시스템 개발기&quot;라는 주제로 올리브영의 경험을 공유해드리려 해요.&lt;/p&gt;
&lt;p&gt;올리브영은 고객의 목소리를 빠르게 듣고 대응하기 위해 하루 평균 3~5개의 리뷰를 Android와 iOS 앱 리뷰를 별도 시스템으로 수신해요. 이번 포스트에서는 기존에 사용하던 앱 리뷰 수신 시스템의 문제점이 무엇이고, 이를 어떻게 개선했는지 이야기 해볼게요.&lt;/p&gt;
&lt;h1 id=&quot;-기존-시스템의-아쉬움&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B8%B0%EC%A1%B4-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EC%95%84%EC%89%AC%EC%9B%80&quot; aria-label=&quot; 기존 시스템의 아쉬움 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚨 기존 시스템의 아쉬움&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-리뷰-배송이-느으려어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A6%AC%EB%B7%B0-%EB%B0%B0%EC%86%A1%EC%9D%B4-%EB%8A%90%EC%9C%BC%EB%A0%A4%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 리뷰 배송이 느으려어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🐢 리뷰 배송이 느으려어요&lt;/h2&gt;
&lt;p&gt;기존 시스템의 가장 큰 문제는 리드타임이에요. 앱 리뷰 수신에 2~3일이 소요되다 보니 신속한 대응이 어려웠어요. 올리브영은 B2C 회사로, 고객의 불편함을 최소화하기 위해서는 이슈 트래킹 속도가 매우 중요해요. 그렇지만 리드 타임이 길어서 문제 발생과 동시에 처리하지 못했고, 해결한 이슈의 처리 완료 여부 또한 확인해야 하는 운영 피로도가 쌓이게 되었어요.
그래서 우리는 데이터 수신이 어느 지점에서 지연되는지 파악할 필요가 있었어요.&lt;/p&gt;
&lt;p&gt;앱 리뷰를 수신하기 위해 App Follow라는 외부 플랫폼을 사용하고 있었으며, 이 과정은 다음과 같아요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;고객이 앱 리뷰 작성 후 Android와 iOS 스토어에서 내용 검증 진행&lt;/li&gt;
&lt;li&gt;검증 완료된 앱 리뷰가 각 스토어에 공식 등록&lt;/li&gt;
&lt;li&gt;App Follow 서비스가 양쪽 스토어의 앱 리뷰 데이터 크롤링&lt;/li&gt;
&lt;li&gt;App Follow에서 Slack으로 앱 리뷰 알림 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;크롤링 특성상 양쪽 스토어에서 주기적으로 정보를 수집하는데, 빈도에 따라 데이터 수신 시간에 차이가 발생해요. 앱 리뷰를 더 빠르게 수신하려면 크롤링 빈도를 높이는 방법이 있죠. 하지만 아쉽게도 우리는 App Follow라는 외부 플랫폼을 사용했기 때문에 크롤링 주기를 직접 설정하기 어려웠어요.&lt;/p&gt;
&lt;h2 id=&quot;️-빠른-방법을-찾아보자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EB%B9%A0%EB%A5%B8-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%B0%BE%EC%95%84%EB%B3%B4%EC%9E%90&quot; aria-label=&quot;️ 빠른 방법을 찾아보자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🕵🏻️ 빠른 방법을 찾아보자!&lt;/h2&gt;
&lt;p&gt;답답한 사람이 우물을 판다는 속담이 있죠. 앱 리뷰를 더 빠르게 수신하기 위해 우리는 시스템을 구축하기로 결정했어요. 백엔드 개발자로서 크롤링보다는 API 활용이 더 익숙했기에, 스토어 두 곳에서 제공하는 API를 통해 앱 리뷰 데이터를 수신하는 방식으로 시스템을 설계했어요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;고객이 앱 리뷰 작성 후 Android와 iOS 스토어에서 내용 검증 진행&lt;/li&gt;
&lt;li&gt;검증 완료된 앱 리뷰가 각 스토어에 공식 등록&lt;/li&gt;
&lt;li&gt;스토어에서 제공하는 API를 통해 앱 리뷰 데이터 수신&lt;/li&gt;
&lt;li&gt;Slack으로 앱 리뷰 알림 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;설계는 끝났고, 이제 새로운 앱 리뷰 수신 시스템의 구성과 개발 과정을 살펴볼게요.&lt;/p&gt;
&lt;h1 id=&quot;-앱-리뷰-수신-시스템-개발기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%95%B1-%EB%A6%AC%EB%B7%B0-%EC%88%98%EC%8B%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0&quot; aria-label=&quot; 앱 리뷰 수신 시스템 개발기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧑🏻‍💻 앱 리뷰 수신 시스템 개발기&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;step-1-데이터-조회에-필요한-인증-절차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-1-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B8%EC%A6%9D-%EC%A0%88%EC%B0%A8&quot; aria-label=&quot;step 1 데이터 조회에 필요한 인증 절차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 1. 데이터 조회에 필요한 인증 절차&lt;/h2&gt;
&lt;p&gt;우리는 Google Play Store와 Apple App Store에서 제공하는 API를 통해 앱 리뷰 데이터를 수신 할 거에요. 이 데이터를 조회하려면 스토어마다 따라야 할 인증 절차가 있어요. 하나씩 살펴볼게요.&lt;/p&gt;
&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/41488af5349095586753442fd42d4927/7a54e/auth_info.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABMElEQVR42n1RCW7DMAzL//84rFuXNItz+MxhJ9ZEocaaopsBQoZFSyJVaROoaXu61h19NR1p62jdIi3rdsK8bPLeqpEu1xvVt46cn2m958DBvbpykXFyDEtq0BwNhXkRMqIPM1kX+NNKxnqatKWeef1oaGDMyyI58MK8UoVuI5MGLtT1E3/2FGNkJIopyX1jJL4joqAaJmmYJF94iJGqthtZphcJ6Iixc84nUIl8fFhI9Vrkl/PIqYwNIgk+QmJ++PzqoPnbRy0THkc+8aVgp7Qk7d0fLAVSjuN4icATwkP4DXuK1/u+S74aeBkodPlseNPf8vjfQeN35kJRSvuv5DIhPIRsdJ20+3MyIDOcDzId/IZVKFLymFImRKJulWz5eSHP2HgZ2jhy/Adyy2Sl8A/3vbthsStZ9gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/41488af5349095586753442fd42d4927/263a4/auth_info.webp 480w,
/static/41488af5349095586753442fd42d4927/a6361/auth_info.webp 960w,
/static/41488af5349095586753442fd42d4927/0b34d/auth_info.webp 1920w,
/static/41488af5349095586753442fd42d4927/d2cb1/auth_info.webp 2216w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/41488af5349095586753442fd42d4927/9aebd/auth_info.png 480w,
/static/41488af5349095586753442fd42d4927/a91f8/auth_info.png 960w,
/static/41488af5349095586753442fd42d4927/ac7a9/auth_info.png 1920w,
/static/41488af5349095586753442fd42d4927/7a54e/auth_info.png 2216w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/41488af5349095586753442fd42d4927/ac7a9/auth_info.png&quot; alt=&quot;auth info&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;  
&lt;h3 id=&quot;인증하기-i---google-play-android&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-i---google-play-android&quot; aria-label=&quot;인증하기 i   google play android permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;인증하기 I - Google Play (Android)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Google Play Store&lt;/strong&gt;에서 제공하는 API에 접근하기 위해서는 &lt;strong&gt;OAuth2.0&lt;/strong&gt;을 통한 사용자 인증이 필요해요.&lt;/p&gt;
&lt;details&gt;
    &lt;summary&gt; ❓OAuth2.0 이란 &lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt; - OAuth 2.0은 애플리케이션이 사용자를 대신하여 자원에 접근할 수 있도록 인증 및 권한 부여를 처리하는 오픈 표준 프로토콜입니다.
 - 주로 타사 애플리케이션이 사용자의 비밀번호를 요구하지 않고도 보안 리소스에 접근할 수 있도록 설계되었습니다.
 - 인증(누구인지 확인)보다는 권한 부여(무엇을 할 수 있는지)에 초점을 맞추고 있습니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;  
&lt;p&gt;OAuth 2.0을 사용하면 앱이 사용자 비밀번호에 직접 접근할 필요 없이 토큰을 통해 제한된 권한을 확보해 API를 호출할 수 있어요.&lt;/p&gt;
&lt;p&gt;Google Play Developer에서 제공하는 공식 문서에는 인증 시나리오가 여럿 존재하는데요, 우리는 그중 &lt;strong&gt;서비스 계정&lt;/strong&gt; 시나리오를 채택했어요. 서비스 계정을 활용하면 서버는 자체 ID로 Google OAuth 2.0 서버와 상호 작용할 수 있고, 이 과정에서 사용자 동의가 필요하지 않아요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Google Play Store 인증 절차&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;먼저 애플리케이션내 서비스 계정을 생성합니다.&lt;/li&gt;
&lt;li&gt;도메인 전체 권한을 서비스 계정에 위임합니다.&lt;/li&gt;
&lt;li&gt;생성한 서비스 계정의 키 파일을 다운로드합니다. (키 파일을 활용해 인증 토큰을 획득하고, Google API를 조회할 수 있습니다.)&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1254px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/be51f8593dac942edb965f953af9a161/a4588/google_oauth.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 91.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB0klEQVR42oWUWYsCMRCE5///IsEHHxQFLxQV8b7xvu+zl6+hh3E2sxsIcZJ0pau6Wm+320mz2ZTBYCDdblfa7bYcj0f5fD7iGuyPx2NptVr+7PV6GrvZbMQjmA3mcDhU8MPhIO/3W4PDwHwvFgsFajQaGtfv96XT6ch2uxXPAvb7vcxmMw14Pp9fYFHZvl4vud1uX495djCdTqVer8tkMtHXCoWCVCoVyeVyKkEY2B42QDvzAY0GF6CRz+f1gWq1KlGDuPv9/rXnA87nc83scrnoARr+N8jQsncCUikAH4+HztPpJOfzWQNdA4cgUVCSL0AqzIVarSblcllpF4tFrSaVp4qAUEDup9NpWa1WGuMEhPJ6vVZgCoId0BBvkiWTzG2FEdpTUCegUQ5WMsrcDGz2JyBVhpLtBSeXyWq5XKq2fEM1EhAtSqWSWoWD6/WqFmK1CQC6ZbNZBaJdSeRPDYOUwzTJxFqUTOnpeDyu4JGA2CRKs/CAgdGNNDaewh6sWIUVuvyTsGJ40xWqTM6cvYz/SB+b0MPJZFJ7OpPJ6BkFMy9S4VQqpfeh7gMaHXyXSCT8thuNRmpoLvM72LMWQ3axWOx3htaXVhCXZq49YsKa/wArmWMLXufcWgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/be51f8593dac942edb965f953af9a161/263a4/google_oauth.webp 480w,
/static/be51f8593dac942edb965f953af9a161/a6361/google_oauth.webp 960w,
/static/be51f8593dac942edb965f953af9a161/88a49/google_oauth.webp 1254w&quot; sizes=&quot;(max-width: 1254px) 100vw, 1254px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/be51f8593dac942edb965f953af9a161/9aebd/google_oauth.png 480w,
/static/be51f8593dac942edb965f953af9a161/a91f8/google_oauth.png 960w,
/static/be51f8593dac942edb965f953af9a161/a4588/google_oauth.png 1254w&quot; sizes=&quot;(max-width: 1254px) 100vw, 1254px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/be51f8593dac942edb965f953af9a161/a4588/google_oauth.png&quot; alt=&quot;google oauth&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;  
&lt;p&gt;아래 &lt;a href=&quot;https://developers.google.com/identity/protocols/oauth2&quot;&gt;Google Developer 문서&lt;/a&gt;에서 키 파일을 이용한 액세스 토큰 발급 방법과 토큰 만료 시 자동 재발급 절차를 확인할 수 있어요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;서비스 계정의 자격 증명에는 고유한 생성된 이메일 주소, 클라이언트 ID, 최소한 하나의 공개/비공개 키 쌍이 포함됩니다.&lt;/li&gt;
&lt;li&gt;클라이언트 ID와 하나의 개인 키를 사용하여 서명된 JWT를 만들고 액세스 토큰 요청을 구성합니다.&lt;/li&gt;
&lt;li&gt;그런 다음 애플리케이션은 토큰 요청을 Google OAuth 2.0 권한 부여 서버로 보내고, 이 서버에서 액세스 토큰을 반환합니다.&lt;/li&gt;
&lt;li&gt;애플리케이션은 토큰을 사용하여 Google API에 액세스합니다.&lt;/li&gt;
&lt;li&gt;토큰이 만료되면 애플리케이션은 프로세스를 반복합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;여기까지 권한이 부여된 서비스 계정을 생성하고 해당 계정의 키 파일을 다운로드했어요. 이제 키 파일을 사용해 인증하고, Google API와 통신하기 위한 토큰 값을 발급 받을 수 있어요!&lt;/p&gt;
&lt;p&gt;다음은 Google Play Console API를 사용하기 위한 인증 방식을 알아볼까요?&lt;/p&gt;
&lt;p&gt;우리는 Google에서 제공하는 &lt;a href=&quot;https://developers.google.com/android-publisher/api-ref/rest&quot;&gt;AndroidPublisher API&lt;/a&gt;로 인증 로직을 구성했어요. 또한 Google Developer에서 제공하는 다양한 API를 적재적소에 활용할 수 있게 구현했어요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;
&lt;span class=&quot;token comment&quot;&gt;// Google Play Console API를 사용하기 위한 인증된 클라이언트를 생성합니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;authenticate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AppType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AndroidPublisher &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; credential &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; runCatching &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;authorizeWithServiceAccount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getOrElse&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;authentication&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; it&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; AndroidPublisher&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;HTTP_TRANSPORT&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; JSON_FACTORY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; credential&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setApplicationName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;packageNameForGooglePlay&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 서비스 계정을 사용한 인증을 처리합니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;authorizeWithServiceAccount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AppType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Credential &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; classLoader &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;javaClass&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;classLoader
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; p12InputStream &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; classLoader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getResourceAsStream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;keyFileForGooglePlay&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IOException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;P12 file not found&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; privateKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; SecurityUtils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;loadPrivateKeyFromKeyStore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        SecurityUtils&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getPkcs12KeyStore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        p12InputStream&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; P12 파일의 비밀번호 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; 키 별칭 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; 키 비밀번호 &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; GoogleCredential&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setTransport&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;HTTP_TRANSPORT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setJsonFactory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;JSON_FACTORY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setServiceAccountId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;appType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceAccountForGooglePlay&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setServiceAccountScopes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Collections&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;singleton&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;AndroidPublisherScopes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ANDROIDPUBLISHER&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setServiceAccountPrivateKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;privateKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기까지 Google Play Store 인증을 위한 절차를 살펴보았습니다. 이어서 Apple App Store에서 진행할 절차를 설명할게요.&lt;/p&gt;
&lt;h3 id=&quot;인증하기-ii---apple-app-store-ios&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-ii---apple-app-store-ios&quot; aria-label=&quot;인증하기 ii   apple app store ios permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;인증하기 II - Apple App Store (iOS)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Apple App Store&lt;/strong&gt;에서 제공하는 API에 접근하기 위해서는 &lt;strong&gt;JWT&lt;/strong&gt;(JSON Web Token)를 통한 인증이 필요해요.&lt;/p&gt;
&lt;details&gt;
    &lt;summary&gt; ❓JWT 란 &lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt; - JWT는 JSON 객체를 사용하여 정보를 안전하게 전송하기 위한 개방형 표준입니다. 
 - JWT는 세 부분으로 구성되어 있습니다: 헤더(header), 페이로드(payload), 서명(signature). 이 세 부분은 점(.)으로 구분되어 있으며, 각각 Base64Url로 인코딩됩니다. 
 - JWT는 주로 인증 및 정보 교환에 사용됩니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;  
&lt;p&gt;이번에도 공식 개발자 사이트에서 제공하는 App Store Connect API 가이드를 참고했어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apple App Store 인증 절차&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api&quot;&gt;App Store Connect API에 대한 API 키를 생성합니다.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/generating-tokens-for-api-requests&quot;&gt;API를 사용하기 위해 JWT 토큰을 생성합니다.&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;App Store Connect API 인증은 Google Play Store에 비해 상대적으로 인증이 수월했어요. (JWT가 더 친근했던건 안비밀 입니다 🙈)&lt;/p&gt;
&lt;p&gt;다음은 App Store Connect API를 사용하기 위한 인증 방식을 우리가 구현한 예시로 설명할게요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;signedJWT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// JWT 헤더 생성 - 서명 알고리즘(ES256), 키 ID(keyId), JWT의 타입(JOSEObjectType.JWT)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; header &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; JWSHeader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;JWSAlgorithm&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ES256&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;keyID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;keyId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;JOSEObjectType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;JWT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// JWT의 클레임 설정 - 발급자(issuerId), 발급 시간(issueTime), 만료 시간(expirationTime, 현재 시간에서 15분 후), 그리고 수신자(audience)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; claimsSet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; JWTClaimsSet&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;issuer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;issuerId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;issueTime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;now&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expirationTime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;expirationTime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;audience&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;appstoreconnect-v1&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// JWT 생성&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; jwt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SignedJWT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;header&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; claimsSet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 서명 처리&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; ecPrivateKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; privateKeyReader&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readPrivateKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;keyPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; jwsSigner &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ECDSASigner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ecPrivateKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        jwt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sign&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;jwsSigner&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Exception&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;printStackTrace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        logger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;App store connect api error [jwt 발급 실패 !!]: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;e&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// JWT 직렬화 및 반환&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jwt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;step-2-데이터-조회-후-가공하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-2-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A1%B0%ED%9A%8C-%ED%9B%84-%EA%B0%80%EA%B3%B5%ED%95%98%EA%B8%B0&quot; aria-label=&quot;step 2 데이터 조회 후 가공하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 2. 데이터 조회 후 가공하기&lt;/h2&gt;
&lt;p&gt;인증을 완료하면 각 스토어에서 제공하는 공식 API를 통해 앱 리뷰 데이터를 조회할 수 있어요 😆😆&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google Play (Android): &lt;a href=&quot;https://developers.google.com/android-publisher/reply-to-reviews?hl=ko#gaining_access&quot;&gt;https://developers.google.com/android-publisher/reply-to-reviews?hl=ko#gaining_access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;App Store (iOS): &lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/get-v1-apps-_id_-customerreviews&quot;&gt;https://developer.apple.com/documentation/appstoreconnectapi/get-v1-apps-_id_-customerreviews&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;올리브영에서는 업무 메신저로 Slack을 사용 중 입니다. Slack API로 제공하는 기능이 풍부해 업무에 적극 활용하고 있어요.&lt;br&gt;
조회한 앱 리뷰 데이터를 수신하기 위해 우리는 Slack API 중 2가지를 사용했어요!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://api.slack.com/block-kit&quot;&gt;Block-kit&lt;/a&gt;: 깔끔하고 일관된 UI 프레임워크로, Slack 메시지를 예쁘게 구성할 수 있어요.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://api.slack.com/messaging/webhooks&quot;&gt;incomming webhook&lt;/a&gt;: 수신 웹훅을 사용하면 애플리케이션이 Slack으로 메시지를 남길 수 있어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;초기에 사용했던 Block json 구조를 살짝 공유할게요. 현재는 이를 개선한 구조로 사용중입니다. :)&lt;/p&gt;
&lt;details&gt;
    &lt;summary&gt;📦 Block json 보기/접기&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;attachments&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;color&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;marketType.color.hex 값&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;blocks&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;header&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;plain_text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;header 텍스트&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;emoji&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;section&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mrkdwn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;rating&gt; &amp;lt;countyFlag&gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;section&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mrkdwn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*&amp;lt;title&gt;*&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;section&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mrkdwn&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;content&gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;context&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;elements&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;plain_text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;App : &amp;lt;appType.description&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;plain_text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;작성자 : &amp;lt;user&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;plain_text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;작성일 : &amp;lt;date&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;plain_text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;버전 : &amp;lt;version&gt;&quot;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;divider&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;fallback&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&amp;lt;previewText&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;  
&lt;p&gt;Block-kit으로 메시지 구조를 만들고 이제 남은 건 메시지 전송뿐! Slack에서 제공하는 incomming webhook을 활용해 원하는 Slack 채널로 메시지를 전송할 수 있어요. Slack API는 각 채널에 매핑되는 URL endpoint를 제공하는데, 이 때 WebClient를 이용해 endpoint로 메시지를 전송해요.&lt;/p&gt;
&lt;p&gt;WebClient가 궁금하신 분은 올리브영의 이전 포스팅을 참고해주세요!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/blog/2022-11-10/oliveyoung-discovery-premium-webclient/&quot;&gt;신규 전시 프로젝트에서 WebClient 사용하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기까지 잘 따라오셨다면, 그리고 당신이 백엔드 개발자라면! 디자이너나 프론트 개발자의 도움없이 앱리뷰 메시지를 Slack 채널로 수신하게 할 수 있습니다👏🏻👏🏻 그것도 아주 감각적인 디자인으로요. 🐵&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1310px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b65aa3518429e705802a2d478fcf2a31/b4bc2/app_review_scrennshot.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.83333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABx0lEQVR42pVS2Y6bQBDc//+ePGQdKU+RstJGgDlsBgO2tWBuMAwYg4/K9PjYdfYhyUNRPd10UdP0E883YI6Hl5dXWKaJIAjgeR7W6zWiOELZFNjv9/+MJ96OcMMapq7AWSzgMAZfCN5Ey232f4LdfoAX1XhbObBthtVyhePxiPP5jNPpJONxHD/hcDg88F1w1w9gIRfuXCyEQ9/3kcQJkuSCOI6RpimyLJPnj5znuaxT3Lbt1aEQdDZcXnU+n4MJNk0LlmVhNptJXHImDN2Qsa7r8kw1wzAkN02DYRiuDoMGiqLIBlVVMZ1OpaBt27KZQHlN06QAgd7RVE3WKM85vwiSQzfq5JdJhJiECBQ7zAETs3UcMeO5fXdNZ1kXTOi67n2GTtjAul6N5kh/eOH5YNfYdV1ZWy6Xsj6zLoK3PIFmeHdIM4zFzlGSC9A8+ihEl6doxFX4B9A7fd9jt9s94H1t6MpxhyApUFYVWtFU1zXGLEZf5qi2W1QiTzliErytCTkiPOzhIB5B1cEMCqhuCD/JYb9t4GUcWZHLZmq68Z8Cnxb7cDrC/vkLPybPmEy+4vu3L1AsBnezRZykKMsCRVFIh38TI8HfbnB95zNubAQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b65aa3518429e705802a2d478fcf2a31/263a4/app_review_scrennshot.webp 480w,
/static/b65aa3518429e705802a2d478fcf2a31/a6361/app_review_scrennshot.webp 960w,
/static/b65aa3518429e705802a2d478fcf2a31/0528b/app_review_scrennshot.webp 1310w&quot; sizes=&quot;(max-width: 1310px) 100vw, 1310px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b65aa3518429e705802a2d478fcf2a31/9aebd/app_review_scrennshot.png 480w,
/static/b65aa3518429e705802a2d478fcf2a31/a91f8/app_review_scrennshot.png 960w,
/static/b65aa3518429e705802a2d478fcf2a31/b4bc2/app_review_scrennshot.png 1310w&quot; sizes=&quot;(max-width: 1310px) 100vw, 1310px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b65aa3518429e705802a2d478fcf2a31/b4bc2/app_review_scrennshot.png&quot; alt=&quot;app review scrennshot&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h2 id=&quot;step-3-앱-리뷰-시스템-cicd-구축하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-3-%EC%95%B1-%EB%A6%AC%EB%B7%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C-cicd-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;step 3 앱 리뷰 시스템 cicd 구축하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 3. 앱 리뷰 시스템 CI/CD 구축하기&lt;/h2&gt;
&lt;p&gt;끝난줄 아셨죠? 새로운 앱 리뷰 시스템을 지속적으로 운영하려면 CI/CD 파이프라인이 필요했어요. 올리브영은 주로 AWS에서 제공하는 서비스를 사용하고 있어요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EC2: 서버를 띄울 공간&lt;/li&gt;
&lt;li&gt;S3: 배포할 결과물을 저장하는 공간&lt;/li&gt;
&lt;li&gt;codeDeploy: 배포를 도와줄 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자주 배포되는 서비스가 아니기 때문에 간단한 CI/CD 구조를 채택했어요. 우리가 구축한 워크플로는 main 브랜치에 푸시하면 Github Actions으로 EC2까지 자동 배포되는 형태에요. 전체적 플로우는 좀 더 직관적인 이미지로 설명할게요.&lt;/p&gt;
&lt;div style=&quot;width: 100%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/76547a5a94cd4a3ed5f54591dba17a3f/206f9/ci_cd.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.958333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABZElEQVR42oVSy0oDMRSdn3Mt+D9+hmDppijoH9SVqxa6EKULpbZFiq0M0s503o/kJjneZMZRFG3gJuRxzzn33Hg4NLQClIAhwav8CqOgygRp4AN12p17h/CMSza/eaoMWpQwmvhet4cHAE07R8kW89cHaK0hhIDii3I1BYVvjKU5FBMTExT/A2ouy47h6Aqn/RPUtcQ+jJFnJRSjGlmiYiKoRqWRFbyfxVgVNgyXmSQ5NClsdz4eF3ecR41mm8wgFqD2nwDrr1VpAf9Sl6Y5MlbiHrYems5LVkeS9wQiw09syS1gnucQUvJFwz6ZTDAc3jifPxUl03vM+2fsuXT9Mdp0QUK71dFwg7zlywrv24DLy7Be+xgMLtDrnWOxWCLchbD921xf4vb4CEWwR5wWiKIEcZwhilPUgr75xYDrzRuCMOxKms2eMR6PUFVVy6ogigyxv+ENd5Jq9ye1i7r5Vi7IefkBTfm4AsyMq+kAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/76547a5a94cd4a3ed5f54591dba17a3f/263a4/ci_cd.webp 480w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/a6361/ci_cd.webp 960w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/0b34d/ci_cd.webp 1920w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/da28f/ci_cd.webp 2880w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/9c325/ci_cd.webp 3420w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/76547a5a94cd4a3ed5f54591dba17a3f/9aebd/ci_cd.png 480w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/a91f8/ci_cd.png 960w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/ac7a9/ci_cd.png 1920w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/f9c26/ci_cd.png 2880w,
/static/76547a5a94cd4a3ed5f54591dba17a3f/206f9/ci_cd.png 3420w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/76547a5a94cd4a3ed5f54591dba17a3f/ac7a9/ci_cd.png&quot; alt=&quot;ci cd&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;codedeploy에서-참고할-appspec-작성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#codedeploy%EC%97%90%EC%84%9C-%EC%B0%B8%EA%B3%A0%ED%95%A0-appspec-%EC%9E%91%EC%84%B1&quot; aria-label=&quot;codedeploy에서 참고할 appspec 작성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;codeDeploy에서 참고할 AppSpec 작성&lt;/h3&gt;
&lt;p&gt;AppSpec 파일은 배포 과정에서 어떤 작업을 수행할지 정의한 스펙 문서에요. YAML 또는 JSON 형식으로 작성해요. CodeDeploy가 애플리케이션 파일을 복사하거나 스크립트를 실행하는 등 배포를 자동으로 관리하는데, 이 배포 단계에서 AppSpec 파일을 참고해요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;❓ &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/application-specification-files.html#appspec-files-on-server-compute-platform&quot;&gt;AppSpec 파일에 대한 설명&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;AppSpec 파일은 배포 패키지의 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-validate.html&quot;&gt;루트 디렉토리&lt;/a&gt;에 위치해야 하며, &lt;code class=&quot;language-text&quot;&gt;appspec.yml&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;appspec.json&lt;/code&gt;이라는 이름으로 저장되어야 해요.&lt;br&gt;
AppSpec 파일의 기본 구조는 다음과 같아요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; linux
&lt;span class=&quot;token comment&quot;&gt;# files 섹션&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;files&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; build/libs/&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;프로젝트 명&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;0.0.1&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;SNAPSHOT.jar 
    &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;대상 인스턴스에 파일이 복사될 절대 경로입니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;                  
    &lt;span class=&quot;token key atrule&quot;&gt;overwrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;대상 위치에 동일한 이름의 파일이 이미 존재할 경우 덮어쓸지 여부를 결정합니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;                            

&lt;span class=&quot;token comment&quot;&gt;# permissons 섹션&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;권한을 적용할 대상 파일 또는 디렉토리의 경로입니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;     
    &lt;span class=&quot;token key atrule&quot;&gt;pattern&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&apos;object&apos;로 지정된 경로 내에서 권한을 적용할 파일 또는 디렉토리의 패턴입니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;       
    &lt;span class=&quot;token key atrule&quot;&gt;owner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;해당 파일 또는 디렉토리의 소유자를 지정합니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;해당 파일 또는 디렉토리의 그룹을 지정합니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;                     

&lt;span class=&quot;token comment&quot;&gt;# hooks 섹션&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;hooks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# ApplicationStop: 애플리케이션을 중지해야 하는 배포 라이프사이클 이벤트입니다.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# &apos;files&apos; 섹션이 실행되기 전에, 현재 실행 중인 애플리케이션을 안전하게 중지시키기 위해 사용됩니다.&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ApplicationStop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;실행할 스크립트의 위치입니다. 배포 패키지 내의 상대 경로로 지정합니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;스크립트 실행을 위해 허용되는 최대 시간(초)입니다. 이 시간을 초과하면 배포가 실패합니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;runas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;스크립트를 실행할 사용자 계정입니다. 지정하지 않으면 root 권한으로 실행됩니다.&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# BeforeInstall: &apos;files&apos; 섹션에서 파일을 복사하기 전에 실행되는 이벤트입니다.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# 주로 이전 버전의 애플리케이션 파일 삭제, 필요한 디렉토리 생성, 사전 작업 등을 수행합니다.&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;BeforeInstall&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 
      &lt;span class=&quot;token key atrule&quot;&gt;runas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# ApplicationStart: 모든 파일이 복사되고 권한 설정이 완료된 후, 새 버전의 애플리케이션을 시작하는 이벤트입니다.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# 일반적으로 웹 서버 재시작, 애플리케이션 프로세스 실행 등의 작업을 수행합니다.&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ApplicationStart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 
      &lt;span class=&quot;token key atrule&quot;&gt;runas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;AppSpec 파일의 각 세션 설명은 공식 문서를 참고하면 자세히 알 수 있어요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-structure-files.html&quot;&gt;files 섹션&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-structure-permissions.html&quot;&gt;permissions 섹션&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html&quot;&gt;hooks 섹션&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;github-actions-workflow-작성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#github-actions-workflow-%EC%9E%91%EC%84%B1&quot; aria-label=&quot;github actions workflow 작성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Github Actions Workflow 작성&lt;/h3&gt;
&lt;p&gt;이제 마지막 단계인 Github Actions Workflow를 작성하면 됩니다. Workflow 파일은 &lt;code class=&quot;language-text&quot;&gt;.github/workflows&lt;/code&gt; 디렉토리에 위치 해야 하며, YAML 형식으로 작성해요.&lt;br&gt;
Workflow 파일의 이름은 자유롭게 지정할 수 있지만 일반적으로 &lt;code class=&quot;language-text&quot;&gt;deploy.yml&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;ci-cd.yml&lt;/code&gt;형태로 명명해요. 저는 deploy.yml로 작성했고, 구조는 아래와 같아요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to Amazon EC2

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; main

&lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;AWS_REGION&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;리전이름&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;S3_BUCKET_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;S3 버킷이름&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;CODE_DEPLOY_APPLICATION_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;codedeploy 애플리케이션 이름&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;CODE_DEPLOY_DEPLOYMENT_GROUP_NAME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;codedeploy 배포 그룹 이름&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; production

    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# (1) 기본 체크아웃&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Checkout
      &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v3

    &lt;span class=&quot;token comment&quot;&gt;# (2) JDK 17 세팅&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Set up JDK 17
      &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/setup&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;java@v3
      &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;distribution&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;temurin&apos;&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;java-version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;17&apos;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# (3) Gradle build (Test 제외)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build with Gradle
      &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; gradle/gradle&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;action@v2
      &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; clean build &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;x test &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;Pprofile=prod

    &lt;span class=&quot;token comment&quot;&gt;# (4) AWS 인증 (IAM 사용자 Access Key, Secret Key 활용)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Configure AWS credentials
      &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;actions/configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;credentials@v1
      &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;aws-access-key-id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.AWS_ACCESS_KEY_ID &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;aws-secret-access-key&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets.AWS_SECRET_ACCESS_KEY &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;aws-region&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; env.AWS_REGION &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# (5) 빌드 결과물을 S3 버킷에 업로드&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Upload to AWS S3
      &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
        aws deploy push \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --ignore-hidden-files \
          --s3-location s3://${{ env.S3_BUCKET_NAME }}/${{ github.sha }}.zip \
          --source .&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# (6) S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to AWS EC2 from S3
      &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
        aws deploy create-deployment \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
          --s3-location bucket=${{ env.S3_BUCKET_NAME }},key=${{ github.sha }}.zip,bundleType=zip&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;(5) 빌드 결과물을 S3 버킷에 업로드&lt;/strong&gt;에 설명을 덧붙이자면,
&lt;code class=&quot;language-text&quot;&gt;$github.sha&lt;/code&gt; 변수는 GitHub에서 각 커밋마다 생성하는 고유한 값입니다. 이 값을 활용하여 파일 업로드 시 이름 중복으로 인한 충돌을 방지했어요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitHub 변수는 &lt;a href=&quot;https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context&quot;&gt;GitHub Context&lt;/a&gt;에서,
deploy.yml은 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/application-revisions-push.html&quot;&gt;AWS CodeDeploy&lt;/a&gt;에서 자세히 알아보세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;step-4-배포-후-모니터링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-4-%EB%B0%B0%ED%8F%AC-%ED%9B%84-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&quot; aria-label=&quot;step 4 배포 후 모니터링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;STEP 4. 배포 후 모니터링&lt;/h2&gt;
&lt;p&gt;서비스를 배포 했다면 내가 만든 시스템이 잘 작동하는지 확인 해야겠죠? 앱 리뷰 시스템은 리뷰가 잘 수신되는지 확인 하는 것이 중요했어요.&lt;br&gt;
리뷰 발송이 끝난 시점에 시스템이 전송한 리뷰의 갯수를 Slack 알림으로 받고, 리뷰 전송에 실패하면 에러로그와 함께 Slack으로 알람을 받아 즉시 대응할 수 있게 구성을 했어요!&lt;/p&gt;
&lt;p&gt;가장 최근에 발생했던 오류 메세지를 공유해볼게요. google play에서 제공해주는 API서버가 다운 된 케이스로 일시적인 오류였어요!&lt;/p&gt;
&lt;div style=&quot;width: 90%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1812px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4a617d4e45c65feb23b3270c9814dd13/5c1e2/error_msg.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.416666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA40lEQVR42qVRyU7DMBTM/38aZ1QVIXGoiJMm9RLvWzy4NgocW3jSaMY+jGeeB8OveH874/V0xuVzBJkXrJThxjjmZQXlomkuJK4rhbYWaS8IaUfMP7ifU+Uh5wKpFLh23/CdlYOozFTXm/Fg0kLaAOXiAWk7CxNgfMKAOpsQDaUU/Heaoa01aK3JOIdzDiklhBDhfWj6kSm/DY0xGMe6P0IwkQla63an6iruDzydMHiPGHuaGFPnFBueXUMz5GLDeqOYJoJ5no+EUsqHKx+Ge87QywmXjxcQqrBpC19rOuf/9ElfT4xx7B4wOP4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4a617d4e45c65feb23b3270c9814dd13/263a4/error_msg.webp 480w,
/static/4a617d4e45c65feb23b3270c9814dd13/a6361/error_msg.webp 960w,
/static/4a617d4e45c65feb23b3270c9814dd13/d38a2/error_msg.webp 1812w&quot; sizes=&quot;(max-width: 1812px) 100vw, 1812px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4a617d4e45c65feb23b3270c9814dd13/9aebd/error_msg.png 480w,
/static/4a617d4e45c65feb23b3270c9814dd13/a91f8/error_msg.png 960w,
/static/4a617d4e45c65feb23b3270c9814dd13/5c1e2/error_msg.png 1812w&quot; sizes=&quot;(max-width: 1812px) 100vw, 1812px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4a617d4e45c65feb23b3270c9814dd13/5c1e2/error_msg.png&quot; alt=&quot;error msg&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;   
다행히 시스템이 안정되어 오류 메시지를 자주 받지는 않아요.🥹
&lt;p&gt;모니터링을 통해 개선이 필요한 점이 있다면, 지속적으로 디벨롭해 나갈 예정입니다!&lt;/p&gt;
&lt;h2 id=&quot;-개선-된-점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B0%9C%EC%84%A0-%EB%90%9C-%EC%A0%90&quot; aria-label=&quot; 개선 된 점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🧑🏻‍🔧 개선 된 점&lt;/h2&gt;
&lt;p&gt;이전에 사용한 App Follow는 이미 해소된 이슈를 수신하거나 고객의 불편함을 늦게 수신하는 경우가 있었는데요, 새로운 앱 리뷰 수신 시스템의 등장으로 가장 큰 문제였던 리드 타임을 단축할 수 있었어요. 덕분에 고객의 목소리에 더욱 신속하고 유연하게 대응할 수 있게 되었어요 👏🏻👏🏻👏🏻&lt;/p&gt;
&lt;div style=&quot;width: 100%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/447cfc806a1b45bc68eea4828e41ed1c/b70ed/result.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 50.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABkUlEQVR42o1SyW7bQAzN//9LmhzaW64pgpzcxnJhKIkjax1Lo83WNlpe9ehOUPRUAhSHHOrxkZwbYwyWZcE0TVedJ2ABSpPCzTd41T9F3/IXHIrdp099L7bop1by53kGsW74GYZBQClmNMh1jufNd7ymDlR3RNQckDQeVOsjbj5EGXfVFofjO5qmRds2V8C6rrHf7wXUilIKX27vkKaZ+LaYFeufTil2zk7OZDiO45WhDbRtC8uYhfqhlxh9UTOI3/e95HVd9+mziADS4eF8PsPzPGEXhiGiKEKiYvjBcWV6QhiEiONY7miZ5/s+kiRBEAQCLi2zMtlxIZb2bGZE9QEv6SO26RO80sVkJmFo8/7+xy6EWMLwcrkIEGU0I5TOcPv1HkVVQp1CPD58WwffiBLkX7EzlZarqoLjONKylW6Z8ct9w7zWKIsc7o8N/kcE0DIjU85Gaw2dZdB5jnxVzkjnhcTpc3bZes8zbVEUkkMrgOybB9qyLGVrtGTeru+rrit0f2LsgsrWbR4tXwRjXMpvfPf+aTgwYDoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/447cfc806a1b45bc68eea4828e41ed1c/263a4/result.webp 480w,
/static/447cfc806a1b45bc68eea4828e41ed1c/a6361/result.webp 960w,
/static/447cfc806a1b45bc68eea4828e41ed1c/0b34d/result.webp 1920w,
/static/447cfc806a1b45bc68eea4828e41ed1c/da28f/result.webp 2880w,
/static/447cfc806a1b45bc68eea4828e41ed1c/0670c/result.webp 3440w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/447cfc806a1b45bc68eea4828e41ed1c/9aebd/result.png 480w,
/static/447cfc806a1b45bc68eea4828e41ed1c/a91f8/result.png 960w,
/static/447cfc806a1b45bc68eea4828e41ed1c/ac7a9/result.png 1920w,
/static/447cfc806a1b45bc68eea4828e41ed1c/f9c26/result.png 2880w,
/static/447cfc806a1b45bc68eea4828e41ed1c/b70ed/result.png 3440w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/447cfc806a1b45bc68eea4828e41ed1c/ac7a9/result.png&quot; alt=&quot;result&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;올리브영은 온라인몰 외에도 &lt;a href=&quot;https://global.oliveyoung.com/&quot;&gt;올리브영 글로벌&lt;/a&gt;/&lt;a href=&quot;https://www.dplot.co.kr/shop?utm_source=google&amp;#x26;utm_medium=paid_sa&amp;#x26;utm_campaign=dplot&amp;#x26;utm_content=pm_title&amp;#x26;utm_term=%EB%94%94%ED%94%8C%EB%A1%AF&amp;#x26;gad_source=1&amp;#x26;gclid=CjwKCAiAxea5BhBeEiwAh4t5K08LRqjxYyn1grWSDKmUGnJCiVyoePFq-DafiVt9-sL5z6bNb1I17xoCvTMQAvD_BwE&quot;&gt;디플롯 - 라이프스타일 플랫폼&lt;/a&gt;도 서비스하고 있어요. 새로운 앱 리뷰 수신 시스템은 올리브영 온라인몰뿐만 아니라 글로벌몰과 디플롯에도 적용되어 고객의 목소리를 신속하게 구성원들에게 전달하는 중요한 역할을 담당하고 있어요.&lt;/p&gt;
&lt;h2 id=&quot;마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리하며&lt;/h2&gt;
&lt;p&gt;불편에 익숙해지지 않고 개선해가는 올리브영의 개발기, 어떠셨나요? 어떻게 보면 귀찮고 번거로울 수 있지만 저와 팀은 새로운 시스템을 만들어가는 과정이 정말 즐거웠습니다! 수신한 앱 리뷰를 활용하여 제품 기획, 마케팅, 운영 개선, 경쟁 분석등을 진행하고 있어요. 이렇게 서비스가 활발하게 활용되는 모습을 보며 제품 메이커로서 큰 성취감을 느낄 수 있었어요.&lt;/p&gt;
&lt;p&gt;올리브영 개발 조직은 고객의 불편사항과 개선 요청에 귀 기울여 더 나은 경험을 창출하는 조직이에요. 앞으로도 고객의 소중한 피드백을 경청하고 신속하게 대응하며, 여러분의 쇼핑 여정이 더 즐겁고 놀라운 경험이 될 수 있도록 열정을 다해 달려가겠습니다!&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다! 😊&lt;/p&gt;</content:encoded></item><item><title><![CDATA[10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장]]></title><description><![CDATA[매장 서비스가 온·오프라인을 아우르는 대고객 서비스로 확장하기까지의 여정 안녕하세요! 올리브영에서 매장 도메인을 담당하고 있는 알렉스입니다 :) 이전 글에서 저희는 DDD…]]></description><link>https://oliveyoung.tech/2025-04-30/store-service-journey-3/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-04-30/store-service-journey-3/</guid><pubDate>Wed, 30 Apr 2025 23:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;매장-서비스가-온오프라인을-아우르는-대고객-서비스로-확장하기까지의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%80-%EC%98%A8%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%EC%95%84%EC%9A%B0%EB%A5%B4%EB%8A%94-%EB%8C%80%EA%B3%A0%EA%B0%9D-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A1%9C-%ED%99%95%EC%9E%A5%ED%95%98%EA%B8%B0%EA%B9%8C%EC%A7%80%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;매장 서비스가 온오프라인을 아우르는 대고객 서비스로 확장하기까지의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 서비스가 온·오프라인을 아우르는 대고객 서비스로 확장하기까지의 여정&lt;/h2&gt;
&lt;p&gt;안녕하세요! 올리브영에서 매장 도메인을 담당하고 있는 알렉스입니다 :)&lt;/p&gt;
&lt;p&gt;이전 글에서 저희는 DDD의 전략적 설계와 전술적 설계를 통해 &lt;strong&gt;매장 도메인을 추출&lt;/strong&gt;하고, &lt;strong&gt;매장 서비스를 구축&lt;/strong&gt;하는 과정을 알아보았습니다. &lt;br&gt;
이번 글에서는 저희가 &lt;strong&gt;지난 1년간 걸어온 아래의 여정&lt;/strong&gt; 중, &lt;strong&gt;Part.3 대고객 서비스로의 확장 사례&lt;/strong&gt;를 소개하겠습니다 🚀&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;✅ &lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3/&quot;&gt;10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h3 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;온·오프라인 대고객 서비스
&lt;ul&gt;
&lt;li&gt;오프라인 서비스 : 올리브영N 성수(혁신매장)&lt;/li&gt;
&lt;li&gt;온라인 서비스 : 올영매장 서비스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매장 관련 정보 제공
&lt;ul&gt;
&lt;li&gt;external / internal 환경별 성격 고려하기&lt;/li&gt;
&lt;li&gt;&lt;del&gt;Rest API&lt;/del&gt; HTTP API 설계하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개발자 관점에서의 인프라 구성 (feat. TeamCity &amp;#x26; AWS ECS)
&lt;ul&gt;
&lt;li&gt;매장 서비스의 초기 구성&lt;/li&gt;
&lt;li&gt;앞으로의 고도화 방향&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모니터링 구성 (feat. Datadog)
&lt;ul&gt;
&lt;li&gt;대시보드 구축하기&lt;/li&gt;
&lt;li&gt;에러 알람 구축하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마무리&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h2 id=&quot;온오프라인-대고객-서비스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A8%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EB%8C%80%EA%B3%A0%EA%B0%9D-%EC%84%9C%EB%B9%84%EC%8A%A4&quot; aria-label=&quot;온오프라인 대고객 서비스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;온·오프라인 대고객 서비스&lt;/h2&gt;
&lt;p&gt;우선 매장 서비스가 구축되고, 가장 먼저 연계해야 했던 프로젝트를 소개해 보려고 합니다. 올리브영 첫 혁신매장인 &lt;strong&gt;올리브영N 성수&lt;/strong&gt;와 온라인 전시 서비스인 &lt;strong&gt;올영매장&lt;/strong&gt;, 각 프로젝트가 고객들에게 어떤 서비스를 제공하는지 같이 알아보시죠 🚀&lt;/p&gt;
&lt;h3 id=&quot;오프라인-서비스--올리브영n-성수혁신매장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%84%9C%EB%B9%84%EC%8A%A4--%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81n-%EC%84%B1%EC%88%98%ED%98%81%EC%8B%A0%EB%A7%A4%EC%9E%A5&quot; aria-label=&quot;오프라인 서비스  올리브영n 성수혁신매장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;오프라인 서비스 : 올리브영N 성수(혁신매장)&lt;/h3&gt;
&lt;p&gt;작년 말, 2024년 11월 22일에 새로 오픈한 5층 규모의 올리브영 매장에 대해서 알고 계신가요? 아래 기사들처럼 매우 많은 매스컴의 주목을 받았었는데요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://n.news.naver.com/article/001/0015008632?sid=101&quot;&gt;CJ올리브영, 혁신매장 1호점 &apos;올리브영N 성수&apos; 내달 열어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://biz.newdaily.co.kr/site/data/html/2024/11/21/2024112100090.html&quot;&gt;제대로 칼 갈았다… 성수동에 1400평 규모로 둥지튼 &apos;올리브영N 성수&apos;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.bizwatch.co.kr/article/consumer/2024/11/21/0035&quot;&gt;&apos;올리브영N 성수&apos; 가보니…올리브영의 미래가 있었다&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.news1.kr/industry/distribution/5506111&quot;&gt;&quot;이번 역은 올리브영역&quot;…CJ올리브영, 성수역 이름 낙찰&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;제가 개발리드로 참여한 올리브영N 성수는 각종 &lt;strong&gt;체험형 서비스&lt;/strong&gt;를 제공하는 것이 기존 매장과 가장 큰 차이점입니다. 아래와 같이 여러 가지 체험형 콘텐츠들은 현장에서 바로 예약할 수도 있고, 온라인 전시 서비스인 올영매장에서 사전 예약으로 진행할 수도 있습니다.&lt;/p&gt;
&lt;div style=&quot;width: 30%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1684px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/adac86364c13df03723e48b3a7fe8fd9/171ca/img1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 138.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAYAAABh2p9gAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEBUlEQVR42qVV224aVxTl8/of7UP70j7koZWqVq1aJW1TO64VCxGpkR3i2nXs2NjGiXFLEpsGN04I5mLuYO4YhoHhzsCwuvfBg1zFAdSOtDUzZ87ss89ae61jaLXaaLc7KMsVKLU6Gs0WiiUJnU4XtXpDjFWrVQRDITRbLYo2+BoMBrjuMvCHqx81bQBVVdHv9dHr9dDv92nBNqRyWTz3+xr0f64LQzySRD5RQjwch/8sgIticWwFky7DxtIK1o1m2G3PsL+7A384jFQqBb//DNFoFJFoDInEOYLBECqV6sTFDC+Xfof5i69htezCYjTijdOJTC6HSCSKdDqDbDZHkaVF0iIuLi5QLJYux3P0XkSZ4Bgl/GN5A5bvf4R1YQ7baytwe9xiQvHK1jk0CsZQ0zQRQzz7o7FRwnQkg+gbL0KuE3gDfkTjMfHhOvCnwvD45d9YWDBidt4EuSj9L0JEwqM1Mx7d+grffPwh3DublA3Uh8R6IoFMJoPzZArRWIzwTIv2mUhK0mFHcvMh+gkPfKtGlKIhJDNZhKiROZhdZjwcjghCJEmCTCJgnGVZRj5fQKEwHBd92JIqSD7dQ+LJKtK2Zchnr5ArFJEjpgUZlyRwcKNz0/Ndj263C/VyXCTkMjuNHCSPHbXQMbpKihiFmDxiWNOmJ0WSykgSThxBauocbet9OI2T3Cih7cAO4+Iq5kyL+PaHOdidLihkBhnCsUTk6MHNzNubWKHjLwfWt7Zh2dmFeWMPTh8phKTn9fkFIayYBDEeiydEs9dqNShKTciQSSnLRE6lIhp8KD3HC5ju/YqHD5ZwY9aMn61v0apKSKWzaJFdNRoN0S4c9Xp9FMPEikjMd10thqPDF5idmcH8/C+4+dNtWKw2wkRDs9l8B7epSFHJSDv0s9ruok3mqeo4Dd4lZipSAm9jcDuD8L320P0EMrk1V8f48JZ5azXh5LRoT51codcVwfPNfaw/2oDtyQGUskwExHF66kEgGITH4xVKOQsEhE8yfpUKHxeKeObjQV+Ujw3DidMH4517cB764TsNo04g5wsF0TbDiTVRMZPDVXMylh4n4oQ8NiSnQgk7MLw6jmD/qQvHi2b47A6a2ESTfm406v/CbWq3UZQW2k0VSpa2Iw6i8T+PI0Zoudcp48i+SeUrNL2LUj5GilCHoifBXzUDvXnHVhj2HuLTjz7A7M3PcPr6T1i3fyMvTApSQqEwKcYnzmS2L1YML8CL6c6jv48a+8Biwtr973D39g3cvfMlth4vo0iGwd7HYOfzedKyNDpnWNP8rUzdwD6o35mQocG6H+P+zCcwzX2OvZVb2Nl6QMw2IZX+23FgaJUT8LvsiJGxShkfSoXzqcB/n1L+AT/nNhs4ydkhAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/adac86364c13df03723e48b3a7fe8fd9/263a4/img1.webp 480w,
/static/adac86364c13df03723e48b3a7fe8fd9/a6361/img1.webp 960w,
/static/adac86364c13df03723e48b3a7fe8fd9/c3f6b/img1.webp 1684w&quot; sizes=&quot;(max-width: 1684px) 100vw, 1684px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/adac86364c13df03723e48b3a7fe8fd9/9aebd/img1.png 480w,
/static/adac86364c13df03723e48b3a7fe8fd9/a91f8/img1.png 960w,
/static/adac86364c13df03723e48b3a7fe8fd9/171ca/img1.png 1684w&quot; sizes=&quot;(max-width: 1684px) 100vw, 1684px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/adac86364c13df03723e48b3a7fe8fd9/171ca/img1.png&quot; alt=&quot;img1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;기존 매장과의 또 다른 점은 &lt;strong&gt;층별 안내 서비스&lt;/strong&gt;입니다. 올리브영N 성수는 5층 규모 및 약 1400평으로 조성된 매장이기 때문에, 어떤 상품들이 어디에 있고 어떤 행사들이 어디에서 진행하는지 찾기가 어렵습니다. 따라서 매장 1층, 2층, 3층에 &lt;strong&gt;안내 키오스크&lt;/strong&gt;를 1대씩 배치하여, 고객들이 상품 및 행사 위치를 직관적으로 볼 수 있는 기능을 제공하였습니다.&lt;/p&gt;
&lt;h5 id=&quot;n성수-키오스크-사진&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#n%EC%84%B1%EC%88%98-%ED%82%A4%EC%98%A4%EC%8A%A4%ED%81%AC-%EC%82%AC%EC%A7%84&quot; aria-label=&quot;n성수 키오스크 사진 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[N성수 키오스크 사진]&lt;/h5&gt;
&lt;div style=&quot;width: 30%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1590px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2a834ccfe54c69ffa73e82c13cb3cde7/684d5/img2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 136.24999999999997%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHIUlEQVR42iWW+U9b2RXHn1RNNRpV6kgjRWlC0pCk6UxmshBCgEDCnhUIIawGY2MMGELZwuIFsI2x2bzb2GYx+xIChC0JhCxtQpI2TZWOptNFbaW2f8mnd5gfjt6777731ffcc873+ySvvQe/y4rP2YPFpMNqNuAcMONyWBgO2Om3mSgrK0OhUFBeXo5MVoxeq8Vm6aGrowOjTo9LvOOydmLv1iOFg3bCIQfD/n6GhwYIiGtoaJBgYICJsAe304ZSWb4XKlUFJaUyOgx6nIOD9PdY8dodhFz9uPtMOHs6kEbExyEBMiLYjArg8Ijrxxh1MznuIzDkoqKiYi8qKyuRl5bSaTDgcbkZ6O3DY3fid/QRcFgJiGylmQkvs5N+ZqZ8TE2KmBAxFWBidpiF5WlGpsZRVmso19SgqqlBplSi7+zA7/HhGLDjd3vxDFgZ7DZgtxiQFueDrK+M8XhziudP59nammVtaZSnw3aehd08dPfRJpOjlStolZXQlHsbW2MjXq9PpG0n4PXj6rdi7WjFrG0WDMe9zEz4eTDnZ+VBiI0ns+zO+fjYpuH9jJ1dbyd9sjy682/TJy9De+MG07dz8A8N4bb/CDjkGtg7Q3e/GenF757w4NE6y94WFsf7WFweZrrHyJbPTW94jFzNIBdlQaLywjR1DZKRlYmvoEAAiVQdDgIeD6OjIYaCPlxuJ9K/vt/iP9+t8GHTwftnI7x7v8hGyMef1ze551rl7PVHpJb9nhNpO9TV95KelkIgL18U0o/X6WTE50Vv6KChTU9NvUj5v9+vMWCspbY8j+ZapdhoxNqqZ8fZhc46znX5fQpMm2QX/1HsuUlOvkSwoJCxUIixoQAjgqnLqGUhbMNh0iL98y9PcDoHRRuYRAt04HR3Md9vYFZ9jbLyBj6/PM2vquZIV/yNVvMoaSlJ2DOzMRu70bfrxFC4meozsxw2sdTbhDRg6+T+7ChqeS4TQSM7OyG2x/p5ZS7jToEC6fgyB2Q2fnphgUJ5DynJCVjTUjF3tFGhKKW/v5etxVleLIzi0FcjZV3PYNxn5JtvTqFS3EGlKUGnlPFhUE1OtoLPUrs5O/IlP0uYISmtgXPRpwjm3OTd1iIzwy60ba0sjQVYdNnIuZqGtG9/BCp5IfsjIjl/PpqDR49TkBjPamMeGVdv8ZMTk3zy5TqfntkVFa7m9OmT2K+mMhsaZKDPQntjHQtBNx5DO5G/+CXSF/sOIiu4Q3FBPjExMXy+bz/yS4kEClPIyZcjHVjik68W+Cxum6QrGs6eOYkuPhaNRo1cIfqyrhpPpxZdpYpjB48g3dVUUilSTLycwp3btyhRFDFmNPDRp6eltZYvotycq/o7Xyn/za2idmJjzmBKSkTbfo+W5no08mKsgqVWpeDXhyKRdp8tCinS8+nPD1KlqeLt2xXezLj434KdphY1kafiSZI1U9w1TX1bF0kJcZguJ3AzI5XTX3/NbyoUjPV00VNfhzo3HWlbzHCHrpHMrBsoyxW8fDnD+3kHH4eMFFVXsf/QEU6fiCDqaCSFuTlcv5KOLvY856POcvzoMZqrKxj/QQ+1rTyeHEQKecyUFuViMzXQZ2nj9as5Piy6+S5koa61gRMnv+aiOLO401Hk3srkxrUMDHExJMRe4IQoYEOVipFeM8HuTtaEWkk7m5OsLAQJ+HpZmB3ize6yAAzw7bANS18XN4UYZGXe5Ep6OvLiQq5npGOIjyExLpZjkUdprFYx4xtgtL8bv0VMysd3G/x2Z0kotEuojo93b1Z5N+Nl196JqceArLCQMnkppUKpawSbK2nJe4CXL8Zz5PARalVyxgYtDFm76KpXIz3fCDM9aqfLcE/oWgdbm2GeB228tLXTIp7JS2RC+pXCAhTUVqnJEKOnjzu/BxhxMIKK0iI8FjGyJj0FmVeR1uY8YuR6hUG143WYWX04xqtJD7sOI6raShTCoNRqFeVCqatFr6ULQF1sNMmXLhIREUFZUR59hhZsQlxTL8YhPRL698MZjo95mAp72X62wvvlSf4xN8r00gyVFeXCS9TC8RTiXklGajK6C+dE+8Rz+NBhivNuYdXfo7O5Dl2NUgjs42kerU6x9GCCh0uT7Lxc4w+LY/xVVOzp6y3BSi0MSrXHdA8wPWWPYULcBQ4cOEjB7SyMrU0YmhsxCVBpe2OaTQH4cGWSR+uzbD9fExYwzLdTQbbf7pCdk01icjLZQvZLy0rEOovm6LOibWK4JNopX6y1DfWYW5sxN9cgPVmbFqYUZnlpnI3VWZ6+2ODVTIg/CaN//WwVv1nHTVGADDHnd4RslV5Lx3s1mW59PRZ9A4amGtrvVmJsVNPbVos0N+FhTljotLDS+/PDrG/MsTk/ykt9MzvGNrZEpYOl+TRER9EWF405IQZdeiJtTVV4rOJPo/0uxqYKuu9VYmnR8H9/8Q4l2tHGAQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2a834ccfe54c69ffa73e82c13cb3cde7/263a4/img2.webp 480w,
/static/2a834ccfe54c69ffa73e82c13cb3cde7/a6361/img2.webp 960w,
/static/2a834ccfe54c69ffa73e82c13cb3cde7/89d57/img2.webp 1590w&quot; sizes=&quot;(max-width: 1590px) 100vw, 1590px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2a834ccfe54c69ffa73e82c13cb3cde7/9aebd/img2.png 480w,
/static/2a834ccfe54c69ffa73e82c13cb3cde7/a91f8/img2.png 960w,
/static/2a834ccfe54c69ffa73e82c13cb3cde7/684d5/img2.png 1590w&quot; sizes=&quot;(max-width: 1590px) 100vw, 1590px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2a834ccfe54c69ffa73e82c13cb3cde7/684d5/img2.png&quot; alt=&quot;img2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h5 id=&quot;n성수-키오스크의-매장-시설상품행사-위치-층별-안내-서비스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#n%EC%84%B1%EC%88%98-%ED%82%A4%EC%98%A4%EC%8A%A4%ED%81%AC%EC%9D%98-%EB%A7%A4%EC%9E%A5-%EC%8B%9C%EC%84%A4%EC%83%81%ED%92%88%ED%96%89%EC%82%AC-%EC%9C%84%EC%B9%98-%EC%B8%B5%EB%B3%84-%EC%95%88%EB%82%B4-%EC%84%9C%EB%B9%84%EC%8A%A4&quot; aria-label=&quot;n성수 키오스크의 매장 시설상품행사 위치 층별 안내 서비스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[N성수 키오스크의 매장 시설·상품·행사 위치 층별 안내 서비스]&lt;/h5&gt;
&lt;div style=&quot;width: 35%; display:block;&quot;&gt;
   &lt;img src=&quot;/9a24fca9419de1559c9b2ee23ea73be3/kiosk_search-crop.gif&quot;&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;온라인-전시-서비스--올영매장-서비스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%A0%84%EC%8B%9C-%EC%84%9C%EB%B9%84%EC%8A%A4--%EC%98%AC%EC%98%81%EB%A7%A4%EC%9E%A5-%EC%84%9C%EB%B9%84%EC%8A%A4&quot; aria-label=&quot;온라인 전시 서비스  올영매장 서비스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;온라인 전시 서비스 : 올영매장 서비스&lt;/h3&gt;
&lt;p&gt;올영매장 서비스는 고객들에게 어떤 서비스를 제공할까요? 가장 많이 사용되고 있는 것은 상품 상세 페이지 내 &lt;strong&gt;구매 가능 올영매장 찾기&lt;/strong&gt; 기능입니다.&lt;/p&gt;
&lt;div style=&quot;width: 35%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1338px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7174240173c365f4ae7ef7fbb073bade/5e075/img3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 136.04166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEVElEQVR42p1VS2wbZRBeceOCOHClEgcet0qVegAkDnDgIfWCEAqnNEpAIlVFpVJBRVsSQpuEZ0sbGppQlAoJmkCVqAiRhkaQ0Di209RNgmPH3thx7PixfuyuvQ+vd/dj/o0dxdQtDb80/ndndr5/5puZ3xxoWZaNlpGfcMzrwfm5eZz+ZRxDM7O46ltEOplGLJ6CoupISwq89PxDIIBBnw99Lhc8QR4T6xvIGQaDAsd+DNPE65eH0bO0gIu+BRz8eRRuPgofH8Hk1F+44b4JTS8jIRXx7WoQB2670e76E20Tv2J0dg5CseSA2TVA1bTw6uBFHLw2jgGXF02XR9A29Qde6ujErrZm7Gp6Db3DP2JUEfGllEFfKYezcgYD0TBGKCNeFOsBRcvEEx1deLK7G8/3X8DD/efx0PAQXv7ua+z+/AT2nu7AK4Nn0ZtN4NNCCifzCXxcSKBXTqMzeBtzmaQDaNn2JqBiVnBkyYf2W160z3vQ/rcPzZPjeLb/CzzefRRPnXofL144gx4hhp58HF3ZGE6SfJiJ4Ux6DXJZr49w+2KnsOUJh7Dnsy48/Ukn9hw/jGe6O/BBMoqj6SiOEchHtB/ZCGOAonb8qv7cdiAmFWvTNBsMYPepE3jsnTfxaOsb2Hv8Pby9FsKBdR6H4jwOx8N4ay2AKTnfGHALuAo4t7yMR/Y34cGmfXhg3wt47t1DOJeKo5n3oyUSQCu/jO+FDZjVjHA3wJpZJ17GPC58c2MSQ+4pLK1FYJL+llTA9ZyAQEm+A6whYKOIg/5lLC4uwqhU/nV4A0CTmrpimmgUqUlgzBbf2EA8kXDAbYeveiibIjWpl9nOSdSUYkFEhU43aHzYXhMGwA50gBwns2rbDMKupszsiqJCVTVwYrGIVEaAKEoo0nM+n4cgCEgmk8hmsyQ55z2by6FQKDi6TCbjfGc2yIwz6OMiObO0ItEogZYaUnA3ftniV1exv6UVX53rA2fnRehpAdFYHAniySiXHT7uZ9VSzufyuHJlFK5ZNzhF0SDRLSLLMiRK+dZKyOHo/y5OUVUUSyUHkO3rxI9pWTsCYZGyYjFOuUrZhCgwsKLDn65rTnFYkWTac0IBFe3+I+YMw4SmllEm7nRddyZE1XSnBcoG6TTitGI1bOKGgKrGHA2k0xlEqcqsMCL1Zo7aRCPbTlPfGj1W+oWFBfj9fqxQYfx0OTBhPccoYBxb9+CW2RiP3PYpYJPCxEmdRNM3U6+92/Z/p82xtBh/SWrsYCCIcCiMleAKVvlV6q9c/TQwwHuITcFxW9cVhauSaASgUR/q1WHfcR/qkSjMUAgKcWcTj1QZ2JEIQGLx/KaOvVf1TFfT1wnTr69ThGNjQCq15VQvEahUGHlpCSLdh3owCMTjoJYAyHnLh+3UHfb0NE3K2FVIN32QZz2Q3XN3SGHGjey0C9mpGUj0px777Rp8ly4h+ft1lLzz5Od1RKJnZWIS/wD4hMT0P1wY2QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7174240173c365f4ae7ef7fbb073bade/263a4/img3.webp 480w,
/static/7174240173c365f4ae7ef7fbb073bade/a6361/img3.webp 960w,
/static/7174240173c365f4ae7ef7fbb073bade/f3741/img3.webp 1338w&quot; sizes=&quot;(max-width: 1338px) 100vw, 1338px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7174240173c365f4ae7ef7fbb073bade/9aebd/img3.png 480w,
/static/7174240173c365f4ae7ef7fbb073bade/a91f8/img3.png 960w,
/static/7174240173c365f4ae7ef7fbb073bade/5e075/img3.png 1338w&quot; sizes=&quot;(max-width: 1338px) 100vw, 1338px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7174240173c365f4ae7ef7fbb073bade/5e075/img3.png&quot; alt=&quot;img3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;해당 기능을 통해 고객은 사고 싶은 &lt;strong&gt;상품에 대한 주변 매장의 재고 정보&lt;/strong&gt;를 한눈에 확인할 수 있습니다. 또한, 해당 매장의 위치·전화번호·영업시간·휴무일 등의 정보도 같이 볼 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7c79c4b63c149e39d7de2346c7c54688/1c4ec/img4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 65.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAChUlEQVR42nVTXUsUURiefxcYhEJBimAi3lSUEVIXZUV1YUb3RaYmatKNFixKuTnqrvkRq7mZtpBkuX7srrvzsTNzZs7M03MGEWvxwMvMcN7zvM/HGe3nfh5lw4Blmqjwmc/volwuo1qtIgh8OI6DjY0NjI+PYV6fwUrmK7LZHBYXkshkFpHb3MTm9zXk1tdZWWiJH9+wvX8A27AQSIkwDCFlSDAJGUZQayo5jTMNbejoaUfX3RtobmvEze47uPWwE4+fPkJH5zU8uN2BJ93PoBkVE54I4HoCpmUflyN8iBCQBEym0qhraMHZnqu433UdzRfOof1yM1ov1eNiUyOaWlpxvr4ObVfuQSsWizGLKIogWcSIK/J9RLQh4n5uYgJD/X1ILCxhdTWD5fk0sitfsPR5FvOpOWTXVpGa1TE9k4a29WsbJR4SrgMIEQOEOzsIt7Yg6Z2zvIxSJgNBC0Jawck4bXmeB+1V33OMjQyi+uc3JEOheWqHWmXc5NPT4uEhPO45RgVupYJqqYTy7i5sDvepQrAk+y3bhvb2zWu8T7zjQT8O5J9FNgEHlMi4dFCAwfRty4LH5AWHSjVcMeaNUMtxXMXwBT5MJuC6LoQvamSo5A/JplAooEhmBpmaZGQqYFqkKuD7seTh4X6k55IIGII8klkDyLvpkIUCEgqAzFSpfqmu2RFDRUobHRlFKq3D8Z046VMZUrJi5XOwAlVs1KUX/AalHjMcHBiAric5SdZ6eARY3ttDnj5WGAg4U/VFrPipSJwEfNnbi4/6JwRhgPA0hvl8LMdkioJSa7rINJasAIfooT43BduzYwlqyslyCKQkG3v7MPiLukza+b8YmOq1LAt/AeuBuEARTxdfAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7c79c4b63c149e39d7de2346c7c54688/263a4/img4.webp 480w,
/static/7c79c4b63c149e39d7de2346c7c54688/a6361/img4.webp 960w,
/static/7c79c4b63c149e39d7de2346c7c54688/0b34d/img4.webp 1920w,
/static/7c79c4b63c149e39d7de2346c7c54688/da28f/img4.webp 2880w,
/static/7c79c4b63c149e39d7de2346c7c54688/656a1/img4.webp 3638w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7c79c4b63c149e39d7de2346c7c54688/9aebd/img4.png 480w,
/static/7c79c4b63c149e39d7de2346c7c54688/a91f8/img4.png 960w,
/static/7c79c4b63c149e39d7de2346c7c54688/ac7a9/img4.png 1920w,
/static/7c79c4b63c149e39d7de2346c7c54688/f9c26/img4.png 2880w,
/static/7c79c4b63c149e39d7de2346c7c54688/1c4ec/img4.png 3638w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7c79c4b63c149e39d7de2346c7c54688/ac7a9/img4.png&quot; alt=&quot;img4&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;매장-관련-정보-제공&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EA%B4%80%EB%A0%A8-%EC%A0%95%EB%B3%B4-%EC%A0%9C%EA%B3%B5&quot; aria-label=&quot;매장 관련 정보 제공 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 관련 정보 제공&lt;/h2&gt;
&lt;p&gt;올리브영N 성수(혁신매장)와 온라인 올영매장 서비스에서 고객에게 제공하고 있는 매장 관련 정보는 모두 &lt;strong&gt;매장 서비스&lt;/strong&gt; 에서 제공합니다. 또한, 대고객 서비스뿐만 아니라 &lt;strong&gt;내부 시스템 간의 연동·통신&lt;/strong&gt;에서도 매장 서비스를 통해 정보를 받아 갑니다.&lt;/p&gt;
&lt;p&gt;여기서 저희는 아래와 같은 고민을 하였습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;대고객 서비스(external)와 내부 서비스(internal)에서 모두 같은 정보가 필요할까?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;h3 id=&quot;external--internal-환경별-성격-고려하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#external--internal-%ED%99%98%EA%B2%BD%EB%B3%84-%EC%84%B1%EA%B2%A9-%EA%B3%A0%EB%A0%A4%ED%95%98%EA%B8%B0&quot; aria-label=&quot;external  internal 환경별 성격 고려하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;external / internal 환경별 성격 고려하기&lt;/h3&gt;
&lt;p&gt;저희는 매장 API를 통해 매장 기본정보를 제공해 주어야 했습니다. 이때 필요한 DB 테이블은 매장 정보 엔티티에 매핑되어 있는 테이블일 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;Part.2: 매장 도메인의 구현 여정&lt;/a&gt;에서 소개한 ERD에서 매장 정보 엔티티의 컬럼 일부를 살펴보겠습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c262813e2551081429926d3cd96ddb32/e074b/img5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.916666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABvUlEQVR42m2RS28SYRSGZ9EYWZCQUiZh5DIzDOhMhzZINBiHxJWomCrX2kYtMZFLC12wJRA2rFmw7KL/wHRXF436D0x3/BIgXPL4dRauepIn5+ScvOfyfVI+n6d6WKX67SvFcpmTL0fUv3/mw8cC5UrZzRUrFQqCg1KJv7e33NlsNmM+n7NYLFgul6xWK9dLsixjGAahhI7yKEgkFEYOBNjx+9GjUUxVZV/4p5EITlDh5vKSs/MOpWKB9/l35HKvORQLVcTgXq+HFAmHsW0bTdW4ixVFIRgMogjxXswgk0jgCF7F47zVNG4uLjgoFMhms65O13Ucx8GyLI6Oj0VDM4FpmiSEKCaKqthIjUTR4gbWi2c8FjVDoIm6sbvLj6sr3uRyZDIZ0um02zCVSqGJYRXxLJIcVojFdOLibF0kw6GQ2FbFSto82bPdOGmZOOkUqWSSn9fXjMdjhsMho9GIfr/PYDCg2+0ymUyQHmxt4fP58Hg8Lg8FXq/XPTvg38G3vY0d0/j08jmWrvLr9x/3U9brNfeZVKvVaDab1Ov1/zQaDdrtNp1Oh3PB6VmbeuuUZqvFdDp1hZvN5t6G/wCBeyY14NTPWwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c262813e2551081429926d3cd96ddb32/263a4/img5.webp 480w,
/static/c262813e2551081429926d3cd96ddb32/a6361/img5.webp 960w,
/static/c262813e2551081429926d3cd96ddb32/0b34d/img5.webp 1920w,
/static/c262813e2551081429926d3cd96ddb32/da28f/img5.webp 2880w,
/static/c262813e2551081429926d3cd96ddb32/736fc/img5.webp 3336w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c262813e2551081429926d3cd96ddb32/9aebd/img5.png 480w,
/static/c262813e2551081429926d3cd96ddb32/a91f8/img5.png 960w,
/static/c262813e2551081429926d3cd96ddb32/ac7a9/img5.png 1920w,
/static/c262813e2551081429926d3cd96ddb32/f9c26/img5.png 2880w,
/static/c262813e2551081429926d3cd96ddb32/e074b/img5.png 3336w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c262813e2551081429926d3cd96ddb32/ac7a9/img5.png&quot; alt=&quot;img5&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;온라인 서비스에서 고객들에게 필요한 정보는 &lt;strong&gt;매장명, 매장 전화번호, 매장 주소&lt;/strong&gt; 등이 있을 것입니다. 그렇다면 &lt;strong&gt;예상 일일 매출금액&lt;/strong&gt;과 &lt;strong&gt;BEP 금액&lt;/strong&gt; 정보는 어떨까요? 고객 입장에서 과연 필요한 정보일까요? 궁금한 고객도 물론 있겠지만, 회사의 대외비 정보를 함부로 노출시킬 수 없습니다.&lt;br&gt;
따라서 매장의 기본정보 API더라도 해당 정보를 사용하는 클라이언트에 따라서 제공하는 데이터가 다를 수 있습니다.&lt;/p&gt;
&lt;p&gt;또 한가지의 다른 점은 데이터 양입니다. 내부 서비스(internal) 간 연동은 보통 많은 컬럼과 데이터를 제공받길 원합니다. 하지만 온라인 서비스(external) 환경에서도 많은 컬럼과 데이터를 한 번에 내려받는다면 어떨까요? 불필요한 컬럼들을 포함한 대형 JSON 페이로드를 한번에 내려받을 경우, HTTP 응답 크기가 불필요하게 커져 네트워크 전송 시간이 길어질 것입니다. 이는 곧 페이지 렌더링 지연이나 API &lt;a href=&quot;https://en.wikipedia.org/wiki/Latency_(engineering)&quot;&gt;Latency&lt;/a&gt; 지연으로 이어지고, 사용자에게 불편함을 초래할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;rest-api-http-api-설계하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#rest-api-http-api-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;rest api http api 설계하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;del&gt;REST API&lt;/del&gt; HTTP API 설계하기&lt;/h3&gt;
&lt;p&gt;그렇다면 내부 서비스(internal)와 온라인 서비스(external)는 각각 어떻게 HTTP API를 설계해야 할까요? 흔히 &quot;&lt;strong&gt;REST API&lt;/strong&gt;&quot;라고도 많이 부르지만, 사실 일반적으로는 Uniform interface의 제약조건 중 Self-descriptive와 HATEOAS를 잘 따르기가 어렵기 때문에 해당 포스팅에서는 &lt;strong&gt;&quot;HTTP API&quot;&lt;/strong&gt; 로 부르겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST의 자세한 내용이 궁금하다면 &lt;a href=&quot;https://en.wikipedia.org/wiki/REST&quot;&gt;Wikipedia&lt;/a&gt;를 참고해 보시길 추천드립니다 😁&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;우선 API 설계에 앞서 HTTP API의 주요 개념에 대해 간단하게 짚고 넘어가겠습니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1086px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c1b86c43aae030226ee15b2f6d4d8ad9/a1954/img6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 18.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA2klEQVR42i2Q226DQAxE+ZCwV7OB3XBJAkmgrVpV6kP//4NODerDkT0eaaRxZY3FOYexOxZjDKd6n6rV22+1+Ud3Z2qcNdj6pH6t/o5qZc+ppG8JzjJ1wjU3DP3Aes/MQ8NUhDEL85R4lY6lzfj+Thxm5LZxmR7c8srYPShpxGpOVbYrXiJrn/h9ZpahQyQcgd/Pjo+lVe1561peKWHLRLN8YpvEdFmZyzuP/ott/OEsmSpomPMeUc4aJMEd9ULwJNWpCcc7ovOK01oeHxus1g0+KJHoBYnp2P8AbHNz1tRiXe4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c1b86c43aae030226ee15b2f6d4d8ad9/263a4/img6.webp 480w,
/static/c1b86c43aae030226ee15b2f6d4d8ad9/a6361/img6.webp 960w,
/static/c1b86c43aae030226ee15b2f6d4d8ad9/537ed/img6.webp 1086w&quot; sizes=&quot;(max-width: 1086px) 100vw, 1086px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c1b86c43aae030226ee15b2f6d4d8ad9/9aebd/img6.png 480w,
/static/c1b86c43aae030226ee15b2f6d4d8ad9/a91f8/img6.png 960w,
/static/c1b86c43aae030226ee15b2f6d4d8ad9/a1954/img6.png 1086w&quot; sizes=&quot;(max-width: 1086px) 100vw, 1086px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c1b86c43aae030226ee15b2f6d4d8ad9/a1954/img6.png&quot; alt=&quot;img6&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;흔히 볼 수 있는 URL은 위와 같은 요소들로 구성되어 있습니다. &lt;br&gt;
여기서 저희는 보통 API를 설계할 때 &lt;strong&gt;패스(path)&lt;/strong&gt;, &lt;strong&gt;쿼리 파라미터(query parameter)&lt;/strong&gt;, &lt;strong&gt;요청/응답 본문(request/response body)&lt;/strong&gt; 등을 주로 설계합니다. 이 때 잘 고려해야 할 부분은 &lt;strong&gt;리소스(자원)&lt;/strong&gt; 와 &lt;strong&gt;행위(메서드)&lt;/strong&gt; 입니다. 위의 예시처럼 매장의 정보가 필요할 경우 &lt;strong&gt;&quot;매장(shops)&quot;이 리소스&lt;/strong&gt;가 되고, &lt;strong&gt;&quot;조회(GET)&quot;가 행위&lt;/strong&gt;가 될 것입니다.&lt;/p&gt;
&lt;p&gt;주로 사용되는 HTTP 메서드(행위) 5개에 대해 핵심 내용만 표로 간단하게 소개하겠습니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1416px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fabf43458bfefbfac73c0ae18079cb21/c6e0f/img14.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3UlEQVR42m1QR6rFQAzLOdILqaSQ3gtJ7n8lfWSYt/hkIQzjUbG0MAwxDAOWZcG6roKmaRDHMXRdh2masG0bjuN8gjtqXNeFKIqgkdh1HYIgkAUnBbdtwziOMouigGVZQv4Pvruui7Is4XkeNKpO0/RzMwwDdV2L43meeN9XPisiQaKC7/vCZTBOSdi27e8EEplQCRLzPOM4DrmkqioxZIi+72XHq9I0FTONZ+77jizLRIydUfC+b+nzeR4hs+MkST775Bt3kpDqdKKg6iXPc+mPiTi5YxVfHSoDdfIfG5+xZHr6YXUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fabf43458bfefbfac73c0ae18079cb21/263a4/img14.webp 480w,
/static/fabf43458bfefbfac73c0ae18079cb21/a6361/img14.webp 960w,
/static/fabf43458bfefbfac73c0ae18079cb21/c9fa7/img14.webp 1416w&quot; sizes=&quot;(max-width: 1416px) 100vw, 1416px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fabf43458bfefbfac73c0ae18079cb21/9aebd/img14.png 480w,
/static/fabf43458bfefbfac73c0ae18079cb21/a91f8/img14.png 960w,
/static/fabf43458bfefbfac73c0ae18079cb21/c6e0f/img14.png 1416w&quot; sizes=&quot;(max-width: 1416px) 100vw, 1416px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fabf43458bfefbfac73c0ae18079cb21/c6e0f/img14.png&quot; alt=&quot;img14&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;추가로 잘 고려해야 할 부분 중 하나는 &lt;strong&gt;멱등성&lt;/strong&gt;(Idempotent)입니다. 1번 호출하든 100번 호출하든 작업 결과가 항상 동일한지를 의미하는 이 멱등성은 추후 여러 이유로 API 요청이 실패하였을 때 &lt;strong&gt;재처리 전략에 용이하게 사용&lt;/strong&gt;됩니다.&lt;br&gt;
따라서 각 HTTP 메서드에 따라 규약에 맞게 멱등성을 잘 보장하도록 설계해야 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;저희는 우선 조회성 기능을 먼저 제공해야 했기 때문에, 아래와 같이 HTTP API 설계를 진행하였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/078137664d474fb20859d85fdde4487a/a12f4/img7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.00000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6UlEQVR42n2S2W7UQBRE/f/fgIR4QvwEIIQELywJTCaz2h5P2+3e3d7tKMVtJ0SMCDwcXbnduqqqrsgqidNxD845rLUwWkMTxmgopaCkepiElBJaafRdi3HoLwhnA83oNm/wbqWRcotcOhSqAhN2mdK1KG2D0tQXU1UdtO+fMPWwnOuqQfThOOPlt3usmcc64TgUDntmkAiP3A4o3IjSz49M4IHqkvDvJBvkukb0OZ3w5nrG7dlhQwtv4gKbk0BcWOzPCsy0kM0MQYv+haxnnFWDIiz8lM54fXWHbe6xywS2mcQxJ4Xc4UhLmW7BXU8qxv8vpHsFRRJ9jEe8+jphw2qyqrEju9tMkUqFmHsceUWLK+RkvSB7z8GDZdXRwvZR4Q/gwBuyKLFJy8XqiTIMKoNC1dwtKkQ9PUuIJCjkhh4lVCSJU+wPCeIkg9IWyli4ylN1HIx10PRd+RrDOC6M4/QXXT+gHwZEm4Jqs7bYnUokTCAXpKo0S4VK7RdChQTl4/sZvptQteMFnjC+g6uph++pNi++3GOVOfw85Lje57janvF9x3CTCKxievmkXLJNRINUtWCUZ07Z/aagDGNRI5MVolUx4e3+4dkZqTv/QU4XMm7AQuFJKaNvTtUw9QjbTE84ylBV/aLyF4kGOyMpn2/CAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/078137664d474fb20859d85fdde4487a/263a4/img7.webp 480w,
/static/078137664d474fb20859d85fdde4487a/a6361/img7.webp 960w,
/static/078137664d474fb20859d85fdde4487a/0b34d/img7.webp 1920w,
/static/078137664d474fb20859d85fdde4487a/68dd9/img7.webp 2194w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/078137664d474fb20859d85fdde4487a/9aebd/img7.png 480w,
/static/078137664d474fb20859d85fdde4487a/a91f8/img7.png 960w,
/static/078137664d474fb20859d85fdde4487a/ac7a9/img7.png 1920w,
/static/078137664d474fb20859d85fdde4487a/a12f4/img7.png 2194w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/078137664d474fb20859d85fdde4487a/ac7a9/img7.png&quot; alt=&quot;img7&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;예시를 보면 매장 관련 정보에서 &lt;strong&gt;매장 기본 정보&lt;/strong&gt;와 &lt;strong&gt;매장 요약 정보&lt;/strong&gt;가 나누어져 있습니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/eb95f3e99e59db8757d5b6fa8a1956e7/a12f4/img8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.00000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACH0lEQVR42n2SW08TURSF+6v9GyY+eElEQAlgIiT6gAjElpuGBxNUWlraaaft3C9nrp12WvRztyiRSHz4snLOnKxZZ51diUyDwfdzXE0jNgzUYEAoqOGQQNcJPI8gDAmCAN/3CYOQcTGinIzvMN+biFaay1tUtz/R3z/Crp5g106xfqt/cka4e4AXprhRjquyhQZJQZiOb1HZZLEfJjmVw2c7vFi74HLnmMt3e2gfjmi/r6EfnGIenuHuHeEmU9x0Jkxx5iR3mX8b+DlWmFH5/PQtm4/2ab7aprm8Tn1pndbKJt31bdqi1m4Vf3SNl5R4YnYffjbDCHLsheGDh2w8rnH1UgxWN2mtvkZbe4MuhtraFs6TJRw1WqT7r2E4wpZKKseNhJXDkGbLot3scyW0GjrNeo9u26A78Ok6CVZcYsv17sOZXzkoxHAkCYc/2GiA5hd0bEXLCEQjBvLHvp9hRgVBfr1I4WXTe/Hzm4SOkkcJZST0nk5H0+npAxkRJUTEcYpSESqKCUWTNGNSlgvKcvoPxXjCeDKh0rJzqpcR7YGLbnpYXozpKiw/xp2Pi2AHCZ70k45npMWUZFTeIRVUWhBnMoe17oznX35SH8ZcaBbfOhZfrwzO2yYN3aPec0Rd2qZC93L6wQhT+rSkuz/Y0mHPyxj6CZW6PeVj5+bZTUln/IUlB4aOwpS0tiQ1Ze3IaKisJMqnt8TSYZCMFyl/AbfpKUTvYEwiAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/eb95f3e99e59db8757d5b6fa8a1956e7/263a4/img8.webp 480w,
/static/eb95f3e99e59db8757d5b6fa8a1956e7/a6361/img8.webp 960w,
/static/eb95f3e99e59db8757d5b6fa8a1956e7/0b34d/img8.webp 1920w,
/static/eb95f3e99e59db8757d5b6fa8a1956e7/68dd9/img8.webp 2194w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/eb95f3e99e59db8757d5b6fa8a1956e7/9aebd/img8.png 480w,
/static/eb95f3e99e59db8757d5b6fa8a1956e7/a91f8/img8.png 960w,
/static/eb95f3e99e59db8757d5b6fa8a1956e7/ac7a9/img8.png 1920w,
/static/eb95f3e99e59db8757d5b6fa8a1956e7/a12f4/img8.png 2194w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/eb95f3e99e59db8757d5b6fa8a1956e7/ac7a9/img8.png&quot; alt=&quot;img8&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;매장 기본 정보&lt;/strong&gt;는 매장에 관련된 모든 마스터성 컬럼을 포함한 기본 정보를 반환해 주는 API입니다. 반면 &lt;strong&gt;매장 요약 정보&lt;/strong&gt;는 필요한 컬럼들만 요약해서 반환해 주고, 여러 가지 컬럼을 조합하여 고유의 &lt;strong&gt;비즈니스 로직&lt;/strong&gt;을 통해 특정 상태를 나타내주는 컬럼까지 포함되어 있습니다.&lt;br&gt;
예시로 &lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;Part.1&lt;/a&gt; 에서 잠깐 소개했던 매장 영업 상태 값은 아래의 컬럼들을 조합하여 비즈니스 로직을 통해 판단하는 컬럼입니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;매장 운영 상태 컬럼&lt;/strong&gt; : 해당 매장이 정상적으로 개점한 매장인지? 아니면 아직 공사 중인 매장이거나 폐점한 매장인지?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;매장 휴무일 컬럼&lt;/strong&gt; : 오늘이 매장 휴무일인지?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;영업시간 컬럼&lt;/strong&gt; : 오늘의 영업시간은 몇 시부터 몇 시인지?&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;따라서 매장의 필수 요약 정보들만 실시간으로 빠르게 볼 수 있는 API는 &lt;strong&gt;external&lt;/strong&gt;, 매장의 모든 마스터성 기본 정보를 방대하게 받을 수 있는 API는 &lt;strong&gt;internal&lt;/strong&gt;로 제공될 것입니다. 실제로 코드상에서도 Presentation 영역인 Controller도 아래와 같이 internal과 external 패키지에 각각 분리되어 있습니다.&lt;br&gt;
아래와 같이 구성되어 있다면 추후 external 환경과 internal 환경을 완전히 분리할 때, 패키지 단위로 모듈을 쪼개기만 하면 간단하게 분리 작업을 완료할 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;width: 30%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 818px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fc385e9a52c92b432a0497a2c666dd60/35f7f/img9.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAYAAAAxFw7TAAAACXBIWXMAAAsTAAALEwEAmpwYAAADn0lEQVR42p2V23LbVBSG/QJwR2eA1nFs63ywjlZly5KlyPLZ9SFO4gwULkppqJM2pHSmQ2bgpnAPwyWPwDORGS7wW/ysbcyQaTo05uK3tiTr09pr/Wspp+gWaqaLl809HKYd7IcxmqIKT1Kh0j1VM7dSjqvYWPASftI0/NLew2UtxAvPx+udXaiyvjU0J9ADHV7G6W4Rz4IQj5IBLqo+vjMdGFYVimpsB2Q/MsmSDYT1GmaHY4xaXUyjBD6lQiKgti1Q1QyIuovIN/HpgI6tETyKckiRm5QSdXvgBqoYqDop+vEQQZphP27hs4oFntKibQtUSCap0uvAO1pgkA2geTVMizyGnIQyRapvF+EGXHExG8WYDTPIehUCRX66U0RD0igt7470JpCKYNsujudNPOgkUC1/7clv7+YJaNNO3rTRv+fa24DsoqzaSGIfIUWZtnsoWy4+IV/+/MEdlHX6j1qBSlI2YvlnKZP+ATKIvlGFAWlroajjlHx42cxwLhl4XpLw64d5nHIKnpZlLJloveQ0nOxyOMsXMOXEjQ8JwDqGiSflyX8Z3fw8aIFPprDDIcxoBCnsw44nsGhtsWtBD3ZzDKkawqGuerxbRo7B6rKGGQEmnEBHgfwn4SG99XtPwSjuYFrrY1Kjo59hUu9jTOsH97O1JvUeRo0hjp0IXxUIWKJEL4l8RtBllOI8TPDKr+NVoYhDgh6UmPhr4q7p7/N5kcOiWMaeoCDHrNBdR8TjzJTxY6JTL9expE7hbW9t6tuIIzFWjhVEpMrx5Lk0tHBy7CMezJCQqZOyCHFTLO2W2lTZWPvLcy2M0wBZsw83iHBgOMj0/9l662pTLzu1FtqdKeKkhYSmzhfUIa6kQ7ol9EYv6+0W6os5xr0RNMphVVRwQgWSbjlob7aeYmHWDbA/SqFXPBoKFtlJwmOCFsmjW/eyylqLBsTRQYTZpA3TbSBP0Kc7JcypSMWKs45W3OjNyHNvewv7cEWeh2gvwrw3QJOuBWT83957HxPynSspCCgVAXn3vqL/N5BtiVX1Sb6IEyfA83qGM83DE9nGuUijTCJr0fmXnIyHNDAuP76HmL6SwqZoOZ0NzmuqkGTDxaNCCU5/BKF7CKm9IB2Bb80hpLP1WmwOccePMaXu6NJ4E6j/2bMUoXF1XeTJK86wf1/eLfzxg8mtXtr+6sKIVi+MYHWh11Zfky5o/Y3RWD0zG6vXH937MxXkK9rV+tm/APZCJv8geVTqAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fc385e9a52c92b432a0497a2c666dd60/263a4/img9.webp 480w,
/static/fc385e9a52c92b432a0497a2c666dd60/a749c/img9.webp 818w&quot; sizes=&quot;(max-width: 818px) 100vw, 818px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fc385e9a52c92b432a0497a2c666dd60/9aebd/img9.png 480w,
/static/fc385e9a52c92b432a0497a2c666dd60/35f7f/img9.png 818w&quot; sizes=&quot;(max-width: 818px) 100vw, 818px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fc385e9a52c92b432a0497a2c666dd60/35f7f/img9.png&quot; alt=&quot;img9&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h2 id=&quot;개발자-관점에서의-인프라-구성-feat-teamcity--aws-ecs&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B4%80%EC%A0%90%EC%97%90%EC%84%9C%EC%9D%98-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%84%B1-feat-teamcity--aws-ecs&quot; aria-label=&quot;개발자 관점에서의 인프라 구성 feat teamcity  aws ecs permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발자 관점에서의 인프라 구성 (feat. TeamCity &amp;#x26; AWS ECS)&lt;/h2&gt;
&lt;p&gt;초기 인프라는 어떻게 구성했을까요? 우선 올리브영은 기본적으로 인프라에 대한 구성 및 관리를 &lt;a href=&quot;https://en.wikipedia.org/wiki/Platform_engineering&quot;&gt;플랫폼엔지니어링&lt;/a&gt; 조직이 담당하고 있습니다. 따라서 인프라 구성에 대한 자세한 내용보다는, 개발자 관점에서 설계한 초기 구성도와 CI/CD 설정 등을 간단하게 소개해보겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;매장-서비스의-초기-구성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%EC%B4%88%EA%B8%B0-%EA%B5%AC%EC%84%B1&quot; aria-label=&quot;매장 서비스의 초기 구성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 서비스의 초기 구성&lt;/h3&gt;
&lt;p&gt;우선 올리브영은 기본적으로 AWS ECS Fargate를 통해 애플리케이션을 구동합니다. 그리고 각각의 역할에 맞는 로드밸런서(LB)를 앞단에 배치합니다.&lt;br&gt;
아래는 초기 매장 서비스의 인프라 구성입니다. 초반부터 external 환경과 internal 환경을 분리하기에는 요구사항이 명확하지도 않을뿐더러 너무 이르다고 판단했기 때문에, ECS 클러스터 내부에 하나의 서비스로만 구성되어 있는 모습입니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1720px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1be2f012f6ecdb97b9e6763e85576696/92aea/infra1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 61.45833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABeUlEQVR42qVTXWvCMBT1//+jvcle9rQhjDFhbAwVbdWqjWmSJjc5O0l1CoM9aGiTtNyeez7SEYJDW8/heo8UI+4dozxttg3UUcNbC4QA3AE8SlEgvSW7BBGCJYIFQSLzlJ/LSLzS5avr/R+G0aOefaFVGjEDSAYTQG3Q7FvUmntIKV5XFfRRUYGUxp5qllUNCR7G2NJnlGetOxjnEHpXJEfLFQGTlcN41hewwOJ232D39ghnDKw1LA04HDuMX1fQxl08XG+2OBJUTv4lBlSU6S3ksCx79bGAUwpq/g7PxqbTcCRhncfL55ZrX6wpDG1mR7mBK6uRuo4qKan6RreYFsGHyRfMbodm+gRHdn1WQ4DO9nh4nl8xpPlds4T3PDZJhoT9EE4qdxqCQrZO2DgOXsfBV8nvqCwEOQGyuK5WUFlyT9o0OHl/SjOewNLQ6Az+b8ocbatI2UAot6R8BrztYCfsD20xuKScpYvcAUj6kX4EAsXfg3z7r/cD+DCvKgwhjSsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1be2f012f6ecdb97b9e6763e85576696/263a4/infra1.webp 480w,
/static/1be2f012f6ecdb97b9e6763e85576696/a6361/infra1.webp 960w,
/static/1be2f012f6ecdb97b9e6763e85576696/fcade/infra1.webp 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1be2f012f6ecdb97b9e6763e85576696/9aebd/infra1.png 480w,
/static/1be2f012f6ecdb97b9e6763e85576696/a91f8/infra1.png 960w,
/static/1be2f012f6ecdb97b9e6763e85576696/92aea/infra1.png 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1be2f012f6ecdb97b9e6763e85576696/92aea/infra1.png&quot; alt=&quot;infra1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;그렇다면 초기 CI/CD 환경은 어떻게 구성했을까요? 올리브영은 현재 &lt;a href=&quot;https://www.jetbrains.com/ko-kr/teamcity/&quot;&gt;JetBrains사의 TeamCity&lt;/a&gt;를 CI/CD 파이프라인 도구로 사용하고 있습니다.&lt;br&gt;
각 프로젝트별로 Build 단계와 Deploy 단계를 분리하여 운영 중인데, &lt;strong&gt;Build&lt;/strong&gt;를 수행하면 Docker 이미지를 생성하여 ECR로 업로드(Push)를 진행하고, &lt;strong&gt;Deploy&lt;/strong&gt;를 수행하면 ECR에 저장된 최신 이미지를 가져와 CodeDeploy를 통해 Blue/Green 배포 과정을 거칩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자세한 내용은 &lt;a href=&quot;https://oliveyoung.tech/2022-05-03/How-to-Set-up-Build-Process-with-Teamcity/&quot;&gt;올리브영 CI/CD 구축기 시리즈&lt;/a&gt;에서 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 887px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6cb0ab39b5e026aaa87b5f292a941f72/713f0/cicd1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAABqklEQVR42q1UyU4CQRDly7149uhFo8aT3gxEEzUxglEOghzQKCFBTNh3mOnZentWNwMEGY0HOul0par7vapXNZPClldqaWm9EVx4cuMITOr/A0rarpKJFxThnFa66HjRGsmfgJ4ToFotI+QRFCHoOFulFJQkIuYCnEMK+c8MhYDiAYQ5CUxKBUlgmsB0FEKRX9MGESZJswHYmgkcvzjgpJOgTFzm2+DhwzPSj0VrD/wQO+dXCOPS57KvqlkD7DgcB/keASp7wWRn1k2hiKfsq7WnIcNZ5g7BONjsoVY/u2yiK6fjCvg+lUsXheCWxGhrspckw4KQMYZmu21fSynWx8ZMhXlk1t7+Fy6v+9b2Q27TmZc3p9ZxuUaa1lsRXvkWIq4ulSTsZ32CwYyeNUsIP7LW1+03cJLZxWjYgOt4YM4ImksE0yH6lYJtZiKgLU/OxyOoFcHec9YejftI3x9hOukicH0EbAYZcUSjLtxaaTluiYCc5s1oJcgWOp7HWLdFJiZuQLxOHb38Bemqfy95MQ6avh67bVMUEUXr4HRGRO5xaZtiNE5t++fwDZj0RYJerWptAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6cb0ab39b5e026aaa87b5f292a941f72/263a4/cicd1.webp 480w,
/static/6cb0ab39b5e026aaa87b5f292a941f72/6f880/cicd1.webp 887w&quot; sizes=&quot;(max-width: 887px) 100vw, 887px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6cb0ab39b5e026aaa87b5f292a941f72/9aebd/cicd1.png 480w,
/static/6cb0ab39b5e026aaa87b5f292a941f72/713f0/cicd1.png 887w&quot; sizes=&quot;(max-width: 887px) 100vw, 887px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6cb0ab39b5e026aaa87b5f292a941f72/713f0/cicd1.png&quot; alt=&quot;cicd1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;앞으로의-고도화-방향&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EA%B3%A0%EB%8F%84%ED%99%94-%EB%B0%A9%ED%96%A5&quot; aria-label=&quot;앞으로의 고도화 방향 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;앞으로의 고도화 방향&lt;/h3&gt;
&lt;p&gt;현재 구성에서 매장 서비스를 사용하는 클라이언트가 많아진다면 어떻게 할까요? 실제로 올리브영에서 &quot;&lt;strong&gt;매장&lt;/strong&gt;&quot;이라는 정보는 거의 모든 영역에서 필요로 합니다. 위의 초기 구성도에서 볼 수 있듯이 초반부터 5개의 클라이언트가 연계되어 있고, 앞으로 점차 늘어날 예정이므로 자연스럽게 매장 API로 들어오는 트래픽 또한 증가할 것입니다.&lt;/p&gt;
&lt;p&gt;따라서 많은 클라이언트를 효율적으로 관리하는 방법의 하나는, External API와 Internal API 환경을 분리하는 것입니다. 환경을 2개로 분리한다면, N성수 키오스크와의 통신을 위해 별도로 사용하던 NLB도 ALB로 통일시킬 수 있습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0ce12658c3446891744ece4bf9536c48/5b5fb/infra2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 65.20833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3ElEQVR42qVTyW7UQBT0//8OF5QDIKKIG0FAEFLIIAIee8bjAS+9L0W9HnsUKckpllq226/r1fJchfGA+W+HGBNyTnjpVUVnUNc1ZqXhtQZCQI4BceqQgwfYJKeIxL1cnk/rWUApiixOMSJ6AsgBZxH1gMyCnHMp3G4b9M1vNp0RvDvvr/czoEj+1zVw7sTmBMhnb5D2G74mzD9qaO1x+HaFuf0JHyK8c8hPSqasptlBaYNgDYEcYCzSeMT0/YqHA/ZvrhFMwHh/A9M3bJLJ0sNahz+1kLFQSlFppGQyGMepfAyrZN7FitUpYRlIp7+9xrS7L2oM/XZkqen9289b1N1YFFaC2uz2sCwK1pIhwcgKS+KrR23b4jhM8DIN9NwYQ1W6eH/xsUZ9mKUaVXIaknQJpaSay5JD8KqARWUxzAb9l3dQzR0cPbS0J5K5sL/8WqM9zuVc5ccOv+5uabo5gchIkGEaesyfXhcPt68+wE0GQ7OB5swG7olvwn6dX1Eq75XMmCb9wK5nyZIgwb0PpcgzJMc+x5v3UO2mpBxEzXI9nJxKBlaSksEukpdQkBYf2bAMN+tkEiSI/ADs0dgkHlAEC6TsyVTkYv0TFj+fXM8BSjISiPgi8l/6L/8Hutz4zyTASxUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0ce12658c3446891744ece4bf9536c48/263a4/infra2.webp 480w,
/static/0ce12658c3446891744ece4bf9536c48/a6361/infra2.webp 960w,
/static/0ce12658c3446891744ece4bf9536c48/0b34d/infra2.webp 1920w,
/static/0ce12658c3446891744ece4bf9536c48/61647/infra2.webp 2128w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0ce12658c3446891744ece4bf9536c48/9aebd/infra2.png 480w,
/static/0ce12658c3446891744ece4bf9536c48/a91f8/infra2.png 960w,
/static/0ce12658c3446891744ece4bf9536c48/ac7a9/infra2.png 1920w,
/static/0ce12658c3446891744ece4bf9536c48/5b5fb/infra2.png 2128w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0ce12658c3446891744ece4bf9536c48/ac7a9/infra2.png&quot; alt=&quot;infra2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;하지만 서비스 환경을 2개로 분리한다면 관리 포인트가 늘어나 유지보수성이 떨어질 수 있습니다. 이 때 도움을 줄 수 있는게 멀티모듈 아키텍처와 AWS ECS 구성입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;Part.2: 매장 도메인의 구현 여정&lt;/a&gt;에서 소개했듯이 현재 멀티모듈 아키텍처 구성은 &lt;strong&gt;api 모듈&lt;/strong&gt;, &lt;strong&gt;service 모듈&lt;/strong&gt;, &lt;strong&gt;domain 모듈&lt;/strong&gt;로 각각 분리되어 있습니다. 그리고 api 모듈에는 external 패키지와 internal 패키지로 나누어져 있습니다. 여기서 우선 api 모듈을 &lt;strong&gt;external-api 모듈&lt;/strong&gt;과 &lt;strong&gt;internal-api 모듈&lt;/strong&gt;로 분리합니다. 그리고 아래 예시처럼 각 환경에 필요한 모듈이 조립돼서 배포될 수 있도록 Build 스크립트와 TeamCity 설정을 진행합니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/244f9c8400af0937e6678d56f64a0d9b/e9762/infra3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 85.83333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAACSklEQVR42m1U7Y6DMAzb+z/hft7p2Ablq5QWKLTk7AK3gW5SVI06jpNYvXVdJ/f7XZxz0hkj0zxLiFGWECQglmUR/rIsk6/v74QzfZ8wV9y6rnKzADx+fsSB2JSlzE0jq9YithegJExTAtb4rp5PcbizwEWca9uKTKMIREQGCtxYJSIpzIvMwyBzVYlTlZSZhmIvAd8CiAmO44TcWTyKe5CaVyXFq5ehHyX6aSeE3Oi9jEisoMLhdEisWyOt7mUZAGZbMaTC1tqkdhhHsbgr606ssbLgLrJlAlc/J0KFqs4NKZQqUuKbMKbCnGGpShmA7zHLPM/FQvFGGA9CKnSSF0VKoMoS5CTkGD4JqbAoVCLkcpjT/xGu2wwT4YiWq3pTSLVK7QrPhM464KrUMhW+8k/CXSGBHHyywRISocYWNWx0UoikZV8QcT3Uti223pmzQhJ6fDD0Ic5pn9U4+RMhLTRCGVv1nDuWx+Ie54fCDdjSZ2izqRvpUPH5fEmJ1g5Cqpr32apSpQ44kge82cGPZ0JUJSHlN02bCAvMpixBCKWfhDWKtCAgIWeZY0EaIijqz9iHwjzfrGJMj00WUtX1vwppFa27RMwcA/LlSqhxSQKCNkJ1ss1bYS01cHwDiGXh/iC8tkxgg5OPBFXQ6NeW2WaNFtMMgadC3fzTMi+zfREaM8weTymwpBMhPEpFD+CaViffEteiwFshLUGrwFMTkv0R+M9zQYTdNjT5BMsQl8JuQWw4fMjKKYHGpmkv52HkZGY+Ufv3axzv4S9syyMAk2qxywAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/244f9c8400af0937e6678d56f64a0d9b/263a4/infra3.webp 480w,
/static/244f9c8400af0937e6678d56f64a0d9b/a6361/infra3.webp 960w,
/static/244f9c8400af0937e6678d56f64a0d9b/0b34d/infra3.webp 1920w,
/static/244f9c8400af0937e6678d56f64a0d9b/fa17d/infra3.webp 2262w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/244f9c8400af0937e6678d56f64a0d9b/9aebd/infra3.png 480w,
/static/244f9c8400af0937e6678d56f64a0d9b/a91f8/infra3.png 960w,
/static/244f9c8400af0937e6678d56f64a0d9b/ac7a9/infra3.png 1920w,
/static/244f9c8400af0937e6678d56f64a0d9b/e9762/infra3.png 2262w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/244f9c8400af0937e6678d56f64a0d9b/ac7a9/infra3.png&quot; alt=&quot;infra3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;그다음 작업으로 매장 서비스 &lt;strong&gt;ECS 클러스터 내부에 External API 서비스와 Internal API 서비스를 각각 생성&lt;/strong&gt;합니다. 그리고 TeamCity CI/CD 설정을 환경별로 구성하고 Build 스크립트를 통해 각 환경에 필요한 모듈을 조립하여 배포한다면, 클라이언트 성격에 따라 매장 API의 유입지점을 분리시킬 수 있어 관리하기 수월해집니다.&lt;br&gt;
개발자 입장에서는 소스코드를 하나의 Repository로 관리하며, Presentation 영역(api)만 분리하고 나머지 모듈은 재사용하면 되므로 유지보수성도 챙겨갈 수 있습니다.&lt;br&gt;
전반적인 인프라 구성은 아래와 같이 될 것입니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c466eec0ce97d19f9969e7f9a6c08840/1e185/infra4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB+ElEQVR42pVU2W7bMBDM//9ZHwukR5KibezKlmUdPESK93RJybLUBAhKYE0ZWs7uzCz1gGWllPDRuqUwpVH1DKdBYJBqd/6h/AmOfuL7IDGUoKf1kJACdX1F0/ZgBPoGUA8Nrs0FwXsgxrmVJSEqhmQVrDUUll4HeGfhKGLw8H7eU4x3wAxkKNkZAzqFlIE3HebFhYRoKsAbRCoWQii7dblAWBkWQCs6aMmpqVg6TM4hGQVfv8CTHPzpAGcDxOEL7HAFYx2q5gBMPX58P+F4UoskC+CkRnR9D0/dERfqgih4B9NVlBYhf55gjYc8foMhsI4kOrdHkoNjokbcvx2ayWBgvFBPS9ydnTWNMUGJgYqa1YSY2RT6fs67AfpFw9JhBltHKG0AY9EtA6SlwG2PxcgNoJM90Zbvjs7dFAHJ+Qq4jez8rsMM1vcDwqJfGZ2cNElKDHBcwvkIVT3D846cddBkGrmHqhpxvui9KYbGpR842e9m/fKuR+jfn+HI5fbTV5jRgJHLU9/iXL/i+fCIxGo0xwqM2zyxSCtlGlKl9Sw4uVtGp9QsnGda9DgSkNeiEAie5o/CUQMxa7sd7FmLuOqwUXDWMB+gCPQ83xRXbkv0y04sdoAffBLWuBVsGMPLnxq/Li2qS/f2Lv/vcjRealSY9ASjze7dXyDnmRBhU8ZcAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c466eec0ce97d19f9969e7f9a6c08840/263a4/infra4.webp 480w,
/static/c466eec0ce97d19f9969e7f9a6c08840/a6361/infra4.webp 960w,
/static/c466eec0ce97d19f9969e7f9a6c08840/0b34d/infra4.webp 1920w,
/static/c466eec0ce97d19f9969e7f9a6c08840/58bdf/infra4.webp 2340w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c466eec0ce97d19f9969e7f9a6c08840/9aebd/infra4.png 480w,
/static/c466eec0ce97d19f9969e7f9a6c08840/a91f8/infra4.png 960w,
/static/c466eec0ce97d19f9969e7f9a6c08840/ac7a9/infra4.png 1920w,
/static/c466eec0ce97d19f9969e7f9a6c08840/1e185/infra4.png 2340w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c466eec0ce97d19f9969e7f9a6c08840/ac7a9/infra4.png&quot; alt=&quot;infra4&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h2 id=&quot;모니터링-구성-feat-datadog&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EA%B5%AC%EC%84%B1-feat-datadog&quot; aria-label=&quot;모니터링 구성 feat datadog permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모니터링 구성 (feat. Datadog)&lt;/h2&gt;
&lt;p&gt;마지막은 서비스 운영에서 가장 중요한 모니터링입니다. 저희 올리브영은 기본적으로 Datadog을 사용해서 모니터링을 진행하고 있습니다. 최근 Datadog 본사에서 주최하는 DASH 2024 컨퍼런스에서 저희 올리브영의 Datadog 사용 사례를 소개했듯이, 올리브영은 Datadog을 굉장히 유용하게 사용하고 있습니다. DASH 2024 후기가 궁금하다면 아래의 글을 참고해 보시길 추천드립니다 😁&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;후기 글 : &lt;a href=&quot;https://oliveyoung.tech/2024-07-31/oliveyoung-datadog-dash2024/&quot;&gt;뉴욕 DASH2024에서 전파한 올리브영의 데이터독 활용 사례&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;발표 영상 : &lt;a href=&quot;https://youtu.be/4vGH5DJ4GSo?feature=shared&quot;&gt;CJ Olive Young’s Journey to Bridging the Gap Between Business Operations and Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;올리브영이 다양한 메트릭을 어떻게 비즈니스에 잘 녹여내고 있는지는 해당 발표 영상에서 상세하게 설명하고 있습니다.  따라서 이번 글에서는 매장 서비스의 모니터링 대시보드와 에러 알람을 어떻게 구축했는지 간단하게 소개해 보겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;대시보드-구축하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;대시보드 구축하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;대시보드 구축하기&lt;/h3&gt;
&lt;p&gt;먼저 대시보드입니다. 대고객 서비스 오픈을 앞두고 어떤 지표들이 필요할까요?&lt;br&gt;
우선 서비스가 잘 살아있는지를 확인해야합니다. 서비스 인스턴스가 몇개가 구동되고 있는지, CPU와 메모리는 얼마나 사용하고 있는지, JVM Heap 메모리와 스레드 개수는 충분히 여유로운지 등을 확인할 수 있어야 합니다.&lt;br&gt;
또한, API 요청이 잘 이루어지고 있는지 확인해야 합니다. 현재 TPS가 얼마나 되는지, 총 요청 수는 어느정도인지, &lt;a href=&quot;https://en.wikipedia.org/wiki/Latency_(engineering)&quot;&gt;Latency&lt;/a&gt; 지연이 발생하고 있지는 않은지, 에러가 발생하고 있는지 등을 확인할 수 있어야 합니다.&lt;/p&gt;
&lt;p&gt;확인해야 할 요소들을 모두 대시보드에 넣는다면, 아래와 같이 구성됩니다.&lt;/p&gt;
&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b9136742e49ea93b9d373967e72e950c/f25fe/monitoring1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.91666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAACG0lEQVR42k1Ty44TQQycT+cTEP/AkcMKTpwQQqy0BwRC7AEQq2Q3yWTymky/n0W5J5HSiuW0LVeXy55OaYMcCpZmg/f9J6ynAcFFaGPgnIMxFt57lFJwe2JKSLTbU2pFl5KXLB6Pf/F2cYfn4wv0+YzVpsfheMJ2t4N1HkY76MlATRbWRD4WW0wxNo4aWnnGA7rlCHxbAztVcZqAnz3wa6ioZFT5ohwBPDKptbAOZG8xKo3TcYLnXfIhJHoCfn3y+Pwv4nEb8fAccL+gXya2GqCUhSKLA8H67YBhv2frFYPpcbde4c2X7+yz/drjIRCwOItqNQp1qsEj0YSd6NF0ox8nhdW6bzKkmLGxa7z7/QevPt4zP2sngDkXdE5baqbZPzWwDjnllryanNNZ4YWA/cCBUe8tGX5YLvD6/gf1zxxB5oAyATM6T5oSiDG0gKMeMUayk5hMsmCidqfxjHxhLnEf4mw+wImxbs+BdYXFWYzJ0gD4WpbBU5NY+L+2Qlmj66CuzOdTmwmBkZp3iq1mFmnL1ZD1oB8pgbQmjIW5TNVQjnLRS3xjS5DrXSyQWCf05UixDEFR05GaCVMBNXZuqeTImONQCJx5L4lTneVpciWRjIDyQi6zJVpIpXUhrK0rvIOAEfvDNC+wjvM60YzJHGRqi6642Ep7dNL3xLavXoqFlbGxfVrSljYOT8sttUyXTwxtH2+1lA2Rz/g/sQ6hq36LcuEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b9136742e49ea93b9d373967e72e950c/263a4/monitoring1.webp 480w,
/static/b9136742e49ea93b9d373967e72e950c/a6361/monitoring1.webp 960w,
/static/b9136742e49ea93b9d373967e72e950c/0b34d/monitoring1.webp 1920w,
/static/b9136742e49ea93b9d373967e72e950c/da28f/monitoring1.webp 2880w,
/static/b9136742e49ea93b9d373967e72e950c/0740a/monitoring1.webp 3120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b9136742e49ea93b9d373967e72e950c/9aebd/monitoring1.png 480w,
/static/b9136742e49ea93b9d373967e72e950c/a91f8/monitoring1.png 960w,
/static/b9136742e49ea93b9d373967e72e950c/ac7a9/monitoring1.png 1920w,
/static/b9136742e49ea93b9d373967e72e950c/f9c26/monitoring1.png 2880w,
/static/b9136742e49ea93b9d373967e72e950c/f25fe/monitoring1.png 3120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b9136742e49ea93b9d373967e72e950c/ac7a9/monitoring1.png&quot; alt=&quot;monitoring1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;에러-알람-구축하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%90%EB%9F%AC-%EC%95%8C%EB%9E%8C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;에러 알람 구축하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;에러 알람 구축하기&lt;/h3&gt;
&lt;p&gt;대시보드 구축을 완료하고, 저는 또 한 번의 고민에 빠졌습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;어떤 종류의 알람을 어떤 방식으로 구성해야 서비스 장애를 빠르게 인지할 수 있을까?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;p&gt;가장 중요하게 생각했던 요소는 두 가지였습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;초기 버전은 조회성 API만 제공하므로, HTTP 응답 코드와 응답 속도 위주로 알람을 구성하자&lt;/li&gt;
&lt;li&gt;어떤 문제가 무슨 API에서 일어나고 있는지 명확하게 볼 수 있도록 가독성을 챙기자&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;HTTP 응답 코드(Status Code)에서의 문제는 크게 &lt;strong&gt;4xx 에러&lt;/strong&gt;와 &lt;strong&gt;5xx 에러&lt;/strong&gt;로 나눌 수 있습니다. 5xx 에러는 서버와 관련있어 발생 즉시 관리자가 인지하는 것이 바람직합니다. 반면 4xx 에러는 클라이언트와 관련있어서 의도적인 공격처럼 짧은 시간에 비정상적으로 많이 발생하는 경우에만 인지해도 된다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;그리고 대고객 서비스에서는 API 요청에 대한 응답 속도가 지연되면 사용자에게 불편함을 줄 수 있으므로, 특정 기준 시간 이상으로 응답 속도가 느려질 경우 저희가 인지할 수 있어야 합니다.&lt;br&gt;
따라서 아래 항목들을 초기 알람으로 설정하였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/03b5d3c4a0f41aee797c73c448fab9e1/1da28/monitoring2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByUlEQVR42m2S23LaMBRF/QedznRK8A3bmEC4ODaWLcmyTWig0MtMJyHTt6bth5Y+9MN2j0TCQJuHbUljaensrWN9e/yJ/f437nc7bLZb0ifcrjb4crfDevMBy3dr7B6+gpUSw6upkdeLjvKD/nHueAGsx+8/8Gv/B3efOe5XHVRiCsYbVE2LUlQGxGWNgldmVM0N0nlBKjFnJe1RyAuBJM0N3Op0Xbx6/Qaj8QTZPMN1lpOY2ZzlJRJaz67nT5DC/OsPRqYa1w/PZCrUE9vtYTJL0S5XaBe3kKo5VrZ6v4VQtZmvNx8RxUNc2J45/JIs/ekScEC3RowhkAIFwbQV1SyMbW0pLwVkvTDWp0mGslKwJYcdxXDo/BlQVxhHl/BYjm4tUZBVA1AtWecomEBKmdqVgH/yGE48gPNk9X9g/xI+ZdjlBQqpDiCqpm6XJruKLMfLG7xNEjiOfwCcVHYGNFCS74XwZwkuqMqcgBnpAJZIhKQKOTpk0aYMdUy2Bv8DtU4Xbhijl2XwFWXID9mZ1iFg2DYIribkZGgeph+PEI7GsLXtFzOkTYGGkeU0ZaZldPjPo24XPTK6ZEY9l9F6zAU6eQr3OQIvwF/G1TI97RyVPQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/03b5d3c4a0f41aee797c73c448fab9e1/263a4/monitoring2.webp 480w,
/static/03b5d3c4a0f41aee797c73c448fab9e1/a6361/monitoring2.webp 960w,
/static/03b5d3c4a0f41aee797c73c448fab9e1/0b34d/monitoring2.webp 1920w,
/static/03b5d3c4a0f41aee797c73c448fab9e1/db8c4/monitoring2.webp 2078w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/03b5d3c4a0f41aee797c73c448fab9e1/9aebd/monitoring2.png 480w,
/static/03b5d3c4a0f41aee797c73c448fab9e1/a91f8/monitoring2.png 960w,
/static/03b5d3c4a0f41aee797c73c448fab9e1/ac7a9/monitoring2.png 1920w,
/static/03b5d3c4a0f41aee797c73c448fab9e1/1da28/monitoring2.png 2078w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/03b5d3c4a0f41aee797c73c448fab9e1/ac7a9/monitoring2.png&quot; alt=&quot;monitoring2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;그리고 이런 알람이 Slack을 통해 발생하더라도, 에러 종류와 원인을 명확하게 표시하지 않는다면 빠르게 대응할 수 없을 것입니다.
따라서 저희는 어떤 API에서 어떤 알람이 발생했는지, 아래와 같이 알람 포맷을 설정하여 가독성을 높였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ca957b3eadf54bd80f3c9fefb02804b9/425d1/monitoring3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAACU0lEQVR42jVRy2oUQRTtlRtBJZPMo7unn1X9fs30zNCGOLqIxIULFwoR3LgxC0XwE8RVxI2E6BhHSMSVBgVBjbrxEwQTJQjqMr8QmeR4q9SGw71NnXvqnFvKxfOL8IsBTD+F5oZoswiqE8he5TFUP0eD/i9fuoJzCxdgxQVxQhjEM6hKrpgRXJ5AyU0b89UsvCRHkXdRpTmY5aJl2NAMB5rpQLcY5uaGmA1CxGmBLIjg+xGYw4knOK6E4Cmx7aLqDcCCGCzKpVPdFQ4D6I6PlsVh8xBDEhzQhXFRQg87aEalhEowk55MYpJLxSMHftaDEXXRDAhhiQbPUSeYUUdGapO4ECxJzOtW4HkfLYqneSlaLIbOxZp8qORW8QwL3SyUKPMI/SJBEXlII47QY3AsC21ay3DuNBIvRF720emUCCi263IYZMiyGWyKL6riE7ksMhRZiixNEXgesiRCEofgjEnYjGN46gxi7sOgQcPmmG7qaGoGLCFKQibBoN0rvG1BpweoEeFYrYEjR4/LWlcNOWg5Hgn66JcD3Li2hE/vP2Jr8yXePN/Eh1ev8WLjGWwhSmJSsEevu/74CZ6O1rD+YITx/RVsrI5w6/pNNNQ2NLrQJFedvIPl23cgvoPJAQ4PD2W/t7cn3TX/cZXhoMLv/X15KEiTyUT2b99tYbreku5FpEG/wuLVJYzJ0crqQ8IIo7Uxlu/eoz26UHXzr+DJbg9ftnews/sd29928Xn7K37++IXxozXMNDQpKIjzZxcQl5WM7zIPnh9iqlbHiakZef4ffwCll2COQX1FWQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ca957b3eadf54bd80f3c9fefb02804b9/263a4/monitoring3.webp 480w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/a6361/monitoring3.webp 960w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/0b34d/monitoring3.webp 1920w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/da28f/monitoring3.webp 2880w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/1a3a5/monitoring3.webp 2942w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ca957b3eadf54bd80f3c9fefb02804b9/9aebd/monitoring3.png 480w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/a91f8/monitoring3.png 960w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/ac7a9/monitoring3.png 1920w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/f9c26/monitoring3.png 2880w,
/static/ca957b3eadf54bd80f3c9fefb02804b9/425d1/monitoring3.png 2942w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ca957b3eadf54bd80f3c9fefb02804b9/ac7a9/monitoring3.png&quot; alt=&quot;monitoring3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이처럼 알람 포맷에 어떤 환경에서 어떤 에러 알람이 발생했고, 현재 수치가 어느 정도이며 해당 이슈에 빠르게 대응하기 위한 관련 Dashboard, Trace, Logs 링크를 포함해 놓음으로써 보다 빠르게 문제를 인지하고 해결할 수 있도록 구축하였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;올해 1월부터 포스팅한 &lt;strong&gt;10년 된 레거시를 현대화하다&lt;/strong&gt; 시리즈는 그레이 영역인 매장 도메인이 중심이었습니다. 아무것도 준비되지 않았던 2024년부터 레거시를 개선하기 위해 고군분투했고, 0에서부터 차근차근 1을 만들어가는 과정을 담아냈습니다. 1로 만든 매장 서비스는 현재 많은 스쿼드에서 사용하며 100을 향해 달려 나가고 있습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;시리즈를 돌아보면 이벤트 스토밍을 통한 도메인 식별을 시작으로, 멀티모듈 스켈레톤 프로젝트 구축과 인프라 아키텍처 설계 및 모니터링 구축까지 소개해 보았는데요. 이외에도 &lt;strong&gt;성능 테스트 전략&lt;/strong&gt;, PDCA 기반의 성능 테스트 &lt;strong&gt;결과 템플릿&lt;/strong&gt;, &lt;strong&gt;Github Actions&lt;/strong&gt;을 활용한 편의성 증대, SonarQube 설정을 통한 &lt;strong&gt;테스트 커버리지 측정 자동화&lt;/strong&gt; 등 아직 소개하지 못한 경험도 많습니다.&lt;/p&gt;
&lt;p&gt;하지만 아쉽게도 저의 이번 시리즈는 여기서 마무리해야 될 것 같습니다.&lt;br&gt;
저는 이제 올리브영에서 매장 도메인 대신 &quot;&lt;strong&gt;정산 도메인&lt;/strong&gt;&quot;을 담당하게 되었는데요. 😁&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &lt;strong&gt;CJ올리브영의 정산 영역&lt;/strong&gt;을 어떻게 차근차근 개선해 나가는지 또 하나의 시리즈로 찾아뵙겠습니다.&lt;/p&gt;
&lt;p&gt;이번 시리즈에 많은 관심을 가져주셔서 매우 감사드리며, 앞으로의 경험도 많은 기대와 응원 부탁드립니다! 🚀&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h3 id=&quot;알렉스-로그&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EB%A0%89%EC%8A%A4-%EB%A1%9C%EA%B7%B8&quot; aria-label=&quot;알렉스 로그 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;알렉스 로그&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;입사
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-11-30/journey-to-joining-oliveyoung&quot;&gt;주니어 개발자의 우당탕탕 입사기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매장 도메인
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3/&quot;&gt;10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정산 도메인
&lt;ul&gt;
&lt;li&gt;To be continued...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;</content:encoded></item><item><title><![CDATA[Web Worker로 이미지 처리 최적화하기]]></title><description><![CDATA[안녕하세요! 올리브영에서 리뷰/SNS 프론트엔드 개발을 담당하고 있는 d9입니다. 저희 조직에서는 올리브영의 SNS…]]></description><link>https://oliveyoung.tech/2025-04-25/web-worker-for-image-processing/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-04-25/web-worker-for-image-processing/</guid><pubDate>Fri, 25 Apr 2025 19:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영에서 리뷰/SNS 프론트엔드 개발을 담당하고 있는 &lt;strong&gt;d9&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;저희 조직에서는 올리브영의 SNS, 셔터 개발과 운영을 맡고 있습니다. 셔터는 올리브영 앱에서 유저가 콘텐츠를 업로드하고 자유롭게 소통할 수 있는 커뮤니티입니다. 그렇다 보니 &lt;strong&gt;이미지 업로드 기능&lt;/strong&gt;이 사용 경험에 직접적인 영향을 주는 핵심 요소로 자리잡고 있습니다.&lt;/p&gt;
&lt;p&gt;그런데 수많은 고화질 이미지를 다루다 보니, 모바일 환경에서 아래와 같은 문제가 종종 발생했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;고해상도 이미지 처리에 &lt;strong&gt;메모리 과다 사용&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;처리 과정에 &lt;strong&gt;UI 반응 저하&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;위 문제가 지속될 경우 &lt;strong&gt;브라우저 강제 새로고침&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div&gt;
    &lt;img src=&quot;/82c295030bb0b0406180e3c7b47230a0/issue-review.gif&quot; alt=&quot;고해상도 이미지 처리 중 브라우저 강제 새로고침 발생 현상&quot; style=&quot;width: 400px;&quot;&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;
 [고해상도 이미지를 렌더링하는 도중 강제 새로고침 되는 예시]
&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;문제-분석&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EB%B6%84%EC%84%9D&quot; aria-label=&quot;문제 분석 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 분석&lt;/h2&gt;
&lt;p&gt;그렇다면 왜 이러한 현상이 발생하는 걸까요? 해결 방안을 찾기 위해 먼저 문제를 분석해보았습니다.&lt;/p&gt;
&lt;h3 id=&quot;모바일-이미지의-특성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%9D%98-%ED%8A%B9%EC%84%B1&quot; aria-label=&quot;모바일 이미지의 특성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모바일 이미지의 특성&lt;/h3&gt;
&lt;p&gt;요즘은 스마트폰 하나로도 &lt;strong&gt;고해상도 이미지&lt;/strong&gt;를 생성할 수 있습니다. 기술의 발전에 따라 기기의 기본 성능이 좋아졌고, 속도와 퀄리티에 반응하며 더 좋은 결과물을 추구하는 소비자 입맛에 잘 맞습니다. 예를 들어 아이폰은 HEIC이라는 고급 압축 포맷을 사용하지만, 업로드 시 호환성 문제로 JPEG로 변환됩니다. 이 과정에 &lt;strong&gt;파일 용량이 커집니다&lt;/strong&gt;.&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 400px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1179px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3d4efd956b6874fc6aebc17b820d9eaf/d2252/example1.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 82.70833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAARABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAQCA//EABYBAQEBAAAAAAAAAAAAAAAAAAIAAf/aAAwDAQACEAMQAAAB6zz4s6pgcehEK//EAB0QAAICAQUAAAAAAAAAAAAAAAACAQNBERITIjH/2gAIAQEAAQUCbjQa1Nd6SS02M3nQzg//xAAWEQEBAQAAAAAAAAAAAAAAAAASABD/2gAIAQMBAT8BMd//xAAXEQADAQAAAAAAAAAAAAAAAAAAEBES/9oACAECAQE/AdFf/8QAHBAAAgICAwAAAAAAAAAAAAAAAAECIQMRICKB/9oACAEBAAY/ArgViR2ivBts2WuH/8QAHRAAAwACAgMAAAAAAAAAAAAAAAERIUEQMVGhwf/aAAgBAQABPyG+Kb76Qr7WFTdWsYLJZMfBZSql9+GhpH//2gAMAwEAAgADAAAAED/ow//EABgRAQADAQAAAAAAAAAAAAAAAAEAEBFB/9oACAEDAQE/EAhjMcb/AP/EABgRAAIDAAAAAAAAAAAAAAAAAAERABBR/9oACAECAQE/ECbYj5f/xAAfEAEAAgICAgMAAAAAAAAAAAABABEhMUGhYcFxgfD/2gAIAQEAAT8QBa40qqZbis4s/QwCWnAIHHEuuqBvHgIKFn9G5cKFMIUTV9MdPmPf6j0e5//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3d4efd956b6874fc6aebc17b820d9eaf/263a4/example1.webp 480w,
/static/3d4efd956b6874fc6aebc17b820d9eaf/a6361/example1.webp 960w,
/static/3d4efd956b6874fc6aebc17b820d9eaf/c47e4/example1.webp 1179w&quot; sizes=&quot;(max-width: 1179px) 100vw, 1179px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3d4efd956b6874fc6aebc17b820d9eaf/a3e66/example1.jpg 480w,
/static/3d4efd956b6874fc6aebc17b820d9eaf/fb816/example1.jpg 960w,
/static/3d4efd956b6874fc6aebc17b820d9eaf/d2252/example1.jpg 1179w&quot; sizes=&quot;(max-width: 1179px) 100vw, 1179px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3d4efd956b6874fc6aebc17b820d9eaf/d2252/example1.jpg&quot; alt=&quot;아이폰에서 촬영된 4MB 이상의 고해상도 HEIC 이미지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;
 [모바일에서 업로드된 대용량 이미지 예시]
&lt;/p&gt;
&lt;p&gt;대용량 이미지는 일반적으로 이런 특성이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;크기 4MB 이상&lt;/li&gt;
&lt;li&gt;해상도 4000px 이상&lt;/li&gt;
&lt;li&gt;HEIC &gt; JPEG 변환 시 품질 저하 없이 용량 증가&lt;/li&gt;
&lt;li&gt;메타데이터(EXIF) 포함으로 추가 용량 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이로 인해 몇 가지 문제가 발생하는데, Web Worker를 도입하는 과정에 고려한 문제는 두 가지입니다.&lt;/p&gt;
&lt;h3 id=&quot;문제-1-브라우저의-이미지-처리-부하&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-1-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-%EB%B6%80%ED%95%98&quot; aria-label=&quot;문제 1 브라우저의 이미지 처리 부하 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 1: 브라우저의 이미지 처리 부하&lt;/h3&gt;
&lt;p&gt;셔터에서는 사용자가 게시물을 작성할 때, 앱 내에서 이미지를 선택하면 업로드 전에 해당 이미지를 미리 볼 수 있도록 이미지 컴포넌트를 렌더링합니다. 이 과정에서 브라우저는 다음과 같은 절차를 따르게 됩니다.&lt;/p&gt;
&lt;div style=&quot;display:flex; justify-content:center; align-items:center; gap:40px;&quot;&gt;
  &lt;div style=&quot;flex:1; max-width:200px;&quot;&gt;
    &lt;img src=&quot;img/example2.png&quot; alt=&quot;셔터의 이미지 선택&quot; style=&quot;width:100%; height:auto;&quot; /&gt;
  &lt;/div&gt;
  &lt;div style=&quot;flex:1; max-width:400px;&quot;&gt;
    &lt;img src=&quot;/17fe5df7b123b5f4dd461377e8c407cd/example3.avif&quot; alt=&quot;브라우저의 이미지 처리 단계 다이어그램&quot; style=&quot;width:100%; height:auto;&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;
 [셔터의 게시물 업로드 화면(좌)과 동작을 표현한 상태 다이어그램(우)]
&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;브라우저에서 진행하는 이미지 처리 과정과 이 때 발생하는 문제를 조금 더 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;먼저, 이미지를 &lt;strong&gt;디코딩 후 메모리에 로드&lt;/strong&gt;합니다. 이미지를 JPEG로 디코딩한 후 압축 해제합니다. 이후 메모리를 할당해 픽셀 데이터로 저장합니다.&lt;/p&gt;
&lt;p&gt;다음은 메인 스레드에서 &lt;strong&gt;레이아웃/페인트 작업&lt;/strong&gt;을 준비합니다. 이미지 크기와 DOM 레이아웃을 계산한 후, 렌더 트리를 구성하면 페인트 명령을 생성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;축소된 이미지라도 &lt;strong&gt;원본 리소스를 기반&lt;/strong&gt;으로 처리되기 때문에 원본 이미지의 모든 픽셀 데이터를 유지합니다. 이는 표시 이미지의 크기와 무관하게 메모리를 사용하는 과정이며, 따라서 리샘플링과 스케일링 연산이 필요합니다.&lt;/p&gt;
&lt;p&gt;문제는 이미지가 많아질수록 가중됩니다. 레이아웃을 계산하는 시간이 늘어남은 물론, 페인트 과부하가 발생합니다. 그러면 메인 스레드가 막히면서 결국 전체 페이지 성능에 영향을 미치게 됩니다. iOS 환경은 특히 메모리를 엄격하게 제한하고 있어서, UI 반응 속도가 저하되면 브라우저를 다시 로드하는 경우가 있습니다. 포스트 초반에 언급한 강제 새로고침이 바로 그 예입니다.&lt;/p&gt;
&lt;h3 id=&quot;문제-2-업로드-성능과-ux&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-2-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%84%B1%EB%8A%A5%EA%B3%BC-ux&quot; aria-label=&quot;문제 2 업로드 성능과 ux permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 2: 업로드 성능과 UX&lt;/h3&gt;
&lt;p&gt;셔터 플랫폼에서는 최대 10장의 이미지를 업로드할 수 있는데, 이 때 최대 &lt;strong&gt;40MB&lt;/strong&gt;의 데이터를 전송해야 합니다.&lt;/p&gt;
&lt;p&gt;네트워크 환경별 업로드 시간 분석해보면, 일반적인 LTE 환경과 불안정한 네트워크의 업로드 속도와 예상 소요 시간은 약 2배에 달하는 차이를 보입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[일반적인 4G LTE 환경]
- 평균 업로드 속도: 11.34Mbps
- 40MB = 320 Megabits
- 예상 소요 시간: 320 ÷ 11.34Mbps ≈ 28.2초

[불안정한 네트워크 환경]
- 평균 업로드 속도: 5Mbps
- 예상 소요 시간: 320 ÷ 5 = 64초&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;업로드 시간이 길어지면 또다른 문제를 야기합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;사용자 불만족&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;업로드 시간이 오래 걸려도 즉각적인 피드백이 없다 보니, 업로드 진행 상태를 명확히 확인할 수 없습니다. 업로드에 실패할 경우 재시도해야 하는 불편함도 있어 사용자의 불만이 쌓이게 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;리소스 낭비&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;불필요한 네트워크 대역폭을 사용하며 서버 저장공간 비효율적으로 사용하게 됩니다. 이로 인해 CDN 비용이 증가하는 리소스 낭비를 피할 수 없습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;에러 발생 가능성&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;네트워크 타임아웃이나 메모리 부족 에러가 발생할 수 있습니다. 업로드 중단 또한 피할 수 없는 이슈 중 하나입니다.&lt;/p&gt;
&lt;p&gt;이처럼 모바일 네트워크 환경에서 업로드 시간이 길어지면 사용자는 곧바로 &lt;strong&gt;리뷰에 불만을 표시&lt;/strong&gt;하는 경향이 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;문제-해결-시도&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%8B%9C%EB%8F%84&quot; aria-label=&quot;문제 해결 시도 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 해결 시도&lt;/h2&gt;
&lt;p&gt;이러한 문제를 해결하기 위해 시도한 방법을 소개합니다.&lt;/p&gt;
&lt;h3 id=&quot;1-포맷-변환&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%ED%8F%AC%EB%A7%B7-%EB%B3%80%ED%99%98&quot; aria-label=&quot;1 포맷 변환 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 포맷 변환&lt;/h3&gt;
&lt;p&gt;이미지를 WebP 등으로 변환해보았지만, 복잡한 이미지일수록 압축 효율이 낮았습니다. 이는 손실 압축 방식의 특성상, 복잡한 이미지에서는 줄일 수 있는 정보가 적기 때문입니다. 효율적인 용량 감소를 위해 품질을 낮추면 시각적인 &lt;strong&gt;퀄리티 저하&lt;/strong&gt;가 발생했고, 그로 인해 실제 적용에는 제약이 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;2-이미지-리사이징-canvas&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-canvas&quot; aria-label=&quot;2 이미지 리사이징 canvas permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 이미지 리사이징 (Canvas)&lt;/h3&gt;
&lt;p&gt;Canvas를 활용해 이미지를 리사이징하여 크기를 줄이는 시도는 성공적이었으며, 파일 크기 감소로 업로드 속도는 크게 개선되었습니다. 그러나 새로운 문제가 발생했는데, 바로 Canvas 자체 처리 비용으로 &lt;strong&gt;메인 스레드를 점유&lt;/strong&gt;하는 것, 디코딩이나 인코딩 과정에 &lt;strong&gt;JS 블로킹&lt;/strong&gt;이 발생하는 것. 뿐만 아니라 디스크 IO까지 밀리는 바람에 &lt;strong&gt;클릭 이벤트를 무시&lt;/strong&gt;하는 현상도 발생했습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 800px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 921px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/813763ba82cd9ad68890319d616ad9f0/24c26/example4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 9.166666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAfElEQVR42iWOyxKCMAxF/RuaYOVRaFphZAG4ce3/f8ox6uLMfWTmTi6lVEIIf1SQVglNQNznaeIcBt5Dz2vseT4q51rZi1FS4j5PrJZZ8sxihnl3MTeqioiPfdUHx5iZu0q6GY1eaTWSxsS6bezHwel0MSL+hIr6XRz95Q/kID9GiPTgagAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/813763ba82cd9ad68890319d616ad9f0/263a4/example4.webp 480w,
/static/813763ba82cd9ad68890319d616ad9f0/9d7ec/example4.webp 921w&quot; sizes=&quot;(max-width: 921px) 100vw, 921px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/813763ba82cd9ad68890319d616ad9f0/9aebd/example4.png 480w,
/static/813763ba82cd9ad68890319d616ad9f0/24c26/example4.png 921w&quot; sizes=&quot;(max-width: 921px) 100vw, 921px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/813763ba82cd9ad68890319d616ad9f0/24c26/example4.png&quot; alt=&quot;이미지 처리로 인한 메인 스레드 블로킹으로 클릭 이벤트 지연 현상&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;
 [메인 스레드가 잠기면서 클릭이 지연되는 예시]
&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 300px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/dda81256e6498ad5b042da9947b9c0a1/42a19/example5.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEoUlEQVR42hVU+VNTZxR9f4MLa9a3v5e4VC2I1JZanbbWalNbLBaMKLImIEtWErKQhLzkZSHw4GXPYwlb2BSwSqFaRyl1BKSK1S7T9sf+Ff06882d+8P9zpx755wDhTvgqktFKTM678bfCMohHVxXKdqO0Q8CZLgTXmKIW5Win4Yorgv2t8pPluWNWrHmKvGZigKvRgbtxBV3GSLUDq8GiGfD1MuUImlGwfTfWeVuggYoU05sN6EAKM5G2YQdy9qxGlVx0oS0VYshfa3kZVLxIk7f8xF/jSnfCoqtGD3nwVurJbNuPGZCfx85BBA3ODpuQu96ybgJfxyhUmYkY0EhrgsZ6EK247SlXj5uw0Hv1cqT3eiKn5h0ohMOZIlBkyZ51CB1NIim7ZIF3/HnsUNvMopNjoKeDNKbQ9SrlCJrg11Noqwd3RhWAKxXmSP/TB3ZHnk/5z+1FKmYHbjoM390L3ltNt64FCxZD2L6Wil0x0vsxKjnUSXXV2nTX7if7UwPNDE9KtZ9M2ErWVsIBFidwXhDSHlstiZ+2G7t7ZhhSh6GMU6PQlNO/GWCWvQfbdOc7zQ3MqxRSDs8rtpQoEHwnkwJHr9bzfRWCQljX/dZ1l2raTqXtSFvMspIJwyN9WB/jNLLoVKH/lzMUsZoj+pqDifapNEG4k4X7qpSCAZ8RIv41RK+RvyEp8G1A7fhp4PUBtj5+yDB6RBPs2jCIsqdPcCV7x8v3W84XeArO5grPZg9vm9WB49cLMrVSeOnCwJXREMama1JtszgqW4Uehgmd+O08ZZszUnESvOHy/IXTxfaPy4KlBcYj+cNlhek1bJsef7YZTH7QaH1siSuQ9nb8C9JxbyHgACBVZaIGNF7bXD4WF7k3fzAiTz208LlsvyFkgNsSZ7pTEHsSjHzzr6B68Uj9SLBJA+3S38IkzkXBmiT4XbY0yLdCKKp+uKeL/Ktnxf5b+KBrwqZq2ToFjzLKB7FTvCGEwu+Uy+yHy6G3rO1l+/EyLteAppx4Vs8xhpOJf2q6ejV7GBVmqt/MGOaiKrnJszjidY7Y90WnWoybbZab6Tjdi6kSQSvrQWwxT4CWgvTewl0c0w1NxVemPT7WVMydD3HVY1mfMszTD/b+nQ9mY18k02azWb1w5WBkKc66FAteeQZCwath6i9BLYxcmk40pEYaBn1fTKb0fd7vnWYL4PaZ1WxPeeDvZXjg2re/VlPR0WL9sspz7F5F9KnkUMzvfjzKPUzfyjnIjuvy70a8Wr/EeBHsNVvArHCoLtxbItHeZ04ZZYaawuDbdK3GRqYzNMigxb7cKAT4EfBirma5bwR5XSwqU7mbUX6u1B7I5y24MF2tKcBBjVjxdvVUnDjtRDJ6xHI1SzrrpNuDtH3A+TrtFJbLQHWe8pRHWrJdC++l1ZMO7HGK2JgUtD/KijH7ZhgRcdt6DD4/PWFopwLXw+RP0aouAkx3pACVKCc1SDxb+7wo34SSHCJwZ/x9LKPeNRP8QZgYXg7Sv2fJIxWPunAIx0wIAOyBmRFwoSC6JhzYyBGwIsakD9HlY8j5Hd+wtkkW2HwygtFPq38dYr+D7aVEl6jHoDPAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dda81256e6498ad5b042da9947b9c0a1/263a4/example5.webp 480w,
/static/dda81256e6498ad5b042da9947b9c0a1/a6361/example5.webp 960w,
/static/dda81256e6498ad5b042da9947b9c0a1/d71bc/example5.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dda81256e6498ad5b042da9947b9c0a1/9aebd/example5.png 480w,
/static/dda81256e6498ad5b042da9947b9c0a1/a91f8/example5.png 960w,
/static/dda81256e6498ad5b042da9947b9c0a1/42a19/example5.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dda81256e6498ad5b042da9947b9c0a1/42a19/example5.png&quot; alt=&quot;Web Worker 등장&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;왜-web-worker가-필요했나&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-web-worker%EA%B0%80-%ED%95%84%EC%9A%94%ED%96%88%EB%82%98&quot; aria-label=&quot;왜 web worker가 필요했나 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 Web Worker가 필요했나?&lt;/h2&gt;
&lt;p&gt;앞서 포맷 변환, 리사이징, 압축 등의 방식으로 자체 해결을 시도했지만, 대부분의 메인 스레드에서 처리되면서 오히려 새로운 병목을 초래했습니다. 결국, &lt;strong&gt;구조적 개선&lt;/strong&gt;이 필요하다는 결론에 도달하게 되었죠.&lt;/p&gt;
&lt;h3 id=&quot;web-worker가-적합한-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#web-worker%EA%B0%80-%EC%A0%81%ED%95%A9%ED%95%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;web worker가 적합한 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Web Worker가 적합한 이유&lt;/h3&gt;
&lt;p&gt;이미지 업로드 전 처리 작업은 아래와 같은 특성을 갖고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU 연산&lt;/strong&gt;이 집중적으로 필요한 작업 (디코딩, 리사이징, 포맷 변환 등)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOM 접근이 불필요&lt;/strong&gt;한 비동기 연산&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 스레드와 병렬&lt;/strong&gt;로 동작하면 이상적인 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 조건에 완벽히 부합하는 것이 바로 &lt;strong&gt;Web Worker&lt;/strong&gt;입니다. Web Worker를 사용하면 여러가지 개선 효과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UI 반응성 유지&lt;/strong&gt;하면서 이미지 처리 가능&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;메모리 사용 분산&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모바일에서도 안정적인 UX 확보&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;web-worker란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#web-worker%EB%9E%80&quot; aria-label=&quot;web worker란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Web Worker란?&lt;/h2&gt;
&lt;p&gt;그렇다면 Web Worker는 무엇일까요? Web Worker는 JavaScript에서 사용할 수 있는 &lt;strong&gt;백그라운드 스레드 실행 환경&lt;/strong&gt;입니다. 브라우저의 &lt;strong&gt;메인 스레드와 완전히 분리&lt;/strong&gt;되어 비동기 작업을 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;주요-특징&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9A%94-%ED%8A%B9%EC%A7%95&quot; aria-label=&quot;주요 특징 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;주요 특징&lt;/h3&gt;
&lt;p&gt;Web Worker의 가장 큰 특징은 메인 스레드와 &lt;strong&gt;완전 분리&lt;/strong&gt;되어 백그라운드에서 실행된다는 점입니다. 이로 인해 CPU 집약적인 작업을 별도로 처리할 수 있지만, 제약 사항도 있습니다. Worker는 DOM에 접근할 수 없으며, 일부 window 객체의 메소드 사용이 제한됩니다. 메인 스레드와는 &lt;code class=&quot;language-text&quot;&gt;postMessage&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;onmessage&lt;/code&gt; 방식의 메시지 기반 통신으로만 데이터를 주고받을 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;web-worker-종류&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#web-worker-%EC%A2%85%EB%A5%98&quot; aria-label=&quot;web worker 종류 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Web Worker 종류&lt;/h2&gt;
&lt;div class=&quot;worker-types&quot; style=&quot;overflow-x: auto;&quot;&gt;
  &lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 20px 0;&quot;&gt;
    &lt;thead&gt;
      &lt;tr style=&quot;background: #f6f8fa;&quot;&gt;
        &lt;th style=&quot;padding: 12px 16px; border: 1px solid #ddd; text-align: left;&quot;&gt;종류&lt;/th&gt;
        &lt;th style=&quot;padding: 12px 16px; border: 1px solid #ddd; text-align: left;&quot;&gt;주요 특징&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px 16px; border: 1px solid #ddd;&quot;&gt;&lt;strong&gt;Dedicated Worker&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: 12px 16px; border: 1px solid #ddd;&quot;&gt;
          단일 페이지에서 동작하는 독립적인 Worker. 1:1 통신으로 데이터 처리에 최적화
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px 16px; border: 1px solid #ddd;&quot;&gt;&lt;strong&gt;Shared Worker&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: 12px 16px; border: 1px solid #ddd;&quot;&gt;
          여러 탭/창에서 공유되는 Worker. 상태 공유나 실시간 데이터 처리에 활용
        &lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
        &lt;td style=&quot;padding: 12px 16px; border: 1px solid #ddd;&quot;&gt;&lt;strong&gt;Service Worker&lt;/strong&gt;&lt;/td&gt;
        &lt;td style=&quot;padding: 12px 16px; border: 1px solid #ddd;&quot;&gt;
          네트워크 프록시 역할을 하는 Worker. 오프라인 지원, 캐싱, 푸시 알림 등에 사용
        &lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;h3 id=&quot;셔터에-적합한-worker는&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%85%94%ED%84%B0%EC%97%90-%EC%A0%81%ED%95%A9%ED%95%9C-worker%EB%8A%94&quot; aria-label=&quot;셔터에 적합한 worker는 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;셔터에 적합한 Worker는?&lt;/h3&gt;
&lt;p&gt;여러 Worker 종류 중에서 이미지 처리에는 &lt;strong&gt;Dedicated Worker&lt;/strong&gt;가 가장 적합했습니다. 1:1 통신 구조로 단순하고, 독립적인 처리가 가능하며, 리소스 관리도 용이하기 때문입니다. 반면 Shared Worker나 Service Worker는 이미지 처리에 불필요한 기능들이 많아 배제했습니다.&lt;/p&gt;
&lt;h2 id=&quot;실제-적용-사례&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%A0%81%EC%9A%A9-%EC%82%AC%EB%A1%80&quot; aria-label=&quot;실제 적용 사례 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실제 적용 사례&lt;/h2&gt;
&lt;div style=&quot;display:flex; justify-content:center; align-items:center; gap:40px;&quot;&gt;
  &lt;div style=&quot;flex:1;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1542px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/cca9396267ae61ccdc7c24376fd7421f/46352/example6.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.79166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACfUlEQVR42oWT60vTYRTHf39Jr3pVUfQ+CDNMsWVNazrz0sW8VJg13UJnomBaSaBmWWghWurCS877Zaa44dRkUb4wUVvNiRriwrnfLn76ObVUog4cnsN5zvN9vuc530fw2Gexd7ZibWllqbuThfY25vR6Fvt7+GkZxG7owmoaxe314pN8fX2df5mw0tvIwNkg2gNDMZ9XYFalYUxOxhhxAVvWVd7IZJReyvIXO1ediKLod5/P91dwYaq2jr5zYXSGKXgfFc9kRQlWYyfDmZmY1Go0B0Mwlb/yF7vW1liT3A/qkuIVBy7nKl6J+W/A6fp6+uUy+pRRWFKSMNzSMlBUjLVbx7vcAspCovnR0+wvXt86uPz5I+OqO3x5UsynwjyM5S839yXGwqxOh0EexqBSSY3sMmmHoxkqLGIo/z6aQ6cxpKcx29KwC3Bx2ERHUDA96lxabqsZLCpB9HnwuN0IE9W1DCrCqQxNJPGAgrdxKUzfy6ZJmcyDgChGclXMdO0GXDIPUxogEZAnoouIZUr/mu/z86w5nQjmF9X0KqLICcwg+/hFGk9FsKDOoPyEkrq4BCaeanHPDW0CSoPYMNvoGNpjsdTLr2DSpGGuKN9q2YcwUq2jITwSbVA6WSevo91/nEmViueKa+jik7CUZTPVVo9XGqgouhCltlYdDmyTMyxPT+OwWVn8avvzhuPNeiqDZcTsO0PSkRieHQ3kW2oCM1XFtKTepC8lklZNPh7pgOhybU5ZAvVKbDZEs1c4gnXsA4bCh5TeeERlagGW3DzmSh+z2NXMeFUVHXdzGKppwiO1696rv611px6F7WA7Ob+wgM1u38ztuPl/P2Qb8BdI69Jj0I9eRQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cca9396267ae61ccdc7c24376fd7421f/263a4/example6.webp 480w,
/static/cca9396267ae61ccdc7c24376fd7421f/a6361/example6.webp 960w,
/static/cca9396267ae61ccdc7c24376fd7421f/6a898/example6.webp 1542w&quot; sizes=&quot;(max-width: 1542px) 100vw, 1542px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cca9396267ae61ccdc7c24376fd7421f/9aebd/example6.png 480w,
/static/cca9396267ae61ccdc7c24376fd7421f/a91f8/example6.png 960w,
/static/cca9396267ae61ccdc7c24376fd7421f/46352/example6.png 1542w&quot; sizes=&quot;(max-width: 1542px) 100vw, 1542px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cca9396267ae61ccdc7c24376fd7421f/46352/example6.png&quot; alt=&quot;Web Worker와 OffscreenCanvas를 활용한 이미지 처리 아키텍처&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;flex:1;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1536px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bad33c528ef316367d0f402e4a3efc81/e8464/example7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAADJElEQVR42gEZA+b8APv28Pr27/348uHd2MC8ub+8uM/Lx9TQy726tsPAu7u4tMC9ub+8t8bDvsXCvsG+uu7p5P/59Pv28Pr17wD48uz79O348u3QzMi2s6+6trLPzMfW0czBvbi0sq67uLS6t7O4tbHBvbnEwLu3tLDj39r18evz7uj38+0AxszOub/BycrH1dLQ6ubh3+DfyM7Qys7OxcnH3drW4t7Z7urlzdHS1tfXycvK19TQ0MzI39nT1dXT3NvZAHeMlUJoenKGj5WkqcrMzMTLzqGrrU9vf1dyf4eWnLO6u8zPz7i9voOUmVl2gn+Pk56oqcnLyb7ExtnY1QBTZGg3WmhLaXZLandSbnphgIx2hYg5VFxFY3FJZW9MZ3FSbnl4jJRYZ2lEY25XcHlccnledHxbdoDCx8cAYGBbIDg9U2hthIiIanl7WHB1gYSBMDk4MEhOcXNyc3h3UGJkc39/ZmReKkBCVmdpgH96cHd1WGlqwcG9AHZ7cx5KSBtiZFKJiTRxcSQyJnh3bURZVA5KSz9xcEp8fB4+OE9MQHuAdyJOSiFgYVd+fURubCYwIa+qoQA9jo0AdnUIi4wSi40afn0FaGRSjIgKe3oBfX0NiowZe3wFbGkycWw+jo8Ac3QOhIYigIIrdnQAW1eitrEAPZiXAIeHBo2NCX16HVRLNZOVYJ2bCYiIAIaHBoqMEVZPJHJvV56fOJSTAH+BDIaHEnZ1KFRJJoWIssfDAGyhoDGNikl6bk5rW0BCMFtURH+NhUCWlzF8dFFzZUNMOkdFNXduYFmZlyp/fFd9b1twX0hMN1xXQsC2qgBqbWVJRztxXEiBaVVlVEJPQCt3dGlLT0ZZTDx9ZFByXk1PPytlWktoa2NGQzZrWUV7ZVJkVkVJOSS2r6UATkxLPTw8S0xMRUZGQ0RDOzs8WlhXMzExRkZGSUpKQUJCNTY2RUVES0lIOjk4SElJQkJDSEhIHyAipKGeAMvHw767t8K+usC8uL+7tr66ts/Lx7+8uMK+usG9ub+7t7u4tMjEwMnGwr67tsK+ur+8t8G9ubq2suPf2VWRl69TZcM8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bad33c528ef316367d0f402e4a3efc81/263a4/example7.webp 480w,
/static/bad33c528ef316367d0f402e4a3efc81/a6361/example7.webp 960w,
/static/bad33c528ef316367d0f402e4a3efc81/f3efb/example7.webp 1536w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bad33c528ef316367d0f402e4a3efc81/9aebd/example7.png 480w,
/static/bad33c528ef316367d0f402e4a3efc81/a91f8/example7.png 960w,
/static/bad33c528ef316367d0f402e4a3efc81/e8464/example7.png 1536w&quot; sizes=&quot;(max-width: 1536px) 100vw, 1536px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bad33c528ef316367d0f402e4a3efc81/e8464/example7.png&quot; alt=&quot;Web Worker 처리 후 최적화된 이미지 결과물&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;offscreencanvas를-활용한-이미지-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#offscreencanvas%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;offscreencanvas를 활용한 이미지 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;OffscreenCanvas를 활용한 이미지 처리&lt;/h3&gt;
&lt;p&gt;OffscreenCanvas를 사용하면 메모리 효율적으로 이미지를 리사이징할 수 있습니다. Worker 없이도 간단하게 이미지 크기를 조정하고 포맷을 변경할 수 있죠.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resizeImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;file&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;800&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;800&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 이미지 파일을 비트맵으로 변환&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; bitmap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createImageBitmap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 비율 계산 (가로/세로 비율 유지)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ratio &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;maxWidth &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; bitmap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxHeight &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; bitmap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; width &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; bitmap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; ratio&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; height &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; bitmap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; ratio&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// OffscreenCanvas 생성 및 이미지 그리기&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; canvas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OffscreenCanvas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ctx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;2d&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bitmap&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// JPEG 형식의 Blob으로 변환&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;convertToBlob&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;image/webp&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.8&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용 예시&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; compressedBlob &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resizeImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imageFile&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 방식은 메인 스레드에서 실행되지만, OffscreenCanvas는 원래 Web Worker에서 사용하기 위해 설계된 API로, 일반 Canvas와 달리 DOM에 연결되지 않습니다. 이러한 특성 덕분에 메인 스레드에서 사용하더라도 메모리 효율성이 높고 렌더링 성능에 이점이 있습니다. 특히 단일 이미지 처리나 간단한 리사이징 작업에 효과적입니다.&lt;/p&gt;
&lt;h3 id=&quot;base64--file-변환-워커&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#base64--file-%EB%B3%80%ED%99%98-%EC%9B%8C%EC%BB%A4&quot; aria-label=&quot;base64  file 변환 워커 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Base64 → File 변환 워커&lt;/h3&gt;
&lt;p&gt;네이티브 앱과의 연동 과정에서 base64 이미지 데이터를 처리하는 경우가 많았습니다. 이 변환 과정을 Worker로 분리한 이유는 두 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;대용량 문자열 처리&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Base64 문자열은 바이너리 대비 약 1.37배 크기입니다. 그래서 디코딩 과정에 메모리 사용량이 급증하고, 그 과정에 메인 스레드 블로킹 현상이 발생하기에 이를 대비해야 했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;반복적인 변환 작업&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;셔터는 여러 이미지를 업로드할 수 있어 이미지 처리 역시 동시에 진행해야 합니다. 이미지를 변환할 때마다 연산 처리가 무거워지면서 UI 응답 속도 또한 저하되는 문제를 해결해야 했습니다.&lt;/p&gt;
&lt;h4 id=&quot;구현-코드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B5%AC%ED%98%84-%EC%BD%94%EB%93%9C&quot; aria-label=&quot;구현 코드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;구현 코드&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// base64-worker.js&lt;/span&gt;
self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; base64String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; mimeType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// base64 데이터 정제&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pureBase64 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; base64String&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;,&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; base64String&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// base64 → 바이너리 변환&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; binary &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;atob&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pureBase64&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; byteArray &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Uint8Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;binary&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; char&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;charCodeAt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Blob 생성 및 File 객체로 변환&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; blob &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Blob&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;byteArray&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; mimeType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; file &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;blob&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; mimeType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; file &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;success&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// main.js&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; worker &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Worker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;base64-worker.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;convertBase64ToFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;base64String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; mimeType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; reject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    worker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;onmessage&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;success&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;reject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    worker&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;postMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; base64String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fileName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; mimeType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;성능-측정-결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95-%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;성능 측정 결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;성능 측정 결과&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Base64 문자열 데이터를 이미지 파일로 변환하고, 해당 이미지를 리사이징하여 화면에 표시할 준비를 마치는 데까지 걸린 총 시간&lt;/strong&gt;과 이 과정에서의 &lt;strong&gt;메인 스레드 블로킹 시간&lt;/strong&gt;을 Worker 도입 전후로 비교 측정한 결과입니다.&lt;/p&gt;
&lt;div style=&quot;display:flex; justify-content:center;&quot;&gt;
  &lt;div style=&quot;width: 600px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1628px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/67ce3a8ff5c1f81f4c004961fd635f2e/134d7/example8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.04166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB70lEQVR42qWSy2pTURSGz0PoC/gUvoETO1EExUkceBkUOhC0SK1FMG0SoyFqqkaMjQMLxYqgbUeiRmwFJc1JbNMUmiaFXE7SJCdNzq09+Txn12jAoQs2e7G//a9/7Ytk2weYRhfL1HDzwdi3TCxDc2Zd5L1e7y8zDUy9+49G0ox9CjWdvKJTrHfIlyp0TFvARttg21nfcsb6dolCWcESqEe16WiqGuW9HoVqjdKuioukHaWM90WYOzE/9+Ihnr95i7xVFAWXM0kmnt1lcsbHo1dPib9botRQBZv78J5bUT/e2CTTszHmP36la5hIudx3PBeOMXL1OLFogNDDcRaWokK0uBDlzNmjjE+cJB57zFRwmHTms2Dh8BWHHSHg9/ByJkooMoKiFJ0O8zmCY8M88F4nEg4ydOocs3Pzhx1+WmTq2iWmA2MEfD5ODJ0m/XNdsNfxCL7Ryzy5f5vRGzc577mIqraRDK3DZipFJplEXl0l8WWZ/O8jN+sKm7JMVnZ4JkMisUJjtyVYZadAzmFrjjYlp/m28gPDMJBs20bTDVqqimVZYnP/Ndt7HbK5HKl0mlqthqZ1/7BypcpaNkt2Y8MxaWDo+uErD34FN3cN+muugeoY9Uer1RLcjd16XZg0m03n7hQxuzqpX2iw8P/ELxL+fGy33060AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/67ce3a8ff5c1f81f4c004961fd635f2e/263a4/example8.webp 480w,
/static/67ce3a8ff5c1f81f4c004961fd635f2e/a6361/example8.webp 960w,
/static/67ce3a8ff5c1f81f4c004961fd635f2e/3a180/example8.webp 1628w&quot; sizes=&quot;(max-width: 1628px) 100vw, 1628px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/67ce3a8ff5c1f81f4c004961fd635f2e/9aebd/example8.png 480w,
/static/67ce3a8ff5c1f81f4c004961fd635f2e/a91f8/example8.png 960w,
/static/67ce3a8ff5c1f81f4c004961fd635f2e/134d7/example8.png 1628w&quot; sizes=&quot;(max-width: 1628px) 100vw, 1628px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/67ce3a8ff5c1f81f4c004961fd635f2e/134d7/example8.png&quot; alt=&quot;Web Worker 도입 전후 이미지 처리 성능 비교 그래프&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[Base64 변환 - 2.31MB 이미지 데이터 기준]
- 처리 시간: 388ms → 137ms (65% 감소)
- 메인 스레드 블로킹: 350ms → 8ms (97% 감소)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;메인 스레드 블로킹 시간이 크게 줄어들었을 뿐만 아니라, 총 처리 시간까지 단축된 것을 볼 수 있습니다. 이는 Worker가 별도 스레드/코어에서 메인 스레드의 다른 작업과 경쟁 없이 병렬로 실행되어, 통신 오버헤드를 상쇄할 만큼 전담 처리 효율이 높기 때문입니다. 물론, 이 결과는 특정 테스트 환경에서의 측정치이며, 실제 사용자 환경(기기 사양, 브라우저 등)에 따라 달라질 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;Web Worker는 성능 최적화를 넘어 웹 아키텍처를 구조적으로 개선할 수 있는 강력한 도구입니다. 이미지 처리처럼 무거운 연산을 메인 스레드에서 분리함으로써 UI는 부드럽게 유지하고, 메모리를 효율적으로 활용할 수 있었습니다. 이 덕분에 사용자 경험 역시 크게 향상됩니다.&lt;/p&gt;
&lt;p&gt;Worker로 성능 개선에 성공했으니, 앞으로는 이를 전략적으로 활용해볼 계획입니다. 더 나은 서비스를 기대하셔도 좋습니다!&lt;/p&gt;
&lt;p&gt;읽어주셔서 감사합니다! 😊&lt;/p&gt;</content:encoded></item><item><title><![CDATA[iOS 개발자를 위한 DocC 실무 튜토리얼]]></title><description><![CDATA[안녕하세요! 올리브영🫒 iOS앱 개발자📱 럭셔Lee 입니다🙇‍♂️ 이번 포스팅에서는 Apple에서 제공하는 문서화 도구, DocC에 대해서 이야기해보고자 합니다.


 🤔 일단 DocC가 뭔가요? DocC는 Xcode13부터 지원하는 Apple…]]></description><link>https://oliveyoung.tech/2025-03-28/swift-docc/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-03-28/swift-docc/</guid><pubDate>Fri, 28 Mar 2025 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 올리브영🫒 iOS앱 개발자📱 럭셔Lee 입니다🙇‍♂️&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Apple에서 제공하는 문서화 도구, DocC에 대해서 이야기해보고자 합니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;-일단-docc가-뭔가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%BC%EB%8B%A8-docc%EA%B0%80-%EB%AD%94%EA%B0%80%EC%9A%94&quot; aria-label=&quot; 일단 docc가 뭔가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🤔 일단 DocC가 뭔가요?&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;DocC는 Xcode13부터 지원하는 Apple의 공식 문서화 도구입니다. Swift 또는 Objective-C 코드에 작성한 주석을 기반으로 문서를 생성할 수 있고, 별도의 Markdown 파일을 등록 가능한 도구입니다. DocC 문서 컴파일러는 주석이나 Markdown 기반 텍스트를 Swift 및 Objective-C 프로젝트를 위한 문서로 변환하고 Xcode 문서 창에 바로 표시합니다. 웹사이트에서도 DocC로 구성한 문서를 호스팅할 수 있습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/549ad571d200d95d6dfd41b9a4dfea5a/eaa1b/docc-01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6ElEQVR42oWT2W7bMBBF9f+/UfS5LwWKPgWJ08RuHcdy0i1egFjWYkmWRFKkqM23JF3LVtqmAxxwOLy4JIeSNbOHuLfHWLs+giiBt43NuMsY4pQiSgi2cYptlCLaEcTJC9JTrrXW4OojJpMbPAcEm7iCExX4tgoVEdZhgSBr4aeNwU1quLuqhxOVWG44Fg5XeglrcvcJTz9t+LHagTYISYXp1xVuxo94nDtYOnGHtxPYZqXRhFml9JWqyc7Q0YYze4T50wwZZSgqgMsGSZYbMsqRkBwpFQaSS7Mua3TwssWO1IjVJlQ0sO7G1+aEheDQsd/v4Xku3M0GjDHkeW445AcYo6CEmpoQAudh2ZNrrBYzFMXJkNKDWEMVaUYglBHPf2+gakRptI5z3jf8cHGF24kNfnZCzTFalbdViRWrEanrVm2D5mz9ZVhv3g/w7nIKxkVneB5V3WhbvP28xmgZImAETJb/Nny4v8bzsn9lKaWaF6Y/Gn31Uo2lLCBV/bheqJqe9wxHw0v8+D7tGYZhqB7Gg+u6ZjSo3A+CQ+57CHwfjrc1/e0Zjr8MsJg/qJP0e9j1UiEq9X0yiaaWqJuqW2v/0svTZ1McX+tPka5QxiEKooxqvBbW8PYCtvqfXzPU0eQC+6bF/+IXg/HiNC0/eEMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/549ad571d200d95d6dfd41b9a4dfea5a/263a4/docc-01.webp 480w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/a6361/docc-01.webp 960w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/0b34d/docc-01.webp 1920w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/da28f/docc-01.webp 2880w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/49f40/docc-01.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/549ad571d200d95d6dfd41b9a4dfea5a/9aebd/docc-01.png 480w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/a91f8/docc-01.png 960w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/ac7a9/docc-01.png 1920w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/f9c26/docc-01.png 2880w,
/static/549ad571d200d95d6dfd41b9a4dfea5a/eaa1b/docc-01.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/549ad571d200d95d6dfd41b9a4dfea5a/ac7a9/docc-01.png&quot; alt=&quot;docc-01&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
이와 같이 프로젝트(프레임워크)에 대한 상세한 설명을 적을 수 있고요.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/564342c3e183b6c8d61fcabd5fe2938f/eaa1b/docc-02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuUlEQVR42pWT227TQBCG/f7vwB0vgBASJWkTUighIYL0JnaS2yi2Wp/tXXu9zs/M0NgOopEYafRrV7vfzmHHeVzPsX5cwQ98ZFmCOIoRP4V4ilLESYokSRCGIaIoQhzH4ryXpglpTPsh7UWifMZZLmbw3DWUKsF2OkGgz88hPZDRpRRFUSDP807TNBVlt9a+3DuJO6vVF+y8NXQHPMHzPGw2m85d1xXf7/fY7XbYbrei7BzV0Jzl9wn221/QugfmWU5rjbquUFUVaS3aNA3atpWoznqOsAO+/zjC9Ot9lzJbUxskUYIsqVDmDYrMoFJGoMYYefQ1c968m+Hth3vkRR8hG9fHPwZUS2oGwZXSEs01mABnn2/wc3F3kTKbtY00hRvBqnWf8tUIx9MR5g8TKN2nbE1DqSqURQ1VWNSVRWNaqeWf2tZdV/+GO6PbG3x7uL2IkC8YglrbvkTUXlwewv4B/ETAcQdkOxwO9HVcqpvC/5pzRykv5pMLIE+C7/soy1JSZOcOn/XaQ854NsJyPkWl+0PchOPxKJ92OCXnBnFJXgVOCfiDxk8NJoVnNwgCAfCa6zj0a13+Dd8d5j6WkzvZAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/564342c3e183b6c8d61fcabd5fe2938f/263a4/docc-02.webp 480w,
/static/564342c3e183b6c8d61fcabd5fe2938f/a6361/docc-02.webp 960w,
/static/564342c3e183b6c8d61fcabd5fe2938f/0b34d/docc-02.webp 1920w,
/static/564342c3e183b6c8d61fcabd5fe2938f/da28f/docc-02.webp 2880w,
/static/564342c3e183b6c8d61fcabd5fe2938f/49f40/docc-02.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/564342c3e183b6c8d61fcabd5fe2938f/9aebd/docc-02.png 480w,
/static/564342c3e183b6c8d61fcabd5fe2938f/a91f8/docc-02.png 960w,
/static/564342c3e183b6c8d61fcabd5fe2938f/ac7a9/docc-02.png 1920w,
/static/564342c3e183b6c8d61fcabd5fe2938f/f9c26/docc-02.png 2880w,
/static/564342c3e183b6c8d61fcabd5fe2938f/eaa1b/docc-02.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/564342c3e183b6c8d61fcabd5fe2938f/ac7a9/docc-02.png&quot; alt=&quot;docc-02&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
프로젝트 내에서 사용하는 함수의 사용 방법 등에 대해서도 적어둘 수 있죠.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-올리브영이-docc를-도입하게-된-계기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%B4-docc%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%98%EA%B2%8C-%EB%90%9C-%EA%B3%84%EA%B8%B0&quot; aria-label=&quot; 올리브영이 docc를 도입하게 된 계기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🫒 올리브영이 DocC를 도입하게 된 계기&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;올리브영에서는 타직군 동료들끼리 서로서로 가이드해줘야 하는 경우가 많고, 그 과정에서 문서를 사용합니다. 앱에서 관리하는 문서는 웹과 앱간의 인터페이스 가이드, 그리고 앱 로컬 빌드 가이드가 있습니다.&lt;/p&gt;
&lt;p&gt;DocC를 도입하기 전, 올리브영 iOS 개발자들은 &apos;컨플루언스&apos;라는 도구를 활용하여 문서를 작성하고 있었습니다. 다시 말해 코드와 문서는 별도로 존재했고, &lt;strong&gt;코드 작성 후 문서를 별도로 업데이트&lt;/strong&gt;해야 했습니다. 하지만 개발에 집중하다 보니 문서 업데이트를 놓치는 경우가 자주 있었죠. 한번 뒤처지기 시작한 문서는 내용이 점점 빈약해졌고, 코드와 문서의 차이가 걷잡을 수 없을 정도로 벌어지기 시작했습니다. &lt;strong&gt;이러한 문제를 해결하기 위해 고민하다가 발견한 도구가 DocC&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;DocC를 도입한 가장 큰 이유는 &lt;strong&gt;문서 업데이트를 강제할 수 있다&lt;/strong&gt;는 점입니다. 예를 들어, JavaScript Interface를 추가하는 상황이라면 DocC 문서까지 만들어야 코드 리뷰를 통과하는 프로세스를 만들었습니다. DocC를 활용한 프로세스를 도입하니 문서가 코드에 비해 뒤처지지 않는 것은 물론이고 문서의 질도 좋아졌어요!👏🏻👏🏻👏🏻  인터페이스가 직관적이라 문서 확인이 수월한 것도 장점입니다.&lt;/p&gt;
&lt;p&gt;심지어, DocC는 &lt;strong&gt;Xcode에 기본 내장&lt;/strong&gt;되어 있어 별도의 설정 없이 바로 사용 가능합니다. 코드에 작성된 타입, 메서드, 파라미터 주석 등을 기반으로 자동 구조화한 문서를 생성해준다는 장점이 있습니다. 이런 장점은 공수가 덜 들어간다는 또다른 장점으로 이어집니다. 웹에 배포 가능한 환경 덕에 웹 브라우저를 통해 &lt;strong&gt;누구나 인터페이스를 확인&lt;/strong&gt;할 수 있기도 합니다. 앱 개발자 이외에, 다른 영역의 개발자들도 인터페이스를 확인해야하는 경우가 있는데 이럴 때 소통 비용이 낮아지는 셈입니다. 꼬리에 꼬리를 무는 장점이죠?&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-docc를-쓰면-뭐가-좋나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-docc%EB%A5%BC-%EC%93%B0%EB%A9%B4-%EB%AD%90%EA%B0%80-%EC%A2%8B%EB%82%98%EC%9A%94&quot; aria-label=&quot; docc를 쓰면 뭐가 좋나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🤔 DocC를 쓰면 뭐가 좋나요?&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-문서-자동화로-시간-절약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%AC%B8%EC%84%9C-%EC%9E%90%EB%8F%99%ED%99%94%EB%A1%9C-%EC%8B%9C%EA%B0%84-%EC%A0%88%EC%95%BD&quot; aria-label=&quot;1 문서 자동화로 시간 절약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 문서 자동화로 시간 절약&lt;/h2&gt;
&lt;p&gt;개발을 하다보면 생각보다 문서 작성에 많은 시간을 소요하게 됩니다. 하지만 DocC를 사용한다면 주석(///)과 마크다운만으로 간단히 API 문서 생성하기 때문에 문서 작성에 드는 시간을 단축할 수 있어요. 또한 문서를 일일이 작성할 필요 없이 일관된 포맷 유지가 가능한 것도 큰 장점입니다. 한번 만들어 놓으면 코드 변경시 문서가 자동 업데이트 되기 때문에, 유지보수 측면에서도 상당한 이점이 있습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/// 카메라로 인식한 물체가 어떤 물체인지에 대한 결과 값을 계산하는 함수 입니다.
func calculateAccuracy(results: [VNClassificationObservation]) {
    guard
        let top1Result = results.first
    else {
            showFailResult()
            return
          }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/27edd5b1f40544c00fd82ed6b0b9150b/eaa1b/docc-03.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABSElEQVR42q2SXU/CMBSG9/9/hdfeeSPyvXUgyEA2QGWwSxIjum7uw3Yb20tXdIkxeMNO8qanbfrkPadHWS0NPK0MLFZzvL69w/OokAdKKXzXw6fIP6gP16XyzHVduUZR+EdhGEAZDjqwzDHWawucc5RRrvv9Hh71EMcxgjBCEIRgjMn7oihwLhS114Ax0bCxTSTJCej7PuyNja2zheM4sG0bG7Hf7XbCSYQsy84Dm6QPQjqYzsZI00QeHtIDWMiko1Kcp+Asw1eciDxBnufS5Y9+Aa9uJ7huP+CGmGA8qUrKD7l8eFJRCf+UK4FtVcNo2MVyOa0cXhJKq9/HUG+Jn54h/e7hRcC2qkJXGzDnI+GwBmBP1zEgTSwWk3pKVsUv69odygGvB6h1QARwNh3UU7LWbYn+3YvBtqrBvghojAgsa4yX58dagEeJpOnP+lfuxAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/27edd5b1f40544c00fd82ed6b0b9150b/263a4/docc-03.webp 480w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/a6361/docc-03.webp 960w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/0b34d/docc-03.webp 1920w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/da28f/docc-03.webp 2880w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/49f40/docc-03.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/27edd5b1f40544c00fd82ed6b0b9150b/9aebd/docc-03.png 480w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/a91f8/docc-03.png 960w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/ac7a9/docc-03.png 1920w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/f9c26/docc-03.png 2880w,
/static/27edd5b1f40544c00fd82ed6b0b9150b/eaa1b/docc-03.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/27edd5b1f40544c00fd82ed6b0b9150b/ac7a9/docc-03.png&quot; alt=&quot;docc-03&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;2-검색-기능-및-직관적-인터페이스-제공&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%B0%8F-%EC%A7%81%EA%B4%80%EC%A0%81-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%A0%9C%EA%B3%B5&quot; aria-label=&quot;2 검색 기능 및 직관적 인터페이스 제공 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 검색 기능 및 직관적 인터페이스 제공&lt;/h2&gt;
&lt;p&gt;개발 중에 API를 통해 서버로부터 데이터를 가져와야 하는 경우가 종종 있습니다. 이 때 API가 제공하는 데이터와 데이터 타입을 확인하기 위해 관련 문서를 찾아야 하는데, 정리되지 않은 문서라면 이따금씩 불편함을 겪은 적이 있을 거에요. DocC를 도입하면 &lt;strong&gt;Xcode에서 한눈에 API 문서를 확인&lt;/strong&gt;할 수 있습니다. 검색 기능이 포함되어 있어 원하는 정보를 빠르게 찾을 수 있죠. 단축키 Cmd + Shift + F를 활용해서 문서를 즉시 검색할 수도 있습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/63ab4751ac5e6080b6783910844e30af/eaa1b/docc-04.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABL0lEQVR42q2ST2+CQBDF+f4fpwc11WpVLDUilv8sy8FbS1Ukol1Q4HUXU4ppepJJXvYy88t7MytZ5gK2tYBhrfAebrDfR1x7RNEOcRRj87lF4FOYjgdCfXjEhU8JdrstTqcjjsekVpIcIL3IA+hvr/A8HWmaQpR4wzBEHHPgxxbagw5VVeG4dgUUOvBhUWVZolnS86AHTZuBEgNZdgUKECEENKCglMKxHbiug/V6XblnjP0B1UB5xB3qCqhv1sD8nIMlrBoUbrP0gpRdwL7OvCerYE3dAOfyGLatcuCvQ9FU5AWK4kdlLfzjrAZO+o+w7MUN8J6Sht3OdYeNyPcBex0oyrA6yrkN4PSpD8OYtxd5zHe4XMoIaGuRu/zTTto9ymo1a8+hMh1VkV1HawX4DXHS58/fnDk+AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/63ab4751ac5e6080b6783910844e30af/263a4/docc-04.webp 480w,
/static/63ab4751ac5e6080b6783910844e30af/a6361/docc-04.webp 960w,
/static/63ab4751ac5e6080b6783910844e30af/0b34d/docc-04.webp 1920w,
/static/63ab4751ac5e6080b6783910844e30af/da28f/docc-04.webp 2880w,
/static/63ab4751ac5e6080b6783910844e30af/49f40/docc-04.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/63ab4751ac5e6080b6783910844e30af/9aebd/docc-04.png 480w,
/static/63ab4751ac5e6080b6783910844e30af/a91f8/docc-04.png 960w,
/static/63ab4751ac5e6080b6783910844e30af/ac7a9/docc-04.png 1920w,
/static/63ab4751ac5e6080b6783910844e30af/f9c26/docc-04.png 2880w,
/static/63ab4751ac5e6080b6783910844e30af/eaa1b/docc-04.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/63ab4751ac5e6080b6783910844e30af/ac7a9/docc-04.png&quot; alt=&quot;docc-04&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;3-마크다운-기반-튜토리얼-및-다양한-포맷의-문서-작성-가능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4-%EA%B8%B0%EB%B0%98-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-%EB%B0%8F-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%8F%AC%EB%A7%B7%EC%9D%98-%EB%AC%B8%EC%84%9C-%EC%9E%91%EC%84%B1-%EA%B0%80%EB%8A%A5&quot; aria-label=&quot;3 마크다운 기반 튜토리얼 및 다양한 포맷의 문서 작성 가능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 마크다운 기반 튜토리얼 및 다양한 포맷의 문서 작성 가능&lt;/h2&gt;
&lt;p&gt;DocC는 주석으로 작성 가능하고, 마크다운(.md)을 활용한 추가 문서 형태로도 작성할 수 있습니다. API 문서뿐만 아니라 &lt;strong&gt;개념 설명, 사용 예제, 튜토리얼 제작&lt;/strong&gt;이 가능하기 때문에 다양한 용도로 활용할 수 있습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# ``ImageClassification_CNN_iOS/LabelPicker``

@Metadata {
    @DocumentationExtension(mergeBehavior: override)
    @Available(&quot;AppVersion&quot;, introduced: &quot;3.9.1&quot;)
}

비교하고자 하는 객체의 이름을 선택하는 PickerView입니다.

## 사용법
```swift
let labelPicker = LabelPicker()

## 파라미터
| params | type | description | required | encoding |
|:---:|:---:|:---:|:---:|:---:|
-| - | - | - | -&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a5d47251e305caf3a7b559f094524125/66e9f/docc-05.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+klEQVR42pWSTW7DIBCFuf+Bqq6yaJsmdg2pEylXsDe1E4L5MeAXQFUrJaglIz0JxOhj3syQD1Zhf/jEJCW0VlBKQSuDeZ7BOYcQAtbadC8RodUGh32DZVlwG13Xoe/79ElULuc2yNOK4Xndwvv7ZG89nHPhzSeVBNm+1KA7igX3QBna4FwZ6AdIX7egdJMFjsMQrOpUZYndb2AFFgaDDNAYEwalkyK0CNgEIKV5YLRc2rvfCt8q7Fidtfw1njCeeII+ZLlp8j3kQmLgCu4RIHuv0bb5PZyMx3kyENLgLC0uyoYl/3tAhNZrHI9tNsnMLik+xe3x4fBfpVdRC1qroXA/7QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a5d47251e305caf3a7b559f094524125/263a4/docc-05.webp 480w,
/static/a5d47251e305caf3a7b559f094524125/a6361/docc-05.webp 960w,
/static/a5d47251e305caf3a7b559f094524125/0b34d/docc-05.webp 1920w,
/static/a5d47251e305caf3a7b559f094524125/da28f/docc-05.webp 2880w,
/static/a5d47251e305caf3a7b559f094524125/98b7d/docc-05.webp 3840w,
/static/a5d47251e305caf3a7b559f094524125/2d0e9/docc-05.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a5d47251e305caf3a7b559f094524125/9aebd/docc-05.png 480w,
/static/a5d47251e305caf3a7b559f094524125/a91f8/docc-05.png 960w,
/static/a5d47251e305caf3a7b559f094524125/ac7a9/docc-05.png 1920w,
/static/a5d47251e305caf3a7b559f094524125/f9c26/docc-05.png 2880w,
/static/a5d47251e305caf3a7b559f094524125/5da7e/docc-05.png 3840w,
/static/a5d47251e305caf3a7b559f094524125/66e9f/docc-05.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a5d47251e305caf3a7b559f094524125/ac7a9/docc-05.png&quot; alt=&quot;docc-05&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;4-강력한-협업-도구로-사용-가능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EA%B0%95%EB%A0%A5%ED%95%9C-%ED%98%91%EC%97%85-%EB%8F%84%EA%B5%AC%EB%A1%9C-%EC%82%AC%EC%9A%A9-%EA%B0%80%EB%8A%A5&quot; aria-label=&quot;4 강력한 협업 도구로 사용 가능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 강력한 협업 도구로 사용 가능&lt;/h2&gt;
&lt;p&gt;팀 협업 시 새로운 팀원이 DocC를 통해 프로젝트의 구조와 흐름을 빠르게 이해할 수 있어 코드 이해도가 향상됩니다. 또한 오픈소스 프로젝트에서는 API 문서를 명확하게 제공할 수 있어 외부 개발자들이 쉽게 활용할 수 있습니다. 사용자는 직관적인 문서를 통해 API를 효율적으로 사용할 수 있으며, 문서는 HTML 형태로 생성되기 때문에 GitHub Pages나 내부 서버를 통해 손쉽게 배포할 수 있습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;5-코드와-문서-동기화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EC%BD%94%EB%93%9C%EC%99%80-%EB%AC%B8%EC%84%9C-%EB%8F%99%EA%B8%B0%ED%99%94&quot; aria-label=&quot;5 코드와 문서 동기화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 코드와 문서 동기화&lt;/h2&gt;
&lt;p&gt;문서는 코드로부터 자동으로 생성되기 때문에 코드가 변경될 때마다 문서 역시 자동으로 업데이트됩니다. 이를 통해 문서와 코드 간의 불일치를 방지할 수 있으며, 결과적으로 문서를 따로 수정할 필요가 줄어서 문서 유지보수에 드는 비용을 크게 절감할 수 있습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h1 id=&quot;-그렇다면-docc로-문서를-어떻게-만들어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B7%B8%EB%A0%87%EB%8B%A4%EB%A9%B4-docc%EB%A1%9C-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 그렇다면 docc로 문서를 어떻게 만들어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🤔 그렇다면 DocC로 문서를 어떻게 만들어요?&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1단계-docc-파일-생성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EB%8B%A8%EA%B3%84-docc-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1단계 docc 파일 생성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1단계: DocC 파일 생성하기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;File &gt; New &gt; File from Template&lt;/strong&gt;을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 593px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/df0e0e3361002d4ec11a591b68f6d37d/a0904/docc-06.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 117.08333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAAAsTAAALEwEAmpwYAAADeUlEQVR42pWVyW7TUBSGI5Y8AGvY9A14hZY1KwoPUyTWCCFFIJZ0wYIpahkyuI6bxHZsx46deMzUlqQRpUJ0H4tzOOcmDmGo2lr6Vdf2/e75z3CTkyRp7ePH3WeSVMkrinyuZFnK67qab7XMfByHed/vLtXtennXdZ6ZprmWM01tw/c7GIYBDAZ9GA4HOFcf+/2eUBxH6AdddN02ttsOJkmM0+lxJjg6OoTx+At6nreRq9Vq6/yhYRgzyzJnjmOntt1KaWFKC9NeL0kJmAaBn6pqIy2XS6mua/QuEaINZ1EUzihSYqjrDNwgAFqWBY7jAN8TVERCUYvoaAF6HQ+tlo26YWIYxRglPQzjHgZRAl0/hDBOUNM0EeFGo1FHRakyDDKbLLbGCgno0EbDXgdHiUfq4GHfx6OBj5NRAMcHIUyPesjpE0CORtd10DQVOCqyKUDZ3yjy0bA9fF75io8/neKTzyf4tPQNH74/wa3CKTza+Q4vlB/48oM9BxpmE/f3FWi1LOh2O7gqthxQQSzbwddyiK+kCD+3pli0JlhoTvBNYwxv1TGU7Sl+qJgLoDEHknWgwqBpGGiYBnI+OUJOOLUFui0NTa2KgWeS3RC/DEMcj0KgNMCo59O7hWWyipVKGTiPHNE8h4mwHMcxtYMriqTpOir7+xjRs0SkQ4iK4kMQhr+LwlWlVljmkKNicCaOkKHUNrwxwdtiU96QBD4DA/83kC1zhak94G9YBuQI59bbIrcLmADSGtEdS6BJ+Wo0GtDpeJC1yqp4dwbxexb9z8VjN0DAn7TpT5oyAtbmQLbJHyw+wv9BefRGoyEeHh4gjxlrMhmL0aN7OD6e0EA4d5Y5JEtilrPey8SW+RltltJ42nQA1Ekq3as0XSrlU3Vdt0GwJh00t5eWKeGiKJyneXWjbOx4I7Z9VigUbubo2traupY775oXRcd6vUZADbiJs6gWEYq8UoRnxWLxckDuw2pVBlneAzoXuclxJVJxpFFuz3Z2dm5dCsgVpAkB6jXIWoMruwqk51cDctvQIsiOrKwgWQ7Z8qWBmeVSqQh7e5KwuwocDshydAXL1KzYbOp8MPwzegxk69SHlwPqev2O07b5lOaF/PuAq6JGBm7mOInOyuXyxcDtd9s3dncL92VZ3lQUZVOWK39IUfbuSVLpQbFSvEug67kLrl9niANIN0GOdQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/df0e0e3361002d4ec11a591b68f6d37d/263a4/docc-06.webp 480w,
/static/df0e0e3361002d4ec11a591b68f6d37d/8ede9/docc-06.webp 593w&quot; sizes=&quot;(max-width: 593px) 100vw, 593px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/df0e0e3361002d4ec11a591b68f6d37d/9aebd/docc-06.png 480w,
/static/df0e0e3361002d4ec11a591b68f6d37d/a0904/docc-06.png 593w&quot; sizes=&quot;(max-width: 593px) 100vw, 593px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/df0e0e3361002d4ec11a591b68f6d37d/a0904/docc-06.png&quot; alt=&quot;docc-06&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Documentation Catalog&lt;/strong&gt;를 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/eaa1b/docc-07.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABoklEQVR42p2TYa+aMBSG+f//YX9imxgRFCmltBSXbJ+MuRFCvF4+7G7LVbJl0XfnFHXMZDdukCeF0D59e2g93x9hPB7B998jikIkSeIQQhAJZJpCEAshkcoUSmXIMklkyPMcaSoQTKc0NsKM8Eajt5hMfIThxH3MtaGOGqYoHLYsUZZLapfUlles7TGmwIICMEopeCwLgjHhu0SKZFE0QzyfQ8Qx5CKGktKlybWGpgmZorAOfo6pXxj2q/NmUUCC4JwwoQ4a6/Ua9WaDithtt2ifntC2LXa7HbaPj2iaBoYmKGkFmtp5PL+WwYtuhKUxqNsvaL79AHCi+/d1Oh7d++FwgKLBlsti9LneApJW8odQUA0/frB4t/yMN6ojw6mXnnqOJMRZyCXgWhoKwKILNwkF1aXA89cXfP8JJxlel/eu6/5NuN/v3UBOdEk3TNh1h/8T3ibk5VNwPL907mfcJbTWYrVaoaoq1HV9pSKaeoPsU43IPqCgn2HvEfKsvEGHnYZoJVHozE3M3CV8jcsJ+atwOp2A4dPCO70fZO+GD8JQ+AtP53xsgydYbgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/263a4/docc-07.webp 480w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/a6361/docc-07.webp 960w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/0b34d/docc-07.webp 1920w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/da28f/docc-07.webp 2880w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/49f40/docc-07.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/9aebd/docc-07.png 480w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/a91f8/docc-07.png 960w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/ac7a9/docc-07.png 1920w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/f9c26/docc-07.png 2880w,
/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/eaa1b/docc-07.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5e2d6d0ed572f6aaffb549c3f74e0ed6/ac7a9/docc-07.png&quot; alt=&quot;docc-07&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;이름과 Target을 설정한 후 &lt;strong&gt;Create&lt;/strong&gt; 버튼을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/08b155ac48f2afca23ba03b8b97d253c/eaa1b/docc-08.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB30lEQVR42pVTa2/aQBDk//+aKi0QGxsbHwa/k+BiW/1QnHxqitKKEmzwg8nuOVSkL1FLo3t4b252d643GHzAcNgHj7quwbIsWMKCEIJgwbanENMpJsLGlMYTZrMZHMehuYA6GtFZHWNCr9+/gqIMoGmK/Om4HgW68Hwfvh8gCKNXhAh/gtZBKOF5PiYkgjGfz9G7ev8OqjokDEjRRJKNxwZMw4BFsE0TDqlxXZfQXSYv9LoLec+kGE3rsuvpugpGp9CiQE8eDkhhvFggTZa4u4sR3X7E7WKJJEmRZSmiiBS+xhqmQSWwZRneELJCn9JZxDFW+WccDgfUdYPv2wLrpw2eNiWqugV/WZrCpRR9Iu3qLaje9i8KqQlhdIOEgvN8haqqsC1KfF2vsXve4rk8oCjLjjDLpDqfMmGiE35LmYufEuH9fS4J91WNXVHg2B7Rti2Ox/ZyQkFdjnwPn7JEEj4+fqGUaxAL2qZB07Qg3v9RKMguLpZJjIeHHJvNhlTW2O9L7HY7lOUeP4oDVt+Amzil+nXd/gvhNW0IaQlFUaUNuNBs4lMwz9ka2sSWfg3DQLrinwrZuPwC2KRsA8b5AflC5jNpqyC4kPASMNkfCUcjBQx+LZzOefAl4MacE74AWrB0Mnsw/W8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/08b155ac48f2afca23ba03b8b97d253c/263a4/docc-08.webp 480w,
/static/08b155ac48f2afca23ba03b8b97d253c/a6361/docc-08.webp 960w,
/static/08b155ac48f2afca23ba03b8b97d253c/0b34d/docc-08.webp 1920w,
/static/08b155ac48f2afca23ba03b8b97d253c/da28f/docc-08.webp 2880w,
/static/08b155ac48f2afca23ba03b8b97d253c/49f40/docc-08.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/08b155ac48f2afca23ba03b8b97d253c/9aebd/docc-08.png 480w,
/static/08b155ac48f2afca23ba03b8b97d253c/a91f8/docc-08.png 960w,
/static/08b155ac48f2afca23ba03b8b97d253c/ac7a9/docc-08.png 1920w,
/static/08b155ac48f2afca23ba03b8b97d253c/f9c26/docc-08.png 2880w,
/static/08b155ac48f2afca23ba03b8b97d253c/eaa1b/docc-08.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/08b155ac48f2afca23ba03b8b97d253c/ac7a9/docc-08.png&quot; alt=&quot;docc-08&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;2단계-docc에-적용한-contents-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EB%8B%A8%EA%B3%84-docc%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%9C-contents-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;2단계 docc에 적용한 contents 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2단계: DocC에 적용한 Contents 만들기&lt;/h2&gt;
&lt;h3 id=&quot;일반-함수-관련-contents-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%BC%EB%B0%98-%ED%95%A8%EC%88%98-%EA%B4%80%EB%A0%A8-contents-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;일반 함수 관련 contents 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;일반 함수 관련 Contents 만들기&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;File &gt; New &gt; File from Template&lt;/strong&gt;을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 593px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fa5009ef0ad84524a63a95efa674822b/a0904/docc-09.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 117.08333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAAAsTAAALEwEAmpwYAAADeUlEQVR42pWVyW7TUBSGI5Y8AGvY9A14hZY1KwoPUyTWCCFFIJZ0wYIpahkyuI6bxHZsx46deMzUlqQRpUJ0H4tzOOcmDmGo2lr6Vdf2/e75z3CTkyRp7ePH3WeSVMkrinyuZFnK67qab7XMfByHed/vLtXtennXdZ6ZprmWM01tw/c7GIYBDAZ9GA4HOFcf+/2eUBxH6AdddN02ttsOJkmM0+lxJjg6OoTx+At6nreRq9Vq6/yhYRgzyzJnjmOntt1KaWFKC9NeL0kJmAaBn6pqIy2XS6mua/QuEaINZ1EUzihSYqjrDNwgAFqWBY7jAN8TVERCUYvoaAF6HQ+tlo26YWIYxRglPQzjHgZRAl0/hDBOUNM0EeFGo1FHRakyDDKbLLbGCgno0EbDXgdHiUfq4GHfx6OBj5NRAMcHIUyPesjpE0CORtd10DQVOCqyKUDZ3yjy0bA9fF75io8/neKTzyf4tPQNH74/wa3CKTza+Q4vlB/48oM9BxpmE/f3FWi1LOh2O7gqthxQQSzbwddyiK+kCD+3pli0JlhoTvBNYwxv1TGU7Sl+qJgLoDEHknWgwqBpGGiYBnI+OUJOOLUFui0NTa2KgWeS3RC/DEMcj0KgNMCo59O7hWWyipVKGTiPHNE8h4mwHMcxtYMriqTpOir7+xjRs0SkQ4iK4kMQhr+LwlWlVljmkKNicCaOkKHUNrwxwdtiU96QBD4DA/83kC1zhak94G9YBuQI59bbIrcLmADSGtEdS6BJ+Wo0GtDpeJC1yqp4dwbxexb9z8VjN0DAn7TpT5oyAtbmQLbJHyw+wv9BefRGoyEeHh4gjxlrMhmL0aN7OD6e0EA4d5Y5JEtilrPey8SW+RltltJ42nQA1Ekq3as0XSrlU3Vdt0GwJh00t5eWKeGiKJyneXWjbOx4I7Z9VigUbubo2traupY775oXRcd6vUZADbiJs6gWEYq8UoRnxWLxckDuw2pVBlneAzoXuclxJVJxpFFuz3Z2dm5dCsgVpAkB6jXIWoMruwqk51cDctvQIsiOrKwgWQ7Z8qWBmeVSqQh7e5KwuwocDshydAXL1KzYbOp8MPwzegxk69SHlwPqev2O07b5lOaF/PuAq6JGBm7mOInOyuXyxcDtd9s3dncL92VZ3lQUZVOWK39IUfbuSVLpQbFSvEug67kLrl9niANIN0GOdQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fa5009ef0ad84524a63a95efa674822b/263a4/docc-09.webp 480w,
/static/fa5009ef0ad84524a63a95efa674822b/8ede9/docc-09.webp 593w&quot; sizes=&quot;(max-width: 593px) 100vw, 593px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fa5009ef0ad84524a63a95efa674822b/9aebd/docc-09.png 480w,
/static/fa5009ef0ad84524a63a95efa674822b/a0904/docc-09.png 593w&quot; sizes=&quot;(max-width: 593px) 100vw, 593px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fa5009ef0ad84524a63a95efa674822b/a0904/docc-09.png&quot; alt=&quot;docc-09&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Markdown File&lt;/strong&gt;을 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4dadd9b925c6772bc01ef00ed8348006/66e9f/docc-10.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABYUlEQVR42n2S226CQBCGef9HarSCkrqy5+XKpFqhHu5aU7nphUxnZkFLW0ryBdiBL//sTjKdTmAyeYA8z6GQilHaIgY0YowdoM193VrH0LMQgknS9BGI1Urcis5Z8D5AKEsoR6BaCOGGUopJ0mwWhWIJRmEKTUIPznpMQ6nMAGstBB8GMu89SCmZJMtSmM2mmHDJIqliOyTcbLdQVTW87g9wOp3geDjCblfxdyQZEcaEAhN6jq6xoMFimuf9OzSfwFfbxnvTNAPZL2Gez4FgIRa0jm1bTPl2/oAry1q4XttOeBkVFkURhYtFxi2zkE4S98kgl0sT08H9OuOacw6cj233DBLehYHHRRLY+nq9hrquoULovnmpYVVu+GB4ZL6J/xbiZitMyGiNawUIpKB2uCWJ26E4Icn61v8XatuNC+2j4xPlnzoo0c92B0KSzecpHsoTF0w3/T2uS9KL4vv4HH4B9sb3xXzFqUkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4dadd9b925c6772bc01ef00ed8348006/263a4/docc-10.webp 480w,
/static/4dadd9b925c6772bc01ef00ed8348006/a6361/docc-10.webp 960w,
/static/4dadd9b925c6772bc01ef00ed8348006/0b34d/docc-10.webp 1920w,
/static/4dadd9b925c6772bc01ef00ed8348006/da28f/docc-10.webp 2880w,
/static/4dadd9b925c6772bc01ef00ed8348006/98b7d/docc-10.webp 3840w,
/static/4dadd9b925c6772bc01ef00ed8348006/2d0e9/docc-10.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4dadd9b925c6772bc01ef00ed8348006/9aebd/docc-10.png 480w,
/static/4dadd9b925c6772bc01ef00ed8348006/a91f8/docc-10.png 960w,
/static/4dadd9b925c6772bc01ef00ed8348006/ac7a9/docc-10.png 1920w,
/static/4dadd9b925c6772bc01ef00ed8348006/f9c26/docc-10.png 2880w,
/static/4dadd9b925c6772bc01ef00ed8348006/5da7e/docc-10.png 3840w,
/static/4dadd9b925c6772bc01ef00ed8348006/66e9f/docc-10.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4dadd9b925c6772bc01ef00ed8348006/ac7a9/docc-10.png&quot; alt=&quot;docc-10&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;일반 함수와 관련된 정보를 작성합니다.
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;#, ##: 대제목, 소제목을 가리킵니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;@Metadata&lt;/code&gt;: 문서를 구성하는 데 필요한 메타 데이터를 선언합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@DocumentationExtension(mergeBehavior: override)&lt;/code&gt;: docc 형식으로 표기하기 위해 필수적으로 선언하는 항목입니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Available(&quot;AppVersion&quot;, introduced: &quot;v3.9.1&quot;)&lt;/code&gt;: 두 스트링 값이 합쳐져 라벨을 형성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;*: 목록 형태로 나열하는 정보입니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;표&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;| param1 | param2 | param3 | .... |&lt;/li&gt;
&lt;li&gt;파라미터를 표 형태로 기입합니다. 마크다운 문법을 사용해 가운데 정렬(|:-:|)합니다. 입력을 원하지 않는 컬럼도 빈 값으로 작성해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;코드 작성&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;```{ programming language }&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;    내용작성
    private var olive: String ...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;스니펫 영역을 지정한 후 해당 코드의 언어를 언급합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/aaa29655d1cc3c396fded3df3af02a88/66e9f/docc-11.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA9ElEQVR42s2SbU/CMBSF9/9/mchWHKgLLzEBATdYN9m6tuuLx9sZSfwkoh+8yUmb5va5p703YpMEEzbCev2EY1kjP7R44RrFkdb8hPpVQCsFrfUgKSW6rkPfaxhjSJbOFXhVouQlIsZuMZ0mKIodVOfQnBTaVtAlCe89LomQV9cVqoojGrMx4uQG+/1mqCzlhxtrDS6NL8A4vSPgaAC2TQMhBPoBaOGd+zlwNmNI0xjb3Qaa/qM3DtZ50hsJoO23Cvln4Gr1iMXiAXm+xbURHHLOSdSU5TLDfH7/a2Bw96fA85P/PzA0JMtSGpvnq4GOxusT+A63ilXjwhkWTQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/aaa29655d1cc3c396fded3df3af02a88/263a4/docc-11.webp 480w,
/static/aaa29655d1cc3c396fded3df3af02a88/a6361/docc-11.webp 960w,
/static/aaa29655d1cc3c396fded3df3af02a88/0b34d/docc-11.webp 1920w,
/static/aaa29655d1cc3c396fded3df3af02a88/da28f/docc-11.webp 2880w,
/static/aaa29655d1cc3c396fded3df3af02a88/98b7d/docc-11.webp 3840w,
/static/aaa29655d1cc3c396fded3df3af02a88/2d0e9/docc-11.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/aaa29655d1cc3c396fded3df3af02a88/9aebd/docc-11.png 480w,
/static/aaa29655d1cc3c396fded3df3af02a88/a91f8/docc-11.png 960w,
/static/aaa29655d1cc3c396fded3df3af02a88/ac7a9/docc-11.png 1920w,
/static/aaa29655d1cc3c396fded3df3af02a88/f9c26/docc-11.png 2880w,
/static/aaa29655d1cc3c396fded3df3af02a88/5da7e/docc-11.png 3840w,
/static/aaa29655d1cc3c396fded3df3af02a88/66e9f/docc-11.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/aaa29655d1cc3c396fded3df3af02a88/ac7a9/docc-11.png&quot; alt=&quot;docc-11&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h3 id=&quot;tutorial-함수-관련-contents-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#tutorial-%ED%95%A8%EC%88%98-%EA%B4%80%EB%A0%A8-contents-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;tutorial 함수 관련 contents 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Tutorial 함수 관련 Contents 만들기&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;File &gt; New &gt; File from Template&lt;/strong&gt;을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 593px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fa5009ef0ad84524a63a95efa674822b/a0904/docc-12.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 117.08333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAAAsTAAALEwEAmpwYAAADeUlEQVR42pWVyW7TUBSGI5Y8AGvY9A14hZY1KwoPUyTWCCFFIJZ0wYIpahkyuI6bxHZsx46deMzUlqQRpUJ0H4tzOOcmDmGo2lr6Vdf2/e75z3CTkyRp7ePH3WeSVMkrinyuZFnK67qab7XMfByHed/vLtXtennXdZ6ZprmWM01tw/c7GIYBDAZ9GA4HOFcf+/2eUBxH6AdddN02ttsOJkmM0+lxJjg6OoTx+At6nreRq9Vq6/yhYRgzyzJnjmOntt1KaWFKC9NeL0kJmAaBn6pqIy2XS6mua/QuEaINZ1EUzihSYqjrDNwgAFqWBY7jAN8TVERCUYvoaAF6HQ+tlo26YWIYxRglPQzjHgZRAl0/hDBOUNM0EeFGo1FHRakyDDKbLLbGCgno0EbDXgdHiUfq4GHfx6OBj5NRAMcHIUyPesjpE0CORtd10DQVOCqyKUDZ3yjy0bA9fF75io8/neKTzyf4tPQNH74/wa3CKTza+Q4vlB/48oM9BxpmE/f3FWi1LOh2O7gqthxQQSzbwddyiK+kCD+3pli0JlhoTvBNYwxv1TGU7Sl+qJgLoDEHknWgwqBpGGiYBnI+OUJOOLUFui0NTa2KgWeS3RC/DEMcj0KgNMCo59O7hWWyipVKGTiPHNE8h4mwHMcxtYMriqTpOir7+xjRs0SkQ4iK4kMQhr+LwlWlVljmkKNicCaOkKHUNrwxwdtiU96QBD4DA/83kC1zhak94G9YBuQI59bbIrcLmADSGtEdS6BJ+Wo0GtDpeJC1yqp4dwbxexb9z8VjN0DAn7TpT5oyAtbmQLbJHyw+wv9BefRGoyEeHh4gjxlrMhmL0aN7OD6e0EA4d5Y5JEtilrPey8SW+RltltJ42nQA1Ekq3as0XSrlU3Vdt0GwJh00t5eWKeGiKJyneXWjbOx4I7Z9VigUbubo2traupY775oXRcd6vUZADbiJs6gWEYq8UoRnxWLxckDuw2pVBlneAzoXuclxJVJxpFFuz3Z2dm5dCsgVpAkB6jXIWoMruwqk51cDctvQIsiOrKwgWQ7Z8qWBmeVSqQh7e5KwuwocDshydAXL1KzYbOp8MPwzegxk69SHlwPqev2O07b5lOaF/PuAq6JGBm7mOInOyuXyxcDtd9s3dncL92VZ3lQUZVOWK39IUfbuSVLpQbFSvEug67kLrl9niANIN0GOdQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fa5009ef0ad84524a63a95efa674822b/263a4/docc-12.webp 480w,
/static/fa5009ef0ad84524a63a95efa674822b/8ede9/docc-12.webp 593w&quot; sizes=&quot;(max-width: 593px) 100vw, 593px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fa5009ef0ad84524a63a95efa674822b/9aebd/docc-12.png 480w,
/static/fa5009ef0ad84524a63a95efa674822b/a0904/docc-12.png 593w&quot; sizes=&quot;(max-width: 593px) 100vw, 593px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fa5009ef0ad84524a63a95efa674822b/a0904/docc-12.png&quot; alt=&quot;docc-12&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Tutorial Table of Contents File&lt;/strong&gt;을 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a17e1a5cb9a788e142163745edafa8fa/66e9f/docc-13.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABQUlEQVR42p2Sa0+DMBSG+f9/SWGwMWGlhRaW6Ad3CYy4xHiZW2JMtr2eU0RRx2IkeXIKJzx9e3E8z4XrXmA8DiGEhEhSCKltnQgFqTJkWZcUaZrasdbawuM4jhFFMZwgGIARIqaGgSKBUs1PeZ6jKIpeuN8ipbQ4g4ELz7ukGa6shNE6gzHaVp69/d4m64oYYwySJLE4PqVj6WQSUTL1KdTa4Ha2QFmWqOsa6/Xawu9nhbxc3/coYQRNDRbZanJsnh4BHGGfY1N3u53tGxZ9oLvCMBwiDANKGNNsX/syLXJcLx9w//KG42GP/f7QCLdbSnQ6oRCiEY5Gv4UFcbPa4vn1W0BsKCELepfcJ+QU1XKGu7pCWVVYEfNlhWS6OL+HfUJG0QFJqexhtRid/l94+u4V54UsGw59e8o/L+tf6QrfAYoW+XnOgMeDAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a17e1a5cb9a788e142163745edafa8fa/263a4/docc-13.webp 480w,
/static/a17e1a5cb9a788e142163745edafa8fa/a6361/docc-13.webp 960w,
/static/a17e1a5cb9a788e142163745edafa8fa/0b34d/docc-13.webp 1920w,
/static/a17e1a5cb9a788e142163745edafa8fa/da28f/docc-13.webp 2880w,
/static/a17e1a5cb9a788e142163745edafa8fa/98b7d/docc-13.webp 3840w,
/static/a17e1a5cb9a788e142163745edafa8fa/2d0e9/docc-13.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a17e1a5cb9a788e142163745edafa8fa/9aebd/docc-13.png 480w,
/static/a17e1a5cb9a788e142163745edafa8fa/a91f8/docc-13.png 960w,
/static/a17e1a5cb9a788e142163745edafa8fa/ac7a9/docc-13.png 1920w,
/static/a17e1a5cb9a788e142163745edafa8fa/f9c26/docc-13.png 2880w,
/static/a17e1a5cb9a788e142163745edafa8fa/5da7e/docc-13.png 3840w,
/static/a17e1a5cb9a788e142163745edafa8fa/66e9f/docc-13.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a17e1a5cb9a788e142163745edafa8fa/ac7a9/docc-13.png&quot; alt=&quot;docc-13&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Tutorial Table과 관련된 정보를 작성합니다.
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Tutorials(name: &quot;&quot;)&lt;/code&gt;: 제목을 입력합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Intro(title: &quot;&quot;)&lt;/code&gt;: 부제를 입력합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Image(source: &quot;&quot;, alt: &quot;&quot;)&lt;/code&gt;: 이미지와 대체 텍스트를 입력합니다&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@TutorialReference(tutorial: &quot;doc:&quot;)&lt;/code&gt;: 값의 맨앞에 &lt;b&gt;doc:&lt;/b&gt;을 입력해 tutorial 파일을 읽어옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f64775230d0e4eeda953f85f902a5d41/66e9f/docc-14.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACL0lEQVR42nVSSWtTYRR9mcxAzJy89725fX2moHtdKRJBQcRCwZUbqyHjwgTqyr3gwpUuxIWU2o2rglJcOWBrk7xMBcGfc7z3q6midXF4w3fvueec7yrTWYDR6BDNVhv3NzbQaDTQarXQbrfR6XQIXfT7PWxuPpLo9XrodLvyjGu4ttlsyr56vQ4lGB/i4OAz0mfTUBQFkUgEsVgM8Xj8F87AMAysVquEc9B1A4lEQoLPo9Go7OFeiel0iMlkAMexUSwWIYSQBKZpUrOAveTBNASc8xfhXrgExzbgOi4sy4ZpmTBogEY9aqWCUqkEZUwKg+AbFVjI5XJQVVWCSV9vvcHe2y3svdvF5as16KYD13Vh2zZsx4LQdaiahkq5LMny+TyUo6Mx5vNATsvl8tCITNMEdKFhd/slPjx7iMH7HdxeXyM1GjxvGStVF8u+I+2zowqrKzJhAcp8PsZsNiKFppygnhAKPH3cx87zJ9h+9QJXajUYjoMlz4O74lMkFrnQfxOSwkKhwJYH4BzZMhNqZEHTVPn99dNHfB98wY/xPq5dv0EubFLowfZXYXo+KRb/EnJ+w+G+vIRsNosy5cEFnOHarZu4e2cdjQf3UPV9WWPTIJOU6XQuOD9yxD18oXwHyoTUMWEmkzlZm3A4jDA9Q6GQBP9PJpNIpVISvDKyblH7B+SlcIbp9PEeLggW72yFbTmUH8fAGUtri737G5wfr85phAy2USofZ7SAtPYfwp+eWGYC3tycxQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f64775230d0e4eeda953f85f902a5d41/263a4/docc-14.webp 480w,
/static/f64775230d0e4eeda953f85f902a5d41/a6361/docc-14.webp 960w,
/static/f64775230d0e4eeda953f85f902a5d41/0b34d/docc-14.webp 1920w,
/static/f64775230d0e4eeda953f85f902a5d41/da28f/docc-14.webp 2880w,
/static/f64775230d0e4eeda953f85f902a5d41/98b7d/docc-14.webp 3840w,
/static/f64775230d0e4eeda953f85f902a5d41/2d0e9/docc-14.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f64775230d0e4eeda953f85f902a5d41/9aebd/docc-14.png 480w,
/static/f64775230d0e4eeda953f85f902a5d41/a91f8/docc-14.png 960w,
/static/f64775230d0e4eeda953f85f902a5d41/ac7a9/docc-14.png 1920w,
/static/f64775230d0e4eeda953f85f902a5d41/f9c26/docc-14.png 2880w,
/static/f64775230d0e4eeda953f85f902a5d41/5da7e/docc-14.png 3840w,
/static/f64775230d0e4eeda953f85f902a5d41/66e9f/docc-14.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f64775230d0e4eeda953f85f902a5d41/ac7a9/docc-14.png&quot; alt=&quot;docc-14&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;@Tutorials(name: &quot;ImageClassfication-CNN-iOS&quot;) {
    @Intro(title: &quot;CNN모델 성능 측정기&quot;) {
        iOS 디바이스 환경에서 CNN모델의 성능을 측정합니다.
        @Image(source: &quot;Intro.png&quot;, alt: &quot;Intro&quot;)
    }

    @Chapter(name: &quot;모델 변경 방법&quot;) {
        @Image(source: &quot;default.png&quot;, alt: &quot;default&quot;)
        @TutorialReference(tutorial: &quot;doc:step-1&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;strong&gt;Tutorial File&lt;/strong&gt;을 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0204df370654a5d18f69ab0a75ca8d93/66e9f/docc-15.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABOUlEQVR42o2SW2/CMAyF+f9/aeuNBlLapLn0uZ0ACXjaJtFnzuz0om4IRqRPSmzr5DjOKo4jRNEbxEagKhVRQikV0FqjrmvUxsCMhDNhrSFsgONFUUDKAqs0jZGmEeSuoISDKjWsVqHQ+wZN8xjv/cxkYpVlCZLkHZJucM7D1MONzrn59skVw+elEMO1VVUFgiAjpYSjpDF2bMmi7T5wOBxwOp1wuVxwPp+x3+//E0zJYRQErfXBgR0dfn5944Zh3W7Dru/7kHsomNAbxvE7NttBUCtqUddBtL9eZ7FJ8EqxR4IlDXRueTu2bClpeHrUOrv5uzjGNVw7YZcOhciR5xl2O3n3Nm3b4ng8/qLruuctC7EeBYvwTZaFPBz+i0s49nQozwRf+X93grnIsM5TFNRy4++LX2Ep+AMV2/nPAPsX9AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0204df370654a5d18f69ab0a75ca8d93/263a4/docc-15.webp 480w,
/static/0204df370654a5d18f69ab0a75ca8d93/a6361/docc-15.webp 960w,
/static/0204df370654a5d18f69ab0a75ca8d93/0b34d/docc-15.webp 1920w,
/static/0204df370654a5d18f69ab0a75ca8d93/da28f/docc-15.webp 2880w,
/static/0204df370654a5d18f69ab0a75ca8d93/98b7d/docc-15.webp 3840w,
/static/0204df370654a5d18f69ab0a75ca8d93/2d0e9/docc-15.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0204df370654a5d18f69ab0a75ca8d93/9aebd/docc-15.png 480w,
/static/0204df370654a5d18f69ab0a75ca8d93/a91f8/docc-15.png 960w,
/static/0204df370654a5d18f69ab0a75ca8d93/ac7a9/docc-15.png 1920w,
/static/0204df370654a5d18f69ab0a75ca8d93/f9c26/docc-15.png 2880w,
/static/0204df370654a5d18f69ab0a75ca8d93/5da7e/docc-15.png 3840w,
/static/0204df370654a5d18f69ab0a75ca8d93/66e9f/docc-15.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0204df370654a5d18f69ab0a75ca8d93/ac7a9/docc-15.png&quot; alt=&quot;docc-15&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Tutorial 파일과 관련된 정보를 작성합니다.
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Tutorial(time: &quot;&quot;)&lt;/code&gt;: 해당 과정을 진행하는데 걸릴 예상 시간입니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Section(title: &quot;&quot;)&lt;/code&gt;: 섹션 제목입니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Steps {}&lt;/code&gt;: 섹션을 수행하기 위해 거치는 단계입니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;@Step {}&lt;/code&gt;: 각 단계를 구체적으로 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;@Tutorial(time: 10) {
    @Intro(title: &quot;모델을 변경하는 방법&quot;) {
        iOS 앱 빌드를 위해 필요한 인증서를 설치하는 단계입니다.
        }

    @Section(title: &quot;포함되어 있는 모델 확인&quot;) {
        @ContentAndMedia {}

        @Steps {
            @Step {
                mlmodel 폴더를 확인합니다.
            }

            @Step {
                원하는 model이 있는지 확인합니다.
            }

            @Step {
                원하는 model이 없으면 추가해줍니다.
            }
        }
    }

    @Section(title: &quot;모델 변경&quot;) {
        @ContentAndMedia {}

        @Steps {
            @Step {
                전 단계에서 확인한 모델을 넣어줍니다.
                @Image(source: &quot;classificationModel.png&quot;, alt: &quot;classificationModel&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;3단계-빌드가-가능한-상태로-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EB%8B%A8%EA%B3%84-%EB%B9%8C%EB%93%9C%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%83%81%ED%83%9C%EB%A1%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;3단계 빌드가 가능한 상태로 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3단계: 빌드가 가능한 상태로 만들기&lt;/h2&gt;
&lt;p&gt;빌드할 수 있도록 트리 구조를 잡습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.docc&lt;/strong&gt; File: 최상단 Docc 파일(Documentation.docc)입니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.md&lt;/strong&gt; Files: DocC를 구성하는 요소입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resources&lt;/strong&gt; Folder: 문서에 노출할 콘텐츠를 모아두는 폴더입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tutorial&lt;/strong&gt; Folder: 튜토리얼 관련 요소를 모아두는 폴더입니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.tutorial&lt;/strong&gt; Files: 튜토리얼을 구성하는 요소입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d0d8c1de34e377d0ba2149520e553f26/66e9f/docc-16.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABLklEQVR42s2RW0/CQBCF+/9/jA/6wIMSojFBEgVRsdJKCJZLQ7q9SPfS2x5miygmRkmf3OxJNpuZL2fmWIP7OwwGPUxnU6zXIda+j5gxRFGEKE6QbDiUlFBK1RJCgHOOLFPI85xU0L8ECwMELIDV7/fwMLyFt1xQY4U4eIdKYuTUoHHcqaqKDIQIQwarc9VBu93Ci+ugIoLgClKKughNgK2Lc5yeneBxNKodCaGQckFjZDAf+giV5QHwpneNbvcSzmQCmWnwlHYlJHIqoruTxtf7BxWHwFf3GUZviwWKQmMTS4g0RZFnZpYPlTsrv4zMKEhmQnFdG874CbO5V+/Q2DfJVQTUZUEqoSnNv4DG3SdwTMDVao6m51soe6Dv/1eg44xg20Msl15jYEl73gO38XFTtl7NxLAAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d0d8c1de34e377d0ba2149520e553f26/263a4/docc-16.webp 480w,
/static/d0d8c1de34e377d0ba2149520e553f26/a6361/docc-16.webp 960w,
/static/d0d8c1de34e377d0ba2149520e553f26/0b34d/docc-16.webp 1920w,
/static/d0d8c1de34e377d0ba2149520e553f26/da28f/docc-16.webp 2880w,
/static/d0d8c1de34e377d0ba2149520e553f26/98b7d/docc-16.webp 3840w,
/static/d0d8c1de34e377d0ba2149520e553f26/2d0e9/docc-16.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d0d8c1de34e377d0ba2149520e553f26/9aebd/docc-16.png 480w,
/static/d0d8c1de34e377d0ba2149520e553f26/a91f8/docc-16.png 960w,
/static/d0d8c1de34e377d0ba2149520e553f26/ac7a9/docc-16.png 1920w,
/static/d0d8c1de34e377d0ba2149520e553f26/f9c26/docc-16.png 2880w,
/static/d0d8c1de34e377d0ba2149520e553f26/5da7e/docc-16.png 3840w,
/static/d0d8c1de34e377d0ba2149520e553f26/66e9f/docc-16.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d0d8c1de34e377d0ba2149520e553f26/ac7a9/docc-16.png&quot; alt=&quot;docc-16&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;4단계-빌드하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4%EB%8B%A8%EA%B3%84-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0&quot; aria-label=&quot;4단계 빌드하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4단계: 빌드하기&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Product &gt; Build Documentation&lt;/strong&gt;을 클릭해 문서를 빌드합니다. 단축키 Ctrl + Cmd + Shift + D 로 빌드할 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 312px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/27b7717d2b26158732558515242bd3f6/f00bc/docc-17.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 190.7051282051282%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAmCAYAAADEO7urAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEX0lEQVR42rVWyW4jVRQNGz4mO8Q3gFixi8SSJZagw/AB2YPojoqmA2LTQQoSIEIcq6NW4jie4ngqz1Uul6fYTuzENt2wopP30OWel5TjkVQvsHRUeeWqk3PvuYOXlm4+b6ytrb0JLC8vvxZWVlbUFRxLzkfP6Z9nspm2ruvFslU2XweFYqFomEYnnU6vjgj5i28azSYxqTzvnlO313WNeqMuXv75kkpmaX1EmM1m15mUQuHQFT8ga/Waa5SM0qvOWYdY4dcjwkwmo1XsCghlMpUkftA1mFD0LnqU0lOPJgjtqk2RaEQex46pWqu6IsNzJdNYTBgMBWU4EqZcPkeNZuNeYrNsEpsizs7PpgizGe02hzJwFKBiqYhku1LKz4j+oD+rsN1pU76Ql1CHfDohuYAY/jGkVGo6ZNumk/iJTCQTlM1llUI3eeR/LiBGz+iThMgHQo4n4pRKp8iqWAr3hT2XkOtQER4Fj2QoHCaQ7h/sk//QrxK/SC3ug/C0dTpJyEWpwXpWJJG/8RzBfWBRDkF42b+cNAUK2X6oknAYITRPmwqtdkuV0CKFKGx0ytyyQcgwBbUYiURgEmoT7s8lRI5ZgEBPz5QNVESPoxKGIIdAWk8TOgfq53UJCKFwhtDJYaVakU7ebvOjgPM0Ie7hOyjEhJoJGW6yqzJ2ElMhcwkhBUrdvK5xnMf4GgwHUwo55DrPw0QqxaYYVGBk83nK5HJUtlhhrbYQhmWJi34f794R5rNp7UW/TbVKQZ6d2tRt16jXucF5q0o392zqjaHbsvk7G++Iv170KJcZI9wO5rUfE9e0cTiQ3wWG9K1/QNr+gB4znvDfG4dDevR8QF/uDegrBZyH9PhgSN8HhuKntKBfDwt3hNquqT3wEX26LaTnF0Gr25Ie/PYPQ9IXv0v6jM8fPJX07oag9xjvPLmm93+4pg+3BHl+FmKV3324Y4yXTVprttuUKxqyYJi4Mkq3V86pWVZ5LRkGt6LFDqMCqlRmGBVb9C77s51iGCU6OgrISDRMQXY3zC5HjyOqn1O8FpJc8H7/gXK+NnJYQQynXUZho1MwsXkNEIYsSgddAjj38Azacaom55VNWsMYxxpF3aHVeNeqyY0z/saMdMbZ1PCYP7FR+dxuqvUwtZ32i8fjhE1oWRbVm3PH2KxCtUYrlYkBy32tQkXoGBLOnnFNCIWq9WIxSiSTo/zBBBAjDa4JMRwwMTjpEnkCOV528F8LCzm8uLyYLRvTNOGkWqPIIcb6fftk4QrAtIEyDFjkK8ShYtC62XwgxFSfIXQWPfIFQ5yx5UbhXELnt02MFUKlU8DTuZxAY0yhPkbIh3UUNof9yrTKgtUKLmbBQ1dgxOOKM16GCVP4G6Ykk8mHI0L+1aA1W031y4GlqzpEZwAwyLmH7YYSQWc44BUqrq6vqGgU7wi3tp6+/ez5s493d3c/2tvb8/h8Po/X5/V4vV4Pzrguws7ODt75ZHNz862l/+PzL4Mzq1osQVleAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/27b7717d2b26158732558515242bd3f6/19a2b/docc-17.webp 312w&quot; sizes=&quot;(max-width: 312px) 100vw, 312px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/27b7717d2b26158732558515242bd3f6/f00bc/docc-17.png 312w&quot; sizes=&quot;(max-width: 312px) 100vw, 312px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/27b7717d2b26158732558515242bd3f6/f00bc/docc-17.png&quot; alt=&quot;docc-17&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h1 id=&quot;-열심히-만든-docc-공유하려면-일련의-과정을-거쳐야-해요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%97%B4%EC%8B%AC%ED%9E%88-%EB%A7%8C%EB%93%A0-docc-%EA%B3%B5%EC%9C%A0%ED%95%98%EB%A0%A4%EB%A9%B4-%EC%9D%BC%EB%A0%A8%EC%9D%98-%EA%B3%BC%EC%A0%95%EC%9D%84-%EA%B1%B0%EC%B3%90%EC%95%BC-%ED%95%B4%EC%9A%94&quot; aria-label=&quot; 열심히 만든 docc 공유하려면 일련의 과정을 거쳐야 해요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📑 열심히 만든 DocC, 공유하려면 일련의 과정을 거쳐야 해요&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1단계-docc를-package로-분리하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EB%8B%A8%EA%B3%84-docc%EB%A5%BC-package%EB%A1%9C-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1단계 docc를 package로 분리하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1단계: DocC를 Package로 분리하기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;새로운 프로젝트를 생성한 뒤, Package를 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empty&lt;/strong&gt;를 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d91d555bc082a263e9c08f9e40a7aeae/cae0a/docc-18.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB8klEQVR42o2SW27aQBSGWUe7jqrbKG0ekq2wAx4QlUDsgktMhaoKqYIWbIOvUNvwAFSVKBSFOFwafP97xiQNSovkkT6fGevMpzNnJvWhVnv9ufWp0W5/uW632pwgCJyqqpwkSVy/34+RpCeOa4lTFCWeC4LICaJ4Tfs+NhqNVyn6XBjfDOiaHhEYWSNMJtOY6XRCsPnkLJZlwTStkOU1m810qlKpvOU7PLqdrtf52vEVWfEt04wxHxiPx38ZEdaIsCxf0zS/1+v5VKlLFaNarb5J0eedKIpMGJEQiizh5+oWNxsHzuEA3/dxOg5ugHsXuL3bQ5ZlkAwkDQeDAWq1WvofIfUJ67sttg7bHuH5iKLjv90usVCCbdsIqLKQNocRkxxFbP0k3CUTylThar3B93UExwvw2/HpiEE83zvHyMb2REg3HOq6fv7IN/YGi00E3/PgOC5c14VHcxaDIHiocJusQpbAjnPar2ddxH0IrOw9XWDCHs5mM8zncywXCyweWS7juFzM0TJ+oa3/gK4q6P1P2O/1wXf5iKRg74nBxOcYqBKGmgzWN4mEVGU4HA5Rr9fTqXK5fMGSREGMBF6Aqmkw49dvnsUwCIpMwvpIhIZhHIWZTOZlPp+/yuVyl4z3hcJlqVRKRIFyHykWi1fZbPbFH55WXk6H6ExqAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d91d555bc082a263e9c08f9e40a7aeae/263a4/docc-18.webp 480w,
/static/d91d555bc082a263e9c08f9e40a7aeae/a6361/docc-18.webp 960w,
/static/d91d555bc082a263e9c08f9e40a7aeae/0b34d/docc-18.webp 1920w,
/static/d91d555bc082a263e9c08f9e40a7aeae/da28f/docc-18.webp 2880w,
/static/d91d555bc082a263e9c08f9e40a7aeae/61a5c/docc-18.webp 3024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d91d555bc082a263e9c08f9e40a7aeae/9aebd/docc-18.png 480w,
/static/d91d555bc082a263e9c08f9e40a7aeae/a91f8/docc-18.png 960w,
/static/d91d555bc082a263e9c08f9e40a7aeae/ac7a9/docc-18.png 1920w,
/static/d91d555bc082a263e9c08f9e40a7aeae/f9c26/docc-18.png 2880w,
/static/d91d555bc082a263e9c08f9e40a7aeae/cae0a/docc-18.png 3024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d91d555bc082a263e9c08f9e40a7aeae/ac7a9/docc-18.png&quot; alt=&quot;docc-18&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Package 이름을 설정하고, 저장할 위치를 정해 &lt;strong&gt;Create&lt;/strong&gt;을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8c290ff4c3d8f04a00f68d6c6af473c3/cae0a/docc-19.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAACM0lEQVR42o2S227aQBCGeY72Oaq+RmlzkbwKb8AFohKIt0hCSIR6gZRAAj4bO2AMJNhRpLTkAFgcbRz77+yiVGolGiz9u+PZ2W9nZydxVjz7XD2vli9rlye1aq0kimJJ1/WSoigkaYuUkqqqfBZFqSRK0gnt+1Eulz8laNiz2hZMw4zNpolutwvXdXfSYDCAbdvodOzIcRxUKpVk4ujo6KtQF9CoN9b1q3qoaVpoGEZIWXKbqdlskm8jZrO1jc8IKctQluWAfDg+Pv6SoOGbJEkMGBMQqqah1W7TyRaGw1+YTqd4eRnDvR/CIT08TriPiWXH9hI0ur6+RrFYTP4DvOJAy6ZrOwPM5zO8vr5isfKx9ANEMeCHQByTQV+/3wfV7n/AOhRVhd3vceB4NMJs6aPj3GP08oz5YonxdIbVasmBvV7vfSC9Hp1MQNfBgjIMIuD25yMCgvhhhNuHJzwOh3+AgiCwa0emaW4BKjJuuhbcOxez2RT+OsTT2EO4Drj97E3hTSaIomi3K8sEbNtt3FFbeJ4H3/d5HdkcBFRHsldBiJsR0Or0IG0DEiwmKJ0o4eKiilqtyq9DwbwM1D5c1CI85ryu0QPqtC4z399ARVYgNAQO1HUN1GME0ShY5UAmBmJ6+2/qKkzT4I/IMmy1Wjg9PU0mDg8P91gGkijFoiCiaRjo8O7vvCsGYVlTU0eWZW2AqVTqYzabPchkMvtM33O5/UKhsJNyFPumfD5/kE6nP/wGNoVTASgw12oAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8c290ff4c3d8f04a00f68d6c6af473c3/263a4/docc-19.webp 480w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/a6361/docc-19.webp 960w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/0b34d/docc-19.webp 1920w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/da28f/docc-19.webp 2880w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/61a5c/docc-19.webp 3024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8c290ff4c3d8f04a00f68d6c6af473c3/9aebd/docc-19.png 480w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/a91f8/docc-19.png 960w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/ac7a9/docc-19.png 1920w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/f9c26/docc-19.png 2880w,
/static/8c290ff4c3d8f04a00f68d6c6af473c3/cae0a/docc-19.png 3024w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8c290ff4c3d8f04a00f68d6c6af473c3/ac7a9/docc-19.png&quot; alt=&quot;docc-19&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Package가 생성되었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6ec77a11623b77ca55f9dc7d598adcb0/66e9f/docc-20.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA8klEQVR42uWSaYvCMBCG8///lx880AWP9Ug92lW2raawNkeb43XsoiAsbPWrA28ShuTJO8OwwbCL0agHzmfIjwXyU4FSltBaQ0oJRbsx5i6lVJOvKoO6rkmW8hqHwx5ploH1Pwbo9jqI4whaGYg8R12eAWfRNnzwSJIYydcebLGYYD4fN0DrHBTB3PkHnlwG71sBHb1L029kRwHG+SeWyykB16ioJFkSyNrrLSCEdg7pYyFOjdhqNWuA220EQ/3QuoLRBpbO1gU4MvmfLC1FIR6Bu11Evfh1FJ6U838C11RhwCvh3xx4m8PNhr8MvM7hDXgB731Vq8aTNZIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6ec77a11623b77ca55f9dc7d598adcb0/263a4/docc-20.webp 480w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/a6361/docc-20.webp 960w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/0b34d/docc-20.webp 1920w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/da28f/docc-20.webp 2880w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/98b7d/docc-20.webp 3840w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/2d0e9/docc-20.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6ec77a11623b77ca55f9dc7d598adcb0/9aebd/docc-20.png 480w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/a91f8/docc-20.png 960w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/ac7a9/docc-20.png 1920w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/f9c26/docc-20.png 2880w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/5da7e/docc-20.png 3840w,
/static/6ec77a11623b77ca55f9dc7d598adcb0/66e9f/docc-20.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6ec77a11623b77ca55f9dc7d598adcb0/ac7a9/docc-20.png&quot; alt=&quot;docc-20&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Pacakge.swift를 작성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5891524c99a6832ac870ab6fc677839a/66e9f/docc-21.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA3klEQVR42u2SW2+CQBBG+f8/qw+mfbBNtRcRFNCES8ouCoLLLnC6EH22MT72y5xkJ5mcTDbjvL3PWSxfCbYuQgiCuGSX1sTpkVwUVKeKpmlQSk2M77quaVuF1tpiOKszSZqQ5znO8/yF2eyJMPSoqhMHeUArNQ0aY/hrwjAgiiIc1/1gtVraZkPdaEohUceSrtV0A5j+NtoO7vc74iTB8f1v1uvPSdj3PcMwXABbNxljuo4sy/ixX+R43tdFuJ1E92RcRBYSKfLHCYtRKMW/8BHC6x0GgX+3sLNncxX+AnA/VVieNJsbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5891524c99a6832ac870ab6fc677839a/263a4/docc-21.webp 480w,
/static/5891524c99a6832ac870ab6fc677839a/a6361/docc-21.webp 960w,
/static/5891524c99a6832ac870ab6fc677839a/0b34d/docc-21.webp 1920w,
/static/5891524c99a6832ac870ab6fc677839a/da28f/docc-21.webp 2880w,
/static/5891524c99a6832ac870ab6fc677839a/98b7d/docc-21.webp 3840w,
/static/5891524c99a6832ac870ab6fc677839a/2d0e9/docc-21.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5891524c99a6832ac870ab6fc677839a/9aebd/docc-21.png 480w,
/static/5891524c99a6832ac870ab6fc677839a/a91f8/docc-21.png 960w,
/static/5891524c99a6832ac870ab6fc677839a/ac7a9/docc-21.png 1920w,
/static/5891524c99a6832ac870ab6fc677839a/f9c26/docc-21.png 2880w,
/static/5891524c99a6832ac870ab6fc677839a/5da7e/docc-21.png 3840w,
/static/5891524c99a6832ac870ab6fc677839a/66e9f/docc-21.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5891524c99a6832ac870ab6fc677839a/ac7a9/docc-21.png&quot; alt=&quot;docc-21&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;DocC를 구성하는 요소(markdown) 파일을 기술합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/66e9f/docc-22.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABqElEQVR42nWSCW7bMBBFff8D9AA9R6pYslauWtBYrVV3cYPEtJPIRTc7gX9nmMoJWoXAB0fC6GnI/ydB8AZBcIY8TaG1RlmWXtpYGJK1hnaWptr+rQ312FMvv8/z3GsynQYIwwBSylNDVVVomhp1XaOsahjL7yv/3DSNF9fPpZTymoThOaLoHEorP0WeZxBC0F5ASYG20fjQVng/H0AErlhPMB6AB2JN4jjCbBbC0HG1NojjGEUh8K69QDn/hNfhNV6dOURvb/Gtv4HbbOG2Pdq29aBRYByH0EpDCY0onEEKCbde4/OXFdL5FunyHh+3e/z6+QO/D/fYPxzRdQt/PTz1+IR0XL7sLMtQUeP12sFdLnH4vsGwjsfjaV8s/gfyVZ2A7DC7ykBudG6DuxuHh8MeY2sMODphkiQe3nUdVquv6Psddrtn6nssr3o0F+2jOS+aYskUgqZZSqYUyMjtjGqemH/AWRtUKOvj9LLLZIqSGrLQiKOEQi4gMmrIFQqqjbT00RNgyOgokGHRbOpDqZVBmtCElEGOjpQKgnZOwL9BfgSxSq8B+Af7cPozeyemjAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/263a4/docc-22.webp 480w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/a6361/docc-22.webp 960w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/0b34d/docc-22.webp 1920w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/da28f/docc-22.webp 2880w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/98b7d/docc-22.webp 3840w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/2d0e9/docc-22.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/9aebd/docc-22.png 480w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/a91f8/docc-22.png 960w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/ac7a9/docc-22.png 1920w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/f9c26/docc-22.png 2880w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/5da7e/docc-22.png 3840w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/66e9f/docc-22.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/ac7a9/docc-22.png&quot; alt=&quot;docc-22&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;빌드 단축키 Ctrl + Cmd + Shift + D를 실행해 문서를 빌드하고 정상 동작하는지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/dcfd3df4a3343186d3d5544ecd525701/eaa1b/docc-23.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB/klEQVR42oWSX2vTUBjG8xVkoENFFEWogk47NrfWghdiV9QLYeDUDsFdqhfzVv00Xnm/G0XBKWVNs3Zdjd1qrWNdl/5J2vRP0iYnyWNOsBkdbfrCDw7POfx4z3sOs8PHwfOb2DsQIFRECFUJJaGGqiij3uxAktsQ603UjiH2aRytac58WFvHxy8c8octlCQdRVHDz3wFfKGC3H79iGLD3uuhLJso1YkLPf/nQEW+qGC/poFZ+/wJ7FYSZamNRseC1NKxnS2ATfLY4DJgU7+Q2MpiYzOD33tlFCtNVOUepDZxKDc0/D1UUSgpEKQeGI79ht2dJBS1A9MCiGGi2WrbdKATw4FmFE0nDnRt2IcpGrHQ1U2Hnm6BSSS+I5tNodtVQMuy7C4lCbIswzRNN+szrph43O5wNz0gpPRlhmFgdfUNwuF7iEQiWHmx4u4NFaZSMeRy21BV5X9kuWJaOiF4+OA+zp0+hYsXzsLvnwKxs5HCTCbhLbRntrT4CNd8lxCYuYFQKGBn+mhhOh33FNJunjxexNz0FELzMwgG5ryFLLs+MMNhwudPl3D39iyCs34E5295X5m1v824GdIr3/Sdx/XLZxC5M+081EhhLPZ14NsMm+FCeAGTJycxcWICV31XYJAxQo774XZ4/K/Rbt6/fYfl6DKiz6J4/fKVZ4f/AHWepGHE/jklAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dcfd3df4a3343186d3d5544ecd525701/263a4/docc-23.webp 480w,
/static/dcfd3df4a3343186d3d5544ecd525701/a6361/docc-23.webp 960w,
/static/dcfd3df4a3343186d3d5544ecd525701/0b34d/docc-23.webp 1920w,
/static/dcfd3df4a3343186d3d5544ecd525701/da28f/docc-23.webp 2880w,
/static/dcfd3df4a3343186d3d5544ecd525701/49f40/docc-23.webp 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dcfd3df4a3343186d3d5544ecd525701/9aebd/docc-23.png 480w,
/static/dcfd3df4a3343186d3d5544ecd525701/a91f8/docc-23.png 960w,
/static/dcfd3df4a3343186d3d5544ecd525701/ac7a9/docc-23.png 1920w,
/static/dcfd3df4a3343186d3d5544ecd525701/f9c26/docc-23.png 2880w,
/static/dcfd3df4a3343186d3d5544ecd525701/eaa1b/docc-23.png 3456w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dcfd3df4a3343186d3d5544ecd525701/ac7a9/docc-23.png&quot; alt=&quot;docc-23&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;2단계-분리된-프로젝트에서-docc를-웹페이지에-배포하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EB%8B%A8%EA%B3%84-%EB%B6%84%EB%A6%AC%EB%90%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-docc%EB%A5%BC-%EC%9B%B9%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;2단계 분리된 프로젝트에서 docc를 웹페이지에 배포하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2단계: 분리된 프로젝트에서 DocC를 웹페이지에 배포하기&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;GitHub에 별도 레포지토리를 생성합니다.&lt;/li&gt;
&lt;li&gt;GitHub에 푸시할 데이터 구조를 잡습니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sources&lt;/strong&gt; Folder: docc와 동일한 이름의 폴더를 생성합니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;docc&lt;/strong&gt; File: 문서에 노출할 콘텐츠를 묶어주는 document package입니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.md&lt;/strong&gt; Files: 문서에 노출할 콘텐츠, 각 스킴 및 인터페이스 단위로 마크다운 파일을 작성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swift&lt;/strong&gt; Files: 실제로 사용하는 스킴 및 인터페이스가 정의 되어 있는 파일입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Package.swift&lt;/strong&gt;: 기본이자 필수 영역으로, 패키지 관련 디스크립션을 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;build-docc.sh&lt;/strong&gt;: docc를 읽어서, 웹에 호스팅하기 위한 환경을 만드는 빌드 스크립트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;build-docc.sh를 작성합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;##!/bin/sh&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# docc 빌드&lt;/span&gt;
xcrun xcodebuild docbuild &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-scheme&lt;/span&gt; ImageClassificationSupport &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 문서화할 스킴 이름&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-destination&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;generic/platform=iOS Simulator&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 빌드 대상 플랫폼&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;-derivedDataPath&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token environment constant&quot;&gt;$PWD&lt;/span&gt;/.derivedData&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 빌드 산출물 저장 경로&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# doccarchive를 static-site로 변환&lt;/span&gt;
xcrun docc process-archive transform-for-static-hosting &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token environment constant&quot;&gt;$PWD&lt;/span&gt;/.derivedData/Build/Products/Debug-iphonesimulator/ImageClassificationSupport.doccarchive&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    --output-path &lt;span class=&quot;token string&quot;&gt;&quot;.docs&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    --hosting-base-path &lt;span class=&quot;token string&quot;&gt;&quot;ImageClassification-Support&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;
&lt;p&gt;빌드 스크립트 실행 및 쓰기(write) 권한을 부여합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;터미널에서 build-docc.sh가 존재하는 경로로 이동합니다.&lt;/li&gt;
&lt;li&gt;chmod +x build-docc.sh 명령어를 입력합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;./build-docc.sh&lt;/code&gt;  명령어를 입력합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GitHub Actions 스크립트를 생성합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project Folder &gt; .github &gt; workflows 경로로 이동합니다.&lt;/li&gt;
&lt;li&gt;deploy-docc.yml 파일을 생성합니다.
&lt;ul&gt;
&lt;li&gt;파일 이름을 자유롭게 부여합니다.&lt;/li&gt;
&lt;li&gt;확장자를 yml로 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다음과 같이 스크립트를 작성합니다.
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yml&quot;&gt;&lt;pre class=&quot;language-yml&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deploy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;docc

&lt;span class=&quot;token key atrule&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;main&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 타켓 브랜치&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;pages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write
  &lt;span class=&quot;token key atrule&quot;&gt;id-token&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; write
  &lt;span class=&quot;token key atrule&quot;&gt;contents&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; read

&lt;span class=&quot;token key atrule&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; macos&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;14&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Checkout Repository
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/checkout@v4
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;fetch-depth&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Build Docs
        &lt;span class=&quot;token key atrule&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./build&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;docc.sh
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Setup for Github Pages
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; pages
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/configure&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Upload pages artifact
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/upload&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;artifact@v3
        &lt;span class=&quot;token key atrule&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; .docs
  &lt;span class=&quot;token key atrule&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;needs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; build
    &lt;span class=&quot;token key atrule&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Deploy to GitHub Pages
        &lt;span class=&quot;token key atrule&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deployment
        &lt;span class=&quot;token key atrule&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actions/deploy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages@v4
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; github&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pages
      &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; steps.deployment.outputs.page_url &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;모든 데이터를 GitHub에 푸시합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Action 탭에서 deploy-docc를 선택합니다.&lt;/li&gt;
&lt;li&gt;Run workflow에서 작업한 브랜치를 선택합니다.&lt;/li&gt;
&lt;li&gt;Run workflow를 선택합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/76cc6ba506e65c5591a2b6d2bc25c814/862aa/docc-24.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABbUlEQVR42qWRbWrCQBCGcyilv4rVkBgpUq/RHqL0FBI8RKH/vYWQFkuhYqqNyW4S853N7nQ2frZQf+jAw8zszPsysEqrdf101+/bg8Fg0jV6Vq93axmGYamajnQRra41HbOqWZqs8U3XdavTUS1V71rt9s2kedWcNxqNR+X1bTqK4hQ+Z3O2WH6Ds/Lga+mA61Hw/RAI9Wuoj9AAPI8AIRTfAnBcAivsHddjaVbA9P3DVFBsAgZjVYGJS7x1wsOs4AnjPK2AZ0i6o+R8nZU8KRjf7SNSC3meDxXbXoxkwzmgnIMQAsKcgZuU4KYl0IKDlwsghYCgFDDDy5ZhAgHO5a4EdUx6RFFkKo7j1obyyG2ulw4hIEoZxNlm7EYeZGW2nez36mEcx6aCrvsLD4YHqopDkqQ1st4YbWZHcdrwVIg/Thcb/hO/DM2NIc+R6kzy7acM9xcef8pFF9q2/UApHRNCnpGXc0C91I7R6/4HYR0foPKMpu0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/76cc6ba506e65c5591a2b6d2bc25c814/263a4/docc-24.webp 480w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/a6361/docc-24.webp 960w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/0b34d/docc-24.webp 1920w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/da28f/docc-24.webp 2880w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/98b7d/docc-24.webp 3840w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/7a560/docc-24.webp 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/76cc6ba506e65c5591a2b6d2bc25c814/9aebd/docc-24.png 480w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/a91f8/docc-24.png 960w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/ac7a9/docc-24.png 1920w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/f9c26/docc-24.png 2880w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/5da7e/docc-24.png 3840w,
/static/76cc6ba506e65c5591a2b6d2bc25c814/862aa/docc-24.png 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/76cc6ba506e65c5591a2b6d2bc25c814/ac7a9/docc-24.png&quot; alt=&quot;docc-24&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;GitHub Actions에서 workflows가 제대로 동작하고 있는지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/133e643452729f0e7968c80e89f78eb6/862aa/docc-25.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABQ0lEQVR42tWRXUrDQBDHcyZp8UlqG5ImiJpr6CHEU5TQQwi+9xaFVSqC0rU1H7vRfHXTzWacTW2hiC/1yYHfzuzM/v8MrNHrndxenl9Qz/OmQ8clrntGHMchpmUjQ8Rqa8vGbFrE0jX2bNsmg4FJTHtI+v3Tafe4Oz/qdG6Mh8fZOMtLeHmdy8XyHYIwhrdlAFHMgSefwHkCDOFJ0tYx9hlD+AcEEYMwZphjWa4qmD09+waKfcCQsq4wKQ1LS5UKqQqpVFnDHrqfrtZqta7V9j2itSCEGBmULsb6ohSgXM8AwrQAmhQQ5hXwSgFDYqE2NeawqACNoWmaFtRJrcuyzDeCIGoN9ZKwDXyEB/wazY9Zq83z3DfQdbchHB7/zNDfGCqB1Acivj9ltNtw71P+siGl9JpzPmGM3SH3h4B6rZ2g19UX+r4gd4IG9B8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/133e643452729f0e7968c80e89f78eb6/263a4/docc-25.webp 480w,
/static/133e643452729f0e7968c80e89f78eb6/a6361/docc-25.webp 960w,
/static/133e643452729f0e7968c80e89f78eb6/0b34d/docc-25.webp 1920w,
/static/133e643452729f0e7968c80e89f78eb6/da28f/docc-25.webp 2880w,
/static/133e643452729f0e7968c80e89f78eb6/98b7d/docc-25.webp 3840w,
/static/133e643452729f0e7968c80e89f78eb6/7a560/docc-25.webp 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/133e643452729f0e7968c80e89f78eb6/9aebd/docc-25.png 480w,
/static/133e643452729f0e7968c80e89f78eb6/a91f8/docc-25.png 960w,
/static/133e643452729f0e7968c80e89f78eb6/ac7a9/docc-25.png 1920w,
/static/133e643452729f0e7968c80e89f78eb6/f9c26/docc-25.png 2880w,
/static/133e643452729f0e7968c80e89f78eb6/5da7e/docc-25.png 3840w,
/static/133e643452729f0e7968c80e89f78eb6/862aa/docc-25.png 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/133e643452729f0e7968c80e89f78eb6/ac7a9/docc-25.png&quot; alt=&quot;docc-25&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;완료되면 노란색 아이콘이 초록색으로 바뀝니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/58cb2f8b47d223fb49cc23e4102ede60/862aa/docc-26.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABNUlEQVR42uWRb0rDQBDFc6aiFJtokpZokaDn0EOIpyihhxD83lsEVlGKqLFpmj+1bdpsujsZZ5PaAxS/OfBjHsy8NwurmbZ5f+leBX332u9duMx2+swwu0y3HKabDuuQ7pz2SHdrrZuNNqifGDbT7XN2Zll+u3382Wod3Wn+0/Nwscxx/PYuPoIQo1mCX+EUJ9MZxukck2ze9DQjnWGcELWeYxSnGCVEnIgNL/HldexpYRh5SCUklNRA8b1YwnpTgAQEiTtI0xBKIWBdcJAS4HefUF7knA+0YDIZYlMCaKeqKiw4p2FJRyQqHx3DLdFoieVW1FrtKsgnVMBqtfK0KIr3gXh41d48zz2NUutAdfz/BHpNIHBCHgjffcpg/8I/+5QgCG6zLBulafpAPB4C+ZV3RFk3P4w/IE6vJpL6AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/58cb2f8b47d223fb49cc23e4102ede60/263a4/docc-26.webp 480w,
/static/58cb2f8b47d223fb49cc23e4102ede60/a6361/docc-26.webp 960w,
/static/58cb2f8b47d223fb49cc23e4102ede60/0b34d/docc-26.webp 1920w,
/static/58cb2f8b47d223fb49cc23e4102ede60/da28f/docc-26.webp 2880w,
/static/58cb2f8b47d223fb49cc23e4102ede60/98b7d/docc-26.webp 3840w,
/static/58cb2f8b47d223fb49cc23e4102ede60/7a560/docc-26.webp 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/58cb2f8b47d223fb49cc23e4102ede60/9aebd/docc-26.png 480w,
/static/58cb2f8b47d223fb49cc23e4102ede60/a91f8/docc-26.png 960w,
/static/58cb2f8b47d223fb49cc23e4102ede60/ac7a9/docc-26.png 1920w,
/static/58cb2f8b47d223fb49cc23e4102ede60/f9c26/docc-26.png 2880w,
/static/58cb2f8b47d223fb49cc23e4102ede60/5da7e/docc-26.png 3840w,
/static/58cb2f8b47d223fb49cc23e4102ede60/862aa/docc-26.png 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/58cb2f8b47d223fb49cc23e4102ede60/ac7a9/docc-26.png&quot; alt=&quot;docc-26&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;9&quot;&gt;
&lt;li&gt;빌드가 완료 되면 Settings → Pages로 이동합니다. 배포가 완료된 주소(Base URL)를 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e794e17022466f4e80d992fcd25ef6df/862aa/docc-27.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABhUlEQVR42p2R307CMBTGeSaD4YooLEOWID6HPoTxKcjCQ5h4z1uQ7ALlRmccsD+wdd26rjueUzfhAknkJL+0pz3n69e21b3qPt2ORu7deDwfDi3HsizHNE3HMG8cc0AMHMMw6zVi4JiU43q/bzgG1vR61/NO5/Ljot1+bC3ellPGM3h3P+WXt4bV2gdvtQGP5psA8SEIIwiiSI9+uEUiDe2vfV0jeS5g8bq0W5xzG35CIIoQQigppcJE+VGsEl6oJJMqE1KJolBN3QHUC3meT1pJkkwpUUrJqqq0Mh4CRVFAISV46HiXcBBFqcGDqBaotoF6qQ+17F9BDFmPuMEAXaJgCd5mCzsmAF1CLkisgiOhexljfwiyVLuUKBjuGNAbZ1mGriW6VHBE87RgmnItQFcLgy3EKBonKUQxR7c5Olf/E5RlCWWNj7+INZDgITHL9HuS01OCdv0pOVIS+NBlM29ygD2HezV5/SmTow7PiL1D13UfwjCcBUHwjLycA/ZT7wy17r8BMukeKMUNutkAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e794e17022466f4e80d992fcd25ef6df/263a4/docc-27.webp 480w,
/static/e794e17022466f4e80d992fcd25ef6df/a6361/docc-27.webp 960w,
/static/e794e17022466f4e80d992fcd25ef6df/0b34d/docc-27.webp 1920w,
/static/e794e17022466f4e80d992fcd25ef6df/da28f/docc-27.webp 2880w,
/static/e794e17022466f4e80d992fcd25ef6df/98b7d/docc-27.webp 3840w,
/static/e794e17022466f4e80d992fcd25ef6df/7a560/docc-27.webp 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e794e17022466f4e80d992fcd25ef6df/9aebd/docc-27.png 480w,
/static/e794e17022466f4e80d992fcd25ef6df/a91f8/docc-27.png 960w,
/static/e794e17022466f4e80d992fcd25ef6df/ac7a9/docc-27.png 1920w,
/static/e794e17022466f4e80d992fcd25ef6df/f9c26/docc-27.png 2880w,
/static/e794e17022466f4e80d992fcd25ef6df/5da7e/docc-27.png 3840w,
/static/e794e17022466f4e80d992fcd25ef6df/862aa/docc-27.png 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e794e17022466f4e80d992fcd25ef6df/ac7a9/docc-27.png&quot; alt=&quot;docc-27&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;10&quot;&gt;
&lt;li&gt;Base URL에 /documentation/{ DocC PackageNamed }을 조합하여 브라우저에 입력합니다. DocC가 GitHub Pages에 잘 배포 되었는지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c5ed9ec0d9a13b4092562d3b570406db/862aa/docc-28.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABxklEQVR42q2SzUoCURTHpV2LFtGiF2gTiCvpY2oTJc1EH5hljW2CzHTQRT5A9AYtopp6AKMWgfseogzuIgmSBMmPUfBjRu9cT+dOjlkQRXTgx7lzOed//sM9jtnZmcMFUcwuLy0++Hw+4vV6iSSJRJQk4vfLZG3dj3mTbMocmciBAAlgDvCMyFtbZF6cexAmJ18EYfrAkcvlLgCjUqnQbDYLmlYCXW9AOp2GSESBWCwG0WjUyoqiQCgUgj0kHA5DMBiEnd0g3N/fUa6B/aqj0Wio8B5NhNlkMhk2NSUwj8fDJEmy4GdBEJjb7bZwOp3M5XKxVCrFe6FWq506qtWq5dA0Tdput6EXXdehVCpBs9kEHAz1eh1oqwWUUgv8KzAMAxhjlkPUUruC9qUdtigP3hyP74MwMQbX11fQMdBbTjsOfyfYQlfrqyswPDQAJyfH3SF23Z8Ewzvb4BodgXP17H8cbvi8MNjfB2fHR++ClH4rqHYEDcS0sYP34qOY8f24OTE2bl4mEtYdDjF76o3Oo5x+6/Cryx/iwyEuo6Jp2m2hULjJ5/PJYrHYhX8/Pj0nX1/zyXK5nMQ1sTKu0iew/wbzLWpF3gB2SOD4QUtowgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c5ed9ec0d9a13b4092562d3b570406db/263a4/docc-28.webp 480w,
/static/c5ed9ec0d9a13b4092562d3b570406db/a6361/docc-28.webp 960w,
/static/c5ed9ec0d9a13b4092562d3b570406db/0b34d/docc-28.webp 1920w,
/static/c5ed9ec0d9a13b4092562d3b570406db/da28f/docc-28.webp 2880w,
/static/c5ed9ec0d9a13b4092562d3b570406db/98b7d/docc-28.webp 3840w,
/static/c5ed9ec0d9a13b4092562d3b570406db/7a560/docc-28.webp 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c5ed9ec0d9a13b4092562d3b570406db/9aebd/docc-28.png 480w,
/static/c5ed9ec0d9a13b4092562d3b570406db/a91f8/docc-28.png 960w,
/static/c5ed9ec0d9a13b4092562d3b570406db/ac7a9/docc-28.png 1920w,
/static/c5ed9ec0d9a13b4092562d3b570406db/f9c26/docc-28.png 2880w,
/static/c5ed9ec0d9a13b4092562d3b570406db/5da7e/docc-28.png 3840w,
/static/c5ed9ec0d9a13b4092562d3b570406db/862aa/docc-28.png 5344w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c5ed9ec0d9a13b4092562d3b570406db/ac7a9/docc-28.png&quot; alt=&quot;docc-28&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;3단계-분리된-프로젝트를-spm으로-가져오기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EB%8B%A8%EA%B3%84-%EB%B6%84%EB%A6%AC%EB%90%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-spm%EC%9C%BC%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0&quot; aria-label=&quot;3단계 분리된 프로젝트를 spm으로 가져오기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3단계: 분리된 프로젝트를 SPM으로 가져오기&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;작업은 메인 프로젝트에서 진행합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;&lt;b&gt;File &gt; Add Package Dependencies...&lt;/b&gt;를 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 425px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9bb5114cea24e99b7a4b2244a7d79f99/df70d/docc-29.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 163.29411764705884%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAD0klEQVR42qVV225aRxSlsvoFlfpQKQlS+9L8jx/T9i/y0FZRVcm/kUKcuiJqoQkKTlIZG1nYxvjC7QDmdm5wDqR4nJfW2Mrs7jUwRtQ4nCRISzMchjVr77X3PqFYLLaUz+e/PDw8vLu5ufn1hyCTydzNZrNfLS8vL4W2ttZvFYqlfv7giCzbuWBcWrZ7aTtTWM74mYIzhmk5lx3TxnrRNi0yqrVBIpH4IhSPp8Ku1RCuWSfXPJG+0yLAs5vUmwD7vovn2DcUhp5F4rVDQ9+Sb/7uku92zjja26F0Kh5OZpvi11cGrf1lyN826vRkq0mRVIEerRfp8YsyRdfL9Mu6QZGXNXrIK/Bk26bnh0P6c9eTiT2fXuZdEYtFbodS8Yfhn5NvxIMXRN8/PZcPkudY6afnI/rh2Yh+TI7ofuKcvls9p28fYR2p/TfRfyf4R957/Jbu/34mXinCVCrcqBuiWimRbbbkwO+S17Wp51pX8Bh+dz56Tkd6rkmu1RKRyISwUCiKbHaHjo6OZbFYomarTY7rku04U9jzwSZKBtXqdaFyCMJisSj29/cpl9uX29vbVKkY1O12yWGiRTBNU1qWTY1GY0wYj8fD1WpVMMgwDFkul6ler/PtdiB0Oh2Jtd1uj0MGIRMJVklc3BJkvV5PHXY5bABq8X2eQn7OCi1qNptThZVKRXC3KMKDgwPO5RFx5ROe7e7uqv3JyYkinxcy4zphLpejvb09ubOzgzAIt2IFOJwbFbZaLYkz1whZHZuSk1CE8BGmDvddBvFls4RwWYfM4SpC/h7Y5bkKYTnCwm28V2EGdZlTIydKpy6DEDnDjzrUIOoA/AemsNIpITsoWDJxLUqsOHCTCfNyiPNXCpHDUqkkJuUikUs4flOZzKtD3/chYky4trYWRkJ11WtXg4YNQs/zpoQ6hxPZUndH0JCRw2uE3G4C7nKYqvVqtZp2MJDLM4TIIQ8EgQ5BDrlbVGHjUJBaROvNlI0eXzCiUCiowoZCTB+oDkI4M210pzAZsdsSK4YD3MYeYX+QyzBCFyk6Ra9csAtziHE3YwrmIatTwwGF/f+SWUSI82iOq16Gy3AXOURB65GlO2YRIcy7aj1tih6wGKZ4r8AcXLKowHVhM/E0ZLxTYAAUHh8fE15YCD1I2VwzJZlM3mHCATv9lglHXJMX/OMF36jAf3gnOD0jzETGazb4ViidTiuXUcy8SuSu3+/T6ekpDYfDhRgMBhLnOeyz1dXVO6FoNPo5T5o/OHcZVrfBeUuz2vfBBovJcJXEV1ZWPgtNPkuMTz8S4PjkP/zBc/q0bLncAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9bb5114cea24e99b7a4b2244a7d79f99/b5bcc/docc-29.webp 425w&quot; sizes=&quot;(max-width: 425px) 100vw, 425px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9bb5114cea24e99b7a4b2244a7d79f99/df70d/docc-29.png 425w&quot; sizes=&quot;(max-width: 425px) 100vw, 425px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9bb5114cea24e99b7a4b2244a7d79f99/df70d/docc-29.png&quot; alt=&quot;docc-29&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;오른쪽 상단에 새로운 레포지토리의 주소를 입력하고 &lt;strong&gt;Add Package&lt;/strong&gt;를 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/66e9f/docc-30.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABqElEQVR42nWSCW7bMBBFff8D9AA9R6pYslauWtBYrVV3cYPEtJPIRTc7gX9nmMoJWoXAB0fC6GnI/ydB8AZBcIY8TaG1RlmWXtpYGJK1hnaWptr+rQ312FMvv8/z3GsynQYIwwBSylNDVVVomhp1XaOsahjL7yv/3DSNF9fPpZTymoThOaLoHEorP0WeZxBC0F5ASYG20fjQVng/H0AErlhPMB6AB2JN4jjCbBbC0HG1NojjGEUh8K69QDn/hNfhNV6dOURvb/Gtv4HbbOG2Pdq29aBRYByH0EpDCY0onEEKCbde4/OXFdL5FunyHh+3e/z6+QO/D/fYPxzRdQt/PTz1+IR0XL7sLMtQUeP12sFdLnH4vsGwjsfjaV8s/gfyVZ2A7DC7ykBudG6DuxuHh8MeY2sMODphkiQe3nUdVquv6Psddrtn6nssr3o0F+2jOS+aYskUgqZZSqYUyMjtjGqemH/AWRtUKOvj9LLLZIqSGrLQiKOEQi4gMmrIFQqqjbT00RNgyOgokGHRbOpDqZVBmtCElEGOjpQKgnZOwL9BfgSxSq8B+Af7cPozeyemjAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/263a4/docc-30.webp 480w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/a6361/docc-30.webp 960w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/0b34d/docc-30.webp 1920w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/da28f/docc-30.webp 2880w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/98b7d/docc-30.webp 3840w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/2d0e9/docc-30.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/9aebd/docc-30.png 480w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/a91f8/docc-30.png 960w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/ac7a9/docc-30.png 1920w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/f9c26/docc-30.png 2880w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/5da7e/docc-30.png 3840w,
/static/c36b77e2a39ba940ca8d1e92da4bcea4/66e9f/docc-30.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c36b77e2a39ba940ca8d1e92da4bcea4/ac7a9/docc-30.png&quot; alt=&quot;docc-30&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;종속되어야 하는 Project 선택 후 &lt;strong&gt;Add Package&lt;/strong&gt; 선택합니다. 이후 프로젝트 빌드가 정상적으로 이루어지는지, DocC가 잘 동작하는지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;photo-item&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e26bb3faa171dcca6a22cdccb37d5df6/66e9f/docc-31.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABcUlEQVR42o2SD2+CMBDF+f6faUOBAqW0/M1cYpaZOGLUxfgNfLtXhTA3szX55aBcH+96F4ThM8LwCTrL4JxD0zQeV9WohLquJBInz/XtuZKcesrlvtbaEyyXIaIoRFmWU0Lbtuj7Dn3XoRP4ztj3/UR3+zZirfUEUbRAHC9gnfUutM5hjEFRaB/pmg66mfj4g/keDZFAqRhJEqGSgzycpqlHKYUsVcjlKoqi8H/nAUbnKl/JQ0GlIjjrYI1DEitPJkKr1QuG4QPb7Ra73Q7n8xmbzfvtDps/HEq5TKQbQofDMIDrcrl4uPb7/dS8e0Fe0STIpFGQ3crzHOv1GqfTCYfDEZ/HA962B7Sv7+ja5v8OWSpJksRD4dE1GQ8yd+z274K1NEVEs/zahFwixYiTCWCn53M47/RPQWmKLR3KwkHFqQy5gdGSUFgUuUFlOTb9wzn8JkixOFlex8FKyak41CzPSIKFkcgJuB/kqxBpPKPgFxqB7yWnZXyiAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e26bb3faa171dcca6a22cdccb37d5df6/263a4/docc-31.webp 480w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/a6361/docc-31.webp 960w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/0b34d/docc-31.webp 1920w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/da28f/docc-31.webp 2880w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/98b7d/docc-31.webp 3840w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/2d0e9/docc-31.webp 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e26bb3faa171dcca6a22cdccb37d5df6/9aebd/docc-31.png 480w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/a91f8/docc-31.png 960w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/ac7a9/docc-31.png 1920w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/f9c26/docc-31.png 2880w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/5da7e/docc-31.png 3840w,
/static/e26bb3faa171dcca6a22cdccb37d5df6/66e9f/docc-31.png 5120w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e26bb3faa171dcca6a22cdccb37d5df6/ac7a9/docc-31.png&quot; alt=&quot;docc-31&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h1 id=&quot;-올리브영이-사용한-docc는-어떤가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%B4-%EC%82%AC%EC%9A%A9%ED%95%9C-docc%EB%8A%94-%EC%96%B4%EB%96%A4%EA%B0%80%EC%9A%94&quot; aria-label=&quot; 올리브영이 사용한 docc는 어떤가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🫒 올리브영이 사용한 DocC는 어떤가요?&lt;/h1&gt;
&lt;hr&gt;
&lt;h2 id=&quot;docc-산출물&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#docc-%EC%82%B0%EC%B6%9C%EB%AC%BC&quot; aria-label=&quot;docc 산출물 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;DocC 산출물&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;로컬 빌드 가이드&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DocC 도입 전 로컬 빌드 1회 당 평균 15-20건이던 빌드 관련 문의가 도입 후 1-2건으로 약 90% 감소했습니다. Tutorial로 작성하였고, 단계마다 이미지를 더해 상세하게 안내하고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JavaScript Interface 가이드&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DocC 도입 전 주 평균 10-15건이던 인터페이스 관련 문의가 도입 후 1-2건으로 약 75% 감소했습니다. 웹과 앱이 통신하는 규격을 DocC로 정리하였습니다. Xcode 하나만으로 가이드를 확인할 수 있어서 직관적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Github Pages로 배포&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Xcode 외에도 인터넷 브라우저를 통해 DocC를 확인할 수 있도록 제공합니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h2 id=&quot;docc가-가져다주는-효과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#docc%EA%B0%80-%EA%B0%80%EC%A0%B8%EB%8B%A4%EC%A3%BC%EB%8A%94-%ED%9A%A8%EA%B3%BC&quot; aria-label=&quot;docc가 가져다주는 효과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;DocC가 가져다주는 효과&lt;/h2&gt;
&lt;p&gt;앱 개발자가 아니어도 DocC로 생성한 문서를 볼 수 있다 보니 iOS 앱 빌드나 인터페이스 명세 정보에 대한 문의가 현저히 줄었어요. Xcode 내에서 인터페이스 검색이 가능한 덕에 앱 개발자들의 개발 속도는 점진적으로 향상되었습니다. 이 뿐만 아니에요. 웹페이지에서 확인할 수 있는, 접근성 측면도 향상되었습니다. DocC가 iOS 앱 개발자들에게 사랑받는 데는 다 이유가 있었어요.💕&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이렇게 DocC를 작성하고 활용하는 방법에 대해서 간단하게 살펴보았습니다. 여러분도 DocC를 통해 문서 자동화에 성공하고, 성공적인 협업 도구로서 활용해 보시길 추천합니다! 더 자세한 내용은 &lt;a href=&quot;https://www.swift.org/documentation/docc/&quot;&gt;&lt;b&gt;DocC 공식 문서&lt;/b&gt;&lt;/a&gt;를 활용해보세요.&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;💍럭셔Lee💍 는 이만 물러가 보도록 하겠습니다~!&lt;br&gt;&lt;/p&gt;
&lt;p&gt;다음번에 만나요 안녕 👋🏻👋🏻&lt;/p&gt;</content:encoded></item><item><title><![CDATA[사용자 경험 개선을 위한 올리브영 테크팀 아이디어톤 현장 전격 공개!]]></title><description><![CDATA[본 글은 2024년도 12월에 올리브영 테크플랫폼센터 오거나이저에 의해 작성되었습니다. 새로운 경험의 서막: 마곡의 한 호텔 연회장에 모인 올리브영 엔지니어들 2024년 11월 1…]]></description><link>https://oliveyoung.tech/2025-02-28/oy-workshop-2024/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-02-28/oy-workshop-2024/</guid><pubDate>Fri, 28 Feb 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;본 글은 2024년도 12월에 올리브영 테크플랫폼센터 오거나이저에 의해 작성되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/eda1b2bb490cee0b68b92a9b83447287/7e068/oy-workshop-01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 134.79166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAAAWJAAAFiQFtaJ36AAAEGklEQVR42p1VyVLjVhQ1o2U8y7YsWZblAfDAEMDMNFDdptxAb4AsQqooKr8ALNiw4o+ySVUW+askqzQn91z7ue1m14tb9+lJ77xzzx0UiUQif4lB7OvQ/4iZs3+KRd74MD09jUQigXQ6jVQqpeuFhQXYto1sNqs+mZS9eFzfxWIxxGXNb2ZmZtSmpqa+TgDyZTKZVG9ZFubm5tRHo1HdmxdvRecxPz//DpDfCQaxIqMXXMst6ovFIlZXV9FsNhGGVVTDChbreWRKHRQ9/13YQ0AoIMNhqIYp/f7+Pi4uLrC2tobjkxN0ltuo+w1Uj85QXepgeioyQYAykakCMiSGOg64uLiIy8tL9Ho97O3t4fj4GB8/9dDd2kKpVBp9awB5fgQ4fpPxjuNgd3cX3W4X9XodjUYDtVpNwSjH+Lc06jp8HjAslXzRKtQMmqzGhxllODS+J1gmk9ELC4XCyJjAIXhEExKEPiphGZlsBrlcDp7n6mHX/ear1Sp839c1AR2ngHw+r+/JcARoxSyES56I7SOVTcAtujg9PdUQPc/TMGk8TKbU8/b2VpJ2iZubGxwcHIxnfBByzs4jnysI27iG3G63FZD6MUE0Mmc1bGxs4Pz8HJubm/hw9AFbkiiTTAVkkVbqRWHpIpNLiSYOdnZ2FKRcLisQzejFEJvLNTRqAYKgjLBSwezs7CRDfsTw4om4siAQw6ReBDMtyIQQtFarSpIqcs4VKQqTgDHRMGg4CIRlOpvUA2TIkEy38EJlKYdztrnAHrGfyDLbplyWLFcCLVCKzxpkl5AlL+AeM6vedVB0PQS+C98tIv992VhWFF4lh5JYOpOALTevrKxo2AyRbOj5zCRlclkUnKKwzcGXC7k/7JJvDINKWeoslLJIqi4n0r/sZ4Zq2BGYJXJ0eIhL6fOzszNcX1/j/v7+uzoUQMez4ZSySKbiyrDVaqHT6SgYk0Gd2DUskefnZzw9PeHh4QGvr694eXl5zzCsSThLoYScUs3W19cVcLzFCFqREiEoE/b4+Ih+v6/lNaEhy6bg2Mg7wiI5KBtTRgQhQ2PU0gxhXsZ+Z/dMALKwG80AS+0QWSlsRwTf3t7WsWVYMnTD1Kx5mdF3QsOoRYZSY05eGCZGhc1BoCEPD5nwyd48c829ycJeEIYtYdgZMCwKQw4AzkKyuLq6ws31jQ4MZp+Zvbu7Q//zZxkSv+r+2GwcDoeCrUYNqQlvpVbMLGvPzEK2Ip9bzRaqQYB6tYay+PEsvy2QYTtAcy2EnU/LIQ9HR4faKSxwTh2jFRn/tNLBl0+n+O2Xn3Hd7+HjwR6sgYb613tj/KlkWs2KWuAFZGeySk3JlHuWFcPyYgMHu13sbW9ht7uB7sYaZgbjS//Lf4j9J/aP2L8/aH8P2f3+Pz2coOlTzh6zAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/eda1b2bb490cee0b68b92a9b83447287/263a4/oy-workshop-01.webp 480w,
/static/eda1b2bb490cee0b68b92a9b83447287/a6361/oy-workshop-01.webp 960w,
/static/eda1b2bb490cee0b68b92a9b83447287/0b34d/oy-workshop-01.webp 1920w,
/static/eda1b2bb490cee0b68b92a9b83447287/a0f4e/oy-workshop-01.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/eda1b2bb490cee0b68b92a9b83447287/9aebd/oy-workshop-01.png 480w,
/static/eda1b2bb490cee0b68b92a9b83447287/a91f8/oy-workshop-01.png 960w,
/static/eda1b2bb490cee0b68b92a9b83447287/ac7a9/oy-workshop-01.png 1920w,
/static/eda1b2bb490cee0b68b92a9b83447287/7e068/oy-workshop-01.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/eda1b2bb490cee0b68b92a9b83447287/ac7a9/oy-workshop-01.png&quot; alt=&quot;oy workshop 01&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;2024 테크플랫폼센터 워크숍 프로그램표&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h1 id=&quot;새로운-경험의-서막-마곡의-한-호텔-연회장에-모인-올리브영-엔지니어들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%88%EB%A1%9C%EC%9A%B4-%EA%B2%BD%ED%97%98%EC%9D%98-%EC%84%9C%EB%A7%89-%EB%A7%88%EA%B3%A1%EC%9D%98-%ED%95%9C-%ED%98%B8%ED%85%94-%EC%97%B0%ED%9A%8C%EC%9E%A5%EC%97%90-%EB%AA%A8%EC%9D%B8-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%93%A4&quot; aria-label=&quot;새로운 경험의 서막 마곡의 한 호텔 연회장에 모인 올리브영 엔지니어들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;새로운 경험의 서막: 마곡의 한 호텔 연회장에 모인 올리브영 엔지니어들&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;2024년 11월 14일 아침, 평소라면 서울역에 위치한 본사로 출근했을 올리브영 테크팀 구성원들이 마곡 메이필드 호텔 연회장으로 하나둘씩 모였습니다. 오늘은 1년에 한번, &lt;strong&gt;올리브영의 기술을 책임지는 테크플랫폼센터(이하 &apos;테크팀&apos;) 구성원 전원이 워크숍에 참여하는 날&lt;/strong&gt;이기 때문입니다.
&lt;/br&gt;&lt;/br&gt;
올리브영이 매년 본부별로 진행하는 워크숍은 당해 실행전략과 과제를 조직별로 점검하고 그 과정을 회고하는 동시에 차년도 전략을 공유하는 자리인데요, 올해는 구성원들이 차년도 전략과 방향성에 공감하고 상위 목표와 정렬된 실행과제를 보다 심도있게 고민할 수 있도록 &lt;strong&gt;아이디어톤 프로그램을 추가&lt;/strong&gt;했습니다. 주제는 &lt;strong&gt;&quot;올리브영의 사용자 경험 개선&quot;&lt;/strong&gt;이었는데 기존 워크숍 방식에서 한 걸음 더 나아가, 테크팀이 직접 사용자의 불편함을 정의하고 해결책을 제시하는 새로운 방식이 시도된 것입니다.
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;blockquote style=&quot;border-left: 4px solid #000000; padding-left: 16px; font-style: italic;&quot;&gt;
  &lt;span style=&quot;color: #000000; font-weight: bold;&quot;&gt;아이디어톤&lt;/span&gt;은 &lt;strong&gt;협업과 창의성을 바탕으로 제한된 시간 안에 문제를 파악하고 실행 가능한 해결책을 제시하는 짧고 집중적인 워크숍&lt;/strong&gt;입니다. 해커톤이 아이디어 구체화와 프로토타입 제작까지 진행하는 기술 중심의 이벤트라면, 아이디어톤은 문제를 정의하고 솔루션의 방향성을 설계하는 데 더 중점을 두는 것이 특징입니다. 아이디어톤 참가자들은 한 공간에서 팀을 이루고, 주어진 시간 안에 원활한 협업을 통해 미션을 완료해야 하는 도전적인 과정에 참여하게 됩니다.
&lt;/blockquote&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/992aaa9008060ece8d7aa34cd12919b8/7e068/oy-workshop-02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 88.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAAWJAAAFiQFtaJ36AAACfElEQVR42p1US1vaUBANECIPRVHeJAKRyMPy8fSFWlSKq678yV113VV/Rbtol21O5wzetGjbhYvzzc3cmTPPXMuyrE8CCH4+ydcgfJKfLaNMJBLY3t5W7OzsIJPJKPL5PHK5HPb29uQuK7osstksUqlUZGPbNuLxuMIy7FTScXd3V2U6nYbjOOpMrHVZpIVoa2tLdSYB+tJWZGjxgxk9L+Pg4ACNRgOHh4coFkuoVsqo14Q876JQqiAW27RnEHJZTL1Wq6kyFospeL67u8Pj4yNmsxlWDw8YD4boNQP4b98jeDNBMhGPfCiZMUkt1s3+mEjGoNvtYrlc4ubmBqenp5hOp5jPFxiNR/A8d8OWYC9ZtmX695yQpS6X75Sw3+9jPB5jMhmj1WqhXq+/IIxK5gezJLuTFDhJOKkkEnY8aoFxpJ1tJ5BM2kLgRKAvt+SJ3NLJcgCe58EPPBz1XBx1PTSaDfi+r/ccku+3cNTx0O6vbdqCVuCKb1Nscr8JOZhCoaAoFooy1TWMzuwcdSQm1PYPO9pEhNyl4XAoPZroVAl+j0Yj9Ho9JWOW7CN1vOOQCPqwx9zbiJAj5+XFxQWurq5wfn6u4Pnk5ETvuaskop4Bz87OVF5eXmIwGGxmSAde3N7e4kF27v7+HtfX11itVroyzJCEJOPUGWixWKg9QeK/lhwEAdrttg7CyGazGWVoMmIwVjOfz5WcO/ui5PUvVsT+/n7UeJ5LpVJEeHx8jE6no+CZROyf67pahSH8IRmG1Wo1LJfLYaVSiSRBvRCG8jiEslahBAglcCQJ2pGDXJZ5unSx/wHemxflfzYmww+C74Ivgq+vBH2/CT7+AseOtLRRczgKAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/992aaa9008060ece8d7aa34cd12919b8/263a4/oy-workshop-02.webp 480w,
/static/992aaa9008060ece8d7aa34cd12919b8/a6361/oy-workshop-02.webp 960w,
/static/992aaa9008060ece8d7aa34cd12919b8/0b34d/oy-workshop-02.webp 1920w,
/static/992aaa9008060ece8d7aa34cd12919b8/a0f4e/oy-workshop-02.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/992aaa9008060ece8d7aa34cd12919b8/9aebd/oy-workshop-02.png 480w,
/static/992aaa9008060ece8d7aa34cd12919b8/a91f8/oy-workshop-02.png 960w,
/static/992aaa9008060ece8d7aa34cd12919b8/ac7a9/oy-workshop-02.png 1920w,
/static/992aaa9008060ece8d7aa34cd12919b8/7e068/oy-workshop-02.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/992aaa9008060ece8d7aa34cd12919b8/ac7a9/oy-workshop-02.png&quot; alt=&quot;oy workshop 02&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;2024 올리브영 테크팀 아이디어톤 주제&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h1 id=&quot;엔지니어에서-프로덕트-메이커로-사용자-중심-사고의-확장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%97%90%EC%84%9C-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8-%EB%A9%94%EC%9D%B4%EC%BB%A4%EB%A1%9C-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A4%91%EC%8B%AC-%EC%82%AC%EA%B3%A0%EC%9D%98-%ED%99%95%EC%9E%A5&quot; aria-label=&quot;엔지니어에서 프로덕트 메이커로 사용자 중심 사고의 확장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;엔지니어에서 프로덕트 메이커로: 사용자 중심 사고의 확장&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;소프트웨어 엔지니어가 비즈니스를 이해하고 사용자 경험에 대해 고찰하는 것은 서비스와 내부 시스템 곳곳에 다양한 센서를 장착한 뒤 이를 다시 사용자에게 전달하는 일이기에 매우 중요한 부분입니다. 사용자의 목소리에 귀를 기울이고 고찰하다 보면 실제 사용자가 어떤 경험을 선호하는지, 어떤 지점에서 마찰을 느끼는지, 어떤 기능에는 반응하지 않는지를 프로덕트 메이커로서 차차 알게 되죠. 이러한 과정을 통해 올리브영 온라인몰 고객, 올리브영 내부 시스템의 실 사용자, 그리고 엔지니어로서 우리의 역할과 팀으로서 집중해야 할 우선순위에 대해 조금이나마 더 깊이 이해하는 효과도 분명 있습니다.
&lt;/br&gt;&lt;/br&gt;
특히 올리브영은 고객 중심으로 모든 밸류체인이 의사결정을 하며 옴니채널화를 규모감 있고 성공적으로 수행해 왔기 때문에 이번 아이디어톤 주제가 공개되자마자 구성원 모두 긴장감과 기대감을 감추지 못했습니다. 특히, 올리브영 테크팀만 단독으로 모여 이 정도 규모의 행사를 진행하는 일이 처음이었던지라 모두가 결과와 과정을 예측할 수 없어 행사일이 다가올수록 분위기가 가속화되었습니다.
&lt;/br&gt;&lt;/br&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🤗 프론트엔드 개발자 신*주님:&lt;/strong&gt; &quot;그동안 경험한 워크숍과 달리 새로운 포맷인만큼 더욱 기대됩니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;😲 백엔드 개발자 김*문님:&lt;/strong&gt; &quot;이렇게 회사 외부로 나가서 크게 하는 워크숍은 처음이라 기대가 큽니다!&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;😳 SRE 엔지니어 이*형님:&lt;/strong&gt; &quot;아이디어톤을 통한 동료들의 멋진 아이디어가 기대됩니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🙄 백엔드 개발자 고*훈님:&lt;/strong&gt; &quot;제가 속한 팀과 유닛을 비롯해 평소 교류하지 못 했던 테크플랫폼센터 구성원들과 다양한 이야기를 많이 나눌 수 있는 소통의 기회가 되었으면 좋겠습니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;😬 백엔드 개발자 우*돈님:&lt;/strong&gt; &quot;다양한 팀의 실무자들이 내는 아이디어와 구현 방법을 아이디어톤이 끝나도 벤치마킹해 실제 업무에 적용하는 기회로 삼았으면 합니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
그렇기에 아이디어톤을 압축적으로 진행하고 구성원으로 하여금 최대한의 몰입을 이끌어내고자 목표 중심적으로 프로그램을 단순하게 설계하였고 가이드 또한 최대한 직관적으로 제공하였습니다.
&lt;/br&gt;&lt;/br&gt;
&lt;table style=&quot;width: 100%; border-collapse: collapse; margin: 16px 0;&quot;&gt;
  &lt;tr style=&quot;background-color: #000000; color: white; text-align: left;&quot;&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;날짜&lt;/th&gt;
    &lt;th style=&quot;padding: 8px;&quot;&gt;내용&lt;/th&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;D-N&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;워크숍 프로그램 및 아이디어톤 주제 안내&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;D-2&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;참여 가이드 안내, 팀별 크루 배정, 팀별 협업 채널 초대&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;D-1&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;팀별 퍼실리테이터 트레이닝&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;D-Day(오전)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;센터장(CTO)/유닛/팀 리드의 전략 발표&lt;/td&gt;
  &lt;tr&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;D-Day(오후)&lt;/td&gt;
    &lt;td style=&quot;padding: 8px; border-bottom: 1px solid #ddd;&quot;&gt;본격적인 아이디어톤 실시, 갤러리워크를 통한 본선진출작 투표 및 선정, 최종 심사&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/42817fc1e08f4b368d5b24cc1789a383/7e068/oy-workshop-03.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 106.87500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAAAWJAAAFiQFtaJ36AAAE0klEQVR42mWVa1CUZRTH373f3n3ZC8vusuJyCVhYZIBFwUCMMHMaNE1RJDRDDXXUFMPBckoMSa2wcjRhdJzIygEvZaYxZoqppImK6JTM+KmZasZP+akPya9nL1ycPpx5nvc8z/Pfc/7nf85KkiTdFYawYUlSIamipgqv4lutVqPRhE2DVqsV3xpxpo6cq0bvSY9jGP1SbPOEqYSpoxdJTEykMBQiI5CN1e6I+MI/olarnnij0+lGwKPo4UtGgxG9wYLeaMVglMWhGkWxkZaWhs/nI2miH2/iBIwms3isxag3Y5WtkegNBkMY4/FohBqNEbPFh9tfTlbxSjILl+BKmoFF+LRaI6FgkO7ubzl77iqnv+9lx64OXpy3nMn5xXhcbnR6fQRYivAmAM3mBLSSh+ra1zl1vo/WtsOsXNOCx5MlKFDIm1RA388DDD34gz8fPuJg5xkat+wiNTWAYrVjkS0RjkcBFasHm9FPk7jU89MNmrbtY+2G3QQyJmMzx5OdXUD/nQc8/Psffv/rEVu2tTN77mtMLSjAYXNgMplGIoymrNcZcNniCQanEsyZTlZGIbm5U0nyJgm/jYlJycxfuJSq6joWCpszt4bnZs6hICcbe5wyvuIxDrUW4jzFWL3TkX0V2FNmoUsoR/Y+gyXuKUyyH6unFH1CGYp/JqbECvSuMuK8paKA9li1I1KLpqyTJ/J8TQvv7vmKrR8c5Z09XbR8coym1k4SsqvwZs1hY/NnvLe3m2bhb/74GG3tJ5hd9yGSnB4FFPqUwiKNcOjJYfP7x7l8c4DB+0PcvHuP/sE7XOy/R+iFBrKmLefw8QsMDA1x9fYgl28NMij273b0iCwKovqNYMUiNFrchMoWs2DJZqqXbWFh7SYWvdrEvJcbSEkvxuGdRPmsJdTUbRZcrqGqZh0LatdTWrEI2eqKAarGF0WH02bHbJLF3ihEbhktVpxiFY/cOD0hQU0+ZnshWjkPlTkXxRHAaLSM75qxdjPFufCn5zIhNVvIJEhe8QxCU0rRqCRsSc9SWdvKsg17qXvjAEs37mfFpn0UV74lgJNjHI5EKDY6sSb5i5j2yiEqV33Kjo3rad+2juql9RhEW+bPqOe73hv8MnCbUxf66L1+k/sPfmP7gTOY3PnjOBxpbrWEQ1Q6qaged1kD1dOm0FiikJnoxi2bycir5M2WA7y98yCrG3bTvLODlrZDLF21HVt8yv851Gl1uFxOXHYFq8mIySLEqhctZXViUxRki4MEURjZnovVlo1BDmCwBoizJ0eGyhiHAtWgF6NHDACHv4D0okrSn36J4PTFpE6uIi1vNgYlBb0SIBCqJadkOfnlq8ktq2dSyQr8wfmo9a4xYWs0WnxeL5JBpmJdI13XrnHw0iXafviR3Vdusf7ERVS+EpLzFrD/y3P0XLpOV08fR89e4Wr/bdY2HxHCzhwvbNWw0y5ax2iidOUKPjr9NbtOdrF6j6hm5zcsaj+GLbWIjFAVja2fc+TkefZ3nmbfF2fpPtPL2q0dWJ3pI0UZlkambdhsTifxvkSxOsRgjUOJ9+B0+1BkWcxKO15fJm5vkHhnsuA7FU9igAR3sqDM8IQOf41V6LGYuMOqyH9L1MJ7tUqsIouojZ2NnIf9Is1/Y2C3/gNMUJzZth4CKAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/42817fc1e08f4b368d5b24cc1789a383/263a4/oy-workshop-03.webp 480w,
/static/42817fc1e08f4b368d5b24cc1789a383/a6361/oy-workshop-03.webp 960w,
/static/42817fc1e08f4b368d5b24cc1789a383/0b34d/oy-workshop-03.webp 1920w,
/static/42817fc1e08f4b368d5b24cc1789a383/a0f4e/oy-workshop-03.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/42817fc1e08f4b368d5b24cc1789a383/9aebd/oy-workshop-03.png 480w,
/static/42817fc1e08f4b368d5b24cc1789a383/a91f8/oy-workshop-03.png 960w,
/static/42817fc1e08f4b368d5b24cc1789a383/ac7a9/oy-workshop-03.png 1920w,
/static/42817fc1e08f4b368d5b24cc1789a383/7e068/oy-workshop-03.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/42817fc1e08f4b368d5b24cc1789a383/ac7a9/oy-workshop-03.png&quot; alt=&quot;oy workshop 03&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;워크숍의 시작과 함께 2024년 성과 및 2025년 전략 방향성을 발표 중인 리더십&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;일반적으로 아이디어톤 참가자들은 깊이 있는 브레인스토밍과 협업을 통해 문제의 핵심을 꿰뚫는 아이디어를 구체화하게 되죠. 올리브영 테크팀 구성원에 기대한 부분도 &lt;strong&gt;올리브영 서비스에 대한 새로운 관점과 사용자 중심 사고를 통해 기존의 문제점을 창의적으로 해결하려는 노력을 동료와 함께 시도해보는 것&lt;/strong&gt;이었습니다. 이는 사용자의 불편함을 해결하고 개선하기 위한 첫 단계로서 매우 중요하며, 엔지니어가 문제 해결 방법(how)에 몰입하는 일에서 더 나아가 올리브영 서비스의 본질(Why)에 대해 깊게 고민하는 시간이 될 것이라 기대했습니다.
&lt;/br&gt;&lt;/br&gt;
그래서 &lt;strong&gt;사용자 유형을 A 타입과 B 타입으로 나누어 참여&lt;/strong&gt;할 수 있도록 했고, 그에 따른 문제 정의, 페르소나 설정, 사용자 여정 맵 작성, 솔루션 도출, 프로토타이핑을 진행할 수 있도록 운영하였습니다. 실제로 올리브영 테크팀 구성원은 각 타입에 맞게 평소 가지고 있던 아이디어를 구체화하며 사용자 경험 개선에 대한 의지를 발산하였습니다.
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🧐 DBA 이*주님:&lt;/strong&gt; &quot;올리브영 앱에서 CJ 임직원 카드 한도 조회 편의성을 향상시켜 앱 사용자 경험을 개선하거나, 사용자 락인 효과를 위해 매거진 기능을 활성화하는 기능을 고민하려 합니다. 이 외에도 거래 취소 및 환불 시 기존의 장바구니에 대한 상태를 보존하면 재구매에 대한 편의성을 향상시킬 수 있다고 생각합니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;😲 백엔드 개발자 정*지님:&lt;/strong&gt; &quot;저는 올리브영 온라인몰의 열렬한 사용자로서, 중복 리뷰 노출을 본 경험이 많습니다. 고객이 양질의 리뷰를 보는 것은 제품 탐색에서 매우 중요한 단계라고 생각하므로 이 문제를 실질적으로 해결해 보고 싶습니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🙄 백엔드 개발자 이*훈님:&lt;/strong&gt; &quot;올리브영 온라인몰이 MSA로 서비스가 늘어나는 과정에서 인증 및 인가를 조금 더 유연하게 처리할 수 있을지, 뒷단의 API 서버를 어떻게 늘릴 수 있을지 고민해보면 좋을 것 같습니다. 더 나아간다면 온/오프라인 통합까지 고민해볼 수 있겠습니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;😬 SRE 엔지니어 이*근님:&lt;/strong&gt; &quot;올리브영에서 배우자, 자녀, 친구 등과 함께 구매하고 싶은 상품을 공유 장바구니에 넣고 결제 가능한 사용자가 합계산하는 기능이나, 원하는 올리브영 제품을 상대방에 구매해달라고 조르는 기능을 추가하는 방향을 동료들과 고민해 보고 싶습니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;h1 id=&quot;아이디어에서-솔루션으로-올리브영-엔지니어의-사용자-경험-개선-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4%EC%97%90%EC%84%9C-%EC%86%94%EB%A3%A8%EC%85%98%EC%9C%BC%EB%A1%9C-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EC%9D%98-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98-%EA%B0%9C%EC%84%A0-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;아이디어에서 솔루션으로 올리브영 엔지니어의 사용자 경험 개선 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;아이디어에서 솔루션으로: 올리브영 엔지니어의 사용자 경험 개선 여정&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;행사 당일 오전, 각 팀은 올리브영 테크팀의 전략 및 실행과제에 대한 실제 발표 내용을 바탕으로 이와 관련된 아이디어를 사전매칭된 팀 크루들과 협의하기 시작했습니다. 아이디어 협의에 어려움을 겪는 팀이 있을 경우, 정해진 시간 안에 원만하게 합의할 수 있도록 &lt;strong&gt;각 팀별로 배정된 퍼실리테이터가 방향성과 아이디어 구체화를 지도&lt;/strong&gt;하기도 했습니다.
(최상의 아이디어톤 경험을 위해 평소 교류가 적은 구성원끼리 팀을 매칭하기 위해 워크숍 스탭과 ChatGPT가 엄청나게 팀 편성을 튜닝한 끝에 총 26개의 팀이 탄생했습니다.)
&lt;/br&gt;&lt;/br&gt;
&lt;strong&gt;&apos;인공지능 시대의 개발자&apos;라는 주제로 국민대학교 소프트웨어학부 이민석 교수님의 테크 톡&lt;/strong&gt;과 함께 진행한 점심식사가 끝난 후, &lt;strong&gt;각 팀별 대표로 1인의 크루 리더가 자신들의 아이디어를 무대에서 간략하게 발표&lt;/strong&gt;하였습니다. 모든 크루 리더의 아이디어 주제 발표 직후에는 본격적으로 팀별 아이디어 디밸롭에 본격적으로 착수하기 시작하였고, 문제 정의/솔루션 도출/프로토타이핑으로 구분해 정리한 내용을 A3 사이즈의 이젤패드에 기록한 후 연회장 벽면에 부착하였습니다. (여기서 흥미로웠던 부분으로는, 올리브영이 2,400여 개의 헬스&amp;#x26;뷰티 브랜드의 상품을 취급하고 있는 만큼 &lt;strong&gt;검색 및 추천 서비스 고도화 등을 통한 &quot;개인화&quot; 키워드가 가장 많이 등장&lt;/strong&gt;한다는 점이었습니다.)
&lt;/br&gt;&lt;/br&gt;
사실 고찰의 결과물로 페이퍼 프로토타입도 고려하긴 했으나, 시간적 제약으로 인해 솔루션 도출 과정에서 활용할 수 있는 방법론으로 &lt;strong&gt;Value Proposition Canvas, Crazy 8 등을 사전에 지정 안내&lt;/strong&gt;하여 구성원의 혼돈을 줄임으로써 짧은 시간 안에 최대한 완성도 높은 결과물이 나올 수 있었습니다.
&lt;/br&gt;
&lt;/br&gt;
이어진 &lt;strong&gt;&apos;갤러리워크&apos; 시간&lt;/strong&gt;에는 마치 학회 포스터 세션처럼 팀별로 큐레이터로 두고 아이디어를 설명하며 심사위원과 동료들에게 피칭했습니다. 열띤 설득에 서로 몰입하는 광경이 순식간에 펼쳐져 정말 인상적이었습니다. 갤러리워크를 마무리하며 &lt;strong&gt;모든 구성원의 투표로 상위 4개의 팀이 본선에 진출을 확정&lt;/strong&gt;했고, 흑백개발자 컨셉으로 컬러정장을 멋지게 소화한 김환 센터장님(CTO)을 비롯한 세 명의 유닛 리더까지 &lt;strong&gt;총 4인의 심사위원의 슈퍼패스를 통해 4개 팀이 추가로 본선에 진출&lt;/strong&gt;하는 행운을 거머쥐었습니다.
&lt;/br&gt;
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5640c353839ebcfdd70de40f6f777614/7e068/oy-workshop-04.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 85.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAWJAAAFiQFtaJ36AAADoUlEQVR42p2U3U+aZxjGQTBD6lDwAxWQb0RBLFBQQQEBKSpSEWq1tltRO1NnXAOu1S6r1CVLt5muyZZsPdofsB3sZCfLsmR/xbLz/QfbTpbN3x5eQZvs42AH1/u8H/d9vdd9X/fzyGQy2Q8CCPzZWP8Z8v/4dpH7rezfguRyubTq9XrGfD7sThftHdoGufxv8Uqlsp5zKnuZoKWl5Rz1gPp7rVaLx+PBarHS0dl5nqxQKBv3ChQiXqVSSXmyppLOTh2hUASfz4/RNIjD4cRoNOKwO7Fa7Xi9QVwuN/FojOW5BZJTMVKT4ywlkxhEFQrpJ4oLQpPJTLl8j0Q8RWExRy6bJTuToJDLCeSJRVMERi8Tm4hQLq2yMJPi8N59dm5tonpFRfur7WdVyRslazQaPG43FrOFoM9LcMyHx+XEZbGI1U5ycoob+RXsNjfDwx48Q0M4bQ78Xj9tbW2oL6nPFDZ7eEmtxmk1YzWb6OvSYh40YzAYhSovNpOBEbeP6fg8hgEL/XoD/X396Hv16Lr0tCpbaW1tbRp5RtimbsfpcDOXmCE+EZKUjo54yKbS5OZz3Nl4k9KNTRxCVTgQFM4HKWUz1KoVJkUb6hySKdJFPERDQZ7uV9kqrlDMpLky5mdjfZ139nbZvbvNyfMveHB4gs8zytXpGLErEZ4fv8fPP/7E6srqmeP1khUNwvlEhK8/P+HmwlVqWxtk4rOk43G2CwtkIxO8/+FnHD35GJfdRXE2xbPqFp/WDvn+qy8pLRclDqmHTYUT/ss8PajywcN9Dstltq8vs317lcqdNeYm/TyuPaNSrRENhihfW+DF4zd4Udvj6H6VdDJzQdgcG5vJyK2lRap7FZ4cPOKj42OOHrxNfjZJZjrCbKZAYfk1EpE4+UyWuZmYGKs4U5FpBvoHLnZXS4sclbC9V9uJ32WjmJ3nrd0KxcUl1pav47VbiAxbSaeXWC2skRVtSITDxENhxv1+Msm0ZOA5YV2mvq8PbYeGge4uluZyYmiT3E6Os1nI09vTg6G3h5XSTXbXS0QDo2TCXoExxsR8zsZiwkDfhctisE81HR3oBKKBAI+qB+zffZ1PHu4It1O4nUPYBgfFAI9Ipe+tXePdnTK5mQgGISQw4qZLiGkolA6H07rUbnEITIfHcYq963UPERWz1dPdw5DNilHfS5dOh05gxGFndHgYv9uBTqvDLPa7WrTspWNM9p3A7wK/Cvz2P/GLwB8C3/wF0fPxdiBe868AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5640c353839ebcfdd70de40f6f777614/263a4/oy-workshop-04.webp 480w,
/static/5640c353839ebcfdd70de40f6f777614/a6361/oy-workshop-04.webp 960w,
/static/5640c353839ebcfdd70de40f6f777614/0b34d/oy-workshop-04.webp 1920w,
/static/5640c353839ebcfdd70de40f6f777614/a0f4e/oy-workshop-04.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5640c353839ebcfdd70de40f6f777614/9aebd/oy-workshop-04.png 480w,
/static/5640c353839ebcfdd70de40f6f777614/a91f8/oy-workshop-04.png 960w,
/static/5640c353839ebcfdd70de40f6f777614/ac7a9/oy-workshop-04.png 1920w,
/static/5640c353839ebcfdd70de40f6f777614/7e068/oy-workshop-04.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5640c353839ebcfdd70de40f6f777614/ac7a9/oy-workshop-04.png&quot; alt=&quot;oy workshop 04&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;아이디어톤에 참여해 아이디어를 구체화하고 갤러리워크를 진행 중인 구성원들의 모습&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h1 id=&quot;무대-위의-순간-주요-리더십-앞에서-펼쳐진-아이디어-피칭과-수상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B4%EB%8C%80-%EC%9C%84%EC%9D%98-%EC%88%9C%EA%B0%84-%EC%A3%BC%EC%9A%94-%EB%A6%AC%EB%8D%94%EC%8B%AD-%EC%95%9E%EC%97%90%EC%84%9C-%ED%8E%BC%EC%B3%90%EC%A7%84-%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4-%ED%94%BC%EC%B9%AD%EA%B3%BC-%EC%88%98%EC%83%81&quot; aria-label=&quot;무대 위의 순간 주요 리더십 앞에서 펼쳐진 아이디어 피칭과 수상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;무대 위의 순간: 주요 리더십 앞에서 펼쳐진 아이디어 피칭과 수상&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;앞서 선발된 8개 팀은 올리브영 테크팀의 리더로 구성된 심사위원 앞에서 자신들의 아이디어를 피칭하였습니다.
&lt;/br&gt;
&lt;/br&gt;
최종 심사 결과, 1위 격인 &lt;strong&gt;&apos;Pioneer Award&apos;&lt;/strong&gt;의 영예는 배송료를 아끼고자 하는 고객이 제품 픽업을 위해 매장을 방문할 경우 누적 걸음 수만큼 마일리지를 적립해 고객의 건강과 행복을 지키겠다는 &quot;돌려드림 서비스&quot;를 발표한 8조 크루들에게 돌아갔는데요, 올리브영이 대한민국 대표 헬스&amp;#x26;뷰티 플랫폼으로써 가진 강점에 고객 행동 유도 장치를 추가해 배송대행 운송 서비스 비용을 아낄 수 있는 효과까지 언급하여 수상의 기쁨을 누리게 되었습니다.
&lt;/br&gt;
&lt;/br&gt;
이외에도 2위 격인 &lt;strong&gt;&apos;Insight Award&apos;&lt;/strong&gt;는 2030 남성 고객이 올리브영 매장에서 겪는 pain을 분석해 신속 정확한 쇼핑 경험을 제공하는 솔루션 &quot;맨즈 네비게이션&quot;을 발표한 5조가, 3위 격인 &lt;strong&gt;&apos;Impact Award&apos;&lt;/strong&gt;는 건강에 관심있는 시니어 고객 특화 서비스 &quot;올리브롱&quot;을 발표해 확장된 라이프스타일 플랫폼으로 나아가겠다는 비전을 발표한 18조가 수상하였습니다. 5조는 남성 고객층을 타겟으로, 18조는 시니어 고객을 타겟 전략으로 &lt;strong&gt;실질적인 사용자 가치를 창출할 가능성이 높은 아이디어들&lt;/strong&gt;이었다는 점에서 모두에게 깊은 인상을 남겼습니다.
&lt;/br&gt;
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b77b06cbf05f7c386eee94064c3048bd/8ab12/oy-workshop-05.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 135.20833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAAAsTAAALEwEAmpwYAAAE6klEQVR42q2W6VOTVxTGw74lrMoeoBCCooyCCyDWWpW26uh0+qHTqV0cW3SqFlAsCauJAhIWDSFAAiTsAtICU1msDqhTZzrt1OlH/xr99d7XJEBxpk6nH56c973nPc89z7nn3huVSqVaFUDgpcf+F3hjlwVUr970kZ+fn2LjExLI27mTTJ0edVTMa7/Htx4BAQEy5uW/EsbGxpKbm0taWjoRarXP5/cP0tDQUPz9/SXX64HgQBXqiDAlKCIiAo06gpAgv7eW7SHER6gOD0WbmkpycjJarVZkpCU2OhKNJlLJMi4ujpiYGCIjIxXIMY1GQ0hIiBIvx6RsH6F0yAA5i3QoCAwkSCIoiODgYMUGivdAz5i0HhKFfAOhdEqp0dHRREVFKZBBbytZTqjUVf6Eh4cTFhZGUlKSIjklJUWBl1zKlZLkhGpRY++YtDJ2A7lMUxJ5ZXol+/sH+Ma80tbDK90reR1URIvahQcHkRC/1ZddkngOC5G1CyFU1DdISJIEstbr6/kG+SrfTK+lhwtpaiFNo8iTkNKlT5JK2bI8YeLdu8Ib4G1QGZSdrUOvzyI76x102hSydZlkZqaTmLiV1JRk8vJ2kJOjR6fXkZGehi4rA534ZoPsgAClGSktPcLk5AgDw05GujtwmGsYtLZi726jsuIcJlMNjx7d5970MNPTo7hdvQy5urF2msWCadZ2l6e7OXT4XVxDDvocVmz9PVhsnXTab9N5p5mKy2cxm+t4/HiZH2dGGRt1MuTupd9ppbWlXtQ8cTNhYXERI2Nu5mYnGJscpsPVz+C4C7fbRo2xnPp6A0tL88zNTTMy3MdAv43B/i7uWOrYvydP4VC4vIT5Bfl0WtuxdrXT1GriVnsLTS3Ctt6govIyZd9+Jcj7cA32MuDooKenHZO5loaacgp2576BMH8X7W3NmBsNGI2VNJkuccNkoL6ugprqC5z5/FMMhitU1xq4ZjRQVX2F8spLGOt+oKBg1xqhd1E+PHaYZ08eMD8zxtL8BAszJlaWb7Gy2MzMWAONjbVMTY0zOtIrsqsT9e7FOdDLyMQoh48c8Z2Ja4tSsp/789PcHXczOuxgavg690YvCWvE3WcWWVfhdLmx2W9hsVTQ0nQRe48NZ18zBwq3b5a8Jz+Ph0tz3J+dUogXZpqZnfiOh3NVzE0YqDVW4Og3izYy4LA30GapFjW/xcBgMyXF2WuEvsaOieb9Q8WUHtrLuwcLKS7aR9G+LE4eL6T0aAnHREkaai9QVVXJ1auVXDz/MWe/Pk5l+SfszkvzEEouD2Go2HLHSo9y5ovPOH36BKdOFnHq9Ackp2oV/0fHT+ASzdx120C3VWR6p5HOm19ib79CceHezRnGbdlCv2uAu1OT/PX8KS9+v8mLP5pw2o2cLyvj8sUy3INtOHtv4ugTO8lWhVXUcrC7nPdKdmwmTBYnzOLsLA8XFnm2OsPzJyb+fNzKb78usPjzT6KG5dhtN+ixNdDdVUtXx/fY2s7h6GqgpGjXBkLl1ouPj2d4yMXTpyus/jLCyoM+Vh+J9lmeZ1zsmGvXrmK9babTUi761Yyl2YDF/A22juscPLDPS/jKd436+fmTkJjA9u3byNbnoN+2DV2OjqycLGH1QkGyOHkyyMiQ0AqkkS6u1qzMNKJ8h4Ny4aseeI6eV//DP4f5vwGkgWkJzESO5AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b77b06cbf05f7c386eee94064c3048bd/263a4/oy-workshop-05.webp 480w,
/static/b77b06cbf05f7c386eee94064c3048bd/a6361/oy-workshop-05.webp 960w,
/static/b77b06cbf05f7c386eee94064c3048bd/0b34d/oy-workshop-05.webp 1920w,
/static/b77b06cbf05f7c386eee94064c3048bd/da28f/oy-workshop-05.webp 2880w,
/static/b77b06cbf05f7c386eee94064c3048bd/98b7d/oy-workshop-05.webp 3840w,
/static/b77b06cbf05f7c386eee94064c3048bd/42445/oy-workshop-05.webp 5374w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b77b06cbf05f7c386eee94064c3048bd/9aebd/oy-workshop-05.png 480w,
/static/b77b06cbf05f7c386eee94064c3048bd/a91f8/oy-workshop-05.png 960w,
/static/b77b06cbf05f7c386eee94064c3048bd/ac7a9/oy-workshop-05.png 1920w,
/static/b77b06cbf05f7c386eee94064c3048bd/f9c26/oy-workshop-05.png 2880w,
/static/b77b06cbf05f7c386eee94064c3048bd/5da7e/oy-workshop-05.png 3840w,
/static/b77b06cbf05f7c386eee94064c3048bd/8ab12/oy-workshop-05.png 5374w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b77b06cbf05f7c386eee94064c3048bd/ac7a9/oy-workshop-05.png&quot; alt=&quot;oy workshop 05&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;아이디어톤 최종 수상팀 리스트와 산출물&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;이번 아이디어톤을 심사한 리더십도 깊은 인상을 받고 구성원들에 감사와 격려를 담은 강평을 남겨 주었습니다.
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🎤 테크플랫폼센터 센터장(CTO) 김환님:&lt;/strong&gt; &quot;이번 워크숍을 준비하는 과정을 지켜보며 저 또한 여러분과 같은 기대와 설렘을 느꼈습니다. 특히 아이디어톤에 참여한 여러분의 모습을 현장에서 직접 지켜보면서, &lt;strong&gt;단순히 개발에 그치지 않고 고객과 서비스에 대해 깊이 고민하는 테크팀의 역량을 갖추었음을 다시금 확인할 수 있었습니다.&lt;/strong&gt; 이러한 통찰과 협업이 지속적으로 이루어진다면, 우리는 더욱 탄탄한 기술 조직으로 성장할 수 있을 것입니다. 제가 생각하는 강한 조직은 뛰어난 역량을 가진 구성원들이 긴밀하게 협력하며, 자신감을 바탕으로 성공하는 비즈니스를 만들어가는 조직입니다. 이를 위해서는 다양한 의견을 자유롭게 교류하고, 이를 실무에 적용해보는 과정이 필수적입니다. &lt;strong&gt;이번 아이디어톤이 바로 그 시작이었으며, 앞으로도 이런 경험이 축적될 때 조직과 서비스, 그리고 고객 모두에게 긍정적인 변화를 가져올 것이라 확신합니다.&lt;/strong&gt; 앞으로도 더 나은 방향으로 나아갈 수 있도록 여러분 모두가 지속적으로 문제에 함께 도전해 나가길 기대합니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🎤 플랫폼사업총괄 이진희님:&lt;/strong&gt; &quot;테크플랫폼센터가 단독으로 워크숍을 개최하는 모습을 보니 감회가 남다릅니다. 특히 이번 아이디어톤에서 발표된 내용을 들으며, &lt;strong&gt;전사적으로 고민하는 페인포인트를 우리 개발자분들도 동일하게 인식하고 있고, 이를 해결하기 위해 주도적으로 아이디어를 제안하고 있다는 점이 인상적이었습니다.&lt;/strong&gt; 오늘 제출된 다양한 솔루션을 보면서 테크팀이 얼마나 깊이 고민하고 창의적으로 접근했는지를 확인할 수 있었고, 그 과정 자체가 매우 의미 있었다고 생각합니다. 앞으로도 이러한 기회를 더욱 확대하여, 구성원들과 더욱 긴밀하게 문제를 공유하고 함께 해결해 나가는 문화를 만들어가고 싶습니다. 오늘의 경험이 테크팀의 성장뿐만 아니라, 올리브영이 더욱 혁신적인 방향으로 나아가는 중요한 발판이 되길 기대합니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/117a5e084520640b6870ddcfb1d46883/7e068/oy-workshop-06.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 106.87500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAAAWJAAAFiQFtaJ36AAAE+ElEQVR42mWVeVDUZRzGdxf2Elhg5VhuUMAFDJTlNP3HAwQvvFCDCMFEU1QOZZFAcAIRN3CGyiyvmjRHRk1HU8QjK7OsxgzzGHTGC6+8MBnDYz+9u7CC02/mO+/9vM/v+T7v+0okEslZEUgkUrNUKkUmk2FnZ/cq7O3tX9UtY2Jen5DY4mVP+bukT+drYQG3lDqdDkN0NIPCwnFy0fYZew0QuVxuW9ONbtldqVKh8/LG19cPb29fvLx80AugiIjB6Dx1ODu7olCqcHDoh0qtQmYnR65QWtkrlUoLxkuJjYmzk4bRwxJp2rOf0+cu8deFK5y9eIVrtx6QN3e+dY7GxQ+FJhJHjwQULgbs1QPROLmhVipQCTJ2FklsgB5aV2Ykj+HcuTaeAfc6/uXhP8/oeGqm3GgUc+zEIk/eySvHuGozK+q3MWJkuuh3EL+qRKNxsjKVSHs00Dg6EqkPp8RYQXnlKtY0rGfbjn2iXUWMIR57qRw/rQ8Lllaz2LSOovoNpI7PxEHhIn7XwSpDN2APQ3etC8FB/ni6u+EoBi1lkL+f0FGHW38tKqGRh0iKj08YgQHh+AdGinow3v37o1ZY9bMlpRvQoZ8zQQMT8Q2Mx8c/RiyIxU0XiaePQSwMYYBfAOGheiIHBDIsIoqykiqajxyn5cdTjJyYZsV4jaFGG0BGXi0Nn2ynpvFrPmjcRsOGnVSZthIWO5XwoBCCfQLInJ5LaWENprI1PH+BVW9DbpYVQyY8K+k2q4TA0Hj2HmnlcWcnd5+9oP25mUdictvVv5kyu1IwCyF3XApvT5jB3LSpzJ6UwfYdLVy/cY/E+ERs1nvF0MHRg8ysRWJ3IxUFxVSWlLHCWMnSvPkiWUNIGzmcwxtNrFm4gHHDR+OlC2ZuzjLmZWTj5urWa3gbw4SEROpWm6hYXk6FsZSlhcWUFi3DVF3LnJxcxo9Noa66hrLlK8hfWExy6jQyM3IZlzIZl74nyHJWLY1lwmu/nTjJwd376DLDjdsdXL5yl2vtHTTtPkBJWRXClly9+YDb959wX/j0+q2H1NR+Ltj69gJaMyMaOe/OY/2HazEVLebS5etcvNzOmdY2Lra18+XWnWRl5bJnbwtbvtjB9qb9HP3+V74S/TmiX+uq/T/gtPSZ5LyVxWdVRg7u2s2dO/d58KiTjsddHDv+M67iHAd4uOPj6UVYiB5DZBTeOm8C/QNw6qfuBrQcEpuGEW8YSBgxkfT0XCalzabYuJoCYx1FpXVk5ywkaUwSs2bOIikpiYnJSUxPGkVaairpU6ejDx7QnWULQ6lUhr2dBFednqz8ekqq1lFeu4nSlZ9a69n5JvQxEzj23Q88FRr+cvIUh5sP09x8hJYDhzj7Rytpkyb3GlsuVwgNNMS8mcqhn9p4Inz48FEHnZ1Ped7VxZkLN8leVCM0O0Hrn+dp3nuAXRs383FBASsrV3P020OkJqf0AspkdmZHBzWhEXEUltbzUeNGqleaaGzcRN6cJby/vIbUlGk0NX1DQ+1asoVVKoU33xsaxeikKWxYt4VRo8baAM3WG9tiHWeNM4P1ERiiDCQa4ogbGkfMkFh07t4iCaEsyi9kyeISBukj8ffyZVhoqLh4o6w+jI6O7T0p4jvfg/5SXJJmcWub5QqFWaFQmlVqtVlsZraXyy3vjTXEXGtIpRKzuFC7+2XSFz1Pwen/AEUNBZGt8REgAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/117a5e084520640b6870ddcfb1d46883/263a4/oy-workshop-06.webp 480w,
/static/117a5e084520640b6870ddcfb1d46883/a6361/oy-workshop-06.webp 960w,
/static/117a5e084520640b6870ddcfb1d46883/0b34d/oy-workshop-06.webp 1920w,
/static/117a5e084520640b6870ddcfb1d46883/a0f4e/oy-workshop-06.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/117a5e084520640b6870ddcfb1d46883/9aebd/oy-workshop-06.png 480w,
/static/117a5e084520640b6870ddcfb1d46883/a91f8/oy-workshop-06.png 960w,
/static/117a5e084520640b6870ddcfb1d46883/ac7a9/oy-workshop-06.png 1920w,
/static/117a5e084520640b6870ddcfb1d46883/7e068/oy-workshop-06.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/117a5e084520640b6870ddcfb1d46883/ac7a9/oy-workshop-06.png&quot; alt=&quot;oy workshop 06&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;팀별 피칭 후 심사를 거쳐 선정된 우수팀에 시상 중인 올리브영 테크팀의 모습&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h1 id=&quot;첫-아이디어톤이-남긴-빛나는-성과-더-나은-사용자-경험을-위한-도약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4%ED%86%A4%EC%9D%B4-%EB%82%A8%EA%B8%B4-%EB%B9%9B%EB%82%98%EB%8A%94-%EC%84%B1%EA%B3%BC-%EB%8D%94-%EB%82%98%EC%9D%80-%EC%82%AC%EC%9A%A9%EC%9E%90-%EA%B2%BD%ED%97%98%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%8F%84%EC%95%BD&quot; aria-label=&quot;첫 아이디어톤이 남긴 빛나는 성과 더 나은 사용자 경험을 위한 도약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;첫 아이디어톤이 남긴 빛나는 성과: 더 나은 사용자 경험을 위한 도약&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;매년 진행하던 워크숍에 아이디어톤이라는 새로운 포맷을 더한 이번 행사는, 소프트웨어 엔지니어들이 단순한 기술 구현을 넘어 프로덕트 메이커로서 사용자 경험을 깊이 고민하는 동시에 프로덕트의 본질을 고민하는 계기가 되었고, &lt;strong&gt;기술과 창의성을 결합하여 더 나은 서비스를 제공하려는 비전을 실현하기 위한 중요한 첫걸음&lt;/strong&gt;이었습니다. 단순히 전략을 듣고 아이디어를 내는 데 그치지 않고, 이 과정에서 얻은 통찰과 아이디어는 올리브영 옴니채널 강화를 위한 로드맵과 팀 운영에 반영되어 실제 사용자 경험 혁신으로 이어질 예정입니다.
&lt;/br&gt;&lt;/br&gt;
특히 처음 함께 한 아이디어톤은 혁신의 씨앗을 심는 자리이자, &lt;strong&gt;사용자 중심의 사고방식과 창의성을 고취시키는 소중한 경험의 장&lt;/strong&gt;이었습니다. 이는 구성원의 아이디어가 실제 변화를 직접적으로 만들어낼 수 있음을 보여주는 중요한 과정으로 앞으로도 새로운 가능성을 위한 이런 자리들이 계속될 것을 기대합니다.
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;👧🏻 백엔드 개발자 진*진님:&lt;/strong&gt; &quot;아이디어톤이 타이트해서 힘들긴 했지만 너무너무 재밌었습니다. 다음에는 해커톤을 해보고 싶습니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;👩🏼 마크업 스페셜리스트 이*미님:&lt;/strong&gt; &quot;참여 전에는 아이디어톤에 대한 부담이 꽤 있었는데 막상 참여하니 생각보다 너무 재밌었어요.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;👱🏼‍♀️ 백엔드 개발자 김*지님:&lt;/strong&gt; &quot;제가 겪은 워크숍 중에 가장 재밌었습니다. 워크숍을 한번 더 하고 싶습니다!&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #f8f9fa; padding: 12px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #000000;&quot;&gt;
  &lt;p&gt;&lt;strong&gt;🧔🏻‍♂️ QA 엔지니어 권*중님:&lt;/strong&gt; &quot;엄청난 스케일의 행사 규모, 쾌적한 환경, 퀄리티 높은 프로그램에 만족했습니다. 다만, 일정이 빡빡해서 다양한 구성원들과 인사를 많이 못 나눈 부분은 아쉬워서 다음에는 밍글링 프로그램이 더 추가되면 좋겠습니다.&quot;&lt;/p&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fdea1f24927134177223994acfc91362/7e068/oy-workshop-09.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 84.79166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAWJAAAFiQFtaJ36AAADpUlEQVR42p2UaUxcVRTHH8MOBUZkH2ZYBhmGzRHKFNms2MqiXYdQli4gA0qlrdgqoVraYC2lUCxrocNiSzFgG4rGULG0aExNtCGhiXVPXKJGjR/84MfCz/ueM0D7sR/+795z37m/c+55511JkqQ5IYQW7eODyLF3Vkhakg0nJye8vLwU+fr64unpibe3tzL38/NDrVYLW37vLcY1uLq6Kj6yv7zf2dlZZizeA3Q4yCA3N7dleXh4KOuubu64u7sJuSvrq4Gyj0qlWpLEQ4kuA1cfIygoiMTERAwGA7qICCK1WvRRAajD4ggO1eB037HtQCQ51dDQ0GWgY0xPT6egoICkpCSyc3JIMBjRh+uJytxAVGwCKifpHn+5NC4uLvJcUjK0G/9HEaNeryc/P5/c3FzS0tLIzMziifW5pKSkKAk4fB1AHx8fpY7S6iir5/KRzWYzqamp6KOjiYmJEUGi0YRrCAkJ4f598key25JSo8fExmTTo5hSTKSmpRItMvRTP0RgcAiBQcEEBAaJ2oXh/3CAyERFcHAgkVGRhGvDCRdB/P39HXCJk6fb+OOfv/nqx+9Z+PYOP/z2M+OTl3jlcBNDYxN0nxumb+g8Y5enqHlxH8nxBl49uI/J9ybot/UwOHyW6uefWwEajAlk5W7kybwCNjyzifV5hZSU76ThyFEGLoxxTmjknQlGL1/BureOnMfXUScA859/zK8/3eGv37+jr6t9Bbi7soK6+nqeysvnUEMD5Xsq2VFWxsuNr9FpG6F3+G0Bvojt4jgVNS+QLYBFWzdRW1vN+eF+bn12nYGejhVgb18P1+Zm2V5cTP/gIM0trVhrrOyx1tB8qoOmE62c6uylb2SUUhFsrSmZ1xsbaDnZQmZ2FnUCPCrAy8Duni6mP7zK04XP0nikif0HD1FlraLSWs2J02eoqK7l8LHjtHX3sdlSTGSEjpcO7Ke1tYXy8jJs3e2MX7CtADu73mLq/SlS1mVQuNVC4ZbtbLNYOP5mC7NznzL+7iQ3PrnJ7S+/4Q2xlhwfy66SIizbtlC1u5Sv528yMTq0DFwsKS1jV6WVA6KOtxduMTMzzdDgAEePNfPB1Y8Yv3SF6ZnrfDG/QFt7Bzt3WCjaXIAuJIDG+r38++cvDJ/txn4vSEvyT75GdHqMaFyzeS0ZGekY4+LQ6SIwGuOJizMqfflIbCwajUb4RRGp0+Ii+lEbFoopORGt6EX7NSbdsKd61x7hQXTXzrj2HzUHM61G2aLQAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fdea1f24927134177223994acfc91362/263a4/oy-workshop-09.webp 480w,
/static/fdea1f24927134177223994acfc91362/a6361/oy-workshop-09.webp 960w,
/static/fdea1f24927134177223994acfc91362/0b34d/oy-workshop-09.webp 1920w,
/static/fdea1f24927134177223994acfc91362/a0f4e/oy-workshop-09.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fdea1f24927134177223994acfc91362/9aebd/oy-workshop-09.png 480w,
/static/fdea1f24927134177223994acfc91362/a91f8/oy-workshop-09.png 960w,
/static/fdea1f24927134177223994acfc91362/ac7a9/oy-workshop-09.png 1920w,
/static/fdea1f24927134177223994acfc91362/7e068/oy-workshop-09.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fdea1f24927134177223994acfc91362/ac7a9/oy-workshop-09.png&quot; alt=&quot;oy workshop 09&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;아이디어톤 수상팀에 수여된 상패&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;h1 id=&quot;그-외-재밌는-올리브영-테크팀-워크숍-속-숨은-9가지-이야기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B7%B8-%EC%99%B8-%EC%9E%AC%EB%B0%8C%EB%8A%94-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%ED%85%8C%ED%81%AC%ED%8C%80-%EC%9B%8C%ED%81%AC%EC%88%8D-%EC%86%8D-%EC%88%A8%EC%9D%80-9%EA%B0%80%EC%A7%80-%EC%9D%B4%EC%95%BC%EA%B8%B0&quot; aria-label=&quot;그 외 재밌는 올리브영 테크팀 워크숍 속 숨은 9가지 이야기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;그 외 재밌는 올리브영 테크팀 워크숍 속 숨은 9가지 이야기&lt;/h1&gt;
&lt;hr&gt;
&lt;ul class=&quot;fun-list&quot;&gt;
  &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🎁&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;푸짐한 수상팀 경품으로 화제!&lt;/strong&gt;&lt;br&gt;
    이번 워크숍에서는 &lt;strong&gt;아이패드 미니(1등), 애플워치 SE(2등), 에어팟(3등)&lt;/strong&gt;이  수상팀 전원에게 지급될만큼 역대급 경품이 준비되었다. 특히, 팀을 리드한 크루에게는 업무 생산성의 신세계를 열어주는 스크림덱까지 추가 제공되어 모두의 부러움을 샀다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🎨&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;이번 워크숍을 위해 특별 제작된 굿즈와 경품 라인업&lt;/strong&gt;&lt;br&gt;
    수상팀 경품 외에도 개발자 감성 충만한 패브릭 키보드 커버, 양말, 집업후드, 키캡, 금속뱃지, 개발자 밈 티셔츠를 비롯해, 올리브영이 운영하는 라이프스타일 플랫폼 ‘디플롯’에서 엄선한 일광전구 조명, 턴테이블, 고급 수건 세트, 노트북 파우치, 케이블 보호캡, 멀티 케이블, 보조배터리까지! 그리고 모두가 탐냈던 에어팟 맥스까지 준비되었다.&lt;/p&gt;
  &lt;/li&gt;
    &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🎰&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;100만 원짜리보다 손맛이 쏠쏠한 3만 원짜리 가챠 머신&lt;/strong&gt;&lt;br&gt;
    워크숍 운영팀은 준비 과정에서 100만 원에 육박하는 브랜디드 고급 머신 대여를 고민했으나, 많은 논의 끝에 귀욤뽀짝한 가챠 머신 두 대를 각 3만 원에 직접 구매! 경품 당첨 확률을 최적화하기 위해 몇날며칠 실험을 거듭하고 인간지능(!)을 도입한 끝에 &lt;strong&gt;무려 92명의 구성원이 경품을 획득하는 ‘고효율 머신’으로 재탄생&lt;/strong&gt;했다는 후문도 있다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🎧&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;행사 종료 직전, 극적인 &apos;아이팟 맥스&apos; 당첨의 순간&lt;/strong&gt;&lt;br&gt;
    모든 경품은 랜덤으로 배정되었는데, 가장 탐나는 &apos;아이팟 맥스&apos;는 마지막 순간까지 당첨자가 나오지 않았다. 결국 행사 종료 직전 QA팀 엔지니어가 극적으로 뽑으며 행사 마무리 및 참가자 퇴장 중이던 현장에 환호성이 터졌다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🏆&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;15초 만에 정답?! 초고속 사고력 퀴즈의 주인공 탄생&lt;/strong&gt;&lt;br&gt;
    워크숍 출근길부터 분위기를 띄우고 행사에 몰입할 수 있도록 출제된 사고력 퀴즈. 그런데 시작하자마자 단 15초 만에 정답을 맞힌 개발자 등장! 덕분에 그는 디플롯 블루투스 턴테이블을 손에 넣었다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🕺&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;아이디어톤 심사위원, 컬러풀하고 위트있는 변신&lt;/strong&gt;&lt;br&gt;
    흑백요리사 안성재 쉐프의 컨셉처럼, 심사위원들이 강렬한 레드, 블루, 퍼플, 아이보리 컬러 정장을 입고 등장! 다들 처음에는 부담스러워했지만, 점점 흑백개발자 컨셉을 즐기는 모습으로 변해가며 분위기를 한층 살렸다.&lt;/p&gt;
  &lt;/li&gt;
    &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🍽&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;10시간의 워크숍 후, 품격 있는 만찬으로 마무리&lt;/strong&gt;&lt;br&gt;
    아이디어톤으로 지친 구성원들을 위한 저녁 메뉴는 농어 구이, 스테이크, 와인까지 곁들여진 정찬 코스. 배고파서 더 맛있었던 것도 있겠지만, 음식의 퀄리티도 훌륭해 구성원들의 만족도가 급상승했다. 이번 워크숍을 기념해 만든 수제마카롱도 생각보다 너무 맛있어서 다들 놀랐다고 한다.&lt;/p&gt;
  &lt;/li&gt;
    &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🎨&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;전문 디자이너 없이 만들어낸 ‘개발자 감성 디자인’&lt;/strong&gt;&lt;br&gt;
    이번 워크숍의 모든 크리에이티브 디자인 요소는 운영진에 포함된 백엔드 개발자와 DevRel 담당자가 직접 제작했다. 워크숍 로고, 타이틀, 무대 공간물, 단체 후드, 디지털 콘텐츠까지 밤낮없이 작업한 덕분에 &quot;워크숍의 품격이 느껴졌다&quot;는 후기가 쏟아졌다.&lt;/p&gt;
  &lt;/li&gt;
    &lt;li&gt;
    &lt;span class=&quot;fun-icon&quot;&gt;🚛&lt;/span&gt;
    &lt;p&gt;&lt;strong&gt;수상은 못 했지만, 크루 리더들을 위한 특별한 보상&lt;/strong&gt;&lt;br&gt;
    수상권에서 벗어났지만 팀을 이끌며 고생한 크루 리더에게는 시중에서 구할 수 없는 올리브영 폴딩카트가 증정되었다. 실용성과 희소성을 동시에 갖춘 선물에 크루 리더들의 귀갓길은 누구보다 힘찼다고.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0a915c7c83262f35e09e34182cd23a37/7e068/oy-workshop-08.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 106.87500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAAAWJAAAFiQFtaJ36AAAEkUlEQVR42p2U20+bZRzHC4XJOA3WsZW20EJLT2/PJ1pp6YkCK4VCOYwWRmEDRtlBVNgR3cZ0Tk10iYvGuHhjdmW8MvFqiYmJiV4YYzTxn3H7+LTAYO7Oi+/7vn2fPp/nd3q/MplM9pcQVVVVz4X4r+RyeUU1NXKqq+UvrVVXVwtVIfY/KzOEfpPtPbyi8obyvb1dhdftwWS20tR6/KW1w6qtrd1/v0uvqamhsbGRo0ePUldXR2NDo4isBoWiFYtkRqNRc0JxnLojtWKtnnrxv2PNTbS2NFf2vnbkSDniZ7L909QqLSPpaQYSaRLRJPFoDL/Hi91uxe7uJhF0M5HsYybVx1QqxmgiyqOdLR5cK2HQ6ZALaBn8AqjTGhjNFIjHMkJD9Pr7kGx2HOYeTHodbouBtbF+3synKY7EOTMQ4ubFIqXCBA0iowaRXQVYLmwZ6LC7ePfWPU4PT+B2BTF2G+jp0tLVoUKjUhL2ORjw21gYDnF2OIzLakQy6NkuFfA7zVSL8tQeBtptDi5f3CAWTWEyWtFrNTisVjyiIZ2ifosTKfr87sohGpWKoFscEHIznQoTdFkqDLm8+iBlvfYk56ZSrEwPMjfaT2l9mPd3ZimMRNB3qrEYjQzFo3jKkUk2HJIdt8eKK2DllEpx0P39CJMRG59uzTE3JFI6HWLn8gx338jhl/SYuzoxdHXhtUt4JSNum4TH4cZsMuJwmmg+1vQqsN9rZXUmTdBpwSUaMDkQYSzai0evZixix6LvJOmTKIynyaXFFPS6RadfR9+hobnpMHAvZZu+g9GwB59Vi0mrxmvtpFdEZ1WfxGlUs70+y8xwVNTRw2x2hHw6TiYWRK8zoFG2H45wF+ixGbhUzPLWlTybW3nu31nmwU6Ja+s5sgE9iV4bH99aYfNSgXNLKe5vL3P36gr3ri9ht2gqjAprHxj2W7h+PsNH1wp89fkaPzx5j++f3ObRnTmuTodYnolxb+MMX9w+yzdfrvLd4yv89O19/vzxM/oD2t0ul1kvBlutYLjfSzroZGogKJ49pEJO+v0ulpJuLs2HKY77GAzaGAz0sD4Z4pPtIl9/UCIhqQ5FuAd0mdVMpvysZLyiGXZRgh68okG5pJ8bs0kWx518eH2IbCrEeMpH2KVjNZ/ixvIoCbv6ALgfoVLRRMxjJBdziI4bsRp1ImolAZeVVCyEzyFxeXGU+bEkc5MJBiM++lwSa8VZYuHAHnBvsMt+16ZQMDEYF7X0EgkECHr8DA1kmMwtoFL3sDy/yI31DZbGMqzmsiLaLW4uL/DHr7+w8fYW+44lK19OtLWhVatZy0+LAot0QlFSiREWFi7w8OFjbJKTM9lJSmeL4mua4rwYm81igc2FWX5++pSVC6UDYNmpj7W0cKK1hYjHRtJvx9Stw28z0+t24bSYifucIsUo89ks+bEcmXCE2eEU7yzPcz5fRKns3J/D5xXHrqkV5lpfT4+oWUjqpkN5klPCWHXKNtqOt2DXtWMy9BAP94sMgmiFOVjE76G+kHhvpEGYcQUo23Xsv/dsvOzcz/+n/tlj/P4vyFZ997cmGooAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0a915c7c83262f35e09e34182cd23a37/263a4/oy-workshop-08.webp 480w,
/static/0a915c7c83262f35e09e34182cd23a37/a6361/oy-workshop-08.webp 960w,
/static/0a915c7c83262f35e09e34182cd23a37/0b34d/oy-workshop-08.webp 1920w,
/static/0a915c7c83262f35e09e34182cd23a37/a0f4e/oy-workshop-08.webp 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0a915c7c83262f35e09e34182cd23a37/9aebd/oy-workshop-08.png 480w,
/static/0a915c7c83262f35e09e34182cd23a37/a91f8/oy-workshop-08.png 960w,
/static/0a915c7c83262f35e09e34182cd23a37/ac7a9/oy-workshop-08.png 1920w,
/static/0a915c7c83262f35e09e34182cd23a37/7e068/oy-workshop-08.png 2687w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0a915c7c83262f35e09e34182cd23a37/ac7a9/oy-workshop-08.png&quot; alt=&quot;oy workshop 08&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;아이디어톤을 즐기는 올리브영 테크팀 구성원들의 모습&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
    &lt;video autoplay=&quot;&quot; muted=&quot;&quot; controls=&quot;&quot; loop=&quot;&quot; style=&quot;width: 70%; max-width: 1000px;&quot;&gt;
        &lt;source src=&quot;https://oliveyoung.tech/video/oy-workshop-video.mp4&quot; type=&quot;video/mp4&quot; width=&quot;100%&quot;&gt;
    &lt;/video&gt;
    &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;아이디어톤 하이라이트 영상 (2분 40초)&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;/br&gt;
&lt;div style=&quot;background-color: #f0f2f5; padding: 16px; border-left: 5px solid #000000; border-radius: 8px;&quot;&gt;
  &lt;h3&gt;📢 Organizer&apos;s Note&lt;/h3&gt;
  &lt;p&gt;올리브영 테크팀의 &lt;strong&gt;Developer eXperience(DX)&lt;/strong&gt;를 책임지는 오거나이저로서, 이번 행사는 단순한 ‘워크숍을 위한 워크숍’이 아니라, &lt;strong&gt;구성원들이 올리브영이 제공하는 서비스의 가치를 깊이 이해하고, &quot;우리가 만들고 싶은 기술&quot;이 아닌 &quot;사용자가 진정으로 필요로 하는 것&quot;을 고민하며 문제를 해결하는 과정 자체에 의미를 두는 것&lt;/strong&gt;에 초점을 맞췄습니다. 단순히 아이디어를 공유하는 자리를 넘어, 이 경험이 구성원 개개인과 조직에 실질적인 변화를 가져와야 한다는 점이 중요했기 때문에, 고객 중심의 사고방식이 개발 문화 전반에 스며들 수 있도록 설계했습니다.&lt;/p&gt;
  &lt;p&gt;특히 이번 워크숍을 기획하며 가장 중요하게 고려한 것은 &lt;strong&gt;참여의 주체성을 통한 변화&lt;/strong&gt;였습니다. 올리브영 테크팀은 이미 우리가 제공하는 서비스의 가치를 깊이 이해하고, 조직과 사용자 모두를 위해 각자의 역할을 고민하며 최선을 다하는 팀입니다. 하지만 여기에서 한 걸음 더 나아가, 보다 개방적인 사고를 바탕으로 변화를 만들어가고 능동적으로 참여하며 스스로 성찰할 수 있는 무대를 제공하고 싶었습니다. 이를 통해 구성원들이 자부심을 느끼고, &lt;strong&gt;직접 의미 있는 변화를 만들어낼 수 있도록 하는 것&lt;/strong&gt;이 이번 행사의 핵심 목표였기 때문입니다.&lt;/p&gt;
  &lt;p&gt;행사가 끝난 후 한 엔지니어가 남긴 &lt;strong&gt;“이렇게 많은 테크팀 구성원들이 한자리에 모여 아이디어를 나누고, 실제 서비스 개선을 함께 다각도로 고민해본 경험은 처음이었다. 우리 조직이 함께 성장하고 있다는 걸 실감할 수 있는 의미 있는 시간이었던 만큼 오래 기억에 남을 것 같다.”&lt;/strong&gt;라는 피드백은 이번 워크숍의 가치를 잘 보여주는 말이었습니다. 이 행사를 통해 각자가 자신의 역할을 다시 한번 돌아보고, 협업의 가치를 깊이 고민하는 계기가 되었다면, 이번 아이디어톤은 충분히 성공적인 경험이었다고 생각합니다.&lt;/p&gt;
  &lt;p&gt;마지막으로, 이번 행사를 성공적으로 만들어주신 리더십과 유관 부서, 그리고 적극적으로 참여해 주신 모든 동료 구성원들께 깊은 감사의 말씀을 전합니다. 여러분의 도움 덕분에 올리브영 테크팀은 또 한 걸음 앞으로 나아갈 수 있었습니다. 앞으로도 아이디어톤, 해커톤, 게임데이, 컨퍼런스 등 다양한 프로그램을 통해 &lt;strong&gt;사용자 경험에 집중하고 문제 해결을 위한 협업을 지속적으로 확장해 나가길 기대하며&lt;/strong&gt; 글을 마치겠습니다.&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title><![CDATA[Let'Swift 2024 X 올리브영: 기술과 경험을 나누는 특별한 만남]]></title><description><![CDATA[안녕하세요. 올리브영에서 iOS 개발을 맡고 있는 럭셔Lee입니다. Let'Swift 2024 컨퍼런스에서 올리브영 부스를 운영하며 많은 iOS…]]></description><link>https://oliveyoung.tech/2025-02-26/letswift2024-review/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-02-26/letswift2024-review/</guid><pubDate>Wed, 26 Feb 2025 09:30:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.&lt;/p&gt;
&lt;p&gt;올리브영에서 iOS 개발을 맡고 있는 럭셔Lee입니다.&lt;/p&gt;
&lt;p&gt;Let&apos;Swift 2024 컨퍼런스에서 올리브영 부스를 운영하며 많은 iOS 개발자분들과 인사를 나눌 수 있어 큰 영광이었습니다. 참가자로 참석했을 때는 주로 세션을 듣는 데 집중했다면, 이번에는 부스 운영자로서 다양한 개발자분들과 직접 소통하며 더 의미 있는 경험을 할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이번 행사는 올리브영의 iOS 개발자 송편님과 DevRel 올리비아님과 함께 준비했습니다.&lt;/p&gt;
&lt;h2 id=&quot;우리의-letswift-2024-부스-준비기-지금부터-함께-보시죠-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EC%9D%98-letswift-2024-%EB%B6%80%EC%8A%A4-%EC%A4%80%EB%B9%84%EA%B8%B0-%EC%A7%80%EA%B8%88%EB%B6%80%ED%84%B0-%ED%95%A8%EA%BB%98-%EB%B3%B4%EC%8B%9C%EC%A3%A0-&quot; aria-label=&quot;우리의 letswift 2024 부스 준비기 지금부터 함께 보시죠  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;우리의 Let&apos;Swift 2024 부스 준비기, 지금부터 함께 보시죠! 🚀&lt;/h2&gt;
&lt;h2 id=&quot;왜-올리브영-개발자가-부스-이벤트를-준비했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EB%B6%80%EC%8A%A4-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%EC%A4%80%EB%B9%84%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;왜 올리브영 개발자가 부스 이벤트를 준비했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 올리브영 개발자가 부스 이벤트를 준비했나요?&lt;/h2&gt;
&lt;p&gt;올리브영 iOS 파트 리더 윌님이 &lt;a href=&quot;https://youtu.be/sAa1Vz5zLCE?si=cp708GorQWztwY7u&quot;&gt;&apos;앱 인텐츠-어떻게 쓰죠?&apos;&lt;/a&gt; 세션을 Let&apos;Swift 2024에서 발표하게 되면서, 이를 응원하기 위해 올리브영이 공식 후원사로 참여했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Let&apos;Swift란?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Swift 개발자들을 위한 컨퍼런스로, iOS 플랫폼을 넘어 Swift 언어를 사용하는 개발자들이 모여 다양한 활용 방식에 대해 이야기하는 행사입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://letswift.kr/2024/&quot;&gt;자세히 알아보기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;특히 이번 Let&apos;Swift에서는 올리브영의 앱을 소개하기 위한 프로그램을 개발자들이 직접 기획하고 준비했습니다.&lt;/p&gt;
&lt;p&gt;iOS 개발자들이 한자리에 모여 경험을 공유하는 행사인 만큼, 저희 올리브영 개발자들은 단순히 발표를 응원하는 것에 그치지 않고, 직접 부스를 기획하고 운영했습니다.&lt;/p&gt;
&lt;p&gt;올리브영 앱을 가장 잘 아는 우리가 직접 운영해야 참가자들에게 진정한 가치를 전달할 수 있다고 믿었기 때문입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;어떤-부스-이벤트를-준비했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EB%B6%80%EC%8A%A4-%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%A5%BC-%EC%A4%80%EB%B9%84%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;어떤 부스 이벤트를 준비했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떤 부스 이벤트를 준비했나요?&lt;/h2&gt;
&lt;h3 id=&quot;아이디어-구상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4-%EA%B5%AC%EC%83%81&quot; aria-label=&quot;아이디어 구상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;아이디어 구상&lt;/h3&gt;
&lt;p&gt;&quot;어떻게 하면 개발자들에게 올리브영 테크 조직을 효과적으로 알릴 수 있을까?&quot;&lt;/p&gt;
&lt;p&gt;올리브영 브랜드는 유명하지만, 내부 개발 조직이 있으며 네이티브 앱 개발자가 있다는 사실을 아는 사람은 많지 않았습니다. 그래서 참가자들이 올리브영 iOS 앱을 설치하고 로그인하면 경품을 받을 수 있는 이벤트를 기획했습니다.&lt;/p&gt;
&lt;p&gt;상품을 준비할 때 고려한 점은 두 가지였습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;올리브영의 정체성을 명확히 전달할 수 있는 상품&lt;/li&gt;
&lt;li&gt;iOS 개발자의 관심을 끌 수 있는 맞춤형 상품&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;논의 끝에 올리브영 PB 상품과 Let&apos;Swift X 올리브영 한정 자체 제작 상품을 제공하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;저희가 준비한 상품을 소개합니다! 🙌&lt;/p&gt;
&lt;p&gt;렛츠스위프트 이벤트를 위해 특별 제작한 한정 에코백과 개발자들이 선호하는 스티커, 그리고 올리브영을 대표하는 제품들도 함께 준비했습니다. 딜라이트프로젝트의 베이글칩과 벌꿀약과, 브링그린의 마스크팩, 그리고 탄탄 슬리밍 티까지 다양하게 마련하여 참가자들에게 더욱 특별한 경험을 선사하고자 했습니다.&lt;/p&gt;
&lt;div class=&quot;photo-gallery&quot;&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1438px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ad4d5d0a6308c39170760c3626ba2427/a1e9e/letswift_gift_cart.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 133.33333333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAbABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAwUC/8QAFgEBAQEAAAAAAAAAAAAAAAAAAwEC/9oADAMBAAIQAxAAAAHozXyjRBEz3kHVxeUh7//EACAQAAIABAcAAAAAAAAAAAAAAAABAgQREgMTISIxMjP/2gAIAQEAAQUC4KouMKNtrtlkpSyJ7E0S+hH5n//EABYRAAMAAAAAAAAAAAAAAAAAABEgIf/aAAgBAwEBPwE1P//EABcRAQEBAQAAAAAAAAAAAAAAAAEAERD/2gAIAQIBAT8BVHLJ5//EABoQAAIDAQEAAAAAAAAAAAAAAAABEBEhUUH/2gAIAQEABj8C2aenIfS2kejHH//EABwQAQEBAAIDAQAAAAAAAAAAAAERACExEEFhcf/aAAgBAQABPyGiBTWYN8HavxdLB2OOdb70lhy7wCQhp/X8ygHV8SG//9oADAMBAAIAAwAAABBw/wBC/8QAGBEAAgMAAAAAAAAAAAAAAAAAABEBECH/2gAIAQMBAT8QfAyK/8QAFREBAQAAAAAAAAAAAAAAAAAAIFH/2gAIAQIBAT8QhQP/xAAfEAEAAwACAQUAAAAAAAAAAAABABEhMUGRUWGB4fD/2gAIAQEAAT8QAAs0D3KoaNaK4nXZZraRcjsheBgtygKxp6h9SiGW1fXH73jVzar3SD26YMCNmbc+Y1v7C/JBOQ8T/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ad4d5d0a6308c39170760c3626ba2427/263a4/letswift_gift_cart.webp 480w,
/static/ad4d5d0a6308c39170760c3626ba2427/a6361/letswift_gift_cart.webp 960w,
/static/ad4d5d0a6308c39170760c3626ba2427/e40a7/letswift_gift_cart.webp 1438w&quot; sizes=&quot;(max-width: 1438px) 100vw, 1438px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ad4d5d0a6308c39170760c3626ba2427/a3e66/letswift_gift_cart.jpg 480w,
/static/ad4d5d0a6308c39170760c3626ba2427/fb816/letswift_gift_cart.jpg 960w,
/static/ad4d5d0a6308c39170760c3626ba2427/a1e9e/letswift_gift_cart.jpg 1438w&quot; sizes=&quot;(max-width: 1438px) 100vw, 1438px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ad4d5d0a6308c39170760c3626ba2427/a1e9e/letswift_gift_cart.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 올리브영 카트&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 550px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b8aedf64b363982dc218791a83389ae5/67061/letswift_gift_monitor.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAAAAECBAX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHu575zckKhcH//xAAbEAABBQEBAAAAAAAAAAAAAAABAAIDEBESM//aAAgBAQABBQJxwdSIbkjQ5R+lYK//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AR//xAAcEAACAgIDAAAAAAAAAAAAAAAAARAhAjERQnH/2gAIAQEABj8COhey214zh5tzqP/EAB4QAQACAgEFAAAAAAAAAAAAAAEAESExEEFRYXGR/9oACAEBAAE/IdBvpPa+zAw7qiw13ghRCDJenhB2QJsF8f/aAAwDAQACAAMAAAAQM8cA/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPxAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPxAf/8QAHhABAAICAQUAAAAAAAAAAAAAAQARITEQQVFxgdH/2gAIAQEAAT8Qd2l8W1cB6+g/Y0xFMNLixUUQbHMooe+LHjSD5I/MW0M8f//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b8aedf64b363982dc218791a83389ae5/263a4/letswift_gift_monitor.webp 480w,
/static/b8aedf64b363982dc218791a83389ae5/7df4b/letswift_gift_monitor.webp 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b8aedf64b363982dc218791a83389ae5/a3e66/letswift_gift_monitor.jpg 480w,
/static/b8aedf64b363982dc218791a83389ae5/67061/letswift_gift_monitor.jpg 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b8aedf64b363982dc218791a83389ae5/67061/letswift_gift_monitor.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 개발자의 필수템, 노트북 받침대&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1058px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3482380e7fec35acf8815238b29dc30f/3a2a7/letswift_gift_ecobag.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 133.33333333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAbABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAgX/xAAVAQEBAAAAAAAAAAAAAAAAAAABAP/aAAwDAQACEAMQAAABpZoJBQFhnMelZID/AP/EABsQAQADAAMBAAAAAAAAAAAAAAEAAhEDEBIx/9oACAEBAAEFAq0hx5L1x+VqrLEGGHQsF8i5/8QAFREBAQAAAAAAAAAAAAAAAAAAESD/2gAIAQMBAT8BI//EABYRAAMAAAAAAAAAAAAAAAAAAAAQEf/aAAgBAgEBPwF0/8QAGhAAAQUBAAAAAAAAAAAAAAAAIQABECAxQf/aAAgBAQAGPwJitk13iYx//8QAHRAAAwACAgMAAAAAAAAAAAAAAAERITFBURCBsf/aAAgBAQABPyGqboUX4NK2J5NxHHMq4KPxcBJDtMTL2LCtdnOH/9oADAMBAAIAAwAAABD/ACzz/8QAFxEBAQEBAAAAAAAAAAAAAAAAAREAQf/aAAgBAwEBPxAsuhrzBS7/xAAXEQADAQAAAAAAAAAAAAAAAAAAAREQ/9oACAECAQE/EKNvJP/EAB4QAQADAAICAwAAAAAAAAAAAAEAESExURBhcZHR/9oACAEBAAE/EHxS9N0jU70J+vmEAYS8CXpLC13RHAPEdDMfuUL68BC73miowqntGzr1GKtWraH5XXnZ/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3482380e7fec35acf8815238b29dc30f/263a4/letswift_gift_ecobag.webp 480w,
/static/3482380e7fec35acf8815238b29dc30f/a6361/letswift_gift_ecobag.webp 960w,
/static/3482380e7fec35acf8815238b29dc30f/1de89/letswift_gift_ecobag.webp 1058w&quot; sizes=&quot;(max-width: 1058px) 100vw, 1058px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3482380e7fec35acf8815238b29dc30f/a3e66/letswift_gift_ecobag.jpg 480w,
/static/3482380e7fec35acf8815238b29dc30f/fb816/letswift_gift_ecobag.jpg 960w,
/static/3482380e7fec35acf8815238b29dc30f/3a2a7/letswift_gift_ecobag.jpg 1058w&quot; sizes=&quot;(max-width: 1058px) 100vw, 1058px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3482380e7fec35acf8815238b29dc30f/3a2a7/letswift_gift_ecobag.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 렛츠스위프트x올리브영 한정 에코백&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/87f244a137bd3cecbaa8cfda23b522da/204f1/letswift_gift_sticker.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 141.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAcABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAEDBAL/xAAWAQEBAQAAAAAAAAAAAAAAAAABAAL/2gAMAwEAAhADEAAAAcFYsqCGfMUWszGj/8QAGxAAAgMBAQEAAAAAAAAAAAAAAQIAEhMRAyP/2gAIAQEAAQUCGZJTzR/lLCX6bQac69Wv3Vpq1dXaf//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABYRAAMAAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPwGs/8QAHhAAAgEDBQAAAAAAAAAAAAAAAAERECEyIjNBkaH/2gAIAQEABj8CiRSyzRtswfRh4cC0GEVuz//EAB4QAAMAAQUBAQAAAAAAAAAAAAABEVEhQWFxoTGR/9oACAEBAAE/IUQRa3kkk01uOn4DL+BFW3uhI8DeovdOY76EPEJalM7j4LXavR//2gAMAwEAAgADAAAAEKDHfv/EABcRAQEBAQAAAAAAAAAAAAAAAAEAEBH/2gAIAQMBAT8QweQIX//EABkRAQACAwAAAAAAAAAAAAAAAAEAERAhQf/aAAgBAgEBPxBu8PIhdT//xAAfEAEBAAIBBAMAAAAAAAAAAAABEQAhMUFRYdGRwfH/2gAIAQEAAT8Qg0wKurjo8EOPxm4IeWrh3zeh9Y3kSUllZvbgBjchb0fpgrcjFW297xNY4xiWv3NUUHItDbLGi3DG8M4es//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/87f244a137bd3cecbaa8cfda23b522da/263a4/letswift_gift_sticker.webp 480w,
/static/87f244a137bd3cecbaa8cfda23b522da/a6361/letswift_gift_sticker.webp 960w,
/static/87f244a137bd3cecbaa8cfda23b522da/0b34d/letswift_gift_sticker.webp 1920w,
/static/87f244a137bd3cecbaa8cfda23b522da/da28f/letswift_gift_sticker.webp 2880w,
/static/87f244a137bd3cecbaa8cfda23b522da/98b7d/letswift_gift_sticker.webp 3840w,
/static/87f244a137bd3cecbaa8cfda23b522da/51acc/letswift_gift_sticker.webp 5300w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/87f244a137bd3cecbaa8cfda23b522da/a3e66/letswift_gift_sticker.jpg 480w,
/static/87f244a137bd3cecbaa8cfda23b522da/fb816/letswift_gift_sticker.jpg 960w,
/static/87f244a137bd3cecbaa8cfda23b522da/aaf92/letswift_gift_sticker.jpg 1920w,
/static/87f244a137bd3cecbaa8cfda23b522da/1d134/letswift_gift_sticker.jpg 2880w,
/static/87f244a137bd3cecbaa8cfda23b522da/9ad4e/letswift_gift_sticker.jpg 3840w,
/static/87f244a137bd3cecbaa8cfda23b522da/204f1/letswift_gift_sticker.jpg 5300w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/87f244a137bd3cecbaa8cfda23b522da/aaf92/letswift_gift_sticker.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 렛츠스위프트x올리브영 한정 스티커&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 550px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ccc41b102c6279a98bf74e9dec240c58/67061/letswift_gift_snack.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGAABAAMBAAAAAAAAAAAAAAAAAAECBAX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAv/aAAwDAQACEAMQAAAB7VJzxW4XMgA//8QAGxAAAQUBAQAAAAAAAAAAAAAAAQACAxIxEBH/2gAIAQEAAQUCc+qMoCGSHxCQ2Gd//8QAFREBAQAAAAAAAAAAAAAAAAAAASD/2gAIAQMBAT8BCP/EABURAQEAAAAAAAAAAAAAAAAAAAEg/9oACAECAQE/AVj/xAAaEAABBQEAAAAAAAAAAAAAAAABABAREjEg/9oACAEBAAY/AnyURTOf/8QAGxABAAIDAQEAAAAAAAAAAAAAAQARITFREEH/2gAIAQEAAT8hNlm5bX83FYexWVJKnSw05miVKOef/9oADAMBAAIAAwAAABCHyDz/xAAYEQACAwAAAAAAAAAAAAAAAAABESAhof/aAAgBAwEBPxACFbD/xAAYEQACAwAAAAAAAAAAAAAAAAABESAhof/aAAgBAgEBPxAjN5D/xAAdEAACAwEBAAMAAAAAAAAAAAABEQAhMWFRQZHh/9oACAEBAAE/EBMYj5CNLXSDO7+xE4DE2Ii20r8ECCZAS+bQ5csjyECWQDOT6goIT//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ccc41b102c6279a98bf74e9dec240c58/263a4/letswift_gift_snack.webp 480w,
/static/ccc41b102c6279a98bf74e9dec240c58/7df4b/letswift_gift_snack.webp 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ccc41b102c6279a98bf74e9dec240c58/a3e66/letswift_gift_snack.jpg 480w,
/static/ccc41b102c6279a98bf74e9dec240c58/67061/letswift_gift_snack.jpg 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ccc41b102c6279a98bf74e9dec240c58/67061/letswift_gift_snack.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 딜라이트프로젝트 베이글칩&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 550px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1fc2c4d27f864addffa763909ff05da8/67061/letswift_gift_snack2.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAECBQME/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAHe528ObpIaiAsD/8QAGhAAAgIDAAAAAAAAAAAAAAAAAQIDEAARIf/aAAgBAQABBQI4rhqbojiZHrVf/8QAFhEAAwAAAAAAAAAAAAAAAAAAAREg/9oACAEDAQE/AQo//8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAGxAAAQQDAAAAAAAAAAAAAAAAEQABEBIgISL/2gAIAQEABj8CXLmTbWP/xAAbEAACAwADAAAAAAAAAAAAAAABEQAQITFBcf/aAAgBAQABPyFuo1ShRocGR5MxsdAHNEOJX//aAAwDAQACAAMAAAAQvMcA/8QAFxEBAAMAAAAAAAAAAAAAAAAAAREgIf/aAAgBAwEBPxAIZdp//8QAFhEBAQEAAAAAAAAAAAAAAAAAAREg/9oACAECAQE/EFRJj//EABwQAQEAAgIDAAAAAAAAAAAAAAERABAhQTFhgf/aAAgBAQABPxBAO2JDwE9aiV7RccrXKXD60AiXPXr/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1fc2c4d27f864addffa763909ff05da8/263a4/letswift_gift_snack2.webp 480w,
/static/1fc2c4d27f864addffa763909ff05da8/7df4b/letswift_gift_snack2.webp 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1fc2c4d27f864addffa763909ff05da8/a3e66/letswift_gift_snack2.jpg 480w,
/static/1fc2c4d27f864addffa763909ff05da8/67061/letswift_gift_snack2.jpg 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1fc2c4d27f864addffa763909ff05da8/67061/letswift_gift_snack2.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 딜라이트프로젝트 벌꿀약과&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/cddbd/letswift_gift_mask.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100.20833333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAMEAv/EABYBAQEBAAAAAAAAAAAAAAAAAAECAP/aAAwDAQACEAMQAAAB02xaiN6bVSRR0B//xAAcEAACAgIDAAAAAAAAAAAAAAAAAgEDEBIhIjH/2gAIAQEAAQUCZua3xclilMddifIU0g//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAwEBPwEjP//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAECAQE/AVhP/8QAGhAAAgMBAQAAAAAAAAAAAAAAAAEQETFR8P/aAAgBAQAGPwIXIvyLZqjXH//EABoQAQADAQEBAAAAAAAAAAAAAAEAETEhscH/2gAIAQEAAT8hwKv2PxeMCCJZjOCFF2o39XAV6TSW6992L7bP/9oADAMBAAIAAwAAABCDN3z/xAAXEQADAQAAAAAAAAAAAAAAAAAAAREx/9oACAEDAQE/EEdKjREf/8QAFxEBAQEBAAAAAAAAAAAAAAAAAAERUf/aAAgBAgEBPxDJHSv/xAAcEAEBAQEBAAMBAAAAAAAAAAABEQAhMWGhweH/2gAIAQEAAT8QqSTeXke5dAj+TjbUKOHfPEHr2R/MIAhQL1nC7zCPnHfU0TGXDwaPKBKu/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/263a4/letswift_gift_mask.webp 480w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/a6361/letswift_gift_mask.webp 960w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/0b34d/letswift_gift_mask.webp 1920w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/da28f/letswift_gift_mask.webp 2880w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/98b7d/letswift_gift_mask.webp 3840w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/cb4f7/letswift_gift_mask.webp 6639w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/a3e66/letswift_gift_mask.jpg 480w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/fb816/letswift_gift_mask.jpg 960w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/aaf92/letswift_gift_mask.jpg 1920w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/1d134/letswift_gift_mask.jpg 2880w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/9ad4e/letswift_gift_mask.jpg 3840w,
/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/cddbd/letswift_gift_mask.jpg 6639w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5dc6e4f1ea8d2d4bdbf0b6ac9d367a62/aaf92/letswift_gift_mask.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 브링그린 마스크팩&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 550px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/25baf4f58917ab5ca988f1fc656e15cc/67061/letswift_gift_tea.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAEEAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAgH/2gAMAwEAAhADEAAAAfZuHTN6HLYoKD//xAAdEAABBAIDAAAAAAAAAAAAAAABEBESIQACAyMx/9oACAEBAAEFApNtIJznsah4bxk//8QAFREBAQAAAAAAAAAAAAAAAAAAEiD/2gAIAQMBAT8BMf/EABYRAAMAAAAAAAAAAAAAAAAAAAEgMf/aAAgBAgEBPwEVP//EABoQAAEFAQAAAAAAAAAAAAAAAAEAAhARYSD/2gAIAQEABj8CpHIbh6//xAAZEAEBAAMBAAAAAAAAAAAAAAABEQAhMRD/2gAIAQEAAT8hdwdYYIi9TyDboJMsN7N5qcAIlMQ+f//aAAwDAQACAAMAAAAQdP8Awv/EABkRAAMAAwAAAAAAAAAAAAAAAAABERAhMf/aAAgBAwEBPxCJsfSvH//EABYRAQEBAAAAAAAAAAAAAAAAABEBIP/aAAgBAgEBPxCkY//EABwQAQACAgMBAAAAAAAAAAAAAAEAESFRMUGBYf/aAAgBAQABPxAy40u2rZj6bF7giWOILRi0ad35DDV7Y9rcFH5FJk6Yhbb7OCf/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/25baf4f58917ab5ca988f1fc656e15cc/263a4/letswift_gift_tea.webp 480w,
/static/25baf4f58917ab5ca988f1fc656e15cc/7df4b/letswift_gift_tea.webp 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/25baf4f58917ab5ca988f1fc656e15cc/a3e66/letswift_gift_tea.jpg 480w,
/static/25baf4f58917ab5ca988f1fc656e15cc/67061/letswift_gift_tea.jpg 550w&quot; sizes=&quot;(max-width: 550px) 100vw, 550px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/25baf4f58917ab5ca988f1fc656e15cc/67061/letswift_gift_tea.jpg&quot; alt=&quot;올리브영 부스 이벤트 상품: 탄탄 슬리밍 티&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;올리브영만의 특색이 나타나는 상품으로 준비했는데 어떠신가요?&lt;/p&gt;
&lt;div class=&quot;photo-item&quot; style=&quot;width: 90%; display:block;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b85bb6fb6b436732e3bfa897a330a159/10175/letswift_gift_desk.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 38.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAEDBf/EABUBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAGzJBgXP//EABkQAQACAwAAAAAAAAAAAAAAAAIAAQMSE//aAAgBAQABBQKmDXZR5N7/AP/EABURAQEAAAAAAAAAAAAAAAAAABAR/9oACAEDAQE/Aaf/xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPwEn/8QAGBAAAwEBAAAAAAAAAAAAAAAAAAECITH/2gAIAQEABj8CUzZxGyf/xAAaEAEAAwADAAAAAAAAAAAAAAABABEhMUFR/9oACAEBAAE/IdA/U5BS/SACJRWM/9oADAMBAAIAAwAAABDwH//EABcRAQEBAQAAAAAAAAAAAAAAAAEAETH/2gAIAQMBAT8QUctv/8QAFxEBAAMAAAAAAAAAAAAAAAAAARARIf/aAAgBAgEBPxAW7H//xAAbEAEBAAIDAQAAAAAAAAAAAAABEQAxIUHw8f/aAAgBAQABPxAGnOG21kTX3AhsE7D2sTdGRgA1n//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b85bb6fb6b436732e3bfa897a330a159/263a4/letswift_gift_desk.webp 480w,
/static/b85bb6fb6b436732e3bfa897a330a159/a6361/letswift_gift_desk.webp 960w,
/static/b85bb6fb6b436732e3bfa897a330a159/0b34d/letswift_gift_desk.webp 1920w,
/static/b85bb6fb6b436732e3bfa897a330a159/da28f/letswift_gift_desk.webp 2880w,
/static/b85bb6fb6b436732e3bfa897a330a159/98b7d/letswift_gift_desk.webp 3840w,
/static/b85bb6fb6b436732e3bfa897a330a159/f92b9/letswift_gift_desk.webp 12730w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b85bb6fb6b436732e3bfa897a330a159/a3e66/letswift_gift_desk.jpg 480w,
/static/b85bb6fb6b436732e3bfa897a330a159/fb816/letswift_gift_desk.jpg 960w,
/static/b85bb6fb6b436732e3bfa897a330a159/aaf92/letswift_gift_desk.jpg 1920w,
/static/b85bb6fb6b436732e3bfa897a330a159/1d134/letswift_gift_desk.jpg 2880w,
/static/b85bb6fb6b436732e3bfa897a330a159/9ad4e/letswift_gift_desk.jpg 3840w,
/static/b85bb6fb6b436732e3bfa897a330a159/10175/letswift_gift_desk.jpg 12730w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b85bb6fb6b436732e3bfa897a330a159/aaf92/letswift_gift_desk.jpg&quot; alt=&quot;상품이 가득한 올리브영 부스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;행사장에서 운영한 이벤트는 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-꽝-없는-뽑기판&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%BD%9D-%EC%97%86%EB%8A%94-%EB%BD%91%EA%B8%B0%ED%8C%90&quot; aria-label=&quot;1 꽝 없는 뽑기판 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) 꽝 없는 뽑기판&lt;/h3&gt;
&lt;p&gt;최대한 많은 참가자들과 만나고자 상품 제공 방법에도 신경을 썼습니다. 처음엔 올리브영 관련 퀴즈를 고려했지만, 많은 인파로 인해 대기 시간이 길어질 것을 우려해 간단하면서도 흥미로운 참여형 이벤트를 선택했습니다. 덕분에 예상보다 많은 참여자들이 몰렸고, 큰 호응을 얻었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이벤트 참여 방법&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;📱 올리브영 앱 설치 후 로그인 화면 보여주기&lt;/li&gt;
&lt;li&gt;💌 뽑기판에서 경품 뽑기 (꽝 없이 1-5등까지 차등 혜택을!)&lt;/li&gt;
&lt;li&gt;🎊 뽑기 쪽지에 적힌 등수에 맞는 상품 수령&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;참가자들의 호응이 예상보다 훨씬 좋아 뿌듯한 순간이었습니다.
총 350여명의 개발자, 예비 개발자, Swift 생태계 관계자가 부스 방문하여 올리브영 개발팀에 대해 긍정적인 인지도를 얻어가셨습니다.
부스 이벤트를 통해 현장에서 약 150명 이상의 참가자들이 올리브영 앱을 설치해주셨어요.&lt;/p&gt;
&lt;div class=&quot;photo-item&quot; style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/252861cf7a6cc696b0d4b891edf763b0/e2159/letswift_luckydraw.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAECAwQF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAL/2gAMAwEAAhADEAAAAcl0kvmiEf/EABkQAAMBAQEAAAAAAAAAAAAAAAECEQMABP/aAAgBAQABBQLFOfzCsjKQIKZsbp//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAwEBPwGH/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGhAAAgIDAAAAAAAAAAAAAAAAABEBISJBcf/aAAgBAQAGPwK4fTGhSMWiUf/EABwQAQADAAIDAAAAAAAAAAAAAAEAESExQVFxkf/aAAgBAQABPyE+TT10ixbflslbPjGa2cQ0X0gvwDRP/9oADAMBAAIAAwAAABCrL//EABURAQEAAAAAAAAAAAAAAAAAABAR/9oACAEDAQE/EJP/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAdEAEAAgIDAQEAAAAAAAAAAAABABEhMVFhsUFx/9oACAEBAAE/EDKTqowXnvyCXH7D9jcqmbBGWwC2PvUVYKTJxuE40lfGPZ//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/252861cf7a6cc696b0d4b891edf763b0/263a4/letswift_luckydraw.webp 480w,
/static/252861cf7a6cc696b0d4b891edf763b0/a6361/letswift_luckydraw.webp 960w,
/static/252861cf7a6cc696b0d4b891edf763b0/0b34d/letswift_luckydraw.webp 1920w,
/static/252861cf7a6cc696b0d4b891edf763b0/a2b77/letswift_luckydraw.webp 2554w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/252861cf7a6cc696b0d4b891edf763b0/a3e66/letswift_luckydraw.jpg 480w,
/static/252861cf7a6cc696b0d4b891edf763b0/fb816/letswift_luckydraw.jpg 960w,
/static/252861cf7a6cc696b0d4b891edf763b0/aaf92/letswift_luckydraw.jpg 1920w,
/static/252861cf7a6cc696b0d4b891edf763b0/e2159/letswift_luckydraw.jpg 2554w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/252861cf7a6cc696b0d4b891edf763b0/aaf92/letswift_luckydraw.jpg&quot; alt=&quot;올리브영 부스 이벤트 뽑기판&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;2-올대면-상담소-올리브영-대면-상담소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%98%AC%EB%8C%80%EB%A9%B4-%EC%83%81%EB%8B%B4%EC%86%8C-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EB%8C%80%EB%A9%B4-%EC%83%81%EB%8B%B4%EC%86%8C&quot; aria-label=&quot;2 올대면 상담소 올리브영 대면 상담소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) 올대면 상담소 (올리브영 대면 상담소)&lt;/h3&gt;
&lt;p&gt;오피스 아워를 올리브영 스타일로 재해석한 &quot;올대면 상담소&quot;도 운영했습니다.&lt;/p&gt;
&lt;p&gt;사전 홍보를 통해 iOS 개발자 커뮤니티에서 참가 신청을 받았으며, 다양한 고민을 가진 상담자들이 방문했습니다. 주제는 올리브영 개발 조직 문화, 진로 고민, 커리어 성장 등 다양했습니다.&lt;/p&gt;
&lt;p&gt;저와 송편님, 그리고 올리브영의 새로운 앱 개발자 테드님이 멘토로써 직접 상담을 진행하며, 참가자들의 고민을 경청하고 해결책을 제안했습니다. 상담자들이 마주한 문제를 함께 고민하며 실질적인 방향성을 제시할 수 있어 의미 있는 시간이었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📝 올대면 상담소의 생생한 후기&lt;/p&gt;
&lt;p&gt;정말 선배 개발자가 꼭 필요한 조언과 따듯한 위로를 해주셨습니다.&lt;/p&gt;
&lt;p&gt;＇코드에 불합리한 부분이 있을 때, 이것을 개선하는데 비용이 드는 점을 설득하기 어렵다＇,&lt;/p&gt;
&lt;p&gt;＇자잘한 일만 하는 것 같아 이력서에 녹여낼 것이 없는 것 같다＇ 등 요즘 하는 고민을 1시간 넘게 털어놨습니다.&lt;/p&gt;
&lt;p&gt;자신의 사례를 들려주며 해주신 현실적인 조언과 속 깊은 위로가 너무 와 닿아서 그만 녹아내렸습니다.&lt;/p&gt;
&lt;p&gt;다시 한 번 감사하다는 말씀 전합니다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br&gt;
&lt;div class=&quot;photo-item&quot; style=&quot;width: 50%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a43b97fdf23e0904bbd1ee592d70e9f5/b126d/letswift_counseling.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 118.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAYABQDASIAAhEBAxEB/8QAGAABAQEBAQAAAAAAAAAAAAAAAAQBAwX/xAAWAQEBAQAAAAAAAAAAAAAAAAABAgD/2gAMAwEAAhADEAAAAbEeS2Jg+dhtwFT/AP/EABsQAQACAgMAAAAAAAAAAAAAAAEAAgQRAxAS/9oACAEBAAEFAuzIG5kCHNVnrVrtpsn/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AR//xAAbEAABBQEBAAAAAAAAAAAAAAAAAQIQMkEhMf/aAAgBAQAGPwKVTDyLcFc2pp//xAAbEAADAQEAAwAAAAAAAAAAAAABETEAIRBBcf/aAAgBAQABPyHyCEC6gO9iwslpFYBMSoUxTDD2MA3r7v/aAAwDAQACAAMAAAAQFO+C/8QAFhEBAQEAAAAAAAAAAAAAAAAAEAFR/9oACAEDAQE/EC4f/8QAGBEAAgMAAAAAAAAAAAAAAAAAABARITH/2gAIAQIBAT8Qgoxf/8QAHRABAAMAAgMBAAAAAAAAAAAAAQARITFhQXGRof/aAAgBAQABPxDiXFgYW9obnL6qZYiWpXZ82XIBBZz3C7qUqp7fMDBrWBl5ATda0APyf//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a43b97fdf23e0904bbd1ee592d70e9f5/263a4/letswift_counseling.webp 480w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/a6361/letswift_counseling.webp 960w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/0b34d/letswift_counseling.webp 1920w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/da28f/letswift_counseling.webp 2880w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/98b7d/letswift_counseling.webp 3840w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/c4ef1/letswift_counseling.webp 6477w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a43b97fdf23e0904bbd1ee592d70e9f5/a3e66/letswift_counseling.jpg 480w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/fb816/letswift_counseling.jpg 960w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/aaf92/letswift_counseling.jpg 1920w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/1d134/letswift_counseling.jpg 2880w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/9ad4e/letswift_counseling.jpg 3840w,
/static/a43b97fdf23e0904bbd1ee592d70e9f5/b126d/letswift_counseling.jpg 6477w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a43b97fdf23e0904bbd1ee592d70e9f5/aaf92/letswift_counseling.jpg&quot; alt=&quot;올대면 상담소를 통해 상담을 진행하고 있는 올리브영 개발자들&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h2 id=&quot;준비-과정에서-있었던-인상-깊었던-점은-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A4%80%EB%B9%84-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EC%9E%88%EC%97%88%EB%8D%98-%EC%9D%B8%EC%83%81-%EA%B9%8A%EC%97%88%EB%8D%98-%EC%A0%90%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;준비 과정에서 있었던 인상 깊었던 점은 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;준비 과정에서 있었던 인상 깊었던 점은 무엇인가요?&lt;/h2&gt;
&lt;p&gt;굿즈 디자인과 발주는 처음이라 시행착오가 많았습니다. 기존 이미지 리소스를 활용했지만 응용하는 것이 쉽지 않았고, 행사 전날까지 굿즈를 수령하기 위해 촉박한 일정 속에서 작업을 완료해야 했습니다.&lt;/p&gt;
&lt;p&gt;특히 여러 품목을 정해진 일정에 맞춰 준비하는 것이 생각보다 복잡한 일이었습니다. 매일 업체와 통화하며 생산 및 배송 일정을 조율한 끝에 최종적으로 문제없이 물품을 수령할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;행사 당일 일부 상품이 발주 과정에서 누락되는 등 예상치 못한 이슈도 있었지만, 팀원들의 노력 덕분에 무사히 부스를 운영할 수 있었습니다. (개발이 제일 쉬웠어요 😆)&lt;/p&gt;
&lt;h2 id=&quot;마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리하며&lt;/h2&gt;
&lt;p&gt;Let&apos;Swift 2024에서 후원과 부스 운영을 통해 올리브영 개발 조직을 더욱 널리 알리고, 많은 개발자들과 직접 소통할 수 있는 뜻깊은 기회를 가졌습니다.&lt;/p&gt;
&lt;p&gt;350명의 개발자, 예비 개발자, 그리고 Swift 생태계 관계자들이 부스를 방문하며 올리브영의 서비스와 개발 조직에 대한 인지도를 높일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;또한, ‘올대면 상담소’를 통해 참가자들은 올리브영의 기술 스택과 개발 문화를 간접적으로 경험했고, 멘토로 참여한 iOS 개발자들 역시 다양한 개발자의 고민을 듣고 조언을 제공하며 의미 있는 인사이트를 얻을 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;숫자로-보는-letswift-2024&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%AB%EC%9E%90%EB%A1%9C-%EB%B3%B4%EB%8A%94-letswift-2024&quot; aria-label=&quot;숫자로 보는 letswift 2024 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;숫자로 보는 Let&apos;Swift 2024&lt;/h2&gt;
&lt;p&gt;🎉 폭발적인 관심!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;부스 이벤트를 통해 150명 이상의 신규 고객이 올리브영 앱을 설치하며 서비스 성장의 발판을 마련했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📈 인지도 향상!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;350명의 개발자 및 관계자가 부스를 방문해 올리브영의 서비스와 개발 조직에 대한 인지도를 높였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🤝 소통과 교류의 장!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;총 3명의 지원자가 오피스아워를 통해 올리브영의 기술과 개발·채용 문화를 직접 경험했으며, 올리브영 iOS 개발자들은 멘토로서 새로운 인사이트를 얻었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🎤 기술 리더십 강화!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;윌님이 2년 연속 Let&apos;Swift 연사로 발표하며 iOS 개발 생태계에서 기술 리더십을 더욱 확고히 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;Swift 2024를 통해 많은 개발자들과 소통하며 의미 있는 시간을 보낼 수 있었습니다. 앞으로도 올리브영 개발 조직의 성장과 발전을 위해 지속적으로 노력하겠습니다! 🚀&lt;/p&gt;
&lt;h2 id=&quot;잠깐-현장-스케치도-보고-가세요-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A0%EA%B9%90-%ED%98%84%EC%9E%A5-%EC%8A%A4%EC%BC%80%EC%B9%98%EB%8F%84-%EB%B3%B4%EA%B3%A0-%EA%B0%80%EC%84%B8%EC%9A%94-&quot; aria-label=&quot;잠깐 현장 스케치도 보고 가세요  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;잠깐! 현장 스케치도 보고 가세요 📸&lt;/h2&gt;
&lt;p&gt;이번 Let&apos;Swift 2024에서 올리브영 부스를 방문해 주신 많은 개발자분들과 함께 의미 있는 시간을 보냈습니다.&lt;/p&gt;
&lt;p&gt;올리브영 개발팀과 직접 소통하며 다양한 이벤트에 참여하는 참가자들의 모습부터, 세션 발표를 성공적으로 마친 윌님, 그리고 부스 운영을 적극 지원해 주신 리더 쌈님까지!&lt;/p&gt;
&lt;p&gt;현장의 생생한 분위기를 사진으로 함께 만나보세요. 🎉&lt;/p&gt;
&lt;div class=&quot;photo-gallery&quot;&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/63582e4c54ef4b48a6dc05c72e1a4574/f90d2/letswift_01.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABAABAv/EABUBAQEAAAAAAAAAAAAAAAAAAAIB/9oADAMBAAIQAxAAAAHeRNKPKqf/xAAcEAACAgIDAAAAAAAAAAAAAAACAwERABIEEyP/2gAIAQEAAQUCM4ubVnIIxNk+yT3Y7r3/AP/EABURAQEAAAAAAAAAAAAAAAAAABEQ/9oACAEDAQE/AVn/xAAYEQACAwAAAAAAAAAAAAAAAAABEBESIf/aAAgBAgEBPwGsDF//xAAeEAABBAEFAAAAAAAAAAAAAAAAAQIRIRITImFxcv/aAAgBAQAGPwLSx4mRGtu7ExWKHeiYs3LfR//EABsQAQADAAMBAAAAAAAAAAAAAAEAESFBYXGx/9oACAEBAAE/IdNpbQIr+TyGzhLVze9EsjbnsfHA7T//2gAMAwEAAgADAAAAEDAf/8QAFxEAAwEAAAAAAAAAAAAAAAAAARARIf/aAAgBAwEBPxCi1f/EABYRAQEBAAAAAAAAAAAAAAAAAAEAcf/aAAgBAgEBPxABEMv/xAAdEAEAAgICAwAAAAAAAAAAAAABABEhMUFxkbHh/9oACAEBAAE/ELVSITLXiq1qJ+dkprjC8yyqRaLbUwppEnT8gCADggRd08zCAaIe6p//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/63582e4c54ef4b48a6dc05c72e1a4574/263a4/letswift_01.webp 480w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/a6361/letswift_01.webp 960w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/0b34d/letswift_01.webp 1920w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/da28f/letswift_01.webp 2880w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/98b7d/letswift_01.webp 3840w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/65c09/letswift_01.webp 4096w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/63582e4c54ef4b48a6dc05c72e1a4574/a3e66/letswift_01.jpg 480w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/fb816/letswift_01.jpg 960w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/aaf92/letswift_01.jpg 1920w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/1d134/letswift_01.jpg 2880w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/9ad4e/letswift_01.jpg 3840w,
/static/63582e4c54ef4b48a6dc05c72e1a4574/f90d2/letswift_01.jpg 4096w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/63582e4c54ef4b48a6dc05c72e1a4574/aaf92/letswift_01.jpg&quot; alt=&quot;올리브영 부스 이벤트에 즐겁게 참여 중인 참가자&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c17d4416a7b112f559c644e70b96a304/bb5f8/letswift_02.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAgMF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAHaqsE8c4Nf/8QAGRAAAwEBAQAAAAAAAAAAAAAAAQIDABES/9oACAEBAAEFAjTezg6nTbq1YtMA8//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABURAQEAAAAAAAAAAAAAAAAAABAR/9oACAECAQE/AYf/xAAbEAABBQEBAAAAAAAAAAAAAAAAAQIRIUFRYv/aAAgBAQAGPwJYwskXzYzjzD//xAAcEAEAAgIDAQAAAAAAAAAAAAABABExQSFRgXH/2gAIAQEAAT8hDcsbgB8x7LJcq5qMFyGncrvZ5UJ0OJ//2gAMAwEAAgADAAAAEDzv/8QAFhEBAQEAAAAAAAAAAAAAAAAAABEh/9oACAEDAQE/EKx//8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERIf/aAAgBAgEBPxDDhD//xAAdEAEAAgICAwAAAAAAAAAAAAABESEAMUHBUWGx/9oACAEBAAE/EIYI0gc0PeBiM3MoHvTNtIUSnGt0PwIg+4kQGbNNFV53k0LM2da6z//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c17d4416a7b112f559c644e70b96a304/263a4/letswift_02.webp 480w,
/static/c17d4416a7b112f559c644e70b96a304/a6361/letswift_02.webp 960w,
/static/c17d4416a7b112f559c644e70b96a304/0b34d/letswift_02.webp 1920w,
/static/c17d4416a7b112f559c644e70b96a304/da28f/letswift_02.webp 2880w,
/static/c17d4416a7b112f559c644e70b96a304/98b7d/letswift_02.webp 3840w,
/static/c17d4416a7b112f559c644e70b96a304/e7055/letswift_02.webp 4144w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c17d4416a7b112f559c644e70b96a304/a3e66/letswift_02.jpg 480w,
/static/c17d4416a7b112f559c644e70b96a304/fb816/letswift_02.jpg 960w,
/static/c17d4416a7b112f559c644e70b96a304/aaf92/letswift_02.jpg 1920w,
/static/c17d4416a7b112f559c644e70b96a304/1d134/letswift_02.jpg 2880w,
/static/c17d4416a7b112f559c644e70b96a304/9ad4e/letswift_02.jpg 3840w,
/static/c17d4416a7b112f559c644e70b96a304/bb5f8/letswift_02.jpg 4144w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c17d4416a7b112f559c644e70b96a304/aaf92/letswift_02.jpg&quot; alt=&quot;참가자들과 적극적으로 소통하는 올리브영 iOS 개발자&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/04f43395832bc7f78bc9b55a30b489e4/797f2/letswift_03.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAgX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/2gAMAwEAAhADEAAAAaMKVm1nND//xAAbEAACAgMBAAAAAAAAAAAAAAABAwACERITIf/aAAgBAQABBQLoDA33oI41o9g0ZZZz/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQIBAT8BiP/EABsQAAIDAAMAAAAAAAAAAAAAAAABESIxEjJy/9oACAEBAAY/Aq6KUdkLzBxLaf/EABsQAQADAQADAAAAAAAAAAAAAAEAETEhQVFh/9oACAEBAAE/IW4UaqEXnF6bEVMvkEXmVEYwcKqvdQGh5Uz/2gAMAwEAAgADAAAAEDsv/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8Qqv/EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/ECX/xAAbEAEBAAMAAwAAAAAAAAAAAAABEQAhMUFh8P/aAAgBAQABPxAZauiEPLOZACCBpZe4uTUm0udUJWjVhlyDhzwO37eHqeUe1z//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/04f43395832bc7f78bc9b55a30b489e4/263a4/letswift_03.webp 480w,
/static/04f43395832bc7f78bc9b55a30b489e4/a6361/letswift_03.webp 960w,
/static/04f43395832bc7f78bc9b55a30b489e4/0b34d/letswift_03.webp 1920w,
/static/04f43395832bc7f78bc9b55a30b489e4/b6de4/letswift_03.webp 2182w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/04f43395832bc7f78bc9b55a30b489e4/a3e66/letswift_03.jpg 480w,
/static/04f43395832bc7f78bc9b55a30b489e4/fb816/letswift_03.jpg 960w,
/static/04f43395832bc7f78bc9b55a30b489e4/aaf92/letswift_03.jpg 1920w,
/static/04f43395832bc7f78bc9b55a30b489e4/797f2/letswift_03.jpg 2182w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/04f43395832bc7f78bc9b55a30b489e4/aaf92/letswift_03.jpg&quot; alt=&quot;올리브영 부스 이벤트에 참여 중인 참가자&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4105db206e10e5ae3750e2ea8019ba54/ec68a/letswift_04.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIEA//EABYBAQEBAAAAAAAAAAAAAAAAAAEAAv/aAAwDAQACEAMQAAABckxy0iDf/8QAGRAAAwEBAQAAAAAAAAAAAAAAAQIDEQAS/9oACAEBAAEFAjQaWXmoAXT3VRg2K9//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAdEAACAQQDAAAAAAAAAAAAAAAAEQEDEiExQWGh/9oACAEBAAY/AlKNmIZV6HZvliXh/8QAGxABAQACAwEAAAAAAAAAAAAAAREAMSFRYYH/2gAIAQEAAT8hCno3znJoJ7k257HJ+usO8YlVQOnzHUH1x//aAAwDAQACAAMAAAAQCM//xAAWEQEBAQAAAAAAAAAAAAAAAAAAARH/2gAIAQMBAT8Qta//xAAXEQADAQAAAAAAAAAAAAAAAAAAASER/9oACAECAQE/EEpTD//EAB0QAQACAQUBAAAAAAAAAAAAAAEAESExQXGRobH/2gAIAQEAAT8Q6OJPPIG27dXyXKssMD3BWJbLdWLYQIUMFLWrmPZEsUKts//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4105db206e10e5ae3750e2ea8019ba54/263a4/letswift_04.webp 480w,
/static/4105db206e10e5ae3750e2ea8019ba54/a6361/letswift_04.webp 960w,
/static/4105db206e10e5ae3750e2ea8019ba54/0b34d/letswift_04.webp 1920w,
/static/4105db206e10e5ae3750e2ea8019ba54/da28f/letswift_04.webp 2880w,
/static/4105db206e10e5ae3750e2ea8019ba54/98b7d/letswift_04.webp 3840w,
/static/4105db206e10e5ae3750e2ea8019ba54/9d3b2/letswift_04.webp 4095w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4105db206e10e5ae3750e2ea8019ba54/a3e66/letswift_04.jpg 480w,
/static/4105db206e10e5ae3750e2ea8019ba54/fb816/letswift_04.jpg 960w,
/static/4105db206e10e5ae3750e2ea8019ba54/aaf92/letswift_04.jpg 1920w,
/static/4105db206e10e5ae3750e2ea8019ba54/1d134/letswift_04.jpg 2880w,
/static/4105db206e10e5ae3750e2ea8019ba54/9ad4e/letswift_04.jpg 3840w,
/static/4105db206e10e5ae3750e2ea8019ba54/ec68a/letswift_04.jpg 4095w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4105db206e10e5ae3750e2ea8019ba54/aaf92/letswift_04.jpg&quot; alt=&quot;많은 참가자가 몰린 올리브영 부스&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f4ec0157dd6c5d78760bcae8e5ace152/00420/letswift_05.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAgMF/8QAFgEBAQEAAAAAAAAAAAAAAAAAAgED/9oADAMBAAIQAxAAAAGsaKZJw4JV/8QAHRAAAgIBBQAAAAAAAAAAAAAAAQMAAgQREhMhIv/aAAgBAQABBQJXa4DqOVdRkbbL9T//xAAYEQACAwAAAAAAAAAAAAAAAAAAAhIhUf/aAAgBAwEBPwGiS4f/xAAYEQACAwAAAAAAAAAAAAAAAAAAARIxUf/aAAgBAgEBPwGyL0//xAAdEAABBAIDAAAAAAAAAAAAAAABABAREgIhMVFx/9oACAEBAAY/ArHkKSG68CvjOy3/xAAaEAEBAQEBAQEAAAAAAAAAAAABEQAxIXGR/9oACAEBAAE/IUpFKzKhJHzuKg6mk48wuoEW76f3f//aAAwDAQACAAMAAAAQ3M//xAAZEQEAAgMAAAAAAAAAAAAAAAABABEhUZH/2gAIAQMBAT8QANuYbHZ//8QAFxEAAwEAAAAAAAAAAAAAAAAAAAEhEf/aAAgBAgEBPxB60gwf/8QAHBABAQACAgMAAAAAAAAAAAAAAREAITFBUXGR/9oACAEBAAE/EG9sx2JZV84CAdqQE6wuKcM3HFdqnYgh9xZYWocDdesooDj/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f4ec0157dd6c5d78760bcae8e5ace152/263a4/letswift_05.webp 480w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/a6361/letswift_05.webp 960w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/0b34d/letswift_05.webp 1920w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/da28f/letswift_05.webp 2880w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/98b7d/letswift_05.webp 3840w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/c13be/letswift_05.webp 5712w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f4ec0157dd6c5d78760bcae8e5ace152/a3e66/letswift_05.jpg 480w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/fb816/letswift_05.jpg 960w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/aaf92/letswift_05.jpg 1920w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/1d134/letswift_05.jpg 2880w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/9ad4e/letswift_05.jpg 3840w,
/static/f4ec0157dd6c5d78760bcae8e5ace152/00420/letswift_05.jpg 5712w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f4ec0157dd6c5d78760bcae8e5ace152/aaf92/letswift_05.jpg&quot; alt=&quot;올리브영 부스 전경&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;div class=&quot;photo-item&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1438px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4fdf33c247888b4a06643b733f6bf3ca/a1e9e/letswift_06.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 133.33333333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAbABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAQX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/2gAMAwEAAhADEAAAAWNQrNtNCWDt6LKw/8QAHBAAAgICAwAAAAAAAAAAAAAAAAECAxETECEx/9oACAEBAAEFAk+MFUZRHZrMo7Vig53akS8SQj//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AR//xAAWEQEBAQAAAAAAAAAAAAAAAAAQASH/2gAIAQIBAT8BJh//xAAbEAABBQEBAAAAAAAAAAAAAAABAAIQESFBIv/aAAgBAQAGPwKact2A44zqsjyuoz//xAAcEAEAAgMBAQEAAAAAAAAAAAABABEhMUFRkbH/2gAIAQEAAT8hq3k9hKSw6ajsGZa7LY5wG1fkSJMqs5yDF39QDceTAxNJ/9oADAMBAAIAAwAAABBf277/xAAXEQADAQAAAAAAAAAAAAAAAAAAEBEh/9oACAEDAQE/EFMhT//EABURAQEAAAAAAAAAAAAAAAAAABBR/9oACAECAQE/ECB//8QAHxABAQABBAIDAAAAAAAAAAAAAREAITFBYXGRUaHw/9oACAEBAAE/ENrU5RE84ZpdMUW33jT1dLGze77YYFYCkefj1gAX7MZWAm89N+s1EcFD9XC0NLOGAgJ07wUBmGZ//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4fdf33c247888b4a06643b733f6bf3ca/263a4/letswift_06.webp 480w,
/static/4fdf33c247888b4a06643b733f6bf3ca/a6361/letswift_06.webp 960w,
/static/4fdf33c247888b4a06643b733f6bf3ca/e40a7/letswift_06.webp 1438w&quot; sizes=&quot;(max-width: 1438px) 100vw, 1438px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4fdf33c247888b4a06643b733f6bf3ca/a3e66/letswift_06.jpg 480w,
/static/4fdf33c247888b4a06643b733f6bf3ca/fb816/letswift_06.jpg 960w,
/static/4fdf33c247888b4a06643b733f6bf3ca/a1e9e/letswift_06.jpg 1438w&quot; sizes=&quot;(max-width: 1438px) 100vw, 1438px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4fdf33c247888b4a06643b733f6bf3ca/a1e9e/letswift_06.jpg&quot; alt=&quot;올리브영 iOS 개발자 윌님과 쌈님&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;더-깊이-알아보기-올리브영-앱-개발-조직의-기술-콘텐츠&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%94-%EA%B9%8A%EC%9D%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%A1%B0%EC%A7%81%EC%9D%98-%EA%B8%B0%EC%88%A0-%EC%BD%98%ED%85%90%EC%B8%A0&quot; aria-label=&quot;더 깊이 알아보기 올리브영 앱 개발 조직의 기술 콘텐츠 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;더 깊이 알아보기: 올리브영 앱 개발 조직의 기술 콘텐츠&lt;/h2&gt;
&lt;p&gt;Let&apos;Swift 2024를 통해 올리브영 개발 조직과 기술 문화에 대해 많은 관심을 받았는데요,&lt;/p&gt;
&lt;p&gt;올리브영의 앱 개발과 일하는 방식에 대해 더 깊이 있는 이야기가 궁금하시다면 아래 콘텐츠를 확인해보세요!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-04-02/app-improve-camera-scan/&quot;&gt;올리브영 앱 스마트 스캐너 개선&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-11-15/swift-macro-part1/&quot;&gt;스위프트 매크로_1탄, 스위프트 매크로가 뭐예요?&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-01-15/swift-macro-part2/&quot;&gt;스위프트 매크로_2탄, 어떻게 쓰는건데요?&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2022-12-15/Mobile-Clean-Architecture/&quot;&gt;올리브영 앱 - 아키텍처 도입 1탄&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-05-20/OliveYoung-iOS-ReactorKit/&quot;&gt;iOS ReactorKit 톺아보기&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;앞으로도 올리브영 개발 조직은 기술적 성장을 이어가며, 더 많은 경험과 노하우를 공유할 예정입니다. 많은 관심과 기대 부탁드립니다! 🚀&lt;/p&gt;
&lt;style&gt;
  .photo-gallery {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
    margin-top: 20px;
  }

  .photo-item {
    position: relative;
    overflow: hidden;
    border-radius: 12px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    transition: transform 0.3s ease, box-shadow 0.3s ease;
  }

  .photo-item img {
    width: 100%;
    height: auto;
    display: block;
    object-fit: cover;
    transition: transform 0.3s ease;
  }

  .photo-item:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
  }

  .photo-item:hover img {
    transform: scale(1.05);
  }
&lt;/style&gt;</content:encoded></item><item><title><![CDATA[올리브영 글로벌몰 주소 자동완성 및 검증 솔루션 도입기]]></title><description><![CDATA[안녕하세요, 올리브영 글로벌몰 PO ZㅣZㅣ입니다. 🙋🏻‍♀️ 올리브영 글로벌몰을 잠깐만 소개할게요. "안녕하세요, 올리브영입니다. 필요하신 물건 있으면 말씀해주세요" 이는 한국에 있는 1,35…]]></description><link>https://oliveyoung.tech/2025-02-14/oy-global-mall-address/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-02-14/oy-global-mall-address/</guid><pubDate>Fri, 14 Feb 2025 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 올리브영 글로벌몰 PO ZㅣZㅣ입니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h1 id=&quot;️-올리브영-글로벌몰을-잠깐만-소개할게요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EA%B8%80%EB%A1%9C%EB%B2%8C%EB%AA%B0%EC%9D%84-%EC%9E%A0%EA%B9%90%EB%A7%8C-%EC%86%8C%EA%B0%9C%ED%95%A0%EA%B2%8C%EC%9A%94&quot; aria-label=&quot;️ 올리브영 글로벌몰을 잠깐만 소개할게요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🙋🏻‍♀️ 올리브영 글로벌몰을 잠깐만 소개할게요.&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;안녕하세요, 올리브영입니다. 필요하신 물건 있으면 말씀해주세요&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이는 한국에 있는 1,350여 개 올리브영 오프라인 매장을 방문하면, 매장 직원들이 고객을 반갑게 맞이할 때 건네는 환영 인사입니다. 여러분도 한번쯤 들어봤을 법한 멘트죠. 그런데 여러분, 한국에 올리브영 매장과 온라인몰이 있다면, 해외에는 올리브영 글로벌몰이 있다는 사실을 알고 계신가요? &lt;strong&gt;2019년부터 운영 중인 올리브영 글로벌몰은 모든 국가가 동시에 사용할 수 있는 이커머스 플랫폼입니다.&lt;/strong&gt; 현재 미국, 일본, 영국, 홍콩 등 각국의 글로벌 고객이 K-뷰티, K-POP 상품을 구매할 수 있는 역직구몰입니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;-글로벌몰-주문서에-고객-경험을-해치는-불편함이-있다구요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B8%80%EB%A1%9C%EB%B2%8C%EB%AA%B0-%EC%A3%BC%EB%AC%B8%EC%84%9C%EC%97%90-%EA%B3%A0%EA%B0%9D-%EA%B2%BD%ED%97%98%EC%9D%84-%ED%95%B4%EC%B9%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%A8%EC%9D%B4-%EC%9E%88%EB%8B%A4%EA%B5%AC%EC%9A%94&quot; aria-label=&quot; 글로벌몰 주문서에 고객 경험을 해치는 불편함이 있다구요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚨 글로벌몰 주문서에 고객 경험을 해치는 불편함이 있다구요?&lt;/h1&gt;
&lt;p&gt;고객은 상품을 탐색하고 국가를 넘나들며 장바구니에 상품을 담기도 하고 배송지 입력 후 결제합니다. 그러면 비로소 주문이 완료됩니다. 이 과정을 거치고 나면, 빨리 배송되기를 바라며 &apos;설렘&apos;은 더욱 증가하죠. &lt;strong&gt;하지만 이때 배송 보류되어 본 경험이 있으신가요?&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;고객님의 주문은 배송보류되었습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;듣기만 해도 머릿속이 복잡해집니다. 이때 설렘은 금세 &apos;실망&apos;과 &apos;불만&apos;으로 바뀌게 되죠. 마주하기 싫은 무언가를 만난 느낌이랄까요? 비슷한 경험을 겪어본 적이 있다면, 누구나 공감할 경험일 것입니다. 배송 보류의 원인은 다양합니다. 주소 불일치, 재고 문제, 택배사 문제, 기타 검수 등으로 인해 발생할 수 있습니다. 이러한 경험은 고객에게 실망을 안기고, 다음 주문을 망설이게 만드는 원인이 되곤 합니다. 그러나 &lt;strong&gt;글로벌몰에서 주소 불일치로 인한 배송보류는 자주 발생하는 문제였습니다.&lt;/strong&gt; 왜였을까요? 바로 &lt;strong&gt;부정확한 주소 때문&lt;/strong&gt;이었습니다. 그럴 만도 한 것이, 주문을 위해 5가지 배송지 정보를 한땀한땀 기입해야 했기 때문이죠.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 78.75000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAACXUlEQVR42n1USW/TYBDtXy8H4NBLQUJI6QkVOKQLFNpSpEocQSVilSKhii6k4MSOd3+bl8fMxI4IkDia2J+f531v3oy9prRFXmg0DXB48gbrdzaxsdnDw60n2LjXw62793FwfIqDo1Os355hD3rbhG0JdkQ5ZVWjUAbMtcZ/vOCbZ4PP2H9xgp1nR9h+uov+7kvsP3+F94MveHf2kbDX6O8d4tHjHTnvETb48BXWVYuE81AaWivkeYqydOiOqm4IV4TnElVVzTEW8idHS2igTYkw9nF+McSNd400nyJTEdJ4DFUkCKIxzr8PcXH1DUHoIVcJYb9o81hymWNBodaOvEzxY3RJCRMUOqMHcxREZp1GmsUY3RA2HRNZOsMUk5Fyyv1L4SyMdUjiBKUrYY1FmiRwzlL5FQyvU7aihCE1aTK7Zuw/JXcqjXhoDMlXSqIqGwr2ScuaMd1eW2tIvVtNmCSxqOIx4sYMJ3142SdSVSMmzFqLuq6lMRyuLJcRkofkWxAEmFIUhZIu/szfItKXcLamkmd21E2Nhn58uOUlO8RFICoCP0AURVJaUzVEUlFpVjxrSDorZD/5GemuWdIUrax4E8cxfN+nkcilfPbNUcOYSFSRb1mWyXlFyVaMZ7KIIs+LdnSpKaQyNSGNTkIec9muRbC6y+xbOJ3KWEgClcdRVvxqFWIBbzgh9Qn5KdPQDvQ/hJp8YF88z5Pg5I6UVfBmnW9cbrfZ0pIL8o9BHgU2n/3qkmp6lwvyNgzD1js732ylh7pVKtGtKSaTAFfXI/hBKJ86/rp0zy32wOI35kLBjzii2zcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ff30c244a74f6a7de90bef5330fe07d2/263a4/global-mall-adress-01.webp 480w,
/static/ff30c244a74f6a7de90bef5330fe07d2/a6361/global-mall-adress-01.webp 960w,
/static/ff30c244a74f6a7de90bef5330fe07d2/0b34d/global-mall-adress-01.webp 1920w,
/static/ff30c244a74f6a7de90bef5330fe07d2/cc271/global-mall-adress-01.webp 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ff30c244a74f6a7de90bef5330fe07d2/9aebd/global-mall-adress-01.png 480w,
/static/ff30c244a74f6a7de90bef5330fe07d2/a91f8/global-mall-adress-01.png 960w,
/static/ff30c244a74f6a7de90bef5330fe07d2/ac7a9/global-mall-adress-01.png 1920w,
/static/ff30c244a74f6a7de90bef5330fe07d2/a3249/global-mall-adress-01.png 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ff30c244a74f6a7de90bef5330fe07d2/ac7a9/global-mall-adress-01.png&quot; alt=&quot;global mall adress 01&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;그림. 올리브영 글로벌몰 기존 주문서 화면 ⓒ 2025. Oliveyoung. All rights reserved&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
그리고 이러한 주문 프로세스가 각종 VOC(Voice of Customer)로 이어졌습니다.
&lt;br&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;주소를 잘못 입력했어요. 수정해도 되나요?&lt;/li&gt;
&lt;li&gt;제 주문은 왜 배송이 안 되나요?&lt;/li&gt;
&lt;li&gt;주문한 지 며칠이 지났지만 아직도 배송 출발이 안 됐어요.&lt;/br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
이를 계기로 &apos;주소 입력 방식을 최적화하여 시간을 줄이자&apos;는 목표를 갖고 주소 자동완성/검증 기능을 도입하게 되었습니다. 지금부터 그 문제에 어떻게 접근하고 해결했는지 공유해보려 합니다.
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-문제-진단--주소-입력-과정에서-불편을-겪고-있었어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%AC%B8%EC%A0%9C-%EC%A7%84%EB%8B%A8--%EC%A3%BC%EC%86%8C-%EC%9E%85%EB%A0%A5-%EA%B3%BC%EC%A0%95%EC%97%90%EC%84%9C-%EB%B6%88%ED%8E%B8%EC%9D%84-%EA%B2%AA%EA%B3%A0-%EC%9E%88%EC%97%88%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 문제 진단  주소 입력 과정에서 불편을 겪고 있었어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔍 문제 진단 : 주소 입력 과정에서 불편을 겪고 있었어요.&lt;/h1&gt;
&lt;/br&gt;
고객의 주문이 배송 보류된 이유를 크게 고객 관점과 운영 관점으로 나눠 분석했습니다.
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-고객-관점---주문서-입력이-너무-번거롭고-불편했어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B3%A0%EA%B0%9D-%EA%B4%80%EC%A0%90---%EC%A3%BC%EB%AC%B8%EC%84%9C-%EC%9E%85%EB%A0%A5%EC%9D%B4-%EB%84%88%EB%AC%B4-%EB%B2%88%EA%B1%B0%EB%A1%AD%EA%B3%A0-%EB%B6%88%ED%8E%B8%ED%96%88%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 고객 관점   주문서 입력이 너무 번거롭고 불편했어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;① 고객 관점 - 주문서 입력이 너무 번거롭고 불편했어요.&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;복잡한 입력 : 5가지 배송지 정보를 수기 입력해야 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;오타 및 오류 : 수기로 입력하는 과정에서 사소한 오타가 빈번히 발생했습니다.&lt;/p&gt;
&lt;p&gt;예시:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;우편번호가 &apos;13090&apos;이지만 &apos;13000&apos;으로 오입력&lt;/li&gt;
&lt;li&gt;주가 &apos;Alberta&apos;지만 &apos;Albertta&apos;로 오입력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-운영-관점---주소-오류로-하루-평균-3040건의-배송보류가-발생했어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9A%B4%EC%98%81-%EA%B4%80%EC%A0%90---%EC%A3%BC%EC%86%8C-%EC%98%A4%EB%A5%98%EB%A1%9C-%ED%95%98%EB%A3%A8-%ED%8F%89%EA%B7%A0-3040%EA%B1%B4%EC%9D%98-%EB%B0%B0%EC%86%A1%EB%B3%B4%EB%A5%98%EA%B0%80-%EB%B0%9C%EC%83%9D%ED%96%88%EC%96%B4%EC%9A%94&quot; aria-label=&quot; 운영 관점   주소 오류로 하루 평균 3040건의 배송보류가 발생했어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;② 운영 관점 - 주소 오류로 하루 평균 30~40건의 배송보류가 발생했어요.&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;배송 리드타임(주문완료~배송완료 시간) 지연 및 증가&lt;/li&gt;
&lt;li&gt;배송지연 관련 문의 급증&lt;/li&gt;
&lt;li&gt;SCM팀과 CX팀의 반복적인 운영 리소스 낭비&lt;/li&gt;
&lt;li&gt;고객 불만 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABaElEQVR42pVR2U7DQAzM//8LUGgBUegLbyCExCfQqodEmjvZ7DnYTlKlkSqBpdHau+PxsdHbxxcQAkKaImQ5IQMm8PTmk0T88XsYxbpp4JxHdHU9QzAWYbtF2HwjHLbAfkfYdzgcoDcb6PVafMHobYiL+Iim1YgW90uweaehs08U2TuC/0FnAWPzFBa1QqU0LPlpUSGv6j7fwXvq8Gb+1F1YC6USFPkOzioY6lprTUSPlior1Ypf143Ak7rWRjhngrNF3yEFWVHj55jDWI+yapDlJSwRC+qEfd5RxR0SguQEyTsTvHt46S88/U2QJBmWfSJwYlnWSDMuZMVn4JLg7eL5JMgEY52IdOJO3loaTdHYljjdmEb2yQVtzzkJzh9XcjGMk6S57M/1hEvGxXNaBeec73D4lL5DvuTuGINN42HnPHrN++RprJP8aLl6PRH+YzzRMUn7z3IkaGVV0bTy1IwxiONYzr/YL2w/V2iL3NwKAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f00b2dfa793e05d3636138febd0728ef/263a4/global-mall-adress-02.webp 480w,
/static/f00b2dfa793e05d3636138febd0728ef/a6361/global-mall-adress-02.webp 960w,
/static/f00b2dfa793e05d3636138febd0728ef/0b34d/global-mall-adress-02.webp 1920w,
/static/f00b2dfa793e05d3636138febd0728ef/cc271/global-mall-adress-02.webp 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f00b2dfa793e05d3636138febd0728ef/9aebd/global-mall-adress-02.png 480w,
/static/f00b2dfa793e05d3636138febd0728ef/a91f8/global-mall-adress-02.png 960w,
/static/f00b2dfa793e05d3636138febd0728ef/ac7a9/global-mall-adress-02.png 1920w,
/static/f00b2dfa793e05d3636138febd0728ef/a3249/global-mall-adress-02.png 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f00b2dfa793e05d3636138febd0728ef/ac7a9/global-mall-adress-02.png&quot; alt=&quot;global mall adress 02&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;그림. 올리브영 글로벌몰 운영 프로세스 ⓒ 2025. Oliveyoung. All rights reserved&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-문제-분석--고객이-신속정확하게-주소를-기입할-방법은-없을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%AC%B8%EC%A0%9C-%EB%B6%84%EC%84%9D--%EA%B3%A0%EA%B0%9D%EC%9D%B4-%EC%8B%A0%EC%86%8D%EC%A0%95%ED%99%95%ED%95%98%EA%B2%8C-%EC%A3%BC%EC%86%8C%EB%A5%BC-%EA%B8%B0%EC%9E%85%ED%95%A0-%EB%B0%A9%EB%B2%95%EC%9D%80-%EC%97%86%EC%9D%84%EA%B9%8C&quot; aria-label=&quot; 문제 분석  고객이 신속정확하게 주소를 기입할 방법은 없을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👀 문제 분석 : 고객이 신속/정확하게 주소를 기입할 방법은 없을까?&lt;/h1&gt;
&lt;p&gt;이 주제를 가지고 많은 고민을 했던 것 같습니다. 고객이 주문서를 귀찮아 하거나 어렵게 대하지 않는 게 목표였죠.&lt;/p&gt;
&lt;p&gt;그래서 해외 솔루션을 연동해 &lt;strong&gt;&apos;주소자동완성&apos;기능을 도입&lt;/strong&gt;하기로 했습니다.&lt;/p&gt;
&lt;p&gt;단 2자만 입력해도 내가 사는 지역이 주르룩 나오는 편리함! 고객의 시간 절약과 더불어 많은 운영 리소스를 줄일 수 있을 것이라 생각했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;-주소-자동완성-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A3%BC%EC%86%8C-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1-%EB%8F%84%EC%9E%85&quot; aria-label=&quot; 주소 자동완성 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;① 주소 자동완성 도입&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;글로벌몰은 원래 주소 검색기능이 없었을까요? 아니요. 제공되고 있었습니다. 딱 &apos;미국&apos;만을 대상으로 제공하고 있었죠. 그것도 &apos;우편번호&apos;로만 검색하도록요. 구체적인 주소 정보는 안내하지 못했지만 주/도시/우편번호 정보를 제한적으로 검색할 수 있었습니다. 미국의 주소입력 프로세스는 총 5단계로, 아래와 같았습니다.
&lt;/br&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;우편번호를 입력한다.&lt;/li&gt;
&lt;li&gt;[Apply] 버튼을 클릭한다.&lt;/li&gt;
&lt;li&gt;추천된 도시 정보(City)목록에서 선택한다.&lt;/li&gt;
&lt;li&gt;기본주소(Address Line1)를 기입한다.&lt;/li&gt;
&lt;li&gt;상세주소(Address Line2)를 기입한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;p&gt;이는 수기입력하는 것보다 복잡함이 겨우 한 단계 낮은 수준이었습니다. 아래 수기입력하는 프로세스도 한번 보시죠.&lt;/p&gt;
&lt;/br&gt;
&lt;ol&gt;
&lt;li&gt;우편번호를 입력한다.&lt;/li&gt;
&lt;li&gt;기본주소(Address Line1)를 기입한다.&lt;/li&gt;
&lt;li&gt;상세주소(Address Line2)를 기입한다.&lt;/li&gt;
&lt;li&gt;도시(City)를 기입한다.&lt;/li&gt;
&lt;li&gt;주(State/Province)를 기입한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 78.75000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAADsUlEQVR42nWT6U+bRxCH33+0LSptCZcxl21MOIKAYI5ifOILYxtjjkISwlFOG7AdTnMFCKFJ+yFQkVCJQInUovjpeKWqElFXWu28787M7v7mGe3d2TnJVJoXa2u8Pv2FjY1NUuk0x69O2NrOsLqaZP/gJQcvD5W9u7un9pLJFFtb27w6OVGxq8kkZ5JLm52bIz8/D71eRygcoayshKKiB/j8Aerq6ygsLqSrs4OODgvf5H1FW1srLpeTvG+/pqbGhNfno7yijGLxi8cTaJlMBn+gj64eK4MjIwQjwzjdbvz9AVoet2M0m4mMRHkyMU1fMITL42J0/AnuXh8DMT+xoTDmGjM6fTHxxCLadmYbl9tLx48WhkcH8QfD2GzdBEMeWltaMZqqiY2FWVpdY2Exjsdv5+2vv5Ebf3HL5NzPGIy1VBvLia9IwqWlOKWlpZiMJoaHR6gxGakor2AgEpXntVFZWYXd5sBut1H4oABLWzubm5ukUylSqZzubzm7+IOLiw9cXV2jLSwsUlJSgtFoIDIwgNFglCSVBAIBGhrqRVOd3NiG1dojfsV0dnXiFkm+y88Xvwo2tza4/njF5eUlH69v0HKVtDsc9PZ6mJ2dw+/3i+huJienCEuRnE4no6OjjIluNrudwcEYk1NT8t+l9hcXl4jFhpR9eHSMluW/kc1mv7DvPmf58/aW6+tr7u7uvvS7F6+dvTsXLdKsra9zevpGcZhOv+Dk5DWZzI7wlWJ//0DNlZVV9vb2OT7+l8OM+J2yvr6hYs7Pf0ebX1ygsLCAqqoK0TAiayVlwmROw7r6h6JbET2CVLe1m4KCH2gXHt29brG/p6GxXnj1oS8vU37xhHCYWF6m2lCFudZMVLSolbVcHILBIBaLRR3kcNjx+bzKtkri/lBIHdzS0qy0q6t7SFV1peoW7fDoiNGfxhl7OsVSYgWnpw+31y8gT9L82ILBVKu6xuFwUVKqo729QxVOr9fTLJz2h3IJhQadjkRiGS0pPDU2Nqr5fOI50aERdYPpmRl5Xge19U2EB2IsxZcZGXvKxPQ884lV6agYza1thITXR81thKNDou0rtHg8jk6yGwzVRKODVMvVi4uK5BbSeq0tsleqNMxplXtmT49N8WoQbpuaHsnh/ZhMJtV+ucJouUrloM1xODc/j8frVRxOT8+Ijv3SIXbGhcHc7OzqUoc+ezYhMVbCkQGmxM/j8Sj4d3Z2hcPs/3Covj/z6e8sVzef+PD+PTc3N9wf9+P/AQc3xTgFo5Q7AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/11b75de2cacf419dbbb877f32b0d33b7/263a4/global-mall-adress-03.webp 480w,
/static/11b75de2cacf419dbbb877f32b0d33b7/a6361/global-mall-adress-03.webp 960w,
/static/11b75de2cacf419dbbb877f32b0d33b7/0b34d/global-mall-adress-03.webp 1920w,
/static/11b75de2cacf419dbbb877f32b0d33b7/cc271/global-mall-adress-03.webp 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/11b75de2cacf419dbbb877f32b0d33b7/9aebd/global-mall-adress-03.png 480w,
/static/11b75de2cacf419dbbb877f32b0d33b7/a91f8/global-mall-adress-03.png 960w,
/static/11b75de2cacf419dbbb877f32b0d33b7/ac7a9/global-mall-adress-03.png 1920w,
/static/11b75de2cacf419dbbb877f32b0d33b7/a3249/global-mall-adress-03.png 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/11b75de2cacf419dbbb877f32b0d33b7/ac7a9/global-mall-adress-03.png&quot; alt=&quot;global mall adress 03&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;그림. 미국&amp;amp;그외 국가 배송지 입력 프로세스 ⓒ 2025. Oliveyoung. All rights reserved&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;전세계가 사용하는 글로벌몰 중 딱 1개국만, 그것도 주/도시/우편번호에 제한된 검색 기능이라는 점에서 많은 개선점이 있다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;그래서 도로명과 건물명을 검색하면 더 정확한 주소가 나오도록 &lt;strong&gt;솔루션 기반 &apos;주소 자동완성&apos; 기능을 도입&lt;/strong&gt; 하게 되었습니다.&lt;/p&gt;
&lt;p&gt;만족도 높은 검색 결과와 검색 시간을 제공하기 위해서는 먼저 주소 자동완성 기능을 다양한 국가로 확장하려면 해외 주소체계에 대한 이해가 필요했습니다.&lt;/p&gt;
&lt;p&gt;한국 주소체계와 달리, 해외는 문장의 뒤로 갈수록 더 상위 주소 범주가 나오기 때문에 고객에게 어떤 입력 방식을 제공할지 고민하며 &lt;strong&gt;해외 주소 입력 체계를 분석&lt;/strong&gt; 했습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;/br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 30.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZUlEQVR42lWRPUsDQRCG/VtaCf4AEREsLCzEQrDwB1jZaGFjZ2OhhSCCFhKECNGgIhpITETQROXUM+aSaOJ97u5dHndP/Fp4mdlhefadmT4/iBBSYdcdjo7PyeXPuKlZuGFM15OpPnyBG8gv6dzTMQgVfigJhSKMzF2k6nO9EHN297IMDI3SPzjM/NIqexc22/kamcIzmbM7ciWbw6LF8aVWscZ55YFC5Y6bezuF/gA9PyJOoHh5zcLiCvMLy+xnj9JPpJAkSaKjIFaKXk8/7PVQUqBEpPMEEQZEQv4FfjnM5k4Ym5hhZHyatfUtWh2Pq+oz5VubUvWV07LFSeWJfOWFXLnOQanOzqlFwQqJtSMzun9AM7uJqTnGJ2fZ2NxJ59J+/8Bpdel4gua7T+PNKKDe9rFbPtXHNi03Ruod/AC/rZpCp+tpubhekC5KqpgoEjQaDo7TTPNY15Ru3yiJDey3XQP8BBXps+d4xkqhAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/07a6d62912eefb31c22776bdd4f4e959/263a4/global-mall-adress-04.webp 480w,
/static/07a6d62912eefb31c22776bdd4f4e959/a6361/global-mall-adress-04.webp 960w,
/static/07a6d62912eefb31c22776bdd4f4e959/0b34d/global-mall-adress-04.webp 1920w,
/static/07a6d62912eefb31c22776bdd4f4e959/cc271/global-mall-adress-04.webp 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/07a6d62912eefb31c22776bdd4f4e959/9aebd/global-mall-adress-04.png 480w,
/static/07a6d62912eefb31c22776bdd4f4e959/a91f8/global-mall-adress-04.png 960w,
/static/07a6d62912eefb31c22776bdd4f4e959/ac7a9/global-mall-adress-04.png 1920w,
/static/07a6d62912eefb31c22776bdd4f4e959/a3249/global-mall-adress-04.png 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/07a6d62912eefb31c22776bdd4f4e959/ac7a9/global-mall-adress-04.png&quot; alt=&quot;global mall adress 04&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;그림. 한국vs해외 주소체계 ⓒ 2025. Oliveyoung. All rights reserved&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;그리고 저는 또 다른 사실을 하나 더 발견하게 되었죠.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;물품을 배송할 때 주(State)나 우편번호(Zip/Postal Code)를 사용하지 않는 국가들도 있다&lt;/strong&gt;는 점이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;싱가포르는 주 개념이 없다.&lt;/li&gt;
&lt;li&gt;홍콩은 우편번호를 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
앞서 말씀드렸듯 60개가 넘는 국가마다 주문서 양식을 모두 다르게 만들 수는 없었습니다.
&lt;p&gt;그래서 싱가포르/홍콩 고객들의 기존 주문 방식을 한번 살펴보았고, 고객 패턴이나 어떠한 공통점을 찾게 되었습니다.
&lt;/br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;싱가포르 고객은 주 인풋필드에는 &apos;Singapore&apos;을 사용한다.&lt;/li&gt;
&lt;li&gt;홍콩 고객은 배송정보에 주로 공통 우편번호인 &apos;999077&apos;이나 &apos;HONGKONG&apos;을 기입한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
1자, 구, 절 단위로 고객에게 필요한 정보를 쪼갰고, 또 고객이 선택한 국가의 주소만 추천하도록 제한해 검색 시간도 단축했습니다. 이때부터 &lt;strong&gt;&apos;고객이 빠르고 정확하게 주문 완료에 도달하게 하자&apos;&lt;/strong&gt;는 목표에 한 발 가까워지는 느낌이 들었습니다.
&lt;h2 id=&quot;-주소검증-기능-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%A3%BC%EC%86%8C%EA%B2%80%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EB%8F%84%EC%9E%85&quot; aria-label=&quot; 주소검증 기능 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;② 주소검증 기능 도입&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;자동완성된 주소를 그대로 사용하는 고객도 있지만, 그렇지 않은 고객(예를 들면 동/호수 정보를 추가 수정하거나 처음부터 수기 입력한 고객)이 존재했기 때문에 &lt;strong&gt;유효한 주소인지 한번 더 점검하는 &apos;주소검증&apos; 기능도 도입&lt;/strong&gt;했습니다. 이는 주문서를 제출하기 전 배송지가 옳은지 최종 점검하는 기능입니다. 즉, 고객이 입력한 주소가 실제로 유효한 주소인지 판단하는 기능이죠. 이 기능을 통해 특송사의 배송불가 여부가 확인될 때까지 며칠이고 고객의 주문이 방치되는 상황을 미연에 방지하고자 했습니다.
&lt;/br&gt;&lt;/br&gt;
이 2가지 기능을 도입하여 &lt;strong&gt;배송지 입력 스텝이 크게 3단계로 줄어들었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;검색창 입력&lt;/li&gt;
&lt;li&gt;주소 선택&lt;/li&gt;
&lt;li&gt;상세주소 입력&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;center&gt;
  &lt;div style=&quot;width: 70%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 39.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuklEQVR42m2RXU8TQRSG+bV44dL0+0YNanUrS7mh7ZYSjLCtNUWK3XIlSBOorVpai4BoayF6gaEfAhci0I993B1Dg4FJJnNmcs5z3vfM0EWnx9l5h063T6PZIp8vUCi8o1arUyqVyeXesr29w87nXdbXc1Qqm1Rr3wZ5h4dNuj1DMC46XYas4PfpGX0DNjbKSNJtnE4H8Xic+6Oj2GwjhEJhVDWCNCIxEQigaTEcDgder9esKWGWCobFGgB7fdgylch+P0+UAIv6EmPjE9y5e4+ZmadEIlEBH1PGicWf43I5cbvdFIs3AE//nAvLe3v7zL9cILnwStj98bPFQeMX3w+arGbX0F6keL2SJbuWJ5FMoetp6vV9YdliXFNY/ljB4/HgdrlI6xna7TaNRoNmq01M03jw0CfUplKLPJb9KIrC5qctMa5rlsUMTVV2u11YSSQS+HyPxD2sqgSDIW4NDyPLMs9mZ81ZS+YIbLz/ULzZsiX7a7VGOKwSjU7zZjXL3JxmfkiIdFonk1licjJIMjnP8vKKaDI1Nc3ul6pwNwD2DYPLbRhWr3/rMrbO45MTjo6O/3u7mneV8Rf6LxbISnF0tAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/15deaf2afaf15769424fad3e7b7847ce/263a4/global-mall-adress-05.webp 480w,
/static/15deaf2afaf15769424fad3e7b7847ce/a6361/global-mall-adress-05.webp 960w,
/static/15deaf2afaf15769424fad3e7b7847ce/0b34d/global-mall-adress-05.webp 1920w,
/static/15deaf2afaf15769424fad3e7b7847ce/cc271/global-mall-adress-05.webp 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/15deaf2afaf15769424fad3e7b7847ce/9aebd/global-mall-adress-05.png 480w,
/static/15deaf2afaf15769424fad3e7b7847ce/a91f8/global-mall-adress-05.png 960w,
/static/15deaf2afaf15769424fad3e7b7847ce/ac7a9/global-mall-adress-05.png 1920w,
/static/15deaf2afaf15769424fad3e7b7847ce/a3249/global-mall-adress-05.png 2787w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/15deaf2afaf15769424fad3e7b7847ce/ac7a9/global-mall-adress-05.png&quot; alt=&quot;global mall adress 05&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
  &lt;/div&gt;
  &lt;figcaption style=&quot;text-align: center; font-style: italic; color: gray;&quot;&gt;그림. 한국vs해외 주소체계 ⓒ 2025. Oliveyoung. All rights reserved&lt;/figcaption&gt;
&lt;/center&gt;
&lt;br&gt;
&lt;p&gt;정말 간편해졌죠? 이 기능이 좋은 시너지를 발휘할 수 있도록 인풋필드 명칭과 UI를 개선하여 고객이 주소지 &apos;검색&apos;부터 &apos;검증&apos;까지의 과정을 쉽게 마칠 수 있도록 넛지하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;인풋필드 &apos;Search Address&apos;와 &apos;Address line1&apos;를 &apos;Address&apos;로 기능 통합&lt;/li&gt;
&lt;li&gt;인풋필드 &apos;Address line 2&apos;를 &apos;Apt/Suite/Other (optional)&apos;로 표기 명확화&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h1 id=&quot;-문제-개선-효과--주소입력시간-감소--운영-효율화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%AC%B8%EC%A0%9C-%EA%B0%9C%EC%84%A0-%ED%9A%A8%EA%B3%BC--%EC%A3%BC%EC%86%8C%EC%9E%85%EB%A0%A5%EC%8B%9C%EA%B0%84-%EA%B0%90%EC%86%8C--%EC%9A%B4%EC%98%81-%ED%9A%A8%EC%9C%A8%ED%99%94&quot; aria-label=&quot; 문제 개선 효과  주소입력시간 감소  운영 효율화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📦 문제 개선 효과 : 주소입력시간 감소 &amp;#x26; 운영 효율화&lt;/h1&gt;
&lt;p&gt;자동완성 기능을 통해 &lt;strong&gt;배송지 입력 시간이 평균 22초에서 평균 14초로 약 36% 감소&lt;/strong&gt;했습니다. 간단하게 동/호수 정도만 입력하면 된다니! 엄청 간편해진 거죠. 고객 만족도는 올랐고, 덩달아 주문서 작성을 마치는 고객의 비율(=주문완료율)도 증가하는 효과까지, 두 마리 토끼를 모두 잡을 수 있었죠. 그리고 주소검증 기능을 통해 유효한 주소 비중이 95% 증가하였습니다. 단순한 오탈자까지도 클릭 한 번으로 쉽게 잡아낼 수 있게 된 것이죠. 그리고 &lt;strong&gt;주문완료~배송완료 리드타임을 약 0.5일이나 단축&lt;/strong&gt;할 수 있었습니다. 배송사에서 주소지 오류 여부를 판단하는 시간을 확 줄인 것이죠. 고객이 주문하는 과정에서 인지하는 것과  여러 단계를 거쳐 소통하는 것은 시간/비용 측면에서 많은 차이가 있었기 때문에 물류나 고객의 CS/VOC를 담당하는 부서의 업무 효율성이 증가하게 되었습니다.👏
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h1 id=&quot;-앞으로도-계속-될-올리브영-글로벌몰의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%95%9E%EC%9C%BC%EB%A1%9C%EB%8F%84-%EA%B3%84%EC%86%8D-%EB%90%A0-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EA%B8%80%EB%A1%9C%EB%B2%8C%EB%AA%B0%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot; 앞으로도 계속 될 올리브영 글로벌몰의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🚀 앞으로도 계속 될 올리브영 글로벌몰의 여정&lt;/h1&gt;
&lt;p&gt;올리브영 글로벌몰 쇼핑 경험이 더욱 편리하고 섬세해질 수 있도록 고객의 소리와 다양한 분석을 바탕으로 성장해 가고 있습니다. PO는 고객의 클레임이 단 1건도 들어오지 않도록 &lt;strong&gt;편리한 사용성을 만드는 큰 Moonshot 목표&lt;/strong&gt;를 가지고 있는 게 아닌가 하는 생각이 들기도 합니다. 글로벌몰 PO는 다양한 글로벌 고객向 서비스로 최적화하기 위해 많은 열정을 쏟아내고 있습니다. 앞으로도 폭발적으로 성장하는 올리브영 글로벌몰, 쭉 지켜봐주세요. 🚀
&lt;/br&gt;
&lt;/br&gt;&lt;/p&gt;
&lt;div style=&quot;text-align: center; margin-top: 30px; padding: 20px; border: 2px solid #f0f0f0; border-radius: 10px; background-color: #fafafa;&quot;&gt;
&lt;p style=&quot;font-size: 1.2em; font-weight: bold; margin-bottom: 15px;&quot;&gt;저희와 함께 하실 멋진 여러분을 기다리고 있습니다. 🚀&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20240403025078&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;🌍 글로벌몰 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/br&gt;
&lt;div style=&quot;text-align: left; margin-top: 30px; padding: 20px; border: 2px solid #f0f0f0; border-radius: 10px; background-color: #fafafa;&quot;&gt;
  &lt;h3 style=&quot;margin-top: 0; font-size: 1.2em;&quot;&gt;📢 올리브영 글로벌몰에 대해 더 알고 싶다면?&lt;/h3&gt;
  &lt;ul style=&quot;list-style-type: none; padding: 0; margin: 10px 0;&quot;&gt;
    &lt;li style=&quot;margin-bottom: 8px;&quot;&gt;🔎 &lt;strong&gt;[웹사이트]&lt;/strong&gt; &lt;a href=&quot;https://global.oliveyoung.com/?srsltid=AfmBOopa1kn9CYVNZn6KDabbO1u79d4q1RmePRey-TtjaH35l0QZHKVe&quot; target=&quot;_blank&quot; style=&quot;color: #000000; text-decoration: none;&quot;&gt;올리브영 글로벌몰 탐색하기&lt;/a&gt;&lt;/li&gt;
    &lt;li style=&quot;margin-bottom: 8px;&quot;&gt;📱 &lt;strong&gt;[APP 다운로드]&lt;/strong&gt; &lt;a href=&quot;https://global.oliveyoung.com/app/download?srsltid=AfmBOor9fFa0lkrFPnsajOkUE3f5srXKnBWoGeiazV0hoT9RBJ9kEd65&quot; target=&quot;_blank&quot; style=&quot;color: #000000; text-decoration: none;&quot;&gt;올리브영 모바일 앱 다운로드하기&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;🛍️ &lt;strong&gt;[매거진]&lt;/strong&gt; &lt;a href=&quot;https://m.oliveyoung.co.kr/m/mtn/magazine/editorial/102121125&quot; target=&quot;_blank&quot; style=&quot;color: #000000; text-decoration: none;&quot;&gt;올리브영 글로벌몰 역직구 베스트템 알아보기&lt;/a&gt;&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;</content:encoded></item><item><title><![CDATA[Monstache로 DocumentDB와 OpenSearch 동기화하기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2025-02-03/monstache-journey/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-02-03/monstache-journey/</guid><pubDate>Mon, 03 Feb 2025 14:18:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;시작하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0&quot; aria-label=&quot;시작하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시작하며&lt;/h2&gt;
&lt;p&gt;안녕하세요, 올리브영의 상품 데이터를 다루고 있는 국밥빌런🍲입니다.&lt;/p&gt;
&lt;p&gt;현재 올리브영의 상품 데이터를 생성하기 위해서는 오프라인 매장과 온라인몰로 관리하는 상품 데이터를 각기 다른 시스템을 통해 등록하고 연동하게 되어있어요.&lt;/p&gt;
&lt;p&gt;저는 이렇게 관리 영역별로 분산되어 있는 상품 시스템을 하나로 통합하는 업무를 진행하고 있는데, 이를 통해 오프라인 매장이나 온라인몰 상품 데이터를 한 곳에서 관리할 수 있게 돕고 이 데이터들을 필요한 영역에 제공하려고 하고 있어요.🎯&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;상품 데이터를 제공 받는 곳에서는 어떤 종류의 데이터를 원할까요? 온라인몰 상품상세에서 필요한 상품 정보 중에는 상품명, 상품이미지, 브랜드, 가격, 재고, 할인 정보와 같은 정보가 있을 거예요. 사실 상품 데이터는 이 외에도 판매 채널, 매입가, 배송 정책, 패키지 규격, 담당자, 안전인증정보 등 많은 정보들로 구성되어 있어요.🧐&lt;/p&gt;
&lt;p&gt;이렇게 다양한 상품 데이터를 받게되는 주체, 시기, 환경에 따라 데이터를 조합해서 필요한 곳에 제공하면 좋겠지만 그렇게 간단한 일이 아니었어요.🥺 다른 데이터도 추가해서 제공해야하거나 데이터가 필요하지 않는 경우 거기에 맞춰 지속적으로 요청과 응답형식을 고쳐야하는 불안한 미래가 그려졌어요.😵&lt;/p&gt;
&lt;p&gt;이런 고민을 줄이기 위해 특정 상품에 대한 모든 정보를 한 번에 볼 수 있도록 &lt;strong&gt;역정규화된 상품 데이터를 AWS DocumentDB에 적재&lt;/strong&gt;하여 제공하기로 했어요.&lt;/p&gt;
&lt;p&gt;문제는 상품 데이터를 대량으로 조회할 때 발생했는데, 부하테스트를 진행했을 때 필요한 TPS에 미치지 못해 대안으로 &lt;strong&gt;DocumentDB에 있는 데이터를 OpenSearch로 동기화하여 대량 데이터 조회를 제공&lt;/strong&gt;하는 방향을 잡게 되었어요.&lt;/p&gt;
&lt;p&gt;안타깝게도 이 동기화에 힘을 쏟을 수 있는 인원은 저 혼자였고😱, 저는 아주 흔한 바쁜 올리브영 개발자 중에 한 명이기 때문에 이런 역할을 수행하는 오픈소스를 찾다가 &lt;strong&gt;Monstache&lt;/strong&gt;를 발견했어요!🕵️‍♂️&lt;/p&gt;
&lt;p&gt;Monstache는 &lt;strong&gt;실시간 데이터 동기화, 필터링, OpenSearch 호환성, 웹서버 기능&lt;/strong&gt;까지 갖춘 솔루션으로, 제가 필요로 하는 조건에 적합했어요. 이를 기반으로 POC(Proof of Concept)를 진행하며 시행착오 끝에 성공적으로 데이터를 동기화할 수 있었고, 이 경험을 공유해보려고 합니다.😊&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;max-width:800px; width:100%; display:block; margin-left:auto; margin-right:auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 928px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8e15c06d48328d90f1f50d8b2644e2b8/cda85/architecture.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 119.37500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAAAsTAAALEwEAmpwYAAADN0lEQVR42n1VW2sTQRTOn/On6IsiPvRBERSE9kUoRQRBpFAEL/ShiIKCRQUrVGmxtVit9Jpkc+sm3etkd2Z2P885yaZJmmbgI7MzZ75z5pzvTEpXb9zG1s4u/DBEN0lgrB2FMReQphqa52O2WmuUtn7tIoxiADksLWZZJr/F4TzPMTyKb7YrbJmc57xX4s1aw8XskxX8PaigqyIoFUsUCUWslBohcuqnuPdoGXuHVXS7CmEYkW1Kv6GQCmHZaeLm3HPs7B2j0+nAj9Qgom63K2RZ1iOs1Fq4dn8R23+OEIUePN+TdXZsmZBD5kOpsbKx9NXgxbe0H9U54cABHeRIxscFQk4qj+1jhd9lNSBM0wRhoFHeDyinOYIoQqaBP+kaPgZPsXH2YTJh4dULEgGP1GRQlMdqxcf29zqCMIXrh0jI30b0Hi8bD/Cp/mo64ez8CeYWTnrX01YSHgQ+Tk6OxY4LxSngoh0elRH18z1CqOIYOk1EOqtfHEJF5uxEUSV7Rek5jMmW5ZTRWpKkIhve5/UBYUqCjDUtqoQMaMMoMWApFNEXGuX1IAgIIWrNDlU5ELuIcis6NDaH8pporz9D1HYofzF8L5BDfD2+slJdQfFdgBvi3EaJ05KmmwTVHTQXr8Av/8RpJ4DrnqLdbovBtDHcSUzMHVNidkl0ZiRvw6MQ84+tGKvrvWIYm0muqF7Y3Ke0KDtoADNMOGkUhA8X9nH97gG4LCrNpCDtCLi15GOvZvqESZ+QCqKInb1OhkXnzEej5co840ekv+d6EV3TigNVRChVpoTGngfl+yPgNUuGmvepkglheN/EpAIqnpyl35xlY+jKaLWARuMiaL2yuYmzvX/QVQfWcSbbMep15CSpkqEIqKwgMVG5gwFyioD0gDszM3i3vAzXqcI5OBixGYDPkipympek9PRhyIMlT+MwFFXi1JDU6sgm7BfgvZyKW2LtcAulLNAxcPWt0dJBHU+B0zPJTmz5CeS/gERUr6fKZu2Ni7ePvXGZjgx5ZDhCIdTTCT+vHOP1POnQ9hnzKYQcneo/S5fBbbtothrSr5fZcK9zk0iEnEeO8jLwa26p5abZMLQ2+A+aZDZWIvF0MAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8e15c06d48328d90f1f50d8b2644e2b8/263a4/architecture.webp 480w,
/static/8e15c06d48328d90f1f50d8b2644e2b8/0743e/architecture.webp 928w&quot; sizes=&quot;(max-width: 928px) 100vw, 928px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8e15c06d48328d90f1f50d8b2644e2b8/9aebd/architecture.png 480w,
/static/8e15c06d48328d90f1f50d8b2644e2b8/cda85/architecture.png 928w&quot; sizes=&quot;(max-width: 928px) 100vw, 928px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8e15c06d48328d90f1f50d8b2644e2b8/cda85/architecture.png&quot; alt=&quot;Monstache를 사용할 영역&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Monstache를 사용할 영역&lt;/figcaption&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;monstache란-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#monstache%EB%9E%80-&quot; aria-label=&quot;monstache란  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Monstache란? 🤖&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://rwynn.github.io/monstache-site/&quot;&gt;Monstache&lt;/a&gt;는 MongoDB(또는 DocumentDB) 데이터를 Elasticsearch(또는 OpenSearch)로 실시간 동기화하는 &lt;strong&gt;Go 언어 기반의 오픈소스 프로그램&lt;/strong&gt;이에요. MongoDB의 &lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/core/replica-set-oplog/&quot;&gt;oplog&lt;/a&gt;를 감지해서 변경된 데이터를 자동으로 OpenSearch로 보내줘요.&lt;/p&gt;
&lt;h3 id=&quot;주요-기능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9A%94-%EA%B8%B0%EB%8A%A5&quot; aria-label=&quot;주요 기능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;주요 기능&lt;/h3&gt;
&lt;p&gt;✅ &lt;strong&gt;실시간 데이터 동기화&lt;/strong&gt;: DocumentDB에서 변경된 데이터를 OpenSearch에 자동 반영 ⚡&lt;br&gt;
✅ &lt;strong&gt;필터링 및 변환 지원&lt;/strong&gt;: 정규식을 통한 특정 컬렉션 데이터만 동기화 가능 🎯&lt;br&gt;
✅ &lt;strong&gt;스크립팅 지원&lt;/strong&gt;: Go 플러그인이나 JavaScript로 커스텀 로직 적용 가능 🛠️
✅ &lt;strong&gt;웹서버 기능 제공&lt;/strong&gt;: &lt;code class=&quot;language-text&quot;&gt;--enable-http-server&lt;/code&gt; 옵션으로 동기화 상태 확인 가능 🌐&lt;/p&gt;
&lt;p&gt;저는 요 녀석의 동기화라는 기본적인 기능 뿐만 아니라 동기화 상태확인이 가능한 점도 필요했기 때문에 가려운 곳을 시원하게 긁어줄 수 있을 거라 판단하고 채택하게 되었어요 🛍️&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;monstache-실행-방법-️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#monstache-%EC%8B%A4%ED%96%89-%EB%B0%A9%EB%B2%95-%EF%B8%8F&quot; aria-label=&quot;monstache 실행 방법 ️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Monstache 실행 방법 🏃‍♂️&lt;/h2&gt;
&lt;h3 id=&quot;1-monstache-다운로드-및-실행&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-monstache-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EB%B0%8F-%EC%8B%A4%ED%96%89&quot; aria-label=&quot;1 monstache 다운로드 및 실행 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. Monstache 다운로드 및 실행&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sh&quot;&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/rwynn/monstache.git
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; monstache
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;branch-or-tag-to-build&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; go &lt;span class=&quot;token comment&quot;&gt;# Go가 설치되지 않았다면 실행&lt;/span&gt;
go &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$GOPATH&lt;/span&gt;/bin
./monstache &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; config.toml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;2-설정-파일을-이용한-실행&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%84%A4%EC%A0%95-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%A4%ED%96%89&quot; aria-label=&quot;2 설정 파일을 이용한 실행 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 설정 파일을 이용한 실행&lt;/h3&gt;
&lt;p&gt;Monstache는 &lt;code class=&quot;language-text&quot;&gt;config.toml&lt;/code&gt; 설정 파일로 &lt;strong&gt;DocumentDB 및 OpenSearch 연결 정보, 동기화할 데이터 범위 등의 설정&lt;/strong&gt;을 정의할 수 있어요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sh&quot;&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;./monstache &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; config.toml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div style=&quot;max-width:550px; width:100%; display:block; margin-left:auto; margin-right:auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 785px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c9dfd9255267d76584173090b27aa344/18baa/init_logs.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 47.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB0klEQVR42mWSbVPaQBSFQxKUV0GhDGAg5D0hBLDW1rZTFUGIjRSJrdSpM/3/v+L07oVOh+mHzO5kkmfvec5K19MY82WKSbzEh8836PQs+IMRepaLrmHR6kA3bRi2h3qjiUKxBFVVkclkIEnS/8/NPEH8uME0ecLllwkBDXjhkCAO7U0YjgeTni7t2x0djWYL1ZMasgcHODzMETy7D7xLHrH8/gvx6hlXswUBfERnF7C8gPe234flBjxlf3gGrWtA0w00TzWCt1F700C+UPgHvF89IX35ja/pC6bxAwFCjN5dwg0jOMGA4/vRCB6tAtpsa6jVGyiWj1A+qvC05UoVuVweuXweUkz+VptXAv7EhOLbXh/D8/cEC3nvElSAhU/hsFI9xvFJnaHiAIu+aZ12eNJCqfTX4TM5XOPj1S1725biMMR0A/bYs12OK34s0XQinixnuBxZUdgpR54sHnC/3mCWpPh0PYNu2AiiMbcsinFIQc90eW1rXXar6eZeEYqiksfizuG3FOudw9vFNvL4ghz2I/YZjs8pdrSNT+/EQUJBi+DCH08oy1Czu7bnyQrLH6/blqcLjjYgiHBjuT43K8Aifjh6yw2LayU8Cz2yrOxdmz80fv4GLTua1wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c9dfd9255267d76584173090b27aa344/263a4/init_logs.webp 480w,
/static/c9dfd9255267d76584173090b27aa344/fb49c/init_logs.webp 785w&quot; sizes=&quot;(max-width: 785px) 100vw, 785px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c9dfd9255267d76584173090b27aa344/9aebd/init_logs.png 480w,
/static/c9dfd9255267d76584173090b27aa344/18baa/init_logs.png 785w&quot; sizes=&quot;(max-width: 785px) 100vw, 785px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c9dfd9255267d76584173090b27aa344/18baa/init_logs.png&quot; alt=&quot;초기 구동 로그&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;초기 구동 로그&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;또한 웹서버 기능을 활성화하면 동기화 상태를 확인할 수도 있어요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sh&quot;&gt;&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;./monstache &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; config.toml --enable-http-server&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div style=&quot;max-width:300px; width:100%; display:block; margin-left:auto; margin-right:auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 363px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/23b2974f95b033957302f9917a896acf/6ddef/stats.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 123.14049586776858%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAYAAAAxFw7TAAAACXBIWXMAAAsTAAALEwEAmpwYAAACo0lEQVR42pWV2U6qQRCEuUKJoCgIyKIsEdklLCKrsskWEFCI7/8iffJ1MifAAQ5edJif+amprqoeLH6/X66ursTpdMrl5aXW9fW1fme32/WZvZubm5PK4vV6JRwOSzablefnZ3l5eZFoNCqJREJ6vZ6Uy2V9PhXUAoNYLCaDwUAWi4WMRiPpdDp6wGQykZ+fHykUCsrY5XL9H5CTYfDx8aEAVLFYlFwup+vhcCihUEhlOIkhJz8+Psr7+7uMx2P5/PxUdrR6d3enzG5vb38HiF6r1UoBaTuVSkmz2dTCNMw5GRANk8mkavj29ibL5VLNSKfTMpvNZD6fS6lUEo/HcxKoMozH49Lv9xUIhpVKRbrdrpoRiUR0/1eAaGgAW62WPDw8KGufz6ftOhwOfRmXKbfbrbXPdYtZ8AJMAMFVDDGfxhwSAbjNZtNCrl3Qv4C0wwus2+22TKdT+f7+llqtJo1GQ9tnCJ6enuT19VW/h8Bu4LcAaRWtyCU5zOfzag6u8xwIBCSTyah5yIMsu4G3GG3Y4GXag+n5+bkWa3S8uLhQNqzPzs7EarXq+mDLRkdCTIuMH2NIyGkX1vzYaHt/f6967zq/BQgYP+KSABDn0YoY1et1BapWqxotBoGDdo3ZahmdmAza22zTXGnmR8cuiS1TgsGgskRsYwTBppDDgHMQBYmDgJRxjHATm6+vL11TzDUuc2mwx0iSgr0tU5xOxhAalrCl0I1PTCBSsOU9umDvYA7ZMJOCk8Z1M2Lmr4BWiRPRYWqOxoaWAWBSaGm9XqvDxAjnOQx2uEvr3PR7GWIIRQucSOYAQCMzLYCQAG5z4kRGGYR/JoUTGDkeEJ3NzX9AU7THocTp6OUAKxNoQ99cU7tl9oy2e3MI2CbQqf+/h4L9Byhcm0VC2oUDAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/23b2974f95b033957302f9917a896acf/c25a5/stats.webp 363w&quot; sizes=&quot;(max-width: 363px) 100vw, 363px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/23b2974f95b033957302f9917a896acf/6ddef/stats.png 363w&quot; sizes=&quot;(max-width: 363px) 100vw, 363px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/23b2974f95b033957302f9917a896acf/6ddef/stats.png&quot; alt=&quot;stats&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;/stats로 확인할 수 있는 정보들&lt;/figcaption&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;h2 id=&quot;monstache-설정-및-최적화-️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#monstache-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EC%B5%9C%EC%A0%81%ED%99%94-%EF%B8%8F&quot; aria-label=&quot;monstache 설정 및 최적화 ️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Monstache 설정 및 최적화 ⚙️&lt;/h2&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# MongoDB 연결 정보&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;mongo-url&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mongodb://{username}:{password}@your-docdb-url:27017/?authSource=admin&amp;amp;replicaSet=rs0&amp;amp;ssl=true&amp;amp;sslcertificateauthorityfile=ca.pem&amp;amp;readPreference=primary&amp;amp;retryWrites=false&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# OpenSearch 연결 정보&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;elasticsearch-urls&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;https://your-opensearch-url&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;elasticsearch-user&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{username}&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;elasticsearch-password&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{password}&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 변경점 감지 설정&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;change-stream-namespaces&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;your-database.your-collection-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;your-database.your-collection-2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# OpenSearch 요청 압축&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;gzip&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Oplog 타임스탬프 인덱싱&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;index-oplog-time&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# AWS 연결 설정&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;aws-connect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;strategy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;aws-자격-증명-설정-변경-️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#aws-%EC%9E%90%EA%B2%A9-%EC%A6%9D%EB%AA%85-%EC%84%A4%EC%A0%95-%EB%B3%80%EA%B2%BD-%EF%B8%8F&quot; aria-label=&quot;aws 자격 증명 설정 변경 ️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;AWS 자격 증명 설정 변경 🛠️&lt;/h3&gt;
&lt;p&gt;처음 POC 단계에서는 AWS 자원에 접근하기 위해 &lt;code class=&quot;language-text&quot;&gt;strategy = 0&lt;/code&gt;으로 설정하고, access/secret key를 사용했어요. 하지만 보안🔒 문제가 발생할 수 있어서, 운영 환경에서는 &lt;strong&gt;EC2 IAM Role을 활용하는 방식&lt;/strong&gt;으로 변경했어요. 🎯 IAM Role을 사용하면 보안에 민감한 값을 직접 관리하지 않아 더 안전하고 편리해요.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;aws-connect.strategy = 4&lt;/code&gt;로 설정하면, access/secret key, 환경변수, IAM Role을 차례로 참조하면서 적절한 인증 방식을 선택하게 돼요.🔑&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;시행착오-1-elasticsearch-healthcheck-timeout-문제-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4-1-elasticsearch-healthcheck-timeout-%EB%AC%B8%EC%A0%9C-&quot; aria-label=&quot;시행착오 1 elasticsearch healthcheck timeout 문제  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시행착오 1: &lt;code class=&quot;language-text&quot;&gt;elasticsearch-healthcheck-timeout&lt;/code&gt; 문제 ⏳&lt;/h2&gt;
&lt;p&gt;처음 Monstache를 실행했을 때, OpenSearch와 연결이 안 되더라고요. 🤔&lt;/p&gt;
&lt;h3 id=&quot;문제-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9&quot; aria-label=&quot;문제 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;문제 상황&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;cannot connect to Elasticsearch: health check timeout: no response&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 구동을 시도했을 때 위와 같은 로그가 발생해버렸어요. &lt;code class=&quot;language-text&quot;&gt;elasticsearch-healthcheck-timeout&lt;/code&gt; 기본값이 &lt;strong&gt;5초&lt;/strong&gt;인데, OpenSearch가 응답하기에는 너무 짧았던 거예요. 🤦‍♂️&lt;/p&gt;
&lt;h3 id=&quot;해결-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;해결 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해결 방법&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token key property&quot;&gt;elasticsearch-healthcheck-timeout&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;200&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 타임아웃 시간을 넉넉하게 설정하고 나니까 연결이 잘 됐어요! 🎉&lt;/p&gt;
&lt;p&gt;이 설정은 특히 네트워크가 불안정하거나 OpenSearch가 높은 부하를 받을 때 유용해요. 만약 여전히 문제가 발생한다면, 값을 더 늘려 보거나 OpenSearch 상태를 확인하는 것도 방법이에요. 📡&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;시행착오-2-documentdb-변경-스트림-활성화-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4-2-documentdb-%EB%B3%80%EA%B2%BD-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%99%9C%EC%84%B1%ED%99%94-&quot; aria-label=&quot;시행착오 2 documentdb 변경 스트림 활성화  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;시행착오 2: DocumentDB 변경 스트림 활성화 🔄&lt;/h2&gt;
&lt;p&gt;DocumentDB에서는 &lt;strong&gt;변경 스트림 기능이 기본적으로 비활성화&lt;/strong&gt;되어 있어요. 그래서 활성화 과정이 필요해요. 또한, &lt;code class=&quot;language-text&quot;&gt;readPreference=primary&lt;/code&gt; 옵션을 포함한 DocumentDB 커넥션 URI를 사용해야 해요. 이 부분은 Monstache 가이드 문서에서도 다루고 있으니 참고하면 좋아요! 📖&lt;/p&gt;
&lt;h3 id=&quot;변경-스트림-활성화-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%80%EA%B2%BD-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;변경 스트림 활성화 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;변경 스트림 활성화 방법&lt;/h3&gt;
&lt;p&gt;1️⃣ DocumentDB에서 변경 스트림을 활성화&lt;br&gt;
2️⃣ &lt;code class=&quot;language-text&quot;&gt;readPreference=primary&lt;/code&gt;가 포함된 URI를 사용하여 연결 설정&lt;br&gt;
3️⃣ &lt;code class=&quot;language-text&quot;&gt;change-stream-namespaces&lt;/code&gt; 옵션을 활용하여 변경 스트림이 활성화된 컬렉션 동기화&lt;/p&gt;
&lt;p&gt;이렇게 설정하면 &lt;strong&gt;변경 사항을 실시간으로 감지&lt;/strong&gt;해서 변경된 데이터를 OpenSearch에 빠르게 반영할 수 있어요! 🚀&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;마무리-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC-&quot; aria-label=&quot;마무리  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리 🏆&lt;/h2&gt;
&lt;p&gt;이외 자잘한 시행착오도 많았지만 저희 팀과 엔지니어링 팀의 도움이 컸어요. 덕분에 &lt;strong&gt;DocumentDB에서 OpenSearch로의 데이터 동기화에 성공&lt;/strong&gt;할 수 있었습니다! 🎉 Monstatche를 안정적으로 구동한 후에는 거의 신경쓰지 않아도 괜찮을 만큼 동기화 시스템을 안정적으로 운용하게 되었어요.&lt;/p&gt;
&lt;p&gt;지금은 에러로그 감지 및 헬스체크를 위한 Datadog 모니터, 대시보드를 만들어 상태를 확인하고 있어요.📊&lt;/p&gt;
&lt;div style=&quot;max-width:900px; width:100%; display:block; margin-left:auto; margin-right:auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/72e8046fadb314676ab2c37544103874/89066/operating_logs.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 14.791666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeUlEQVR42lWNaw7CMAyDexAQQ9v6TtrB/c9m6lSbxg8rdqL4c1I7UizYVg+pDfsW8HwsUO3muef0e7w0c/jLwU/vWv/iaB+8lxW1KGLIo/A1ytVAFL1KtzuhnHdfstidcqIH2jCzUIzEQiv3yQB8OB9zqpfOTOiECH6fQVjT25VPEAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/72e8046fadb314676ab2c37544103874/263a4/operating_logs.webp 480w,
/static/72e8046fadb314676ab2c37544103874/a6361/operating_logs.webp 960w,
/static/72e8046fadb314676ab2c37544103874/95f20/operating_logs.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/72e8046fadb314676ab2c37544103874/9aebd/operating_logs.png 480w,
/static/72e8046fadb314676ab2c37544103874/a91f8/operating_logs.png 960w,
/static/72e8046fadb314676ab2c37544103874/89066/operating_logs.png 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/72e8046fadb314676ab2c37544103874/89066/operating_logs.png&quot; alt=&quot;운영 상태 로그&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1170px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/50be927dc8efd6988ca4803bc9b7e117/77666/dashboard.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 85.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAAD5klEQVR42lWTu28UVxTG91+gwARs7877ceexO+vxrtf2YvZhe/3Exq+1MTZWLBET2xhEQApEERQUSSSKiCIoEUqZJpEiUQQlbTpKmhT5V345M5aQUnw69557vzPfPd+ZwuBml4G9OT7Z63Fpfw7/aIPkYA19YwZjcxZzu4e1PM3Vywd0Lp7QuniX6cvHrIePWVVf0JZ9Z0ByA/eYGNqhsLHY49HNLU5vXOf+whx3VhZpPOnTeLZD+nSd6OtV3H6bVneXpb17zPePmd0+orNxyPLOKQs7J/T6UnDniLSxROF7t8q/0VU+dFf4J5rirUoZ+aZN88112j/eoPpmFnVzBP+swfzbAz774zGLvx3gv7xG9VWPwz8fcffdlzR/3cTarVK4pi2zqG0yb/VZ0LdY8neYrG5TjzZpxKK0sk3qLRFpPiN2TM1JGHUq1NQIaZBQV1UaQUrqV4iMgEJTP6Rt3adlnnJVE/nRA1bCh8wUT5gtnTIzfMrk0E3GtJhJI6FWDBjTI7rROF2jzugFl/SCQ/2CR3LJp+B5AXElISpXUGGEblj4QUQo+zCu4KqA4qBO84m04Jdb1F6vUv95jeR5D6USOU+wvRhbFBqmouDaIbYdyCbAsgJsgetEWJLP9o4bow3ZrL64zYv3P/Dt36/57v1PrL46xJQ2OG4Z24lzmKYoNE2dODCEbOJ6jhTWMW0L25W1JdFzuXJ5iJmnN3j413POfv+KB++esfLylnAcokhepuRFEk3ToTBakjHR16lra3lsaOvSo3NkuTF9jaazRSfdZ0LMyo0q95kI+9JXua9v5hjXt6lqPQot81N6zhkz1jFt7egjOvqR5MQY+4SV5CHL8RldybeG7zCt3WXWzM5OmTaPaWl35OxzEbOVmaIIohjXV2iGmaOkG/lzo7iML6bYjkexZKDp51BhSCAGeirrsZffPz+zxBRHTLEUljTUEXNsiaqcCiHBd8X1koOpe1iGhxdXicYmJB/n5lniasZ1ZK3CqjhezkzxKCc1QhnSrHgGX9aeH8sHFJ6MhCuFfbmcdrqk7Y6YoXJOoCo4TpCfZwWzdcEXQixf9v2IUsmkOGwweGUYrWhSjlLK8ShROIJp+LlSQ3PzQnGUSIxzAcXiOacknHwO46BGJRwnUjWqcZPF9m1qSUue4+PK76bcJI8ZHFvU+OX8bjlsEPryC5anWGjv57lC1otmo8e1cRmf0S4T9Vnm27uMpR0ZeCXqqnhu+LEdGTzp4dT4PFONBcZGOkzW51js7ouYCQqG7solhSEOlUR6sahzZXBYnq7nf0xWzNCd/8EUgxzh6Bkna5NwBgeH8vV/HH42kzqRjRQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/50be927dc8efd6988ca4803bc9b7e117/263a4/dashboard.webp 480w,
/static/50be927dc8efd6988ca4803bc9b7e117/a6361/dashboard.webp 960w,
/static/50be927dc8efd6988ca4803bc9b7e117/b47fd/dashboard.webp 1170w&quot; sizes=&quot;(max-width: 1170px) 100vw, 1170px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/50be927dc8efd6988ca4803bc9b7e117/9aebd/dashboard.png 480w,
/static/50be927dc8efd6988ca4803bc9b7e117/a91f8/dashboard.png 960w,
/static/50be927dc8efd6988ca4803bc9b7e117/77666/dashboard.png 1170w&quot; sizes=&quot;(max-width: 1170px) 100vw, 1170px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/50be927dc8efd6988ca4803bc9b7e117/77666/dashboard.png&quot; alt=&quot;운영 대시보드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;운영 대시보드&lt;/figcaption&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;저처럼 AWS 서비스를 주로 활용하면서 동기화만 잘 하면 된다👌 하는 분들은 &lt;a href=&quot;https://docs.aws.amazon.com/opensearch-service/latest/developerguide/configure-client-docdb.html&quot;&gt;OpenSearch Ingestion Pipeline을 활용한 방법&lt;/a&gt;을 참고하시는걸 추천해요.😌 이 포스트가 저와 비슷한 고민이 있는 분들께 도움이 되면 좋겠어요. 😊&lt;/p&gt;</content:encoded></item><item><title><![CDATA[10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정]]></title><description><![CDATA[매장 도메인을 구현해 나가는 여정 안녕하세요! 올리브영에서 매장 도메인을 담당하고 있는 알렉스입니다 :) 이전 글에서 저희는 이벤트 스토밍, 바운디드 컨텍스트 식별, 컨텍스트 매핑 등 DDD…]]></description><link>https://oliveyoung.tech/2025-01-24/store-service-journey-2/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-01-24/store-service-journey-2/</guid><pubDate>Fri, 24 Jan 2025 19:30:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;매장-도메인을-구현해-나가는-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EB%8F%84%EB%A9%94%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4-%EB%82%98%EA%B0%80%EB%8A%94-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;매장 도메인을 구현해 나가는 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 도메인을 구현해 나가는 여정&lt;/h2&gt;
&lt;p&gt;안녕하세요! 올리브영에서 매장 도메인을 담당하고 있는 알렉스입니다 :)&lt;/p&gt;
&lt;p&gt;이전 글에서 저희는 이벤트 스토밍, 바운디드 컨텍스트 식별, 컨텍스트 매핑 등 DDD의 &lt;strong&gt;전략적 설계&lt;/strong&gt; 과정을 통해 &lt;strong&gt;매장 도메인을 추출&lt;/strong&gt;하는 과정을 알아보았습니다. &lt;br&gt;
이번 글에서는 저희가 &lt;strong&gt;지난 1년간 걸어온 아래의 여정&lt;/strong&gt; 중, &lt;strong&gt;Part.2 전술적 설계를 바탕으로 매장 도메인을 실제 구현한 사례&lt;/strong&gt;를 소개하겠습니다 🚀&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;✅ &lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3/&quot;&gt;10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h3 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;매장 서비스 구축을 위한 준비
&lt;ul&gt;
&lt;li&gt;어떤 것들이 필요한가?&lt;/li&gt;
&lt;li&gt;멀티모듈 아키텍처&lt;/li&gt;
&lt;li&gt;CQRS 패턴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;도메인 모델 구축
&lt;ul&gt;
&lt;li&gt;테이블 경량화·통합&lt;/li&gt;
&lt;li&gt;Entity와 VO&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다음 이야기&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h2 id=&quot;매장-서비스-구축을-위한-준비&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%EC%B6%95%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A4%80%EB%B9%84&quot; aria-label=&quot;매장 서비스 구축을 위한 준비 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 서비스 구축을 위한 준비&lt;/h2&gt;
&lt;h3 id=&quot;어떤-것들이-필요한가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EA%B2%83%EB%93%A4%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C%EA%B0%80&quot; aria-label=&quot;어떤 것들이 필요한가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떤 것들이 필요한가?&lt;/h3&gt;
&lt;p&gt;도메인 모델을 구축하고 매장에 관련된 서비스를 제공하려면 어떤 것이 필요할까요?&lt;br&gt;
가장 먼저 &lt;strong&gt;코드를 구현할 프로젝트가 필요&lt;/strong&gt;합니다.&lt;br&gt;
현재 올리브영 대부분의 팀에서는 코프링(코틀린+스프링부트) 형태의 스켈레톤 프로젝트를 사용하고 있습니다.&lt;br&gt;
하지만 제가 오프라인 팀에 합류했을 때는 구성원들 대부분이 이미 자바 프로젝트로 개발을 진행하고 있었고, 코틀린을 사용해 본 인원이 한 명도 없던 상태였습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;이 상황에서 무작정 코틀린을 팀에 도입한다면 어떨까요?&lt;br&gt;
물론 어떻게든 개발은 할 수 있겠지만, 저는 &lt;strong&gt;코틀린의 주요 특징 및 함수형 패러다임과 객체지향 패러다임&lt;/strong&gt;을 확실히 이해하고 사용하지 않는다면 오히려 코드가 더 난잡해질 수 있다고 생각합니다.&lt;br&gt;
모든 코드 구석구석에 &lt;code class=&quot;language-text&quot;&gt;?&lt;/code&gt;가 포진되어 있는 장관이 펼쳐질 수도 있죠.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;코틀린의 특징·장점이 궁금하다면 저희 팀원이 최근에 작성하신 &lt;a href=&quot;https://oliveyoung.tech/2024-12-08/kotlin-advantages/&quot;&gt;Java를 주로 다루는 개발자가 생각하는 Kotlin 장점 🌼&lt;/a&gt; 글을 참고해 보시길 추천드립니다 😁&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;따라서 저희는 오프라인 신규 프로젝트들을 위한 자프링(자바+스프링부트) 형태의 스켈레톤 프로젝트를 구축하기로 결정했습니다.&lt;br&gt;
팀장님과 테크리더 분은 한 달이라는 기간을 주시면서, 저에게 아래와 같은 미션을 할당해 주셨습니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기본 기능(JPA, MyBatis, Swagger, Test Code, DB Multi Datasource)과 함께 간단한 예시용 API가 개발된 스켈레톤 프로젝트&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;AWS ECS 환경 구축&lt;/li&gt;
&lt;li&gt;TeamCity를 통한 CI/CD 파이프라인 구축&lt;/li&gt;
&lt;li&gt;Datadog을 이용한 모니터링 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이번 글에서는 애플리케이션 영역까지만 소개하고, AWS &amp;#x26; TeamCity &amp;#x26; Datadog 등 인프라 영역에 대한 내용은 다음 포스팅에서 소개해보겠습니다 🙌&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;멀티모듈-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%80%ED%8B%B0%EB%AA%A8%EB%93%88-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;멀티모듈 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;멀티모듈 아키텍처&lt;/h3&gt;
&lt;p&gt;저는 곧바로 테크리더와 함께 애플리케이션 아키텍처를 설계하기 시작했습니다.&lt;br&gt;
매장 서비스에서는 &lt;strong&gt;내부 서비스끼리의 통신(internal)과 대고객 서비스와의 통신(external)&lt;/strong&gt; 이 모두 이루어질 예정이었는데요.&lt;br&gt;
하지만 요구사항들이 구체화 되지 않은 상태에서, 처음부터 바로 &lt;strong&gt;external 환경과 internal 환경 자체를 분리하는 것은 아직 이르다고 판단&lt;/strong&gt;하였습니다.&lt;br&gt;
따라서 초반에는 단일 환경에서 서비스를 시작하고, 추후 환경을 분리하기 쉽도록 멀티모듈 아키텍처를 도입하기로 하였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6beacb1c3ed5fcdf9e2a85506f511847/1ca8f/multi-module-example.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 99.58333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAABy0lEQVR42pVUi3KDMAzr///mdtetD55JIC80y4SOMti63OWARJFlW+EUY8Q0TdgOruSc0A8BgycmK86MAa3ziDHNOFnvHDFJ1gJOKSXsDR723uN6u6FuGjAwsdY5dL3R95yzPlv5tm5ACC8QXi4XVFX1RJDkYE6zYl2L/E5/EMr6OAzo+x7GGIRxZJR5T9Q+YYUUJdgxoUR7KKxrRPlGwU4x/QjOYMeEEo0qSNi2raqMguPBKc/PJ8Ki+JBwASzd5/PhBJKuU+beEyG7l6UBcS6yqjtsVIZxI5rOyeH8nU3BK2EIHp2xuNUt+I64T7YcYD0/Pi8YpUmqXMuQvwm9D3DOouu6uZOUT6U7k9ao6wrn81nrqxmtsikKg9qilsieUQksNthOdtpYq00iYd6U50HIzUE8l0r7cVBDEtzvd7y9v88pE1fS/bXLSrqkvqmhlWx6sZIq3GB+NfYeqbRAGhdFXURe2eU1wh1SWqztLe7NXMP/Ey6jkA5St0aaV0kdjbhi69fXCcsNoa14FXm/+ePYJVx+sH9NDpI08m9kp2k1tc1qn1yvK5RBi9GD1+tVbTbtuOBkxajcXCb9tcztN7G8UVRJhetznNz/AvfuKaCtrFJ3AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6beacb1c3ed5fcdf9e2a85506f511847/263a4/multi-module-example.webp 480w,
/static/6beacb1c3ed5fcdf9e2a85506f511847/a6361/multi-module-example.webp 960w,
/static/6beacb1c3ed5fcdf9e2a85506f511847/0b34d/multi-module-example.webp 1920w,
/static/6beacb1c3ed5fcdf9e2a85506f511847/c184d/multi-module-example.webp 2152w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6beacb1c3ed5fcdf9e2a85506f511847/9aebd/multi-module-example.png 480w,
/static/6beacb1c3ed5fcdf9e2a85506f511847/a91f8/multi-module-example.png 960w,
/static/6beacb1c3ed5fcdf9e2a85506f511847/ac7a9/multi-module-example.png 1920w,
/static/6beacb1c3ed5fcdf9e2a85506f511847/1ca8f/multi-module-example.png 2152w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6beacb1c3ed5fcdf9e2a85506f511847/ac7a9/multi-module-example.png&quot; alt=&quot;multi module example&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;위의 사진에서 볼 수 있듯이, 우선 store-core 모듈은 모든 모듈에서 의존합니다.&lt;br&gt;
단, 흔히 얘기하는 &lt;strong&gt;common(core)의 비대함을 방지&lt;/strong&gt;하기 위해 몇 가지 규칙을 세웠는데요.&lt;br&gt;
core 모듈에 들어가는 코드는 &lt;strong&gt;특정 도메인 및 비즈니스와 관련이 없어야 하고&lt;/strong&gt;, 범용적으로 사용되는 횡단 관심사(AOP)나 POJO로 이루어진 유틸리티 클래스 등만 허용됩니다.&lt;/p&gt;
&lt;p&gt;store-domain 모듈에는 각 도메인 패키지 내부에 entity와 data 패키지로 나누어져 있습니다.&lt;br&gt;
entity 패키지에는 JPA Entity 객체가 존재하고, 동일한 관심사끼리 컬럼·로직을 응집시키고 불변을 유지하기 위한 Value Object도 함께 위치합니다.&lt;br&gt;
data 패키지에는 DTO가 존재하는데요. 이는 Querydsl이나 MyBatis를 사용해서 조회한 값을 바인딩할 때 사용되는 객체입니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;store-service 모듈에서는 application layer와 infrastructure layer를 통해 유스케이스를 구현하고, 비즈니스 요구사항을 수행합니다.&lt;br&gt;
말 그대로 매장 관련 &lt;strong&gt;서비스를 담당&lt;/strong&gt;하는 모듈인 셈이죠.&lt;/p&gt;
&lt;p&gt;store-api, store-consumer 모듈은 모두 presentation 영역(클라이언트)을 담당하며, store-service 모듈을 재사용합니다.&lt;/p&gt;
&lt;p&gt;이런 아키텍처일 때 얻을 수 있는 장점은 무엇일까요?&lt;br&gt;
API 서버, Consumer 서버, Batch 서버가 추후 각각의 환경으로 분리되더라도 필요한 모듈만 조합하여 배포할 수 있습니다.&lt;br&gt;
즉 중복 코드를 방지할 수 있고, 비즈니스 로직 또한 응집시킬 수 있다는 장점이 있습니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;그리고 store-api 모듈 안을 보면 패키지로 분리된 external과 internal이 존재합니다.&lt;br&gt;
추후 API 서버에서도 external 환경과 internal 환경이 서로 분리된다면, 이 또한 해당 패키지를 그대로 각자의 모듈에 이관하고 나머지 모듈은 그대로 조립하여 배포만 하면 손쉽게 구성되겠죠.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;cqrs-패턴&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cqrs-%ED%8C%A8%ED%84%B4&quot; aria-label=&quot;cqrs 패턴 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CQRS 패턴&lt;/h3&gt;
&lt;p&gt;몇몇 분들은 눈치채셨을 수도 있겠지만, store-api 모듈과 store-service 모듈 안에는 각각 command 패키지와 query 패키지가 분리된 것을 볼 수 있습니다.&lt;br&gt;
또한, store-domain 모듈에서도 entity와 data가 분리되어 있죠.&lt;br&gt;
저희는 멀티모듈 아키텍처와 같이 CQRS 패턴을 함께 적용하기로 하였습니다.&lt;br&gt;
(사실 &lt;strong&gt;현재의 모습은 CQS에 가깝습니다만&lt;/strong&gt;, 최종 아키텍처 기준으로 말씀드리기 위해 CQRS로 소개하고 있습니다 😅)&lt;/p&gt;
&lt;div style=&quot;width: 55%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1e73a73e34b5f2523d780e2a3e697181/3004a/cqrs-design.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 99.58333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAACEklEQVR42oWUD4vbMAzF+/0/2GBwYxxlW4+jDA5Cywpt879J2uj0E3lemnbMICzb0pMsPXt1vV5tGAbr+966PLe+rq33NXsat9vNsJPt4LZ9VYUPa50hq67r7HK5WNu2VhWF1WUZOnsAjeMYjsw4N01jjQctPTh2iAKClQBxKhwMwXEOqGxxwo6zswMKBBsFDkAUIjHPdQHiyEAnw7kP4CpLACpiRD2fLffIynoJyMw+oKfTKewAVbAEyIEOkf8BylaluQMUWOVdOx6PIbUXnb0lIGuyK7x5h8MhfB6uPM8myzLb7/cpk2cZkhXZYYv+0BQVl43tdmu73S500WEJCADZbTabyJTAD10eJiKre1oDsOyy7MQEUeruyjhgEKR1IaqYr8jsU7NqeiH4sUe92UN/aAq0QdQUARKAc8rxx6/LmusiEByqoacaRvsB5gk6WDu9HMAYicTQaups4i/rieDgrFSf+CCggBsuh4BDh07TExzdZ5zZU8cEGOk7B8cngHcDSnlWJy/N4HWzWbAEGL+Fy43O0pDZd/VU/IrX7vLX1v3ACEDVIurk9Wso7kSbpwIXPbPWbyOaCSP9h/10zfWvd3v98ZaIuhQyYHxkO/v6bW1V3aTrihXp6eHw+vPdvq/fbP4DzSU+BD/7/ZHZlxcHrGraFIFShnCu9E8VnuV5EVLXVXDxX1IUpfM1T0SXEPATzEgbfMSkr4IAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1e73a73e34b5f2523d780e2a3e697181/263a4/cqrs-design.webp 480w,
/static/1e73a73e34b5f2523d780e2a3e697181/a6361/cqrs-design.webp 960w,
/static/1e73a73e34b5f2523d780e2a3e697181/0b34d/cqrs-design.webp 1920w,
/static/1e73a73e34b5f2523d780e2a3e697181/ea3cd/cqrs-design.webp 2222w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1e73a73e34b5f2523d780e2a3e697181/9aebd/cqrs-design.png 480w,
/static/1e73a73e34b5f2523d780e2a3e697181/a91f8/cqrs-design.png 960w,
/static/1e73a73e34b5f2523d780e2a3e697181/ac7a9/cqrs-design.png 1920w,
/static/1e73a73e34b5f2523d780e2a3e697181/3004a/cqrs-design.png 2222w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1e73a73e34b5f2523d780e2a3e697181/ac7a9/cqrs-design.png&quot; alt=&quot;cqrs design&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;CQRS 패턴의 특징은 명령성(CREATE, UPDATE, DELETE)과 조회성(SELECT)을 서로 분리하는 디자인 패턴인데요.&lt;br&gt;
자칫 복잡할 수도 있는 이 패턴을 적용한 이유는 뭘까요?&lt;/p&gt;
&lt;p&gt;저희는 새로운 도메인을 신규로 개발하는 것이 아닌, &lt;strong&gt;기존 레거시에서 매장에 대한 도메인을 추출&lt;/strong&gt;하여야 합니다.&lt;br&gt;
최종적으로는 매장 도메인 자체의 DB를 별도로 구축하는 것이 목표입니다.&lt;br&gt;
이때 Command 영역(entity)에서 JPA로 구현할 도메인 엔티티는 유용하게 사용될 것입니다.&lt;br&gt;
또한 Command 패키지를 따로 분리함으로써 데이터 추가/수정/삭제에 대한 커맨드를 단순화시키고, 관련 비즈니스 로직들을 응집시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 조회 쪽도 JPA로 구현된 도메인 엔티티를 그대로 사용하면 어떨까요?&lt;br&gt;
레거시에서 기존에 제공하는 &lt;strong&gt;매장 관련 조회 쿼리는 보통 적게는 몇십 줄에서 많게는 몇백 줄까지의 SQL 문&lt;/strong&gt;으로 이루어져 있습니다. 이런 쿼리들을 한 번에 JPA로 변경하는 것은 쉽지 않겠죠.&lt;br&gt;
따라서 Query 영역에서는 Querydsl과 MyBatis를 같이 사용할 수 있도록 아키텍처를 설계하여, 기존 조회 기능을 손쉽게 이관하고 신규 조회 기능도 더욱 빠르게 개발할 수 있도록 했습니다.&lt;br&gt;
또한 Query 영역을 Command와 분리한다면 추후 조회 관련 성능을 높이기에도 용이할 겁니다.&lt;/p&gt;
&lt;p&gt;이뿐만 아니라 올리브영에서는 Master DB와 별개로 Read Replica DB를 구성해서 함께 사용하고 있는데요. 이 역시 CQRS 패턴을 선택하게 된 사유 중 하나였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;도메인-모델-구축&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%AA%A8%EB%8D%B8-%EA%B5%AC%EC%B6%95&quot; aria-label=&quot;도메인 모델 구축 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;도메인 모델 구축&lt;/h2&gt;
&lt;p&gt;자 이제 위의 아키텍처를 사용해서 프로젝트의 뼈대는 구축이 완료되었습니다.&lt;/p&gt;
&lt;div style=&quot;width: 25%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 732px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c5729a522103344301493b8028e3cdbb/61a54/skeleton-multi-module.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 130.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAD0ElEQVR42oVVyVYbVxTkR2IbNLVaarWm1sSgASEJITS0BBowg5nkIAw4YMAOAQf7hHOy9iKL5GSRP8g+31Z5dRMIjiV5cfW6W9317r1Vt96E7jMRT8wiFLVQz6/jqPsjkqk0qvUmiqUKSovLmJnNYqFQxlKljmDIgs8fhN8IDY0JAhIgmohjvTzA9dnvqLbaOD9/h+OTM3x3eoHV9hpu3n/E5dsfEArHxgPyhy9oXgO6PwCXpsHl0TE56cbTZ048eeqAw6lhyuHBs0kXnC7vSDAB9OoBVVIOsVQSu/U3+HTxJ5arLZXZOb49OMLhq9ewm23s7L6U6739Ayl7JCCzC4XjMIJhZBIllGprSGayqNWa0jNGJruA8lINleWG9JLfeDT/aEAzaCFghpGMZVFIVmEaUUSshPSL2bCKWHxariPRhCJxRogaWXImswArlcCgeYVfPv4Fu93D7e1PuLi8wtt319jY3MHthztc33zA91fvJd4o0lLTadnMCIQ/J4VM67oJTffDYXjh9ZvweHxSllut/IjX98F7Ph8pm5SSTSQeR2exj+udT0inF9Bd24Btt9FsdTCfL6Fhr6JWb2FltSckjdKkAMbiMwhZFpr5TQz6d4qYBg4OjoXZ/f4A9cYKBocncr/2fAsvtvflmv3k90N1yD98holgJCJlUW/U5uSU+0GHXB+XPkzgAhgwI3C5dfR6m/j1tz8wPZORjDgp2zt9yZLR6a7j/v3HRHwBKMSo3SxVQmG+iLCSC3VHaRSKS7JyEyuWkqy/OnoS6iWfErgzW0YynUcslkQ6kxdpUIPJ1Bzm0vOywTiD+AeQ6av5NRrr0F7foVJfwb4aMY7a1os9HB2fysj1Xx7i9OwSs3M5yXRY2Q8ZGkYYmqHMwaeJBtlTksCV/1N3zIoaHGsOD2B+A7nQIvrxUxRzFdjKwlqtrsiDWmRPqYRRZAwFLEaq6IS3Ua7URG8UcWulK0K+BxwH9kXJbr8uwLpmwuHSRIvMiOUyKBeWPS7LicdgDauHq+TPsOsdDF6diBZpAvRF6pFBgsj2V0nxGUFEAykkzDlElWQsKyXyMIPRh6w4arQ0go3PUL2saYY6mGxUG7aYKI2AmmPv2EeaQyI5+3BwjdUhATmbBNnc2lVHgC266/Y2xAupPwY34P8kbBTjE589UBqLKOd2uX3qQHKKBknMN0+m5IDiPVfqUzx0COv/TYpXnRGFOtyDG2SKy9j717raneeSFctm0LqYNaeI5Ax1bAHUVaOzi/CqQ4rzSw9kv3LKLFg620Bd8jn7ynvO9/8n52+OtnpGAniQCAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c5729a522103344301493b8028e3cdbb/263a4/skeleton-multi-module.webp 480w,
/static/c5729a522103344301493b8028e3cdbb/02596/skeleton-multi-module.webp 732w&quot; sizes=&quot;(max-width: 732px) 100vw, 732px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c5729a522103344301493b8028e3cdbb/9aebd/skeleton-multi-module.png 480w,
/static/c5729a522103344301493b8028e3cdbb/61a54/skeleton-multi-module.png 732w&quot; sizes=&quot;(max-width: 732px) 100vw, 732px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c5729a522103344301493b8028e3cdbb/61a54/skeleton-multi-module.png&quot; alt=&quot;skeleton multi module&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;이제 본격적으로 도메인 모델을 구축해야 하는데요.&lt;br&gt;
앞서 말씀드렸듯이 저희는 기존 레거시에서 매장에 대한 도메인을 추출하여야 합니다.&lt;br&gt;
즉 테이블을 새로 만드는 것이 아닌, &lt;strong&gt;기존 테이블을 사용&lt;/strong&gt;해야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;매장에 관련된 기존 테이블은 어떻게 구성되어 있었을까요?&lt;br&gt;
단순 매장 정보에 관련된 테이블만 봐도 매장부가정보, 부가상세정보, 상세정보, 일반정보상세, 추가정보, 통합정보 등 굉장히 많은 테이블들이 현재 사용하지 않는 컬럼들과 함께 포진되어 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;테이블-경량화통합&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B2%BD%EB%9F%89%ED%99%94%ED%86%B5%ED%95%A9&quot; aria-label=&quot;테이블 경량화통합 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;테이블 경량화·통합&lt;/h3&gt;
&lt;p&gt;저희는 정리되지 않은 채로 오랜 시간 방치되어 왔던 테이블들을 모두 그대로 가져갈 수는 없다고 판단했습니다.&lt;br&gt;
따라서 저는 도메인 모델을 구축하기 전, 2가지의 작업을 먼저 시작하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;필요한 컬럼 식별&lt;/li&gt;
&lt;li&gt;필요한 테이블 식별&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h5 id=&quot;필요한-컬럼-식별&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%84%EC%9A%94%ED%95%9C-%EC%BB%AC%EB%9F%BC-%EC%8B%9D%EB%B3%84&quot; aria-label=&quot;필요한 컬럼 식별 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[필요한 컬럼 식별]&lt;/h5&gt;
&lt;p&gt;우선 저희의 가장 큰 목표는 매장 서비스에서 관리해야 할 테이블과 컬럼들을 최대한 통합하고 줄이는 것이었습니다.&lt;br&gt;
흔한 레거시의 특징이겠지만, 매장 테이블인데도 &lt;strong&gt;매장 정보와 직접적으로 관련 없는 컬럼&lt;/strong&gt;들이 많이 들어가 있었습니다.&lt;br&gt;
우선 도메인 전문가와 함께 이 컬럼들을 일차적으로 걸러내는 작업을 진행하였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 25%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 354px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8c8e587317d5743320ce7e9554b42d2b/16bd5/column-identify.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 118.07909604519776%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAAAsTAAALEwEAmpwYAAAD0klEQVR42nVV7VIaWRCdB9mKUeQbFAWDgRVmBAYYUFAxJEFRYUQQtIxrVaq2Kqn9s297n+F2zplyLCDkx6nbNDN9T3ef7jGKxaLq9/syHo/1zc2NPDw8yGQy8bCzsyPhcFii0egC6Gu327K1tSWRSGThPyOdTivbtuXo6EhXq1WpVCoe6vW6bG9vSywWk3g8vgAGARFJJpML/9M2TNNUV1dXcnd3pwE5Pz+Xy8tLmU6nsru7673MB+dB38nJiXchWfl+jyHSUqVSSWq1mn49pVwuC1mTwfwLPphyPp+XRCLxe0DLstRgMBDUUd/f38vT05PMZjOPYSaT8R5aTpm+TqcjqVTq95TJkIwAr4asHdmRLRkuF91vChn69Vxoit/lXq/nMXx+fvZYXl9fy97enncrU5sHfWdnZ54KGNT30zZyuZxqtVpshmaKgUDAu3WVXOYZFgqF1QwbjqMG0N+t62oTUojj9i2kSiRx6yrwmdbxseyghgkE9f20jUqjofq3t/Lp4kJncjnZwI1B/BHES5t/AJ8pQqthXBoAq7f/yHBQLqsnpPzS6eizdFqK799LGWmXABupVVegvLkpQzTNQRA7FPJ8NfoB42c+r75//SpT29Y/cP4PCb1grF4g3GMwreLlWjC4APpcy5I6gvF3HSjBvn33ToxeNqsKzaZ8yOW0BdkQBcjoAAximIQQgoZQn3kwvezBgUQwy/wdAdZgHyMr47FS8Wr4pdvVY4yeC4bucCh76HgQTNiAZcRQq2aj4TWCdpL1g326vi7GRSajymB4aJq6gYe4GCjabDYrWBxvWluelP39/TftsbubaNDpxgYY2rYaPT7KBMuBI0e4ruutL54UsD+/vuYYhNr1t01yPmAP66uIupUsS5MdlwNWmTiOI2TMsSRjriueZM3gZOjPL8sQANsTpvytWlUjsLobDvUIjFjDay7c0Ujux2OZYRzpf8Cy4NnERXGwbEPYKTSC7LYRLAy7w6b0MHr7XFmOowtYCiZsC6BdAdO/wfADxmwd6a4j0AbY8EyDLRsR4BAAfyHlJlP+8fGjcpHyP62W/oZb/+125WevJzME/Q+L9jvW1BQ2NefrjjocUdgU+pwOR9ThaSqlJgj4iE/AFOm8oAkTBLgxTTHX1qSIh6zX6fFxiFr1oUNORwmsfD9tYxcpOwjSwfpiiu3Pn6UJVrTjaEAItQlT4BTxK5jmIS6PQlJBpOr7aRuNel1BMtLF+hpi6+CUFoIlKGDUbZWwo/A3+BFDEAp7XvAGpKCoNWxrTblQKtzWy+t9WdiUEz+jy5+AX8axJzvY/i/tAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8c8e587317d5743320ce7e9554b42d2b/7e454/column-identify.webp 354w&quot; sizes=&quot;(max-width: 354px) 100vw, 354px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8c8e587317d5743320ce7e9554b42d2b/16bd5/column-identify.png 354w&quot; sizes=&quot;(max-width: 354px) 100vw, 354px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8c8e587317d5743320ce7e9554b42d2b/16bd5/column-identify.png&quot; alt=&quot;column identify&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;그리고 추가로 예전에는 사용했던 컬럼이지만, &lt;strong&gt;현재는 사용하지 않는 컬럼&lt;/strong&gt;들도 존재하였습니다.&lt;br&gt;
이 경우에는 운영환경 DB에서 데이터가 null인 건수와 백오피스 플랫폼에서 해당 컬럼에 대한 사용처들을 리스트업한 후 도메인 전문가와 함께 상의하여 걸러내는 작업을 진행하였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1050px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a81f4bf39c30cda6e255835454788316/dd14e/column-identify2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 18.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA5UlEQVR42j2O3WqDQBSEfZWQaFyNFxGDu7r+1MbgT9QUWyWEQi/66uc5pkcLXnwszOzMHKPvOvjnM1zH2XCEgH08QvDruu6m1VUFHcf/nm1vbDnG+JpnfE4TiusVNguCCxaSLMN7WSJUChaHFq9uW+g0xd40IaMI3TBA8cDiO6fTOm78jiO+73fMtxs0CxFfsvCR56tWS4nQsqA49Goa9Foj2O/RcuGS7ZMEqedB8qDHpcazLOnFtJcLadOkxLJWHkrRmKbUBAHFh8PqPYuCBikp3O2o8n36aRqa8pzGLKM3/uMLQX+BxIHY3vHUVgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a81f4bf39c30cda6e255835454788316/263a4/column-identify2.webp 480w,
/static/a81f4bf39c30cda6e255835454788316/a6361/column-identify2.webp 960w,
/static/a81f4bf39c30cda6e255835454788316/5e9fb/column-identify2.webp 1050w&quot; sizes=&quot;(max-width: 1050px) 100vw, 1050px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a81f4bf39c30cda6e255835454788316/9aebd/column-identify2.png 480w,
/static/a81f4bf39c30cda6e255835454788316/a91f8/column-identify2.png 960w,
/static/a81f4bf39c30cda6e255835454788316/dd14e/column-identify2.png 1050w&quot; sizes=&quot;(max-width: 1050px) 100vw, 1050px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a81f4bf39c30cda6e255835454788316/dd14e/column-identify2.png&quot; alt=&quot;column identify2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h5 id=&quot;필요한-테이블-식별&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%84%EC%9A%94%ED%95%9C-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%8B%9D%EB%B3%84&quot; aria-label=&quot;필요한 테이블 식별 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[필요한 테이블 식별]&lt;/h5&gt;
&lt;p&gt;이제 매장 관련 모든 테이블의 필요 컬럼이 식별됐으니, 다음 작업은 필요한 테이블을 식별하는 작업입니다.&lt;br&gt;
주로 사용되는 DB 테이블이 있지만, 컬럼 한두 개를 제외하고는 거의 사용되지 않는 레거시 테이블도 존재했습니다.&lt;/p&gt;
&lt;p&gt;저희는 이 &lt;strong&gt;레거시 테이블에 있는 필요 컬럼을 주로 사용되는 테이블로 이관하는 작업(테이블 통합)&lt;/strong&gt; 을 3가지의 순서로 진행하였는데요.&lt;br&gt;
&lt;strong&gt;필요한 컬럼을 식별하는 작업처럼 단순히 체크(?)&lt;/strong&gt; 만 하는 것과는 달리, 해당 작업은 실제 &lt;a href=&quot;https://en.wikipedia.org/wiki/Data_definition_language&quot;&gt;DDL&lt;/a&gt;문과 &lt;a href=&quot;https://en.wikipedia.org/wiki/Data_manipulation_language&quot;&gt;DML&lt;/a&gt;문을 같이 실행합니다.&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;주로 사용되는 테이블에 이관해야 할 필요 컬럼을 추가 생성합니다. (&lt;a href=&quot;https://en.wikipedia.org/wiki/Data_definition_language#ALTER_statement&quot;&gt;ALTER&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;백오피스 플랫폼에서 해당 데이터가 INSERT/UPDATE 되는 곳을 찾아서 통합하려는 테이블에도 데이터가 같이 반영되도록 수정하여 배포합니다.&lt;/li&gt;
&lt;li&gt;레거시 테이블의 필요한 모든 컬럼 데이터를 주로 사용되는 테이블의 해당 컬럼으로 마이그레이션 합니다. (&lt;a href=&quot;https://en.wikipedia.org/wiki/Update_(SQL)&quot;&gt;UPDATE&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;※ Row Lock 주의 ※&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 추후 이 레거시 테이블의 기존 사용처들이 모두 저희 매장 서비스 API를 사용하도록 전환된다면, &lt;a href=&quot;https://en.wikipedia.org/wiki/Data_definition_language#DROP_statement&quot;&gt;DROP&lt;/a&gt; 문을 통해 레거시 테이블과 깔끔한 작별 인사를 할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1876px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/78e4b2ccc3a648617ef92bc61f2f44f9/bd320/erd.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 99.79166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAADa0lEQVR42o1Uy24cRRTtj0DyPPrdVd1d3T3d87ATe+IZJ8aOY2ccGQfHIkSYSEFESEgoSERCLJDYsQAW7PkGxIIdX8An8C+Hc2umR3bkRRZHXVVTc+8995xbTp7nWCPLYIY1is0RjMmRZQYf7t/F+dkcSqUoCoMb92+Bs94wQK5TmN070AdTqChEWeQo+FsSRfDdPkLfY5L0PQMSxhjEYQR3o8M/++j2etjY2ECn00GX6HW7SNP3CWiWVAyhlEIYhgiCAK7rot/vo8fAAlmneba+f3vAjAull/3jxSRJ4HsefFbYBloHJHQUk3ZmcWtAU1coF/tIqwKa1VVliaqqKECBOI6RCJIYmomyUY3J5QLNeISSySVoSaEEZlW1k1GMINcIwgCe5yIk1ZBrnwL0et11dSkFk7uaiSWBVKu0QsxzP06QiKA2IJsceD4DhfBWNLts/rsQMeSu73rw2Fs5kzZdHd7Ht2fHOJtNbVBHyg4oggjR/llUbdGeaa0tJfkKFFGw4qd79/Dy6AEOt7egmNCqHNFnAlHWQhJEyyS9vstW+MjybG2tFrIPEgU3SkhdLym/e8leTKkiBUqSFIvHU8ymI8RBzACi7lLNwizFkG9xLcENY8u0ZOzDYL6Nu88WKAc15tMhZs+PMXp6gqaprbpyN6TVPIrRJrh1Utp5zpoKzWybQmjE9J3m3mwNrYWiwIdmr053d/DJ/gyDsrjhyZuzvFqnzJ5wBBW/mvQVg8bsp/Ul/ZgywGcP7+PrJ0doqtLu1wHbHrTuz1YTY2hsRfqXF3NM741t4OKagf0whhtEq2DGvk4CJ2YVAS9PPj7G+KNHqAaVFaW8M8b44gSHVwuMR5wcU6BmNQ0nqeB3cnqAzUcP7Fl+rRjnc5b+y9WFpayoYkTbpJHQopH5fIlZ/b6H316c45+3r/Hvj9/gir7zKlY1HEDFES0W4avHBxbO3uYY5/MpKUT4oOuiRyEmh3to6tpSy8ig3t3Gw6N9nM52cML1gNQ9P7SCyQQJ7S06QOCIu7vsx4uDPfz+8hl+eH6GcmeTD2uGRMW2L/mkgRoU6PD5Knn+15sv8N/Pb/H3d68xZIuEqjBSbJUjvpKgr0jjjy8/xU+XT0jRtRMjb6OIICon8kYSI9rkV7bozzev8P3FYvkorAQRUf4Hkv1aR8rD7Z0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/78e4b2ccc3a648617ef92bc61f2f44f9/263a4/erd.webp 480w,
/static/78e4b2ccc3a648617ef92bc61f2f44f9/a6361/erd.webp 960w,
/static/78e4b2ccc3a648617ef92bc61f2f44f9/09047/erd.webp 1876w&quot; sizes=&quot;(max-width: 1876px) 100vw, 1876px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/78e4b2ccc3a648617ef92bc61f2f44f9/9aebd/erd.png 480w,
/static/78e4b2ccc3a648617ef92bc61f2f44f9/a91f8/erd.png 960w,
/static/78e4b2ccc3a648617ef92bc61f2f44f9/bd320/erd.png 1876w&quot; sizes=&quot;(max-width: 1876px) 100vw, 1876px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/78e4b2ccc3a648617ef92bc61f2f44f9/bd320/erd.png&quot; alt=&quot;erd&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;해당 사진은 위와 같은 작업들을 거치며 &lt;strong&gt;식별된 영역 중에 일부&lt;/strong&gt;인 매장 정보에 대한 DB 테이블 목록입니다.&lt;br&gt;
이처럼 테이블 경량화·통합 작업을 완료했다면, 실제로 &lt;strong&gt;매장 도메인에서 필요한 테이블과 컬럼들만 집중적으로 관리·사용할 수 있는 환경이 마련&lt;/strong&gt;됩니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;entity와-vo&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#entity%EC%99%80-vo&quot; aria-label=&quot;entity와 vo permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Entity와 VO&lt;/h3&gt;
&lt;p&gt;이제 본격적으로 도메인 모델을 구현할 차례입니다.&lt;br&gt;
DDD의 전술적 설계에서, 도메인 모델은 크게 Entity와 Value Object로 구성합니다.&lt;br&gt;
그리고 특정 군집에 포함되는 여러 Entity와 Value Object를 에그리거트(Aggregate)라고 정의합니다.&lt;br&gt;
해당 군집을 대표하는 Entity를 에그리거트 루트(Aggregate Root) 혹은 루트 엔티티(Root Entity)라고 부릅니다.&lt;br&gt;
이 애그리거트들이 모여서 하나의 바운디드 컨텍스트를 형성합니다.&lt;/p&gt;
&lt;p&gt;예시를 간략하게 그려보면 아래와 같이 구성될 겁니다.&lt;br&gt;&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/8f53e83f1c7f765a7bc0c5b97ab170ab/20759/ddd-modeling-example.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.04166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABgElEQVR42n1TCZLDIAzL/3/aM21DCObySt64Q7adZUaNMUL46tRa03H13pW+Usob67oaaq1vHznkjovnE398uRB9fXjgcrno9Xr9uOzCB0F3SEoq26aNgkDGvoPQctbG72DzjBxyeUdEjhEWkgQCaVMFIT8eGs9n7SHgJdGyRa2AQqgh9Yho8zzbvgOZHGiYIDeMkq91iPUYNd5uGk4nzc+nCVb4CQoU+AIeiyiD8T0riDHSKYFk9YMz4tUHyfgWRDJDdGE0iFSAgIeeEOJZRBa0+eVdLmodBDe8ToIsi1ZEur1emoAMm+A5fTwjh9wE34cgO+njQlR2G/6EqBKioU3IHiltNqfvfN3HxwQzOjeOjo0KO0YxpJjud62cQcD2AM+OE/jbYWpNPnvjPJkgBVBLdpwpsm60hXvYo6Bl5V12IR9oXxJWDWhIvN9UINBatbm02fwzzGWv4b//lISiL0h3RZQSV9UvKdYv4iboTfHGWD0wU6xJAfIXuNh4l8H8ANow/AJ5TaDIAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8f53e83f1c7f765a7bc0c5b97ab170ab/263a4/ddd-modeling-example.webp 480w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/a6361/ddd-modeling-example.webp 960w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/0b34d/ddd-modeling-example.webp 1920w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/da28f/ddd-modeling-example.webp 2880w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/53dbb/ddd-modeling-example.webp 3360w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8f53e83f1c7f765a7bc0c5b97ab170ab/9aebd/ddd-modeling-example.png 480w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/a91f8/ddd-modeling-example.png 960w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/ac7a9/ddd-modeling-example.png 1920w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/f9c26/ddd-modeling-example.png 2880w,
/static/8f53e83f1c7f765a7bc0c5b97ab170ab/20759/ddd-modeling-example.png 3360w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8f53e83f1c7f765a7bc0c5b97ab170ab/ac7a9/ddd-modeling-example.png&quot; alt=&quot;ddd modeling example&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;전술적 설계를 더 깊숙하게 들어간다면 아래처럼 굉장히 많은 개념과 전략들이 존재합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에그리거트 루트를 통한 불변식 검증 응집&lt;/li&gt;
&lt;li&gt;도메인 서비스를 통한 여러 에그리거트 간 협력&lt;/li&gt;
&lt;li&gt;이벤트 기반 아키텍처를 통한 바운디드 컨텍스트 간 느슨한 결합 유지 및 시스템 확장성, 유연성 향상&lt;/li&gt;
&lt;li&gt;이벤트 소싱을 통한 명확한 상태 변경 이력 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;하지만 이번 포스팅에서는 Entity, Value Object에 대해서만 설명하고, 간략한 &lt;a href=&quot;https://en.wikipedia.org/wiki/Snippet_(programming)&quot;&gt;코드 스니펫(Code Snipet)&lt;/a&gt;을 통해 어떻게 구현했는지 소개해보려고 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h5 id=&quot;entity&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#entity&quot; aria-label=&quot;entity permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[Entity]&lt;/h5&gt;
&lt;p&gt;우선 JPA 덕분에(?) 비교적 친숙한 Entity입니다.&lt;br&gt;
Entity의 가장 큰 특징은 &lt;strong&gt;식별자를 가지는 것&lt;/strong&gt;입니다.&lt;br&gt;
이 식별자는 Entity 객체마다 고유하면서 바뀌지 않기 때문에 &lt;strong&gt;식별자만 같다면 두 엔티티는 같다고 판단&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;p&gt;이런 특징을 이용해서 저는 아래와 같이 추상 클래스를 구현하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DomainEntity&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DomainEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TID&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TID&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
  
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; other&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;other &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;other &lt;span class=&quot;token keyword&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DomainEntity&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; otherEntity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; otherEntity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;otherEntity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
    &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TID&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;DomainEntity 객체는 현재 딱 한 가지의 역할을 수행하고 있습니다.&lt;br&gt;
바로 equals() &amp;#x26; hashCode() 재정의를 통해 &lt;strong&gt;식별자(id)가 같다면 두 엔티티는 동등하다고 판단&lt;/strong&gt;하는 역할을 충실히 수행합니다.&lt;br&gt;
식별자(id)만 같고 나머지 모든 필드 값이 다르더라도, Entity는 동등하다고 판단합니다.&lt;/p&gt;
&lt;p&gt;이제 Entity를 구현할 때, 해당 DomainEntity를 상속받는다면 자동으로 Entity 동등성 비교가 이루어질 것입니다.&lt;br&gt;
또한, 해당 Entity가 동일한 트랜잭션 범위에서 JPA 영속성 컨텍스트에서 관리되고 있다면 동일성까지 보장될 겁니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TestEntity&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DomainEntity&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TestEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
  
    &lt;span class=&quot;token class-name&quot;&gt;TestEntity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;phone &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;DomainEntity를 상속받아 구현한 TestEntity에 대해 테스트 코드를 작성하고 실행한 결과는 아래와 같습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1174px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c08db6a63c7da5cdcf61052494d8bed3/683d4/domainEntity-test.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 34.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABGElEQVR42oWR226DMBBE+Zw2QDB3QsIlEBNsMNCoqdo+9P//Yup1gpSHRn0YeS1LZ2bWllgmXD++8fn1g1Fd0J4EqpqjKFuUVavnE+pj96/m5Yp8X8FiYYy8qtALBd5JrQFpdkCc5IjiHZgfY2N7RrbD4Lj+nwrCFFsvhGXbDF4SgffSQIWcIIdZO75DTRftzrHLSxyKozF5Dsw0MILlOD78LEEnBggx6co9zv1owKsB1V4TPwNSE49pINUgYC8Vjk2Hpj2bhKN6M2ASrSJJ93h5deFuAwNYz1WeBt4q34FSzeBcGgAB14TDuBjRndKTEc30YY9AP0huCakyiyPUDTd7onqUtCgbk5Z2uIreCEozreDxk9g94S+SFO3826k9fQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c08db6a63c7da5cdcf61052494d8bed3/263a4/domainEntity-test.webp 480w,
/static/c08db6a63c7da5cdcf61052494d8bed3/a6361/domainEntity-test.webp 960w,
/static/c08db6a63c7da5cdcf61052494d8bed3/6ba46/domainEntity-test.webp 1174w&quot; sizes=&quot;(max-width: 1174px) 100vw, 1174px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c08db6a63c7da5cdcf61052494d8bed3/9aebd/domainEntity-test.png 480w,
/static/c08db6a63c7da5cdcf61052494d8bed3/a91f8/domainEntity-test.png 960w,
/static/c08db6a63c7da5cdcf61052494d8bed3/683d4/domainEntity-test.png 1174w&quot; sizes=&quot;(max-width: 1174px) 100vw, 1174px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c08db6a63c7da5cdcf61052494d8bed3/683d4/domainEntity-test.png&quot; alt=&quot;domainEntity test&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h5 id=&quot;value-object&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#value-object&quot; aria-label=&quot;value object permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[Value Object]&lt;/h5&gt;
&lt;p&gt;다음은 Value Object입니다.&lt;br&gt;
줄여서 VO라고도 많이 불리는 이 객체는 개념적으로 완전히 하나를 표현할 때 주로 사용됩니다.&lt;br&gt;
VO의 가장 큰 특징은 &lt;strong&gt;불변(immutable)&lt;/strong&gt; 입니다.&lt;br&gt;
즉 한번 값이 할당되면 변경될 수 없으므로, 값을 바꾸고 싶다면 아예 새로운 인스턴스를 생성해야 합니다.&lt;/p&gt;
&lt;p&gt;객체가 불변이라면 자칫 사용하기 불편할 수도 있을 텐데 어떤 장점들이 있을까요?&lt;br&gt;
우선, &lt;strong&gt;참조 투명성이 보장&lt;/strong&gt;되어 안전한 코드를 작성할 수 있습니다.&lt;br&gt;
따라서 &lt;strong&gt;멀티 스레드에 안전&lt;/strong&gt;하다는 특징도 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;또한, VO는 Entity와는 다르게 식별자(id)가 존재하지 않습니다.&lt;br&gt;
따라서 VO는 &lt;strong&gt;모든 필드 값이 같아야만 동등하다고 판단&lt;/strong&gt;하는 특징이 있습니다.&lt;/p&gt;
&lt;p&gt;이런 특징들을 이용해서 저는 아래와 같이 추상 클래스를 구현하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ValueObject&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ValueObject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
  
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; other&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;other &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;other &lt;span class=&quot;token keyword&quot;&gt;instanceof&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ValueObject&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; otherValueObject&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Arrays&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getEqualityFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; otherValueObject&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getEqualityFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;17&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; each &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getEqualityFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
            hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;31&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;each &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; each&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;hashCode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; hash&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
    &lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getEqualityFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Arrays&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getDeclaredFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
                &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
                    field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setAccessible&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
                    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;IllegalAccessException&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
                    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toArray&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ValueObject 객체도 마찬가지로 현재 딱 한 가지의 역할을 수행하고 있습니다.&lt;br&gt;
equals() &amp;#x26; hashCode() 재정의를 통해 &lt;strong&gt;모든 필드 값이 같다면 두 VO는 동등하다고 판단&lt;/strong&gt;하는 역할을 충실히 수행합니다.&lt;br&gt;
근데 여기서 &lt;code class=&quot;language-text&quot;&gt;getEqualityFields()&lt;/code&gt; 메서드가 의문이 들 수도 있을 겁니다.&lt;/p&gt;
&lt;p&gt;DomainEntity 경우에는 제네릭을 통해 식별자(id)의 타입을 알고, &lt;code class=&quot;language-text&quot;&gt;getId()&lt;/code&gt; 메서드를 추상화함으로써 상속받는 Entity 객체는 항상 &lt;code class=&quot;language-text&quot;&gt;getId()&lt;/code&gt;를 구현할 수밖에 없습니다.&lt;br&gt;
따라서 해당 &lt;code class=&quot;language-text&quot;&gt;getId()&lt;/code&gt; 메서드를 통해 추상 클래스에서 바로 equals() &amp;#x26; hashCode()를 재정의할 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 ValueObject인 경우에는 &lt;strong&gt;동등성 비교 시 모든 필드 값을 알아야 하는데&lt;/strong&gt;, 실제 VO를 구현하기 전까진 추상 클래스에서는 어떤 필드 값들이 있는지 확인할 방법이 없습니다.&lt;br&gt;
따라서 &lt;a href=&quot;https://docs.oracle.com/javase/tutorial/reflect/&quot;&gt;Java의 Reflection API&lt;/a&gt;를 통해 선언된 필드 값을 가져와서 equals() &amp;#x26; hashCode()를 재정의합니다.&lt;/p&gt;
&lt;p&gt;이제 VO를 구현할 때 해당 ValueObject를 상속받는다면 자동으로 동등성 비교가 이루어질 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TestVO&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ValueObject&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TestVO&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
  
    &lt;span class=&quot;token class-name&quot;&gt;TestVO&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;phone &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
  
    &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt;  
    &lt;span class=&quot;token keyword&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getEqualityFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;  
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; phone&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다만, 여기서 한 가지 특징이 있습니다.&lt;br&gt;
ValueObject 추상클래스를 상속받아 구현한 실제 VO 객체에서도 &lt;code class=&quot;language-text&quot;&gt;getEqualityFields()&lt;/code&gt; 메서드를 한번 더 재정의해 주는 겁니다.&lt;br&gt;
사실 Java Refelction API는 여러 가지 이유로 보통 사용을 지양합니다.&lt;br&gt;
따라서 VO 객체를 구현할 때 정의한 필드를 그대로 &lt;code class=&quot;language-text&quot;&gt;getEqualityFields()&lt;/code&gt; 메서드에도 재정의해 준다면, Reflection API를 사용하지 않고도 동등성 비교를 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;ValueObject를 상속받아 구현한 TestVO에 대해 테스트 코드를 작성하고 실행한 결과는 아래와 같습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1240px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6fd765ff0b4f767fde3cd0f17b92745e/09504/valueObject-test.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.708333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABIElEQVR42m2R23KCQBBE+ZkkiqCyiOIFcLkjFy18yf//SGd7KCwrycNUT03tnu3ptbrHiPH5ja4fkVwLRHFmKv23LhFVv/Up4mQ6X5Q3VHUPK0xiNO0d/TCaQYfwGEH5B+yCI/xdKBrsT1L7w1mUM/a74IStF8BdKyhz9hBeYDmehyhL0dzuKKsWddO/9NZOs9mJTktkeW02yZFmlSiB9mqD9caX3nI9JcDa2M2L5gVru4d5ZJCLfJm1WLr4+LTxtXCkX9prrJwtHNebXJrNLFcpxFkmQMK49rtLQqnMiA75AN16ai+guQjkTIC6KtE0g1yanTHT4f4UJXB2eTonkiMBf4Di0Kx80Rpal7jqQqBUOmQEzIzO5gxZ/Lh51d/AH0E28CY2qtROAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6fd765ff0b4f767fde3cd0f17b92745e/263a4/valueObject-test.webp 480w,
/static/6fd765ff0b4f767fde3cd0f17b92745e/a6361/valueObject-test.webp 960w,
/static/6fd765ff0b4f767fde3cd0f17b92745e/53f84/valueObject-test.webp 1240w&quot; sizes=&quot;(max-width: 1240px) 100vw, 1240px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6fd765ff0b4f767fde3cd0f17b92745e/9aebd/valueObject-test.png 480w,
/static/6fd765ff0b4f767fde3cd0f17b92745e/a91f8/valueObject-test.png 960w,
/static/6fd765ff0b4f767fde3cd0f17b92745e/09504/valueObject-test.png 1240w&quot; sizes=&quot;(max-width: 1240px) 100vw, 1240px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6fd765ff0b4f767fde3cd0f17b92745e/09504/valueObject-test.png&quot; alt=&quot;valueObject test&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;다음-이야기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%9D%8C-%EC%9D%B4%EC%95%BC%EA%B8%B0&quot; aria-label=&quot;다음 이야기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;다음 이야기&lt;/h2&gt;
&lt;p&gt;이번 글에서는 도메인 주도 설계에서의 전술적 설계를 간략하게 소개하였습니다.&lt;br&gt;
도메인 모델을 구축하기 위한 &lt;strong&gt;스켈레톤 프로젝트 구축을 시작으로 테이블 경량화·통합 및 Entity와 VO까지 구현&lt;/strong&gt;해보았습니다.&lt;br&gt;
사실 전술적 설계에서는 정말 많은 개념과 전략들이 포함되어 있지만, 더 깊게 들어가기에는 이번 포스팅 주제와 맞지 않아 보여서 일부분만 소개해 드렸는데요.&lt;br&gt;
더 자세한 부분은 오프라인 매장 서비스의 여정 시리즈를 마무리한 후에 별도로 포스팅해보겠습니다 😁&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 HTTP API 설계를 시작으로 AWS, CI/CD, 모니터링 등 인프라 환경에 대한 내용을 소개하며, 본격적으로 매장 서비스를 오픈해 나가는 이야기를 해보려고 합니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다 🙂&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h3 id=&quot;알렉스-로그&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EB%A0%89%EC%8A%A4-%EB%A1%9C%EA%B7%B8&quot; aria-label=&quot;알렉스 로그 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;알렉스 로그&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;입사
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-11-30/journey-to-joining-oliveyoung&quot;&gt;주니어 개발자의 우당탕탕 입사기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매장 도메인
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3/&quot;&gt;10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정산 도메인
&lt;ul&gt;
&lt;li&gt;To be continued...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;</content:encoded></item><item><title><![CDATA[10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음]]></title><description><![CDATA[매장 도메인 분리의 첫걸음 안녕하세요! 올리브영에서 매장 도메인을 담당하고 있는 알렉스입니다 :) 2023년도 11월에 주니어 개발자의 우당탕탕 입사기로 첫 포스팅을 하고 벌써…]]></description><link>https://oliveyoung.tech/2025-01-24/store-service-journey-1/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-01-24/store-service-journey-1/</guid><pubDate>Fri, 24 Jan 2025 19:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;매장-도메인-분리의-첫걸음&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%B6%84%EB%A6%AC%EC%9D%98-%EC%B2%AB%EA%B1%B8%EC%9D%8C&quot; aria-label=&quot;매장 도메인 분리의 첫걸음 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 도메인 분리의 첫걸음&lt;/h2&gt;
&lt;p&gt;안녕하세요! 올리브영에서 매장 도메인을 담당하고 있는 알렉스입니다 :)&lt;/p&gt;
&lt;p&gt;2023년도 11월에 &lt;a href=&quot;https://oliveyoung.tech/2023-11-30/journey-to-joining-oliveyoung/&quot;&gt;주니어 개발자의 우당탕탕 입사기&lt;/a&gt;로 첫 포스팅을 하고 벌써 1년이라는 시간이 흘렀는데요. &lt;br&gt;
이전 글에서 마무리 멘트로 &quot;다음 글에서는 &lt;strong&gt;O2O 서비스를 모듈화·모던화&lt;/strong&gt; 하면서 직면하는 기술 문제들을 해결해 나가는 경험을 소개하겠다&quot;고 했었습니다.&lt;br&gt;
그 약속을 지키러 다시 돌아왔습니다 😄&lt;/p&gt;
&lt;p&gt;저희가 &lt;strong&gt;지난 1년간 어떤 여정을 떠나왔는지&lt;/strong&gt; 아래와 같은 시리즈로 소개해 보려고 하는데요!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;✅ &lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3/&quot;&gt;10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;이번 포스팅에서는 &lt;strong&gt;Part.1 도메인 분리의 첫걸음&lt;/strong&gt;이라는 주제로 시작하겠습니다 🚀 &lt;br&gt;
(이 글에서는 &quot;기술&quot; 얘기가 많이 없으니 편하게 봐주세요 🙌)&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;10년 된 레거시에는 어떤 문제들이 있었을까?
&lt;ul&gt;
&lt;li&gt;레거시에 한 발짝 다가가기&lt;/li&gt;
&lt;li&gt;비즈니스 로직의 분산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매장 서비스의 시작
&lt;ul&gt;
&lt;li&gt;도메인 경계 식별하기&lt;/li&gt;
&lt;li&gt;컨텍스트 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다음 이야기&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;h2 id=&quot;10년-된-레거시에는-어떤-문제들이-있었을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#10%EB%85%84-%EB%90%9C-%EB%A0%88%EA%B1%B0%EC%8B%9C%EC%97%90%EB%8A%94-%EC%96%B4%EB%96%A4-%EB%AC%B8%EC%A0%9C%EB%93%A4%EC%9D%B4-%EC%9E%88%EC%97%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;10년 된 레거시에는 어떤 문제들이 있었을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;10년 된 레거시에는 어떤 문제들이 있었을까?&lt;/h2&gt;
&lt;h3 id=&quot;레거시에-한-발짝-다가가기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%EA%B1%B0%EC%8B%9C%EC%97%90-%ED%95%9C-%EB%B0%9C%EC%A7%9D-%EB%8B%A4%EA%B0%80%EA%B0%80%EA%B8%B0&quot; aria-label=&quot;레거시에 한 발짝 다가가기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;레거시에 한 발짝 다가가기&lt;/h3&gt;
&lt;p&gt;레거시를 모던화할 때의 첫 단추는 무엇일까요? 저는 그 레거시의 히스토리를 파헤치고, 이해하는 것이 가장 우선이라고 생각합니다. &lt;br&gt;
10년 전으로 거슬러 올라가 보겠습니다.&lt;/p&gt;
&lt;p&gt;올리브영은 초창기에 내부 개발자가 존재하지 않았습니다. 그때 당시 모든 CJ 계열사의 전반적인 IT 담당은 CJ올리브네트웍스가 맡았기 때문에, 올리브영의 개발·운영 관련 업무 역시 올리브네트웍스가 진행했었습니다.&lt;/p&gt;
&lt;div style=&quot;width: 40%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1426px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5e9e7b52c96ca4fd4da504caf110b1c0/0f849/oliveone.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 165.41666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAhCAYAAADZPosTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFjElEQVR42q1WaU+UVxSeT/ZP1C+1TSXR2GpN7EI30qQlxmKlVjMiIyKbLDrMDDMjIDPADOsMO0RQBERRoaiprYrSQklZCkRQFhU3EFH2zX15+j63uQO0Tb/om5zce8+95znnnnPuOa/qzp076O7uxpUrVzA4OIje3l5Bd+/exfXr13Ht2jX09PSI/f7+fly9ehXj4+MYGBgQ80uXLgl5zimv6ujoQF1dHRoaGsRGY2MjmpqaBAjH1tZWscczVHTx4kUhSLnm5mbU1NSgvr4ebW1tAlT19OlTPHnyBPyeP3+Oly9f4sWLF2LkJ8f/+3hGnlM9fvwYs7Ozgh48eICHDx+65nJNBVTG8Z9E/qNHj/Ds2TMBqsJr/lTyqtRAotZXAuT1zGYzoqKioNVqERERgfDwcAQHByM4KBgBgUHY7h8Av+3+2LZNgy1bfKBWq+Hj4wONRgMPDw8hLw1TTU5Oig2CEZSbRpMR9sQ4JMRGYfdOP6g3rsPn7h9hxfIVcFu6FEuWLFHobbi5uWHRG4vg5eU1Bzg1NYVdu3bBYrHAbrch3eFEUVExyssO4uM1n2DTxs2w2RzQ6w0wRYfArA+FPiIIm7294O3tDXd3d8WYyDnA6elpYZ2VgEkpKM5NxM/lqciz6/HO4jexfNkq+Gh0UPuFITIpALrYeERbsrHq/Q8VOQOOH/8RWVnZrvRRzczMwGAwwGqNh9PpwOnDGeiqq0BDZTpKk0NwIN4P1dmhqMozoDhtJ1JMGuxPV9wS6oskkz+ynOmwKLL8GFARFKPRKJg5WQ70Np5Ax4Uj+PNUIdpPOHC2NA6NJ5LR92sKuv+oQmf9EfQ0VaK/twXnyuwwR4bAaI6eA2TymkwmxO61IDMtERUOLY7kJqG2uhBnDzlRmW9E3p7vUGILhCN2BxzWSGSlRqOksABHCzMQs9tX8X/8QkC9TgetzoB8x17kJUTBHmPBuVInCuIiUBCrhn23B2xaJRN8PoXvpnU4GOaJasMPsCfYELZ1rXK7hDlAPq3w8DAEh4SirCAZPa3nUJoWh/YcK3qP5eJ8RTJSozeg2BmNxEg1YnRBqM3Vo9oeisQ9enzrvgwp1piFgEFKAgcGBqA4cy8666pwpiQVR1MNqMiJQ5kCbN6xHumGrfi9KhvN1Vn4qciK/D3bUGTZiQztJpwpz/k7yhLwG09PrHxvBdZ+thLff7UG/hu+gG7r14jZ7glb4Fpk6TbioMUPv+wzobYkFg0VdjSdylP8fAAZilsOFeUstHD16g/w7luL4bv+SxQm61FTkYkLh9NwXqH66gL8drIItSeLcfrYPlSW5+NwSQFK9xcg05kCfz8NMpR0WxCUlpYWtCmFdGhwAFOT45idmcT0xCgmxoYxPjqMsZFhjJJGRzA8PIz79+/j3r17GBoaws2bN8XaVRzm17bXUr5YrWUhJcnCOr/Y8jXxic7f45qy86u1AJyYmBA9gmaPjo66xpGREYyNjYmRjYzEPV6TPF6Zyv5lIcs3AQjMbkZr6CfOyeOcPJY5KpBnuZ5vAHEEIDV1dnaKNsmudePGDdH92EIlj45nO+U57nHNDtjV1SXaKM/RPQKQlvDKvEpfX58AYs8l79atW6IXk08iEPs1lVIRa6nsmgu6Hp0sg0O/0HzyOZJksHhGniefaxZVgroAeWVqpyW0iNppIee09vbt22KPc460kntSRv5ByLRzAZLoFwpyfvnyZUFUQP+RR5+RxzXn9J2UdVnIyPG3g4cYOeYXR/qH0ZURnU/k0/eUlTnpslB2fhn2V34pzDMZWfqG0ZXXlBFmZOdfj37kKM9QloFx+bC9vV34hD6kg3mQABTkmoHhSJ5UzpfDfc5pgCsPmSbMQRIzniOzn5plZSFfVhe66L/+vuSV/wKhSrivtlj5ZAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5e9e7b52c96ca4fd4da504caf110b1c0/263a4/oliveone.webp 480w,
/static/5e9e7b52c96ca4fd4da504caf110b1c0/a6361/oliveone.webp 960w,
/static/5e9e7b52c96ca4fd4da504caf110b1c0/50e10/oliveone.webp 1426w&quot; sizes=&quot;(max-width: 1426px) 100vw, 1426px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5e9e7b52c96ca4fd4da504caf110b1c0/9aebd/oliveone.png 480w,
/static/5e9e7b52c96ca4fd4da504caf110b1c0/a91f8/oliveone.png 960w,
/static/5e9e7b52c96ca4fd4da504caf110b1c0/0f849/oliveone.png 1426w&quot; sizes=&quot;(max-width: 1426px) 100vw, 1426px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5e9e7b52c96ca4fd4da504caf110b1c0/0f849/oliveone.png&quot; alt=&quot;oliveone&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;이때 탄생한 백오피스 플랫폼이 바로 올리브원이라는 시스템입니다. &lt;br&gt;
이 올리브원이라는 백오피스 플랫폼에는 매장, 발주/입고, 재고, 상품, 오프라인 쿠폰, 포인트 등 정말 다양한 도메인들이 한 통에 담겨있었습니다.&lt;/p&gt;
&lt;p&gt;그러다 2021년도를 기점으로 올리브영에 뛰어난 개발자분들이 대거 합류하기 시작하면서, 7명에서 시작한 내부 개발 조직은 현재 20배 이상으로 급격하게 늘어났습니다. &lt;br&gt;
이에 따라 발주/입고 등을 관리하는 SCM 스쿼드, 재고를 관리하는 인벤토리 스쿼드, 상품을 관리하는 상품 메타 스쿼드, 쿠폰/증정을 담당하는 쿠폰증정 스쿼드 등 정말 많은 스쿼드가 생겼는데요. &lt;br&gt;
현재 각 스쿼드는 각자의 오너십 도메인을 올리브원으로부터 분리해나가고 있는 상태입니다.&lt;/p&gt;
&lt;p&gt;하지만 여기서 애매한 도메인이 있습니다. &lt;br&gt;
바로 &lt;strong&gt;매장&lt;/strong&gt;이라는 도메인인데요. 올리브영의 모든 서비스는 항상 &lt;strong&gt;매장&lt;/strong&gt;에서 시작되고 끝나기 때문에, 대부분의 스쿼드에서 이 매장에 대한 정보를 필요로 합니다. &lt;br&gt;
하지만 이 매장에 대한 오너십을 가지고 API나 기타 방법으로 정보를 제공하는 스쿼드가 없었기 때문에, 각 스쿼드는 DB에서 매장 테이블을 직접 바라보는 사태가 벌어졌습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;비즈니스-로직의-분산&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81%EC%9D%98-%EB%B6%84%EC%82%B0&quot; aria-label=&quot;비즈니스 로직의 분산 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;비즈니스 로직의 분산&lt;/h3&gt;
&lt;p&gt;여러 스쿼드가 매장 테이블을 직접 바라볼 경우 어떤 문제점들이 있을까요? &lt;br&gt;
한가지 예시를 들어보겠습니다.
Alex 손님은 오늘 올리브영N 성수점을 방문하고 싶습니다. &lt;br&gt;
따라서 현재 영업 중인지, 아직 영업 전인지, 아니면 마감했는지 미리 확인하고 싶은 상황이라고 가정해 보겠습니다.&lt;/p&gt;
&lt;p&gt;이때 판단 기준은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;해당 매장이 정상적으로 개점한 매장인지? 아니면 아직 공사 중인 매장이거나 폐점한 매장인지?&lt;/li&gt;
&lt;li&gt;오늘이 매장 휴무일인지?&lt;/li&gt;
&lt;li&gt;오늘의 영업시간은 몇 시부터 몇 시인지?&lt;/li&gt;
&lt;li&gt;현재 시간이 영업시간 안에 포함되어 있는지&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;여기서 한가지 정책이 추가됐다고 가정해 보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존 매장 영업시간은 월요일부터 일요일까지 각각의 오픈/마감 시간이 세팅되어 있습니다.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;수요일 영업시간은 10:00 ~ 22:00&lt;/strong&gt; 라고 가정해 봅시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;※ 추가 정책 ※&lt;/strong&gt; : 설날, 추석 등의 공휴일일 때는 별도의 영업시간으로 관리해야 합니다.
&lt;ul&gt;
&lt;li&gt;오늘은 설날(수요일)이고 &lt;strong&gt;설날 영업시간은 10:00 ~ 18:00&lt;/strong&gt; 라고 가정해봅시다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;이 경우 어떤 일이 발생할까요? &lt;br&gt;
모든 스쿼드가 이 정책을 잘 전달받아서 로직을 수정하면 다행이지만, 이 정책을 놓친 스쿼드가 있다고 해보겠습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1100px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/92e12a2a0871ea569f695344d71f659f/28a3f/n-seongsu.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.041666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABrElEQVR42nWRW28ScRDF9+v5JXwympj43Bh9UaM+NDFN1Ie+a9pae0vTtHJzUSnXgoWFFMqtCyQLu7AsZQt7KT//kOKDiSc5mZPJZGbOjGToOoVcBkPr0KhWSZzGiYRlNH2I40wZDExM08SyLMbjMbZtM8dsNltwqW/vKNUuqwQO9yhloxyLuL2xyYuXbymrBtlcjpNQmF/xOKGIzGkyRb5YYnRzg26KgZ7HEq7Qk+kUqd5QkcPH1JQUcvCEg50vfN09QO0Oca0hWuMKvalyrXUZqC2sdgejKXL1Jv1uD9Ma0dP7+L6/aCw11Q7JRJRgZA85EOLocJ+jQBC9reHEYvTlKIPMGXpBoZM+o/f7nO6cmSwjkZ+4rjiFjbds2Go2ePfhFa/XV9jZ3ebNx1U+7W/SVvLMZBkvlcQpFhlelDHyBUxhua8UGQjtZTLcHfGvdWnr8zoPH9xja+M9a2vPefb0CY8f3Sf28we2dU25VOGqUsfSerQu61SVC/RWByYTZo7Dv5Bq1Yr4bJhsKk0+neA8FSP+/Ru6pi0KJo7L1PPxxRbzOLfo+rf8D38ALQ30d5zR7NsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/92e12a2a0871ea569f695344d71f659f/263a4/n-seongsu.webp 480w,
/static/92e12a2a0871ea569f695344d71f659f/a6361/n-seongsu.webp 960w,
/static/92e12a2a0871ea569f695344d71f659f/cb2d6/n-seongsu.webp 1100w&quot; sizes=&quot;(max-width: 1100px) 100vw, 1100px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/92e12a2a0871ea569f695344d71f659f/9aebd/n-seongsu.png 480w,
/static/92e12a2a0871ea569f695344d71f659f/a91f8/n-seongsu.png 960w,
/static/92e12a2a0871ea569f695344d71f659f/28a3f/n-seongsu.png 1100w&quot; sizes=&quot;(max-width: 1100px) 100vw, 1100px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/92e12a2a0871ea569f695344d71f659f/28a3f/n-seongsu.png&quot; alt=&quot;n seongsu&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;해당 스쿼드의 서비스에서는 분명 영업시간이 10:00 ~ 22:00 라고 표시됐기에, Alex 손님은 19:00 쯤에 매장을 방문했지만 오늘은 설날이라 18:00까지 밖에 영업을 안 하니 이미 문을 닫은 상태일 겁니다. &lt;br&gt;
이런 사례가 반복된다면 자연스럽게 &lt;strong&gt;사용자의 서비스 신뢰도는 하락&lt;/strong&gt;하겠죠. &lt;br&gt;
하지만 &lt;strong&gt;매장 정보를 한 곳에서 통합적으로 관리하고 제공&lt;/strong&gt;한다면 이 문제는 쉽게 해결될 수 있을 것입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;매장-서비스의-시작&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%EC%9E%A5-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%9D%98-%EC%8B%9C%EC%9E%91&quot; aria-label=&quot;매장 서비스의 시작 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;매장 서비스의 시작&lt;/h2&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1350px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f391cbbd38098aea6a65081112798bf3/9ac8b/msa.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.83333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAAC50lEQVR42n2TW28TRxiGBwlQK0BIVVVVCAQXSFwg9Sf0Eql3vehP4D8USAJIQSKKQBwapFYVtGmBhBxNAokJPgkcx6fEG8fGsRPv+oCbrBN7iU84Jz/MLikSN4z06pv9VvPM9828I3Z2dtjcbO5pE/O72WyytbXF7u4u5thttax5a0+1Wo3C2wIZLfuZspkcolqtMjs3iz/gx+lwoGkqoVCIucgcuq6TzWZpNBoWuNX6CDTH9vY2/xVWPoH+lzArMqFG2aAoAWZc04sYpTLVSoX1tSLlcpn39RrvJbhlgVsWvFFvkFEzqKqGmtYsuDB3X0onWdQWya8WiGUWiErFs0lSWprUclbCSyzLFlWpimFQb9RNrFWpuUmpZFCr1lld0RHGuzIjk5OMTTnpezrGH32/cfevLm72dvHkyQjJBRVtZY1h7zhjwSnmF1NUKhsEfNN0XW3n795eeu7c5eLFS0xM2BG1egW3y81Q/xj35c/bPd3cu3Od3x/cwvaon6XEErnVdQacNkb9DkLybLc2ylzpvoU4fJIjX3/F0e+OI4Sgva3dbLmG2+NmZMjG8MgEDx72MzA6QN/gI54PTjEdnqW6UaGwrpNbe4teLMhrb3L5sYt9v/zJkVM/cOD8fcSZn7jc3YOoyAsJx+eJxRKM2xw8fT6I/cUAtheDKBIWUgLSJk1y70L4s/+SKk5bZ9fW0WFV9c258+z/B8SPF2i/dgOxqq/wetaNNxjA5/Nj99l4GRxnKDhM/8wwo8Fn5At5DKNMyShRLOqWJzs7Ozm4fx/fnjjNsZ87OPT9Wa60/YowfafMz6EoCrH4ApE380TfyBhTiCxErFw8HkeJKESjURKJBPl8XipHMpnC533Fq6lnvPY4CAQCiHQ6Lc2ckR7S0FRV+movmjkZNZlPpZIWzOVy4ff7JShJOr1MOBzG653GN2N2NyPnXkQkEuFLMis3X47H48EhX5LL6ZSLfThlnJR2s9vtn+kDSvwhcPWaDcEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f391cbbd38098aea6a65081112798bf3/263a4/msa.webp 480w,
/static/f391cbbd38098aea6a65081112798bf3/a6361/msa.webp 960w,
/static/f391cbbd38098aea6a65081112798bf3/cbeb9/msa.webp 1350w&quot; sizes=&quot;(max-width: 1350px) 100vw, 1350px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f391cbbd38098aea6a65081112798bf3/9aebd/msa.png 480w,
/static/f391cbbd38098aea6a65081112798bf3/a91f8/msa.png 960w,
/static/f391cbbd38098aea6a65081112798bf3/9ac8b/msa.png 1350w&quot; sizes=&quot;(max-width: 1350px) 100vw, 1350px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f391cbbd38098aea6a65081112798bf3/9ac8b/msa.png&quot; alt=&quot;msa&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;저희 스쿼드는 팀장님과 테크 리더의 지원 하에 매장 서비스를 구축하기로 결정했습니다.&lt;br&gt;
매장 서비스는 &lt;strong&gt;매장 도메인에 대한 오너십&lt;/strong&gt;을 맡고, 매장에 관련된 정책을 응집하여 관리하고 매장 정보를 제공하는 서비스입니다.&lt;br&gt;
물론 올리브원이라는 레거시에서 매장 도메인을 떼어내는 것도 포함입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;도메인-경계-식별하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EB%A9%94%EC%9D%B8-%EA%B2%BD%EA%B3%84-%EC%8B%9D%EB%B3%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;도메인 경계 식별하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;도메인 경계 식별하기&lt;/h3&gt;
&lt;p&gt;특정 도메인을 추출하려고 할 때 우선으로 필요한 것이 무엇일까요?
먼저 매장 도메인과 다른 도메인 간의 관계를 대략적으로 파악해야 합니다.&lt;/p&gt;
&lt;div style=&quot;width: 50%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e45eacb8b5318fd0569ad0f5d3c72e27/27773/domain-model-example.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 85.625%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAABpElEQVR42o2UC2/DIAyE+///ZCetqpS0TfN+NYH5YzlEtqUakhUw5uw7TE7LsvijwV7Xdf5dTDper5c/HQWv6+rrug7WNE0IJvadTdP0HhAg2TAMhzaOo2/b1ldVdQwoCo/H41+UBRwBOYzDObcDJGvqYw419lI2fd9/a8gCUEqGGqAK4kJa8xEssN4qud1uvrDKAWZwhjk4AXCe5yA+1WC64drASluTCDCsMqCP89lfr9dAk0Ex7O1uGdBU6OCzzF1ZRg0dVJ9P31vl41adWksYEVBXHzYsuDOaCzQ2DZ2tXQKiWOQAaNeH6jlKZy5dKtNqNbqr7TkltkOlVU08QFSnS4uXghOdACRwMLDaqM12MAKaDI6eNA0/Lxef53kAljw7QAywNBuB9f2O4t5BCb/ZunWEihCjX09P1KVLaB9urijCN+1F4jisdvqzQioigOxKgq8xWtPWm0qMnxeU9mcE1GvIsszfjSIaCuxpOmamFdRoZtHEzzp9BLFt1NgE6/mkP4fCKONPD7Em4c83HipUH0lo5jwjvvqLaK0vPp3DJ+P8F1G5NKMMODhPAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e45eacb8b5318fd0569ad0f5d3c72e27/263a4/domain-model-example.webp 480w,
/static/e45eacb8b5318fd0569ad0f5d3c72e27/a6361/domain-model-example.webp 960w,
/static/e45eacb8b5318fd0569ad0f5d3c72e27/0b34d/domain-model-example.webp 1920w,
/static/e45eacb8b5318fd0569ad0f5d3c72e27/a5deb/domain-model-example.webp 2412w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e45eacb8b5318fd0569ad0f5d3c72e27/9aebd/domain-model-example.png 480w,
/static/e45eacb8b5318fd0569ad0f5d3c72e27/a91f8/domain-model-example.png 960w,
/static/e45eacb8b5318fd0569ad0f5d3c72e27/ac7a9/domain-model-example.png 1920w,
/static/e45eacb8b5318fd0569ad0f5d3c72e27/27773/domain-model-example.png 2412w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e45eacb8b5318fd0569ad0f5d3c72e27/ac7a9/domain-model-example.png&quot; alt=&quot;domain model example&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;이렇게 간략하게만 그려봐도 매장 정보들의 전체적인 흐름을 직관적으로 볼 수 있는데요. &lt;br&gt;
매장 도메인은 POS, 매장 전시, 상품 등 여러 타 도메인과 협력하는 관계입니다. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;하지만 더 중요한 것은 매장 도메인과 관련된 하위 도메인들을 식별하는 작업입니다. &lt;br&gt;
이 매장 하위 도메인들이 &lt;strong&gt;특정 기준을 갖고 경계&lt;/strong&gt;가 지어지는데, 이 경계를 &lt;strong&gt;바운디드 컨텍스트&lt;/strong&gt;라고 부릅니다. &lt;br&gt;
오랫동안 경계가 모호했던 영역이라서 명확하게 분리하기는 어려웠지만, 이 바운디드 컨텍스트를 기준으로 저희 스쿼드의 업무 범위가 결정될 예정이었으므로 최대한 이 경계를 식별할 필요가 있었습니다.&lt;/p&gt;
&lt;p&gt;바운디드 컨텍스트를 최대한 식별해야 할 때 어떤 요소들이 필요할까요?
저는 아래 3가지의 요소가 필요하다고 생각합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;해당 도메인 전문가&lt;/li&gt;
&lt;li&gt;레거시 히스토리를 잘 아는 고인물(구세주)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Event_storming&quot;&gt;이벤트 스토밍 워크숍&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;다행히 저희 스쿼드에는 매장에 대한 도메인을 누구보다 잘 알고, 실제로 CJ올리브네트웍스에서 올리브원을 개발하다 오신 PO와 개발자분들이 계셨습니다. &lt;br&gt;
따라서 저희는 이분들과 함께 회사의 지원으로 AWS Korea에서 퍼실레이터의 가이드에 따라 &lt;strong&gt;이벤트 스토밍 워크숍&lt;/strong&gt;을 진행하였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/8162e/event-storming-env.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAADNElEQVR42g2QbUzUBQDG/1kBIwlXmdWo2w3agGLhUYthUBGK56BxJBQnJsRBdwrcCxwc48Xj5Y7zTu48DuSCO16PhIBFKox0pW2IJmLTiWhIZwi2tVowbX38dR+ePV+ePW9CmUaPqlxHfkEJRcUqBjptuKxNjPV1MDnQiaetGYexCnuDLsB6Osy1dNvNDLqdjPt6GR3w4nbYaDJo0RTnIfgmpzB3D6NpttN0zMrYxCgTAQz4hnB1uTlut3PMaqW5qZGGWgMGvZrKciU1FWXo1Bo0gTIGg5G6OguyDCnC/PoGM79tMHJthbO/LHF+5U++vnQT36XbjC746f3pBv0XFmiwtqEqVVL4eQ75OXs5XJTLhYs/cnVpg5srj1jzb1KtViHM3ltnbv0x3995yMyNZWYf/sfU4h+MX3/A5N1N+i/76Tw9h+mEiy5PNx5vN+7ONrzeDvx3r7B2f41fV/9l+dYaLlsLwumFQJMri4xfXWJidp6L9/9m6tYqY3O3mVrZxPHNHPs/0qCrtjJyZprvpme4fO1nJs9Mcn3xHsv+v/A/eBTgx3Q5TAhn7/yOoUKLpbWVnv4hjFYHLS1mihRK7EOnqPf8gHRXBRaTh+FeJ8XZGQwODzEy3IvnpJNB71f4+r309rhxn2hGaNSXUSXfizovkz5jJdKEOPbERBErEiNLlKD5UssXsjwiX4mi5ch+VB+nEC0Wc1R/BGd9Me1HS3A1KnE1HabfWYtQqtZj0imwlH5GuTyTmvx0EqNFpL8rYbr9ON+6bLTqK8mKe5Uecx197nYK5bkUFRyitvQgtqpDOGoKsFXK+TQ5FiF1dybZ6R+gkH2I6WAGstQkYrY/jzRaTFZCbMDQgk5ZT8T2eNJSPuF8n5numgMostPY834SudIUyg7so1qRxXuROxA0hfLAnJeRJr9NhzKb3ZJYtgUF81J4OFtDQpFKXifihecQhCDCQkNJiIog7rUd7HpDTIksFWniW2QkSzjn0pITL0IwKPMDnwREUSKSdsaw700R4SFBPPHkU4QEB7MtbCvBgYCnt2whdWckE8Y85l0FrJ7S8s85G7lp7/DMs2GcVGcS/2Io/wMj5BmDpJ95wgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/263a4/event-storming-env.webp 480w,
/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/a6361/event-storming-env.webp 960w,
/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/0b34d/event-storming-env.webp 1920w,
/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/2c4db/event-storming-env.webp 2005w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/9aebd/event-storming-env.png 480w,
/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/a91f8/event-storming-env.png 960w,
/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/ac7a9/event-storming-env.png 1920w,
/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/8162e/event-storming-env.png 2005w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9ea5ad2f7340b3c8fa9d8b175acea6e6/ac7a9/event-storming-env.png&quot; alt=&quot;event storming env&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;하루 종일 진행한 이벤트 스토밍에서는 다양한 커뮤니케이션을 통해 정말 많은 정보가 도출되었습니다. &lt;br&gt;
아래는 이벤트 스토밍의 흔적인데요.&lt;/p&gt;
&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6af5f0f2d8bf902c25b93c876329d733/181f3/event-storming.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.00000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB50lEQVR42nVSS2/TQBDO3wZByo0LJy5ckJA4IlQJqVC1iFJSkbQIEKhqhVKFIpvYSRrH9q5f+/yYXSdpQ2FWu56d+fzNY6dzdzfCnbcZtj7U6B4WuL+foHtQoPue087woFfhUV/g8SDFwyOGewc52du95XSPLTzW2TrTOETFY1j+A4ZdQERfgWgXVsRw0rAQi+AVmukXQDVoxULTKWrCsE+kBFhJh6mACM+h81NUWR+1/A09eQMjEg+o52fIgx1wfkn+8yUdcdNRijnM4hCKX3ibpdURJqdobWQXteanMFfbsKolFKrGJN6D1WyZl4G11vvG2QgqeYlp+A2zhLUZrpyOzjhCdgYz3YHRmbdWVFYyfELM8bpcj6ZPnh7Dln0orSCVXhGaJaiNz6gsFTyDaX55gJQ5mvEAevgCZnYEk51QrTGUAHi0B82o33WyDnYjw5awLKif46cw9ajNRFVQ5U/IaB+GD4m0R8YCWhqIlHo3/0j38vpR/ias+Yh6+I56eLVRojUWN8Vd0/kl5OwzOe26yo0eClolAQy9siX9mvD2dsGTqAeVDlrUkudWhnwxgAqfw1Zjlxb+LdY/YJF8h65C1EUAmZ+4qd0kdKCmIefkNY0D9caq/xK6v2QzI7Wh8iWNVeZH6g/wo0SJwyqEIAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6af5f0f2d8bf902c25b93c876329d733/263a4/event-storming.webp 480w,
/static/6af5f0f2d8bf902c25b93c876329d733/a6361/event-storming.webp 960w,
/static/6af5f0f2d8bf902c25b93c876329d733/0b34d/event-storming.webp 1920w,
/static/6af5f0f2d8bf902c25b93c876329d733/12e29/event-storming.webp 2532w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6af5f0f2d8bf902c25b93c876329d733/9aebd/event-storming.png 480w,
/static/6af5f0f2d8bf902c25b93c876329d733/a91f8/event-storming.png 960w,
/static/6af5f0f2d8bf902c25b93c876329d733/ac7a9/event-storming.png 1920w,
/static/6af5f0f2d8bf902c25b93c876329d733/181f3/event-storming.png 2532w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6af5f0f2d8bf902c25b93c876329d733/ac7a9/event-storming.png&quot; alt=&quot;event storming&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;해당 워크숍을 통해 저희는 &lt;strong&gt;저희 스쿼드가 관리해야 할 바운디드 컨텍스트를 식별하고 각각의 도메인 모델을 도출&lt;/strong&gt;하였습니다.&lt;/p&gt;
&lt;div style=&quot;width: 60%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0bd2847e7212de6c975c00acdc8d660a/c977d/boundedcontext.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 72.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAABj0lEQVR42pVT266DMAzj//9xe0A7A8aAQmm5DJZjR82Epr1QqaxNG8d1vGwYBnm9XrIsizRtK4/HQ5qmkWVdZcUMIcj7/ZZ933VtsbIspa5raZ5PmaZJ4957yQZ8tm2TCOAJF5k4jaNEi8eogLa2seEeYzyzQXIKODonXVlIi6oTkhxZ3u8ScDaD+Z6SyYRjxm/oe/Fdp8V5xqEMRwRWJDnQH1GBFT0KDHg2n0EQY2iAQ9dKlefyd71KWxRa4APID6uQnaN2AK9uN8kvF5Ug/gBc10XZc8ak8QeQ76ZerqrEQWBWa7GuAcr4Lw2Puh2Hasgns7KDHlzP8ywjgHo822PPGDXULqe9T4WYd5w8y2gZVjRrWAM+hQBcQKcKrGkpxmmbkBxxnMTKrEPHoc8DmFoITJbUabKfk6ZkyNh33k9AM7GH0XuwCjQ/7g2H/SlAxgISmOjR+Zi8Rr9yb90/BWj+oy7054imsSkb9jjQ81OAZhFlBjAPS/HfsSfLWFNOachmcNJfND07bDHz5zfgP+LvRHHQfZwIAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0bd2847e7212de6c975c00acdc8d660a/263a4/boundedcontext.webp 480w,
/static/0bd2847e7212de6c975c00acdc8d660a/a6361/boundedcontext.webp 960w,
/static/0bd2847e7212de6c975c00acdc8d660a/0b34d/boundedcontext.webp 1920w,
/static/0bd2847e7212de6c975c00acdc8d660a/080e1/boundedcontext.webp 2574w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0bd2847e7212de6c975c00acdc8d660a/9aebd/boundedcontext.png 480w,
/static/0bd2847e7212de6c975c00acdc8d660a/a91f8/boundedcontext.png 960w,
/static/0bd2847e7212de6c975c00acdc8d660a/ac7a9/boundedcontext.png 1920w,
/static/0bd2847e7212de6c975c00acdc8d660a/c977d/boundedcontext.png 2574w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0bd2847e7212de6c975c00acdc8d660a/ac7a9/boundedcontext.png&quot; alt=&quot;boundedcontext&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
 &lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;컨텍스트-매핑&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%A7%A4%ED%95%91&quot; aria-label=&quot;컨텍스트 매핑 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;컨텍스트 매핑&lt;/h3&gt;
&lt;p&gt;여기서 한가지 고려해야 할 것이 있습니다. 바로 하나의 하위 도메인이 여러 개의 컨텍스트로 분리되는 경우인데요. &lt;br&gt;
예시를 위해 위의 바운디드 컨텍스트 안에 있는 상품 정보들을 가져와 보겠습니다.&lt;/p&gt;
&lt;p&gt;우선 상품 도메인이 상품 컨텍스트에 있을 경우 &lt;strong&gt;상품 메타 정보&lt;/strong&gt;를 의미합니다.&lt;/p&gt;
&lt;p&gt;하지만 상품 도메인이 매장 바운디드 컨텍스트에 있다면, &lt;strong&gt;매장에서 관리하는 상품&lt;/strong&gt;을 의미할 겁니다. &lt;br&gt;
즉 해당 매장이 판매하고 있는 상품을 의미하는 &lt;strong&gt;매장 취급상품&lt;/strong&gt;으로 정의할 수 있죠.&lt;/p&gt;
&lt;p&gt;상품 도메인이 진열 바운디드 컨텍스트에 있다면, 해당 &lt;strong&gt;상품의 위치&lt;/strong&gt;로 정의할 수 있습니다. &lt;br&gt;
더 나아가 매장 내 상품의 위치·배열·진열 방식을 시각적으로 나타냄으로써, 매대별로 상품을 효과적으로 배치·관리하기 위한 전략적 도구를 의미하는 &lt;strong&gt;POG&lt;/strong&gt;로 정의할 수도 있을 겁니다.&lt;/p&gt;
&lt;br&gt;
&lt;div style=&quot;width: 35%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1798px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d1ce2fae14442b1c97cf740e4b00aea1/ababd/context-mapping-exam.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 120.20833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAAAsTAAALEwEAmpwYAAADDklEQVR42nWV15LbMAxF9f+/tw+ZPNi7695UbRWrGLkHEhXFO+EMxhQIXrRLOOq6zl6vl8swDFYUxT9yv98tz3MX9u/n3Anr+XxaBGBYbdvK6C5ws67rre8Hl+12a9/f3/N31w0eAA4A+QHY95089VZXlUeC8agXqH7rpra6rv/qJNgQYVWVfhdd0zQWPR6PKZ2HxXFi5/PZHo9ySkkpKopbHEt/8b3rJNhcLhe73WK/i32WZRYlSeLpjSG3bsgahpf/VlWtlHe22WytLKsfZ00zpkwprterRWmaOhB1q+vGo5hrqIt5llumKAo5TiWhjvSC7ABlj/3tdpsA1Qx81s0IONBxSama3gWy//qy7WpthS48ynI+J5tKtVWLrOsXgG07dprwqUdIi8jT6832n5+2W68tUc0oPBkglICsWHT+KtuIQhJZr7irqcvsiRqvOMjTzArZUXhSe3at2+T6JmL2jShD8yI80lkEvq0VCd07nU4uvp/O3/Wfinyz2cz3qalHCCHhFVwjiiUP0R2OR9vtdp7BkocQGx17HsVcw/BaAMbLWMPxSZHS6XCws0ABWJ6VOiPDoHNAIgR9CRjeNY4KOcwvVyvEMaiDLefvgH3o8hKQw7vz8DWXIFa9Vr9+2e+PD7vu91YJJJwDiA37fkmbkDLAGC1XrvNUDUAydTFExwIsDAcy8pdCijw/IqVze0UBddDhDEkE5LLQYXOgtuoud9HR0IgUR8PMB8BOgFkGYDoD4OSopji4dJxhczgc/Q530eFkHA79NBwWKYfU8LoVZbYTbTy9VxgOlRPamzL8bzhMA5YBwNPKNNKKhOGgVJU2INSLp+lv2YfDaxoO8c/hEN5yLc+J6FLqfLNa2U6volKtEkUR7MNwUM+tXQK2U5cJ/z4Ru1UZUhlclOpu/enCnii7QGxFW088nKcNNQQEMB47341KgCGvJBZoIsNEv7HAAgj2qSLmDtlg64Bwj/8MSE0DoA51ovtQCgBqGX7RjYO18ibQWe4GcR6Gj2CEpyB8L+X9zDutuxAc2vwB6bc82NVvW60AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d1ce2fae14442b1c97cf740e4b00aea1/263a4/context-mapping-exam.webp 480w,
/static/d1ce2fae14442b1c97cf740e4b00aea1/a6361/context-mapping-exam.webp 960w,
/static/d1ce2fae14442b1c97cf740e4b00aea1/0532e/context-mapping-exam.webp 1798w&quot; sizes=&quot;(max-width: 1798px) 100vw, 1798px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d1ce2fae14442b1c97cf740e4b00aea1/9aebd/context-mapping-exam.png 480w,
/static/d1ce2fae14442b1c97cf740e4b00aea1/a91f8/context-mapping-exam.png 960w,
/static/d1ce2fae14442b1c97cf740e4b00aea1/ababd/context-mapping-exam.png 1798w&quot; sizes=&quot;(max-width: 1798px) 100vw, 1798px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d1ce2fae14442b1c97cf740e4b00aea1/ababd/context-mapping-exam.png&quot; alt=&quot;context mapping exam&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;이렇듯 같은 상품이라는 도메인도 &lt;strong&gt;어느 컨텍스트에 있고, 어느 관점에서 바라보는지에 따라&lt;/strong&gt; 의미가 달라질 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;이때 매장 컨텍스트의 매장 취급상품 정보에 추가로 상품 컨텍스트의 상품 메타 정보가 필요하다고 가정하겠습니다. &lt;br&gt;
이 경우 &lt;strong&gt;각 컨텍스트 간의 협력&lt;/strong&gt;이 필요할 겁니다. &lt;br&gt;
컨텍스트 간의 상호 협력, 즉 &lt;strong&gt;컨텍스트 매핑&lt;/strong&gt; 종류는 여러 개가 존재합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소비자와 공급자(Customer-Supplier)&lt;/li&gt;
&lt;li&gt;공유 커널(Shared Kernel)&lt;/li&gt;
&lt;li&gt;준수자(Confirmist)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;충돌 방지 계층(ACL-Anti-Corruption Layer)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;공개 호스트 서비스(OHS-Open Host Service)&lt;/li&gt;
&lt;li&gt;발행된 언어(PL - Published Language)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;div style=&quot;width: 80%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2b1571d68d6586abbc230f167b31f8ea/512a1/context-mapping2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 33.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAACHElEQVR42g2NW0iTAQCF/4deKrTCQulCJmg3oYeiByEiC+mhC05XTc3UUEJDrWkrmRpYxrS8/GrO26Cypc5qW7KloRaFFeqsVnPK2tx0qUxCJXsI4et/O3C+7xxBdlUkp6SJjMJa4lJKSM57QH6ZjuRr1Ry9oCY2qRR5bh2J2RqpqyK9QOSYQk1qYT0KiU2XvOT8atKUtciyyhGMI0v8Xlrhq3eFh51DtLwcpm0wQIPJgebpMPeaLTy2fsLm9LK4vIJreo6KJhP6V33U6q04J3wsBxawu2cwD9gQ7M4p7A4XreYxlBoDDU96eWR10Ng1hLregqgz4fb58foDfBi2U6kzo9K082PSw8h3N4GFRSZcPrSGN2hajAih0XI270tkU+RZ1oWfIijiNGHRCYQfTCJ0f4KU5Ww7oGBjVDwbomQER8YTsieBqJgMdsekEXH4IjsPpRCyV84WaUc4n11Bzi2RgttNZKtE8osbOS5TErzrDKm597l0vYYrN2pQlmrJKxIpLG3g5DkVQdJ5YmYZSRJz866O4vJWFFllCM2dRgZsblZXV7G7ZrB7fqGufMb6HXG0dLxG39OLvneEpT9/mVr4h20W7ojdrN16Ar1pkP7PYzgmp/AsQrv1G8LHsXGcbj/+2QDtxj60HRYuq+pYsz0WQ887Rh0/+TLuYW5+nu63E1SZvWQWaRHCjvDC8h63x8dzYw/9o9NUtFn5D43Gelj0FU7hAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2b1571d68d6586abbc230f167b31f8ea/263a4/context-mapping2.webp 480w,
/static/2b1571d68d6586abbc230f167b31f8ea/a6361/context-mapping2.webp 960w,
/static/2b1571d68d6586abbc230f167b31f8ea/0b34d/context-mapping2.webp 1920w,
/static/2b1571d68d6586abbc230f167b31f8ea/da28f/context-mapping2.webp 2880w,
/static/2b1571d68d6586abbc230f167b31f8ea/98b7d/context-mapping2.webp 3840w,
/static/2b1571d68d6586abbc230f167b31f8ea/db8b8/context-mapping2.webp 4026w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2b1571d68d6586abbc230f167b31f8ea/9aebd/context-mapping2.png 480w,
/static/2b1571d68d6586abbc230f167b31f8ea/a91f8/context-mapping2.png 960w,
/static/2b1571d68d6586abbc230f167b31f8ea/ac7a9/context-mapping2.png 1920w,
/static/2b1571d68d6586abbc230f167b31f8ea/f9c26/context-mapping2.png 2880w,
/static/2b1571d68d6586abbc230f167b31f8ea/5da7e/context-mapping2.png 3840w,
/static/2b1571d68d6586abbc230f167b31f8ea/512a1/context-mapping2.png 4026w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2b1571d68d6586abbc230f167b31f8ea/ac7a9/context-mapping2.png&quot; alt=&quot;context mapping2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;저희의 경우 매장 바운디드 컨텍스트와 상품 바운디드 컨텍스트의 관계를 충돌 방지 계층(ACL)으로 정의하였습니다.&lt;br&gt;
즉 Anticorruption Layer를 통해 상품 서비스의 도메인 모델을 매장 도메인 모델에 맞게 변환하여, 외부 도메인 모델이 매장 도메인 모델을 침범하지 않도록 막는 형태입니다.&lt;/p&gt;
&lt;div style=&quot;width: 55%; display:block;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/61b24663d2b7b0d129d999195915b9ff/20970/acl-example.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 58.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABF0lEQVR42o2Sxw7EMAhE/f8/mUOUKL33xuohzWpvG0vEYwPDQByWZTFsnmdb19XqurZhGEz38p3naf8WMQHwPI8bi+R93033CqTYK8Jt22wcR1dCklSBq6pyA6vIX0KCRThNkxsq2Wm973v3vSa879sTjuMwMInCqNcZ/IoQJRCiJk1Ti6LIkiSxsiy/SungtUIq0zbJJBZFYU3TOBH3mutrQohkmidYz0VYf5x1XZcns/9iRhP4yHCgsOu6b4Dmp3fIPQU4U5xRgYmjq6AEzZF227Z1jGL5UIpKznr8GJiXADlCgqoRmGWZxXHsBjH3zI4ECPUi8jz3nwYJ7xQMGTmBD23ioBpOdmEFo5Z2KUwsecSwyyj8AZAiptv+vViTAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/61b24663d2b7b0d129d999195915b9ff/263a4/acl-example.webp 480w,
/static/61b24663d2b7b0d129d999195915b9ff/a6361/acl-example.webp 960w,
/static/61b24663d2b7b0d129d999195915b9ff/0b34d/acl-example.webp 1920w,
/static/61b24663d2b7b0d129d999195915b9ff/43113/acl-example.webp 2828w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/61b24663d2b7b0d129d999195915b9ff/9aebd/acl-example.png 480w,
/static/61b24663d2b7b0d129d999195915b9ff/a91f8/acl-example.png 960w,
/static/61b24663d2b7b0d129d999195915b9ff/ac7a9/acl-example.png 1920w,
/static/61b24663d2b7b0d129d999195915b9ff/20970/acl-example.png 2828w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/61b24663d2b7b0d129d999195915b9ff/ac7a9/acl-example.png&quot; alt=&quot;acl example&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;하지만 사실 두 개의 컨텍스트가 어떤 관계인지 정의하는 것은 크게 중요하지 않습니다. &lt;br&gt;
컨텍스트 매핑 종류를 참고해서 컨텍스트 간의 협력 방법을 정하고, &lt;strong&gt;각각의 경계를 유지하면서도 각 경계 간의 도메인 모델을 적절히 변환·연계&lt;/strong&gt;하여 비즈니스 요구 사항을 만족시키는 것이 더욱 중요할 겁니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;다음-이야기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%9D%8C-%EC%9D%B4%EC%95%BC%EA%B8%B0&quot; aria-label=&quot;다음 이야기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;다음 이야기&lt;/h2&gt;
&lt;p&gt;이번 포스팅에서는 도메인 주도 설계에서의 전략적 설계를 중점으로 소개하였습니다. &lt;br&gt;
보시다시피 저희는 매장 도메인을 구현하기에 앞서 많은 준비와 과정을 거쳤는데요.&lt;/p&gt;
&lt;p&gt;설계와 구현의 과정 속에서, &lt;strong&gt;구현을 들어갔을 때 설계에 대한 변경이 잦을 경우 많은 비용이 발생&lt;/strong&gt;합니다. &lt;br&gt;
하지만 설계 단계에서 탄탄하고 좋은 설계를 한다면, 구현 단계에서는 구현에만 집중할 수 있어 비용을 크게 절약할 수 있습니다.&lt;/p&gt;
&lt;p&gt;저희 또한 많은 준비를 한 덕에 구현에 드는 비용을 많이 줄일 수 있었습니다. &lt;br&gt;
다음 포스팅에서는 실제 매장 도메인을 어떻게 구현해 나갔는지에 대해서 소개해 드리겠습니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다 🙂&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h3 id=&quot;알렉스-로그&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EB%A0%89%EC%8A%A4-%EB%A1%9C%EA%B7%B8&quot; aria-label=&quot;알렉스 로그 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;알렉스 로그&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;입사
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-11-30/journey-to-joining-oliveyoung&quot;&gt;주니어 개발자의 우당탕탕 입사기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매장 도메인
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-1/&quot;&gt;10년 된 레거시를 현대화하다 - Part.1: 도메인 분리의 첫걸음&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-01-24/store-service-journey-2/&quot;&gt;10년 된 레거시를 현대화하다 - Part.2: 매장 도메인의 구현 여정&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2025-04-30/store-service-journey-3/&quot;&gt;10년 된 레거시를 현대화하다 - Part.3: 대고객 서비스로의 확장&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정산 도메인
&lt;ul&gt;
&lt;li&gt;To be continued...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;</content:encoded></item><item><title><![CDATA[CSP를 중심으로 본 자동화 테스트 실전 사례]]></title><description><![CDATA[요즘 QA…]]></description><link>https://oliveyoung.tech/2025-01-05/csp_auto_test/</link><guid isPermaLink="false">https://oliveyoung.tech/2025-01-05/csp_auto_test/</guid><pubDate>Sun, 05 Jan 2025 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;요즘 QA 직무에서 자동화 테스트에 관한 관심이 뜨겁습니다.
그래서 그런지 많이 듣는 질문 중 하나가 &quot;올리브영은 자동화 테스트를 어떻게 하고 있나요?&quot; 였습니다.
그래서 드디어 가져왔습니다!!
올리브영에서 진행되고 있는 자동화 테스트 그중의 일부를 소개하고자 합니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;자동화-테스트를-도입하게-된-배경이-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%98%EA%B2%8C-%EB%90%9C-%EB%B0%B0%EA%B2%BD%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;자동화 테스트를 도입하게 된 배경이 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;자동화 테스트를 도입하게 된 배경이 무엇인가요?&lt;/h2&gt;
&lt;p&gt;테스트하다 보면 반복적인 테스트를 진행하는 경우가 많다고 느끼실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;이걸 내가 매번 확인해야 해? 지겹네? 리소스 낭비 아니야??&lt;/code&gt; 라는 생각이 든다면!! 자동화 테스트를 도입해야 해야 할 때라고 생각합니다.
이러한 반복적인 테스트를 자동화하여 효율적으로 테스트를 진행하고자 하였습니다.&lt;/p&gt;
&lt;p&gt;또한 항시 서비스 모니터링을 진행하고 있을 수 없으므로 서비스에 문제가 발생했을 경우 인지 자체를 못 하는 경우가 있습니다. 이러한 문제를 해결하기 위해 자동화 테스트를 도입하게 되었습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;올리브영은-자동화-테스트를-어떻게-활용하고-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%80-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;올리브영은 자동화 테스트를 어떻게 활용하고 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영은 자동화 테스트를 어떻게 활용하고 있나요?&lt;/h2&gt;
&lt;p&gt;올리브영에서 자동화 테스트를 활용하는 용도는 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;STG 통합테스트, 정기 배포, 긴급 배포, 분리 배포 후 테스트&lt;/li&gt;
&lt;li&gt;상시 운영 모니터링&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;크게 2가지 영역으로 활용되고 있고 다양한 자동화 테스트가 존재합니다.&lt;/p&gt;
&lt;p&gt;대표적으로 3가지만 말씀드리면 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CSP 자동화 테스트&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;CSP를 기반으로 사용자 플로우 위주로 자동화 테스트를 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;API 자동화 테스트&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;API를 기반으로 자동화 플로우 테스트를 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;UI 자동화 테스트&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;각 영역별 상세한 UI에 대한 자동화 테스트를 진행
오늘은 STG 통합테스트, 정기 배포, 긴급 배포, 분리 배포 후 테스트에 &lt;strong&gt;CSP(Critical Serving Path)를 기반으로 자동화 테스트&lt;/strong&gt;를 도입한 것에 관한 이야기 하려고 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;csp-자동화-테스트는-무엇이며-어떤-영역을-자동화-테스트하고-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#csp-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EB%A9%B0-%EC%96%B4%EB%96%A4-%EC%98%81%EC%97%AD%EC%9D%84-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;csp 자동화 테스트는 무엇이며 어떤 영역을 자동화 테스트하고 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSP 자동화 테스트는 무엇이며 어떤 영역을 자동화 테스트하고 있나요?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-01-23/incident/&quot;&gt;올리브영은 인시던트를 어떻게 관리하고 있는가?&lt;/a&gt; 블로그 글을 읽어보신 분들은 아시겠지만, 올리브영은 &lt;strong&gt;CSP&lt;/strong&gt;를 정의하고 있습니다.&lt;/p&gt;
&lt;p&gt;정의된 CSP에 대해서 사용자 플로우 위주로 자동화 테스트를 진행하도록 **사용자 플로우 위주로 TC(Test Case)**을 구성하였습니다.&lt;/p&gt;
&lt;p&gt;CSP 플로우를 흘러가는 동안 꼭 확인해야 할 항목들만 확인하고 있습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 여기서 왜 CSP 플로우에 포함된 페이지의 모든 기능, 항목들에 대해서 전부 확인을 하지 않나요? 라는 의문이 들 수 있습니다.&lt;/p&gt;
&lt;p&gt;모든 항목을 자동화하면 좋지만, 자동화 테스트를 구축하는 것도 어쨌든 인력이 소모되게 됩니다. 그렇기 때문에 UI 변경이 잦은 영역까지 모두 자동화하기에는 오히려 유지보수 측면에서 비효율적이라고 판단하였습니다.&lt;/p&gt;
&lt;p&gt;수동 테스트를 하는 것보다 UI 변경이 잦음으로 인해서 인력이 더 소모되게 된다면 그것은 성공적인 자동화 테스트 도입이라고 보기 어렵습니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 자동화 테스트를 성공적으로 도입하기 위해서는 &lt;em&gt;&lt;strong&gt;어떤 영역을 자동화 테스트할 것인지, 어떤 항목들을 자동화 테스트할 것인지에 대한 선별이 중요&lt;/strong&gt;&lt;/em&gt;하다고 생각합니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;어떤-환경에서-자동화-테스트를-진행하고-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%EC%A7%84%ED%96%89%ED%95%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;어떤 환경에서 자동화 테스트를 진행하고 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떤 환경에서 자동화 테스트를 진행하고 있나요?&lt;/h2&gt;
&lt;p&gt;자동화 테스트에는 여러 방법이 있지만 가장 정확하게 테스트하고, 결제 테스트까지 진행하기 위해 실기기를 이용하기로 했습니다.&lt;/p&gt;
&lt;p&gt;CI/CD 툴로는 &lt;strong&gt;Teamcity&lt;/strong&gt;을 사용하고 있으며 테스트 자동화 도구로는 &lt;strong&gt;selenium&lt;/strong&gt;과 &lt;strong&gt;appium&lt;/strong&gt;을 이용하여 자동화 테스트를 구축하였습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;csp-자동화는-언제-수행되나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#csp-%EC%9E%90%EB%8F%99%ED%99%94%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%88%98%ED%96%89%EB%90%98%EB%82%98%EC%9A%94&quot; aria-label=&quot;csp 자동화는 언제 수행되나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSP 자동화는 언제 수행되나요?&lt;/h2&gt;
&lt;p&gt;CSP 자동화는 현재 아래의 경우 수행되고 있으며 서로 다른 목적을 지닙니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. STG 통합테스트&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정기 배포 이전, 모든 코드를 STG 브랜치에 병합한 후 통합 테스트를 진행합니다. 이 과정에서 CSP 자동화 테스트를 활용하여 다음과 같은 문제를 사전에 방지합니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;코드 병합으로 인한 사이드이펙트 발생&lt;/li&gt;
&lt;li&gt;컨플릭트 수정 후 기존 기능의 비정상 동작
&lt;ul&gt;
&lt;li&gt;STG 통합 테스트는 매주 1회 실행되며, CSP 자동화를 통해 테스트 리소스를 효율적으로 절감하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. 정기 배포&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;매주 1회 이루어지는 정기 배포에서도 CSP 자동화 테스트를 동일하게 수행합니다. 이는 STG 환경과 동일한 모니터링 테스트를 통해 품질을 보증하며, 리소스 절감 효과를 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 긴급 배포, 분리 배포&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;과거에는 리소스 문제로 긴급 배포와 분리 배포 시 모니터링을 진행하지 않아 품질 검증이 어려웠습니다. CSP 자동화 테스트 도입 후 주요 기능에 대한 품질 보증이 가능해졌으며, 배포 시 발생할 수 있는 리스크를 최소화할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 매일 오전 1회&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;코드 배포가 없는 상황에서도 CSP 자동화 테스트는 매일 오전 1회 실행됩니다. 이는 운영상의 다양한 문제를 사전에 탐지하기 위함입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;탐지 대상: 인프라 문제, 백오피스 설정 오류 등&lt;/li&gt;
&lt;li&gt;도입 배경: 올리브영은 이커머스 서비스로, 매일 상품, 프로모션, 쿠폰, 콘텐츠 등 다양한 데이터가 변경됩니다. 이 과정에서 설정 오류가 발생할 경우 기대한 결과와 실제 결과 간 차이가 생길 수 있습니다. CSP 자동화 테스트는 이러한 문제를 조기에 발견하여 해결하는 데 기여하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;csp-자동화-테스트-수행-방식은-어떻게-되나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#csp-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%88%98%ED%96%89-%EB%B0%A9%EC%8B%9D%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%90%98%EB%82%98%EC%9A%94&quot; aria-label=&quot;csp 자동화 테스트 수행 방식은 어떻게 되나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSP 자동화 테스트 수행 방식은 어떻게 되나요?&lt;/h2&gt;
&lt;p&gt;시점별로 &lt;strong&gt;수행 방식&lt;/strong&gt;도 약간씩 상이합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;STG 통합테스트&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;수행 방식: TeamCity에서 수동으로 버튼을 눌러 실행&lt;/li&gt;
&lt;li&gt;특이 사항: 배포 시간과 순서가 일정하지 않아 자동화 실행은 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;정기 배포, 긴급 배포, 분리 배포&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;수행 방식: 운영 배포 시 Blue-Green Deployment를 활용&lt;/li&gt;
&lt;li&gt;특이 사항:
&lt;ul&gt;
&lt;li&gt;배포 완료 후 유저 트래픽을 Blue or Green Zone으로 전환&lt;/li&gt;
&lt;li&gt;Datadog에서 Hit Rate를 모니터링하며, 특정 기준치를 초과하면 자동으로 TeamCity를 실행하여 CSP 테스트를 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;매일 오전 1회 테스트&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;수행 방식: TeamCity의 스케줄링 기능을 활용하여 자동 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c8fe510fb8a57b66452644c45bdd67d5/0eb09/deploy_finish.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 84.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAABYlAAAWJQFJUiTwAAACCElEQVR42u2UW2sTQRiG9yfYNklt2nR3MzObZN00xzaxaVKhaEEoxhNV26ZnUUG88N7cemWT1r+mQsCqIFgQMTSxcbuH19ldoYTUpD/AgYd3vpn5Xma+3RnhdfUAmytPML94E7PXbkDLXIU2XQCLZ0HUJEgswTXlKYsNRHj6+CXuzC8ikp1DdKYEmi2C5RdAuJJ0ASSZB0nNgiRyXhKN9kUIBEPQxidBJBEyx9Gwo7LUCyH9oQwCjWjIhBmoO0DPILQ3vohhmKlIy5Qbhgcn/Dfsa6jwwjPGLkR/Q0VFSiK4HAjA5/fD/w98vjMkSQKl9HxDmf+MOSWG6qsq6gdv8Wavjr3afg+1Wh21+j6fryGXL0B0yqQ4tyPahSCGFZTiSXz59BlOMy0LNlcb3c20vRGLz5dv38N4SAaLXOm9eiKJoKgl8OHdezfBMA2YpulheZzyfrOtw+Z9Xddxq3x3sGGj0fB20vnNE21YhgnbMDimS/NYR7N1CmuQ4aREUVSncPjx0DkvjFYbneMWrJMO9J9NF7v9C52j7zj59gOtr0dYWipjQiRQohqvo9qFoPKXJJWZxvbOLp49f4GNrV1U1jdR2djC8qM1zipWePxwtYK1yjoe8DjKNxDihqLMehCmrt9HprSAuUIJqfQMXxx3v96IbwxDw6MYHhl1dYjrpb86FhQRnJDO5Q//J2qTZuX2/gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c8fe510fb8a57b66452644c45bdd67d5/263a4/deploy_finish.webp 480w,
/static/c8fe510fb8a57b66452644c45bdd67d5/601b1/deploy_finish.webp 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c8fe510fb8a57b66452644c45bdd67d5/9aebd/deploy_finish.png 480w,
/static/c8fe510fb8a57b66452644c45bdd67d5/0eb09/deploy_finish.png 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c8fe510fb8a57b66452644c45bdd67d5/0eb09/deploy_finish.png&quot; alt=&quot;deploy finish&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(정기배포 완료, CSP 자동화 테스트 시작)&lt;/figcaption&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;csp-자동화-로직에-대해서-자세히-알고-싶어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#csp-%EC%9E%90%EB%8F%99%ED%99%94-%EB%A1%9C%EC%A7%81%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%9E%90%EC%84%B8%ED%9E%88-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94&quot; aria-label=&quot;csp 자동화 로직에 대해서 자세히 알고 싶어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSP 자동화 로직에 대해서 자세히 알고 싶어요.&lt;/h2&gt;
&lt;p&gt;예를 들어, 고객이 임의의 상품을 구매한다고 가정해보겠습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/01b58eec26a99a0f45e367cc77ae40ae/ff716/case.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.458333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA8ElEQVR42mWQyW7DMAxE/f+flVNvRQ/NIUWL1omdzXYWydqlV8po0QIRMCAJzpDUNDkX/qMUsPPINHTcLnumsWJLCIb6vPcopZY8RY9Ve3Jyi67qm1KyJD+QnJLoJ0s/znydDN2gac8GH4KIrPCcwAvPYpzl41jjX68J4U22vhDjyDw/EXyPtnDTjveDYXdS7MaINhNarbD2WYZvhbtC6SOvW8991hjROremyXkgpV6mG0JsKfnO4RrlQsOmU7QnLYPlCuuEdxEo0nLlVZbMrFstsdY3ga5frr49olrweXac755BRWKqdjxyrE+Ld7+9bwPUg0EBNAbyAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/01b58eec26a99a0f45e367cc77ae40ae/263a4/case.webp 480w,
/static/01b58eec26a99a0f45e367cc77ae40ae/a6361/case.webp 960w,
/static/01b58eec26a99a0f45e367cc77ae40ae/0b34d/case.webp 1920w,
/static/01b58eec26a99a0f45e367cc77ae40ae/5dad4/case.webp 2578w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/01b58eec26a99a0f45e367cc77ae40ae/9aebd/case.png 480w,
/static/01b58eec26a99a0f45e367cc77ae40ae/a91f8/case.png 960w,
/static/01b58eec26a99a0f45e367cc77ae40ae/ac7a9/case.png 1920w,
/static/01b58eec26a99a0f45e367cc77ae40ae/ff716/case.png 2578w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/01b58eec26a99a0f45e367cc77ae40ae/ac7a9/case.png&quot; alt=&quot;case&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(일반 고객이 주문할 수 있는 경우의 수)&lt;/figcaption&gt;
&lt;p&gt;이 간단한 플로우에서도 조합에 따라 다양한 케이스들을 확인해야 합니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;그러면 일단 &lt;code class=&quot;language-text&quot;&gt;상품 유형&lt;/code&gt;에 대해서 생각해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;직매입, 위수탁 두 가지 케이스&lt;/code&gt;로 나눠서 확인해야 한다고 해보겠습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 여기서 직매입, 위수탁 상품을 어떻게 선정할 것인가요? &lt;strong&gt;랜덤&lt;/strong&gt;으로 선정할 것인가요? 아니면 &lt;strong&gt;미리 정해진 상품&lt;/strong&gt;을 선정할 것인가요?&lt;/p&gt;
&lt;p&gt;처음에는 미리 정해진 상품을 선정해 놓고 테스트할지 생각했습니다. 하지만 수시로 변경되는 상품 상태, 상품 프로모션 등 대한 부분은 어떻게 체크할 것인가? 라는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;많은 분이 자동화 테스트를 도입하실 때 UI 위주로 버튼이 눌리는지, 상품 가격이 뜨는지 등은 확인하시지만 그 상품 가격이 맞는지, 그 상품이 품절 상태가 맞는지는 고려하지 않는 경우가 많습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;정확한 테스트가 되기 위해서는 &lt;strong&gt;데이터의 정합성&lt;/strong&gt;도 체크가 되어야 한다고 생각합니다.&lt;/p&gt;
&lt;p&gt;그래서 &lt;strong&gt;존재하는 API를 이용하여 상품을 선정하고 상품 정보를 받아오는 방식&lt;/strong&gt;을 선택하였습니다.&lt;/p&gt;
&lt;p&gt;백오피스 API를 통해 직매입 상품 코드를 확인하고 그 상품 코드를 통해서 재고 API, 상품 상태, 적용된 상품 프로모션 등을 확인하여 상품 상태, 가격, 할인 금액, 옵션별 금액 등의 정보를 저장합니다.&lt;/p&gt;
&lt;p&gt;만약 재고가 존재하는 케이스를 테스트를 위한 상품을 선정 중이라면 백오피스에서 찾은 상품이 재고가 없다면 다음 상품을 가지고 다시 테스트 케이스에 적합한 상품인지를 확인하고 있습니다.&lt;/p&gt;
&lt;p&gt;이 저장된 정보를 가지고 상품에 진입하여 상품 가격의 원가, 프로모션가 등이 맞는지 확인하고 결제로 넘어갔을 때 배송비 정책 등이 맞는지 확인 후 결제 완료 시에도 결제 금액이 맞는지 확인하도록 구성했습니다.&lt;/p&gt;
&lt;p&gt;올리브영은 아직 rest api로 되어있지 않은 부분들이 있어 이런 부분들에 대한 정보를 가져오도록 하는 부분이 어려웠고 정보를 가져오는 부분에서 넘기는 파라미터마다 어떤 의미를 가지고 있는지에 대한 부분의 문서가 없는 상태여서 추측하여 진행하는 부분이 있었습니다.&lt;/p&gt;
&lt;p&gt;그래서 하나하나 테스트를 진행하면서 정보를 확인하여 구축을 진행하였는데 rest api로 되어있는 시스템이라면 한결 구축하는데 수월할 것으로 생각됩니다.&lt;/p&gt;
&lt;p&gt;그리고 아직 api로 정보를 가져오지 않고 db에서 정보를 조회하여 보여주는 부분들이 있어 그 부분들에 대한 처리를 어떻게 할 것인지에 대한 고민이 있습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;이렇게 구성한 이유에 대해서 &lt;strong&gt;쉬운 예시&lt;/strong&gt;를 드려보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;품절 테스트&lt;/strong&gt;를 하기 위해서 재고가 없는 상품을 선정했다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p&gt;해당 상품에 접근했는데 주문이 된다면? 이는 &lt;strong&gt;과주문을 발생시킬 수 있는 문제&lt;/strong&gt;이기 때문에 이러한 케이스를 찾아내는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;만약 상품의 재고를 확인하지 않고 UI만 확인했다면 이 상품이 품절 상태로 떠야 하는지, 구매 가능한 상태여야하는지 판단하기 어려웠을 것입니다.&lt;/p&gt;
&lt;p&gt;자동화 테스트에 관심이 많으신 분들이 UI 쪽 기능에만 포커스를 맞추기보다는 &lt;strong&gt;API를 통한 정보들과 연동을 통해서 더 정확한 테스트를 할 수 있는 방식을 고민&lt;/strong&gt;하시면 좀 더 정확한 테스트를 진행하실 수 있다고 생각합니다.&lt;/p&gt;
&lt;p&gt;그렇다면 여기서 왜 직접 설정하여 상품을 등록하거나, 프로모션 등록 등을 하여 로직을 구성하지 않았는지에 대해 의문이 드는 분들이 계실 것입니다.
이 부분은 회사마다 다른데 올리브영의 경우는 정책상 STG나 운영에는 테스트 상품이나 테스트 프로모션 등 등록하는 것을 지양하고 있습니다. 그렇기 때문에 API를 통해 정보를 가져와서 테스트를 진행하고 있습니다.&lt;/p&gt;
&lt;p&gt;만약 등록이 가능한 서비스라고 한다면 백오피스에 입력한 정보들과 고객이 접근해서 보는 정보가 일치하는지를 확인하면 좋을 것 같습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;결과-리포트는-어떻게-하고있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC-%EB%A6%AC%ED%8F%AC%ED%8A%B8%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EA%B3%A0%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;결과 리포트는 어떻게 하고있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과 리포트는 어떻게 하고있나요?&lt;/h2&gt;
&lt;p&gt;모든 케이스는 testrail에 등록되어 있고 결과를 &lt;strong&gt;testrail에 기입&lt;/strong&gt;하게 되어있습니다.&lt;/p&gt;
&lt;p&gt;그리고 테스트 결과에 대한 리포트는 &lt;strong&gt;slack으로 전송&lt;/strong&gt;되게 되어있습니다.&lt;/p&gt;
&lt;p&gt;테스트 결과에 대한 리포트는 아래와 같이 전송되게 됩니다.&lt;/p&gt;
&lt;p&gt;결과에 대한 수치는 &lt;strong&gt;testrail에서 제공하는 API&lt;/strong&gt;를 활용하고 있습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ac997d93531eab4045f029f1578ffd7f/0eb09/csp_auto_test_result.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.49999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA20lEQVR42p2Qxw6CQBCGuVpQTyouSAug0hYpQTT22OIDmfhO3ny+32U9eVATDl9mMsl8U4T7/YZZscJ2f2Acsd7seH66XDGbLxHQBLbjwvUpDNMBUXTIQ+MrwvP5QJIWWK23OBzP8III9tiDG1BY9gSaYUPVLR7LhlL4C4EoBsIoRpzmoNMUOtuiJymc/kDFQNY++CuUiMqECZIsh2mNWfFXk/YHJiynhjRGlhfwwyk/T1HNSvAfSkTjJ+fFgv+vKbZRb4iV6PbJe8PRxEcUZ3BGHsRWB7V6s7LwBR4awaiZ3KMkAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ac997d93531eab4045f029f1578ffd7f/263a4/csp_auto_test_result.webp 480w,
/static/ac997d93531eab4045f029f1578ffd7f/601b1/csp_auto_test_result.webp 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ac997d93531eab4045f029f1578ffd7f/9aebd/csp_auto_test_result.png 480w,
/static/ac997d93531eab4045f029f1578ffd7f/0eb09/csp_auto_test_result.png 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ac997d93531eab4045f029f1578ffd7f/0eb09/csp_auto_test_result.png&quot; alt=&quot;csp auto test result&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(slack 테스트 결과 알림)&lt;/figcaption&gt;
&lt;p&gt;testrail에 결과를 기입하다 보면 FAIL로 발생하는데 왜 FAIL였지? 하는 의문이 들곤 했습니다.&lt;/p&gt;
&lt;p&gt;테스트 당시에 일시적으로 문제가 발생하는 경우도 있고 element를 못 찾는 경우도 있는데 element가 없어서 못 찾은 건지, 진짜 없었는지에 대한 판단이 어려웠습니다.&lt;/p&gt;
&lt;p&gt;그래서 테스트하는 동안 &lt;strong&gt;핸드폰 녹화 기능&lt;/strong&gt;을 이용하여 당시 상황을 녹화하여 PC에 저장해주는 방식을 &lt;strong&gt;동시에 활용&lt;/strong&gt;하고 있습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;빠른-결과리포트-확인이-중요한-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%A0%EB%A5%B8-%EA%B2%B0%EA%B3%BC%EB%A6%AC%ED%8F%AC%ED%8A%B8-%ED%99%95%EC%9D%B8%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;빠른 결과리포트 확인이 중요한 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;빠른 결과리포트 확인이 중요한 이유&lt;/h2&gt;
&lt;p&gt;결과 리포트 확인이 중요한 이유는 문제가 되는 기능들은 주요 기능으로 문제가 있을 시 &lt;strong&gt;빠른 대응&lt;/strong&gt;을 위해서입니다.&lt;/p&gt;
&lt;p&gt;올리브영은 자동화 테스트가 완료되면 결과 리포트를 즉각적으로 확인하여 문제가 있을 경우 모든 사람이 참여되어있는 채널에 공유하고 있습니다.&lt;/p&gt;
&lt;p&gt;그러면 해당 영역의 담당자분들이 내용을 확인하여 범위를 확인 후 &lt;a href=&quot;https://oliveyoung.tech/2024-01-23/incident/&quot;&gt;&lt;strong&gt;인시던트&lt;/strong&gt;&lt;/a&gt;를 선언할지 &lt;strong&gt;단순 이슈 사항&lt;/strong&gt;인지 확인을 진행하고 있습니다.&lt;/p&gt;
&lt;p&gt;인시던트일때는 빠른 대응이 중요하기 때문에 이렇게 빠른 결과 리포트 확인을 통해 &lt;strong&gt;빠른 대응&lt;/strong&gt;을 할 수 있습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;csp-자동화-테스트를-통해-검출된-이슈의-예시에는-무엇이-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#csp-%EC%9E%90%EB%8F%99%ED%99%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%ED%86%B5%ED%95%B4-%EA%B2%80%EC%B6%9C%EB%90%9C-%EC%9D%B4%EC%8A%88%EC%9D%98-%EC%98%88%EC%8B%9C%EC%97%90%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;csp 자동화 테스트를 통해 검출된 이슈의 예시에는 무엇이 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;CSP 자동화 테스트를 통해 검출된 이슈의 예시에는 무엇이 있나요?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;주문서 UI 깨짐 현상&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 300px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/14d6024ae3c2a3e18a7dbed941cbe6e2/a8a0d/order_error.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 216.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAArCAYAAAB4pah1AAAACXBIWXMAAAsTAAALEwEAmpwYAAADaklEQVR42q1W23KbQAzl/38nH9C0k7TPmcQPcepLXMeJsQEbm8tyU3W0rIPJYkjTndHAAiukI+lIDtUrz3MKw5CiKKIhq6oqOZOmKW02G7nHcvACK45jGo/HNJ/PTwfMtSlmeZ4nipbLJV1dXVEQBFoh/eflZFlGh8NBNmVZnlmGhT8vFguaTqfk+/7Zu6b7ZwphPh5CoRHz0W63o9lsRo+Pj7TdbvsVJkki2OHAZDKhKR/Gfr1eM66J9XATTwRlHx4kKEVR6KCYDa7GOi12K2wRP1nYfgkIfD8gn7HbcxoF7LLH+5g9yfOCMv6pSJaf7WEQFDvtvxyPET0v/nA+xuIO8lLfK4p5D8W4HvlZxKkWJ/wNQ5PV3n2wEA8VW/nhOf8QVpxD8i5lbZAzBBcsVNFs/nxKsS5MnUvKmqlkq5azM/X3nRaGnAqe55O72ZLruoztkRLBMOWrEixln6Zy1RheUAhl+32o/15bmTGGiqObqkyUQlGCe6Uo53fYO32MYhaiHAQ7VqY6v0NpDiYHpNPb25pQWZ9WiOROgFWi8xDUZguUEaSToTSrQpd5bjqb08vLStwMdnvGcy8Hi5aypkLX3dgVwioEBRZiKcYNUZbDFslrhdutNwzDphWXgteJYTuJYSGqBDl5iQ8HRxkJi3TJLDXeqxDMsnp9o1dOEVDXmoFGUMCVtgj3BgUlN39eSN7hQ/AefiJB6VGIduHY3NMfVI2kPgo32tpqG+9e+sLVdEZlKbv2mUFBgcvL5cs7WVzoL05f/sFV4Kg5TzNzl/QqhIsoQTQs0BY4D1XRJdae0lwIDlInkaYUdZaekcEYKibRvoD0YtguvdXqVVz/UlCaynVwsn+3UDHlr7k5YWpAGUrloGez+1CsGoJ9UeR2DI07IFQQLCyDsrTubtLlmCePcSwTg0wOSdLf9czw1NxDKeYZVGVVi74fUHofSINLDzMPrP305PClkRh+Vw0XIEVR9ooeT7SlGp5CnstIjFEXXIYrxKRHN01VEt0D0xpYKIp0gIqSJ1hw3f39vYzBd3d3NHp4ODVzG05Nuh+NRiIb7jWmvZ5cBlvUQdMvhUHIKoWQcClpoufGdyicKFZsOlN8lIiEfJ+qnMZPE/p2/Z1ub3/Rzc1PEdxfX/+gp98TdrmU76MYE5k6yV+yQjTuvZsECAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/14d6024ae3c2a3e18a7dbed941cbe6e2/77434/order_error.webp 300w&quot; sizes=&quot;(max-width: 300px) 100vw, 300px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/14d6024ae3c2a3e18a7dbed941cbe6e2/a8a0d/order_error.png 300w&quot; sizes=&quot;(max-width: 300px) 100vw, 300px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/14d6024ae3c2a3e18a7dbed941cbe6e2/a8a0d/order_error.png&quot; alt=&quot;order error&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(주문서 UI 깨짐 현상 발생)&lt;/figcaption&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;부분 취소 오류&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 300px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2813d05bb3072c369ca25233cabf7ec8/a8a0d/order_cancel_error.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 216.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAArCAYAAAB4pah1AAAACXBIWXMAAAsTAAALEwEAmpwYAAADfUlEQVR42u2Va08TQRSG9z+Q2pZCLQJSQNQIalAxIoaEbrfYFk1bggYol96MtqFQioYvICQECCTcKT+NLxoSLiFBiIQKr+dM2YYKaExrwgc3eXZnzpx5993Z2T3SyckJmGQyib29PRweHoIPNX4Zx8fHODg4wP63b0geHYk+xyU+8bG/v4/19XXs7u6mBX93bG1t4cvXr9jc2EDy+3f8IEE+pM3NTTCcsLOzI65q7E9sb2+fmyMtLS1BZXFxUXA29rdI2Uy+UFB1lStyKphThyw2OzubG8GFhQUsLy9jamrqv+CVFeRANszPz4ttkxZcXV3F2toakUAikSLVT6HGEonVS+E8ITg3N4doNIqurm54vV3o6elFd3cPOju9FOtKx3p7exEIBC7E5/NjYGAAExMTkMbHx1FSUorKyluorX2EmzfLYDaX4/HjJ3jw4CGqq++jrMyM4uJSaDRaXLumy0Cr1SMvT0PzzBgeHoY0OjoqBOx2OznrhtPZApfLRQ474fG0oq3tDVpaXqGxsREFBUaYTDdw/XpRGpOpGIVGEyoqKjE0NARpZGREuKqquo36+udoaHhB7urw7Fk96uqeoqbmPsXrhVu93oDCQqMQTmMwQK/TwlxWnhJkh2xXp8tHfn6BmMSobYOhUIxx20hOMiDxotI7MJbcJYdVJBhnwc/iMXh9eCKviVarXvUZ/cz1o75Gg/zie9CZqlFSeupwZmYG7e3ttIYOWj/n3+Ow42WzItZ8bGwMEm/M7Db3ooC/lMnJyX/0peQCdjg9PZ3bmsJur3bV+y94VQWz/Up+ReIdvrKyInZ6NrAG1ybJ7/dTzeih2hBEMBhCKJQiGAyKesHjZ+sH93lMzVNzuebwH1/i306rx4P44AA+vH9HBccniEQiiMViGBwcpAIUE20uZnzlMS5MLM709/fD7XaLMiF5vSTY9haRoc8IRD7BHwiJpHA4IipZPB6nm7FwjH6gH8UNwuEw5fiEY3YXjfaRKRdkWYZktVphUZrRpDhgsTmhKDbYFAUc5wTZ0kRxRwpuWyyw2uxQHC7KVaBwHs2X7S4xlwQp6HDTgEVgtcqw2WwimUWtchPk152EV4xZZQtkpwey23c6TnNeOiG3haA0O04FlfOI5MvgnNObpmFDdBWPfHaA3fFjdXR0iBfQ19cnXgbDbY7xi+QcW4Zoqv0TQs6p20bOKREAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2813d05bb3072c369ca25233cabf7ec8/77434/order_cancel_error.webp 300w&quot; sizes=&quot;(max-width: 300px) 100vw, 300px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2813d05bb3072c369ca25233cabf7ec8/a8a0d/order_cancel_error.png 300w&quot; sizes=&quot;(max-width: 300px) 100vw, 300px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2813d05bb3072c369ca25233cabf7ec8/a8a0d/order_cancel_error.png&quot; alt=&quot;order cancel error&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(부분 취소 오류)&lt;/figcaption&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;A쿠폰을 받았는데 B 쿠폰이 받아짐&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6911a3f4cb69114d74117d5980698c94/0eb09/coupon_error.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.45833333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB/UlEQVR42oWSTWsTQRiAl0YPFfUgHkRK2uzsR3Y3G3fTzSa7SWyIJNjENqXgZ6lHwVMR9Q94sEh7tIL04KF4ET+KtfYuSAWxWkOPRQTBi0c9P05SBPXQHB5m3pl5H+addxRVCDJZHy8X4rgepuXiB0W80QK66ZDxAvx8hCfX/Hws9yIsN4dqZhBdDKc3amkXQ6IIYWB6eby4hh/XsUcrmH6MLkllCmjZSMYlDC/CyJXkPOrtCTcvCSQhIruHap2SQi1N2vcJW+Pkz7XJT0xiFUpYYYxTHcMMAjTHRrdsVJFGlee7OULvYv3FXqyohkZWnKVx4iHt4fs0jj+gll0mNpcpH33E+NASM/oCbqpJSp4Vuv2f6F+UlKniD19g6sAOlw51aCodzt/6zotPP3mz0KGzuMbTyWfYx67IklJourO/UDVTuEOXaSg7NBPvqSmbzJ3e5cfbJ2xVHH7NN3h+p8PI4AyaPSKT+giTQqca1Lkdr9BKbFGXwhuVXb4tzbI9VYbH46zf3UQ7MtsT9r3hyaTD1bbOyvU5WoM7tAfecc3+zOriF1bvfWVjfpuPNz8w5V1kSKjoRh9h96EDfYLm4XWqyivOJNaoDbykqKxTkHGovKZ5cAMvOS2bovYveUQ1KUdFqmNVwjDCzeTkJ81hO34PR6LpWflV9u/uH+FvLX9WTpt2QfEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6911a3f4cb69114d74117d5980698c94/263a4/coupon_error.webp 480w,
/static/6911a3f4cb69114d74117d5980698c94/601b1/coupon_error.webp 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6911a3f4cb69114d74117d5980698c94/9aebd/coupon_error.png 480w,
/static/6911a3f4cb69114d74117d5980698c94/0eb09/coupon_error.png 500w&quot; sizes=&quot;(max-width: 500px) 100vw, 500px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6911a3f4cb69114d74117d5980698c94/0eb09/coupon_error.png&quot; alt=&quot;coupon error&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;(쿠폰다운로드 오류)&lt;/figcaption&gt;
&lt;p&gt;이런 영역들은 고객 주문에 가장 밀접한 부분이지만 기존에는 이러한 이슈가 발행하여도 voc가 들어오거나 배포 후 한참 지난 후에 알게되는 경우가 있었는데 그런 부분을 많이 줄 일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;또한 csp 영역 모니터링을 자동화로 대체하여 기존에 모니터링 하던 리소스를 제외하여 평균 2MD의 리소스 감소를 가져올 수 있었습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;마치며&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;저도 자동화를 도입하면서 여러가지 시행착오를 거치게 되었고 계속해서 어떻게 해야 &lt;strong&gt;더 오류 검출을 잘하고, 유지보수는 덜하고 쉽게&lt;/strong&gt; 할 수 있을까? 라는 고민을 하고 있습니다.&lt;/p&gt;
&lt;p&gt;자동화 테스트를 도입하는 것에는 &lt;strong&gt;정답이 없는&lt;/strong&gt; 것 같습니다. 서비스마다의 특징이 있고, 중요한 부분이 다르기 때문입니다.&lt;/p&gt;
&lt;p&gt;기술에 대한 부분을 많이들 걱정하시는데 기술이 좋은 것도 중요하지만 &lt;strong&gt;그 기술을 어떻게 활용하느냐&lt;/strong&gt;가 더 중요한 것 같습니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 자동화 테스트를 도입할 때는 &lt;strong&gt;어떤 효과를 얻고 싶은지, 어떤 부분을 자동화 테스트 도입했을 때 효율적인지, 어느 부분에서 문제가 많이 발생하는지&lt;/strong&gt; 등에 대해서 먼저 고민을 해보는 것이 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;효과에 대한 고민을 하여 커리어 성장에도 도움이 되고 팀에도 도움이 되는 자동화 테스트를 도입할 수 있길 바래봅니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;올리브영 UI 테스트 자동화 코드 구조가 궁금하시면 &lt;a href=&quot;https://oliveyoung.tech/2023-11-11/qa/&quot;&gt;UI 테스트 자동화 구조&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div style=&quot;text-align: center; margin-top: 30px; padding: 20px; border: 2px solid #f0f0f0; border-radius: 10px; background-color: #fafafa;&quot;&gt;
&lt;p style=&quot;font-size: 1.2em; font-weight: bold; margin-bottom: 15px;&quot;&gt;올리브영 QA Engineer가 되어 자동화 프로세스를 직접 구축 및 개선해보고 싶다면 🤩&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250807033067&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;코어플랫폼 QA 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033063&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;SCM QA 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://recruit.cj.net/recruit/ko/recruit/recruit/bestDetail.fo?direct=N&amp;zz_jo_num=J20250804033551&quot; target=&quot;_blank&quot; style=&quot;display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; font-size: 1em;&quot;&gt;Back Office QA 채용공고 보기&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded></item><item><title><![CDATA[올리브영 결제수단 연동, 이렇게만 하면 끝!]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2024-12-30/oliveyoung-order-pay-method/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-30/oliveyoung-order-pay-method/</guid><pubDate>Mon, 30 Dec 2024 11:00:00 GMT</pubDate><content:encoded>&lt;h4 id=&quot;안녕하세요-올리브영-주문-결제-스쿼드에서-백엔드-개발을-담당하고-있는-그거아세용-입니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%A3%BC%EB%AC%B8-%EA%B2%B0%EC%A0%9C-%EC%8A%A4%EC%BF%BC%EB%93%9C%EC%97%90%EC%84%9C-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9D%84-%EB%8B%B4%EB%8B%B9%ED%95%98%EA%B3%A0-%EC%9E%88%EB%8A%94-%EA%B7%B8%EA%B1%B0%EC%95%84%EC%84%B8%EC%9A%A9-%EC%9E%85%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;안녕하세요 올리브영 주문 결제 스쿼드에서 백엔드 개발을 담당하고 있는 그거아세용 입니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;안녕하세요, 올리브영 주문 결제 스쿼드에서 백엔드 개발을 담당하고 있는 &lt;strong&gt;❓그거아세용❓&lt;/strong&gt; 입니다.&lt;/h4&gt;
&lt;p&gt;온라인 커머스의 가장 중요한 요소 중 하나는 단연 &lt;strong&gt;주문결제 시스템&lt;/strong&gt;이라고 생각합니다.
고객이 원하는 상품을 선택하고 결제까지의 과정이 원활하고 빠르게 이루어질 때, 고객들이 느끼는 만족도가 높아지기 때문입니다.&lt;/p&gt;
&lt;p&gt;올리브영에서는 이 주문결제 프로세스를 최적화하고, 편리하고 안전하게 쇼핑을 즐길 수 있도록 보다 나은 결제 환경을 구축하는 데 집중하고 있습니다.
이 과정에서 얻은 경험을 바탕으로, 안정성 있는 시스템을 통해 신뢰를 쌓아가는 데 기여하고자 하는 것이 저의 목표입니다. 😆&lt;/p&gt;
&lt;p&gt;이 글에서는 API 연동을 통해 두 가지 결제수단을 연동한 개발 과정을 &lt;strong&gt;⭐️쉽게⭐️️&lt;/strong&gt; 다뤄보겠습니다!! Let&apos;s Go!!&lt;/p&gt;
&lt;div style=&quot;max-width: 350px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 368px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/03f886e8498c9c0bc65cfe2f826e287c/55493/gogo.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 96.46739130434783%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAAEsUlEQVR42j2U21NTZxTF+Qc6feybT/0HOh3Hh/bFl3Y6Tn3QOpZpO07VpzqO9VoVpQ5SFSwIiIggpki43xMgkVvIDXIhF9CQEK6COYDhZlKuUn/d50CbzHe+c863Z52919p7pcwN9TE/ZEXdlWCf7FZi6vMrJ8vjXpbHvCyOurW1PDbI2/AAbyRuIeRkMeJibtiGIvHzeytFvSj/BUhwbMjGYtSrBfmMlThqy+h9/hhHnQ5Pq54xRzuJ6SAjfQb65Ez7uIDOaUkJ4JyALY26GHN2MNhWw7tJv+xVdOge437RwYjfw+uJUVzdZjr0OtrLS+mtKMZer6P01jXmXtolmX4tAQ1wIeQgYm9jsL0anyy3oZLWshI8XWY+8J7N7QQ7bDI7EcFcU876VoKg20lLUS5T7k4aCrLwddQSl+oUreRXDnR/pGMsLWCktxl7h5ERn4vuej3JtWXWd9bYln9vSy2FN66ScfoEyusogQEbpqcFND7MxtVcIRy7tPJTVO58pjp8bZVY6p5jNbbw5FYal48cpqupBn9LFf62Bh7d/p3TB7/gm0/3UVmYLzlvSiVFRG1G4iNOrfQ54T9FTVPlLdTTKAD1PLmXSeqXB9j/8UdcP3qYlXPfMX3yECU3LnNw3ydkHvmKLVM5xKewmdu07JIzIZRAryZOSixoIRGLMmhqwNfVDi4jZalfc+3Uj1TfTmPnUirvL3xPb24mq9sbbHXr+XD/F+jS4+234hT1E9PD0imWXUBFbZOIG69ZAMvyoOgSO7X3QSRxNFYzezEVitMwZacTioTw6h6SbCmFlRmsDVW4m56zLG0WC+6prAzbiUuDhoULU3EO7x3NjNYUk0guMjUzTueFn5k4f5zmB3ewSwX2HjOhYT/JN+PUZVwhPuZndTLAgoiraBy+tPEmYNEI7SzLp6ogB5fTRnjIxz/SNuORYQa725iLTbO1vcaw10lXTgadeXfxGKqJvKgj0KQjbK6VLPukbUTlqNWAv+kvGTcvTU+LUaJBpoc8rCbibGy9E9gNNreSqL8+YwNF138jPjPJkEF4rCtlcsDEjKcLReVQzWwh7GJKXgZaKxi2mGktLSLv0lnWt/+WLD9IL26wlFxiMbHIurTLi5Z6RjwugsYabOUP8TU8Y1KGY/6lY5fDmCAHjXrM+ZmEzI1MjcpUiCDLSwqu9iZ6SvLpKfwTc04mXY9zcZsMrK0u4hVBLGUP8NY8YdplFpXtu32ogs7LPE65OrX7pYkg3foSjAXZ9GaJuqU5KMJTvL2SCckmGXYTj/qEMzuzATtvR/qlsR2a0imqbak3C6EBgpZ2pvw2itKvUl/8iJunTqDLy+Hc8WNYOwzoC3O5d/ki5TlZPLtzi5I7GfTUV7Ay6iEmwiq7gOJ/gT7WYhHK7t2m9tEDDn/+Gc/uZ5N/Mw0l7Of8saNU5ueS/etZjh/Yz5lvD5EuH7vy0w/MhgaJj3o1wF372vND1Tw95mbunjuDPjeLbnGWMa9N+HUy0FbPmKubsKMTa2Ml/YY6DDLHlqZqzYhje8b8P6CqtPpSdZ64TE1yeoiV8UF5tmtDvyQlqWdvpcWW5Fz1z3eTvl3L0krdBVT3fwE9jcpKRVEQ4AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/03f886e8498c9c0bc65cfe2f826e287c/ef070/gogo.webp 368w&quot; sizes=&quot;(max-width: 368px) 100vw, 368px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/03f886e8498c9c0bc65cfe2f826e287c/55493/gogo.png 368w&quot; sizes=&quot;(max-width: 368px) 100vw, 368px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/03f886e8498c9c0bc65cfe2f826e287c/55493/gogo.png&quot; alt=&quot;가보자고&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;h2 id=&quot;어떤-결제수단을-연동했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EA%B2%B0%EC%A0%9C%EC%88%98%EB%8B%A8%EC%9D%84-%EC%97%B0%EB%8F%99%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;어떤 결제수단을 연동했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;❓어떤 결제수단을 연동했나요❓&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;올리브영-현대카드-리워드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%ED%98%84%EB%8C%80%EC%B9%B4%EB%93%9C-%EB%A6%AC%EC%9B%8C%EB%93%9C&quot; aria-label=&quot;올리브영 현대카드 리워드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영 현대카드 리워드&lt;/h3&gt;
&lt;p&gt;올리브영 현대카드 리워드는 고객이 &lt;strong&gt;PLCC카드(올리브영x현대카드)&lt;/strong&gt; 를 발급받고 사용할 때마다 일정 금액이 리워드로 적립됩니다. 이 리워드는 고객이 올리브영에서 자유롭게 사용이 가능한 &apos;올리브영 현대카드 리워드&apos; 입니다.&lt;/p&gt;
&lt;p&gt;PLCC(Private Label Credit Card)는 카드사가 특정 제휴사와 협력하여 그 제휴사에 집중된 혜택과 서비스를 제공하는 신용카드를 의미합니다.
일반 신용카드와는 달리, 해당 카드사와 제휴된 특정 상업자에서만 제공되는 특별한 혜택을 받을 수 있는 특징이 있습니다.&lt;/p&gt;
&lt;p&gt;고객이 PLCC 카드를 사용할 때 자동으로 리워드 포인트가 적립되고, 이 적립된 리워드를 사용시 차감되는 로직을 구현했습니다. 이를 통해 고객은 리워드를 손쉽게 사용할 수 있게 되었습니다!&lt;/p&gt;
&lt;div style=&quot;max-width: 350px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 544px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2d5d334a2b9ad4aa77adca9a024db2b0/cfd82/card.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100.20833333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAE/0lEQVR42l2U2VdTVxSH71/V9kHFqaAiDigOTFYcUBBWlQ4SCEOAJICGMYCRQSKSRJAQZkyIIYEQMpCQACFMUXFVfehb378eY11d7cO39r7n7vu7v73vuUfaim/wlSj/5hv4V5eZsU3i8jhwLNiwO61Y7TPML8wRjgaJ7a3/p/6bhhSLr4uba8TiX1hnczfCpz/38a64GTD2MzZlZmR8KMHLESOWyRFRs8b7j7uJ+H9RaVuo7sa3BTts78XY24/x3NCHrqcT06sXvDDpeWHUi7VnDI0aE3TqtIxPm3n3x1fRhNuE43WktV0/rq1X2GODeLds7H98i6qhluraCnr1OjqetNHVraWlXUNT2yN6+nW0dmroH+zl/ecdNnZCbO6FBZFEp1Jf5B7tq9l0hHPoDOdi2Whi+12Uaes4rywmhi1GjMLpjLgeGjXwTN+DY95OfH9XuNpkOx5jJ74l8ihRISxpQ9l0rv7Ek3BeIjYFL/B6qwfdEx2KmiqUqlq6nmqZtk/x0vwSeWMprWY5E7EmjOFKhteUzEZ7CW0vJsYmaVdz0AqHiRjKoTV0la7VG0T2PWx/WmPzU5DNdxHi0TCeNQMj7kJ6lq7wOHCGR4HTNApU/mQ0/nQm1rVI7eGsfwSzaRO0Bi+LEXxZk6GeLKVUX0SzW86erp31Zhnhnga86iLaFzJoXr2IZiUdTeAc9b4T1C4fRmpZyaQtmE17ULgTebP/Em2hXOGghN+N+dzvuUmlIw/vQDVvFXLCOiUhtYxW1yUeB9Np9J+j0ZdG/XIqKk8KUrntOPK5bxwTHEXhPE2xIZMiUzYlY9cpteSw0HyPjZpi1mseEKooQTGejMyeRJn1MGW2JGS2Q5RaDyE1iN4bfeeF5bM0CB4l3pjBtc6z3B/OpXAgk+KhDJwdd4lUFbJZJSNSKUMzd5r6wCnUyymovSfEs6k0eNOQmnxXxXAvCTISaPxXaV65RtFAFrd7rpDTcp6ivgs4WvPYeFjMjryKFcUvaBYzhIkvIqk0CRNq3ynq/KJl81QBdUHhzHdRzCOd+pXTKEVRgSmNLG0KF9tSuP48GXPHFd4o8/DJH2CT30LpOIk68EXwFMrACVqWzjE2kY80NVqESrSs8p1B48vG7NZhDQ9iWdDT51RgNf2Gq7uMWe19DI+u0WG9TF/0pNhiqdRPJlPjSEY3n8HkaAHtjnNIlrF8NEsXaPCk0b1cgOuNG687QNi7xfjiAO8fV/OXspP9Dj1z+Xnop7MY+nSciQ+lNE3/SN9gljhAblPnPkaVJwlpwnKXWfM9RsbymJ4rxef3Yp2bJboW43XQQPTxQz4r1Oz+KmOqOJfe4RzM9jv0TmWJ/zwd44sbKJaPoXAfoWbxCFLjgvi67rNUeA4Jp9k4nU5crnn8gRAe9yh76nI+lCvYeyBjrPAqxpES7H2NjHVW0F97m5qZ49QsH6VuUUQhKpUvHES+kESTJxPrigmr1cqsdRqTYZjeiXqCqp+J/yYnWlTCZHE2esMdzC0i769ktFtG7eRxFEtHqHYJhI5k9KoYD+jwhp2EI0G8S34cc/NMW+yYbB0EKovYvvuQzZsPGLuVSbf+JgZlAbN9KszaMrHBj1LhPkiV8xCVrgNIq5EAwYg3gW1liBYxn1ZbIe22e9Rbr9DyPEWcNmlou0+h7UrFPHCHgafXaTFfom4wBbn1AHLnD5Q7vhd8x98Tc4Akasju7wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2d5d334a2b9ad4aa77adca9a024db2b0/263a4/card.webp 480w,
/static/2d5d334a2b9ad4aa77adca9a024db2b0/07652/card.webp 544w&quot; sizes=&quot;(max-width: 544px) 100vw, 544px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2d5d334a2b9ad4aa77adca9a024db2b0/9aebd/card.png 480w,
/static/2d5d334a2b9ad4aa77adca9a024db2b0/cfd82/card.png 544w&quot; sizes=&quot;(max-width: 544px) 100vw, 544px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2d5d334a2b9ad4aa77adca9a024db2b0/cfd82/card.png&quot; alt=&quot;카드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;div style=&quot;text-align: center;&quot;&gt;
      (예쁜 카드 디자인 구경하고 가세요~~ 💳)
   &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;h3 id=&quot;모바일-상품권&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%83%81%ED%92%88%EA%B6%8C&quot; aria-label=&quot;모바일 상품권 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모바일 상품권&lt;/h3&gt;
&lt;p&gt;모바일 상품권은 온라인과 오프라인 모든 영역에서 사용이 가능하며, 잔여 금액 연동 시스템으로 구축하여 소비자가 남은 금액을 계속해서 사용할 수 있는 유연한 결제 방식을 제공합니다.
고객은 &lt;strong&gt;지급되는 난수번호&lt;/strong&gt;를 주문서에 입력함으로써, 해당 상품권의 잔액만큼 결제 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;고객이 난수 번호를 입력하는 과정에서 직관적인 UI와 결제 흐름을 고려한 연동 작업을 진행하여, 결제 과정에서의 불편함을 최소화하고, 사용자가 쉽게 결제를 완료할 수 있도록 했습니다. 이를 통해 모바일 상품권을 더 매력적인 결제 수단으로 자리매김 할 수 있었습니다. 🎵&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;이렇게 결제수단으로 올리브영 현대카드 리워드, 모바일 상품권이 올리브영 주문서에 들어왔습니다! 짜잔~~ 💕💕💕
(올리브영 현대카드가 궁금하다면? &lt;a href =&quot;https://m.oliveyoung.co.kr/m/plcc/hc/apply.do?t_page=%EC%9D%B4%EB%B2%A4%ED%8A%B8&amp;t_click=%EB%AA%A8%EB%93%A0%ED%9A%8C%EC%9B%90_%EC%9D%B4%EB%B2%A4%ED%8A%B8%EB%B0%B0%EB%84%88&amp;eventCode=OLVB0&quot;&gt; 클릭!🖱️ &lt;/a&gt;)&lt;/p&gt;
&lt;div style=&quot;max-width: 350px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 362px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d67ce49f71e39affbfcf706aa696cf4b/7afe1/orderform.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 191.43646408839777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAmCAYAAADEO7urAAAACXBIWXMAAAsTAAALEwEAmpwYAAAE20lEQVR42q1WaXPbNhDl//8v/d6J20ni1KltxYfiS2csWbcoifcJEq9vQV2WFU8mU2jeLASCIHbfXpbvefB9H2ZovYUutVnq9/u4ubnF5cUlhoNRtU2eyeNDcN0KwhB+EOBwaF0duHIcdLpdNFstLJfLV8+2e/fmVlbrQHUmyJ6GyBoV0scBkscXYgDVGkP3FgaqM0X6NNju20A1R8i5L621Yd39cYI/Tz7gn0+f8fnjJ5x8OMHZ6Rc83T/g419/47p2hcAPUOQKuiiBA8j6Yr5AXpZI7vqw4ocecl5aKYUkSZDnOfJCodAlMq7lKud7Jd4b8q6MvD2G5Vw10Z+OMRqOMB6P0X8ZQEcR4LoA7YeieGPXIwavDqTa1uLyCS+zCYY8SA4cjkbQJEo7K+gVwa/rDfMb8FeS0YIqy6OSGshnM9rSymhkM0r95ouVi1TqLhYLuI6LIAiRJimyLMdkYtOl7nB//wTb8Q2JVkqWjAuVeutOx0aUxIjTxNhV0Qw54fghxjMbg/EUXpoilRsK1UWUoliRSSck1tINURpEBtqPUQq8yKCQdY+m8RNoj+tRgvS6S5VbExTDFdTLkligoBTkPRvp82yHH1MkRPpjdoDdWiKkZHQTm8Z3PBd+GGBFdpeugzhL8b6z7LkNXWxjLms0HOL08ykuzi/wvf4dX8++4urbNzw/PyNNMzHuqxg/RElbLpeO8d+Ccwv/87AGgwGub27w1Gig3W7TBR6MLMvy9w5MSHcYRojj2IReyCgRKQduHFmso/VriOskaU6z5EYKxPWsasPb1OW4PgJ+yCccV3JmCJd502OiUKpAminM5kucX9Qqx144Zt3a3WJ3mEAMLNKEFecCmW9unvPlkL7nB5GRAkkilmwUhlKqLqpuMsdvk2LbNnq9PhokpU63mc/nx7Py+v+hRm8O/JnKOROn2MTELW8tmqh1HJfmeWHUndtL44dBEJvsY1V20Wt77Vj0SEBE5sMohusFfDk0kGxTGFJyQ8rVdR2NZmdHyqE621uu09j29gc5UW6TiRaUQpDMZd34ocdSOp1OsWJMz2YzaK5BKqHIA1Mcq47769Zq5Zja22w20aNsdzpMSx40yZFSoH9CiJmvo6nKp5U7We8xtj/EdmLTKE6MekKavXBZr7us2z0sV96OFMNcUTltuXbofciQ7kLCU6InzTIeWhhSGs02Wu0uS4RTHbhf/bfQ+uih+3NxnVKXW76Kjcq2vcAdM4xPB5fSKbbDkUzzy6SELJnj8QQJbUSnY40IjLHf3FC/vnkVsmod56WZ/xIpm+c2GyXHdU32SZjJM/rehDa8rt/h7rGB+dJdkxKnUFLNAubAMNnJPWhWxYhGj/lS6gbImcoyVj1nssDkeYhR9wUu54r7rPRrA6o3h+pODfLu5CgKVjVlsNsnsuS7AtVj5TtvsUh9ucFN4xG39TrOazUTw+/2Me/1NlLo44c+Us1kyVCLGBmbxLqfULfY+Oo6wUZMqgEzTuCzhJAU0zlkj4PX/cw6pFbsYyS7SMpfMjxdzzelQKSShJywt5kucHZ2jvrtPeaOh5hNqiWdq173zfonbe5+z7OZK344phYpP55Js8m1tCW9zb9NFDNSPmAbMlxuofhfDQXyv3pW/d/soxxVa8XARjF1kNXa+A+OYH7Eo51MVAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d67ce49f71e39affbfcf706aa696cf4b/8ca7e/orderform.webp 362w&quot; sizes=&quot;(max-width: 362px) 100vw, 362px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d67ce49f71e39affbfcf706aa696cf4b/7afe1/orderform.png 362w&quot; sizes=&quot;(max-width: 362px) 100vw, 362px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d67ce49f71e39affbfcf706aa696cf4b/7afe1/orderform.png&quot; alt=&quot;주문서&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;h2 id=&quot;결제수단을-어떻게-연동했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EC%A0%9C%EC%88%98%EB%8B%A8%EC%9D%84-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%97%B0%EB%8F%99%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;결제수단을 어떻게 연동했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;❓결제수단을 어떻게 연동했나요❓&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;이 두 가지 결제수단은 &lt;strong&gt;API&lt;/strong&gt; 를 통해 연동하였습니다. 비록 API가 새로운 기술은 아니지만 외부 서비스와의 안정적인 연동과 향후 추가적인 서비스 통합을 고려하여 쉽게 확장이 가능한 방법으로 진행하였습니다.
위에서 말씀드렸다시피 ⭐️쉽게⭐️️ 설명드리기 위해 잠깐 API 란 무엇인지 설명해드리겠습니다.✌️&lt;/p&gt;
&lt;br/&gt;
&lt;h3 id=&quot;apiapplication-programming-interface&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#apiapplication-programming-interface&quot; aria-label=&quot;apiapplication programming interface permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;API(Application Programming Interface)&lt;/h3&gt;
&lt;p&gt;API는 애플리케이션 간의 상호작용을 가능하게 해주는 인터페이스입니다.
즉, 서로 다른 시스템이나 애플리케이션들이 데이터를 주고받거나 특정 기능을 호출할 수 있도록 정의된 규칙과 프로토콜입니다.
이를 통해 개발자들은 외부 시스템과의 복잡한 연결 작업을 추상화하고, 효율적인 데이터 처리를 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;아래의 사진을 예를 들면, 올리브영은 고객의 요청을 받아서 외부 시스템에 그 요청을 던지고, 그 응답을 받아 다시 고객에게 전달하는 형태로 API를 활용합니다.
이 과정에서 올리브영은 API 호출을 통해 외부 시스템과의 연결을 관리하며, 고객에게 필요한 서비스를 제공합니다.&lt;/p&gt;
&lt;div style=&quot;width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 701px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3e4c33b12dde2c43c13fca3e0f951979/62230/api.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 33.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABU0lEQVR42q2Ry07CUBRF+WTnfoPEgQ50ok40JogaQSOKUBQjkUe10YDyUKFQrIBY2tKWsiwP31Nvcm/O3idZ5+RuH/98fKOnKOVIbK2j97Sf3SFYrsmzdoPl6CM59SeVbhgUyxVKDw/0bfsLGNnZxD87Q7P+NDZd18UdDsZ1yUySbwa46x5Oep4/nAI1TeMoekLi7AzDg4+BWqNOLnHKxtIyspjG1I3PLTq9LlIzS1zeJSXHaXv6Y3VNN8mVFSKXt8SuSuSrLRzHwffmwVxBQAmF8UZxHY9TfnrkRpQISiFW5QCLmRXWalsECnvcF4oo9RohIc38eQd/rMpcTGZBqPHaM/G9iCKdixSqd19TKQqZLGqrhaHpZGoiocYx+9UoB2oMoZSkIStYfYNKXWVbahMUVQLZJuFrBaNvTf7wWwY/M3GHDOwBjuVg9+2x/pPar5TfAVhfCM9CWj5rAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3e4c33b12dde2c43c13fca3e0f951979/263a4/api.webp 480w,
/static/3e4c33b12dde2c43c13fca3e0f951979/123bf/api.webp 701w&quot; sizes=&quot;(max-width: 701px) 100vw, 701px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3e4c33b12dde2c43c13fca3e0f951979/9aebd/api.png 480w,
/static/3e4c33b12dde2c43c13fca3e0f951979/62230/api.png 701w&quot; sizes=&quot;(max-width: 701px) 100vw, 701px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3e4c33b12dde2c43c13fca3e0f951979/62230/api.png&quot; alt=&quot;api&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;h3 id=&quot;연동의-주요과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%B0%EB%8F%99%EC%9D%98-%EC%A3%BC%EC%9A%94%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;연동의 주요과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;연동의 주요과정&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;서비스별 API 제공받기 🙇‍♀️&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;첫 번째 단계는 올리브영 현대카드 리워드와 모바일 상품권을 제공하는 외부 시스템에서 API를 제공받는 것입니다. (계속되는 설명에서는 &apos;&lt;strong&gt;외부 시스템&lt;/strong&gt;&apos; 이라고 줄여서 설명하겠습니다.)&lt;/li&gt;
&lt;li&gt;각 결제 수단은 연동을 위한 고유의 API 문서를 가지고 있으며, 이를 통해 사용자가 결제수단을 및 정보를 처리할 수 있는 방법이 정의되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;API 인증 및 보안 처리 🔐&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;각 결제 수단의 API는 보안을 위해 &lt;strong&gt;인증 절차&lt;/strong&gt;가 필요합니다.&lt;/li&gt;
&lt;li&gt;인증방식은 &lt;code class=&quot;language-text&quot;&gt;Basic Authentication&lt;/code&gt; 을 사용하여, API 호출 시 &lt;code class=&quot;language-text&quot;&gt;Authorization&lt;/code&gt; 헤더에 인증키를 포함시켜 인증을 처리했습니다.
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;  &lt;span class=&quot;token comment&quot;&gt;// 요청 헤더 설정&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; headerMap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HashMap&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  headerMap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  headerMap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Basic &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;API&lt;/span&gt;인증키&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;인증키는 외부 API를 호출할 때 사용하는 인증 정보입니다. 서비스 제공자가 발급한 고유의 키로, API 서버는 이 키를 통해 요청자가 인증된 사용자인지 권한이 있는 사용자인지를 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;주문서 연동 🤝&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 인증 후, 각 결제 수단을 주문서에 연동합니다.&lt;/li&gt;
&lt;li&gt;고객이 주문서에 진입하여 해당 결제 수단으로 결제를 진행시, API를 호출하여 잔여 금액 조회 및 결제 차감을 처리합니다.&lt;/li&gt;
&lt;li&gt;HttpClientUtil 클래스의 jsonToJsonUtfSend 메서드를 사용하여 JSON 데이터를 API에 전송하는 방법을 사용했습니다.
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;JsonResponse&lt;/span&gt; jsonResponse &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HttpClientUtil&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;jsonToJsonUtfSend &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      apiUrl&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;                     &lt;span class=&quot;token comment&quot;&gt;// API URL&lt;/span&gt;
      jsonString&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;                 &lt;span class=&quot;token comment&quot;&gt;// 전송할 JSON 데이터&lt;/span&gt;
      headerMap&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;                  &lt;span class=&quot;token comment&quot;&gt;// HTTP 헤더&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;결제 완료 및 상태 반환 👍&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;결제가 완료되면, JsonResponse 객체에 반환된 응답 값을 담아 요청의 성공 여부와 반환된 데이터를 처리합니다.&lt;/li&gt;
&lt;li&gt;이 응답은 성공 또는 실패 메시지와 함께 상세한 처리 결과를 전달합니다. 결제 성공 시, 고객에게 결제 완료 페이지를 표시하고, 실패 시에는 오류 메시지를 출력하여 재시도할 수 있도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;
&lt;h2 id=&quot;어떤-api를-연동했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-api%EB%A5%BC-%EC%97%B0%EB%8F%99%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;어떤 api를 연동했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;❓어떤 API를 연동했나요❓&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;사용 가능 금액 조회&lt;/strong&gt; : 고객이 현재 사용가능 금액을 확인할 수 있는 기능입니다. 고객의 정보를 기반으로 사용 가능 금액을 조회하는 방식으로 구현합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;금액 사용&lt;/strong&gt; : 고객이 결제 시 보유 금액을 사용할 수 있는 기능을 구현합니다. 결제 요청 시 금액을 차감하고, 결제 금액과 차액을 처리하는 로직을 구성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;금액 취소&lt;/strong&gt; : 결제가 취소되거나 변경될 경우, 이미 사용된 금액을 되돌리는 기능을 구현합니다. 만약, 금액 사용 및 주문에 오류가 발생할 경우 &lt;strong&gt;결제 금액 사용이 취소&lt;/strong&gt; 되어야 합니다. 이때, 금액 취소 요청을 API로 전송하고 적절한 처리가 이루어지도록 합니다. 롤백 과정에서 취소 API 호출을 놓치지 않도록 처리하는 것이 중요합니다!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;소멸 예정 금액 조회&lt;/strong&gt; : 고객의 보유 잔액 중 소멸 예정인 금액을 확인할 수 있도록 서비스를 제공합니다. 소멸될 금액을 미리 파악하고 이를 사용하도록 유도할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;h2 id=&quot;어떤-역할을-하고-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EC%97%AD%ED%95%A0%EC%9D%84-%ED%95%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;어떤 역할을 하고 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;❓어떤 역할을 하고 있나요❓&lt;/h2&gt;
&lt;p&gt;위와 같이 올리브영 서버는 고객의 다양한 요청이 발생하면 해당하는 API를 호출하여 고객의 요청을 진행합니다.
해당 요청이 완료되면 각 결과를 바탕으로 고객에게 알맞는 화면을 제공합니다. 결제는 고객의 쇼핑 여정에서 중요한 부분이기 때문에, 이 과정이 얼마나 원활하고 직관적인지에 따라 전체적인 만족도가 달라지게 됩니다.
이렇게, API는 고객의 결제 흐름을 원활하게 만들고, 결제 시스템을 안정적으로 운영하는 데 중요한 역할을 합니다.&lt;/p&gt;
&lt;p&gt;올리브영 현대카드 리워드와 모바일 상품권 결제 수단 연동을 통해 &lt;strong&gt;올리브영에서만 사용 가능한 리워드💸&lt;/strong&gt; 설계로 리워드 중심의 추가 매출을 확보와 재방문 유도를 위한 구조를 확립하였습니다.
특히, 올리브영 현대카드 리워드는 PLCC 카드 사용을 통해 적용되는 리워드가 온/오프라인 제한없이 사용이 가능하다는 점! 완전 럭키비키잖아~~~ 🍀🍀🍀&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;어떤-점을-느낄-수-있었나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EC%A0%90%EC%9D%84-%EB%8A%90%EB%82%84-%EC%88%98-%EC%9E%88%EC%97%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;어떤 점을 느낄 수 있었나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;❓어떤 점을 느낄 수 있었나요❓&lt;/h2&gt;
&lt;p&gt;매 프로젝트를 진행하면서 결코 혼자의 힘으로 완성될 수 없다는 사실을 다시 한 번 깨달았습니다. 특히 이번 프로젝트에서는 협업의 중요성을 깊이 실감할 수 있었던 시간들이었습니다.
시스템 연동 과정에서, 외부 시스템을 담당하는 다른 팀과의 끊임없는 협의를 통해 요구사항을 명확히 하고, 문제를 해결해 나갔습니다.
제공받은 API 명세와 실제 동작 간의 차이를 확인하고, 이를 해결하기 위해 명확한 질문과 논의를 이어나갔던 순간들이 협업의 중요성을 실감하게 했습니다.
그리고 이 어려운 순간들을 헤쳐 나간 팀원분들 덕분에 프로젝트를 성공적으로 마무리할 수 있었습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;또한, 우리가 구축한 시스템이 고객에게 실질적인 가치를 제공하고, 그 사용 과정에서 직접적인 도움이 되고 있다는 점에서 큰 뿌듯함을 느낍니다. 이번 프로젝트 또한 그런 의미에서 하나의 큰 성과라고 생각합니다. 🌟
이 글을 쉽게 설명하려고 노력했는데, 잘 전달이 되었을까요!? 관심을 가지고 끝까지 읽어주셔서 진심으로 감사드립니다.
다시 한번, 고생 많으셨고 정말 감사합니다. 😊&lt;/p&gt;
&lt;div style=&quot;max-width: 350px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
   &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 374px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b21869929b1ec829b9755950416096d3/b0af8/bye.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 136.8983957219251%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGyUlEQVR42jWVWVdTWRqGzx/pm+oru2v1srq6y+6aFMpCARkEIYQgYUjIRMgMIRAyz3MYQgaSMAgWiiKiIqhY6ALbtnr1cF2/5ekNq/tir31W1tnPeb/vffcX6bPPfsOttmacUw6sXi9ai4WAQU/AomVW3YtX00faPETWa8chfrt8+feMqkfoutPNHy5f4pur3zE6bkRnMtPS3YV06Xe/5XafDJPNRr9OT79Kg1M9zKxJSy2XYDE4g0unxOc0oxsbvIAoR5WYLGa+/ubP/HCjCb19ErPTiWV2BunzLz7namcv12WD/LG5myuNrUx0dxByT/P06IhXJyc8eLTDs/2nHLx8QSaRpFos889//4vVe2tcv3Udq9tBupQmXkgifXHlT3zfMcDlpg6+bL1DQ7sMx10ZW/fq3H/0mN3nh7x6/4Hjs7/z7pd/8Pr+Pu+KD9kqLFOvV9CahpkO25hwaRgclyN9dfWvNCsUtA3JuTUsp00uw2/Ss1IrsrK2yqP9fbYeP2b9wTYbT7apxBNUNWbCCjlZjRqbZQSlsZ/e0dt0DYkeXmlsoEneR0t/Dy0KGc2i3LhrkoOjF2zt7lDb2qCyUWNlo36x5zMJ/KMjBJQDJBW9TIz20CpvpqW3iY6BW0jf/thAY087jZ1t/NjVwY3OVrLxEMcf3rPzbI/61hrV1RqlWpn54jwLhTzpXJJYPIBrsB9NXyftik465Z00tDYiDSivY5zsRGVuR+voQufoJr0cYvflPg92t6kLVcVynmQmTCzhJxyZu1jBuFhhF2ajgj5lAzLlNQZUPyBZzF3M+eXMzPXg9vQxPSsjveAmnY4Qi3rIZiO4Z4xMOtSEw058PhsejwW9th9/wI7TOYhO38S4sQXjRAuSYayFSHCEkH+I6MU+TCo9SamUJ5UKkEz6sBgGsYvmO+1qDDoZXq+JcY0cr9vI9JQMq7kVm7kNu7UDyTbeTTygIjg3hM+lxOsaxO8ZJZPyEPRPEg5OEotNExHq7GYlDtsQ4egUieSseM9AaHaQuBCTDqvIxMaQUkENqeAYycDYxR6dGxbQuwQ8evHVIaHYTirpFUABic2STfuFag/hkB33pIB5VcS8o8R9KiIeJdJibJxcSMd81EDKryYjPpANG8jFLUT8BqHUTSYbIpcPsbgYF70VwLgbt3OMMUUjfuddpia6mLXJ8EwqkPIRPecqc2E9i3GjgGsopsxslNzUlqaJhycELEC5nGMhE2JBuBubU5EPGUh71cyL97NBtTh/fm4CKRvVk43oSAjZaaEwJUrPCdXFtJlS1ko9PiluhIa0TktRDI/a3WFKHh2LMbFCWpbE2UXBKMQMlBJGpGrBSb04RSVvpZSxXoDKGQvV/z1vRqZ5l6uy7fKR6eimfquH0vgI+aiOjBAxL6pbEsBFAa6IyqT9nRyP7sf5qR5kq+plveiklrdTSk6wINpxf9LKG2+G8piR1M12Ss1d1FSDZANq8qKinF9FTpR8DlwIa5HeHlR4/azEq71lDnay7G5G+KkWYH15lpWMjSOXh11/jCXXNAstXSw3tbOq6Kcgep4RccsKYMilwD8pIyJ26ex4ndPXq5we1Xgn4G+eFjh6PM+LhxmersV44/Lz7vCIg+d7ZAUs90MLlbYeKm7R+/AYCY+K6OwQMfcQwRkxvj6ebHL2Zo0PF9AqJwdlfn5e5nBvgde1LCf+FA+26qzmokQaGliUN7Lc3kp9xsBG2SmMMAnHhTnClKW4DunTyQYfjlf5eLzG396s8v7lCm9fiBY8W+SX9RWe22aIGxTE5C2Exeyc7/meilLG/LSKctpIQZhTTpzvwpiwGun0sMzH1zU+Ceint+ucidJPBfTsVY2yVUuo9Tru777Ed/0avu42CuYRarEJKlmTMG6c5ZieBZHFc2Oy/hGkk+cF3h8WBaDK6eEKPz9b5vBxnoNHOaJ2Md57brJo1xN2mMhETCLwU1TnRaxyZpEGK5WMiXLKSFHAz1VKT+5F2NuKsreZ4GE1yJONuDAkz3ZdjK/IDA93Njn9cMz2VkY4P8VmcZr1JScr2f9DzdQXrCIRpos8SutlL2tFF/dqIU7f7/Prr//h49lLklErfvFfnMhExMSxMi9yVs05WFtysbHsZL0glOYsomwjlbTpIrcFcXWlpaSFbHSc7fsFXhw8Zc7jwDYhF1EYwe0YRNl/gxFFEw7DHXxTQwSmR8RdFlfUryPpFZPKpyXt04jn8wiJ8RWYVRGYUeMw3qXt5td8+5dLtN34isGea2iHWjBru7Dpey52k0assU4Mo+0Y1R2YNbexG3pxmuS4LAPMWPr5L1K8QW9NFVJ/AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b21869929b1ec829b9755950416096d3/e6bdf/bye.webp 374w&quot; sizes=&quot;(max-width: 374px) 100vw, 374px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b21869929b1ec829b9755950416096d3/b0af8/bye.png 374w&quot; sizes=&quot;(max-width: 374px) 100vw, 374px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b21869929b1ec829b9755950416096d3/b0af8/bye.png&quot; alt=&quot;안녕&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;div style=&quot;text-align: center;&quot;&gt;
      (스르륵 퇴장,,,)
   &lt;/div&gt;
&lt;/div&gt;</content:encoded></item><item><title><![CDATA[스마트 승급 시스템, 회원 승급 자동화의 혁신 스토리]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2024-12-22/member-upgrade-renewal/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-22/member-upgrade-renewal/</guid><pubDate>Sun, 22 Dec 2024 11:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;스마트-승급-시스템-회원-승급-자동화의-혁신-스토리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%EB%A7%88%ED%8A%B8-%EC%8A%B9%EA%B8%89-%EC%8B%9C%EC%8A%A4%ED%85%9C-%ED%9A%8C%EC%9B%90-%EC%8A%B9%EA%B8%89-%EC%9E%90%EB%8F%99%ED%99%94%EC%9D%98-%ED%98%81%EC%8B%A0-%EC%8A%A4%ED%86%A0%EB%A6%AC&quot; aria-label=&quot;스마트 승급 시스템 회원 승급 자동화의 혁신 스토리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&quot;스마트 승급 시스템, 회원 승급 자동화의 혁신 스토리&quot;&lt;/h1&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요. 회원 스쿼드 백엔드 개발자 뮤즈입니다!🙌🏻&lt;/p&gt;
&lt;p&gt;서비스에서 고객과 브랜드의 관계를 가장 잘 보여주는 것이 무엇일까요? 저는 멤버쉽 서비스라고 생각합니다.&lt;/p&gt;
&lt;p&gt;멤버쉽 서비스는 단순히 포인트 적립이나 할인 혜택을 제공하는 것을 넘어, 고객과 브랜드 사이의 신뢰를 구축하고, 고객 충성도를 강화하는 핵심적인 접점 역할을 합니다.&lt;/p&gt;
&lt;p&gt;이렇게 중요한 멤버쉽 서비스에서 매년 이루어지는 회원 등급 승급 작업은, 고객들에게 더 큰 혜택을 제공하고 브랜드에 대한 만족도를 높이는 중요한 순간입니다.&lt;/p&gt;
&lt;p&gt;하지만 이 승급 작업은 고객 경험의 기반이 되는 동시에, 방대한 데이터를 다루는 복잡한 기술적 도전 과제이기도 합니다.&lt;/p&gt;
&lt;p&gt;수작업 기반의 승급 프로세스는 많은 문제를 안고 있었고, 올리브영은 이를 해결하기 위해 올해 대대적인 혁신을 추진한 바 있습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 멤버쉽 승급 프로세스가 왜 중요한지, 그리고 올리브영이 어떻게 기술적으로 이를 자동화하고 효율적으로 개선했는지를 자세히 소개하겠습니다.&lt;/p&gt;
&lt;p&gt;고객과 브랜드의 신뢰를 기술로 어떻게 구현했는지 그 비하인드 스토리를 함께 알아볼까요!&lt;/p&gt;
&lt;h2 id=&quot;1-올리브영의-회원-규모&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%ED%9A%8C%EC%9B%90-%EA%B7%9C%EB%AA%A8&quot; aria-label=&quot;1 올리브영의 회원 규모 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 올리브영의 회원 규모&lt;/h2&gt;
&lt;p&gt;현재 약 1,400만명 이상의 고객이 올리브영 온라인몰을 이용하고 있습니다.&lt;/p&gt;
&lt;p&gt;이는 대한민국 전체 인구(약 5,000만명)의 약 1/5에 해당하며, 대한민구 국민 5명 중 1명이 올리브영의 고객이라는 놀라운 규모를 보여주고 있습니다.&lt;/p&gt;
&lt;p&gt;이렇게 방대한 고객 기반을 보유한 올리브영은 고객과의 지속적인 관계를 유지하며 더 나은 서비스를 제공하기 위한 &lt;strong&gt;올리브 멤버스&lt;/strong&gt;라는 멤버쉽 서비스를 운영하고있습니다.&lt;/p&gt;
&lt;p&gt;아래는 올리브 멤버스에서 현재 운영 중인 회원 등급 시스템입니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 550px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 321px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/db1498b0ceb075efdd498994af2be002/4f068/img_9.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 80.06230529595015%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAACm0lEQVR42nVU2XKbQBDk/78nT6m82K7EVsqKZcXoQrfQgS7QBcsCak+PQEkeQnlq17uzvd09s3LixCDYhBrL1Q7+coPoeMHpnCCMTgjWe2x2EdbbEOdLIuuxxj/zcjTGwuHG82sTX74+4uFHHd8ea1itN0hMit5whofnNzRcD6+NDmyWybqFSS3iJNUcztM0wyU2iCUcMpxME7Q7sYwpfN9iODJy4wXb/QH90RwTP8BsscZc2I9nSwwnCywDUSNrw/FCc6gstRkcInd7BvV6KqAJel6CD9eIpBiL1RaNDw+9wQzeyIc39NHqjdHqjhVkIGAfnRHeJGfir1SBExsjyAmy3MDKaK3R+VEYkj1vreSlIs/K/1me62GdZznyvFDpzHG4QQa8aSrSrlfcg8X4Xmvi6eUdbneEpttHeDiBX1EUknO9B+SPFzmpvd1aBRcZSZoqu6oAnJOBFUZFcQMhaCFjLqMla3pIQPozGM/Rl6pykR8PsFV+vrVRf++g25+Kbz6Opwsavz3pDFdUDfFSd7Fc7/SMekjAfXhEdDjjIP3HxUJuo19GGLHS3GORIpHLfeZv95GCH45nGc+4kiUZMmEZbJUhgxIrhptdiGZroP6ximwTgnS8CVxhx/y2VF09ROkhASltJbTZa/SJHyu3Cw9aKF5IpsyjIvbgQtY25bnK00wlCyNWkz1HMG4QjPT5nMhsNg8QbPfYR0ecRDqbm2wJyrFqnbIomd5C05lcyWXl6E9L5HYHUz1IcDJttvpqw0Cam42ely1EYIeofC3nONHW4E2MVN+oVZb8kaB3fPelXWX/4a7oxjC/SW62+1p+elY1LYNWPNXeUfvVUlZk83dT07vKP4a2DSvzJ6G4N7ayLYF1r2RS7f8vPgG4lsKTpgwnIQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/db1498b0ceb075efdd498994af2be002/6b614/img_9.webp 321w&quot; sizes=&quot;(max-width: 321px) 100vw, 321px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/db1498b0ceb075efdd498994af2be002/4f068/img_9.png 321w&quot; sizes=&quot;(max-width: 321px) 100vw, 321px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/db1498b0ceb075efdd498994af2be002/4f068/img_9.png&quot; alt=&quot;올리브 멤버스 등급&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;올리브 멤버스 등급&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h2 id=&quot;2-올리브영의-회원-등급&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%ED%9A%8C%EC%9B%90-%EB%93%B1%EA%B8%89&quot; aria-label=&quot;2 올리브영의 회원 등급 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 올리브영의 회원 등급&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;올리브영에서 멤버쉽 회원 등급을 부여하는 승급작업은 매년 1월, 7월에 2번 이루어지며, 이때 약 1,400만명의 회원이 대상이 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그래서 승급 작업 시에 DML이 수행되는 데이터 건 수도 굉장히 많아질테고 데이터 처리 및 마케팅 서비스에 연동되는 처리량도 굉장히 많아져서 이슈가 자주 발생했었고 이에 대한 대응이 필요했어요.&lt;/p&gt;
&lt;p&gt;우리는 어떻게 승급 작업을 정확하고 빠르게 또한 가장 중요한 부분인 &lt;strong&gt;장애없이 안정적&lt;/strong&gt;으로 회원 승급 작업을 수행할 수 있을지 고민하고 개선하기 시작했어요.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 412px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a106f65e9aabbf5b14129c0a359341a7/1f9f1/img_4.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.24271844660194%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEAUlEQVR42iWT+0+TVxzG3y1RWZyIuGSC46KiAopz0cwAAlOZqDjHNHPswnSiTm7LRtGCmwRRlkEBoZTLKBelQGnftm/7tn3bAi3XcXMzW0g2/5vPjvjDN+f89JzP833OI8lOP27vNF5/BL82ixZ8NQvMzj1nMTSObG5g2PgAzWXBPj2FUXHQrcgYZCvtdiuttjGeyBMYnTYM4i69EnO5g/gCc/gDM/i0CDMzC8jdt8lM3cqmNyTeiY0mPTmG6qoiBoMeTEK0XQh0OCZ4Isbosm880i7ukjcwi1udwuubFmTzhAWZb6CSpFiJzW/FUFv3Cx7Vx5hV5tqXV9DXlzMcnqJtQ0gWoraNB3rdDiFuQ1J8EVRhVRWWFVeQv8w6ar+KR5IkHj9q5OW//7H8xyIvXvzNP+sv0d25ytN+PU+8Kp1OQSismwRdr9tJh9OO5BFCPjFaaA7nqBN/1R5OZbxJzM54QgGNUHAKTQuzthhkeWUNk7GTutqbdPtVOuzjdIi9dQvLvYKy8zXhq0DCwnKYyMwK1TcubNAlJiXhcTiZ9LoJT1Sjth1gRr6HYh3le91dTKqDVusIhrFhugRpnxDsEnsVoUziFcHIDq84Q9y4VcX2mBiioqJo1OvwP+uh//5xvsmRMOjO0FKv59IXxXS5bLT392MeG6fPYqFlsA/DswERiiBU3AHGRidQFA9dXV3k5JzcoExKiEdX9i3Xi69yOO0QBfnnSDuYSt6JdEwN32F89BPDHQ30NOmoK82n5KPjSE6nBy0QQZZdhMMLVFVWE70tmh2CcsvmLWzetIkd0W8T9+4uUvakcGB/Bj98fZZnD4ow379Mf91n9Ok/wVRzkbKzR5FGR8cFoYaqhghok5Rev0VyQvLGvLc7QUwisTt2sm3rNmK3xxAfl0BZcQFD9Vcw111hqKwA8518THcvoyvORLKMWLDZHLg9fiEa5FLhp+xPSSM5cS9xu3azOy5R2EwnOzOboktFZGWd5MSxD2muOE9vTSE91YUYK87w663TlOTtRRocGGJkRFRMdjM0aCE3O4/0tIzXogl7qNXX4fN6mRa1W1leZnVlhYbGVq4VnaO7Mo+m0jyab5+muTSXO+czkHrbmhkwtiM/NdP8sIH3Mz7gUPoR9u1LE/v8kfX1dZ4//5OlpSUWFhdYW1tjflKhrb6MCaOempufU3gqC1fPz4y13xU7bKnBLsrvH/qNpnvlHD1yjNSDh0lLzcA2YWN1dZX5+XlmZ2eYCgXQfD6MDeX8/rAEq2yhr6ebyvIKFNESVfxFydX7GM+gAc1ixNL5kNysbFJSUjn78QUhMkskEiYYFKGJanlkB4rdjtdhJaSMo4ouqy4HAZ+KW5yKy8H/BRj178U+//4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a106f65e9aabbf5b14129c0a359341a7/65760/img_4.webp 412w&quot; sizes=&quot;(max-width: 412px) 100vw, 412px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a106f65e9aabbf5b14129c0a359341a7/1f9f1/img_4.png 412w&quot; sizes=&quot;(max-width: 412px) 100vw, 412px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a106f65e9aabbf5b14129c0a359341a7/1f9f1/img_4.png&quot; alt=&quot;img 4&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;그럼 먼저 승급 작업 프로세스에서 어떤 점들이 문제였는지 자세히 알아볼까요??&lt;/p&gt;
&lt;h2 id=&quot;3-회원-승급-프로세스의-문제점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%ED%9A%8C%EC%9B%90-%EC%8A%B9%EA%B8%89-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90&quot; aria-label=&quot;3 회원 승급 프로세스의 문제점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 회원 승급 프로세스의 문제점&lt;/h2&gt;
&lt;h3 id=&quot;수기-검증-프로세스의-문제점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%98%EA%B8%B0-%EA%B2%80%EC%A6%9D-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90&quot; aria-label=&quot;수기 검증 프로세스의 문제점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;수기 검증 프로세스의 문제점&lt;/h3&gt;
&lt;p&gt;사전 데이터 검증 작업과 승급 작업은 대량의 데이터와 복잡성으로 긴 시간이 소요되고, 각 데이터 상태를 일일이 확인해야 하므로 휴먼 에러가 발생할 가능성이 높았습니다.&lt;/p&gt;
&lt;p&gt;기존 승급 작업 순서는 아래와 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;승급 전날, 승급 대상 회원의 상태를 사전에 검증합니다. 회원 상태가 정상인지, 약관 철회나 탈퇴 상태가 아닌지 확인합니다.&lt;/li&gt;
&lt;li&gt;승급 작업은 사용자가 가장 적은 새벽 시간대에 이루어집니다. 개발자들이 직접 PL-SQL을 통해 온라인 스토어 DB와 오프라인 스토어 DB에 등급을 업데이트하고 이력을 저장한 후, 정상적으로 적용되었는지 확인합니다.&lt;/li&gt;
&lt;li&gt;마케팅 서비스에 회원 등급 데이터를 연동합니다.&lt;/li&gt;
&lt;li&gt;승급 작업 완료 후, 쿠폰 발급 작업을 진행합니다.&lt;/li&gt;
&lt;li&gt;쿠폰 발급에 대한 데이터를 마케팅 서비스와 연동합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;승급 작업 중 수작업으로 진행되는 과정과 문제점은 아래와 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;밤새 순차적으로 온라인 DB와 오프라인 DB에 대한 DML 작업 및 검증 작업을 수행합니다.&lt;/li&gt;
&lt;li&gt;수행된 데이터를 각각 온라인 스토어 DB와 오프라인 스토어 DB에서 검증합니다.&lt;/li&gt;
&lt;li&gt;7개 부서의 각 담당자들이 새벽부터 대기하며 작업 후 검증 작업을 진행합니다. (많은 인적 리소스 소모)&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 635px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/caf1441e775deb25ce126e1df528785a/ee2c8/img_1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.58333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAADFElEQVR42j2S+0+TdxSH3/9hv23L2BaXxRmQ1Dk2R8jcPVuCQaakc2ZGQHAzbl5KomEol7KhbggYhIp0QifMXmihUijY96WFogXcoMUhwhbqIBjrEBGl9PL4jSb74ck5Jyd5cj7JkbSFhRw+oOH74nIMV0fpeggdoYcUG6xkH/+FE10e5CjIKzHq+rwUanbD8jVi9wcFV4kLYosjxB4MEVvoR9Jqi2noasPQYaH19xb0BhvnrQo19a381Gzk15FbVLS7+OKrLFJVa1Gte4XTZbms3r1CfNEjZH1CNvgM0Uslh9Ro9m3jdGke+pP7MdUeRTZV0mOuwtur5+yZUo7mpzN2IZubTXkczEzhpReew3GxGB71E1lwPZM+xY2kyVBxID2Joh0plO9OpSI3jcq9aZTufEvMmziSpcJZlslM0zeETPswFnxC8roEfPYKuNNJNNQjosv/SyWLdiu28gxsZQJtBs6TmXgvleHvNzA93smdcQtz9dn0H/kcy7ebOZyejPHsQZYnW1n5x0xs3kH8v16i4tLogoI0cD4Hr24XI/oc/M053JuyAI+Jx4Oi3iYemeFKoZqGrck4tNu5PXCG8N0uVoJtRIJWonN2YqFueOCCRRmpu+RjBmvU+M7tYtzxI9HHE4Qf3WA1LFgNEF4YYm6kGU+jhvmhBpi7THi+k9VZO9F/7XCvV1zo4v6UjdCEGcm4fyP1XycSsBQQWfaLpcKSr5aVwG+w5BYCI7PuKhG9FYI2IiImSzLxkBNmO/jbo6PqWD57d2ag3vIBUsX29ZRseQ1PezXTwTEmx5wM95nwey7hc+gYuKjFba7HfU3hD287vlEX3lGZ4YDC9Qnxl9VVfKZWk5aSyPubVEhvvvEyH72bSIfSiS8YQvFPceqClUqDnbo2hSaHl+HpGRrbuqk1OTknfrLO2ovusoeWP29hnl1iT2U1iWue550Na5FeTXiRbZ+m8l3+DrTHD3G9p5mA0sJfsp6b7T8z2VJEY8GXnPghB9l6ikFHDSZdEXm5WaS8l8bbH24meUMS619PYGPSGp4AyU6KbEM9KT8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/caf1441e775deb25ce126e1df528785a/263a4/img_1.webp 480w,
/static/caf1441e775deb25ce126e1df528785a/99b64/img_1.webp 635w&quot; sizes=&quot;(max-width: 635px) 100vw, 635px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/caf1441e775deb25ce126e1df528785a/9aebd/img_1.png 480w,
/static/caf1441e775deb25ce126e1df528785a/ee2c8/img_1.png 635w&quot; sizes=&quot;(max-width: 635px) 100vw, 635px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/caf1441e775deb25ce126e1df528785a/ee2c8/img_1.png&quot; alt=&quot;img 1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;h3 id=&quot;대용량-데이터-작업의-문제점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%91%EC%97%85%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90&quot; aria-label=&quot;대용량 데이터 작업의 문제점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;대용량 데이터 작업의 문제점&lt;/h3&gt;
&lt;p&gt;회원 등급 변경 및 쿠폰 발급 작업이 끝난 후, 변경된 데이터를 마케팅 시스템으로 연동하는데 몇 가지 문제점이 발생했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;대용량 DML 작업 후 Oracle GoldenGate를 이용하여 마케팅 시스템으로 데이터가 연동되는 과정에서 동기화 지연 발생&lt;/li&gt;
&lt;li&gt;데이터 연동 과정에서 지연이 발생하면, 회원 등급 변경 정보와 쿠폰 발급 데이터가 각 서비스에 제대로 반영되지 않아 정상적인 서비스 이용의 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;실제로 새벽에 승급 작업을 하고 나서, 마케팅 시스템에 연동이 되어야 하는데 매장 오픈 시간 전까지 데이터가 연동이 되어있지 않았습니다.&lt;/p&gt;
&lt;p&gt;그래서 매장에서 고객이 사용할 수 있는 쿠폰이 조회되지 않아 쿠폰을 사용할 수 없을 수 있는 문제가 발생하기도 했어요.&lt;/p&gt;
&lt;p&gt;그 당시에 연동시간이 대략 9시간 정도 넘게 소요되었습니다.&lt;/p&gt;
&lt;p&gt;이렇게 긴 시간동안 데이터가 연동되지 않으면, 매장 오픈 후 서비스를 이용하는데 불편함을 느낄 수 밖에 없었어요.&lt;/p&gt;
&lt;p&gt;이런 다양한 문제들로 인해 기존의 수동 방식의 회원 승급 프로세스는 대용량 데이터 작업에 대해서 수작업 검증에 의존하고 있어서 효율성과 정확성 측면에서 너무 많은 한계를 가지고 있었어요.&lt;/p&gt;
&lt;p&gt;이러한 문제점을 해결하기 위해, 회원 승급 프로세스를 자동화하고, 대용량 데이터 작업을 효율적으로 처리할 수 있는 방안을 모색해야 했습니다.&lt;/p&gt;
&lt;p&gt;그럼 이제 과연 이러한 많은 문제점들을 어떻게 해결했는지 소개해드릴게요!&lt;/p&gt;
&lt;h2 id=&quot;4-회원-승급-프로세스는-어떻게-개선되었나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%ED%9A%8C%EC%9B%90-%EC%8A%B9%EA%B8%89-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%9C%EC%84%A0%EB%90%98%EC%97%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;4 회원 승급 프로세스는 어떻게 개선되었나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 회원 승급 프로세스는 어떻게 개선되었나요?&lt;/h2&gt;
&lt;h3 id=&quot;승급-작업-자동화-개선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%B9%EA%B8%89-%EC%9E%91%EC%97%85-%EC%9E%90%EB%8F%99%ED%99%94-%EA%B0%9C%EC%84%A0&quot; aria-label=&quot;승급 작업 자동화 개선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;승급 작업 자동화 개선&lt;/h3&gt;
&lt;p&gt;기존 PL-SQL 기반으로 이루어지던 수작업을 AWS 배치로 자동화하여 효율적으로 처리할 수 있도록 개선했어요.&lt;/p&gt;
&lt;p&gt;AWS 배치는 대규모 데이터를 효율적으로 처리할 수 있는 스케줄링 및 병렬 처리 기능을 제공하여, 기존 수작업 프로세스의 비효율성을 해소하는데 적합했어요.&lt;/p&gt;
&lt;p&gt;아래와 같은 작업 방식 변경을 통해 개발자의 수작업을 최소화하고 효율적인 프로세스를 정립할 수 있도록 하였습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;기존 : 온라인, 오프라인 DB각각 PL-SQL을 통해 DML 작업을 직접 수행하고 결과 검증
&lt;br&gt;변경 : AWS 배치를 통해 온라인 스토어 DB, 오프라인 스토어 DB에 대한 사전 데이터 검증, 승급 DML 작업을 한번에 수행하고, 작업 결과에 대한 검증도 함께 자동화&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그래서 우리는 비효율적인 프로세스를 개선함으로써 다음과 같은 효과를 얻을 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;인적 리소스 절감&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기존 수기 작업으로 인해 소요되던 시간과 리소스를 대폭 축소&lt;/li&gt;
&lt;li&gt;매번 승급 대상 데이터를 사전 검증하고 PL-SQL을 수행하고 승급 결과를 검증하는 반복적인 작업 최소화&lt;/li&gt;
&lt;li&gt;담당자는 배치 수행 모니터링과 배치 수행 결과를 확인하는 역할로 전환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;휴먼 에러 감소&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수기 작업에서 발생할 수 있는 검증 누락 등 오류를 방지&lt;/li&gt;
&lt;li&gt;데이터 검증과 승급 작업이 어플리케이션 로직 내에서 수행되므로 작업에 대한 정확도 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;배치 스케줄링&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS 배치를 통해 작업 스케쥴링 가능&lt;/li&gt;
&lt;li&gt;기존에는 담당자가 특정 주기에 맞춰 DML 작업을 직접 수행했지만, 배치 작업이 자동으로 실행되므로 승급 주기를 단축 가능&lt;/li&gt;
&lt;li&gt;슬랙 웹훅을 활용한 알림 전송을 통해 배치 수행 상황을 실시간으로 확인하며 모니터링 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div style=&quot;max-width: 550px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 741px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/484004dfbb6aabe614bc639663f8dbb5/660ed/img_3.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 128.33333333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAaCAYAAAC3g3x9AAAACXBIWXMAAC4jAAAuIwF4pT92AAADK0lEQVR42p1Va1faQBTk9/dLP9ifY/W0eqrVI4IEiBAC5EHIA8IzBCGZ3lkbCkqtNoc9uyHJZGZn7k1pvlxiEAwwmcUYDj2EYYjBYADf9zGbzbBer5EkCdbpWq3TNEWWZfjbUbrWajg5+YQrzcGPixtcXHzH5eUlrq+v4XkeHNuBaZro9XpqdLtdzOdzLBYLTCYTjEYjbLfbP4DhZIp6u41yJUNds1GtltHpdKBpmprbrfYOqN/vw3VdxHEM27aVkq7ZVax3gHnOKUe9Djw++gL6AL2pwx/6CqhjdBRTsqBUzhy5PMjzPM/w9PSkzhVgpuYMjQag644Al4XlA+7u7hTDVqu1k6zrunqZ0TZgW6b8bwiJ5+ubzeaQoSiE7YlE6ytOT89Qua/g/Pwc5XJZHnrEzc0Nbm9vodU0/JS16X7DdNkFn9836Tcg5EYgiFYIxy4cx4Ur+8M3c5+azaYCNQwDq9VKJIvMbYIs37x2eQcoDMPwSVztKYC+SKzVarIFdVQqFSX/paNHY1MAyrMY+imCcQTLHcL1fDn3EUWhAmImD4/8bUCjDTjBFt1xgn6UwIpTpJvDABdOvsmwWGTZFkn4gOnQxHIkldJldDxMp1O0JadvVccLwPz5pwCriAcGFpGLQADDwMdCStNxnPcDLqSMwjBCJGO2WL2SWMjcX+8H/Dnc+W6UQtlws6PDEwNaj7qUkin72VYzS4wxoSHpKlUlthTGRYiPN4dGHZ+/fEa934MmFcIgM8DMHaWydi3LUoPVwlzyRePxGEEQqEZxADhdLGENfPjStvoCSgA2AUaFpcYsXl1dqZnXCBRFkWoOtmWrmt9PwM5ltqOCEZlwTXcZaDLmf++JTml/s9869jf+2PmRHGaYxiNlAuWyefLYZsfd/CfgRvqYY1tIBJCy+Ql4b3UcBVTMxjFG0UhtOr8jdJHNleB0l07TFI7Cfao5MKVY8OPUkC5LE+guTalWq7i/v8fZ2Zmaec8knqjYcM0XEvyoy2TIm8jOG3iKGZ1lPIrIfEjyR1x+lylbcXEceKq0yI5yCvcLhz/EcCNtna1/uUzkgz9U0v/H5V9K5sNkBwT24wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/484004dfbb6aabe614bc639663f8dbb5/263a4/img_3.webp 480w,
/static/484004dfbb6aabe614bc639663f8dbb5/d34f7/img_3.webp 741w&quot; sizes=&quot;(max-width: 741px) 100vw, 741px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/484004dfbb6aabe614bc639663f8dbb5/9aebd/img_3.png 480w,
/static/484004dfbb6aabe614bc639663f8dbb5/660ed/img_3.png 741w&quot; sizes=&quot;(max-width: 741px) 100vw, 741px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/484004dfbb6aabe614bc639663f8dbb5/660ed/img_3.png&quot; alt=&quot;슬랙 알림 전송 내용&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;슬랙 알림 전송 내용&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;대용량-데이터-동기화-개선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%9A%A9%EB%9F%89-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%8F%99%EA%B8%B0%ED%99%94-%EA%B0%9C%EC%84%A0&quot; aria-label=&quot;대용량 데이터 동기화 개선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;대용량 데이터 동기화 개선&lt;/h3&gt;
&lt;p&gt;기존에는 Oracle GoldenGate를 통해 데이터 동기화를 수행했으나, 대용량 데이터 처리에 한계가 있었습니다.&lt;/p&gt;
&lt;p&gt;Oracle GoldenGate는 실시간 복제와 데이터 정합성을 보장해 안정적이지만, 분당 약 3만 건의 처리량으로 대규모 데이터 처리에는 적합하지 않았습니다.&lt;/p&gt;
&lt;p&gt;특히, 트랜잭션 기반 동작 특성상 수백만 건 이상의 대용량 데이터 처리에서는 성능 이슈가 발생했습니다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 Apache Kafka를 도입하여 데이터 동기화 속도를 개선했습니다.&lt;/p&gt;
&lt;p&gt;Apache Kafka는 고성능, 고가용성의 분산 메시지 플랫폼으로, 대용량 데이터 처리에 특화되어 있으며 분당 수십만 건 이상의 데이터 처리가 가능합니다.&lt;/p&gt;
&lt;p&gt;이를 통해 기존의 성능 한계를 극복하고 데이터 연동 속도를 대폭 향상할 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;처리 속도 향상
&lt;ul&gt;
&lt;li&gt;Oracle GoldenGate의 분당 처리량(약 3만건)에 비해, Kafka는 분당 10만건 이상의 데이터 처리가 가능하기 때문에 데이터 동기화 성능이 약 3배 이상 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;지연 리스크 감소
&lt;ul&gt;
&lt;li&gt;Apache Kafka는 서비스 간 데이터 전달 과정에서 발생할 수 있는 버퍼링 지연을 최소화&lt;/li&gt;
&lt;li&gt;동기화 과정에서의 병목 현상을 제거하여 데이터 일관성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;확장성 강화
&lt;ul&gt;
&lt;li&gt;Apache Kafka는 수평적으로 확장 가능한 구조를 가지고 있어, 데이터 처리량 증가에도 유연하게 대응 가능&lt;/li&gt;
&lt;li&gt;추가적인 서비스가 연결되더라도 데이터 처리 성능에 영향을 주지 않으므로, 향후 비즈니스 확장에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;마케팅 서비스 연동 시간 개선
&lt;ul&gt;
&lt;li&gt;Oracle GoldenGate 연동시간 : 9시간 41분&lt;/li&gt;
&lt;li&gt;Apache Kafka 적용 후 연동시간 : 58분 (약 90% 이상 감소)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;div style=&quot;max-width: 550px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/536880af1530618c6c977ee93e99440c/aeeb0/img_7.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.54166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAC4jAAAuIwF4pT92AAAC9UlEQVR42o2S7U9TZxiHz9/g/oMtS4jui8kiyWIqihr2FrAlke2LsSZz1Ja30wIKYk1qG2HlgyJmsIkz+LKo2VCUDo0zcfIiFUEQCwSEYqA0tD1AaWs57bXT047MZHHeyS+/+76f57lznudcAkq4xyY40drO2CtvqiSRTKqezLjf72dubg6fz6f20l2IRuP8Mh3g0NACN7xBkusJhNTC4+EXlDjP89QzoW5sa2vDarVit9txOBxUVlZSVlaGKIrYbDZsp+zU2218V/sDWcfvoTk7zEeO50wtRRFGp2a42NnN8OQ0rb934fX5aXQ60ev1GAwGiouLMRqNmEwm1dWeIlPxYbSHK9E29XLy7iy5zS8ZnV9FaGi/SV3LJVo7XFQ3t9F8s5NYPJ6+Mv8dciK98uilj03GTj50jKKp76HL9QfCFdcDfvrtDhc6ujh//Ra/3ntI9M27B2aelkA4xveXnrHNOUT19REG3f0IoWCQxUUffkXz8/NIoRDvFZmp0cgaS/4FVkJBIpFI+qf878F/PunfeaaUN4hIUyFEwuusra4TXo6rvrayjiwned9I7ZQzmTrw1IEhLF8+oa7oKce0A5Tv7eNFXzDNozJYnppCnphE9niQx8dJSNLGsFgszjVvCHFskduvJZKywmFFXj+HPn3EjzUeDJq/0G/tpfRgI5aj5ZjNVYglpYimEkRjCWbFzRUViJYqqi1mvi21stl6n8+ahvn49HOmUxxavnqC88gIF06O02QZxbDdTb5Gz66cT9i9I5vdOxXtymbPzrRSdW7ONvZqtpKdl0/BmR6O3Z4h52yKwzCC9ZtBLtomaKn1cLlhkrrCEYo+F9EV5lGo06LTFiheoLqaq76P/dqvydEd5AOFw6z6MYXDXu66uhHEL/rVN6zVDVKjc6vXf9gxy5tEmGBAQpKWNxSSpIwvs7qyzIwvwIGfB9jiGKT86hDuvh6Efpef7iterp17Rlf7K/68scDSQvQtgN/FYUzhMKByuKRy+DcFG45Q7J38tAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/536880af1530618c6c977ee93e99440c/263a4/img_7.webp 480w,
/static/536880af1530618c6c977ee93e99440c/a6361/img_7.webp 960w,
/static/536880af1530618c6c977ee93e99440c/0b34d/img_7.webp 1920w,
/static/536880af1530618c6c977ee93e99440c/38708/img_7.webp 2338w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/536880af1530618c6c977ee93e99440c/9aebd/img_7.png 480w,
/static/536880af1530618c6c977ee93e99440c/a91f8/img_7.png 960w,
/static/536880af1530618c6c977ee93e99440c/ac7a9/img_7.png 1920w,
/static/536880af1530618c6c977ee93e99440c/aeeb0/img_7.png 2338w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/536880af1530618c6c977ee93e99440c/ac7a9/img_7.png&quot; alt=&quot;데이터 연동&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;데이터 연동 속도&lt;/figcaption&gt;
&lt;/div&gt;
&lt;div style=&quot;max-width: 550px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0693ad3e102c88ef8d3b3581281b77f4/f39b7/img_8.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 19.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA10lEQVR42o2PTW6DMBSEuVsP0kv0Fl132TMVqeEnQGNjbGLiAgqYigjRrxB12UVnMxrp6c18wevLM0pVhOEbQgiSOMaez0yTZxxHvJ8YhwG++ZeCp8cHDklOmmZE7weiOOMkJJWuybMcuZV47zHGUJYlUkqstWit73kfsfueh604uLgWc1EkuiQ1llxGHMV2aDShcsSiovjI6Pqe9JjhnKMoit/1fiOZ7o+mjWRZFoKdpLtaKldjhyu1iRFK0TiL7L8w7UR+MtxuM59txzzPNLZhXdc/kX8AktQpxJhh3UcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0693ad3e102c88ef8d3b3581281b77f4/263a4/img_8.webp 480w,
/static/0693ad3e102c88ef8d3b3581281b77f4/a6361/img_8.webp 960w,
/static/0693ad3e102c88ef8d3b3581281b77f4/0b34d/img_8.webp 1920w,
/static/0693ad3e102c88ef8d3b3581281b77f4/da28f/img_8.webp 2880w,
/static/0693ad3e102c88ef8d3b3581281b77f4/b92d5/img_8.webp 3186w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0693ad3e102c88ef8d3b3581281b77f4/9aebd/img_8.png 480w,
/static/0693ad3e102c88ef8d3b3581281b77f4/a91f8/img_8.png 960w,
/static/0693ad3e102c88ef8d3b3581281b77f4/ac7a9/img_8.png 1920w,
/static/0693ad3e102c88ef8d3b3581281b77f4/f9c26/img_8.png 2880w,
/static/0693ad3e102c88ef8d3b3581281b77f4/f39b7/img_8.png 3186w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0693ad3e102c88ef8d3b3581281b77f4/ac7a9/img_8.png&quot; alt=&quot;데이터 연동 완료&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;데이터 연동 완료&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 프로젝트를 통해 회원 승급 프로세스를 자동화하고 대용량 데이터 처리 방식을 개선하여, 기존 수작업의 한계를 극복하고 안정적인 서비스 제공의 기반을 마련했습니다.&lt;/p&gt;
&lt;p&gt;특히, AWS Batch와 Apache Kafka를 도입해 대용량 데이터 처리와 동기화 성능을 크게 향상시켰으며, 수작업으로 이루어지던 비효율적인 업무들을 자동화하여 인적 리소스를 절감하고 휴먼 에러를 감소시킬 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이를 통해 승급 작업의 효율성과 안정성을 모두 강화할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;물론 여전히 개선해야 할 점들이 남아 있습니다. 하지만 우리는 지속적으로 서비스를 개선해 업무 효율성을 높이고, 사용자들에게 더 나은 서비스를 제공할 수 있도록 노력할 것입니다.&lt;/p&gt;
&lt;p&gt;앞으로도 올리브영 회원 서비스에 많은 관심과 응원 부탁드립니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Spring Boot MongoDB 트랜잭션 도입 실전 가이드]]></title><description><![CDATA[👉 들어가기에 앞서 안녕하세요! 올리브영에서 상품 도메인을 책임지고 있는 윤긱스입니다.
혹시 지금.. 이런 고민을 하고 계신가요? 🙋 : MongoDB…]]></description><link>https://oliveyoung.tech/2024-12-17/catalog-mongo-transaction-2/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-17/catalog-mongo-transaction-2/</guid><pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h3 id=&quot;-들어가기에-앞서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0%EC%97%90-%EC%95%9E%EC%84%9C&quot; aria-label=&quot; 들어가기에 앞서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👉 들어가기에 앞서&lt;/h3&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요!&lt;/p&gt;
&lt;p&gt;올리브영에서 상품 도메인을 책임지고 있는 윤긱스입니다.
혹시 지금.. 이런 고민을 하고 계신가요?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🙋 : MongoDB 트랜잭션을 도입하고 싶지만 어렵고 막막해요!&lt;/li&gt;
&lt;li&gt;🙋‍♀️ : 도입은 했는데, 이게 진짜 잘 돌아가는건지 모르겠어요!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그렇다면 걱정하지 마세요.&lt;/p&gt;
&lt;p&gt;여기 &apos;MongoDB 트랜잭션 생존 가이드&apos;에서 올리브영에서 겪은 경험을 통한 실전 꿀팁을 아낌없이 대방출합니다.
Replica Set부터 Oplog 동작 원리까지, 운영 중에 발생할 수 있는 상황을 헤쳐 나갈 실전 가이드! 시작해 보겠습니다!&lt;/p&gt;
&lt;h3 id=&quot;-mongodb-트랜잭션의-주요-특징&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-mongodb-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%98-%EC%A3%BC%EC%9A%94-%ED%8A%B9%EC%A7%95&quot; aria-label=&quot; mongodb 트랜잭션의 주요 특징 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👉 MongoDB 트랜잭션의 주요 특징&lt;/h3&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-트랜잭션을-지원하는-mongodb-버전&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%84-%EC%A7%80%EC%9B%90%ED%95%98%EB%8A%94-mongodb-%EB%B2%84%EC%A0%84&quot; aria-label=&quot;1 트랜잭션을 지원하는 mongodb 버전 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) 트랜잭션을 지원하는 MongoDB 버전&lt;/h4&gt;
&lt;table class=&quot;catalog-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;MongoDB Engine Version&lt;/th&gt;
      &lt;th&gt;Spring 관련 모듈 version&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Single-Document&lt;/td&gt;
      &lt;td&gt;MongoDB 4.0 이전&lt;/td&gt;
      &lt;td&gt;Spring Data MongoDB의 모든 버전&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Multi-Document&lt;/td&gt;
      &lt;td&gt;
        &lt;p&gt;Replica Set: MongoDB 4.0 이상&lt;/p&gt;
        &lt;p&gt;Sharded Cluster: MongoDB 4.2 이상&lt;/p&gt;
      &lt;/td&gt;
      &lt;td&gt;Spring Data MongoDB 2.1.x 이상&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;2-트랜잭션이-동작하려면&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%A0%A4%EB%A9%B4&quot; aria-label=&quot;2 트랜잭션이 동작하려면 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) 트랜잭션이 동작하려면?&lt;/h4&gt;
&lt;p&gt;MongoDB 트랜잭션은 모든 MongoDB에서 통하는 것은 아닙니다!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replica Set 또는 Sharded Cluster 환경에서만 가능&lt;/li&gt;
&lt;li&gt;WiredTiger 스토리지 엔진이어야만 작동&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;여기서-잠깐-왜-replica-set이나-sharded-cluster에서만-트랜잭션이-가능할까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%AC%EA%B8%B0%EC%84%9C-%EC%9E%A0%EA%B9%90-%EC%99%9C-replica-set%EC%9D%B4%EB%82%98-sharded-cluster%EC%97%90%EC%84%9C%EB%A7%8C-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%A0%EA%B9%8C&quot; aria-label=&quot;여기서 잠깐 왜 replica set이나 sharded cluster에서만 트랜잭션이 가능할까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;❓여기서 잠깐! 왜 Replica Set이나 Sharded Cluster에서만 트랜잭션이 가능할까?&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;1. 복제 없이 데이터 무결성 보장 불가&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standalone MongoDB는 노드가 하나이기 때문에 장애 시 복구 불가&lt;/li&gt;
&lt;li&gt;복제 기능이 없으니 트랜잭션도 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;2. 트랜잭션 = Primary 노드 + Secondary 노드 동기화 &lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션 중에는 Primary 노드에서 데이터를 쓰고, Secondary 노드로 Oplog를 통해 복제&lt;/li&gt;
&lt;li&gt;이렇게 동기화해야 데이터의 스냅샷 상태를 보장할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;3. Standalone MongoDB의 경우, oplog 불필요&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;복제본이 없으니 당연히 oplog 불필요 → 트랜잭션 기능 미지원&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;cf) RDB는 트랜잭션 로그(WAL)와락(lock) 메커니즘으로 데이터 무결성과 일관성 보장 → MongoDB는 이런 방식 대신 Replica Set을 기반으로 동작&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;-mongodb-트랜잭션-동작-방식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-mongodb-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D&quot; aria-label=&quot; mongodb 트랜잭션 동작 방식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👉 MongoDB 트랜잭션 동작 방식&lt;/h3&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-mongodb의-격리-수준isolation은-어떻게-될까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-mongodb%EC%9D%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80isolation%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%90%A0%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;1 mongodb의 격리 수준isolation은 어떻게 될까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) MongoDB의 격리 수준(Isolation)은 어떻게 될까요?&lt;/h4&gt;
&lt;p&gt;MongoDB는 기본적으로 Read Committed를 사용합니다.&lt;/p&gt;
&lt;p&gt;즉, 다른 트랜잭션이 완료한 데이터는 읽을 수 있습니다. &lt;/br&gt;&lt;/p&gt;
&lt;p&gt;MongoDB에는 또 하나의 비장의 무기 &apos;Snapshot Isolation&apos;이 있습니다.&lt;/p&gt;
&lt;p&gt;이 방식은 트랜잭션 진행 중 데이터가 외부에서 변경되지 않도록 보장합니다.&lt;/p&gt;
&lt;h4 id=&quot;2-snapshot-isolation-어떻게-작동할까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-snapshot-isolation-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9E%91%EB%8F%99%ED%95%A0%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;2 snapshot isolation 어떻게 작동할까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) Snapshot Isolation, 어떻게 작동할까요?&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;트랜잭션 시작 시&lt;/b&gt;: 데이터베이스가 해당 트랜잭션만을 위한 스냅샷을 찍습니다.이제 트랜잭션 내에서는 이 스냅샷을 기준으로 데이터가 반환됩니다.  &lt;/br&gt;
&lt;b&gt;트랜잭션이 동시에 진행될 때&lt;/b&gt;: 다른 트랜잭션이 데이터를 막 변경해도,내 트랜잭션은 처음 찍은 스냅샷 데이터만 봅니다.&lt;/br&gt;
&lt;b&gt;즉, 다른 트랜잭션의 변경 내용은 모른 척한다는 뜻!&lt;/b&gt;&lt;/p&gt;
&lt;h4 id=&quot;3-snapshot-isolation의-장점과-단점은&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-snapshot-isolation%EC%9D%98-%EC%9E%A5%EC%A0%90%EA%B3%BC-%EB%8B%A8%EC%A0%90%EC%9D%80&quot; aria-label=&quot;3 snapshot isolation의 장점과 단점은 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3) Snapshot Isolation의 장점과 단점은?&lt;/h4&gt;
&lt;table class=&quot;catalog-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;항목&lt;/th&gt;
      &lt;th&gt;장점&lt;/th&gt;
      &lt;th&gt;단점&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;일관성&lt;/td&gt;
      &lt;td&gt;데이터 일관성 보장&lt;/td&gt;
      &lt;td&gt;Write Conflict 발생 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;동시성&lt;/td&gt;
      &lt;td&gt;높은 동시성 지원&lt;/td&gt;
      &lt;td&gt;여러 트랜잭션 충돌 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;읽기 작업&lt;/td&gt;
      &lt;td&gt;읽기 작업 시 항상 일관된 데이터 제공&lt;/td&gt;
      &lt;td&gt;충돌 방지 로직 필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;안정성&lt;/td&gt;
      &lt;td&gt;다른 트랜잭션으로 인한 데이터 꼬임 방지&lt;/td&gt;
      &lt;td&gt;트랜잭션 실패 시 재시도 필요&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;-여기서-잠깐-write-conflict-시나리오&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%97%AC%EA%B8%B0%EC%84%9C-%EC%9E%A0%EA%B9%90-write-conflict-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4&quot; aria-label=&quot; 여기서 잠깐 write conflict 시나리오 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;💥 여기서 잠깐! Write Conflict 시나리오&lt;/h4&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/5f4ca/writeconflict.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 80.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAACS0lEQVR42pVU32vTUBi9XX8mqWnaUbZK5tCFaeOaCpeWIIP74CqBlfXpvnQbRSqVCWOWghv4cB8m26TWWiyFUiFsZQ7umzJE8WFvPhX/A/+WeBNbUNfW7oOTLyHfdzj3HBIACPAwTI2FBVxDMWz292V8YQzcsAS9NlSs+hBBHhsO6Z/lPP+f0LXwIjeXOFy+pe7pivLknnp3//7CHZJalLHOjSVs79JQ/Xkn/vLZm5islzkd17jEaj58e41chys4pKpMHUCeJEFS0kQSbJe8ymfDP5KwWqkK50062200RFCmXIz0eG2jImiaJijGumirted0ijnYhl5MsdvuIwlNsyAddzbjzWZhNpZt89GnNJhEBSmVejCtP8QRWyEG2J0mhgg/ZXmlYfiRWQiMVUi2icwgziMS0DIVAWazvKZlhHnkLDqzco15Zzn3rr+CuaSQmNIJoUqHdCI38odh5VEjWmTKKIShr5mMMJhzfPs34WGEDdIVz15/meuyDhDyKMa2n2Ds+5ZKTV+gHAuh5wXYcuu1Mgd7Aw9Loz082j0KHZQPlPpOXYIQenVd5wyF+VQoBGCp55XL3zlALJ92UhF0qnMqxT5kotEetlr58PG7zcVT1mNZwidW98IrEIcq7LiUKbX6KdtEEx55XazuP775/tXatShuBuViJ5LL7UgX6bT8Y9mIWv1QJiY8b22Fzzpb8Y9vizNEVYMflpZmThOJcAlm+W7aEH+ycK5EePn9YMlyYUzdpK8QsDBGLEz0LU9eDiEF7pG/p6uCgKlf7AHaogKKZDMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/263a4/writeconflict.webp 480w,
/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/a6361/writeconflict.webp 960w,
/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/0b34d/writeconflict.webp 1920w,
/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/51b42/writeconflict.webp 2178w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/9aebd/writeconflict.png 480w,
/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/a91f8/writeconflict.png 960w,
/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/ac7a9/writeconflict.png 1920w,
/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/5f4ca/writeconflict.png 2178w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c81c7ebf21c4cbb27cbcd476dae5a4a0/ac7a9/writeconflict.png&quot; alt=&quot;Write Conflict&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/img&gt;
&lt;b&gt;1. Transaction-1 Start&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;{ &quot;val&quot;: 3 } 값을 조회함과 동시에 데이터의 스냅샷을 기록하고 작업을 시작합니다.  (이 데이터는 메모리에 저장됩니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;2. Transaction-2 Start&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transaction-1 이 시작된 시점의 동일한 데이터를 읽고 작업에 들어갑니다. (아직 Transaction-1의 변경사항은 모르는 상태입니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;3. Transaction-1 Update &amp;#x26; Commit&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transaction-1이 작업을 끝내고  { &quot;val&quot;: 5 }  메모리에 반영하고 데이터를 디스크에 반영합니다. 이제 데이터베이스에 새로운 값이 반영되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;4. Transaction-2 Update &amp;#x26; Commit&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transaction-2가 데이터를 { &quot;val&quot;: 7 } 수정하려는데...?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;5. Write Conflict 발생!!!&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;디스크 데이터는  { &quot;val&quot;: 5 } 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;이유는 간단합니다. Transaction-2는 이전  { &quot;val&quot;: 3 } 데이터를 기준으로 작업 중이었으니까요. MongoDB는 해당 격리수준으로 Transaction 을 관리하고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;-spring-boot-mongodb-트랜잭션-설정-요소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-spring-boot-mongodb-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%84%A4%EC%A0%95-%EC%9A%94%EC%86%8C&quot; aria-label=&quot; spring boot mongodb 트랜잭션 설정 요소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👉 Spring Boot MongoDB 트랜잭션 설정 요소&lt;/h3&gt;
&lt;hr&gt;
&lt;p&gt;코드부터 보시죠! MongoClient에서 트랜잭션을 설정하는 간단한 예제입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; MongoConfig &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;

	&lt;span class=&quot;token annotation builtin&quot;&gt;@Bean&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mongoClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MongoClient &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; mongoClientSettings &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; MongoClientSettings&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;applyConnectionString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mongodb://localhost:27017&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// MongoDB 서버 URL 설정&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeConcern&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;WriteConcern&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ACKNOWLEDGED&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// WriteConcern 기본 설정 옵션&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readPreference&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ReadPreference&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;secondaryPreferred&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;//  ReadPreference Replica시 많이 설정하는 옵션&lt;/span&gt;
			&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; MongoClients&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mongoClientSettings&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;1-write-concern-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-write-concern-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;1 write concern 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) Write Concern 설정&lt;/h4&gt;
&lt;p&gt;Write Concern은 간단히 말하면, 데이터가 &quot;잘 저장되었는지&quot; 확인하는 장치입니다.&lt;/p&gt;
&lt;p&gt;이 설정 덕분에, 데이터의 내구성과 일관성을 보장할 수 있습니다.&lt;/br&gt;
아래 표처럼 Write Concern은 여러 옵션이 있습니다.&lt;/p&gt;
&lt;table class=&quot;catalog-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Write Concern&lt;/th&gt;
      &lt;th&gt;설명&lt;/th&gt;
      &lt;th&gt;특징&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;ACKNOWLEDGED&lt;/td&gt;
      &lt;td&gt;기본값은 최소 1개의 노드에서 응답을 받을 때 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;빠른 응답 속도를 제공하고 널리 사용되지만, 데이터 일관성이 낮을 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MAJORITY&lt;/td&gt;
      &lt;td&gt;복제본 세트의 과반수 노드에 데이터 기록이 완료되면 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;높은 데이터 일관성을 보장하며, 과반수에서 기록이 완료되지만 성능이 저하될 가능성이 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;UNACKNOWLEDGED&lt;/td&gt;
      &lt;td&gt;응답을 받지 않고 작업이 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;매우 빠른 응답을 제공하며 성능 최적화가 가능하지만, 데이터 손실 가능성이 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;W1&lt;/td&gt;
      &lt;td&gt;최소 1개의 노드에서 성공하면 작업이 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;빠른 성능을 제공하며 최소 1개 노드에서 성공 시 완료되지만, 데이터 일관성이 부족할 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;W2&lt;/td&gt;
      &lt;td&gt;2개의 노드에서 성공적으로 기록될 때 작업이 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;높은 데이터 일관성을 보장하며, 2개 노드에서 기록이 보장되지만 성능 저하가 있을 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;FSYNCED&lt;/td&gt;
      &lt;td&gt;디스크에 기록된 후 작업이 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;높은 내구성을 보장하며, 디스크 동기화 후 기록이 보장되지만 성능 저하가 심할 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;JOURNALED&lt;/td&gt;
      &lt;td&gt;복구 로그에 기록된 후 작업이 성공으로 간주됩니다.&lt;/td&gt;
      &lt;td&gt;장애 시 복구가 가능하며 안전한 기록을 보장하지만, 성능 저하가 있을 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 참고 &lt;/br&gt;
Write Concern 설정은 복제본 세트 환경에서만 의미가 있습니다! Standalone 환경에서는 무시되기 때문에, 이 점을 반드시 기억하세요! &lt;/br&gt;
만약, 복제본 세트에서 운영 중이라면 이 설정을 적극 활용하고, Standalone 환경에서는 그 효과를 기대하기 어려운 점을 유의해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;2-read-preference-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-read-preference-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;2 read preference 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) Read Preference 설정&lt;/h4&gt;
&lt;p&gt;복제본 세트를 사용하는 환경에서 중요한 역할을 하며, 데이터를 읽는 시점과 일관성을 어떻게 처리할지를 정할 수 있습니다. 아래 표에서 각 옵션의 특성과 장단점을 확인해 보세요!&lt;/p&gt;
&lt;table class=&quot;catalog-table&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Read Preference&lt;/th&gt;
      &lt;th&gt;설명&lt;/th&gt;
      &lt;th&gt;특징&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;PRIMARY&lt;/td&gt;
      &lt;td&gt;기본 설정. 쓰기 작업이 이루어지는 주요 노드에서 읽습니다.&lt;/td&gt;
      &lt;td&gt;가장 신뢰할 수 있는 데이터를 읽을 수 있어 데이터 일관성이 보장됩니다. 읽기 성능은 떨어질 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SECONDARY&lt;/td&gt;
      &lt;td&gt;복제본 노드에서 읽습니다. 데이터 일관성보다는 읽기 성능을 우선시 합니다.&lt;/td&gt;
      &lt;td&gt;읽기 작업을 복제본에서 처리하여 부하를 분산시킬 수 있습니다. 최신 데이터가 아닐 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;PRIMARY_PREFERRED&lt;/td&gt;
      &lt;td&gt;PRIMARY에서 읽습니다. PRIMARY가 사용 불가하면 SECONDARY에서 읽습니다.&lt;/td&gt;
      &lt;td&gt;PRIMARY 노드를 우선으로 사용하여 높은 가용성을 보장합니다. SECONDARY에서 읽을 때 최신 데이터가 아닐 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SECONDARY_PREFERRED&lt;/td&gt;
      &lt;td&gt;SECONDARY에서 읽습니다. 읽을 수 없으면 PRIMARY에서 읽습니다.&lt;/td&gt;
      &lt;td&gt;복제본에서 읽을 수 있으면 성능이 향상됩니다. PRIMARY에서 읽을 때보다 부하가 분산됩니다. PRIMARY에서 읽을 때 일관성 보장 가능성은 낮을 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;NEAREST&lt;/td&gt;
      &lt;td&gt;여러 복제 노드 중 네트워크 지연시간이 가장 적은 노드를 선택하여 데이터를 읽습니다.&lt;/td&gt;
      &lt;td&gt;네트워크 대기시간을 줄여 빠른 응답 시간을 제공합니다. 분산 환경에서 유용합니다. 읽은 데이터가 최신이 아닐 수 있습니다.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;3-transactionmanager-설정-필수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-transactionmanager-%EC%84%A4%EC%A0%95-%ED%95%84%EC%88%98&quot; aria-label=&quot;3 transactionmanager 설정 필수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3) transactionManager 설정 (필수!)&lt;/h4&gt;
&lt;p&gt;트랜잭션을 제대로 사용하려면 MongoTransactionManager를 @Bean 으로 등록해야 합니다. &lt;/br&gt;
그래야 @Transactional 어노테이션을 사용할 때 트랜잭션 처리가 제대로 동작합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 참고 &lt;/br&gt;
Spring Boot 에서는 RDBMS의 트랜잭션 매니저를 자동으로 @Bean 으로 등록해 주죠. (예:  spring-boot-starter-data-jpa가 자동 설정)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;-spring-boot-트랜잭션--replica-설정-시-이슈와-그-해결책&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-spring-boot-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98--replica-%EC%84%A4%EC%A0%95-%EC%8B%9C-%EC%9D%B4%EC%8A%88%EC%99%80-%EA%B7%B8-%ED%95%B4%EA%B2%B0%EC%B1%85&quot; aria-label=&quot; spring boot 트랜잭션  replica 설정 시 이슈와 그 해결책 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;👉 Spring Boot 트랜잭션 + Replica 설정 시, 이슈와 그 해결책&lt;/h3&gt;
&lt;hr&gt;
&lt;p&gt;이제, 우리가 사용한 설정들에 대해 소개합니다.
이 설정들이 어떤 문제를 일으킬 수 있는지, 그리고 어떻게 개선하면 더 효율적으로 사용할 수 있을지 알아보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;writeConcern(WriteConcern.ACKNOWLEDGED)&lt;/b&gt;: 이 설정, 과연 완벽할까요?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;readPreference(ReadPreference.secondaryPreferred())&lt;/b&gt;: 이 설정은 장점이 많지만, 몇 가지 문제를 야기할 수 있습니다. 사용 시 주의가 필요합니다!&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제, 위 설정들이 실제 운영시 서비스에 어떻게 영향을 미치는지 아래 예시를 통해 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;RequestAddServiceImpl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt; 
&lt;span class=&quot;token comment&quot;&gt;//생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestAddService &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token annotation builtin&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mongoTransactionManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ResponseDto &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;//  Dto to Entity &amp;amp;  domain Logic Setting&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; request &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; dtoConverter&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;convertToAddRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// Goods 도메인 등록정보 Save&lt;/span&gt;
        goodsAddService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// 결재를 상신한다 (상품정보를 재 조회하는 로직 존재)&lt;/span&gt;
        goodsApprovalService&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;submit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        
        &lt;span class=&quot;token comment&quot;&gt;//Event Send (SQS Publish)&lt;/span&gt;
        eventSender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 

        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ResponseDto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token annotation builtin&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;EventSenderImpl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; amazonSQS&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AmazonSQS&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; EventSender &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sendMessageDto &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SendMessageRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;withMessageBody&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;objectMapper&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeValueAsString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        amazonSQS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sendMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sendMessageDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;1-primary와-secondary-간-lag로-인한-비일관성-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-primary%EC%99%80-secondary-%EA%B0%84-lag%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EB%B9%84%EC%9D%BC%EA%B4%80%EC%84%B1-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;1 primary와 secondary 간 lag로 인한 비일관성 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1) Primary와 Secondary 간 Lag로 인한 비일관성 문제!!&lt;/h4&gt;
&lt;p&gt;Replica Set이나 Sharded Cluster 환경에서는 Write Concern과 Read Preference 설정이 큰 영향을 미칩니다.&lt;/br&gt;
아래 그림은 WriteConcern.ACKNOWLEDGED와 ReadPreference.secondaryPreferred 설정이 데이터 조회에 어떤 방식으로 영향을 미치는지 보여줍니다.
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/40385bd48977aaa612cfd93fd7acd3ce/911a3/lag.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACNklEQVR42p2Sz2sTQRTHXzbJNpvfMQkxEAm1aZNOSEKYdln8tSAKOQh6mZsQTCDSUAlW1ranAWGVdpNDqJc9edAirCJGpJceehHx4LHgyYvQv2OdCV3Nr4r64MvOm9n5vHk/ACh4mARH9NeaCgC2C7jZ4BruOcbXfG/UdzT244Thlumt1GlUIT2J+7maHK7crkQ5TFVVT+FOIZ5v5EMjV1yTQB5VOP3C6ta7+OXHb4rXtwcp7i80q7nFTnUZUSRmiCIttFeLS+vy/Nh9YhG3433odhNvdb2w93QvDq3DSO7BlzAQIqovsj7FUiRkEREscE88AM4EHltU7HbNRG37IBljwPTG1wRqt4PYxH70XA3KfTmMqMp9r3qkerL1rG8KOLuGtsAiuYHaAm5hL08RUSJmeorElevX5rj4/l8CT5vHOo0IFQmHcyMsG9YMJ1UejGscSH7XhAPy2qdQXnsfStKj4JWt/di1zVelqxuvL/Dz0trKxeJ9ucIgfkSQuNxYqS6t4wI7cs+s4cfd3fMvn+zk7+lWEja/R+Lat1CyTYMp42Ygw5qSMsoBuIX9Iy8SRmDTwIGupwaGMX9AadgE7LVYh3tsPHYal0LP1koxk73sGCHRZmnbGHttGNZQOBNo6XpyX9MWPzeb52wA3w8lI5k47TfulgOHN3Ck01GjdSbaRkHjUTnAv1PAyaacpNP+E4yHssvlwEOSkYCNCNgscD83B2wmh76j8Vn8c5f/w2YCXRP6J+BPqp+fzQwMHT0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/40385bd48977aaa612cfd93fd7acd3ce/263a4/lag.webp 480w,
/static/40385bd48977aaa612cfd93fd7acd3ce/a6361/lag.webp 960w,
/static/40385bd48977aaa612cfd93fd7acd3ce/0b34d/lag.webp 1920w,
/static/40385bd48977aaa612cfd93fd7acd3ce/10308/lag.webp 2171w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/40385bd48977aaa612cfd93fd7acd3ce/9aebd/lag.png 480w,
/static/40385bd48977aaa612cfd93fd7acd3ce/a91f8/lag.png 960w,
/static/40385bd48977aaa612cfd93fd7acd3ce/ac7a9/lag.png 1920w,
/static/40385bd48977aaa612cfd93fd7acd3ce/911a3/lag.png 2171w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/40385bd48977aaa612cfd93fd7acd3ce/ac7a9/lag.png&quot; alt=&quot;Write Conflict&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/img&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 중요! &lt;/br&gt;
결국, PRIMARY와 SECONDARY 간의 복제 지연 때문에, SECONDARY에서 데이터를 찾지 못하게 되는 문제입니다.
마치SECONDARY가 아직 데이터를 못 받아서 &quot;그거 아직 없어요!&quot;라고 말하는 상황입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;q-무엇이-문제였을까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EC%98%80%EC%9D%84%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;q 무엇이 문제였을까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Q. 무엇이 문제였을까요?&lt;/h4&gt;
&lt;p&gt;위 코드와 그림에서 드러난 주요 문제는 WriteConcern 설정은 기본값인 ACKNOWLEDGED를 사용해 빠른 응답을 유지하려 하면서도, Read Preference가 secondaryPreferred로 설정되어 있다는 점입니다.&lt;/br&gt;
이로 인해 PRIMARY와 SECONDARY 간 복제가 완료되지 않은 상황에서 데이터를 읽으려 하면 문제가 발생할 수 있습니다. 예를 들어, id = a라는 정보를 다시 조회하려 할 때 기대한 결과를 얻지 못할 가능성이 있는 것이죠.&lt;/br&gt;
즉, SECONDARY 노드에서 복제 지연이 발생하면 데이터를 찾지 못하고, 결국 Not Found Exception이 발생하는 상황으로 이어질 수 있습니다. 그렇다면, 위 그림의 문제를 실제 예제에 대입해 살펴보겠습니다.&lt;/p&gt;
&lt;h4 id=&quot;how-이-상황-어떻게-해결할-수-있을까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-%EC%9D%B4-%EC%83%81%ED%99%A9-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;how 이 상황 어떻게 해결할 수 있을까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How? 이 상황, 어떻게 해결할 수 있을까요?&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;1. MongoClient WriteConcern 옵션을 변경하여 정확도를 높여 조정한다면?&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WriteConcern을 MAJORITY로 설정하면,  Secondary의 과반수 이상에 데이터가 저장된 후에 다음 프로세스가 진행됩니다.&lt;/li&gt;
&lt;li&gt;이 방법은 정확성을 높일 수 있지만, 모든 요청에 대해 Secondary 과반수 ACK 응답을 기다리면 큰 오버헤드를 발생시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;&quot;정확하게 하자!&quot;는 의도가, 성능을 떨어뜨릴 위험이 있다는 점을 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;2. MongoClient Read Preference 옵션을  PRIMARY 로 설정한다면?&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ReadPreference를 PRIMARY로 설정하여,  모든 읽기 트래픽을 PRIMARY에 집중 시킬 수 있습니다.&lt;/li&gt;
&lt;li&gt;하지만, 이 경우 SECONDARY 노드가 유휴 상태로 남아 리소스를 낭비할 수 있습니다.&lt;/li&gt;
&lt;li&gt;SECONDARY 노드가 자꾸 놀고 있는 상황을 피하고 싶다면, PRIMARY만을 우선하는 설정은 완벽한 해결책이 되지 않겠죠?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;solution-우리의-해결책-이렇게-정리했습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#solution-%EC%9A%B0%EB%A6%AC%EC%9D%98-%ED%95%B4%EA%B2%B0%EC%B1%85-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%A0%95%EB%A6%AC%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;solution 우리의 해결책 이렇게 정리했습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Solution 우리의 해결책, 이렇게 정리했습니다!&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Bean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mongoTransactionManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;transactionManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dbFactory&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MongoDatabaseFactory&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MongoTransactionManager &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; transactionOptions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; TransactionOptions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 트랜잭션이 선언되어 있다면, PRIMARY에서 읽어야 합니다!&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readPreference&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ReadPreference&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;primary&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;MongoTransactionManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dbFactory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; transactionOptions&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;트랜잭션 상황에서만 특별히 PRIMARY를 고집하기로 했습니다.&lt;/br&gt;
그 이유는 아래 두 가지로 딱 정리할 수 있습니다.&lt;/br&gt;&lt;/p&gt;
&lt;h4 id=&quot;-이유-1-데이터-일관성-놓칠-수-없죠-데이터-일관성-보장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4%EC%9C%A0-1-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%BC%EA%B4%80%EC%84%B1-%EB%86%93%EC%B9%A0-%EC%88%98-%EC%97%86%EC%A3%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9D%BC%EA%B4%80%EC%84%B1-%EB%B3%B4%EC%9E%A5&quot; aria-label=&quot; 이유 1 데이터 일관성 놓칠 수 없죠 데이터 일관성 보장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 이유 1. 데이터 일관성, 놓칠 수 없죠! (데이터 일관성 보장)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션에서는 가장 최신 데이터가 필요합니다.&lt;/li&gt;
&lt;li&gt;PRIMARY는 항상 실시간 데이터를 보장하니, 당연히 트랜잭션 중엔 믿고 사용할 수 있겠죠?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;-이유-2-트랜잭션은-쓰기-중심이라서요-트랜잭션-사용의-특수성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4%EC%9C%A0-2-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%80-%EC%93%B0%EA%B8%B0-%EC%A4%91%EC%8B%AC%EC%9D%B4%EB%9D%BC%EC%84%9C%EC%9A%94-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%82%AC%EC%9A%A9%EC%9D%98-%ED%8A%B9%EC%88%98%EC%84%B1&quot; aria-label=&quot; 이유 2 트랜잭션은 쓰기 중심이라서요 트랜잭션 사용의 특수성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 이유 2. 트랜잭션은 쓰기 중심이라서요! (트랜잭션 사용의 특수성)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션은 보통 쓰기 작업과 쌍으로 움직입니다.&lt;/li&gt;
&lt;li&gt;PRIMARY에서만 쓰기가 가능하니, 읽기와 쓰기를 한 곳에서 처리하는 게 훨씬 자연스럽습니다.&lt;/li&gt;
&lt;li&gt;이렇게 하면 데이터 정합성도 더 깔끔하게 유지됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 결론 &lt;/br&gt;
성능을 고려하면서 여러 DB에서 일관성(Consistency)을 요구하고자 할 때, 트랜잭션에만 옵션을 추가해보시는 건 어떨까요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;2-event-발행시-비일관성-문제-발생&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-event-%EB%B0%9C%ED%96%89%EC%8B%9C-%EB%B9%84%EC%9D%BC%EA%B4%80%EC%84%B1-%EB%AC%B8%EC%A0%9C-%EB%B0%9C%EC%83%9D&quot; aria-label=&quot;2 event 발행시 비일관성 문제 발생 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2) Event 발행시 비일관성 문제 발생!&lt;/h4&gt;
&lt;p&gt;이벤트 발행, 타이밍이 생명입니다!&lt;/br&gt;
특히 eventSender.send(request)처럼 MongoDB에 데이터를 저장하면서 SQS로 메시지를 발행하는 과정에서는 순간의 어긋남이 치명적일 수 있습니다. 코드로 문제의 원인을 파악해 볼까요?&lt;/br&gt;&lt;/p&gt;
&lt;h4 id=&quot;q-무엇이-문제였을까요-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EC%98%80%EC%9D%84%EA%B9%8C%EC%9A%94-1&quot; aria-label=&quot;q 무엇이 문제였을까요 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Q. 무엇이 문제였을까요?&lt;/h4&gt;
&lt;p&gt;goodsAddService.add(request)를 통해 데이터를 MongoDB에 저장한 후,&lt;/br&gt;
아래 코드에서 GoodsEventSender가 데이터를 SQS로 메시지를 발행하는 기능을 담당하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Event Send (SQS Publish) &lt;/span&gt;
eventSender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;문제는 여기서 발생합니다.&lt;/strong&gt; 위 메소드는 @Transactional로 감싸져 있어, 트랜잭션이 커밋되기 전에 메시지가 발행됩니다.&lt;/br&gt;
그렇다면 Consumer는 어떻게 될까요?&lt;/br&gt;
MongoDB에는 데이터가 아직 저장되지 않았는데, Consumer는 데이터를 조회하려다 Not Found Exception을 만나게 됩니다. 타이밍 하나 잘못 맞춰서 시스템이 흔들리는 셈이죠.&lt;/p&gt;
&lt;h4 id=&quot;how-이-상황-어떻게-해결할-수-있을까요-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#how-%EC%9D%B4-%EC%83%81%ED%99%A9-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EA%B2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94-1&quot; aria-label=&quot;how 이 상황 어떻게 해결할 수 있을까요 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;How? 이 상황, 어떻게 해결할 수 있을까요?&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;1. SQS 메시지 지연 전달 방식은 어떨까?&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메시지를 지연 전송하는 방법은 간단하고 기존 로직을 거의 수정하지 않아도 됩니다. 그러나, 지연 시간이 너무 짧다면 타이밍 문제가 여전히 발생할 수 있어 임시방편에 불과하다고 생각했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;2. Outbox 패턴이나 로그 테일링 패턴을 활용한 아키텍처 도입?&lt;/b&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터와 메시지를 분리하여 관리하는 좋은 방법이지만, 현재 상황에서는 설계 및 구현에 걸리는 시간과 비용이 부담됩니다. (여유가 있다면 추천하지만, 당장 불 끄는 데는 조금 과한 솔루션이죠.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;solution-우리의-해결책-이렇게-정리했습니다-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#solution-%EC%9A%B0%EB%A6%AC%EC%9D%98-%ED%95%B4%EA%B2%B0%EC%B1%85-%EC%9D%B4%EB%A0%87%EA%B2%8C-%EC%A0%95%EB%A6%AC%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4-1&quot; aria-label=&quot;solution 우리의 해결책 이렇게 정리했습니다 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Solution 우리의 해결책, 이렇게 정리했습니다!&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Component&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SellerRegistrationEventPublisher&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; processStepFactory&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; SellerProcessStepFactory&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Async&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@TransactionalEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;phase &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; TransactionPhase&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;AFTER_COMMIT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestProcessedEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sendMessageDto &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SendMessageRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;withMessageBody&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;objectMapper&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeValueAsString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
 
        amazonSQS&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sendMessage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sendMessageDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
 
&lt;span class=&quot;token annotation builtin&quot;&gt;@Service&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;RequestAddServiceImpl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//나머지 선언 생략&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; applicationEventPublisher&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApplicationEventPublisher&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestAddService &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Transactional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mongoTransactionManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addRequest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requestDto&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RequestDto&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ResponseDto &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 윗부분은 위와 동일&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 커밋 후 이벤트 발행&lt;/span&gt;
    applicationEventPublisher&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;publishEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;RequestProcessedEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
 
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ResponseDto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같이 Spring의 ApplicationEventPublisher 인터페이스를 사용해 문제를 해결했습니다.&lt;/br&gt;
트랜잭션 커밋 후 이벤트가 동작하도록 했는데요. 여기서 궁금증 하나! 왜 ApplicationEventPublisher 를 사용했을까요? &lt;/br&gt;&lt;/p&gt;
&lt;h4 id=&quot;-이유-1-트랜잭션-커밋-후-발행-보장&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4%EC%9C%A0-1-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%BB%A4%EB%B0%8B-%ED%9B%84-%EB%B0%9C%ED%96%89-%EB%B3%B4%EC%9E%A5&quot; aria-label=&quot; 이유 1 트랜잭션 커밋 후 발행 보장 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 이유 1: 트랜잭션 커밋 후 발행 보장&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;TransactionPhase.AFTER_COMMIT 옵션 덕분에 데이터가 MongoDB에 확실히 저장된 후 이벤트가 발행됩니다.&lt;/li&gt;
&lt;li&gt;Consumer는 더 이상 Not Found Exception으로 고통받지 않아도 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;-이유-2--책임-분리의-미학&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4%EC%9C%A0-2--%EC%B1%85%EC%9E%84-%EB%B6%84%EB%A6%AC%EC%9D%98-%EB%AF%B8%ED%95%99&quot; aria-label=&quot; 이유 2  책임 분리의 미학 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 이유 2 : 책임 분리의 미학&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&quot;트랜잭션은 트랜잭션만, 이벤트는 이벤트만!&quot; 작업과 이벤트 발행을 분리하면 코드가 더 깔끔하고, 추후 확장도 쉽습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;-이유-3-비동기로-더-빠르게&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%9D%B4%EC%9C%A0-3-%EB%B9%84%EB%8F%99%EA%B8%B0%EB%A1%9C-%EB%8D%94-%EB%B9%A0%EB%A5%B4%EA%B2%8C&quot; aria-label=&quot; 이유 3 비동기로 더 빠르게 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🎯 이유 3: 비동기로, 더 빠르게!&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;@Async로 비동기 처리까지 추가하면, 이벤트 발행 작업이 메인 로직을 방해하지 않고 진행됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 결론 &lt;/br&gt;
트랜잭션 커밋 후에 이벤트를 발행하면 데이터 정합성도 지키고, 시스템 안정성도 챙길 수 있습니다.이제 Not Found Exception은 안녕~ 올바른 타이밍의 선택이 성공적인 시스템을 만든다는 사실, 잊지 마세요!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;-마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot; 마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;😁 마무리&lt;/h3&gt;
&lt;hr&gt;
&lt;p&gt;Spring Boot MongoDB 트랜잭션 설정과 Replica Set 도입, 그리고 성능 테스트 과정에서 겪었던 문제를 공유해 봤습니다.&lt;/br&gt;
이 경험들이 여러분의 프로젝트에도 도움이 되었길 바랍니다! 🙌&lt;/br&gt;
&lt;/br&gt;
혹시 더 좋은 아이디어나 다른 의견이 있으시다면&lt;/br&gt;
언제든 댓글로 알려주세요. 더 나은 방향을 함께 고민해봐요! 😊&lt;/br&gt;&lt;/p&gt;
&lt;h3 id=&quot;-참고자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%B0%B8%EA%B3%A0%EC%9E%90%EB%A3%8C&quot; aria-label=&quot; 참고자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📚 참고자료&lt;/h3&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/core/transactions/&quot;&gt;MongoDB Transactions - 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/reference/write-concern/&quot;&gt;Write Concern - MongoDB Manual v8.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mongodb.com/ko-kr/docs/manual/core/wiredtiger/&quot;&gt;WiredTiger - MongoDB 스토리지 엔진&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-thanks-to&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-thanks-to&quot; aria-label=&quot; thanks to permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;😝 Thanks to&lt;/h3&gt;
&lt;hr&gt;
&lt;p&gt;이 모든 과정을 함께 헤쳐나간 &lt;code class=&quot;language-text&quot;&gt;카탈로그서비스개발팀&lt;/code&gt;, 진심으로 감사드립니다.&lt;/br&gt;
여러분 덕분에 한 걸음 더 성장할 수 있었습니다. 🎉&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;앞으로도 더 재미있고 유익한 이야기로 찾아뵙겠습니다! 🚀&lt;/br&gt;&lt;/p&gt;
&lt;style&gt;
.catalog-table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 15px; 
  margin-top:5px;
}

.catalog-table th,
.catalog-table td {
  border: 1px solid #ddd;
  padding: 10px;
  vertical-align: top;
  font-size:small;
}

.catalog-table th {
  background-color: #f4f4f4;
  font-weight: bold;
}

.catalog-table tr:nth-child(even) {
  background-color: #f9f9f9;
}

.blog-post-content blockquote  {
    border-left: .25rem solid #016bf8 !important;
    color: #333e4c !important;
    background-color: #f6f9fc !important;
}
.blog-post-content ul {
    margin-left: 20px !important;
}
.blog-post-content ul li {
    padding: 3px 0 !important;
    margin: 5px 0 !important;
}

.blog-post-content ul li:before {
    height: 4px !important;
    width: 4px !important;
}

.blog-post-content hr {
   margin-top: 0px !important;
}

.blog-post-content {
   font-size: 14px !important;
}

h4{
    font-size:16px !important;
}
&lt;/style&gt;</content:encoded></item><item><title><![CDATA[디자인 시스템 중 디자인 토큰을 여러 도구를 이용하여 자동화 하는 방법]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2024-12-16/Design-System-Token-Automation/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-16/Design-System-Token-Automation/</guid><pubDate>Mon, 16 Dec 2024 18:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;-목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot; 목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;📖 목차&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot;&gt;개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%94%94%ED%94%8C%EB%A1%AF-%EA%B8%B0%EC%88%A0%EC%8A%A4%ED%83%9D&quot;&gt;디플롯 기술스택&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%94%94%ED%94%8C%EB%A1%AF-%EB%94%94%EC%9E%90%EC%9D%B8%EC%8B%9C%EC%8A%A4%ED%85%9C&quot;&gt;디플롯 디자인시스템&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B5%AC%ED%98%84%ED%95%98%EA%B3%A0%EC%9E%90-%ED%95%98%EB%8A%94-%EB%B0%A9%ED%96%A5&quot;&gt;구현하고자 하는 방향&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%A0%81%EC%9A%A9&quot;&gt;실제 적용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC&quot;&gt;결과&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot;&gt;마무리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4&quot;&gt;참고 레퍼런스&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot; aria-label=&quot;개요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개요&lt;/h2&gt;
&lt;p&gt;안녕하세요, 생소하시겠지만 올리브영 내 &lt;code class=&quot;language-text&quot;&gt;디플롯 서비스 개발팀&lt;/code&gt;에서 프론트엔드 개발을 담당하고 있는 &lt;code class=&quot;language-text&quot;&gt;노땅&lt;/code&gt; 문지훈🦊 입니다&lt;/p&gt;
&lt;p&gt;디플롯 서비스 개발팀 프론트엔드 개발자로서 디자인 담당자와 함께 고객에게 더 나은 서비스를 위해 유기적인 협업을 진행하면서 해결책 또는 방향성을 찾기 위해 노력하고 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#&quot; aria-label=&quot; permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h3 id=&quot;현재-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%98%84%EC%9E%AC-%EC%83%81%ED%99%A9&quot; aria-label=&quot;현재 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;현재 상황&lt;/h3&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2db1a174050cc27b2297308725874ff9/9a350/framework.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 22.916666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAz0lEQVR42mNggIH//xlBlMO+1mKLwpzNemoBNuaxqTPt1zfMlJ8fzwFVZQ7Ed0CqgXgXEEtAxRkZ0EHoqlBmEO18ssvDaVnjf1P76JeOk2v/u5zorAOJSxpLcgGp60B8D4o/APFSqHYmBqygvh4s4bS/fabznKb/DhubThqfqecCy7EzKEFdNg+IJwLxGahr8QCot5131ys572t777yvPRgiDBZnB+JrQPwYiNcD8RMgXoHfhUjAbnOjrdWGTl40DaAwvAnEP4G4H4il0MMQAG8QQKRAVijpAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2db1a174050cc27b2297308725874ff9/263a4/framework.webp 480w,
/static/2db1a174050cc27b2297308725874ff9/a6361/framework.webp 960w,
/static/2db1a174050cc27b2297308725874ff9/0b34d/framework.webp 1920w,
/static/2db1a174050cc27b2297308725874ff9/da28f/framework.webp 2880w,
/static/2db1a174050cc27b2297308725874ff9/b2bd4/framework.webp 3476w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2db1a174050cc27b2297308725874ff9/9aebd/framework.png 480w,
/static/2db1a174050cc27b2297308725874ff9/a91f8/framework.png 960w,
/static/2db1a174050cc27b2297308725874ff9/ac7a9/framework.png 1920w,
/static/2db1a174050cc27b2297308725874ff9/f9c26/framework.png 2880w,
/static/2db1a174050cc27b2297308725874ff9/9a350/framework.png 3476w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2db1a174050cc27b2297308725874ff9/ac7a9/framework.png&quot; alt=&quot;framework&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;p&gt;아티클 설명에 앞서 디플롯 서비스의 현 상황을 말씀드리자면 기존 디플롯은 Vue v2.x 프레임워크를 통해 개발된 Single Page Application(SPA) 입니다.&lt;/p&gt;
&lt;p&gt;현재 여러 니즈에 따라서 홈 화면과 상품 상세 화면을 &lt;code class=&quot;language-text&quot;&gt;NextJs로 SSR 환경으로 신규 마이그레이션&lt;/code&gt; 하였습니다. 당연히 내년 마이그레이션 로드맵도 그려나가고 있는 상황입니다.&lt;/p&gt;
&lt;p&gt;이번 아티클을 통해 디플롯의 신규 아키텍쳐로 마이그레이션 하면서 초기 진행했던 디자인 시스템 중 &lt;code class=&quot;language-text&quot;&gt;디자인 토큰을 자동화&lt;/code&gt;하고자 하는 니즈가 있었고 그에 따라 진행한 자동화 프로세스에 대해 디플롯 서비스는 어떻게 풀어나갔는지 소개해 드릴려고 합니다.&lt;/p&gt;
&lt;h1 id=&quot;-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-1&quot; aria-label=&quot; 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h3 id=&quot;해당-프로세스-도입-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EB%8B%B9-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EB%8F%84%EC%9E%85-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;해당 프로세스 도입 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;해당 프로세스 도입 이유&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;매번 디자인이 변경될 때마다 해당 영역을 전부 찾아서 변경된 스타일을 일일이 적용해주는 경험을 다들 해봤을 터인데 이 때 발생하는 &lt;code class=&quot;language-text&quot;&gt;사이드이펙트&lt;/code&gt;가 경험상 항상 발생했습니다.&lt;/li&gt;
&lt;li&gt;디자인 담당자가 피그마에 해당 디자인 토큰을 업데이트 하면 &lt;code class=&quot;language-text&quot;&gt;변경사항이 자동으로 Github PR로 생성&lt;/code&gt;되어 개발자가 변경사항을 확인하는데 매우 용이하다고 생각했습니다.&lt;/li&gt;
&lt;li&gt;신규 아키텍쳐로 설계하는 과정에서 성능 좋고 사용성 좋은 &lt;code class=&quot;language-text&quot;&gt;Zero Runtime CSS-IN-JS&lt;/code&gt; 라이브러리를 사용해보자는 니즈가 있었고 이런 프로세스에 대한 지원을 잘해주고 있어서 궁합이 좋다고 생각했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;-2&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-2&quot; aria-label=&quot; 2 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h3 id=&quot;어떤-사람에게-도움이-되는지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EC%82%AC%EB%9E%8C%EC%97%90%EA%B2%8C-%EB%8F%84%EC%9B%80%EC%9D%B4-%EB%90%98%EB%8A%94%EC%A7%80&quot; aria-label=&quot;어떤 사람에게 도움이 되는지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떤 사람에게 도움이 되는지?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;디자인이 업데이트되고 변경된 디자인에 따라 스타일을 변경해야하는데 사이드이펙트가 걱정되시는 분들 &lt;code class=&quot;language-text&quot;&gt;(자동화로 모든 해소가 되는 건 아니지만 해소가 되는 부분이 확실히 존재)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;디자인시스템을 도입할 생각이고 디자인 토큰화하여 디자인시스템을 구현하고 싶은 니즈가 있는 분들&lt;/li&gt;
&lt;li&gt;디자인 담당자가 업데이트한 내용을 코드에 직접 반영하지 않아도 알아서 적용되었으면 하는 니즈가 있는 분들&lt;/li&gt;
&lt;li&gt;디자인 토큰을 서포트하는 최신 CSS-IN-JS Library를 활용하고자 하는 니즈가 있는 분들&lt;/li&gt;
&lt;li&gt;Github Action을 통해 디자인 토큰 변환을 자동화하고 싶은 니즈가 있는 분들&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;-3&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-3&quot; aria-label=&quot; 3 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;디플롯-기술스택&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%94%94%ED%94%8C%EB%A1%AF-%EA%B8%B0%EC%88%A0%EC%8A%A4%ED%83%9D&quot; aria-label=&quot;디플롯 기술스택 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;디플롯 기술스택&lt;/h2&gt;
&lt;p&gt;위에 언급했지만 디플롯 서비스는 &lt;code class=&quot;language-text&quot;&gt;Classic&lt;/code&gt; 이라고 불리는 Vue 프레임워크로 SPA 형식으로 동작하는 부분이 있고 &lt;code class=&quot;language-text&quot;&gt;Modern&lt;/code&gt; 이라고 불리는 NextJs 기반 SSR 형식으로 동작하는 부분으로 나뉘어져 있습니다.&lt;/p&gt;
&lt;p&gt;해당 아티클 내용과 연관된 부분은 Modern 기반이니 Classic의 내용은 생략하겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;NextJs v14 (app route) (SSR 프레임워크)&lt;/li&gt;
&lt;li&gt;Typescript&lt;/li&gt;
&lt;li&gt;Yarn berry/Yarn workspace Monorepo&lt;/li&gt;
&lt;li&gt;TanStack Query (API 데이터 캐시)&lt;/li&gt;
&lt;li&gt;Jotai (글로벌 스토어)&lt;/li&gt;
&lt;li&gt;Panda CSS (Zero Runtime CSS-IN-JS)&lt;/li&gt;
&lt;li&gt;Storybook (컴포넌트 테스팅)&lt;/li&gt;
&lt;li&gt;AWS ECR/ECS/S3&lt;/li&gt;
&lt;li&gt;Datadog (APM)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;모노레포 구조로 Panda CSS 를 활용하여 생성한 디플롯 디자인 시스템 컴포넌트를 서비스 어플리케이션(현재는 디플롯 웹서비스 뿐이지만... 더욱 방대해지리라...)에서 재사용하는 방식으로 구현되어 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;-4&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-4&quot; aria-label=&quot; 4 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;디플롯-디자인시스템&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%94%94%ED%94%8C%EB%A1%AF-%EB%94%94%EC%9E%90%EC%9D%B8%EC%8B%9C%EC%8A%A4%ED%85%9C&quot; aria-label=&quot;디플롯 디자인시스템 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;디플롯 디자인시스템&lt;/h2&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c5793c46a66774b00f463924bf0ae062/0e78a/design-system-for-figma.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.041666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABCElEQVR42n2R226DMAyGef/X4F26O6R1m1RVWwMhEGDjkNAC4dB/xluR2MUsWbH8W58P8dqrRdMYdF2Hhw3DgCiK2MMwhBCCXoGI4gvFaz5N002TUm71XttaBszzvAGdcxssSRKUZYU8L5CkGlXVMCAIAsrlpJUQF8FgBjrX437Hzh7AtagoCmrokOsMpqlxOp1xODzB933ScmpQQVMjpdQPsLMG4zTtgOM4bivwelqjrGoImeDl9Q3H4zOstbh2A59qWRYYY3kj73ZrOfEfcC3MsgzvIqZJEmiKqzKHilO6vyVN82l4wrquiW4w0ZSrr7fs+34H/AuXsYK8nFGoDxSfX7+fFjLwG4hqwLiJWRZPAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c5793c46a66774b00f463924bf0ae062/263a4/design-system-for-figma.webp 480w,
/static/c5793c46a66774b00f463924bf0ae062/a6361/design-system-for-figma.webp 960w,
/static/c5793c46a66774b00f463924bf0ae062/0b34d/design-system-for-figma.webp 1920w,
/static/c5793c46a66774b00f463924bf0ae062/37970/design-system-for-figma.webp 2624w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c5793c46a66774b00f463924bf0ae062/9aebd/design-system-for-figma.png 480w,
/static/c5793c46a66774b00f463924bf0ae062/a91f8/design-system-for-figma.png 960w,
/static/c5793c46a66774b00f463924bf0ae062/ac7a9/design-system-for-figma.png 1920w,
/static/c5793c46a66774b00f463924bf0ae062/0e78a/design-system-for-figma.png 2624w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c5793c46a66774b00f463924bf0ae062/ac7a9/design-system-for-figma.png&quot; alt=&quot;design system for figma&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;아래에서 다시 설명드리겠지만 Figma로 디자인 시스템이 정의되어 있고 Figma에서 디자인 토큰을 정의 후 Figma 플러그인 &lt;a href=&quot;https://www.figma.com/community/plugin/843461159747178978/tokens-studio-for-figma&quot; target=&quot;_blank&quot;&gt;Tokens Studio For Figma&lt;/a&gt;를 활용하여 Export 및 Github Branch로의 Push까지 진행하고 있습니다.&lt;/p&gt;
&lt;p&gt;현재 진행형으로 확장되고 있고 네이티브 전환을 앞두고 네이티브향 디자인 시스템도 추가되고 있습니다. &lt;code class=&quot;language-text&quot;&gt;(11월 중 네이티브 전환 완료!)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Color, Typography, Icons, Layout, Shape, Components&lt;/code&gt; 정도로 구분되어 있고 스토리북을 통해 개발된 컴포넌트를 확인할 수 있도록 설계되어 있습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c80208a2d4cf96511a352e02ee93b7c4/b70ed/storybook-1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.79166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABbElEQVR42p1S7U7DMAzs+78NQjwEoIlJ/AM0JobGNrp2aVM3nz1st0yTmAbCUpTUSc93Zxe1aVGZDuQTTGtRHw5wPiDGhGEY8NeQtzlnFLWxDETwcUB1MPjYbOCc0wfee/R8znyWb8l3RBeBi6rpsD+0cCGDeg9rSStJdB2h3FegCeR1ucTt3b2ev9/8ACxFbh/Q+8wroGXZPgS97HunoMJU4m31jtnD/BfAukVZGQZLIOdhmgYppQmwV0+FoUjelXu8LJZHz85LZg+3Zc1gkf0KMKbRhkg4ZtZaq4CZi9Ts8Wq9uQz4yQyrEw+NMcxwlOOYsWXJxEwFXJbtustN2dU8Kg3p2FiRHRJikpUVQOQPU5dzHkdD9jE3MdUdSqQQOU66OGQ05GAo6KX8FGJkdo6bFPmcJgBowRGcJ4F6BVJAzhXXT4SbZ8LVImJr8+TPycBOfp3zTDKBi512vJivCbNVi8ddROPy8eF/4wuCwV2l7Jh8bgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c80208a2d4cf96511a352e02ee93b7c4/263a4/storybook-1.webp 480w,
/static/c80208a2d4cf96511a352e02ee93b7c4/a6361/storybook-1.webp 960w,
/static/c80208a2d4cf96511a352e02ee93b7c4/0b34d/storybook-1.webp 1920w,
/static/c80208a2d4cf96511a352e02ee93b7c4/da28f/storybook-1.webp 2880w,
/static/c80208a2d4cf96511a352e02ee93b7c4/0670c/storybook-1.webp 3440w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c80208a2d4cf96511a352e02ee93b7c4/9aebd/storybook-1.png 480w,
/static/c80208a2d4cf96511a352e02ee93b7c4/a91f8/storybook-1.png 960w,
/static/c80208a2d4cf96511a352e02ee93b7c4/ac7a9/storybook-1.png 1920w,
/static/c80208a2d4cf96511a352e02ee93b7c4/f9c26/storybook-1.png 2880w,
/static/c80208a2d4cf96511a352e02ee93b7c4/b70ed/storybook-1.png 3440w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c80208a2d4cf96511a352e02ee93b7c4/ac7a9/storybook-1.png&quot; alt=&quot;storybook 1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a6135a08620a5479320e77b5b9f6e709/553b9/storybook-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.41666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA+UlEQVR42q2T3UrEMBCF8/6vIF76QKJX3ikI/nTb5rfJJJPjzGoLFYXCbuCQEJIvJ5MTMw0Ws0+IpSGJemfU1tCYZdxxtOlalbEunIEhV+TKuLSZYXJQqburAEdxZ8NyCMgHymAGG5EWQsoN5RrAzzng9X2AiyQO++VX9s4jWItem7zuFWp49xBwcx9x+0h4mlji0kEKF1Gt4HMcsBNz/4nJWobv+SaGzMuY8HxKePOMD18QpJ7rgkIVMabdJpUeplCVQraDNIfORywpocmDxCWj5LzZL0QYp1kAde/wD9erjIviKhWJjQAzgahsQHUSxKH2v3/Ffyn4AuPdZG9y/8OHAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a6135a08620a5479320e77b5b9f6e709/263a4/storybook-2.webp 480w,
/static/a6135a08620a5479320e77b5b9f6e709/a6361/storybook-2.webp 960w,
/static/a6135a08620a5479320e77b5b9f6e709/0b34d/storybook-2.webp 1920w,
/static/a6135a08620a5479320e77b5b9f6e709/da28f/storybook-2.webp 2880w,
/static/a6135a08620a5479320e77b5b9f6e709/67b29/storybook-2.webp 3450w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a6135a08620a5479320e77b5b9f6e709/9aebd/storybook-2.png 480w,
/static/a6135a08620a5479320e77b5b9f6e709/a91f8/storybook-2.png 960w,
/static/a6135a08620a5479320e77b5b9f6e709/ac7a9/storybook-2.png 1920w,
/static/a6135a08620a5479320e77b5b9f6e709/f9c26/storybook-2.png 2880w,
/static/a6135a08620a5479320e77b5b9f6e709/553b9/storybook-2.png 3450w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a6135a08620a5479320e77b5b9f6e709/ac7a9/storybook-2.png&quot; alt=&quot;storybook 2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;h3 id=&quot;디자인-토큰&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%94%94%EC%9E%90%EC%9D%B8-%ED%86%A0%ED%81%B0&quot; aria-label=&quot;디자인 토큰 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;디자인 토큰&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Primitive Tokens, Semantic Tokens, Component-Specific Tokens&lt;/code&gt; 이렇게 세 가지로 분류됩니다.&lt;/p&gt;
&lt;p&gt;토큰을 정의하는 곳마다 사용하는 용어는 조금씩 다를 수 있지만, 기본적인 계층 구조는 동일합니다.&lt;/p&gt;
&lt;p&gt;저희는 Semantic Tokens와 Primitive Tokens로 관리하고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Primitive Tokens&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;디자인 내에서 색상, 폰트 종류, 간격 등과 같은 기본적인 값을 정의하는 토큰입니다. 해당 토큰은 다른 토큰의 기초가 되므로, 보통 참조용으로 사용을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Semantic Tokens&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;의미에 기반한 토큰으로, Primitive Tokens을 참조하여 ‘surface/primary’나 ‘text/default’처럼 의미에 따라 디자인 요소를 추상화한 토큰입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;-5&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-5&quot; aria-label=&quot; 5 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;구현하고자-하는-방향&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B5%AC%ED%98%84%ED%95%98%EA%B3%A0%EC%9E%90-%ED%95%98%EB%8A%94-%EB%B0%A9%ED%96%A5&quot; aria-label=&quot;구현하고자 하는 방향 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;구현하고자 하는 방향&lt;/h2&gt;
&lt;p&gt;제가 생각한 방향성은 아래와 같았습니다. 이 과정이 정답은 아니니 여러분은 더 좋은 방법으로 구현하실 수 있을 겁니다.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bcc41d4fe28af71f9bc30b42e73e1bd9/46dcf/flowchart.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.79166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABXUlEQVR42p2SWYvCQBCE5///MR8k4gUG431F420SxbPka2h392nBgaF7+qiuqZlQFIWOx6OwZVkqz/PP+XQ6me97v9/rcDhYHb7HibGpDzRut1ulaarhcKjJZGJF/X5fcRyr3W6rWq2qXq/budPpqNvtqtfrqVarqVKpKIoiJUliGAFkWDERMHxii8XCBjFwOp1qNpvpfD5rtVpZDpbUMXi9XltdlmUKJGiELkGKdrudrtfrn+u7T93tdpMTAdCvzsCAQ2Kz2Rhl14IzPjE2rGAJCwgsl0uLkcOfz+c/DH1So9GwJhihE0zRtdVqqdlsWs14PDZLM7qhMX0eCzShC5MGg8FnOklnA8hoNDKdYYVmxLkFg+n1voBWl8vFNCCBPrC+3+96PB52fj6fHx/7er3Mp4Yc1nP2yogJIIwAR0PiLJp/2/+WPQrofh3A/eN+BQgbNPGfj6YwBfQbwDfCbUNsyl8ldQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bcc41d4fe28af71f9bc30b42e73e1bd9/263a4/flowchart.webp 480w,
/static/bcc41d4fe28af71f9bc30b42e73e1bd9/a6361/flowchart.webp 960w,
/static/bcc41d4fe28af71f9bc30b42e73e1bd9/0b34d/flowchart.webp 1920w,
/static/bcc41d4fe28af71f9bc30b42e73e1bd9/63696/flowchart.webp 1948w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bcc41d4fe28af71f9bc30b42e73e1bd9/9aebd/flowchart.png 480w,
/static/bcc41d4fe28af71f9bc30b42e73e1bd9/a91f8/flowchart.png 960w,
/static/bcc41d4fe28af71f9bc30b42e73e1bd9/ac7a9/flowchart.png 1920w,
/static/bcc41d4fe28af71f9bc30b42e73e1bd9/46dcf/flowchart.png 1948w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bcc41d4fe28af71f9bc30b42e73e1bd9/ac7a9/flowchart.png&quot; alt=&quot;flowchart&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;p&gt;수동이라고 되어 있는 부분도 자동화하면 좋겠는데 제 능력 부족이라 몇 가지 과정만 자동화로 진행되었습니다.&lt;/p&gt;
&lt;h1 id=&quot;-6&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-6&quot; aria-label=&quot; 6 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;실제-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;실제 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;실제 적용&lt;/h2&gt;
&lt;h3 id=&quot;디자인-시스템-업데이트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8&quot; aria-label=&quot;디자인 시스템 업데이트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;디자인 시스템 업데이트&lt;/h3&gt;
&lt;p&gt;이 스텝은 디자인 담당자의 프로세스라 간략하게만 설명하겠습니다.
자세한 내용은 &lt;a href=&quot;https://docs.tokens.studio&quot;&gt;Tokens Studio for Figma Docs&lt;/a&gt;에서 확인가능 합니다.&lt;/p&gt;
&lt;p&gt;디자인 시스템을 피그마에 적용 후 &lt;code class=&quot;language-text&quot;&gt;Tokens Studio For Figma&lt;/code&gt; 플러그인을 활용하여 Tokenization을 진행합니다.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span style=&quot;display: block; width: 300px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 816px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c1dcbc46136fd9f31af951d57e49faca/07e4e/token-studio.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 160.20833333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAYAAAASYli2AAAACXBIWXMAABYlAAAWJQFJUiTwAAAEBklEQVR42o1Wy5LbRBTVlj9hE7bwBfmy7PgBqsICdlQ2hAVQxZYKVWyI4yGZeOzx+DG2ZL0sWY+WJY0P91y5UxqXnUFV193uvrr39LmPllOaCkYkTVOVKIoRhBGieAt3E+Dt8AqDdwORdxfl7WCAq6t/8fPrX+AYY1CWJVarFe7v71XW6xVc18V8Pkcl+089h8NBx5ubGxqs8PDw8GmRo51zvW1bFc4vSdM0qv9xNIKzyzLMZjOEYagbQRAgkzUa4TyKIn2pj6TvtI9yRINVte+QHFHaedN2Yx9d35DVrUXP1J1OURRwiCBNEmS7HbZxrGIRnT5ETFksFnA9D4e2wTSq8WYmMfB8sZHCidMcmamRFnskhUS73COvWkRFg0o8W1SUuq5V9vu9SK2OU9PA29UoSoM8z+HMIoM/h1MM5xHmO2CxO+A6aPDiDxfrVBwlMcaTiSr3eTzlr6oq5d5JBVVWNdiVtSBrdE6EYd4hZKCoSFSWt1OxPCdCm5PnhXrh+ZfLBWZ3U0GT6RqNJcLver1W7i5xq6kjVERxAodJfXg4RqxuUIsRGmraDh33YwkUDRNJn9O+UJeV5mQShE2cYeO5jxKaR+TI4zJHGQx75H76cJ38clQO86KE54dabtwgInpiTvEljkRIdP2k7iNj+VqunTRNMJdKubu7U2H9Ms9sEHZCNHOVKFnffOmcYSKkrsOf8XisZXN7eyuBWapBc2wKROtJEtMoDfL/qUFLEd9x6HEyGWMqxj5eXyvK6XSq3viQAlLBozMwRHLOoDXqJEkmZeRjsVxhLQjYKCwKKm23267UZI8noOHziW20rzppVuI+kloUSQvrvVNkILoy238KyqXEprDROKV6FOi62T7qf7aFkV9y+LnSIzWdQZmoZ+XjsWJ7TG7bFD7XZCmsZ4fHufRQienC1s6r4SmEGhSL8JyiPb5FSjl3HdiSpK0nEfq+r5GfSAtjStm0OUXIHNQ8/D8Ibf1eurDatutCJQ2SyEsPX2Ydk0NGmght+pzjsOwQmicR0qkeSdKCba09XmR9UaM0yLvB8nXOIPOPDWMtzTf0VhcvejrUKNsJySYSzjnaOcup2RuMNjleDbf4fWLw69jgt0knr0cGb5aiezyFdhvWKpsEmwI/SWxU2Qyq4wm+Gxh8+TLG1z9E+Or7EN/8GOHZyxBffOvh+U8JNtJTeXVoUGx+9UeLutEgHODKVfnPusJY7uEPmz3eu0akwlBkGlVoj41W04ZVQFT6sST9kJcSHXEtYxcvcrhBjI1Imcn9Euww+nuB679m8GY+4nCDMAi7ezmW9sTUCOUzjmXGzzkelVT4fiDrke7FcaTroXzm+WGM5cLHcu7D24T6+beVvSAM8R8WCKJGX/RPmQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c1dcbc46136fd9f31af951d57e49faca/263a4/token-studio.webp 480w,
/static/c1dcbc46136fd9f31af951d57e49faca/8110d/token-studio.webp 816w&quot; sizes=&quot;(max-width: 816px) 100vw, 816px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c1dcbc46136fd9f31af951d57e49faca/9aebd/token-studio.png 480w,
/static/c1dcbc46136fd9f31af951d57e49faca/07e4e/token-studio.png 816w&quot; sizes=&quot;(max-width: 816px) 100vw, 816px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c1dcbc46136fd9f31af951d57e49faca/07e4e/token-studio.png&quot; alt=&quot;token studio&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/span&gt;
&lt;/p&gt;
&lt;h3 id=&quot;git-branch-push&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#git-branch-push&quot; aria-label=&quot;git branch push permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Git Branch Push&lt;/h3&gt;
&lt;p&gt;Git에 대한 Access 권한을 얻은 뒤 Repo 설정과 브랜치를 설정하고 위에서 정의한 디자인 토큰을 JSON으로 Export 할 파일 경로를 설정하면 Figma 설정은 끝이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span style=&quot;display: block; width: 300px&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1158px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4c61d229dde8c2b23ac6dcb0337643b4/69083/figma-repo.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 141.04166666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAYAAABh2p9gAAAACXBIWXMAABYlAAAWJQFJUiTwAAACzUlEQVR42p2VSW7bQBBFdbzss8sBcoCcIMfIJkE2QZbZ5QwBsjQs25olmxopiiJFcdBQ6Vdy0bQgS0Ia+OiBzepfVb+ra2G0kslkLIvFQsIwlPl87uYTHfu+L9PpVOd5nst+v5fdbncSfMuyTGoYHAwGMhyNZDwey9PTk85ns5mOO52OtNttNXipFUVxMOj7M2WCEXrYwgpst1vdDINzoHFobRknMnU/jhxDDMDSjHuep5twCcPn3C0ZrrPiwOCZxXG7xAzEcawHbzYbqWXF9pXBY6PnEmHAEL0y9INQWq2mdLtdTYIlot/va0+25Q32J5OyWmcqlSAIZLlclvIxCa3X66tYskcNpvkLXQMumBvXxNAOVIPVGL518jUoXZ7NF3J3V9eYmeb+t6VpepBNFC01hsRrtVppfylmVUAEluSglm92mgCuF6Lu9XoqbHkOwbUugyiKDgz37seqNKqB5nRDNdvHCaGpwWi1djG8U801m025ubnR8f39vWqz0WjI7e2tgnW+czOOCZQGkU3uyg5xI6hJkujY5lVQnsCpe126nOYFR+jCWwWg6vK5pgYpX1QVaqDh8fGxrI24yRz3SdhwOFSJsQYYU6k4WA0GYeQWexq/h4cHjZkVVeIIWAe2p9VqadyZs589GCO2WhxsI5tggIyoh3a/GfMcVJ8FvrEOuPOly0maaS1LnxNjEqiWr0vV5lWWuXoNxw43iSWn1+t1ZW2yMfeqWjyuQKVBhB3HkbrDAmCMqwAZ4QFyqWb9lALUINUGjVkdpCe4FmS+XdtK2Qxc6qsPlMmCnoCTKJMUEkE+jD0nq/EQeI6hkw1Z5qZYYcU1QHLMTSu2mjjH1goptyvNcgniTPwok2InLy5f2zCqb7c/15L89U8s775M5P23qXz6FUiaqGx4d7evqsopwJIE4b43GlOf5cffUD589+Tjz5F8/u1LlsTyD4rjb0OrLJX/AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4c61d229dde8c2b23ac6dcb0337643b4/263a4/figma-repo.webp 480w,
/static/4c61d229dde8c2b23ac6dcb0337643b4/a6361/figma-repo.webp 960w,
/static/4c61d229dde8c2b23ac6dcb0337643b4/10f44/figma-repo.webp 1158w&quot; sizes=&quot;(max-width: 1158px) 100vw, 1158px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4c61d229dde8c2b23ac6dcb0337643b4/9aebd/figma-repo.png 480w,
/static/4c61d229dde8c2b23ac6dcb0337643b4/a91f8/figma-repo.png 960w,
/static/4c61d229dde8c2b23ac6dcb0337643b4/69083/figma-repo.png 1158w&quot; sizes=&quot;(max-width: 1158px) 100vw, 1158px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4c61d229dde8c2b23ac6dcb0337643b4/69083/figma-repo.png&quot; alt=&quot;figma repo&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
  &lt;/span&gt;
&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;semantic tokens/dark&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;color&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;primary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.common.white}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.10}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;background&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;primary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.100}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.90}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;radius&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;minimal&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{radius.xs}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dimension&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;rounded&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{radius.2xl}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dimension&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;semi-rounded&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{radius.sm}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dimension&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string-property property&quot;&gt;&quot;semantic tokens/light&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;color&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;primary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.100}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.80}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;background&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;primary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.10}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;secondary&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{color.neutral.20}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;color&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string-property property&quot;&gt;&quot;radius&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;minimal&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{radius.xs}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dimension&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;rounded&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{radius.2xl}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dimension&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&quot;semi-rounded&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{radius.sm}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string-property property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dimension&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같은 디자인 토큰값으로 매핑된 JSON 파일(디자인 토큰 정보)을 특정 경로에 생성하고 Git Push를 하게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;github-action-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#github-action-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;github action 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Github Action 설정&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Create &lt;span class=&quot;token constant&quot;&gt;PR&lt;/span&gt; from design&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;system to main

# design&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;system 브랜치의 tokens&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json 파일에 대한 push 감지
on&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
  push&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
    branches&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; design&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;system
    paths&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;packages/{package-name}/{file-name}.json&quot;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 예시&lt;/span&gt;

jobs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
  createPullRequest&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
    runs&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;on&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ubuntu&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;latest
    steps&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; uses&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; actions&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;checkout&lt;span class=&quot;token decorator&quot;&gt;&lt;span class=&quot;token at operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;v4&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; uses&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; actions&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;setup&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;node&lt;span class=&quot;token decorator&quot;&gt;&lt;span class=&quot;token at operator&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;v4&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          node&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;version&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;18&lt;/span&gt;
      # 디자인 파일 변환 후 생성된 파일도 push해서 main 브랜치로 병합하는 &lt;span class=&quot;token constant&quot;&gt;PR&lt;/span&gt;을 생성
      # 아래 경로나 파일명은 예시입니다&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Run Token Transformer
        run&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;
          npx token&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;transformer packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;file&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;dark&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;target&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json &lt;span class=&quot;token string&quot;&gt;&quot;semantic tokens/dark&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;primitive/Value&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;primitive/Value&quot;&lt;/span&gt;
          npx token&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;transformer packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;file&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;light&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;target&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json &lt;span class=&quot;token string&quot;&gt;&quot;semantic tokens/light&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;primitive/Value&quot;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;primitive/Value&quot;&lt;/span&gt;
          npx token&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;transformer packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;file&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;primitive&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;target&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json &lt;span class=&quot;token string&quot;&gt;&quot;primitive/Value&quot;&lt;/span&gt;
          npx token&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;transformer packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;file&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;typography&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;desktop&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;target&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json &lt;span class=&quot;token string&quot;&gt;&quot;typography/desktop&quot;&lt;/span&gt;
          npx token&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;transformer packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;file&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json packages&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;typography&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;mobile&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;target&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json &lt;span class=&quot;token string&quot;&gt;&quot;typography/mobile&quot;&lt;/span&gt;
          git config &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;global user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token string&quot;&gt;&quot;{user-name}&quot;&lt;/span&gt;
          git config &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;global user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email &lt;span class=&quot;token string&quot;&gt;&quot;{email}&quot;&lt;/span&gt;
          git add &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
          git commit &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;m &lt;span class=&quot;token string&quot;&gt;&apos;피그마 디자인 토큰 파일 변환&apos;&lt;/span&gt;
          git push
        env&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token constant&quot;&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;DESIGN_SYSTEM_ACCESS_TOKEN&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Create Pull Request
        run&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; gh pr create &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt; main &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;H&lt;/span&gt; design&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;system &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;title &lt;span class=&quot;token string&quot;&gt;&apos;💄 디자인 토큰 업데이트&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;body &lt;span class=&quot;token string&quot;&gt;&apos;디자인 토큰이 업데이트 후 변환작업을 수행했습니다.&apos;&lt;/span&gt;
        env&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token constant&quot;&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; $&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; secrets&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;DESIGN_SYSTEM_ACCESS_TOKEN&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;특정-코드-라인-설명&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8A%B9%EC%A0%95-%EC%BD%94%EB%93%9C-%EB%9D%BC%EC%9D%B8-%EC%84%A4%EB%AA%85&quot; aria-label=&quot;특정 코드 라인 설명 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;특정 코드 라인 설명&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;명령어 &lt;code class=&quot;language-text&quot;&gt;npx token-transformer&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;npx&lt;/code&gt;는 npm package runner로 npm 레지스트리에서 원하는 패키지를 실행(Execute)합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;token-transformer&lt;/code&gt;는 &lt;a href=&quot;https://www.npmjs.com/package/token-transformer&quot;&gt;https://www.npmjs.com/package/token-transformer&lt;/a&gt; 패키지로 &lt;a href=&quot;https://tokens.studio/plugin&quot;&gt;tokens studio&lt;/a&gt;에서 오픈소스로 만든 Figma 오픈소스입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run Token Transformer 태스크
&lt;ul&gt;
&lt;li&gt;업데이트된 &lt;code class=&quot;language-text&quot;&gt;tokens.json&lt;/code&gt; 파일을 특정 타입의 토큰으로 분류해주는 변환 과정과 Git 커밋 후 Push와 PR 생성까지 하는 부분입니다.&lt;/li&gt;
&lt;li&gt;토큰 변환 명령어 &lt;code class=&quot;language-text&quot;&gt;npx token-transformer {input JSON} {output JSON} {sets} {excludes}&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;input JSON: 변환할 대상 파일&lt;/li&gt;
&lt;li&gt;output JSON: 추출할 대상 파일 (존재하지 않으면 생성)&lt;/li&gt;
&lt;li&gt;sets: 변환시 참조할 Key&lt;/li&gt;
&lt;li&gt;excludes: 추출 제외할 Key&lt;/li&gt;
&lt;li&gt;자세한 내용은 &lt;a href=&quot;https://velog.io/@seo__namu/Token-Transformer-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-u6uwunqe&quot;&gt;Token Transformer 사용하기&lt;/a&gt; 해당 레퍼런스를 참고하시면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;transform-dark-tokens.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Dark Theme에 대한 Semantic Tokens의 Key Value 매핑된 JSON 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;transform-light-tokens.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Light Theme에 대한 Semantic Tokens의 Key Value 매핑된 JSON 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;transform-primitive-tokens.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;타이포그래피 관련 Tokens가 제외된 Primitive Tokens의 Key Value 매핑된 JSON 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;transform-typography-desktop-tokens.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Desktop 버전의 타이포그래피 관련 Primitive Tokens의 Key Value 매핑된 JSON 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;transform-typography-mobile-tokens.json&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;Mobile 버전의 타이포그래피 관련 Primitive Tokens의 Key Value 매핑된 JSON 입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;panda-css의-token으로-변환-및-정의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#panda-css%EC%9D%98-token%EC%9C%BC%EB%A1%9C-%EB%B3%80%ED%99%98-%EB%B0%8F-%EC%A0%95%EC%9D%98&quot; aria-label=&quot;panda css의 token으로 변환 및 정의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Panda CSS의 Token으로 변환 및 정의&lt;/h3&gt;
&lt;p&gt;위 Step에서 변환된 토큰들은 아직 Panda CSS에서 사용할 수 없는 형태입니다. 결국 우리가 사용할 사용할 수 있는 토큰으로 변환하고 바인딩하는 과정이 필요했습니다.&lt;/p&gt;
&lt;p&gt;저희는 Panda CSS를 사용하고 있기에 &lt;a href=&quot;https://panda-css.com/docs/theming/tokens&quot;&gt;Panda CSS Tokens&lt;/a&gt; 문서를 보고 Panda CSS가 지원하는 형태로 변경하는 과정을 거치기로 판단했습니다.&lt;/p&gt;
&lt;p&gt;디플롯 웹 소스에서 디자인시스템(모노레포 환경)이 빌드되는 과정에서 해당 프로세스를 거치게 되는데요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Panda CSS가 해석할 수 있는 토큰 형태로 변환 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Panda CSS에서 지원하는 형태의 토큰 카테고리 Key&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; keys&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TokenCategory&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;aspectRatios&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;zIndex&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;opacity&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;colors&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;fonts&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;fontSizes&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;fontWeights&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;lineHeights&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;letterSpacings&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;sizes&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;shadows&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;spacing&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;radii&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;borders&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;borderWidths&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;durations&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;easings&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;animations&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;blurs&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;gradients&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;breakpoints&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token string&quot;&gt;&apos;assets&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; defaultInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  dark&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  light&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  primitive&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  mobileTypo&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  desktopType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 위 각 토큰 카테고리마다 defaultInfo 형태로 구조를 만들어 준다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; defaultMap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; keys&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;acc&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cur&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  acc&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cur&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cloneDeep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;defaultInfo&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; acc&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 이전 Step 에서 변환과정을 거쳤던 각 디자인 토큰 JSON을 Panda CSS가 해석할 수 있는 Key의 형태로 변환해주는 과정을 거친다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; tokenInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTransformedPrimitiveTokens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;defaultMap&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; primitiveTokens&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
tokenInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTransformedDarkTokens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; darkThemeTokens&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
tokenInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTransformedLightTokens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; lightThemeTokens&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
tokenInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTransformedTypoDesktopTokens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; typoDesktopTokens&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
tokenInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTransformedTypoMobileTokens&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; typoMobileTokens&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Panda CSS Token으로 사용할 완전체 형태를 만들어 준다. (계속 디자인 시스템 확장 중)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tokens &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  colors&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;primitive &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    dark&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dark &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    light&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;light &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  spacing&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;spacing&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;primitive &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;spacing&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dark &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  radii&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;radii&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;primitive &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;radii&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dark &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  opacity&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;opacity&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;primitive &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  fontSizes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    desktop&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fontSizes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    mobile&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fontSizes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mobile &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  fontWeights&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    desktop&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fontWeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    mobile&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fontWeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mobile &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  lineHeights&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    desktop&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lineHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    mobile&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lineHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mobile &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  fonts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    desktop&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    mobile&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mobile &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  shadows&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    dark&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shadows&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dark &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    light&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tokenInfo&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shadows&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;light &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 해당 토큰을 Panda CSS의 설정파일에 추가만 해주면 된다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; tokens &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;토큰 변환 helper 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 각 변환과정에서 재사용할 기능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;recursion&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;obj &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;isEmpty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; rv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  entries&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; replaceKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; &apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;-&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      rv&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;replaceKey&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;description &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      rv&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;replaceKey&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;recursion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; rv&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Primitive 디자인 토큰을 Panda CSS 가 해석할 수 있는 토큰 Key 값으로 바인딩한다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getTransformedPrimitiveTokens&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;infoMap&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; infoMap&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; clone &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cloneDeep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;infoMap&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; keys &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; rv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;recursion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  keys&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; targetKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;color&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        targetKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;colors&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;alpha-color&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        targetKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;colors&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;radius&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        targetKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;radii&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;spacing&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;opacity&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        targetKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      clone&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;targetKey&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;primitive &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;clone&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;targetKey&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;primitive &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;rv&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; clone&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Desktop 버전의 타이포그래피 디자인 토큰을 Panda CSS 가 해석할 수 있는 토큰 Key 값으로 바인딩한다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getTransformedTypoDesktopTokens&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;infoMap&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;input &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;infoMap&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; infoMap&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; clone &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cloneDeep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;infoMap&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; rv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;recursion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  clone&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fonts&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;family&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// semantic&lt;/span&gt;
  clone&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fontWeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;weight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  clone&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fontSizes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  clone&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lineHeights&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;desktop &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;font&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;line-height&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; clone&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;//원하는 방향의 기능들을 생각하신대로 구현하면 됩니다.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;개발시-사용할-수-있도록-panda-css-설정-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C%EC%8B%9C-%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8F%84%EB%A1%9D-panda-css-%EC%84%A4%EC%A0%95-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;개발시 사용할 수 있도록 panda css 설정 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개발시 사용할 수 있도록 Panda CSS 설정 적용&lt;/h3&gt;
&lt;p&gt;Panda CSS는 설정파일이 &lt;code class=&quot;language-text&quot;&gt;panda.config.ts&lt;/code&gt; 또는 &lt;code class=&quot;language-text&quot;&gt;js&lt;/code&gt; 로 되어 있습니다.
해당 파일에 설정해주시면 됩니다. 아주 간단하죠?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; definePreset &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@pandacss/dev&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; preset &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;definePreset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  staticCss&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    css&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;staticCss&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Useful for theme customization&lt;/span&gt;
  theme&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    extend&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// extend 일 경우 Panda CSS가 기본 제공해주는 토큰들도 함께 사용할 수 있다. 직접 구현한 토큰들만 사용하고자 할 경우에는 extend: {} 를 제외하고 theme: tokens 로 정의하면 된다.&lt;/span&gt;
      tokens&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 정의한 토큰을 바인딩.&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;자세한 내용은 &lt;a href=&quot;https://panda-css.com/docs/theming/tokens&quot;&gt;Panda CSS Tokens&lt;/a&gt; 참고하시면 됩니다.
요즘은 이런 token support 기능이 있는 CSS-IN-JS가 많아서 구현이 편해진 것 같아요.&lt;/p&gt;
&lt;h1 id=&quot;-7&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-7&quot; aria-label=&quot; 7 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과&lt;/h2&gt;
&lt;p&gt;Panda CSS 빌드 후 결과물을 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ColorToken&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;current&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;black&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;white&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;transparent&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.800&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.900&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rose.950&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.800&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.900&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pink.950&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.800&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.900&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fuchsia.950&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.800&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.900&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;purple.950&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.800&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.900&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;violet.950&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.800&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.900&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;indigo.950&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.50&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.100&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.200&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.300&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.400&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.500&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.600&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sky.700&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RadiusToken&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;full&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;minimal&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rounded&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;semi-rounded&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OpacityToken&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;5&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;6&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FontSizeToken&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;4xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;5xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;6xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;7xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;8xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;9xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.display-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.display-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.display-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.display-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.display-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.display-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-xl&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FontWeightToken&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;thin&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;extralight&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;light&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;normal&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;medium&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;semibold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;bold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;extrabold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;black&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.regular&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.semibold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.bold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.bold-italic&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.regular-italic&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.semibold-italic&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.regular&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.semibold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.bold&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.bold-italic&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.regular-italic&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.semibold-italic&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LineHeightToken&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;none&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;tight&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;snug&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;normal&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;relaxed&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;loose&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.display-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.display-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.display-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.heading-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.body-xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;desktop.label-xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.display-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.display-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.display-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.heading-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-sm&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-xs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.body-xl&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-md&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-lg&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mobile.label-xl&quot;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Tokens&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  aspectRatios&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AspectRatioToken
  borders&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BorderToken
  easings&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; EasingToken
  durations&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DurationToken
  letterSpacings&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LetterSpacingToken
  blurs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BlurToken
  sizes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; SizeToken
  animations&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AnimationToken
  colors&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ColorToken
  spacing&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; SpacingToken
  radii&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RadiusToken
  opacity&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; OpacityToken
  fontSizes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FontSizeToken
  fontWeights&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FontWeightToken
  lineHeights&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LineHeightToken
  fonts&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FontToken
  shadows&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ShadowToken
  breakpoints&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BreakpointToken
  animationName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; AnimationName
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;token&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;never&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TokenCategory&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;aspectRatios&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;zIndex&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;opacity&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;colors&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fonts&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fontSizes&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;fontWeights&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lineHeights&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;letterSpacings&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;sizes&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;shadows&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;spacing&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;radii&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;borders&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;borderWidths&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;durations&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;easings&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;animations&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;blurs&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;gradients&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;breakpoints&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assets&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같이 정의한 디자인 토큰 Key와 Value 타입이 제대로 생성이 되었는지를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;실제 사용할 디자인 토큰은 일부분이지만 어떻게 변환되었는지 볼까요?&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1550px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6734dd4a3b5ea45f9801c018f6988033/3e79e/token-result-1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.95833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAADH0lEQVR42k2U3W7bZBjHew8cMG1FS5c0iZ34M3Zsx4ntNF8tQ4gJCt0HmpC4BKRpt4AmpknTdjAh7oCT9WOgbmu6tknREHAR7ICknDe0+u1xso5aevX6fd7Xf/+e//PYc8fHxxwMh+zu7TGQeft5n+cv+uz0d6ex3X6f9adP2dz6lZc7r2Sk8VfsDQ4Yyvl0vb29w/7+kMlkwtw/oxH1VpMwSbh562uyxRL5kk6xrKG6Nh2/xqdeSFbVyKs6i0qZkm0QRBE3V2+haCYLBYV60mI8PmJuPB4TtdqYTpVYgrmCJsOgpFvojoMr8arlslhMxQyKJQOtYmO5PlHYlLXF5axKPe5wdPQvc6ORCHbahFHMjbUbQmeg6jaaZVGuOiRejW41pCAkqmGjlE00xyaoNfjq41XKZoVFoY9bPRGcEo6Iux1cP2Cl05NNg4JqykFLHqzgOB6uPSPMp4RlA92VuBC2vUReYLGwqBI1zwhFMOmJYBCw3FuhIITKlNDG8KvU/ZCGG1AQspQ89cyoVqiKt8thl5IhhMoZ4TvBSAjDoMb15avk3pGUDAvTc/E8H18o8yl5yZx6aHgOQbXGNbeFqs0IG8k5wqjdRjdtGkGdohAoZx7aFbT0XugUeVCRQqmyr9lCb7rUjGBGeN7DVDBe7lIXkrV4idyUxKAsKZelmlc/u8YXX14XCmknZUaoS8p1efnt6BMUqXImJx6eJ2yKoGVViNPUJK00NV3WduARxokYLqkJSdl0hNAWKxwcKWIziIXQJiceRkvds7YRwl4XTzY+r/jSHtY0jZyicim7QD1K6LRX+ODCPB/OZ5i/nJ156AWshp1p2+TUc0VJ26a+1MIUf1rSCmlRPsoUuP3Ntzx89Jjv793n3g8PpuPBw8d8d+cuimFgVTx82xMLTDLS2I3/G3tEo9smsB3WwoSCCF/MZHny40+k18nJyWw+nc0HgwEXr2QwpYhdq8aVvMIlOV+tRYzST2/y34Q///iL/cEh/eEhr1//zvDwN/5+8+a94Onp6Xvh1PONjU3WN5/x8/omW89+YWNrixcv+6Q/mrfNnlRFIizA9gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6734dd4a3b5ea45f9801c018f6988033/263a4/token-result-1.webp 480w,
/static/6734dd4a3b5ea45f9801c018f6988033/a6361/token-result-1.webp 960w,
/static/6734dd4a3b5ea45f9801c018f6988033/db250/token-result-1.webp 1550w&quot; sizes=&quot;(max-width: 1550px) 100vw, 1550px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6734dd4a3b5ea45f9801c018f6988033/9aebd/token-result-1.png 480w,
/static/6734dd4a3b5ea45f9801c018f6988033/a91f8/token-result-1.png 960w,
/static/6734dd4a3b5ea45f9801c018f6988033/3e79e/token-result-1.png 1550w&quot; sizes=&quot;(max-width: 1550px) 100vw, 1550px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6734dd4a3b5ea45f9801c018f6988033/3e79e/token-result-1.png&quot; alt=&quot;token result 1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3bcb6543cc8d79b783876f95dde614c7/78929/token-result-2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 61.04166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAACLUlEQVR42p2SW2sTQRTH8+6LL32qMc1ld5PMXia7m0ureREUsUqxTduk0UJDi34QS1ERfBYtFT+Osc21uTRJI1QLebIvCViSvzOTC6UEEQ8czuzsmd/5zznjOG23UalWcZTLI5cvoHxcQbVWRb1RR75QwOFRDseVKmr1E5w0GsixvFKpjFqtzvbqKFcqyGYPkc8X0WqdwrG6kYSsqlBDJqSgDrdPBTEozFiY7dnwqxRSwEDQCCEUi7BIIbM8KaBDsy3hfo3C6ZGwtLwOx1p6A35dBw3zZBMKoSLSaAS6bYu1Xw2JgkY4DEKH3zyP/9ctGyq1cNuj4OlKEo5EKgmJDBUqqgGvoomKPFHAyFAhB/EiPHKYEjREjsacGAzoHQFTm8/ZpgVrfoFVCjOAxaLNDkehmTYCGgdwhRZCDKiZ1kg1hcEUctVCIQcmGLDZaqLImsyHUSiW0GSN/fDpI1yyBF+QYM6nYNblgy9AQEwTMtHhkYlwLkQo1E043SOFuGL9fl/Er9+yeLaVwdbOC2xmtpHZfomHj58gGo8jejcOO3YHZmQBNBKZXNk1Vsgh130wGEyKjNe/Li5wdvYDP8/P0el00P7exlIiAUVj02avYTIUTDEOmeZXC/R6PSwn1yGpmuj51Cv/zcbQcVu63S5W0ykE2LukdgxuOYiVtfS/A6+3gAPvLz7CzC0nZue8uHFzBvceLP4/8PLyN/Y/H2B37w3evnuPV3uvsX/wBX8AxuJ2Rju22dUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3bcb6543cc8d79b783876f95dde614c7/263a4/token-result-2.webp 480w,
/static/3bcb6543cc8d79b783876f95dde614c7/a6361/token-result-2.webp 960w,
/static/3bcb6543cc8d79b783876f95dde614c7/0b34d/token-result-2.webp 1920w,
/static/3bcb6543cc8d79b783876f95dde614c7/8beca/token-result-2.webp 2546w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3bcb6543cc8d79b783876f95dde614c7/9aebd/token-result-2.png 480w,
/static/3bcb6543cc8d79b783876f95dde614c7/a91f8/token-result-2.png 960w,
/static/3bcb6543cc8d79b783876f95dde614c7/ac7a9/token-result-2.png 1920w,
/static/3bcb6543cc8d79b783876f95dde614c7/78929/token-result-2.png 2546w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3bcb6543cc8d79b783876f95dde614c7/ac7a9/token-result-2.png&quot; alt=&quot;token result 2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/p&gt;
&lt;p&gt;위와 같이 이쁘게 토큰화가 되었습니다. 이쁘죠잉?🦊&lt;/p&gt;
&lt;h1 id=&quot;-8&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-8&quot; aria-label=&quot; 8 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;개인적으로 해당 프로세스를 설계하고 실무에 사용하면서 느낀 점을 조금 남겨보자면&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;미비한 타입 정의 (타입스크립트)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;타입 정의를 명확하게 하여 보는 분들로 하여금 어색하거나 애매한 부분이 없도록 구현하고 싶었지만 역시 타입스크립트를 게을리한 죄로 명확한 타입을 정의하지 못하였습니다. 이 부분을 계기로 타입스크립트 핸드북을 참고하긴 했지만 타입 정의에 대한 노력을 끊임없이 &lt;del&gt;해야 할 것 같다고&lt;/del&gt; 아니 하겠다고 다짐했습니다.&lt;/p&gt;
&lt;p&gt;여러분도 개인적인 프로젝트라도 타입 정의를 게을리 하지 마시고 챌린지 해보기시길 바랍니다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;자동 완성 부재&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; border&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;1px solid token(colors.red.400)&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위에 결과물과 같이 Value가 모두 string 타입으로 구현되어 있고 Panda CSS를 활용해 style을 정의할 때 string으로 구현하다보니 자동완성이 되지 않는 아주 불편한 DX(개발자 경험)를 몸소 느꼈습니다. 내부적으로 해당 토큰을 사용할 때 객체 또는 특정 타입으로 읽어드릴 수 있도록 미들웨어나 기능을 구현해서 사용해보도록 하는 것을 목표로 하여 더 구체적으로 타입 추론과 자동완성이 되도록 리팩토링 해보겠습니다.&lt;/p&gt;
&lt;h1 id=&quot;-9&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-9&quot; aria-label=&quot; 9 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/h1&gt;
&lt;h2 id=&quot;참고-레퍼런스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4&quot; aria-label=&quot;참고 레퍼런스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;참고 레퍼런스&lt;/h2&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.tokens.studio&quot;&gt;Tokens Studio for Figma Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@seo__namu/Token-Transformer-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-u6uwunqe&quot;&gt;Token Transformer 사용하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://panda-css.com/docs/theming/tokens&quot;&gt;Panda CSS Tokens&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.npmjs.com/package/token-transformer&quot;&gt;token-transformer NPM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;</content:encoded></item><item><title><![CDATA[올리브영 초대량 쿠폰 발급 시스템 개선기]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2024-12-11/oliveyoung-coupon-mess-issue/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-11/oliveyoung-coupon-mess-issue/</guid><pubDate>Wed, 11 Dec 2024 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 쿠폰 스쿼드 포덕입니다.&lt;/p&gt;
&lt;p&gt;이전 게시글에서는 고객분들의 많은 불편사항이었던 선착순 쿠폰의 개선에 대한 내용이었습니다.&lt;/p&gt;
&lt;p&gt;(&lt;a href=&quot;https://oliveyoung.tech/2023-09-18/oliveyoung-coupon-rabbit/&quot;&gt;이전 작성글 참고&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;그리고, 이전 작성글 말미에, 대량쿠폰도 같이 개선했다는 작은 문장이 있었습니다.&lt;/p&gt;
&lt;p&gt;올리브영 쿠폰은 RabbitMq를 이용한 발급 시스템 구성을 하였고, Rabbit의 유연한 동작의 핵심인 Exchange 가운데에서도, Direct Exchange를 사용했습니다.&lt;/p&gt;
&lt;h2 id=&quot;여기서-exchange란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%AC%EA%B8%B0%EC%84%9C-exchange%EB%9E%80&quot; aria-label=&quot;여기서 exchange란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;여기서 Exchange란?&lt;/h2&gt;
&lt;p&gt;RabbitMq는 Publish, Exchange, Route, Queue, Consume 에 의해 동작합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Publish&lt;/strong&gt; 는 메시지를 전송하고 &lt;br&gt;
&lt;strong&gt;Exchange&lt;/strong&gt; 는 메시지를 수신하고 &lt;br&gt;
&lt;strong&gt;Route&lt;/strong&gt; 는 특정 Routing Key를 가지고, binding된 &lt;strong&gt;Queue&lt;/strong&gt; 를 찾아가서 메시지를 적재합니다. &lt;br&gt;
그리고 적재된 메시지를 &lt;strong&gt;Consume&lt;/strong&gt;을 통해, 처리할 수 있습니다. &lt;br&gt;&lt;/p&gt;
&lt;p&gt;여기서 RabbitMq는 Exchange라는 특징을 통해 다양한 처리방식을 지원합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Direct Exchange&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1779px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ec03d058f36e4d2b6490e206bd10f8a0/015d4/Direct.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 11.666666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAjElEQVR42h3MwQ2CMBiAURbVHYwXRzBu4AI6Che9kpQ2gRIg8dRqFNq/wCc6wHvZ9nBmf7oyhIByitzkaK9JS2KaJ6y3FI+CuERSTLjkqJ4V7dAiixBSoB5rlFfY0ZJt1nB3vDDEEfMy3Jo72mlkFiQJ3bvDePMPJQhOHM2noQ89cY6Mq/tFpS+xwfIFX5iR6F0E7agAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ec03d058f36e4d2b6490e206bd10f8a0/263a4/Direct.webp 480w,
/static/ec03d058f36e4d2b6490e206bd10f8a0/a6361/Direct.webp 960w,
/static/ec03d058f36e4d2b6490e206bd10f8a0/507e1/Direct.webp 1779w&quot; sizes=&quot;(max-width: 1779px) 100vw, 1779px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ec03d058f36e4d2b6490e206bd10f8a0/9aebd/Direct.png 480w,
/static/ec03d058f36e4d2b6490e206bd10f8a0/a91f8/Direct.png 960w,
/static/ec03d058f36e4d2b6490e206bd10f8a0/015d4/Direct.png 1779w&quot; sizes=&quot;(max-width: 1779px) 100vw, 1779px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ec03d058f36e4d2b6490e206bd10f8a0/015d4/Direct.png&quot; alt=&quot;Direct&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;특정 Exchange 키 값으로 메시지를 전송합니다. Exchange는 작성된 Routing Key에 binding된 Queue로 메시지를 적재합니다.&lt;/p&gt;
&lt;p&gt;이 값은 Default Exchange 값에 해당합니다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;Topic Exchange&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1779px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6433f9f36c674117507e8c6c48482127/015d4/Topic.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 11.666666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAYAAABYBvyLAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAj0lEQVR42h3MMQ6CMBhAYe/p4BVcjIkXcPACxtnBm/QIQMtACZC4QEmA/qXA0zC9fMs7nG4vLo8PPgi606hcYZwhbpFlXbDOknwTZBNiiLjoKPqCeqqZt5kQA9ZbtNNYsRyO1yfn+5vRT5jeoIwiazMkCmEOVENF3uXI+rcPtKGlHEoaaZBFGGXch2mX7v0BXaGRtzUcCtcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6433f9f36c674117507e8c6c48482127/263a4/Topic.webp 480w,
/static/6433f9f36c674117507e8c6c48482127/a6361/Topic.webp 960w,
/static/6433f9f36c674117507e8c6c48482127/507e1/Topic.webp 1779w&quot; sizes=&quot;(max-width: 1779px) 100vw, 1779px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6433f9f36c674117507e8c6c48482127/9aebd/Topic.png 480w,
/static/6433f9f36c674117507e8c6c48482127/a91f8/Topic.png 960w,
/static/6433f9f36c674117507e8c6c48482127/015d4/Topic.png 1779w&quot; sizes=&quot;(max-width: 1779px) 100vw, 1779px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6433f9f36c674117507e8c6c48482127/015d4/Topic.png&quot; alt=&quot;Topic&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;특정 Exchange 키 값으로 메시지를 전송합니다. Exchange는 작성된 Routing Key에 binding된 Queue로 메시지를 적재합니다.&lt;/p&gt;
&lt;p&gt;여기서 Direct방식과 다른 점은 Exchange의 키 값을 Pattern으로 작성 가능합니다.&lt;/p&gt;
&lt;p&gt;해당 Pattern에 정확히 일치하거나, 포함되는 모든 곳에 메시지를 적재합니다.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;Fanout Exchange&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1367px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2a400d3ef4647240124fc78f1acd2c04/44e21/Fanout.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.12500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAC4jAAAuIwF4pT92AAABtElEQVR42o1S207bQBD1H/ED/Yq+90Oq9rHv7V8gte+V+k4UBQkkYglZhsTxBZJgY3sdZ9d7PXg2LVQgAbM6Gu3aOztzzgnwjjDj6kx3gO1gnPHn3HGkMkWhCxRmhCoQOOdeLSb2Auv9GvE+xmw1Q9zHqHUNPWhs2RYLvsB5fo75Zo5MZQjokrUOLwr/3Q7DAGEEGtOgkpUvJp3033vd+64yniETGW71LYI/kxC7sQuKnd2h0Y0HswzKKWiln73z9DAVLk2JxjWobY3KVAiOPn7Flx+//K8LtsR0McVJdIKwDNHpDqIfu5MNUpEiqiJkQ+a5dNqhEQ2WfImojhCz+DDyh0/f8Pn7Txgtcd1e4zQ9xSSe4GJ7ASYZetaj5CVWYoX5eo6EJ2CGwUiDqq+QiAThJsRleYlc5QiOf09Rj5coSMHWtmhNC+aYH4lGpjHtuP7Pnl83YKM3uHf3qGyFO313EIUE8aK4JzEo05mSI49O+8eoM8oaB1655chljkIWHjfqBoE2Fq9Zh/fc2+Zqf4VZMvOZlFZCedsQh2f5mafj0TZvBaldm/oRtP9nbCpC3BHI2A995fsO0B29xQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2a400d3ef4647240124fc78f1acd2c04/263a4/Fanout.webp 480w,
/static/2a400d3ef4647240124fc78f1acd2c04/a6361/Fanout.webp 960w,
/static/2a400d3ef4647240124fc78f1acd2c04/883d7/Fanout.webp 1367w&quot; sizes=&quot;(max-width: 1367px) 100vw, 1367px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2a400d3ef4647240124fc78f1acd2c04/9aebd/Fanout.png 480w,
/static/2a400d3ef4647240124fc78f1acd2c04/a91f8/Fanout.png 960w,
/static/2a400d3ef4647240124fc78f1acd2c04/44e21/Fanout.png 1367w&quot; sizes=&quot;(max-width: 1367px) 100vw, 1367px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2a400d3ef4647240124fc78f1acd2c04/44e21/Fanout.png&quot; alt=&quot;Fanout&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;특정 Exchange 키 값으로 메시지를 전송합니다. Routing Key에 상관없이 binding된 모든 Queue로 메시지를 적재합니다.&lt;/p&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;strong&gt;Header Exchange&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1403px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1155ee4e8ea523fc54867ab5c4d97a9f/d4ca1/Header.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 14.791666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAyElEQVR42jWNQWrCQABFcyc3vYfgDXqJUvEGQnuAQte6Le5cCa4UQiDSVScZoxgNGUzqzGScvOJAP3w+PD7/R6/vc54nH1R1w0OlO5OeU5JjQm5ybG8Df6RyirqraX3Lv0pXktscaSX7bk/0NJowGI6Zfa1D4UcJVt8rlvGSXbXjdr/RNi0HcyCpEjbFBmEF2unAC12QXlK2covQgmj6ueDlbU59/Q2DJ3cKQ3ERh2fjDXjQXqPuKrjxDX3f470P/cxkZDpDdpI/TavaRShyYBoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1155ee4e8ea523fc54867ab5c4d97a9f/263a4/Header.webp 480w,
/static/1155ee4e8ea523fc54867ab5c4d97a9f/a6361/Header.webp 960w,
/static/1155ee4e8ea523fc54867ab5c4d97a9f/5f431/Header.webp 1403w&quot; sizes=&quot;(max-width: 1403px) 100vw, 1403px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1155ee4e8ea523fc54867ab5c4d97a9f/9aebd/Header.png 480w,
/static/1155ee4e8ea523fc54867ab5c4d97a9f/a91f8/Header.png 960w,
/static/1155ee4e8ea523fc54867ab5c4d97a9f/d4ca1/Header.png 1403w&quot; sizes=&quot;(max-width: 1403px) 100vw, 1403px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1155ee4e8ea523fc54867ab5c4d97a9f/d4ca1/Header.png&quot; alt=&quot;Header&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;특정 Header 값과 함께 메시지를 전송합니다.&lt;/p&gt;
&lt;p&gt;Routing Key에 상관없이, 전달받은 Header값이 binding시 지정된 값과 일치한다면, 메시지를 적재합니다.&lt;/p&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;이 가운데, 대량발급에서도 Direct Exchange가 사용되었으며, 이로 인해 발생했던 문제와 개선에 대해 다뤄보려 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;도입-배경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EC%9E%85-%EB%B0%B0%EA%B2%BD&quot; aria-label=&quot;도입 배경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;도입 배경&lt;/h2&gt;
&lt;br&gt;
올리브영에는 선착순 쿠폰만큼 중요한 쿠폰이 있습니다. 
&lt;p&gt;바로 &lt;strong&gt;멤버십 승급 쿠폰&lt;/strong&gt;🫡 입니다.&lt;/p&gt;
&lt;p&gt;올리브영은 굉장히 많은 고객님들께서 사용하고 계십니다.&lt;/p&gt;
&lt;p&gt;예시로 가장 많은 BABY 등급만 해도 1천 만명 이상이 있으며,&lt;/p&gt;
&lt;p&gt;해당 멤버십 등급의 고객님들에게 쿠폰을 발급하는 과정에서만, &lt;strong&gt;12~15시간&lt;/strong&gt;이 소요되고 있었습니다. (다른 등급까지 생각한다면..😱)&lt;/p&gt;
&lt;p&gt;그리고 이러한 문제로 인해 다음과 같은 애로 사항이 발생했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;백오피스는 대량 발급뿐만 아니라 다양한 기능을 수행하고 있습니다.&lt;br&gt;
쿠폰 발급 외에도 다른 도메인과 관련된 내부 작업들이 진행되고 있습니다.&lt;br&gt;
다만, 대량 발급 작업이 이루어지게 되면, 많은 리소스를 소모하여, 다른 업무에 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;백오피스에서 발급 과정이 장기화될 경우, 시스템 변경(예: 긴급 배포)이나 다른 작업의 영향을 받아 안정적인 사용에 불편이 발생할 수 있었습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;작업 시간이 과도하게 길어지면, 해당 업무의 진행을 기다리는 내부 인력의 리소스가 불필요하게 소모됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;지나치게 긴 작업 시간으로 인해 승급과 쿠폰 발급 사이의 간격이 생길 수 있으며, 이는 쿠폰의 지연은 고객 경험에 부정적일 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;개선-필요사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0-%ED%95%84%EC%9A%94%EC%82%AC%ED%95%AD&quot; aria-label=&quot;개선 필요사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선 필요사항&lt;/h2&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/513295602ef717c65883362ce352694e/eb2a1/ASIS.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 41.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAC4jAAAuIwF4pT92AAABI0lEQVR42oWSwU7DMAyG+5wceQgOvAQ3jkjjFUCCAzeEEOKIBAcO3aqBtCJtHWnXxE7yYyfrBJNYrf5KUtuf7SjF1c0dEOULETHGtE/nGHdad4S6segdY8yK1reoNhV0VVu5Feq+Rk8WwXt0G4vj8yccnd3j/aNJMVr3X2DjGpRtCcMGCMDSLrHoFrDspOsAYsbJ5Bmnk0dU9Xoc6KWLwYIAPHmwjBb/ZOno4hN/PETbBzrn4MjBR5/gqozL9hvY94S2s2DO+V4KBtEOqIH6Y97NMTVTmT6AiaH+QY4or45wffuCi8sHvL59ppKz7xlKU+4BpQOODAqkQ+46HMz73LXGGtOj/mpAckV61jxVwXLp2D4PhQdNEqX9NjmELB1v7A5/AIxbb3faHeEzAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/513295602ef717c65883362ce352694e/263a4/ASIS.webp 480w,
/static/513295602ef717c65883362ce352694e/a6361/ASIS.webp 960w,
/static/513295602ef717c65883362ce352694e/0b34d/ASIS.webp 1920w,
/static/513295602ef717c65883362ce352694e/9db58/ASIS.webp 2800w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/513295602ef717c65883362ce352694e/9aebd/ASIS.png 480w,
/static/513295602ef717c65883362ce352694e/a91f8/ASIS.png 960w,
/static/513295602ef717c65883362ce352694e/ac7a9/ASIS.png 1920w,
/static/513295602ef717c65883362ce352694e/eb2a1/ASIS.png 2800w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/513295602ef717c65883362ce352694e/ac7a9/ASIS.png&quot; alt=&quot;ASIS&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;쿠폰 대량 발급 시스템은 구성원이 개별적으로 발급하기 어려운 상황을 해결하기 위해 만들어진 만큼, 발급 시작부터 종료까지 외부 요인의 영향을 받지 않고 안정적으로 운영되어야 합니다.&lt;br&gt;&lt;/p&gt;
&lt;p&gt;또한, 멤버십 승급과 연계된 작업이기 때문에, 고객이 쿠폰 발급과 승급 간의 시간 차이를 느끼지 않도록 신속한 처리가 중요합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;처리 방식의 변경&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;대량 발급은 올영세일의 선착순 발급에서도 사용하던 Direct exchange로 구성되어 있었습니다.&lt;/p&gt;
&lt;p&gt;발급은 지정된 Queue에 적재되며, 발급도 concurrency를 통해 처리되고 있습니다.&lt;/p&gt;
&lt;p&gt;물론 기다리면, 언젠가 처리는 되겠지만, 너무 장기간입니다.&lt;/p&gt;
&lt;p&gt;작업량이 가령 1억 건이라면? (언젠가 되지 않을까요? 😎 )&lt;/p&gt;
&lt;p&gt;낼 수 있는 최대의 성능을 내더라도, 병목이 발생할 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;가능한, 발급 대상 조회에서 pending되는 시간을 줄이고, 빠르게 발급하여,&lt;/p&gt;
&lt;p&gt;작업시간을 단축할 필요가 있었습니다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;작업 주체의 변경&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;기존에 발급 대상을 조회하고 전송하던 역할을&lt;/p&gt;
&lt;p&gt;백오피스에서 독립된 환경을 구성하고, 전송할 수 있어야 했습니다.&lt;/p&gt;
&lt;p&gt;백오피스는 운영 상황에 따라, 긴급한 변경이나, 다른 작업에서 영향이 있을 수 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 이러한 변경은 대량 발급의 안정적 동작에 변수로 작용할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;개선-진행&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%84%A0-%EC%A7%84%ED%96%89&quot; aria-label=&quot;개선 진행 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개선 진행&lt;/h2&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/485ef0edcf8e8f41dc0e2cf269779838/f557a/TOBE.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA/ElEQVR42nWQwU7DMAyG+/4PwIX3QEJM3BES4jCYhqatCI1Lt9GkSWrXcX6Slu4yZuVPHFn5/DsVciRN0Kglxcdmh6fnNZrGoo8Bjh1Ucy3llVI5pvuVqCQJLFl4DmBh+D5g8bhCXR/h1MKwBQnBS4fWM24XG3wf/fi4NLgAxhTRaw+KjJjhLgTcPyyx3TV/QDM2CuLw4wg3d+/YH7qCwz88VHMyjxHz6J9fBxjbQzCAlTEMA0Tk7Mjnpqv1FiEQokZInGpFVdk06Shc+Z8gHkbMGagZ8vJaY/m2ByfCiU6XDucgIrSthbVddjmpNSbLjjJZRDzCyzSzs9nhL69p0mCm2ipRAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/485ef0edcf8e8f41dc0e2cf269779838/263a4/TOBE.webp 480w,
/static/485ef0edcf8e8f41dc0e2cf269779838/a6361/TOBE.webp 960w,
/static/485ef0edcf8e8f41dc0e2cf269779838/0b34d/TOBE.webp 1920w,
/static/485ef0edcf8e8f41dc0e2cf269779838/da28f/TOBE.webp 2880w,
/static/485ef0edcf8e8f41dc0e2cf269779838/98b7d/TOBE.webp 3840w,
/static/485ef0edcf8e8f41dc0e2cf269779838/926d5/TOBE.webp 4917w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/485ef0edcf8e8f41dc0e2cf269779838/9aebd/TOBE.png 480w,
/static/485ef0edcf8e8f41dc0e2cf269779838/a91f8/TOBE.png 960w,
/static/485ef0edcf8e8f41dc0e2cf269779838/ac7a9/TOBE.png 1920w,
/static/485ef0edcf8e8f41dc0e2cf269779838/f9c26/TOBE.png 2880w,
/static/485ef0edcf8e8f41dc0e2cf269779838/5da7e/TOBE.png 3840w,
/static/485ef0edcf8e8f41dc0e2cf269779838/f557a/TOBE.png 4917w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/485ef0edcf8e8f41dc0e2cf269779838/ac7a9/TOBE.png&quot; alt=&quot;TOBE&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;해당 프로세스를 정리해보겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;ASIS&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;먼저 백오피스에서 대량발급을 실행합니다.&lt;/li&gt;
&lt;li&gt;내부적으로 대상을 조회하여, LOOP문을 통해 메시지를 전송합니다.&lt;/li&gt;
&lt;li&gt;메시지가 적재된 Queue를 Consume하여, 발급합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;TOBE&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이전과 동일하게, 실행은 백오피스에서 발생합니다.&lt;/li&gt;
&lt;li&gt;백오피스는 Fanout Exchange를 호출합니다.&lt;/li&gt;
&lt;li&gt;호출시, 전달한 메시지에 대량발급 ID값을 이용하여, Consume 할 때, 분기처리됩니다.&lt;/li&gt;
&lt;li&gt;Consume에서 대상을 조회하여, LOOP하여, 메시지를 전송합니다.&lt;/li&gt;
&lt;li&gt;메시지가 적재된 Queue를 Consume하여, 발급합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;변경은 크게 2가지 정도입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Exchange 변경&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;먼저 Exchange를 Fanout으로 변경하였습니다.&lt;/p&gt;
&lt;p&gt;기존의 지정된 Queue로의 적재가 아닌, Multi Queue로 적재됩니다.&lt;/p&gt;
&lt;p&gt;그리고 Exchange에 바인딩되는 Multi Queue Name은 Task를 활용하여 생성되도록 하였습니다.&lt;/p&gt;
&lt;p&gt;메시지에 대한 Consume 작업시, 동일 메시지에 대한 중복처리를 방지하기 위해,&lt;/p&gt;
&lt;p&gt;대량 발급 ID값에 따라 별도로 분기되어 처리하도록 구성하였습니다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;작업 주체의 변경&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;기존에 발급 대상을 조회하고 전송하던 역할을 백오피스에서&lt;/p&gt;
&lt;p&gt;Fanout Exchange를 받는 Trigger Worker구성을 별도로 하였습니다.&lt;/p&gt;
&lt;p&gt;Trigger Worker는 전달받은 메시지에 따라, 대상을 조회하고 발급 Worker로 전달하여,&lt;/p&gt;
&lt;p&gt;발급을 진행하게 됩니다.&lt;/p&gt;
&lt;p&gt;이로써, 백오피스에서는 대량발급시 실행만 담당하게 되고,&lt;/p&gt;
&lt;p&gt;실 대상 조회 및 발급은 별도의 시스템에서 진행하는 것으로, 동작의 안정성을 갖게 되었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;결과&lt;/h2&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 360px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d4b9832c87df9573de3e6c271a5503e3/53918/IMPROVE.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABj0lEQVR42pWTzytEURTH30YWsvbjD1D+EEs2kmFlyEKTjRkbosQCWY8yajBLKYpShMnCZlJkJiU/QtMYSnkz73nvvve+7rn3PmOYmnHr1Ot77/m87znnXk3XdTiOUxaMMRG/9WpBOZphGHBdF4wLtKL7Z+iYWUX3YgKXD1mh0R6dqRYE1UzThOd5PMkVyaOxHWhdETT2TyOZvhOaw/foTLUgaMkhkw4ja7uo751Ey+AcTjMSSHs1OyQg0R3lMMyBdT0TaArOcuD9/x3atv1/IDlymAj6LgNSD8WE1FDG4j+A6colCwDXRSjQd8m+Q1aLQ1eesW5S0A9iKBxvgL0+SrCCapZlqTukhrLOhxLgQxmqMBRmi+SPrXlkB5qRG2mDeXEoNCq/osPQyja0zjAa+qZwcnWr7qFyyJMggAvIBVvxEmrnwCOheequCod+D2glkucILCUwHN3E9XNeluz3zZVVGKk95JdDeIuPw37KSKBfMjn0G+qowRSLBdCP5PT/PjGX6+96EQXzU55hpaf3BaUqcc5qVyMjAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d4b9832c87df9573de3e6c271a5503e3/33ecc/IMPROVE.webp 360w&quot; sizes=&quot;(max-width: 360px) 100vw, 360px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d4b9832c87df9573de3e6c271a5503e3/53918/IMPROVE.png 360w&quot; sizes=&quot;(max-width: 360px) 100vw, 360px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d4b9832c87df9573de3e6c271a5503e3/53918/IMPROVE.png&quot; alt=&quot;IMPROVE&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;작업시간 단축&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;기존에 &lt;strong&gt;12~15시간&lt;/strong&gt; 소요되던 작업 시간이 &lt;strong&gt;5~6시간&lt;/strong&gt;으로 대폭 감소했습니다.&lt;/li&gt;
&lt;li&gt;이를 통해 승급과 쿠폰 발급 간의 간격이 줄어들어, 고객 경험이 더욱 향상되었습니다.
&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;안정적인 운영 환경 구축&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;기존 백오피스 시스템에서 독립적인 구조로 변경되어 작업을 수행할 수 있게 되었습니다.&lt;/li&gt;
&lt;li&gt;대량 쿠폰 발급 작업의 안정성을 확보함과 동시에, 자원 사용량 및 운영 상황을 모니터링할 수 있는 환경을 보다 원활하게 구성되었습니다.
이 개선을 통해 시스템 효율성과 안정성을 모두 강화할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 개선을 통해 시스템 효율성과 안정성을 모두 강화할 수 있었습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 글은 어쩌다보니 시리즈로 이어지게 된, 쿠폰 발급 개선 과정을 다룬 이야기였습니다.&lt;/p&gt;
&lt;p&gt;개선 이후에도 지속적인 모니터링과 올리브영 고객들의 VoC(Voice of Customer)에 대한 빠른 대응을 통해 시스템의 안정성을 유지하기 위해 노력해왔습니다.&lt;/p&gt;
&lt;p&gt;앞으로도 더욱 안정적이고 신뢰할 수 있는 올리브영 쿠폰을 제공하기 위해 최선을 다하겠습니다. 🫡🫡&lt;/p&gt;</content:encoded></item><item><title><![CDATA[고성능 캐시 아키텍처 설계 - 로컬 캐시와 Redis로 대규모 증정 행사 관리 최적화]]></title><description><![CDATA[안녕하세요.️ 쿠폰증정스쿼드에서 백엔드 개발 담당하는 어푸입니다! 지난 글에서는 Redis Pub/Sub…]]></description><link>https://oliveyoung.tech/2024-12-10/present-promotion-multi-layer-cache/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-10/present-promotion-multi-layer-cache/</guid><pubDate>Tue, 10 Dec 2024 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.️ 쿠폰증정스쿼드에서 백엔드 개발 담당하는 어푸입니다!&lt;/p&gt;
&lt;p&gt;지난 글에서는 &lt;a href=&quot;https://oliveyoung.tech/2023-08-07/async-process-of-coupon-issuance-using-redis/&quot;&gt;Redis Pub/Sub을 활용한 쿠폰 발급 비동기 처리&lt;/a&gt;로 개선된 쿠폰 발급 프로세스를 소개해 드렸는데요.&lt;/p&gt;
&lt;p&gt;이번에는 올리브영에서 증정 행사를 어떻게 운영하고 있는지 소개해 보려고 합니다. 아래 목차대로 잘 따라와 주세요!&lt;/p&gt;
&lt;h3 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%EC%A6%9D%EC%A0%95-%ED%96%89%EC%82%AC-%EC%A6%9D%EC%A0%95%ED%92%88&quot;&gt;올리브영의 증정 행사? 증정품?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%A6%9D%EC%A0%95-%ED%96%89%EC%82%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%EB%90%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot;&gt;증정 행사 데이터는 어떻게 관리되고 있나요?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#u%EC%A6%9D%EC%A0%95-%ED%96%89%EC%82%AC%EC%9D%98-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C%EC%9A%94u&quot;&gt;증정 행사의 기반 데이터는 어디에 저장될까요?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#uamazon-elasticache%EB%A1%9C-%EA%B8%80%EB%A1%9C%EB%B2%8C%EC%BA%90%EC%8B%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-%EB%B4%85%EC%8B%9C%EB%8B%A4u&quot;&gt;Amazon ElastiCache로 글로벌캐시를 사용해 봅시다!&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#-%EC%BA%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EA%B5%AC%EC%A1%B0&quot;&gt;캐시 데이터의 구조&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#-%EC%BA%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%98%84%ED%96%89%ED%99%94&quot;&gt;캐시 데이터 현행화&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#-%EA%B0%9C%EC%84%A0-%ED%8F%AC%EC%9D%B8%ED%8A%B8&quot;&gt;개선 포인트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#u%EB%8B%A4%EC%A4%91-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%9C%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B0%9C%EC%84%A0%ED%95%A9%EB%8B%88%EB%8B%A4u&quot;&gt;다중 레이어 캐시를 활용하여 개선합니다!&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#-%EB%B6%84%EC%82%B0-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%A1%9C%EC%BB%AC%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9-%EC%8B%9C-%EC%9C%A0%EC%9D%98-%EC%82%AC%ED%95%AD&quot;&gt;분산 환경에서 로컬 캐시 적용 시 유의 사항&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#-%EB%8B%A4%EC%A4%91-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95&quot;&gt;다중 레이어 캐시 적용 방법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#-caffeine-cache%EB%A1%9C-%EB%A1%9C%EC%BB%AC%EC%BA%90%EC%8B%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot;&gt;Caffeine Cache로 로컬 캐시 구현하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%8B%A4%EC%A4%91-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%9C%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%84%B1%EB%8A%A5%EC%9D%B4-%EC%96%BC%EB%A7%88%EB%82%98-%EA%B0%9C%EC%84%A0%EB%90%A0%EA%B9%8C%EC%9A%94&quot;&gt;다중 레이어 캐시를 적용하면 성능이 얼마나 개선될까요?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot;&gt;마치며..&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;올리브영의-증정-행사-증정품&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%EC%A6%9D%EC%A0%95-%ED%96%89%EC%82%AC-%EC%A6%9D%EC%A0%95%ED%92%88&quot; aria-label=&quot;올리브영의 증정 행사 증정품 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영의 증정 행사? 증정품?&lt;/h1&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;img src=&quot;/4af994c87ebbb514d8aa7640e45cf111/free.webp&quot;&gt;&lt;figcaption&gt;출처 - &lt;a href=&quot;https://giphy.com/gifs/cetfreedom-drlisaturner-dr-lisa-turner-lisaturner-1yMZsLtI3aCFCdwW6a&quot;&gt;GIPHY&lt;/a&gt;&lt;/figcaption&gt;&lt;/p&gt;
&lt;p&gt;올리브영에서 증정품을 받아보셨나요?&lt;/p&gt;
&lt;p&gt;올리브영에서 제공하는 증정품은 화장품의 작은 샘플, 인기 아이돌 포토 카드, 화장품 파우치, 혹은 쇼핑백과 같은 다양한 품목으로 이루어져 있습니다.&lt;/p&gt;
&lt;p&gt;고객은 상품 상세 페이지나 주문서에서 상품 구매 시 받을 수 있는 증정품 정보를 확인할 수 있는데요.&lt;/p&gt;
&lt;p&gt;이러한 증정품 혜택은 구매 고객에게 부가적인 만족감을 제공할 뿐만 아니라, 브랜드 충성도를 높이는 등 마케팅 전략으로 중요한 역할을 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1181px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/f49d540ad9d3e75348daed299c223a3e/f6909/product_present2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 79.375%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAC4jAAAuIwF4pT92AAADKElEQVR42m1TWWsTURid/6MIPosPCoJUK9hF69LWWATFBfVBXMCnWkRFRYsP2oqh1mix+OJWNU3dahex1aB2S2qbZVIzk2QymTUzOX73JhPrcuHjXmbuPfec850rrN3iw7Gzl7AYT2I28gNKNodoPIqLXV0YejsKNhzHheu6fB0MhrBi5Sps3rQOjY0NqKtvxI4dO7G1oQ5b6xsgrNrQjP4nQSRii4glRUzNLWBiahrXb9/FQGi4CugN3TAwPz+PSDQCWZZh6Do0TaPZQC6nQFiz/QjuPR7CeHgG41+jmJgT8XR8Gk3HL+D50Ng/gGxkSUUqlYKiKNA9QNOgWYewutaHW4FHePN6EC+CLzH8bgD+O9dQU7ceHz994ACu61TBSqUSMcmjQIfzeZXAszCMMphOLIUzp47icvtR3Gjfj6tnfejqaEHgSiue3dyFTPJzBeRPhmLqJzJZhTPLZLIoFDS6tATTtCDohQIs04RtF1EssnJgF11YlsPZeKy8NWjKkGRV1WDZNp2zq/8sy4aQljNIJJJYWlqCKKa4sSpdwjaywVioqspnD9xxHDps8T1sXSbjcOmCLUkQ43HEEwn+o8KjGpNwOIzu7m4EAgGSZPJvKoEzDy3az0D5TOoMw4SgKSoychayJBO73B8SPSk9PT3o7OxEKBQqdzmnEqBRYWpzq8qSmYd0q0kfWYeY1IpNVYbJZBJ+v5+XKIplG2gvawBrRPli/PbQohwZ5JFCIdXzeWhKDi7d5NpWuaMEEolEECdbPPYFkpxXC7y438SQRUvXSTKUDCTyb3JkBDPhL4h+/4bY3Cy0JRElXfuvBVlqnETNlCRWZJWSp2CbvAQ7K0OTJaQXFyDR88uJCaRjMSwSaMkyufwSyV8eIQaYJiAG6qWCZVGhfggsgxpJltISL41+FivZ8nxcPsqxceG4lXJ+vyIWHYHli71J9tDZPDn5Ge+HhzE6NsaNXw7k5bLvYT/u+HtwtzeA+w/60HsvQOtevAoOQmC0VTI3zdhRFE6cPI2NNbVobvFxf/5+KezbocNHsLulFXvb9mH/gYPY42tD47YmnOs4j18CAof5RAyCagAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f49d540ad9d3e75348daed299c223a3e/263a4/product_present2.webp 480w,
/static/f49d540ad9d3e75348daed299c223a3e/a6361/product_present2.webp 960w,
/static/f49d540ad9d3e75348daed299c223a3e/8866f/product_present2.webp 1181w&quot; sizes=&quot;(max-width: 1181px) 100vw, 1181px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f49d540ad9d3e75348daed299c223a3e/9aebd/product_present2.png 480w,
/static/f49d540ad9d3e75348daed299c223a3e/a91f8/product_present2.png 960w,
/static/f49d540ad9d3e75348daed299c223a3e/f6909/product_present2.png 1181w&quot; sizes=&quot;(max-width: 1181px) 100vw, 1181px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f49d540ad9d3e75348daed299c223a3e/f6909/product_present2.png&quot; alt=&quot;product present2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;올리브영 모바일 상품 상세 페이지&lt;/figcaption&gt;&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1181px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ea8f876d097aaa29b523eef07b3d4002/f6909/order_present.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 79.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAC4jAAAuIwF4pT92AAAC6UlEQVR42nWTTU8TYRDH95twQQIkhNBigRse8GBED+qFoyCkKSpqPKhfQGNUECXVQLyQaPgI3ADhwJuoCcEEjUBLu9td9n23u335O/NAS0GYZDJ9mn1+M/+ZZyTXdVEqlYRnszLy+TzY+Fwul1ExPrMtLS2hpaUFXV1diMViiEaj6OjoQHt7O3p6LkPSNA1BEPwH4XgWcHFxEQ0NDWhra0MkEhFA9tbWVnR3X4IUhgU4rnN0jSAoHcZzgAsLX1FXV4fm5mY0NTWjsbGJYhPq6y+gs7MLkqpqMEwTpXKpepl45wI3NjbQ19eH/v5+DA4OIZFICI/H4xgZeQDJ833YtoXUbgYZ9xfmlLdQvD8ohKgmqYUXCgXIchapVAomFcL/F4tFER3HgcT9C8IAeT+E4vzFijIDI58BsxhYgdVWa9s2PM+jVrmwLEs4q3QcFxLTWU4YhpSdMhW5omOJm5ubmJ2dxdzcXBVq2S503QL3X5ZlUSkn4LOkmzYUVYdpObAdDzZldTwfrpcnaBk+tWRychJjY2NYXV0RQFXTkd7PiiIMwxAV82/+XpJzB9hLZ5HJ5pCRcwQ/wIFukgQHRfqAezY9PY1kMonl5WUBNEmiRlCuiBOifDw4STkC7hMwR5VqAmYLWWzb29uYn5+vwtgcl5TYDn1ji+j7eXrLIXxaColl6oZFfghhqZ4fiFgoHA+l9tFblk0qjCPXReTXIipMp9NiU3w/EJnYWQr3g/2srWHJOXq/qmiPIaqrDJeA+1SdIXpVa6chtcYQWeEWqVByKilzBJAZ0tb6Kr4tLmBrfQ2/f/7A9vcNbK2tihhywyurc2KLTieuDIWmPDwwgMjFGHqvXMWtGzdxvfcaou0x9NCiq3u7h5dq+2daePL0GQaH4hi+ex8PHz1GYvgebg/cwfMXL+lhGzpKtHpFLYcCeVnXYNJqZXd2gCB/ohIxYdqGqalPeDM6jnfvJ5D88BHj7ybw6vUoPn+ZwT8NjXMIK7fZcwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ea8f876d097aaa29b523eef07b3d4002/263a4/order_present.webp 480w,
/static/ea8f876d097aaa29b523eef07b3d4002/a6361/order_present.webp 960w,
/static/ea8f876d097aaa29b523eef07b3d4002/8866f/order_present.webp 1181w&quot; sizes=&quot;(max-width: 1181px) 100vw, 1181px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ea8f876d097aaa29b523eef07b3d4002/9aebd/order_present.png 480w,
/static/ea8f876d097aaa29b523eef07b3d4002/a91f8/order_present.png 960w,
/static/ea8f876d097aaa29b523eef07b3d4002/f6909/order_present.png 1181w&quot; sizes=&quot;(max-width: 1181px) 100vw, 1181px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ea8f876d097aaa29b523eef07b3d4002/f6909/order_present.png&quot; alt=&quot;order present&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;올리브영 모바일 주문서 페이지&lt;/figcaption&gt;&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;하지만 이러한 증정품 제공은 생각보다 단순한 일이 아닙니다.&lt;/p&gt;
&lt;p&gt;인기 있는 증정품 행사가 있는 경우, 수만 수십만 명의 사용자가 동시에 접속해 데이터를 조회하고, 행사 조건을 검증해야 할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 증정품들이 원활히 제공되기 위해서는 사전에 철저히 설정된 증정 행사 정보가 필요합니다.&lt;/p&gt;
&lt;p&gt;이를 위해 올리브영의 쿠폰 증정 스쿼드는 각 증정 행사에 대한 지급 조건 데이터를 설정하고, 고객이 특정 조건을 충족 시 자동으로 증정품이 지급되도록 설계 및 관리하고 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;증정-행사-데이터는-어떻게-관리되고-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A6%9D%EC%A0%95-%ED%96%89%EC%82%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B4%80%EB%A6%AC%EB%90%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;증정 행사 데이터는 어떻게 관리되고 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;증정 행사 데이터는 어떻게 관리되고 있나요?&lt;/h1&gt;
&lt;h2 id=&quot;u증정-행사의-기반-데이터는-어디에-저장될까요u&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#u%EC%A6%9D%EC%A0%95-%ED%96%89%EC%82%AC%EC%9D%98-%EA%B8%B0%EB%B0%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C%EC%9A%94u&quot; aria-label=&quot;u증정 행사의 기반 데이터는 어디에 저장될까요u permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;u&gt;증정 행사의 기반 데이터는 어디에 저장될까요?&lt;/u&gt;&lt;/h2&gt;
&lt;p&gt;증정품 지급을 위해서는 고객의 구매 정보와 미리 설정된 행사 조건 데이터를 정확히 조합해야 합니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 고객이 특정 상품을 몇 개 구매했는지, 행사 대상 상품에 포함되는지 등을 판단하여 지급할 증정품의 종류와 수량을 결정합니다.&lt;/p&gt;
&lt;p&gt;이 데이터는 관계형 데이터베이스(RDBMS)인 Amazon RDS에 저장됩니다.&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 640px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/cbc26de714305ed60ad0de094114f5e6/0f09e/AmazonRDS.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFGUlEQVR42h2V2VMUZxTF5z/IW/KU8iHRPIgpK+5G0UowxIqYUuMSMCGQiCJu4C5upSGIyr6qEBYZwWEdhtlnenp2BmbYRlwQcY2apCr/wi+3++Gr6u/r/k7de865pw1m+zBWpx27x4FDceGW5fV7CcdCREbD+EIK/oiq75WgIssrZz7UkIrH78GtenEqbv3+sNOKweKw4vQ69ZdKwEskFmQyGcdsMzNkszDzaJKx8RG8qoeReJT4xAjR0QiBiF/OowTCqhTgwaW6cXgdGKxuBx6fC19ALoyG8EtFx4svkLZ1J8VXS3WQnv5eOrq6sNitmOT5bne3vq5XVhOLRwgKqKJVKzgGu1SnCFhI2hqLh8nKzWPt1xmcvHCFTpOJ7IITlFXW0tzeTuqmLRQcO87E1BgexcOBI0Wcu3yV5EwCVahQ/G4MLkHVAMfGI7g8Nj5buoZjZy4wIq0/m3/IiUsl7Mo5RMqKVD74cAE7snKEDgt/v3/O2YuXWJu2FZvLTmRElfZ9GNxSajCqSrt+Zmen2FdwlG17sglH/STGwzyZnaZ7oJ+MXT+RnXeYGzV10m4XpTfLWbEhnW937KW104gqRWltC6AXl+LAbOnTQZ7MPdQrXLb+G0rLK3n3fp6J6QTBWITHszNYHcOs35TBKqFld24BN+sb6TUP0tR8B4fHjkER+VWxg8U2RO9AjwjkRgkFqLrdQlu3iSOnz/N91j6KLpQwYLMxnpzgem0DN2rrpVUHoWiQIbGeKlaKiqgGnyg0/SDO06dJYnKQnBkXgfy8/usZpRXVLFmdRsbuHPYXnqPm1h2mHkzx8s28UBTg+YuHzD59QHgkwOyzGdEhhkGVyxrJ/YP9OFw2WjvadMU0QKfXzV7hbcuubLLyjgqXZjGxR4zt55Fw6/S66DB2Yuo10X63XfeqIS4W0CySmZNPTt5BFnySIpNj5cWrOeZfPqWhuZWi4suckG/yj5+lVLz3739vef12nou/l5J3qFDu5bNk+TrxsqicEMJvi8e2Zeby6eKVLFy8gvL6W5TcrKD4yh/0DA6wJ+cg69K3k33gCLUNDezNzuV+fx81jbfYm5vP0tUbWZSygoaWVgxhGaPpB+OikIPDJ84IUBXXq2v5fPVX7Px5H+/+ecV9AV2Wuhlj3wB1wmPKF2vI+CETi8Oh73dk/kJ+4WmqGu9g8ItK4WiAcZnXOTFyQmY1PjFKw5+tqCLYqzdzEg5B0rdnsfG7XVy+UU2XcHnxWgUnz1+hb8gss6xwq6UFu9uJwRv06baxiyBOt3D38rFMSUjm0s3zl09ITI6KD+P0Dw9RUl5NVVOzGNtE/Z0WoWOQNuM9OrU5tw5JYKgaoKrHjk2WT5ze3duDsfsexi4jNyoqhKtezFaLHldaTJmHLaLsXbpMXdQ1NgmnjVTV1tHReRefFGbwigW0fNNSIzEZY9uPuRQUnmJD2maWrUrFeL9bALTRUvQYu1RyjVPFF8n+bb9wuZYWEbTfPKBbSaPP4FIV4cqve2hM+DstYZC+LZOPPl7I8i/TqKxvYtBiJiphkRTxymrq2S9WWboylUVLVsq+AYtw5xbLaFNncPi0FPZJOLpxCW9TMxPcbmsj73ARl0uvU1ZRRbu0E5AuEhMxeT9Oc0cHWb/mU3DyPGV1TQw47AIoCR7UAKVCDV2NBCQAwkTGokw/nGL60TRRCVetVS2VY4koozJa8ckxko+TTM5M6YHhCQplEfk9hAO4/Qr/A6h9jsuoK2YeAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cbc26de714305ed60ad0de094114f5e6/263a4/AmazonRDS.webp 480w,
/static/cbc26de714305ed60ad0de094114f5e6/0c8fb/AmazonRDS.webp 640w&quot; sizes=&quot;(max-width: 640px) 100vw, 640px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cbc26de714305ed60ad0de094114f5e6/9aebd/AmazonRDS.png 480w,
/static/cbc26de714305ed60ad0de094114f5e6/0f09e/AmazonRDS.png 640w&quot; sizes=&quot;(max-width: 640px) 100vw, 640px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cbc26de714305ed60ad0de094114f5e6/0f09e/AmazonRDS.png&quot; alt=&quot;AmazonRDS&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;ChatGPT로 생성된 이미지입니다.&lt;/figcaption&gt;&lt;/p&gt;
&lt;p&gt;행사 기간과 증정품을 지급하는 대상 상품, 배송할 수 있는 지점의 정보 등을 Amazon RDS 내 여러 테이블에 나누어 저장하고 있습니다.&lt;/p&gt;
&lt;p&gt;고객이 구매하려는 상품 정보와 저장되어 있는 증정 행사 데이터를 조합하면, 어떤 증정품을 받을 수 있는지 확인할 수 있죠!&lt;/p&gt;
&lt;p&gt;그런데 상품 상세 페이지와 주문서에 고객이 접근할 때마다 RDS로 증정 행사 데이터를 조회하면 어떨까요?&lt;/p&gt;
&lt;p&gt;유입이 아주 적은 쇼핑몰이라면 괜찮을 수 있을지 모르겠지만 올리브영엔 정말 많은 고객분이 찾아주셔서요.&lt;/p&gt;
&lt;p&gt;디스크 기반으로 데이터를 조회하는 RDS에 부하가 발생하면 응답 속도가 늦어지고, 장애로 이어질 가능성이 큽니다.&lt;/p&gt;
&lt;p&gt;그래서 메모리 기반으로 빠른 조회 속도를 보장하는 Amazon ElastiCache를 사용하여 자주 조회하는 데이터를 제공하도록 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;uamazon-elasticache로-글로벌캐시를-사용해-봅시다u&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#uamazon-elasticache%EB%A1%9C-%EA%B8%80%EB%A1%9C%EB%B2%8C%EC%BA%90%EC%8B%9C%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-%EB%B4%85%EC%8B%9C%EB%8B%A4u&quot; aria-label=&quot;uamazon elasticache로 글로벌캐시를 사용해 봅시다u permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;u&gt;Amazon ElastiCache로 글로벌캐시를 사용해 봅시다!&lt;/u&gt;&lt;/h2&gt;
&lt;h3 id=&quot;-캐시-데이터의-구조&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%BA%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EA%B5%AC%EC%A1%B0&quot; aria-label=&quot; 캐시 데이터의 구조 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 캐시 데이터의 구조&lt;/h3&gt;
&lt;p&gt;자주 조회하는 데이터에 대해 메모리 기반으로 빠르게 읽을 수 있도록 Redis를 대중적으로 사용합니다.&lt;/p&gt;
&lt;p&gt;쿠폰 증정 스쿼드에서는 캐시용 Redis 서버로 Amazon ElastiCache를 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;캐싱할 데이터를 어떤 형태로 담아서 조회 속도를 높일 수 있을지 고민이 많았는데요.&lt;/p&gt;
&lt;p&gt;증정 행사 기반 데이터 중 &lt;u&gt;오늘 진행하는 행사&lt;/u&gt;에 대해 List 형태로 캐싱해 두고, 캐시 된 데이터를 조합하여 빠르게 행사 정보를 전달하도록 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7b5104e85664fba2e42449cee3cf7e0c/e1cb3/list_cache_data.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 37.291666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAC4jAAAuIwF4pT92AAABP0lEQVR42l2R227CMAyG+/7vAZrEbsfNtjsuxxsMhDoK2zQoTZqTc/hnpzCJRbKc2rH9+2uzbQ/4OHyh7b5x7gekFBFjBMXJi4UQ4NnklFLu/G63w3L5hNVqhbf1Go1SGlplGGcRuZk8jKlUfytKOdeGFAjWBB6aqsmJ7EdjJxEU0ThncP4Bqxs5SchcLAPGkVilr0US814Ucm4oOPUWJmgYbiTqRZTzHtY6NNYajLrAckF/6XE6nxC4j+aGg77gePzkAoX9vptyDvzOwZHBfDbH+2YL2SOnzArpXmEgX4NEhe+pfkeaeGpteC2CswVKEzxZdN2BNzG4DArGWljnhKGqPALzkWJZT1hOnPLEkO+eVxKm6co511y5xjIEd4wJjXBwzv+ZsPpvEhfot79bG+ZpWNu2mM0fsFg84vnlFb/01Bxnu+9xeQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7b5104e85664fba2e42449cee3cf7e0c/263a4/list_cache_data.webp 480w,
/static/7b5104e85664fba2e42449cee3cf7e0c/a6361/list_cache_data.webp 960w,
/static/7b5104e85664fba2e42449cee3cf7e0c/0b34d/list_cache_data.webp 1920w,
/static/7b5104e85664fba2e42449cee3cf7e0c/5d4cc/list_cache_data.webp 2362w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7b5104e85664fba2e42449cee3cf7e0c/9aebd/list_cache_data.png 480w,
/static/7b5104e85664fba2e42449cee3cf7e0c/a91f8/list_cache_data.png 960w,
/static/7b5104e85664fba2e42449cee3cf7e0c/ac7a9/list_cache_data.png 1920w,
/static/7b5104e85664fba2e42449cee3cf7e0c/e1cb3/list_cache_data.png 2362w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7b5104e85664fba2e42449cee3cf7e0c/ac7a9/list_cache_data.png&quot; alt=&quot;list cache data&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;아래 내용은 어떤 방식으로 데이터가 캐시 되어 사용자에게 보이는지 이해를 돕기 위한 설명입니다. 위 그림의 번호와 매칭된 내용을 참고해 주세요.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;u&gt;오늘 진행하는 증정 행사&lt;/u&gt;에 대한 정보가 여러 개의 List 형태로 캐시 되어 있습니다.&lt;/li&gt;
&lt;li&gt;1번의 데이터들을 조합하여 고객에게 제공할 수 있는 증정 행사 데이터를 만듭니다.&lt;/li&gt;
&lt;li&gt;2번으로 만들어진 데이터를 고객이 볼 수 있는 화면에 보여줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(위 예제는 이해를 돕기 위하여 만들어진 것으로 실제 데이터와는 상이합니다.)&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;-캐시-데이터-현행화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%BA%90%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%98%84%ED%96%89%ED%99%94&quot; aria-label=&quot; 캐시 데이터 현행화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 캐시 데이터 현행화&lt;/h3&gt;
&lt;p&gt;증정 행사의 설정 정보가 변경되면 캐시 데이터도 그에 맞춰 변경되어야 하는데요.&lt;/p&gt;
&lt;p&gt;증정 행사의 데이터가 변경될 때마다 새로운 버전의 캐시 데이터를 생성하고, 고객이 증정 행사 데이터를 조회할 때는 항상 최신 버전의 캐시 데이터를 조회하도록 했습니다.&lt;/p&gt;
&lt;p&gt;이를 위해 버전 정보도 ElastiCache에 저장해두고 있습니다. 버전은 1, 2, 3... 으로 숫자가 증가하는 형태이고, List 형태로 행사 정보가 들어가 있는 캐시 데이터의 key는 버전 정보를 담고 있죠.&lt;/p&gt;
&lt;br&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 827px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2b70ae16396b786d5c693275af95a0ad/38095/renew_cache_version.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAC4jAAAuIwF4pT92AAACU0lEQVR42m2T7W/SUBTG+1/7P2hm/Dx1X3SJ020MhE3jyzRGjWEDxgaMAqXv7e0bL0WghfJ42g60zJPcNO3vPk/OPc8t11AXeHAQYO9riLi8SQQ2imDSilZAEATgOwLm9IzL8VNu0zOuxWJBvIfpdJa8c4IxwavvPr61ZhuBPowgu0ssSGPbNjpdAY7jgvwTM22wpJUaxt+7PQGWZaeGjJnQlD6Wi9RwXStSz2ZTSJIEVdUwGHiIomjDl8sl8RlkWaalwPPc5BsnCH3IinZnskq6WB9F1/XETDfMv3yV7nA9j5hKWgWabmw49/MHw9mZhMbNYKvDVFj+ZeH0VMRVzaUOV/f4RdlGqSSiVnUSzlUrLj5+MGjp+F9d1TycfzLw/p26MVmPJK7ruocvn02clVQ6MhmabIhW00Cnk87g3wqCkIY9Bs9b4NsOjSHc4gEsNkK7bSbc931wPUFAs9WEYRgYjcaZ4ziOA1GUSNCGaZpwXS/DGWMQKbTrxg0xO9nHHb6WsP+Sp5Y1MpgnG9ezct0AuWM54cWCSqbTDB8NQ5wcK9h/weNtXoWuTcHtPKwjn5Ow9/QWHX54dyVSgSz5eLJTJ5GEZ7stCs7LcEP/jceP6sgdiXi+e4vKpQMun5NxWlRxQqbjcbjV4RyFvIxSUcHRoUR3LcjwIXVYLCiJ/s2BCNuag5NESulcoMsZxx5mkpxMxtSlS7yPvmBTKPPM1fH9ERTFTfTdroUwnIFzHAuVSpnmZ29CWRdjFv16FqrVS0qb0d8yvBdKrKtWL8Ask04wwB+bWstDAL1E3wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2b70ae16396b786d5c693275af95a0ad/263a4/renew_cache_version.webp 480w,
/static/2b70ae16396b786d5c693275af95a0ad/8c31b/renew_cache_version.webp 827w&quot; sizes=&quot;(max-width: 827px) 100vw, 827px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2b70ae16396b786d5c693275af95a0ad/9aebd/renew_cache_version.png 480w,
/static/2b70ae16396b786d5c693275af95a0ad/38095/renew_cache_version.png 827w&quot; sizes=&quot;(max-width: 827px) 100vw, 827px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2b70ae16396b786d5c693275af95a0ad/38095/renew_cache_version.png&quot; alt=&quot;renew cache version&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;앞서 언급했던 증정 행사 캐시 데이터 중 &lt;strong&gt;행사 지급 조건&lt;/strong&gt;을 예로 들어보겠습니다!&lt;/p&gt;
&lt;p&gt;위의 그림은 왼쪽부터 오른쪽으로 시간의 흐름에 따라 데이터 변경이 발생한 케이스입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;하루에 한 번 캐시 생성 배치가 실행됩니다. 이때 최초로 버전 1의 캐시 데이터가 생성됩니다. → 캐시 key: 행사_지급_조건_&lt;strong&gt;v1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;행사 담당자가 갑자기 A라는 증정 행사를 중단하고 싶을 수도 있겠죠? 중단된 행사에 대해서는 증정품이 지급되면 안 되기 때문에 즉시 캐시 데이터에도 반영되어야 합니다. 변경 사항을 반영한 버전 2의 캐시를 생성합니다. → 캐시 key: 행사_지급_조건_&lt;strong&gt;v2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;다른 행사 담당자는 B라는 증정 행사의 행사명을 변경합니다. 이 내용도 즉시 반영할 수 있도록 버전 3의 캐시를 생성합니다. → 캐시 key: 행사_지급_조건_&lt;strong&gt;v3&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그림에서는 &lt;strong&gt;버전 3이 가장 최신 데이터&lt;/strong&gt;이니 이후 행사 데이터 조회 요청이 들어오면 버전 3의 캐시 데이터인 &lt;strong&gt;행사_지급_조건_v3&lt;/strong&gt;만 바라보고 처리합니다.&lt;/p&gt;
&lt;p&gt;이렇게 메모리 기반으로 조회할 수 있는 Redis 서버를 두었으니 RDS를 직접 조회하는 것보단 빠른 응답시간을 보장할 것입니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;-개선-포인트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EA%B0%9C%EC%84%A0-%ED%8F%AC%EC%9D%B8%ED%8A%B8&quot; aria-label=&quot; 개선 포인트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 개선 포인트&lt;/h3&gt;
&lt;p&gt;하지만... 저희에겐 숙제가 하나 더 남아있었습니다.&lt;/p&gt;
&lt;p&gt;자원을 모니터링하면서 유독 수치가 높게 유지되는 항목이 있었는데요. 바로 Amazon ElastiCache의 Network Bytes out 즉, &lt;strong&gt;송신 네트워크 바이트&lt;/strong&gt; 수치였습니다.&lt;/p&gt;
&lt;p&gt;앞서 &lt;strong&gt;&apos;오늘&apos; 진행하는 행사&lt;/strong&gt;에 대해서만 List 자료구조로 캐시 데이터를 저장해두고, 이를 조회한다고 말씀드렸는데요.&lt;/p&gt;
&lt;p&gt;진행하는 증정 행사의 개수가 많을수록 ElastiCache 에서 송신하는 데이터의 크기가 커지기 때문에, 진행하는 행사와 유입되는 사용자가 많아지면 송신 네트워크 바이트 수치가 높게 유지되었습니다.&lt;/p&gt;
&lt;p&gt;송신 네트워크 바이트 수치가 지속해서 높으면 네트워크 대역폭 포화로 인해 응답지연이 발생할 수 있고 최악의 경우 장애가 발생할 확률이 높아집니다.&lt;/p&gt;
&lt;p&gt;이를 방지하기 위해 로컬 캐시를 적용하여 ElastiCache로 접근하는 횟수를 줄이기로 했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;u다중-레이어-캐시를-활용하여-개선합니다u&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#u%EB%8B%A4%EC%A4%91-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%9C%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B0%9C%EC%84%A0%ED%95%A9%EB%8B%88%EB%8B%A4u&quot; aria-label=&quot;u다중 레이어 캐시를 활용하여 개선합니다u permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;u&gt;다중 레이어 캐시를 활용하여 개선합니다!&lt;/u&gt;&lt;/h2&gt;
&lt;h3 id=&quot;-분산-환경에서-로컬캐시-적용-시-유의-사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%B6%84%EC%82%B0-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EB%A1%9C%EC%BB%AC%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9-%EC%8B%9C-%EC%9C%A0%EC%9D%98-%EC%82%AC%ED%95%AD&quot; aria-label=&quot; 분산 환경에서 로컬캐시 적용 시 유의 사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 분산 환경에서 로컬캐시 적용 시 유의 사항&lt;/h3&gt;
&lt;p&gt;로컬 캐시를 사용하면 서버의 메모리에 바로 접근해서 빠르게 데이터를 조회할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 무턱대고 로컬 캐시를 사용하면 분산 환경에서는 제공하는 데이터의 일관성을 보장하지 못할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;보통 서비스를 운영할 때 여러 대의 서버를 띄워두고 클라이언트의 요청에 응답하게 되는데요.&lt;/p&gt;
&lt;p&gt;운영되고 있는 서버가 아래처럼 3대라고 가정하면, 서버마다 다른 캐시 데이터를 저장할 수 있어서 일관된 데이터를 제공하지 못할 수도 있습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 709px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d4d0a83d1763ecd7d274564cbc664373/2fc92/different_cache_in_server.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAC4jAAAuIwF4pT92AAABpklEQVR42j2RTW/TQBCG81O5Ik5cEf+AE4ILAgkugMQNiZK0FMENqVWTuEnq1InjBK8bfybx+jO2w8PGgq40Gs37zMzO7HYePH7G6w8n+NEW4fpYYo1Mcyzb5eGT57z91GMTJypesxQescyItpJHT1/w6v0Xwm2sdBexDpgv7+i8fPeZ3o8+Uu4Jo6y1pjlw50W8+djl9OeQPG/w/IRok1HVNXGStezr9z5JUuEHCbu4xFE1nc024/Ii4vybw1lPcPEroKpq0rxQiTn9q4jTrlBmczPZIpOC5tAQhhkDxc7PHLonNtpwh70O6azdGHMuydK6vc24jUnTPUVVYlkxnpsrfU+WVSzMuN3gDwccJ2a1TJRet9v9XmXMLf/YULK0Uv6f+UyqBiVlVbQNo7C8Z6Yp1WTH3APCkQg7u2fCzpktPDpBGDPsu0xGPsOB8kOfqtyTFgVBIFs2/sdMPVQTFdRNo55jh6a08bWvvMd8EiGEmvB2tkQb33A91hmMdCzdwHNcZL7HMFdooyObok2mrKYGruNRqU/TjUXLNFUz0mcIVTcdG/wFDPb9tN+ZCrAAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d4d0a83d1763ecd7d274564cbc664373/263a4/different_cache_in_server.webp 480w,
/static/d4d0a83d1763ecd7d274564cbc664373/6e19d/different_cache_in_server.webp 709w&quot; sizes=&quot;(max-width: 709px) 100vw, 709px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d4d0a83d1763ecd7d274564cbc664373/9aebd/different_cache_in_server.png 480w,
/static/d4d0a83d1763ecd7d274564cbc664373/2fc92/different_cache_in_server.png 709w&quot; sizes=&quot;(max-width: 709px) 100vw, 709px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d4d0a83d1763ecd7d274564cbc664373/2fc92/different_cache_in_server.png&quot; alt=&quot;different cache in server&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;모든 서버가 일관된 데이터를 제공할 수 있도록 하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p&gt;바로 ElastiCache와 로컬 캐시를 결합한 다중 레이어 캐시 전략과 캐시의 버전 정보를 활용하면 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;-다중-레이어-캐시-적용-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%8B%A4%EC%A4%91-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%9C-%EC%A0%81%EC%9A%A9-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot; 다중 레이어 캐시 적용 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ 다중 레이어 캐시 적용 방법&lt;/h3&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 827px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4ff4e71fd5c904a6663b194667e2fc06/38095/multi_layer_cache.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 106.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAAC4jAAAuIwF4pT92AAACzUlEQVR42pVU2XKjMBD0/39XKq/ZvOzGB7FxnMRgmxt0a7Zn8O2qrY2qBAyCVk9PjyZJsqHZbE3rdU7L5Rd9fx+o6zymI+89EQXMSDEGCiE8xHznmEfbtjSx1lLfd/Ii4v3hEGizMVgcP9rtAqWpobqWkPb7Ma6qcb0oIshYWR+GnibGaAGMQGNC06mil5eOsszLD/O5pqenbzAf4yTR9PycYVMr8cdHoNdXRdvtGdBT03ihzgydIzKGBJwHPysV5D0PJERax3PMd15nNfoegF3LLCzl+Sc+NADgGeTuj6ijdpfB2VyP07oAslarFTNQxOk3jUXskPIWzy207EWfGpemaY6sbzdw7g4wTS+LShGt03iupLWe3v44FKFAAQ4oWkOrZTgX8YFhXXuazzRsouXnFgyX72wZh50dscbTqcWzIXZE03YA1wJi7fiNtSxPGG1TFJpefxWoWk5lWdPQB5EgRi8aCsM3J5pagGdZjop/gs0gmg+Dlsoz26IoGJAZjQYOwZMaYIX1teAEwEjWYdoIEEd1VePuJTYmwptj8cqypElZWvGeUi3SbrEzazpqGLCt85HmU0cxWEmt7zjlCpn0YAxHaE1fn0Z6pSxvGPqRoXpkOPuN6hd78saSBlCeZ2QBxGa1SlO2dReGDJiu4k2V7wHn80C7fY423FFVD9A7SJGGYaAsZ03VNaA5a8g26VGUdBWOmgbx2GzmoZmCJB3WNe13fnSBVNnDZk42lpTrytP7O1MerdK2DqcOf3CyjUNRrMjBPzOw91bW2AWjE4xszo0gxl7fGfs+5SSNAsSdwgW5H4+tJ84fTw/WOkkM6B/OKS+Ovjx1xj97GbJAI01ltRPq3MuLhYFeDdgqCG8oWdDPDodUGBhJq64MfBhuUl7MfwDYNJzyrYabzfXP46n834BKeeh1sQk3OR9P/HyaXP3b2N/E7nja8uHwF8nuYKDPMSVIAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4ff4e71fd5c904a6663b194667e2fc06/263a4/multi_layer_cache.webp 480w,
/static/4ff4e71fd5c904a6663b194667e2fc06/8c31b/multi_layer_cache.webp 827w&quot; sizes=&quot;(max-width: 827px) 100vw, 827px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4ff4e71fd5c904a6663b194667e2fc06/9aebd/multi_layer_cache.png 480w,
/static/4ff4e71fd5c904a6663b194667e2fc06/38095/multi_layer_cache.png 827w&quot; sizes=&quot;(max-width: 827px) 100vw, 827px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4ff4e71fd5c904a6663b194667e2fc06/38095/multi_layer_cache.png&quot; alt=&quot;multi layer cache&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;이해를 돕기 위해 그림에 번호를 붙여두었습니다. 자세한 내용이 궁금하시면 번호와 매칭되어 있는 아래 설명과 함께 그림을 확인해 주세요.&lt;/p&gt;
&lt;p&gt;(위 예시는 상품 상세 페이지에서 고객이 특정 상품을 조회한 상황을 가정했습니다.)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자가 상품 상세 페이지에 진입합니다.&lt;/li&gt;
&lt;li&gt;조회한 상품을 구입했을 때 받을 수 있는 증정품을 알기 위해 상품 상세 페이지에서 증정 행사 조회 API를 호출합니다.&lt;/li&gt;
&lt;li&gt;증정 행사를 관리하는 Application은 캐시 된 증정 행사 데이터를 조회해야 하는데요. 최신 데이터를 조회해야 하므로 ElastiCache에 저장되어 있는 &lt;strong&gt;최신 버전 정보를 조회&lt;/strong&gt;합니다.
→ 1, 2, 3... 와 같은 숫자로 버전 정보가 조회됩니다.&lt;/li&gt;
&lt;li&gt;최신 버전을 확인했으니 이제 &lt;strong&gt;해당 버전에 맞는 로컬 캐시를 조회&lt;/strong&gt;합니다. 메모리로부터 반환된 로컬 캐시 데이터가 있다면 바로 &lt;strong&gt;7번&lt;/strong&gt;으로 이동합니다.&lt;/li&gt;
&lt;li&gt;메모리로부터 조회된 로컬 캐시 데이터가 없는 경우 &lt;strong&gt;ElastiCache에 해당 버전에 맞는 캐시 데이터를 조회하여 반환&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;ElastiCache로부터 조회된 데이터를 로컬 캐시로 저장합니다.&lt;/li&gt;
&lt;li&gt;증정 행사 Application에서 조회된 데이터를 요청한 상황에 맞게 가공합니다.&lt;/li&gt;
&lt;li&gt;가공된 증정 행사 데이터를 상품 상세 페이지로 반환합니다.&lt;/li&gt;
&lt;li&gt;이제 고객은 상품 상세 페이지에서 증정 행사 정보를 확인할 수 있습니다!&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h3 id=&quot;-caffeine-cache로-로컬캐시-구현하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-caffeine-cache%EB%A1%9C-%EB%A1%9C%EC%BB%AC%EC%BA%90%EC%8B%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot; caffeine cache로 로컬캐시 구현하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;✅ Caffeine Cache로 로컬캐시 구현하기&lt;/h3&gt;
&lt;p&gt;자 그럼 로컬 캐시를 구현하기 위해 어떤 방식으로 구현해야 할지 선택해야 하는데요.&lt;/p&gt;
&lt;p&gt;Spring Boot에서 사용할 수 있는 여러 로컬 캐시 중 가장 성능이 뛰어나다는 Caffeine Cache를 선택했습니다.&lt;/p&gt;
&lt;p&gt;대표적인 캐시의 종류와 특징들은 아래와 같으니 참고해 주세요.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1798px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/ababd/many_caches.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.333333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABBklEQVR42k1R2W6EMAzM/39fHypVLdrVAksChPtKwtRjQdVIVmxn7PE4ZppX2LrDz6MA/WXdsCyb3oyfeYW67SXeMUl+Xla8Cgvfj1i3HXnpkD1KfH2/8LYtTOsH0PLCYd8Dtu3Ath9/vqu9Fu5HUEvplFyHVQgOiTkM48p5dP0E8/GZoXzXaGSKYVx0mpuEACtA343oh1ntPKENvJecvNOPMYGHZCZ7lnBNh0HAPCftVFeBddNLfCWuIpLeTSrXqhJCYoww3B0nGadFwf8thKRkIURE8XmnlFQN5bPJrYCT8M0UVS1SZ1SyUMok821sYGUC7pFF9tpT0w5KRNJGFJTSg5/Fhr+HPs5uhrV77AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/263a4/many_caches.webp 480w,
/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/a6361/many_caches.webp 960w,
/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/0532e/many_caches.webp 1798w&quot; sizes=&quot;(max-width: 1798px) 100vw, 1798px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/9aebd/many_caches.png 480w,
/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/a91f8/many_caches.png 960w,
/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/ababd/many_caches.png 1798w&quot; sizes=&quot;(max-width: 1798px) 100vw, 1798px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2cc0ec78c1ecddb096e5c9759f5c8fb0/ababd/many_caches.png&quot; alt=&quot;many caches&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;p&gt;Caffeine Cache는 성능도 매우 뛰어나지만, 아주 쉽게 여러 가지 옵션을 설정할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;maximumSize: 캐시에 포함할 수 있는 최대 키 개수를 지정합니다. (maximumWeight와 함께 설정할 수 없습니다.)&lt;/li&gt;
&lt;li&gt;maximumWeight: 캐시에 포함할 수 있는 최대 무게를 지정합니다. (maximumSize와 함께 설정할 수 없습니다.)&lt;/li&gt;
&lt;li&gt;expireAfterAccess: 캐시가 생성되고 나서 마지막으로 읽은 후 설정한 시간이 지나면 자동으로 캐시에서 제거됩니다.&lt;/li&gt;
&lt;li&gt;expireAfterWrite: 캐시가 생성되고 나서 설정한 시간이 지나면 자동으로 캐시에서 제거됩니다.&lt;/li&gt;
&lt;li&gt;refreshAfterWrite: 캐시가 마지막으로 업데이트된 후 설정된 시간 간격으로 새로고침 됩니다.&lt;/li&gt;
&lt;li&gt;weakKeys: Weak References로 키를 저장합니다. 키에 대한 강력한 참조가 없는 경우 GC에 의해 수집됩니다.&lt;/li&gt;
&lt;li&gt;weakValues: Weak References로 값을 저장합니다. 값에 대한 강력한 참조가 없는 경우 GC에 의해 수집됩니다.&lt;/li&gt;
&lt;li&gt;softValues: Soft References를 사용하여 값을 저장합니다. 메모리 수요에 따라 LRU(Least-Recently-Used) 방식으로 GC에 의해 수집됩니다.&lt;/li&gt;
&lt;li&gt;recordStats: 캐시에 대한 통계를 적용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;Caffeine Cache는 yml, properties 파일이나 클래스로 설정할 수 있는데요.&lt;/p&gt;
&lt;p&gt;yml, properties 파일로 설정할 때는 캐시별로 개별 설정이 불가능하다는 단점이 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 저희는 Enum 클래스로 개별 옵션을 설정할 수 있도록 했습니다.&lt;/p&gt;
&lt;p&gt;적용한 코드 예제를 아래에 간단히 소개합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;build.gradle 파일에 Caffeine Cache 의존성을 추가합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;dependencies &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;com.github.ben-manes.caffeine:caffeine:3.1.8&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;implementation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;org.springframework.boot:spring-boot-starter-cache&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Configuration 파일을 추가하여 설정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@EnableCaching&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CaffeineCacheConfig &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
 
    &lt;span class=&quot;token annotation builtin&quot;&gt;@Bean&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;caffeineCacheManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CacheManager &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cacheManager &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SimpleCacheManager&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; caches &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; CaffeineCacheType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cache &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;CaffeineCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cacheName&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                Caffeine&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;newBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expireAfterWrite&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                        cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;expiredAfterWrite&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                        TimeUnit&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;SECONDS
                    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;maximumSize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cache&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maximumSize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        cacheManager&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setCaches&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;caches&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cacheManager
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;enum class로 캐시 타입을 지정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CaffeineCacheType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cacheName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; expiredAfterWrite&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; maximumSize&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;TODAY_PRESENT_CONDITION&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;todayPresentCondition&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 생성 후 60초 뒤에 소멸되며, key는 최대 100개까지 저장할 수 있습니다.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;캐시 대상 데이터를 @Cacheable 어노테이션으로 지정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@Cacheable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheNames &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;todayPresentCondition&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#version&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;todayPresentCondition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;version&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Condition&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; cacheDao&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;version&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ElastiCache에서 조회한 결과값&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;메소드 위에 선언된 어노테이션 속성을 보시면 key에 파라미터로 전달받은 version이 들어가 있죠? 이 버전은 ElastiCache로부터 조회된 값이며, 이 버전 정보로 분산 환경에서도 로컬 캐시의 일관성을 지킬 수 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;h1 id=&quot;다중-레이어-캐시를-적용하면-성능이-얼마나-개선될까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%A4%91-%EB%A0%88%EC%9D%B4%EC%96%B4-%EC%BA%90%EC%8B%9C%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%84%B1%EB%8A%A5%EC%9D%B4-%EC%96%BC%EB%A7%88%EB%82%98-%EA%B0%9C%EC%84%A0%EB%90%A0%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;다중 레이어 캐시를 적용하면 성능이 얼마나 개선될까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;다중 레이어 캐시를 적용하면 성능이 얼마나 개선될까요?&lt;/h1&gt;
&lt;p&gt;자, 그럼 로컬 캐시를 적용했을 때 대량 트래픽이 유입된 상황에서 성능이 얼마나 좋아졌는지, Redis Network Bytes out 수치가 얼마나 개선되었는지 확인해 봐야겠죠?&lt;/p&gt;
&lt;p&gt;증정 행사의 조회가 가장 많은 상품 상세 페이지를 기준으로 성능 테스트를 진행했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/054a82defcfce8fb5dc1d951ef4aeec5/c406e/performance_test.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 44.79166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAC4jAAAuIwF4pT92AAABsUlEQVR42l2SuU7DQBCG82509PRQU/AGNHQICjokShqgQZTkQBwiHEJCCIHkEGQbnMSOHa+vmPjan9m1Y0FGmmLn+Peb2W1kWYaiKKTneQ5hw+EQitKDpuno9/swTYvOCnRdh2EMZM40zbpPOOccSZKgIQ5zE0FhjuNQo0HCA3Q6bXS7XdzcXKPVakmh0WiEMAyxaALun+CiccoJccYYptMpXNdFdecfCNQxKSgKLcuSgYIygrIeY4FcU1UEgixLkEd+FZ87aOQUDc/zoPR6NREq4lR7hX+8BZ5nZTk1t5vnMP0Y6cMp2P46RKU74QTEEcfUk5KgGOODFi97xhY4LZ6rX4gOd2FvLoGns5ry8qIDOyK67gm+dlah+oDS53h/47QOMXJF+FERFlGE9OoC2d094s4RnK1lEkxqwXaziXE4Q3p7AmdvDc+0qfbnD3QvK6cShEEQQNXUcgfKE4pJuc/Z2zUm2yvgdOvcHu/v4BBh+ngGdrAhY+ynwIsRyz2WI9MLui6DxzyYQwPiHEb0oo4Ny/iG7/vyIYIgpG80krUTm3IDAz7FQgJy7LH8CYx5+AVk0KX59MeeggAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/054a82defcfce8fb5dc1d951ef4aeec5/263a4/performance_test.webp 480w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/a6361/performance_test.webp 960w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/0b34d/performance_test.webp 1920w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/da28f/performance_test.webp 2880w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/98b7d/performance_test.webp 3840w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/63688/performance_test.webp 6563w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/054a82defcfce8fb5dc1d951ef4aeec5/9aebd/performance_test.png 480w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/a91f8/performance_test.png 960w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/ac7a9/performance_test.png 1920w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/f9c26/performance_test.png 2880w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/5da7e/performance_test.png 3840w,
/static/054a82defcfce8fb5dc1d951ef4aeec5/c406e/performance_test.png 6563w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/054a82defcfce8fb5dc1d951ef4aeec5/ac7a9/performance_test.png&quot; alt=&quot;performance test&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;figcaption&gt;성능 테스트 결과&lt;/figcaption&gt;&lt;/p&gt;
&lt;p&gt;동일한 자원 환경과 데이터 기준으로 TPS(초당 트랜잭션 수)는 478% 증가, Redis Network Bytes out 수치는 99.1% 감소했습니다.&lt;/p&gt;
&lt;p&gt;다중 레이어 캐시를 적용하여 성능이 크게 향상되었고, Redis Network Bytes out 수치는 대폭 하락하면서 장애 발생 가능성을 낮추었습니다.
&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며..&lt;/h1&gt;
&lt;p&gt;2024년 한 해 동안 올리브영의 온라인 증정 행사 시스템을 구축하면서 많은 시행착오를 겪었습니다.&lt;/p&gt;
&lt;p&gt;캐시 데이터 구조를 어떻게 설계해야 할지, 성능을 어떻게 개선할 수 있을지, 네트워크 대역폭이 포화될 경우 어떤 문제가 발생하고 이를 어떻게 해결할지 등 수많은 고민과 도전을 거듭했습니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 많은 분들의 도움과 조언을 받을 수 있었고, 덕분에 보다 나은 방향으로 문제를 해결하며 시스템을 지속적으로 개선할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이 글을 빌려 함께 협업한 스쿼드와 귀중한 조언을 아낌없이 나눠주신 시니어 개발자분들에게 감사의 말씀을 드립니다. 🙇🏻‍♀️&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;이번 글에서는 올리브영 온라인몰의 증정 행사 시스템을 소개해 드렸는데요. 현재는 매장에서 운영 중인 증정 행사 시스템의 개편 작업도 진행 중이며, 이를 통해 더 효율적이고 안정적인 운영 환경을 만들어 나갈 계획입니다.&lt;/p&gt;
&lt;p&gt;앞으로도 고객 여러분이 더 편리하게 혜택을 누릴 수 있도록 기술적인 고민을 이어가며 부족한 부분을 꾸준히 개선해 나가겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;p&gt;&lt;img src=&quot;/281ee2db9ba3f58d24ca6ef657e42834/thankyou.gif&quot;&gt;&lt;figcaption&gt;출처 - &lt;a href=&quot;https://giphy.com/gifs/thedrewbarrymoreshow-drew-barrymore-cole-sprouse-the-show-XxcOCzT9kBsc6NQfKw&quot;&gt;GIPHY&lt;/a&gt;&lt;/figcaption&gt;&lt;/p&gt;
&lt;div style=&quot;text-align:center;font-size: 24px;font-weight: bold;&quot;&gt;긴 글 읽어주셔서 감사합니다!!!&lt;/div&gt; 
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;</content:encoded></item><item><title><![CDATA[Java를 주로 다루는 개발자가 생각하는 Kotlin 장점 🌼]]></title><description><![CDATA[목차 개요 간결한 문법과 편리해진 기능 Class Function 마무리 개요 안녕하세요! 올리브영의 파트너를 위한 서비스를 제공하는 파트너플랫폼 스쿼드 백엔드 개발자 🌼 들국화입니다. 이번 글에서는 Java에 익숙한 개발자가 Kotlin…]]></description><link>https://oliveyoung.tech/2024-12-08/kotlin-advantages/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-08/kotlin-advantages/</guid><pubDate>Sun, 08 Dec 2024 13:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h2&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot;&gt;개요&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EA%B0%84%EA%B2%B0%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%ED%8E%B8%EB%A6%AC%ED%95%B4%EC%A7%84-%EA%B8%B0%EB%8A%A5&quot;&gt;간결한 문법과 편리해진 기능&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#class&quot;&gt;Class&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#function&quot;&gt;Function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot;&gt;마무리&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot; aria-label=&quot;개요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개요&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요!&lt;/p&gt;
&lt;p&gt;올리브영의 파트너를 위한 서비스를 제공하는 파트너플랫폼 스쿼드 백엔드 개발자 🌼 들국화입니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 &lt;strong&gt;Java&lt;/strong&gt;에 익숙한 개발자가 &lt;strong&gt;Kotlin을 사용하며 경험할 수 있는 장점&lt;/strong&gt;들을 소개하려고 합니다.&lt;/p&gt;
&lt;p&gt;또한, 제가 느낀 만족도를 별점으로 표현해 보았는데요. 😊 ⭐️⭐️⭐️⭐️⭐️&lt;/p&gt;
&lt;p&gt;그럼 지금부터 &lt;strong&gt;Kotlin&lt;/strong&gt;의 장점을 하나씩 알아보겠습니다!&lt;/p&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;간결한-문법과-편리해진-기능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%84%EA%B2%B0%ED%95%9C-%EB%AC%B8%EB%B2%95%EA%B3%BC-%ED%8E%B8%EB%A6%AC%ED%95%B4%EC%A7%84-%EA%B8%B0%EB%8A%A5&quot; aria-label=&quot;간결한 문법과 편리해진 기능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;간결한 문법과 편리해진 기능&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 와 다르게 사용되는 여러 가지 문법이 있지만, 장점이라고 생각될만한 간결해진 문법을 소개합니다.&lt;/p&gt;
&lt;p&gt;또한 &lt;strong&gt;Kotlin&lt;/strong&gt; 에서 제공하는 여러 가지 편리한 기능들을 소개하겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;1️⃣-프로그램-진입점-️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F%E2%83%A3-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%A7%84%EC%9E%85%EC%A0%90-%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;1️⃣ 프로그램 진입점 ️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1️⃣ 프로그램 진입점 ⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;public static void main(String[] args)&lt;/code&gt; 메서드를 사용하여 프로그램을 실행합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;fun main()&lt;/code&gt; 함수를 사용하여 프로그램을 실행합니다.&lt;/p&gt;
&lt;p&gt;프로그램 진입점이 되는 &lt;code class=&quot;language-text&quot;&gt;main function&lt;/code&gt;을 자주 사용하진 않지만, 매우 간결화된 &lt;code class=&quot;language-text&quot;&gt;main function&lt;/code&gt; 덕분에 배움의 시작부터 편리함을 느꼈습니다 !&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;2️⃣-세미콜론-️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EF%B8%8F%E2%83%A3-%EC%84%B8%EB%AF%B8%EC%BD%9C%EB%A1%A0-%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;2️⃣ 세미콜론 ️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2️⃣ 세미콜론 ⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 은 세미콜론을 사용하지 않아도 됩니다 ! 처음엔 어색해서 적응이 필요했지만 적응하는데에 약 5분 정도 걸렸습니다. (거의 즉시 적응)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;intellij IDE&lt;/strong&gt; 를 통해 자동완성으로 항상 세미콜론을 붙여온 저로서는 &lt;code class=&quot;language-text&quot;&gt;command&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;shift&lt;/code&gt; + &lt;code class=&quot;language-text&quot;&gt;enter&lt;/code&gt;를 입력하지 않아도 된다는 점이 편리했습니다.&lt;/p&gt;
&lt;p&gt;👍 (편안 ☺️)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Hello Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 세미콜론 생략 가능&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;3️⃣-var-val-️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EF%B8%8F%E2%83%A3-var-val-%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;3️⃣ var val ️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3️⃣ var, val ⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 변수를 선언할 때 &lt;code class=&quot;language-text&quot;&gt;final&lt;/code&gt; 키워드를 사용하여 불변 변수를 선언합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;var&lt;/code&gt; 키워드를 사용하여 변수를 선언하고, &lt;code class=&quot;language-text&quot;&gt;val&lt;/code&gt; 키워드를 사용하여 불변 변수를 선언합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;변경 가능한 name&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 변경 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; immutableName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;변경 불가능한 name&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 재할당 불가능&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;4️⃣-nullable-non-nullable-type-️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4%EF%B8%8F%E2%83%A3-nullable-non-nullable-type-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;4️⃣ nullable non nullable type ️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4️⃣ Nullable, Non-Nullable Type ⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;을 허용하는 &lt;strong&gt;Nullable&lt;/strong&gt; 타입과 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;을 허용하지 않는 &lt;strong&gt;Non-Nullable&lt;/strong&gt; 타입을 구분합니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; 을 허용하는 변수에는 &lt;code class=&quot;language-text&quot;&gt;?&lt;/code&gt; 를 붙여 &lt;strong&gt;Nullable&lt;/strong&gt; 타입으로 선언할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Non-Nullable&lt;/strong&gt; 타입을 다룰 때에는 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;에 대한 걱정 없이 코딩할 수 있기 때문에 코드의 안정성을 높일 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; nullableName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// null 허용&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; nonNullableName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// null 허용하지 않음, compile error 발생&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;5️⃣-nullable-type을-다룰-때-사용하는-safe-call-operator---elvis-operator--️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5%EF%B8%8F%E2%83%A3-nullable-type%EC%9D%84-%EB%8B%A4%EB%A3%B0-%EB%95%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-safe-call-operator---elvis-operator--%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;5️⃣ nullable type을 다룰 때 사용하는 safe call operator   elvis operator  ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5️⃣ Nullable Type을 다룰 때 사용하는 Safe call operator &lt;code class=&quot;language-text&quot;&gt;?.&lt;/code&gt; , Elvis operator &lt;code class=&quot;language-text&quot;&gt;?:&lt;/code&gt; ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;가장 마음에 드는 &lt;strong&gt;Kotlin&lt;/strong&gt;의 기능들 &lt;strong&gt;Safe call operator&lt;/strong&gt; 와 &lt;strong&gt;Elvis operator&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;p&gt;저희는 &lt;strong&gt;Java&lt;/strong&gt; 로 Reference type을 다룰 때 항상 &lt;code class=&quot;language-text&quot;&gt;NPE&lt;/code&gt;에 대한 두려움을 가지고 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; check를 해왔습니다 😭&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 8&lt;/strong&gt; 이후로 &lt;code class=&quot;language-text&quot;&gt;Optional&lt;/code&gt;을 통해 모던한 코드로 &lt;code class=&quot;language-text&quot;&gt;NPE&lt;/code&gt;를 방지할 수 있지만, &lt;code class=&quot;language-text&quot;&gt;Optional&lt;/code&gt; 객체를 생성해서 활용해야 하는 번거로움이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;으로 코딩할 때는 추가 객체 생성 없이 &lt;code class=&quot;language-text&quot;&gt;?.&lt;/code&gt; , &lt;code class=&quot;language-text&quot;&gt;?:&lt;/code&gt; 연산자를 사용하여 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; check와 동시에 간결하게 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;✅ &lt;strong&gt;Java&lt;/strong&gt; 의 &lt;code class=&quot;language-text&quot;&gt;Optional&lt;/code&gt;, &lt;strong&gt;Kotlin&lt;/strong&gt; 의 &lt;strong&gt;Safe call operator&lt;/strong&gt; 코드 비교&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;?.&lt;/code&gt; operator는 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt; check를 하고, &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;이 아닐 경우에만 함수를 호출합니다.&lt;/p&gt;
&lt;p&gt;아래 두 코드는 모두 &lt;code class=&quot;language-text&quot;&gt;book&lt;/code&gt;을 찾고 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;이 아닐 경우 제목을 변경하는 코드입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 코드의 경우 &lt;code class=&quot;language-text&quot;&gt;Optional&lt;/code&gt; 처리하기 위해 &lt;code class=&quot;language-text&quot;&gt;findBook()&lt;/code&gt; 메서드 내부적으로 &lt;code class=&quot;language-text&quot;&gt;Optional&lt;/code&gt;을 생성하여 &lt;code class=&quot;language-text&quot;&gt;return&lt;/code&gt;하도록 코딩해야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 코드의 경우 추가 객체 생성 없이, 언어에서 지원하는 &lt;strong&gt;Safe call operator&lt;/strong&gt; 를 통해 &lt;strong&gt;Java&lt;/strong&gt; 코드보다 훨씬 간결하게 작성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1216px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/fb26d72ab9e23c62fb9158fe5a88be37/9d8cf/safe-call.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 15%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlklEQVR42jWPWQ6EMAxDOVK3JHRhmTIgcf8DeVw0fDw5kWJbmXIp2KrBNEFEiSCEAO8dNcJKRTFFM0ElfavY9xU1K2IcdwHBeyoJHpPZjL7uuI4TvXcsW0OSCO8cDRG5NpYkFM04jy/u+8KHgYWBy9LIAmVhpCdJwKQMdDSPhqHv/AaW1h6DqFHt+UI52zz/d3lILB38AJy2YnbzirzOAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/fb26d72ab9e23c62fb9158fe5a88be37/263a4/safe-call.webp 480w,
/static/fb26d72ab9e23c62fb9158fe5a88be37/a6361/safe-call.webp 960w,
/static/fb26d72ab9e23c62fb9158fe5a88be37/de35b/safe-call.webp 1216w&quot;
              sizes=&quot;(max-width: 1216px) 100vw, 1216px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/fb26d72ab9e23c62fb9158fe5a88be37/9aebd/safe-call.png 480w,
/static/fb26d72ab9e23c62fb9158fe5a88be37/a91f8/safe-call.png 960w,
/static/fb26d72ab9e23c62fb9158fe5a88be37/9d8cf/safe-call.png 1216w&quot;
            sizes=&quot;(max-width: 1216px) 100vw, 1216px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/fb26d72ab9e23c62fb9158fe5a88be37/9d8cf/safe-call.png&quot;
            alt=&quot;[safe-call.png]&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;✅ &lt;strong&gt;Java&lt;/strong&gt; 의 &lt;code class=&quot;language-text&quot;&gt;Optional&lt;/code&gt;, &lt;strong&gt;Kotlin&lt;/strong&gt; 의 &lt;strong&gt;Elvis operator&lt;/strong&gt; 코드 비교&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;?:&lt;/code&gt; operator는 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;일 경우 대체 값을 지정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;아래 두 코드는 모두 &lt;code class=&quot;language-text&quot;&gt;book&lt;/code&gt;을 찾고 &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;일 경우 비즈니스 예외를 발생시키고, &lt;code class=&quot;language-text&quot;&gt;null&lt;/code&gt;이 아닐 경우 제목을 변경하는 코드입니다.&lt;/p&gt;
&lt;p&gt;이 예제 역시 마찬가지로 &lt;strong&gt;Java&lt;/strong&gt; 로 작성한 코드보다 훨씬 간결하게 작성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1510px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/45454b70cc11c03b909f5ec0b8fba802/1d574/elvis-operator.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 16.249999999999996%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAk0lEQVR42j2OWRaDIAxF3ZGIQCDMVm33v6HXQIePe/LxpizMAckbGCPsBtZYOOegtZZLiDHiKIyeI87W8HreKCWCiMAcEUJAYIb3YeYW8oSWPM7HgVo7ehfaMcumiTxSZNzXhVoyck5SZhAkl3OegwNyHs5aKRxLY0XYjYbeNyilJqPMiT6+HahNfbWPZ13XP7/MG0aAYvD4b3PFAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/45454b70cc11c03b909f5ec0b8fba802/263a4/elvis-operator.webp 480w,
/static/45454b70cc11c03b909f5ec0b8fba802/a6361/elvis-operator.webp 960w,
/static/45454b70cc11c03b909f5ec0b8fba802/67619/elvis-operator.webp 1510w&quot;
              sizes=&quot;(max-width: 1510px) 100vw, 1510px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/45454b70cc11c03b909f5ec0b8fba802/9aebd/elvis-operator.png 480w,
/static/45454b70cc11c03b909f5ec0b8fba802/a91f8/elvis-operator.png 960w,
/static/45454b70cc11c03b909f5ec0b8fba802/1d574/elvis-operator.png 1510w&quot;
            sizes=&quot;(max-width: 1510px) 100vw, 1510px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/45454b70cc11c03b909f5ec0b8fba802/1d574/elvis-operator.png&quot;
            alt=&quot;[safe-call.png]&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;p&gt;&lt;strong&gt;사랑해요 safe call, elvis .. 🥰&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;6️⃣-in-키워드-️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6%EF%B8%8F%E2%83%A3-in-%ED%82%A4%EC%9B%8C%EB%93%9C-%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;6️⃣ in 키워드 ️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6️⃣ in 키워드 ⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;in&lt;/code&gt; 키워드를 사용하여 값이 어떤 범위에 포함되는지 편리하게 검사 가능합니다.&lt;/p&gt;
&lt;p&gt;항상 범위에 대한 검사를 하기 위해 &lt;code class=&quot;language-text&quot;&gt;&gt;=&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;&amp;lt;=&lt;/code&gt; 연산자를 사용했는데, &lt;code class=&quot;language-text&quot;&gt;in&lt;/code&gt; 키워드로 한 번에 해결 가능합니다 😊&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// kotlin&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; num &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;num &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// java&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; num &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;out&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; num &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; num &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;7️⃣-is-키워드-smart-casting-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#7%EF%B8%8F%E2%83%A3-is-%ED%82%A4%EC%9B%8C%EB%93%9C-smart-casting-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;7️⃣ is 키워드 smart casting ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;7️⃣ is 키워드, Smart casting ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;is&lt;/code&gt; 키워드를 사용하여 타입을 검사할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;is&lt;/code&gt; 키워드는 &lt;strong&gt;Java&lt;/strong&gt; 의 &lt;code class=&quot;language-text&quot;&gt;instanceof&lt;/code&gt;와 비슷한 기능을 제공하지만, 더 간결하고 편리한 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;is&lt;/code&gt; 키워드로 타입을 검사하면, 해당 타입으로 자동 캐스팅되어 추가 변환 없이 바로 사용할 수 있습니다 !!&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;is&lt;/code&gt; 키워드에 &lt;code class=&quot;language-text&quot;&gt;!&lt;/code&gt;를 사용하여 &lt;code class=&quot;language-text&quot;&gt;not&lt;/code&gt; 연산도 가능합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// obj는 is 키워드로 검사된 이후 자동으로 형 변환되어 사용 가능합니다.&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Length : &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;8️⃣-string-template-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#8%EF%B8%8F%E2%83%A3-string-template-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;8️⃣ string template ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;8️⃣ String template ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 String template 을 사용하여 문자열을 간결하게 작성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그동안 &lt;strong&gt;Java&lt;/strong&gt; 개발자들은 &lt;code class=&quot;language-text&quot;&gt;String.format()&lt;/code&gt; 같은 메서드를 사용하여 문자열을 조합하거나 직접 붙여서 표현했는데,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 String template을 사용하여 더 간결하게 문자열을 조합할 수 있습니다. (진짜 .. 너무편리해요 😭)&lt;/p&gt;
&lt;p&gt;사용법은 간단합니다 &lt;code class=&quot;language-text&quot;&gt;${변수명}&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;$변수명&lt;/code&gt; 의 형태로 사용하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;이름은 : &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이름은 : 들국화&lt;/span&gt;
log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;name : &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;name&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// name : 들국화&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;9️⃣-raw-string-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#9%EF%B8%8F%E2%83%A3-raw-string-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;9️⃣ raw string ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;9️⃣ Raw String ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;Raw String은 큰따옴표 세 개(&lt;code class=&quot;language-text&quot;&gt;&quot;&quot;&quot;&lt;/code&gt;)로 감싸진 문자열입니다 ! Raw String을 사용하면&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;여러 줄의 문자열 작성 가능합니다.&lt;/li&gt;
&lt;li&gt;문자열 Escape 처리 없이 그대로 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;코드 블록처럼 들여쓰기를 작성 가능하며 마지막에 &lt;code class=&quot;language-text&quot;&gt;trimIndent()&lt;/code&gt;로 정리 가능합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal multiline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&quot;
    Hello, World!
    &quot;안녕하십니까 ?&quot; 
    &quot;Java 개발자가 생각하는 &quot;Kotlin&quot; 장점은 무엇인가요 ?&quot;
    &quot;Kotlin은 Java보다 간결하고 실용적이라고 생각합니다.&quot;
&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trimIndent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Hello, World!&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &quot;안녕하십니까 ?&quot; &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &quot;Java 개발자가 생각하는 Kotlin 장점은 무엇인가요 ?&quot;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &quot;Kotlin은 Java보다 간결하고 실용적이라고 생각합니다.&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;앞으로-우리는-이런-코드를-보지-않아도-됩니다-&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%9E%EC%9C%BC%EB%A1%9C-%EC%9A%B0%EB%A6%AC%EB%8A%94-%EC%9D%B4%EB%9F%B0-%EC%BD%94%EB%93%9C%EB%A5%BC-%EB%B3%B4%EC%A7%80-%EC%95%8A%EC%95%84%EB%8F%84-%EB%90%A9%EB%8B%88%EB%8B%A4-&quot; aria-label=&quot;앞으로 우리는 이런 코드를 보지 않아도 됩니다  permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;앞으로 우리는 이런 코드를 보지 않아도 됩니다........&lt;/strong&gt; 😇&lt;/h3&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1256px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/2e938f4637562b3214338948bcd27819/30373/raw-string-omg.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 45.62500000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABRElEQVR42oWS/XKDIBDEfZqKyNfBgaDGTPL+77Q9TRPbaTv5Y0eYgR+7e3bcKjxnRGfQSCO5AZVGsNeYyGCrHtfLhNv9jsvCmAuj5IDRjBjH3+oSTwiUYIwBMyPnDAoBHAneO3D2GAYtGo4LWv8NOoH5BLbWsF02LHPB/TajlIi6JKxLRErhLewExgdwd9H3PayR6FnctQjKhFYcWrKwzkHvF/8Ba63RcWkvh9ZaGBF5g9oIPCfUq/Qr0GlNSBPJOiAVqaTGY19WRkzyoPQeg0eXpwZP3xwqJWsthywCOzhyIAF49khVgIUeMBFxQJZKSB7JYUQKZndYX8BSytGjd+JAJl9rxZQTCkdROKPqM/az12cVP4DHULZNIscDtqwr5lqw1CxfOTPq90N5RrbOQg0K/Ud/lKskupIBKTU89PXbvAN+AqZ6GBlQMV3tAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2e938f4637562b3214338948bcd27819/263a4/raw-string-omg.webp 480w,
/static/2e938f4637562b3214338948bcd27819/a6361/raw-string-omg.webp 960w,
/static/2e938f4637562b3214338948bcd27819/deed9/raw-string-omg.webp 1256w&quot;
              sizes=&quot;(max-width: 1256px) 100vw, 1256px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2e938f4637562b3214338948bcd27819/9aebd/raw-string-omg.png 480w,
/static/2e938f4637562b3214338948bcd27819/a91f8/raw-string-omg.png 960w,
/static/2e938f4637562b3214338948bcd27819/30373/raw-string-omg.png 1256w&quot;
            sizes=&quot;(max-width: 1256px) 100vw, 1256px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2e938f4637562b3214338948bcd27819/30373/raw-string-omg.png&quot;
            alt=&quot;[raw-string-omg.png]&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;-when--if--try-catch-expression-️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-when--if--try-catch-expression-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot; when  if  try catch expression ️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;🔟 when , if , try-catch Expression ⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;when&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;try-catch&lt;/code&gt; 가 단순히 &lt;strong&gt;구문(Statement)&lt;/strong&gt; 이 아니라 &lt;strong&gt;식(Expression)&lt;/strong&gt; 으로 동작합니다.&lt;/p&gt;
&lt;p&gt;Expression은 결과값을 반환할 수 있는 코드 블록을 말합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 모두 &lt;strong&gt;식&lt;/strong&gt;으로 동작하기 때문에 값을 반환할 수 있으며, 더 간결하게 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;if&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서 &lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt; 문은 식으로 동작합니다. 즉, 값을 반환할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt; 문 내부에서 값을 반환할 수 없지만, &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;if&lt;/code&gt; 식 자체를 반환할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/91d5d1cf26230ee2400fe9e1d830630e/4e1ad/if.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 22.916666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAyUlEQVR42n2PUXLDIAxEuY1rQIBBwjgZ2+T+h9oK0v6l/dhhBu0+aY1wRYoBRy14NkEVwX13PB4HWmtgzggpIRUBb4TzGPMbOWedFfR+4zxP7FLQVEaSg3BUGE9gUeN1XdM04CEQtlywKTCQR28Rr+vAPvy6tPeOtjfEmBCJYHLSQAn4sharyo53XafsauGcA9eqHoYjbZJJG3lQpOlZluXtnVkHE8Yw+Bn8JO8VKArMDK8X0NT4/5wx7of8J1BrilbKLP8u/gV+Azn7nBFvOWG8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/91d5d1cf26230ee2400fe9e1d830630e/263a4/if.webp 480w,
/static/91d5d1cf26230ee2400fe9e1d830630e/a6361/if.webp 960w,
/static/91d5d1cf26230ee2400fe9e1d830630e/0b34d/if.webp 1920w,
/static/91d5d1cf26230ee2400fe9e1d830630e/23e01/if.webp 2008w&quot;
              sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/91d5d1cf26230ee2400fe9e1d830630e/9aebd/if.png 480w,
/static/91d5d1cf26230ee2400fe9e1d830630e/a91f8/if.png 960w,
/static/91d5d1cf26230ee2400fe9e1d830630e/ac7a9/if.png 1920w,
/static/91d5d1cf26230ee2400fe9e1d830630e/4e1ad/if.png 2008w&quot;
            sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/91d5d1cf26230ee2400fe9e1d830630e/ac7a9/if.png&quot;
            alt=&quot;[if.png]&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;when&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서 &lt;code class=&quot;language-text&quot;&gt;when&lt;/code&gt; expression 사용해 &lt;strong&gt;Java&lt;/strong&gt; 의 &lt;code class=&quot;language-text&quot;&gt;switch-case&lt;/code&gt; 문을 완벽히 대체할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;when&lt;/code&gt; expression은 &lt;code class=&quot;language-text&quot;&gt;default&lt;/code&gt; 대신에 &lt;code class=&quot;language-text&quot;&gt;else&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;식으로 동작하기 때문에 &lt;code class=&quot;language-text&quot;&gt;when&lt;/code&gt; 절이 동작한 후 바로 값을 반환할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;when + enum&lt;/code&gt;을 같이 사용할 경우 강력하고 간결한 분기 처리가 가능합니다 !!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 enum 상수에 대해 분기 처리가 강제되며, 컴파일 단계에서 누락된 분기 처리를 자동으로 감지하여 실수를 방지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// else를 사용하지 않고 전체 상수에 대한 분기 처리 강제.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BookType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    BookType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PAPER &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handlePaperBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    BookType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;EBOOK &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleEbook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    BookType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;AUDIO &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleAudio&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// else 사용, 전체 상수에 대한 분기 처리를 하지 않는 대신에 else로 나머지 케이스를 처리.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BookType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    BookType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PAPER &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handlePaperBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handleEtc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Java&lt;/strong&gt; 에서도 14 버전부터 &lt;code class=&quot;language-text&quot;&gt;switch expression&lt;/code&gt; 이 정식 release 되어 위와 같은 기능으로 사용할 수 있습니다 !&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;try-catch&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서 &lt;code class=&quot;language-text&quot;&gt;try-catch&lt;/code&gt; 문 또한 식으로 동작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;safeParseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;str&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      str&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NumberFormatException&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 변환 실패 시 기본값 return&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;class&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#class&quot; aria-label=&quot;class permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Class&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서의 클래스와 &lt;strong&gt;Java&lt;/strong&gt; 에서의 클래스를 코드로 먼저 살펴보고, &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 얼마나 간결하게 사용 가능한지 비교해 보겠습니다.&lt;/p&gt;
&lt;p&gt;왼쪽이 &lt;strong&gt;Kotlin&lt;/strong&gt;  클래스, 오른쪽이 &lt;strong&gt;Java&lt;/strong&gt;  클래스 입니다.&lt;/p&gt;
&lt;p&gt;처음 &lt;strong&gt;Kotlin&lt;/strong&gt; 을 접했을 때 가장 놀랐던 부분이 바로 Class 선언 부분이었습니다. &quot;아니.. 왜 이렇게 뭐가 없지 ? 🤔 &quot; 라는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 874px; &quot;
    &gt;
      &lt;a
    class=&quot;gatsby-resp-image-link&quot;
    href=&quot;/static/2130f6574882fe096be3ade3f7abef94/7bff9/class_diff.png&quot;
    style=&quot;display: block&quot;
    target=&quot;_blank&quot;
    rel=&quot;noopener&quot;
  &gt;
    &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 86.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAACBklEQVR42pWUXXLiMBCEOVGwrX/Jsg3GLATI1qaK+x+l0yMbKiQPCw9TqrJGn2emW1pdPq/4/HfGcdpgM07YH94xTnuM+z/YbDfo+h7T3ysu1yuOhwm73YjT+YLj+6nEYVn7fkCICSvvI7xzSMEhhIiU2rIRubZtQvABzido4+B0U3JDCLDW3sPxW9M0qKqKwNShjw59kkQP7z201mWzrusSPma0KWFIFtlrBGuYMwNucctdBQK9VRii56Eep9MJW7a6Xq+/AVtYx59ZzTDlhxJSVVM397wFmGEMN8uHpiQppR6SZmBANDXaYNEPA8EBamnzASjJSpuHjZ9JkmNYYfQG0RkYVSPGUGZn1FzpL2D9A/I9wq1C2yA5xVUh54wYEoF2meMLQMeDAgyszntHB9AZVN9oVYRs25bVOyqunwPGNkMTuAkV9jli6Ed8nA/YDW2xUamWQGPUs8CuzFDrCsFwhtoVtQUibnhbv3Gd234KeFNZ8lSxS10OS4gjJIqFGM+JkmZgpiDbIWPcTVTYw3FmKXGWxt6d8SQwF2DyvCnJI0VpfxZIRBGwNXP1LwA959cwalheOzmTede7riv+TFRe7vVLMwzO8uD8cNyunlhHs7rqFWOLypaPRjQVMtvt+aTJ3JyVts0yw/Xy2vwHKHc8EShmNtZBiW0IkvYEJKOw9KIYXdYv66Qqhm8V+FoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2130f6574882fe096be3ade3f7abef94/263a4/class_diff.webp 480w,
/static/2130f6574882fe096be3ade3f7abef94/d7b9e/class_diff.webp 874w&quot;
              sizes=&quot;(max-width: 874px) 100vw, 874px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2130f6574882fe096be3ade3f7abef94/9aebd/class_diff.png 480w,
/static/2130f6574882fe096be3ade3f7abef94/7bff9/class_diff.png 874w&quot;
            sizes=&quot;(max-width: 874px) 100vw, 874px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2130f6574882fe096be3ade3f7abef94/7bff9/class_diff.png&quot;
            alt=&quot;[class_diff.png]&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;1️⃣-클래스-선언-시-body를-생략할-수-있습니다-️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F%E2%83%A3-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%84%A0%EC%96%B8-%EC%8B%9C-body%EB%A5%BC-%EC%83%9D%EB%9E%B5%ED%95%A0-%EC%88%98-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4-%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;1️⃣ 클래스 선언 시 body를 생략할 수 있습니다 ️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1️⃣ 클래스 선언 시 body를 생략할 수 있습니다. ⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;간결 그 자체, 중괄호 생략이 가능합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;2️⃣-생성자-선언과-동시에-field를-선언할-수-있습니다-️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EF%B8%8F%E2%83%A3-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%84%A0%EC%96%B8%EA%B3%BC-%EB%8F%99%EC%8B%9C%EC%97%90-field%EB%A5%BC-%EC%84%A0%EC%96%B8%ED%95%A0-%EC%88%98-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4-%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;2️⃣ 생성자 선언과 동시에 field를 선언할 수 있습니다 ️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2️⃣ 생성자 선언과 동시에 field를 선언할 수 있습니다. ⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 생성자를 선언함과 동시에 field를 선언할 수 있습니다. 이 문법을 통해서 생성자를 통해 field를 초기화하는 코드를 줄일 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Class를 선언하면서 소괄호를 통해 생성자를 동시에 선언할 수 있다 !! &lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;3️⃣-getter-setter를-자동으로-생성해-줍니다-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EF%B8%8F%E2%83%A3-getter-setter%EB%A5%BC-%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C-%EC%83%9D%EC%84%B1%ED%95%B4-%EC%A4%8D%EB%8B%88%EB%8B%A4-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;3️⃣ getter setter를 자동으로 생성해 줍니다 ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3️⃣ getter, setter를 자동으로 생성해 줍니다. ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;정말 너무 마음에 드는 기능입니다. 반복적으로 작성해야 했던 &lt;code class=&quot;language-text&quot;&gt;getter&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;setter&lt;/code&gt;를 자동으로 생성해 줍니다.&lt;/p&gt;
&lt;p&gt;field를 만들면 compile 단계에서 자동 생성해 줍니다.&lt;/p&gt;
&lt;p&gt;정말 마음에 드는 건 lombok 없이 간결한 코드를 작성할 수 있다는 점입니다.&lt;/p&gt;
&lt;br&gt;
&lt;h3 id=&quot;4️⃣-객체-생성-시-new-키워드-생략할-수-있습니다-️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4%EF%B8%8F%E2%83%A3-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1-%EC%8B%9C-new-%ED%82%A4%EC%9B%8C%EB%93%9C-%EC%83%9D%EB%9E%B5%ED%95%A0-%EC%88%98-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;4️⃣ 객체 생성 시 new 키워드 생략할 수 있습니다 ️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4️⃣ 객체 생성 시 new 키워드 생략할 수 있습니다. ⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서 객체를 생성할 때 반복적으로 사용하던 &lt;code class=&quot;language-text&quot;&gt;new&lt;/code&gt; 키워드를 생략할 수 있습니다 ! 와우..&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// new 키워드 생략 가능&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;5️⃣-getter-setter-호출-️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5%EF%B8%8F%E2%83%A3-getter-setter-%ED%98%B8%EC%B6%9C-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;5️⃣ getter setter 호출 ️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5️⃣ getter, setter 호출 ⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;getter&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;setter&lt;/code&gt;를 호출할 때는 class 멤버변수에 접근하듯 사용하면 &lt;code class=&quot;language-text&quot;&gt;getter&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;setter&lt;/code&gt;를 호출해 줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// getter 호출 &lt;/span&gt;
  
  book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Kotlin 장점에 대하여&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// setter 호출&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;6️⃣-클래스-상속-인터페이스-구현-️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6%EF%B8%8F%E2%83%A3-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%83%81%EC%86%8D-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EA%B5%AC%ED%98%84-%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;6️⃣ 클래스 상속 인터페이스 구현 ️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6️⃣ 클래스 상속, 인터페이스 구현 ⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;extends&lt;/code&gt; 키워드를 사용하여 상속하고, &lt;code class=&quot;language-text&quot;&gt;implements&lt;/code&gt; 키워드를 사용하여 인터페이스를 구현합니다.&lt;/p&gt;
&lt;p&gt;하지만 &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 간결하게 &lt;code class=&quot;language-text&quot;&gt;extends&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;implements&lt;/code&gt; 모두 &lt;code class=&quot;language-text&quot;&gt;:&lt;/code&gt; 콜론으로 표현합니다 !&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서 특이한 점은 &lt;code class=&quot;language-text&quot;&gt;open&lt;/code&gt; 키워드를 사용해 상속 가능한 클래스로 만들어주어야 상속할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; Flyable &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;open&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Animal

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Dog &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Animal&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Bird &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Flyable&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;7️⃣-override-️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#7%EF%B8%8F%E2%83%A3-override-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;7️⃣ override ️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;7️⃣ override ⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 메서드를 오버라이드할 때 &lt;code class=&quot;language-text&quot;&gt;@Override&lt;/code&gt; 어노테이션을 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;override&lt;/code&gt; 키워드를 사용하여 메서드를 오버라이드합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;@Override&lt;/code&gt; 어노테이션을 사용하지 않아도 문제가 없지만,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;override&lt;/code&gt; 키워드를 사용하지 않은 채 function을 재정의하는 경우 compile 단계에서 에러가 발생합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Bird &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Flyable &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// fly logic&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;8️⃣-data-class-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#8%EF%B8%8F%E2%83%A3-data-class-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;8️⃣ data class ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;8️⃣ Data class ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;code class=&quot;language-text&quot;&gt;data class&lt;/code&gt;를 사용하여 &lt;code class=&quot;language-text&quot;&gt;toString()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;equals()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;hashCode(), copy()&lt;/code&gt; 메서드를 자동으로 생성해 주는 클래스를 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;data class&lt;/code&gt; 를 활용하면 반복적으로 &lt;code class=&quot;language-text&quot;&gt;Override&lt;/code&gt;하여 작성하던 Dto 클래스 코드를 매우 간결하게 작성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;copy()&lt;/code&gt; 함수를 통해 간편하게 객체를 복사하면서 일부 속성만 변경된 객체를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;🫒 올리브영에 강연하러 오신 &lt;code class=&quot;language-text&quot;&gt;Josh Long&lt;/code&gt; 😎 형님께서는 &lt;code class=&quot;language-text&quot;&gt;data class&lt;/code&gt; 를 사용하면 lombok을 사용하지 않아도 된다고 말씀하셨습니당 😊&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BookDto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bookDto1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BookDto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bookDto2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;BookDto&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bookDto3 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; bookDto2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;copy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;author &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;수정된 작가&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  
  &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookDto1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookDto3&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookDto1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookDto2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookDto1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookDto3&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  
  &lt;span class=&quot;token comment&quot;&gt;// BookDto(title=Effective Kotlin, author=들국화)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// BookDto(title=Effective Kotlin, author=수정된 작가)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// true&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// false&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h2 id=&quot;function&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#function&quot; aria-label=&quot;function permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Function&lt;/h2&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1️⃣-default-parameter-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F%E2%83%A3-default-parameter-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;1️⃣ default parameter ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1️⃣ Default parameter ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 함수의 매개변수에 기본값을 지정할 수 있습니다 !&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 오버로딩을 통해 기본값을 지정하는 method를 만들어 활용할 수 있지만, &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 오버로딩하지 않고 함수 선언 시 매개변수에 기본값을 지정할 수 있습니다 !! 😊&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 오버로딩을 하지 않는다면 method 내부에서 기본값을 할당하는 코딩을 해야 하지만, &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 함수 선언 시 매개변수에 기본값을 지정할 수 있어 코드가 더 간결해집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;default title&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;default author&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;title&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; author&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; defaultBook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;defaultBook&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Book(title=&apos;default title&apos;, author=&apos;default author&apos;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Book(title=&apos;Effective Kotlin&apos;, author=&apos;들국화&apos;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;2️⃣-named-argument-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2%EF%B8%8F%E2%83%A3-named-argument-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;2️⃣ named argument ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2️⃣ Named argument ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 함수 호출 시 매개변수의 순서 상관없이 이름을 지정하여 인자를 전달할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 매개변수의 순서에 맞게 인자를 전달해야 하지만, &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 매개변수의 이름을 지정하여 인자를 전달할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Named argument&lt;/strong&gt; 덕분에, 매개변수가 많은 함수라도 builder 패턴을 사용하지 않고 간결하게 코드를 작성할 수 있습니다!&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;default title&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  author&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;default author&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  publishDate&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  totalPage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  price&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createDateTime&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createUserId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;//... create book&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// argument의 name을 지정하여 전달할 수 있다.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;createBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  price &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  publishDate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  totalPage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  author &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createDateTime &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LocalDateTime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createUserId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;user1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;3️⃣-확장함수-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EF%B8%8F%E2%83%A3-%ED%99%95%EC%9E%A5%ED%95%A8%EC%88%98-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;3️⃣ 확장함수 ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3️⃣ 확장함수 ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 기존 클래스에 새로운 함수를 추가할 수 있는 확장함수를 제공합니다.&lt;/p&gt;
&lt;p&gt;이 기능은 정말 마음에 드는 기능 중 하나입니다. 제가 &lt;strong&gt;Java&lt;/strong&gt; 를 다루면서 항상 꿈 꾸었던 기능이었습니다 😭&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;예를들어 &lt;code class=&quot;language-text&quot;&gt;List&lt;/code&gt;에서 첫번째 element를 가져오는 기능을 이용하고 싶으면 &lt;code class=&quot;language-text&quot;&gt;org.springframework.util.CollectionUtils&lt;/code&gt; 등과 같은 라이브러리 메서드를 사용해야 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; books &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Java&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt; firstBook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CollectionUtils&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;firstElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그런데 만약 첫번째 element가 아닌 임의의 element 객체를 얻고 싶다면 아래와 같이 Util method를 만들어 사용 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OYUtils&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findAnyElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; list&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; list&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAny&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;orElse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;Book&lt;/span&gt; anyBook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;OYUtils&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAnyElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 라이브러리를 이용하면서도, Util method를 만들면서도 이런 생각을 하죠 .. &quot;아.. 이런 기능은 왜 List에 만들어 놓지 않았을까..? &lt;strong&gt;Java&lt;/strong&gt;  version이 올라가면 생길까 ? &quot; 😭&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;그러나 ! &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 이러한 needs를 확장함수를 통해 간단하게 충족시킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;확장함수 사용법은 아래와 같습니다. &lt;code class=&quot;language-text&quot;&gt;fun &amp;lt;클래스&gt;.함수이름()&lt;/code&gt; 으로 선언하면 됩니다 !&lt;/p&gt;
&lt;p&gt;너무너무 아름답게도 &lt;code class=&quot;language-text&quot;&gt;List&lt;/code&gt;객체라면 전부 제가 확장시킨 함수를 사용할 수 있습니다. 😊&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;T&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;T&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAnyElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAny&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;orElse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; books &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; numbers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; anyBook &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAnyElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; anyNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; numbers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;findAnyElement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;anyBook&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Book(title=&apos;Effective Kotlin&apos;, author=&apos;들국화&apos;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;anyNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;4️⃣-lambda-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4%EF%B8%8F%E2%83%A3-lambda-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;4️⃣ lambda ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4️⃣ lambda ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;함수형 프로그래밍 특징을 가지는 &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;strong&gt;Java&lt;/strong&gt; 와 달리 함수를 일급객체로 취급합니다.&lt;/p&gt;
&lt;p&gt;함수를 parameter로 넘길 수 있고, 변수에 넣을 수 있습니다. 이는 익명 &lt;strong&gt;함수&lt;/strong&gt; 인 lambda를 사용할 때에도 마찬가지입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 &lt;strong&gt;Java&lt;/strong&gt; 와 달리 functional interface를 통해 lambda를 표현할 필요가 없습니다. 함수 식을 선언하고 할당하고, 전달할 수 있습니다 !&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; books &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// function body가 존재하는 lambda&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; eqTitle &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Boolean &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// function body를 생략한 lambda&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; eqTitle2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 중괄호로 감싸진 lambda&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; eqTitle3 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;filterBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; eqTitle&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;filterBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; eqTitle2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;filterBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; eqTitle3&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;filterBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// lambda를 선언하며 전달&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// filter 라는 매개변수는 함수 자체를 매개변수로 받고 있다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;filterBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;books&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filter&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Book&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; Boolean&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filter&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;h3 id=&quot;5️⃣-collections-package-️️️️️&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5%EF%B8%8F%E2%83%A3-collections-package-%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F%EF%B8%8F&quot; aria-label=&quot;5️⃣ collections package ️️️️️ permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5️⃣ collections package ⭐️⭐️⭐️⭐️⭐️&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 의 collections package는 엄청나게 다양하고 매우 편리한 컬렉션 관련 함수, 확장함수를 제공합니다.&lt;/p&gt;
&lt;br&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;✅ Collection을 다루는데 있어서 매우 편리한 함수들을 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;listOf()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;setOf()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;mapOf()&lt;/code&gt; 등의 함수를 통해 읽기전용 collection을 생성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;mutableListOf()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;mutableSetOf()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;mutableMapOf()&lt;/code&gt; 등의 함수를 통해 수정가능한 collection을 생성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;읽기전용&lt;/strong&gt; collection 타입과 &lt;strong&gt;수정가능&lt;/strong&gt;한 collection 타입이 분리되어 있고, &lt;code class=&quot;language-text&quot;&gt;add()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;remove()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;put()&lt;/code&gt; 등의 수정 기능을 가진 함수를 제공하지 않습니다. 읽기 전용임이 매우 명확하게 설계되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 읽기전용 타입은 수정 가능한 함수를 제공하지 않습니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; books&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bookSets&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Set&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; map&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;BookKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// collection을 수정할 수 있는 함수들을 제공하지 않음.&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; mutableBooks&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MutableList&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mutableListOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; mutableBookSets&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MutableSet&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mutableSetOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; mutableBookMap&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MutableMap&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BookKey&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mutableMapOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;BookKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

mutableBooks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
mutableBookSets&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
mutableBookMap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;BookKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또한 &lt;code class=&quot;language-text&quot;&gt;get()&lt;/code&gt; 함수 대신 &lt;code class=&quot;language-text&quot;&gt;[]&lt;/code&gt; 대괄호로 값을 가져올 수 있습니다 !&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; books &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; map &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;BookKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// books.get(0) 대신 books[0] 사용 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; map&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;BookKey&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// map.get(BookKey(&quot;key&quot;)) 대신 map[BookKey(&quot;key&quot;)] 사용 가능&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;✅ 다양하고 편리한 확장함수를 제공합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 에서는 &lt;strong&gt;Stream API&lt;/strong&gt;를 통해 사용하던 편리한 함수들을 &lt;strong&gt;Kotlin&lt;/strong&gt; 에서는 편리하게 구현되어 있는 확장함수로 &lt;strong&gt;Stream&lt;/strong&gt; 객체 생성 없이 사용할 수 있습니다 !!&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;filter()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;map()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;mapNotNull()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;all()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;none()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;any()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;sortedBy()&lt;/code&gt;,
&lt;code class=&quot;language-text&quot;&gt;firtst()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;firstOrNull()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;last()&lt;/code&gt;, &lt;code class=&quot;language-text&quot;&gt;lastOrNull()&lt;/code&gt; 등등 와우.. ☺️&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; books&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;들국화&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Book&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// stream 객체 생성 hell에서 벗어난 모습&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; filteredBooks&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Book&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Effective Kotlin&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; titles&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;String&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; book &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; book&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;title &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; nullableBook&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;firstOrNull&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; book&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; nullableLastBook&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lastOrNull&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; lastBook&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Book &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; books&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;br&gt;
&lt;br&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; 를 주로 다루는 개발자가 &lt;strong&gt;Kotlin&lt;/strong&gt; 을 접하면서 느낀 장점들을 정리하고 별점을 매겨보았습니다.&lt;/p&gt;
&lt;p&gt;간결하고 효율적인 문법, null에 대한 고민을 줄여주는 설계, 함수형 프로그래밍의 지원, 확장 함수 등.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt; 은 &lt;strong&gt;Java&lt;/strong&gt; 에 익숙한 개발자라면 쉽게 접근할 수 있으며, 빠르게 학습할 수 있는 언어입니다. 또한, OOP와 FP의 장점을 모두 지닌 강력한 언어입니다.&lt;/p&gt;
&lt;p&gt;지금까지 &lt;strong&gt;Java&lt;/strong&gt; 만 다뤄본 분이 계시다면 &lt;strong&gt;Kotlin&lt;/strong&gt; 을 한번 경험해보세요! 😊&lt;/p&gt;
&lt;p&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;br&gt; 
&lt;br&gt; 
&lt;br&gt; </content:encoded></item><item><title><![CDATA[TeamCity로 윈도우 클라이언트 배포 파이프라인 만들기]]></title><description><![CDATA[안녕하세요 올리브영의 POS서버를 맡고있는 개발자 Q평E평 입니다. 여러분은 운영 중인 시스템 배포를 어떻게 하고 있으신가요? 최근엔 다양한 CI/CD…]]></description><link>https://oliveyoung.tech/2024-12-06/sco-teamcity/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-06/sco-teamcity/</guid><pubDate>Fri, 06 Dec 2024 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요 올리브영의 POS서버를 맡고있는 개발자 &lt;strong&gt;Q평E평&lt;/strong&gt; 입니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;여러분은 운영 중인 시스템 배포를 어떻게 하고 있으신가요?&lt;/p&gt;
&lt;p&gt;최근엔 다양한 CI/CD 도구를 사용하여 손쉽게 배포 파이프라인을 만들 수 있는데요, 이번 글에서는 저희 올리브영 셀프계산대의 TeamCity 기반 배포 파이프라인 구축 과정을 소개하려고 합니다!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;걱정거리가 많았던 배포 시스템에서, 점진적으로 발전해 가는 저희 POS팀의 배포 이야기 들려드리겠습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;pos팀은-어떻게-배포해-왔을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pos%ED%8C%80%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B0%B0%ED%8F%AC%ED%95%B4-%EC%99%94%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;pos팀은 어떻게 배포해 왔을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;POS팀은 어떻게 배포해 왔을까?&lt;/h2&gt;
&lt;p&gt;놀랍게도 올리브영의 &lt;strong&gt;POS&amp;#x26;셀프계산대 및 서버&lt;/strong&gt;는 얼마 전까지만 하더라도 아래와 같이 &lt;strong&gt;수기 작업이 많은 배포 프로세스&lt;/strong&gt;를 거쳤습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;개발자 PC에서 변경된 브랜치를 &lt;code class=&quot;language-text&quot;&gt;Pull&lt;/code&gt; 후 빌드&lt;/li&gt;
&lt;li&gt;빌드된 전체 파일 중에서 배포할 파일 선별&lt;/li&gt;
&lt;li&gt;파일을 배포
&lt;ul&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;Server&lt;/code&gt; : class 파일을 SFTP를 통해 수기로 옮기고 서버를 재기동&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;POS Client&lt;/code&gt; : 배포될 파일을 특정한 물리 장비에 수기로 옮겨 놓으면 배포 솔루션을 통해 배포&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;셀프계산대 Client&lt;/code&gt; : S3에 배포될 파일을 수기로 올려놓고 버전 파일의 정보를 수기로 변경하여 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;p&gt;지금은 어느 정도 개선되었지만, 빌드된 파일 하나를 수기로 변경하고 몇 번을 다시 확인했는지...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;심각한 걱정이&lt;/strong&gt;인 저로서는 매우 힘든 작업이었습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;올리브영의-현재-배포-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81%EC%9D%98-%ED%98%84%EC%9E%AC-%EB%B0%B0%ED%8F%AC-%EC%83%81%ED%99%A9&quot; aria-label=&quot;올리브영의 현재 배포 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영의 현재 배포 상황&lt;/h2&gt;
&lt;p&gt;올리브영은 현재 전사적인 배포 도구로 &lt;strong&gt;TeamCity&lt;/strong&gt;를 선택하여 사용하고 있습니다. 관련하여 과거 &lt;a href=&quot;https://oliveyoung.tech/blog/2022-05-03/How-to-Set-up-Build-Process-with-Teamcity/&quot;&gt;“코코리&quot;님이 작성하신 글&lt;/a&gt;이 있습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1034px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7f179ed6b3951640fe0e37011f8a9f92/b69f7/teamcity_thumbnail.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.041666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAABiUlEQVR42lWRPUsjURSGJ6xis1FZTDDGRG1WsNyF2AhiaWyEFFqJGJT4sai4KNosbCdW/gIrf4E/QLFTI0YbJX4hFqPBfO4kM3cmk2fvHd1iL7xcOJfzcJ5ztVgsRiAQJBzuoiPgp7P9C12hbsLdEUKhIG1tLaTP06hjWgJhOzj1Oraj4nq3JRzvzRI2WjQaRdM0Pmk+Br4m8U+MoHX4vFpTs59IT4LM1TOFUoG3fIlCsYyuv5LLvaG/vMrkKJXKVGuWhEpgb2+f19zc9JnvQ0vEJ0dpjU8xHN8nOZfjxxrcZA0ymXPO0hdcZC45PDrm5PRM1q64vsnyousSaGIpYCQakUAf4YEEraltgv3fGJw+YPUXLKzUmV10ub1XqjUqhulNoTSFXZfqrqevlGumeJ9wPLFHYjJLahnGfjeY2GgwP+eSnFewBjMpuHuwMYwi5bLh6VUqf8jni1SrpqdaU/kH3NmFn1symy6b6w3WJXBt66Mms7oBj082jmPJZvEOMMXHzsR/UZ/yFwcDZ5uIM6gFAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7f179ed6b3951640fe0e37011f8a9f92/263a4/teamcity_thumbnail.webp 480w,
/static/7f179ed6b3951640fe0e37011f8a9f92/a6361/teamcity_thumbnail.webp 960w,
/static/7f179ed6b3951640fe0e37011f8a9f92/e4622/teamcity_thumbnail.webp 1034w&quot; sizes=&quot;(max-width: 1034px) 100vw, 1034px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7f179ed6b3951640fe0e37011f8a9f92/9aebd/teamcity_thumbnail.png 480w,
/static/7f179ed6b3951640fe0e37011f8a9f92/a91f8/teamcity_thumbnail.png 960w,
/static/7f179ed6b3951640fe0e37011f8a9f92/b69f7/teamcity_thumbnail.png 1034w&quot; sizes=&quot;(max-width: 1034px) 100vw, 1034px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7f179ed6b3951640fe0e37011f8a9f92/b69f7/teamcity_thumbnail.png&quot; alt=&quot;코코리님의 TeamCity 관련 글&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;코코리님의 TeamCity 관련 글
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;다행히 &lt;strong&gt;API 서버&lt;/strong&gt;의 경우 올해 “IDC 물리 장비 + 기업 프레임워크”에서 “AWS 인스턴스 + Spring Boot”로 이관을 진행했습니다.
지금은 &lt;strong&gt;TeamCity&lt;/strong&gt;를 통한 배포 파이프라인을 구성하여 현재는 마음 편한 배포를 하고 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 저희는 발맞추어 POS&amp;#x26;셀프계산대도 &lt;strong&gt;TeamCity&lt;/strong&gt;를 사용한 배포 파이프라인을 점진적으로 적용하기 시작합니다!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;배포-프로세스-개선의-배경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%ED%8F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EA%B0%9C%EC%84%A0%EC%9D%98-%EB%B0%B0%EA%B2%BD&quot; aria-label=&quot;배포 프로세스 개선의 배경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배포 프로세스 개선의 배경&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;현재 올리브영의 POS와 셀프계산대는 배포 프로세스가 다르므로, 모든 프로세스 개선이 완료된 셀프계산대를 기준으로 이야기를 진행하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div style=&quot;max-width: 500px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 526px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/55310d147cda52b683cf642bf7b78e15/55e98/sco.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 96.25000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAAE+UlEQVR42h2T2W9UZRjGT9REb7wwkcQbr0y8MYYoIoot3aazdfbtzDlzZs7snc4MM9NtOrRDp63dSy1bW3Baii1Y2YorBDUBA0RBBExAo3hhYqJ/gTHx5ucnF2+ek+/ke9/3exYpXdLJ9ccp9YXJD8bJjnWSHNMojMiUBjRiCQ9eTztedzs+UVrQRiYt01vSGK4lmDuS5+yZGseP9DE3nkMKdmpECjH0TAAtGyJd7UQfEmc9HmIFGaeioeaGUbND+DsrmH0hCntVyv061WqU0ckES4cLjNSSVAdiSM6AC6/iQw7a8fhtqFoA2edE9rvxuqwMTS3y+YN/WL/xB1sP/6VyYJ2UYiOr2kkHTKR8JjICQ14DcdmM1NDayO6mBgy2NhpNTTQaW2g2t9JmNdEivisTC5y//ifz67c4+fVf1JY2eaPpTQzhKIZohLaoSlsiSGvQhVFzInlicbzpAr5cF47OJJ5cFkcmiWtvFls6Sqoyw8rNn1m8cZ21H36hZ7ZOW1cC77E1gvUVQivHiJ1ZIzA1SvjDOtJ78zUOLExQm9xHXzXP9FyV/eM9VCd76akmGZxaJvcwScNPEoHfXqdvrk46p5AqKCSLGsmeCPFKEi2vkpgtIk2U44z26gyJn8Wkm2oxRH9epl9guRiguzbF8u2vmLl9gsWfrwoOT6AFjMiyiaDSgRZ2oobshIJWwlEXUi3toT9qY2/YSjLQLlAQHrHTpbvp1OzkK2N8fP9fLtz/myu/87ihLluIqA5imoPOmJdMzM3euJuJfWmkfKiDUsRGKeogHTTSK7YsJVwUhf96Ul6y3WXmzn/LzPkvWbx4j9LgNF0RpxjmeKyqLtQNOZuJCsyKBaSehJNqzsdIURg5ZuPdkkIt72Mg5SSvGjn9/ixXLpzm1Llphs75qW+MITutRBUzQXczXlsD3o530NwtJIMWpELMSSUrP66UYkT3N6N59qC4G3Cad7L14SKP7tzg42t1ui6FOLk1g8diwO9qJqmJ1OhOcceI6t5DQjEhHRzt5Xx9ns83joqn+nBa3kb1GQgrFuz2PWysHmSiNsAXn5zj2s27XL64yWAxyvhghlNL41wSAz848i7dgqL/KZO2Vua5fGqJ/aUErbu3E1McYrIbv9PInnfeYGF6P19sbfDRicOcqs/x/qFxjkxW+HT9EBdWD/DJ2kHOiYV0r0lo4RAbDhfpFIq52t8mF3GTi8ukdR8R2UHAZSGfVPHY25HdHXgd/0fMSjkbYXqwwCHxuvp7FZan+xnIKEQ8bSIpJkGqeTfT5TRLE90cOBplbEanT1wqpVWGulNYDE1Uy3lG9vdR68+KhmH25XTGhE0OHU9z9HCGraUxakVdNLS8heptIK62C4ItwlcmdK2VkNKM2bIDU/truG27sHfsxC7QJoSyGnYQsDcQk41ktA7SqrCPv02I2YzUuOtl9KCZ0cE0KaFYVLWREBgL2xneJ4hfmWRteZQTolYXh1ldGmFhph9VeFAW6vpdBlz2JlR/O4MicZK5abvgzCW48vDCtmd58gmJ5597lmeefoqo5uLRvUs8uHWRB7cv8uN3F/j17mdsrs3y0qvbcAcbGJ6IYu3YxSuvvMiYWErSPE0EXW0szw4Ioxqw2RoJ+k24rC3UKl3cuX6G77/Z5Pa1TW5dPc29G2c5vTqForTQW3Zx7IM8xw/2kRcRHi4n+Q9qDWCIQnqKCwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/55310d147cda52b683cf642bf7b78e15/263a4/sco.webp 480w,
/static/55310d147cda52b683cf642bf7b78e15/7ecc4/sco.webp 526w&quot; sizes=&quot;(max-width: 526px) 100vw, 526px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/55310d147cda52b683cf642bf7b78e15/9aebd/sco.png 480w,
/static/55310d147cda52b683cf642bf7b78e15/55e98/sco.png 526w&quot; sizes=&quot;(max-width: 526px) 100vw, 526px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/55310d147cda52b683cf642bf7b78e15/55e98/sco.png&quot; alt=&quot;올리브영의 셀프계산대&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;올리브영의 셀프계산대
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;1-외부-운영-솔루션과의-의존성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%99%B8%EB%B6%80-%EC%9A%B4%EC%98%81-%EC%86%94%EB%A3%A8%EC%85%98%EA%B3%BC%EC%9D%98-%EC%9D%98%EC%A1%B4%EC%84%B1&quot; aria-label=&quot;1 외부 운영 솔루션과의 의존성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 외부 운영 솔루션과의 의존성&lt;/h3&gt;
&lt;p&gt;Client의 기존 배포 프로세스를 변경하기 어려웠던 이유는, &lt;code class=&quot;language-text&quot;&gt;Bigfix&lt;/code&gt;라는 외부 운영 솔루션에 &lt;strong&gt;깊숙한 의존성&lt;/strong&gt;을 가지고 있었기 때문입니다.&lt;/p&gt;
&lt;p&gt;코드부터 시작하여 &lt;code class=&quot;language-text&quot;&gt;Bigfix&lt;/code&gt;를 위한 물리 장비까지 존재했고 이것에 굉장히 의존적이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;Bigfix&lt;/code&gt;는 스크립트를 통해 클라이언트에게 배포하기 좋은 기능을 갖추었지만, 물리 장비에 귀속되고 코드에 깊숙이 관여돼야 하므로 많은 부분 제약되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;2-환경에-따른-휴먼에러-발생-위험성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%ED%99%98%EA%B2%BD%EC%97%90-%EB%94%B0%EB%A5%B8-%ED%9C%B4%EB%A8%BC%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D-%EC%9C%84%ED%97%98%EC%84%B1&quot; aria-label=&quot;2 환경에 따른 휴먼에러 발생 위험성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 환경에 따른 휴먼에러 발생 위험성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;환경에 의한 영향&lt;/strong&gt; : 개발자가 직접 PC에서 빌드하므로 배포마다 다른 개발자의 PC에서 빌드될 경우 환경에 의한 영향을 받을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;휴먼에러&lt;/strong&gt; : 빌드 과정에서 개발자가 직접 빌드하거나, 파일을 옮기는 등 수기 프로세스가 존재하므로 휴먼에러의 발생 가능성이 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
&lt;h3 id=&quot;3-업데이트를-수행하는-주체가-개별-단말기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EB%A5%BC-%EC%88%98%ED%96%89%ED%95%98%EB%8A%94-%EC%A3%BC%EC%B2%B4%EA%B0%80-%EA%B0%9C%EB%B3%84-%EB%8B%A8%EB%A7%90%EA%B8%B0&quot; aria-label=&quot;3 업데이트를 수행하는 주체가 개별 단말기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 업데이트를 수행하는 주체가 개별 단말기&lt;/h3&gt;
&lt;p&gt;올리브영의 POS&amp;#x26;셀프계산대는 &lt;strong&gt;3천 대&lt;/strong&gt;가 넘고 장비가 항상 켜져있지 않기 때문에 Server에서 한 번에 업데이트 명령을 날릴 수 없습니다.&lt;/p&gt;
&lt;p&gt;구조 자체가 업데이트를 Client가 요청하는 &lt;strong&gt;업데이트의 주체&lt;/strong&gt;가 Client인 구조기 때문에 이 또한 고민 포인트였습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;다행히 셀프계산대는 POS보다 먼저 S3를 통해 빌드 파일을 받아올 수 있도록 최근에 개발하였고, &lt;strong&gt;TeamCity&lt;/strong&gt;의 배포 시작점을 셀프계산대로 진행합니다!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;어떻게-바꾸려고-노력했을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B0%94%EA%BE%B8%EB%A0%A4%EA%B3%A0-%EB%85%B8%EB%A0%A5%ED%96%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;어떻게 바꾸려고 노력했을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떻게 바꾸려고 노력했을까?&lt;/h2&gt;
&lt;p&gt;셀프계산대의 배포 프로세스에서, &lt;strong&gt;개발자가 수기로 진행하는 요소들&lt;/strong&gt;을 최대한 줄이고자 했습니다.
기존 프로세스는 다음과 같았습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 github에서 소스의 브랜치를 Pull한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 로컬환경에서 최종 코드를 빌드한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;파일을 클렌징하고, 선별한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 빌드된 파일을 압축하여 S3의 배포경로로 업로드한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 빌드버전이 명시된 JSON파일을 다운로드한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 빌드버전이 명시된 JSON파일을 변경한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 빌드버전이 명시된 JSON파일을 업로드한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;셀프계산대가 실행 시, 버전파일을 읽는다.&lt;/li&gt;
&lt;li&gt;업데이트가 필요한 버전이라면, S3를 통해 업데이트된 파일을 다운로드 받는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;p&gt;위 스텝 중 &lt;span style=&quot;color:red;&quot;&gt;붉은색&lt;/span&gt;으로 처리된 부분이 &lt;strong&gt;개발자가 직접 수기로 진행&lt;/strong&gt;하던 부분입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;어떻게-바뀌었을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%B0%94%EB%80%8C%EC%97%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;어떻게 바뀌었을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떻게 바뀌었을까?&lt;/h2&gt;
&lt;p&gt;아래와 같이 CI&amp;#x26;CD 구성을 변경하기로 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 TeamCity 서버를 통해 빌드 명령을 보낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Build Agent가 github에서 소스를 Pull한다.&lt;/li&gt;
&lt;li&gt;Build Agent가 최종 코드를 빌드한다.&lt;/li&gt;
&lt;li&gt;(필요 시) 파일을 클렌징한다.&lt;/li&gt;
&lt;li&gt;AWS CLI를 통해 S3로 파일을 전송한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color:red;&quot;&gt;개발자가 TeamCity 서버를 통해 빌드버전이 명시된 JSON파일의 변경 명령을 보낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;셀프계산대가 실행 시, 버전 파일을 읽는다.&lt;/li&gt;
&lt;li&gt;업데이트가 필요한 버전이라면, S3를 통해 업데이트된 파일을 다운로드 받는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/br&gt;
&lt;p&gt;한눈에 보아도 개발자가 수기로 작업하는 &lt;span style=&quot;color:red;&quot;&gt;붉은색&lt;/span&gt;이 많이 줄었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;그리고 다음과 같은 순서로 빌드 프로세스 구성을 진행했습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-as-is-배포-프로세스를-분석하고-cicd의-경계선-구분&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-as-is-%EB%B0%B0%ED%8F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%A5%BC-%EB%B6%84%EC%84%9D%ED%95%98%EA%B3%A0-cicd%EC%9D%98-%EA%B2%BD%EA%B3%84%EC%84%A0-%EA%B5%AC%EB%B6%84&quot; aria-label=&quot;1 as is 배포 프로세스를 분석하고 cicd의 경계선 구분 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. AS-IS 배포 프로세스를 분석하고, CI&amp;#x26;CD의 경계선 구분&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;업데이트의 주체&lt;/strong&gt;가 Client이므로, 이에 알맞는 CI&amp;#x26;CD의 경계선을 구분지었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;2-cicd-각각의-프로세스를-작은-단위로-분리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-cicd-%EA%B0%81%EA%B0%81%EC%9D%98-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EB%A5%BC-%EC%9E%91%EC%9D%80-%EB%8B%A8%EC%9C%84%EB%A1%9C-%EB%B6%84%EB%A6%AC&quot; aria-label=&quot;2 cicd 각각의 프로세스를 작은 단위로 분리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. CI&amp;#x26;CD 각각의 프로세스를 작은 단위로 분리&lt;/h3&gt;
&lt;p&gt;스크립트 작성 및 테스트를 쉽게 할 수 있도록 전체의 프로세스를 &lt;strong&gt;작은 스텝으로 분해&lt;/strong&gt;했습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;3-셀프계산대의-개발-언어인-c빌드-에이전트-리소스를-신규-발급&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%85%80%ED%94%84%EA%B3%84%EC%82%B0%EB%8C%80%EC%9D%98-%EA%B0%9C%EB%B0%9C-%EC%96%B8%EC%96%B4%EC%9D%B8-c%EB%B9%8C%EB%93%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A5%BC-%EC%8B%A0%EA%B7%9C-%EB%B0%9C%EA%B8%89&quot; aria-label=&quot;3 셀프계산대의 개발 언어인 c빌드 에이전트 리소스를 신규 발급 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 셀프계산대의 개발 언어인 C#빌드 에이전트 리소스를 신규 발급&lt;/h3&gt;
&lt;p&gt;코코리님의 글을 보면 &lt;strong&gt;TeamCity&lt;/strong&gt;의 에이전트들은 다음과 같이 1개의 &lt;strong&gt;TeamCity Server&lt;/strong&gt;와 N개의 &lt;strong&gt;TeamCity Agent&lt;/strong&gt; 구조를 가지고 있습니다.
&lt;strong&gt;TeamCity Server&lt;/strong&gt;는 &lt;strong&gt;TeamCity Agent&lt;/strong&gt;에게 작업을 할당하고, 사용 가능한 리소스 기반으로 작업수행이 가능한 Agent가 그 명령을 수행하는 구조입니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 893px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/57284a772aa1561e85dce5ba857a398d/59080/teamcity-structure.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABeUlEQVR42pVTy07DQAzs53PlKxDiwIELEgiBkOgJISQeQoXempRAoe0m+/A+BnvTpK3opY6s7CbjyXi8GbxevUDPNQIFeOPh7XaScZCoxhVGd6MWZ3fhCN55DJ4vnvqilBK6SKuLNOX99K3E+3DUvfwXUkuWMAjeQ7HCyfcMxGtrLKx1IKI2+ctCICp2EW0SCmYgm8v7Dxwcn2XCz+kUVVVBKYWY4p6ErFBkmsD9B/aGFTVNk5UF2Xu/P2H0sbdFHmqtuWULx22Lh0LUtSOgtNvCNaFMLXFrEFNZWV3XGSBrYwyaZYMUU55gjDEXSsp6K0NsPZShaOUwU8tM7JzrQdK2qU1PLNjFQmPe1FlA2CD05NdDuX0Y4/D0HI4LimKCsiy5cJGB2UMONVdI/IGT60cc3Qxh2euiKDJWfA8+rBQ6wg8flV9WkmI7CFG2OZSUh0JZ4Ze2qLV0EXqcJDFPJnT8MgiYHzg+4NQn9XeJ7q8RrJBvY9qUP+UPhKdaH2X0xcYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/57284a772aa1561e85dce5ba857a398d/263a4/teamcity-structure.webp 480w,
/static/57284a772aa1561e85dce5ba857a398d/612e0/teamcity-structure.webp 893w&quot; sizes=&quot;(max-width: 893px) 100vw, 893px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/57284a772aa1561e85dce5ba857a398d/9aebd/teamcity-structure.png 480w,
/static/57284a772aa1561e85dce5ba857a398d/59080/teamcity-structure.png 893w&quot; sizes=&quot;(max-width: 893px) 100vw, 893px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/57284a772aa1561e85dce5ba857a398d/59080/teamcity-structure.png&quot; alt=&quot;빌드 에이전트 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;빌드 에이전트 구조, 출처 : https://oliveyoung.tech/blog/2022-05-03/How-to-Set-up-Build-Process-with-Teamcity/
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;기존에 Java용 빌드 에이전트만 존재했었고, 최대한 다른 빌드시스템에 영향을 받지 않기 위해 새로운 C# 빌드용 &lt;strong&gt;EC2 리소스&lt;/strong&gt;를 새롭게 요청했습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;4-cicd-과정에-필요한-툴-사전-설치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-cicd-%EA%B3%BC%EC%A0%95%EC%97%90-%ED%95%84%EC%9A%94%ED%95%9C-%ED%88%B4-%EC%82%AC%EC%A0%84-%EC%84%A4%EC%B9%98&quot; aria-label=&quot;4 cicd 과정에 필요한 툴 사전 설치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. CI&amp;#x26;CD 과정에 필요한 툴 사전 설치&lt;/h3&gt;
&lt;p&gt;에이전트 리소스를 할당받았다면 빌드를 위한 초기 세팅을 해주어야 합니다.&lt;/p&gt;
&lt;p&gt;빌드 및 배포에 필요한 &lt;code class=&quot;language-text&quot;&gt;nuget&lt;/code&gt; (패키지 관리자), &lt;code class=&quot;language-text&quot;&gt;msbuild&lt;/code&gt;  (빌더), &lt;code class=&quot;language-text&quot;&gt;jq&lt;/code&gt; (Json Parser) 등을 세팅 해 주었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;5-build-step-구성-및-테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-build-step-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;5 build step 구성 및 테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. Build Step 구성 및 테스트&lt;/h3&gt;
&lt;p&gt;전체 프로세스를 정리한 뒤 최대한 작은 스텝으로 나누고 이를 빌드 스텝으로 만들어 테스트를 진행하며 구성해 나아갔는데요.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1195px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e4a9d523d9894e5b09b6675cb9b57043/08158/teamcity_step.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA90lEQVR42o2SC2+DMAyE+/9/5qSWR5MAeYcAt3O2TJVWabNkGSz543zm5kPEOCvoZUHOGaUUnOeJ67paPY6v7L2/4jZoi495Q6gXNucwjhN8CLB8Dqxl37/Bx/+Ao3G4K8vBAyElKGOwWQfnA/bKXkx4KtVqygWZG0i/UvVrZs5L/zYtvilMpbJRkQgVVT2897g/HjBmgdamvff1X1NgDWhjhtoCPOsPkCr6eoEeD8NIoIHSGjHGXzDJSkuqAJ+rxTBrHmWjMgI5IH71EMA4TVjWFYaHs7TjnZdtdQFuLvAYHoH+yDX7F3sIXFRn8Y9/QeUW76LPfALPiyOIWydtEwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e4a9d523d9894e5b09b6675cb9b57043/263a4/teamcity_step.webp 480w,
/static/e4a9d523d9894e5b09b6675cb9b57043/a6361/teamcity_step.webp 960w,
/static/e4a9d523d9894e5b09b6675cb9b57043/632a5/teamcity_step.webp 1195w&quot; sizes=&quot;(max-width: 1195px) 100vw, 1195px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e4a9d523d9894e5b09b6675cb9b57043/9aebd/teamcity_step.png 480w,
/static/e4a9d523d9894e5b09b6675cb9b57043/a91f8/teamcity_step.png 960w,
/static/e4a9d523d9894e5b09b6675cb9b57043/08158/teamcity_step.png 1195w&quot; sizes=&quot;(max-width: 1195px) 100vw, 1195px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e4a9d523d9894e5b09b6675cb9b57043/08158/teamcity_step.png&quot; alt=&quot;빌드 스텝&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;빌드 스텝 플로우
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1191px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5c6672866beba3a7b415fb81a8118466/77f62/teamcity_buildstep.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 50.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABMUlEQVR42p2SiWrDMBBE/f+/1x8oaWyS2LKt+7I0WW3iQi4oFQwCCc3OPLuTWuM8TpiXBVavsNYipowQwoNqrazntZ/v6n5GieOkEQrYuO97jGLF9+GIYRhwuYyYJoG8bfjL6o6TxHnWiHmDoXTWOpRSobSBVBraGISY+N45j0R75WTvRAmHSaEnsaGxbNrSaNqd97DNhBC0IaWUl4ovlaXxEMrCEyfnDGKM/HAUC8SiYFyAJhkfYUOCTxviVt+qDe1OQuJCj1el6CAxh2bYBkSqmhLV3RVv2jjpa2VmKFaJmeRC5LrtazdWq1TELrIh1631XvuzuHID7u/QNTGcxMzsPA14VPhNmnK+DXgSG/q7YYgZOVOdaFHyXusz/I+/TTNzBLtsGV+nhMMcGkX8d10BABEP2c96qcwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5c6672866beba3a7b415fb81a8118466/263a4/teamcity_buildstep.webp 480w,
/static/5c6672866beba3a7b415fb81a8118466/a6361/teamcity_buildstep.webp 960w,
/static/5c6672866beba3a7b415fb81a8118466/3301e/teamcity_buildstep.webp 1191w&quot; sizes=&quot;(max-width: 1191px) 100vw, 1191px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5c6672866beba3a7b415fb81a8118466/9aebd/teamcity_buildstep.png 480w,
/static/5c6672866beba3a7b415fb81a8118466/a91f8/teamcity_buildstep.png 960w,
/static/5c6672866beba3a7b415fb81a8118466/77f62/teamcity_buildstep.png 1191w&quot; sizes=&quot;(max-width: 1191px) 100vw, 1191px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5c6672866beba3a7b415fb81a8118466/77f62/teamcity_buildstep.png&quot; alt=&quot;빌드 스텝2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;빌드 스텝
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;위와 같이 최대한 작은 스텝인 &lt;strong&gt;1~5줄로 구성되는 작은 스크립트&lt;/strong&gt;로 구성해 나가는 게 테스트에 도움이 되었습니다.&lt;/p&gt;
&lt;p&gt;설명으로는 간단해 보이지만 사실 스텝 하나하나 검증하며 굉장히 많은 시행착오가 존재했습니다... 🥲&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실행해 보면서 스크립트 검증을 해야 하는데 Windows 관련 빌드 사례가 많이 없어서 다수의 검증 시도&lt;/li&gt;
&lt;li&gt;파라미터의 경우 &lt;code class=&quot;language-text&quot;&gt;Key=Value&lt;/code&gt; 형태로 구성되는데, &lt;code class=&quot;language-text&quot;&gt;Key&lt;/code&gt; 문자열에 대한 동적 할당이 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;/br&gt;
&lt;h3 id=&quot;6-빌드-파일을-적용하여-qa&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EB%B9%8C%EB%93%9C-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%98%EC%97%AC-qa&quot; aria-label=&quot;6 빌드 파일을 적용하여 qa permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6. 빌드 파일을 적용하여 QA&lt;/h3&gt;
&lt;/br&gt;
&lt;h3 id=&quot;7-점진적-배포-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#7-%EC%A0%90%EC%A7%84%EC%A0%81-%EB%B0%B0%ED%8F%AC-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;7 점진적 배포 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;7. 점진적 배포 적용&lt;/h3&gt;
&lt;hr&gt;
&lt;p&gt;최종적으로 저희의 배포 파이프라인은 다음과 같은 모습을 갖추게 되었습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1305px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/fea733189a54acb12b72c2df5c52f2d3/2851b/deploy1.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.08333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABvElEQVR42o1SwW7UMBTM5/FJ/AEXbly4wqFSQUBvFaJFSD0iVIRoRatW1S5saXc3601iJ7ETv2Ge02yLKFItWXZiz3jevMlwzwgh4PTsAt63EInoug4xRjxkZN4HGLNOHyKSVutqfDv6jlc7P/D46T5my8Ut4ubOfwmD9yjM6q/Lfa9qBJ8Pp3j34SuKqtooFKoXYmLbplXCMEdsJjzo83z4QZA0NXpbwVO1NTlWqyUt6ND1PaClW5tI0XHWFlBbijWUZ0MoxiRCqWvEXz8Re/Wthy1LmAUJuVdfRR/mbCqL2fUCHQl0RsUpjxI6ljM5OdE6EbkXqos8UUVVUSKfz0l4o7BpkqKlKXGwtwt7uIXgC6wPPiH/+D6RZ5PpFG+3twEaH87PB5VKSFW1dSipfiCkh+oZLVEl1dkX/H72iJGYY7nzBtcvng8KO5biSCJ8PRKsFkSWr4SOpRUrAx9uCaHnBDp6aa5mKVotH2zIo5jsbssjI+SdJUBSiYOHC3iugeWDq3Y2dXtM0Ri3scuJKA4fRxdzPHn5Ou3b1qOmasdMNnXDpnb/5vCeTGZ3/1+VDseTy03Ix6ALHj7+AOi9VGi9qkgTAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/fea733189a54acb12b72c2df5c52f2d3/263a4/deploy1.webp 480w,
/static/fea733189a54acb12b72c2df5c52f2d3/a6361/deploy1.webp 960w,
/static/fea733189a54acb12b72c2df5c52f2d3/d9f0a/deploy1.webp 1305w&quot; sizes=&quot;(max-width: 1305px) 100vw, 1305px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/fea733189a54acb12b72c2df5c52f2d3/9aebd/deploy1.png 480w,
/static/fea733189a54acb12b72c2df5c52f2d3/a91f8/deploy1.png 960w,
/static/fea733189a54acb12b72c2df5c52f2d3/2851b/deploy1.png 1305w&quot; sizes=&quot;(max-width: 1305px) 100vw, 1305px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/fea733189a54acb12b72c2df5c52f2d3/2851b/deploy1.png&quot; alt=&quot;셀프계산대 CI구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;셀프계산대 빌드 자동화 구조
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1302px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/156f071b099e09879458aee270d66928/3b5a6/deploy2.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 54.58333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABlElEQVR42oVTTUvEMBTcH+pvUW/+AI+exIsgehRUEEFPih8g+IGwK2hF3e6mTbrbNEkzzmtdV1dZHw28kNd5M/OSDhgxRkxikhXFCPcPXTjv4L1HCB51XeO/6EyRonxf4EOV4/buFhtbN1hePUCqs6bmX0DCoDCGDD67h4CoNVmFhu/h8QO29q9Qugp1CNPm1gJ/MO7EGPD6rrCyvo27bg8YFfCPPXiCWqUwTF8x6L81e2c0YjlGLEggeWbjvN3zn1jZqWRPJefdZ2htvjoF5+EqB6My5OkAnnngaljJEvmTnEua/PBQpH8fjlggsnWWIRsQkHJbG1grADN+CssZwJ8RCOA4XZ3lUP0+GVp48Y0W1WwCSkXtWehb32cB8QuwbgFzAqYp/TONh+A1GiQJLs4uYSvi5gZeztJ+Y9EcQGEYkA2HHExKyW0DiXxc4eb0COZgiZNXUHu7eFpZhH5J5jMsSwuVayjKljyE8Ok2YHrXSNYWAPsOdXKMZGcT5TyGv17Qt1xeTEk/R2MLx9vgeSbcZXAfDiZWLnx7gZcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/156f071b099e09879458aee270d66928/263a4/deploy2.webp 480w,
/static/156f071b099e09879458aee270d66928/a6361/deploy2.webp 960w,
/static/156f071b099e09879458aee270d66928/633a6/deploy2.webp 1302w&quot; sizes=&quot;(max-width: 1302px) 100vw, 1302px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/156f071b099e09879458aee270d66928/9aebd/deploy2.png 480w,
/static/156f071b099e09879458aee270d66928/a91f8/deploy2.png 960w,
/static/156f071b099e09879458aee270d66928/3b5a6/deploy2.png 1302w&quot; sizes=&quot;(max-width: 1302px) 100vw, 1302px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/156f071b099e09879458aee270d66928/3b5a6/deploy2.png&quot; alt=&quot;셀프계산대 CD구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;셀프계산대 배포 자동화 구조
&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;배포 자동화에 대한 부분이 &lt;strong&gt;버전이 명시된 JSON&lt;/strong&gt; 파일을 변경하는 이유는 &lt;strong&gt;Client 업데이트에 대한 주체&lt;/strong&gt;가 Server가 아닌 Client에 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;(셀프계산대는 서버의 &lt;strong&gt;버전이 명시된 JSON&lt;/strong&gt;파일을 읽어 버전을 구분하고 프로그램 업데이트를 진행합니다.)&lt;/p&gt;
&lt;p&gt;그렇기에 Client가 버전을 업데이트할 수 있도록 에이전트 서버에서 준비를 마치는 것이 CD라고 판단했습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;어떤-점이-좋아졌을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%96%A4-%EC%A0%90%EC%9D%B4-%EC%A2%8B%EC%95%84%EC%A1%8C%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;어떤 점이 좋아졌을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;어떤 점이 좋아졌을까?&lt;/h2&gt;
&lt;h3 id=&quot;1-빌드-환경의-자유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%B9%8C%EB%93%9C-%ED%99%98%EA%B2%BD%EC%9D%98-%EC%9E%90%EC%9C%A0&quot; aria-label=&quot;1 빌드 환경의 자유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 빌드 환경의 자유&lt;/h3&gt;
&lt;p&gt;적용 후 가장 큰 장점은 개발자가 더 이상 각자의 PC에서 빌드하지 않음으로써 &lt;strong&gt;빌드환경에 신경 쓰지 않아도 된다는 점&lt;/strong&gt;이었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;2-빌드를-위한-세팅-불필요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%B9%8C%EB%93%9C%EB%A5%BC-%EC%9C%84%ED%95%9C-%EC%84%B8%ED%8C%85-%EB%B6%88%ED%95%84%EC%9A%94&quot; aria-label=&quot;2 빌드를 위한 세팅 불필요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 빌드를 위한 세팅 불필요&lt;/h3&gt;
&lt;p&gt;이제 각자 세팅해 둔 PC가 아닌 TeamCity Server에 접근하여 버튼만 누를 수 있다면 &lt;strong&gt;환경에 상관없이 빌드가 가능&lt;/strong&gt;해졌습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;3-no-more-it-works-on-my-machine&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-no-more-it-works-on-my-machine&quot; aria-label=&quot;3 no more it works on my machine permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. No more “It Works On My Machine”&lt;/h3&gt;
&lt;p&gt;“제 PC에선 잘 됐는데요?”의 변명이 사라졌습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;4-빌드-시-휴먼-에러-방지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EB%B9%8C%EB%93%9C-%EC%8B%9C-%ED%9C%B4%EB%A8%BC-%EC%97%90%EB%9F%AC-%EB%B0%A9%EC%A7%80&quot; aria-label=&quot;4 빌드 시 휴먼 에러 방지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 빌드 시 휴먼 에러 방지&lt;/h3&gt;
&lt;p&gt;항상 같은 환경에서 같은 스크립트로 빌드가 진행되기 때문에 &lt;strong&gt;휴먼에러의 발생 소지가 줄어&lt;/strong&gt; 개발자가 걱정할 필요가 없어졌습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;5-배포-프로세스에-소요되는-시간-개선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%B0%B0%ED%8F%AC-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%97%90-%EC%86%8C%EC%9A%94%EB%90%98%EB%8A%94-%EC%8B%9C%EA%B0%84-%EA%B0%9C%EC%84%A0&quot; aria-label=&quot;5 배포 프로세스에 소요되는 시간 개선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;5. 배포 프로세스에 소요되는 시간 개선&lt;/h3&gt;
&lt;p&gt;기존 배포 프로세스 대비 상당한 &lt;strong&gt;시간적 개선&lt;/strong&gt;이 있었는데요!
배포의 처음부터 완료까지 걸리는 시간이 기존 대비 &lt;strong&gt;66.8%&lt;/strong&gt; 개선되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;h3 id=&quot;6-웹훅-알람으로-빌드배포-결과-공유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EC%9B%B9%ED%9B%85-%EC%95%8C%EB%9E%8C%EC%9C%BC%EB%A1%9C-%EB%B9%8C%EB%93%9C%EB%B0%B0%ED%8F%AC-%EA%B2%B0%EA%B3%BC-%EA%B3%B5%EC%9C%A0&quot; aria-label=&quot;6 웹훅 알람으로 빌드배포 결과 공유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;6. 웹훅 알람으로 빌드&amp;#x26;배포 결과 공유&lt;/h3&gt;
&lt;p&gt;TeamCity에는 빌드&amp;#x26;배포 결과를 Slack 웹 훅을 통해 메신저로 보내주는 기능이 있습니다.
이로써 모든 담당자가 빌드&amp;#x26;배포 결과를 인지할 수 있게 되었습니다.&lt;/p&gt;
&lt;/br&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#whats-next&quot; aria-label=&quot;whats next permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;What&apos;s Next?!&lt;/h2&gt;
&lt;p&gt;셀프계산대가 아닌 POS의 경우 외부 솔루션에 대한 의존도가 크기 때문에 아직은 완전한 배포 프로세스를 구성하지 못한점이 아쉬운 부분인데요!&lt;/p&gt;
&lt;p&gt;완전한 배포 자동화를 이루기까지는 시간이 걸리겠지만, 사용성 좋은 시스템을 위해 빠르게 개선 해 나아갈 예정입니다!&lt;/p&gt;
&lt;/br&gt;
&lt;p&gt;앞으로도 이러한 개선을 통해 더욱 완성도 높은 올리브영 POS시스템을 만들어가고자 합니다.
감사합니다!&lt;/p&gt;
&lt;p&gt;( 이 글을 빌어 많은 도움 주신 올리브영 PE팀께 감사인사를 드립니다 😄 )&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영 온라인몰 정산개편 이야기]]></title><description><![CDATA[안녕하세요. 올리브영 정산 스쿼드에서 백엔드 개발을 맡고 있는 감자를 닮은 감자맨이라고 합니다.
정산 스쿼드에서 23년 부터 2…]]></description><link>https://oliveyoung.tech/2024-12-02/oliveyoung-settlement-overhaul-resource-savings/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-12-02/oliveyoung-settlement-overhaul-resource-savings/</guid><pubDate>Mon, 02 Dec 2024 13:52:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영 정산 스쿼드에서 백엔드 개발을 맡고 있는 감자를 닮은 감자맨이라고 합니다.
정산 스쿼드에서 23년 부터 24년 초까지 온라인몰 정산개편 프로젝트를 진행했었습니다.
이번 포스트에서는 이 프로젝트를 진행하게 된 배경과 성과에 대해 공유드리고자 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;정산개편-이전의-온라인몰-정산시스템의-문제점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EC%82%B0%EA%B0%9C%ED%8E%B8-%EC%9D%B4%EC%A0%84%EC%9D%98-%EC%98%A8%EB%9D%BC%EC%9D%B8%EB%AA%B0-%EC%A0%95%EC%82%B0%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90&quot; aria-label=&quot;정산개편 이전의 온라인몰 정산시스템의 문제점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;정산개편 이전의 온라인몰 정산시스템의 문제점&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;온라인몰에서는 주문, 배송, 클레임 등 여러 프로세스가 동시에 이루어집니다. 기존의 정산 시스템은 각 프로세스에서 발생한 데이터를 별도로 정제하여 정산 테이블에 저장하는 방식으로 운영되었습니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 주문이 발생하면 해당 주문 데이터를 주문 테이블에 먼저 저장한 후, 이를 가공하여 정산 테이블에 반영하는 방식이었습니다. 마찬가지로, 반품이 발생했을 경우 클레임 테이블에 관련 데이터를 저장하고, 이를 정산에 맞게 가공한 후 정산 테이블에 저장하는 구조였습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto; text-align:center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1263px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/93594ddc4e8720467f5d38f3eedf281d/e0796/asis_settlement.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.333333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+UlEQVR42lVQ2aqDUAz0/39LUSwKamlxX1to3fc+zmUCR7gPIedMJplJtDiOEYYhsixDVVVXvN9vPB4PFEWBpmkEq+tachAE8H0fr9fr4rNWliU0Ptg0jiOO48C+71jXFV3X4fv9Sp6mCed5Yts2zPMsODFmclWNmAykUpqm8DwPlmWJOsmfz0eEns8nXNeFaZry7vv+EuXftm3ouo4kSaDRJi3neS5Fx3FkIJs4jI08y/1+x+12Ew5d/n4/cRRFkYgZhiGmNN5PqXJdtRaH8XZ0uizLVVuWWZyRz6xWZp09Wtu20sRMggoW6Zy4albBf98PGIbhH0buHzg/uQGdFXDPAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/93594ddc4e8720467f5d38f3eedf281d/263a4/asis_settlement.webp 480w,
/static/93594ddc4e8720467f5d38f3eedf281d/a6361/asis_settlement.webp 960w,
/static/93594ddc4e8720467f5d38f3eedf281d/cc8b6/asis_settlement.webp 1263w&quot; sizes=&quot;(max-width: 1263px) 100vw, 1263px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/93594ddc4e8720467f5d38f3eedf281d/9aebd/asis_settlement.png 480w,
/static/93594ddc4e8720467f5d38f3eedf281d/a91f8/asis_settlement.png 960w,
/static/93594ddc4e8720467f5d38f3eedf281d/e0796/asis_settlement.png 1263w&quot; sizes=&quot;(max-width: 1263px) 100vw, 1263px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/93594ddc4e8720467f5d38f3eedf281d/e0796/asis_settlement.png&quot; alt=&quot;기존 정산 데이터 수집 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;&amp;lt;기존 정산 데이터 수집 구조&amp;gt;&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;주문-배송-cx-프로세스의-복잡도-증가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EB%AC%B8-%EB%B0%B0%EC%86%A1-cx-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%9D%98-%EB%B3%B5%EC%9E%A1%EB%8F%84-%EC%A6%9D%EA%B0%80&quot; aria-label=&quot;주문 배송 cx 프로세스의 복잡도 증가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;주문 배송 CX 프로세스의 복잡도 증가&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;온라인몰의 주문 트랜잭션 내에서는 정산 기초 데이터를 적재한 후, 이를 토대로 정산 업무에 필요한 데이터를 생성합니다. 그러나 이와 같은 로직의 문제점은, 주문, 클레임, 배송 등의 프로세스에서 데이터를 적재할 때 정산에 맞는 형태로 데이터를 재가공해야 한다는 점입니다. 이 과정에서 데이터 정합성을 항상 신경 써야 하므로 각 프로세스의 복잡도가 증가하게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;정산-데이터의-부정확성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EC%82%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EB%B6%80%EC%A0%95%ED%99%95%EC%84%B1&quot; aria-label=&quot;정산 데이터의 부정확성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;정산 데이터의 부정확성&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;주문, 클레임, 배송 등의 프로세스에서 정산데이터를 적재할 때, 정산 관점에서 정합성이 맞지 않는 데이터가 다수 발생합니다. 원천 데이터는 정상적이지만, 정산 데이터를 생성하는 과정에서 부정확한 데이터가 생성되어 정산적 정합성이 틀어지는 상황이 발생하게 됩니다. 이러한 데이터는 QA 및 운영 검증이 어려워, 결국 정산 개발자의 데이터 분석을 통한 보정에 의존하게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;리소스-낭비-심화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A6%AC%EC%86%8C%EC%8A%A4-%EB%82%AD%EB%B9%84-%EC%8B%AC%ED%99%94&quot; aria-label=&quot;리소스 낭비 심화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;strong&gt;리소스 낭비 심화&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;위와 같은 보정이 필요한 데이터가 발생하면 정산 개발자가 데이터를 분석하여 문제를 찾아 수정합니다. 하지만 이러한 데이터 보정은 고스란히 정산 운영 시스템의 리소스 부하로 이어집니다. 주문, 클레임, 배송 등 모든 프로세스마다 정산 데이터가 적재되다 보니, 온라인몰 거래량의 증가는 곧 정산 기초 데이터의 증가로 연결됩니다. 거래량이 증가함에 따라 정산 기초 데이터도 늘어나고, 위에서 언급한 것처럼 정합성이 틀어진 데이터도 같은 비율로 쌓이게 됩니다.&lt;/p&gt;
&lt;p&gt;세일과 같은 특별한 행사가 있는 경우에는 정산 개발자가 출근부터 퇴근까지 정산 데이터만 보정하는 상황도 발생할 정도로 운영 리소스의 부하가 심각했습니다. 또한, 정산 데이터를 토대로 진행되는 월 마감에서는 정산 담당자뿐만 아니라 정산 개발자가 마감 종료까지 함께 대기하며 진행해야 했습니다. (매월 마감 때마다 정산 스쿼드는 항상 야근을 했습니다.)&lt;/p&gt;
&lt;p&gt;정산뿐만 아니라 정산 데이터의 수정은 이를 사용하는 모든 유관 부서에도 추가적인 작업을 요구하기 때문에, 해당 부서의 리소스 낭비를 야기했습니다.&lt;/p&gt;
&lt;h2 id=&quot;정산개편&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EC%82%B0%EA%B0%9C%ED%8E%B8&quot; aria-label=&quot;정산개편 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;정산개편&lt;/h2&gt;
&lt;p&gt;이러한 문제를 해결하기 위해 도입된 정산 개편 구조는 데이터를 보다 효율적으로 관리할 수 있도록 설계되었습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto; text-align:center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1266px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/4a90e28f3bac993332691afdc862dc10/03d4c/tobe_settlement.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.125%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA9ElEQVR42m2QSauEMBCE/f8/S1BxPbjgctBRBxz33TnWUA3KO7xD6KS+dHUlSpIkSNMUvu/j9XqhLEtUVYW6rp8z93Ecw7Zt5HmO9/stjDr53UOm3AIv7PuO4ziwbRu6rsM8z+j7Xup1XcKXZUHbtqKxruv6sGEYoNCIE4MggOd5MAxD0vJy0zQYxxFRFMFxHGiaJkmpcSiHhWEI0zShqqq89DFkE6HrulLZNE2TJLi/xLIsMWTK7/crKckYRNd1ZFkGhUYUi6KQqXwyTRifpkxBg/M8hXNPxkVGjYx9PCt8Gv+LSVnvyzS711+D/9itfz4f/AByR7h68VvACwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4a90e28f3bac993332691afdc862dc10/263a4/tobe_settlement.webp 480w,
/static/4a90e28f3bac993332691afdc862dc10/a6361/tobe_settlement.webp 960w,
/static/4a90e28f3bac993332691afdc862dc10/f80fb/tobe_settlement.webp 1266w&quot; sizes=&quot;(max-width: 1266px) 100vw, 1266px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4a90e28f3bac993332691afdc862dc10/9aebd/tobe_settlement.png 480w,
/static/4a90e28f3bac993332691afdc862dc10/a91f8/tobe_settlement.png 960w,
/static/4a90e28f3bac993332691afdc862dc10/03d4c/tobe_settlement.png 1266w&quot; sizes=&quot;(max-width: 1266px) 100vw, 1266px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4a90e28f3bac993332691afdc862dc10/03d4c/tobe_settlement.png&quot; alt=&quot;개편된 정산 데이터 수집 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;&amp;lt;개편된 데이터 수집 구조&amp;gt;&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;주문, 클레임, 배송 데이터는 각 원천 테이블에만 적재하고, 정산 단계에서는 필요한 시점에 원천 데이터를 수집하여 전표 생성 및 매출 전송 등 정산에 필요한 데이터를 생성합니다. 특히, 원천 테이블에 적재된 데이터가 무결성을 보장한다는 전제 하에 정산 데이터를 수집하는 방식으로 정산 정합성 불일치 문제를 근본적으로 해소했습니다. 이러한 구조는 데이터 흐름을 단순화하고, 원천 데이터를 정확히 활용함으로써 정산 프로세스의 신뢰도와 안정성을 크게 높였습니다.&lt;/p&gt;
&lt;h2 id=&quot;정산개편의-성과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EC%82%B0%EA%B0%9C%ED%8E%B8%EC%9D%98-%EC%84%B1%EA%B3%BC&quot; aria-label=&quot;정산개편의 성과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;정산개편의 성과&lt;/h2&gt;
&lt;p&gt;정산 개편은 여러개의 Sub Project로 순차적 오픈을 하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-운영업무-감소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9A%B4%EC%98%81%EC%97%85%EB%AC%B4-%EA%B0%90%EC%86%8C&quot; aria-label=&quot;1 운영업무 감소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;1. 운영업무 감소&lt;/h3&gt;
&lt;p&gt;정산 개편의 가장 눈에 띄는 성과는 정산 운영 업무의 획기적인 감소입니다. 위에서 서술한 것처럼, 과거에는 정산 정합성 보정 작업이 빈번하게 발생하여 한 달에 150건 이상의 보정 작업이 이루어지기도 했습니다. 그러나 정산 개편 이후 각 Sub Project를 단계적으로 오픈하면서 보정 건수는 점차 감소하였고, 2024년 4월 마지막 Sub Project가 오픈된 이후에는 정합성 보정 작업이 완전히 사라졌습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto; text-align:center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 808px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d6e605e4fa7c48798595cfab3c64347b/4a1c5/modifyTable_blur.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 52.29166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAABsUlEQVR42n2SCW/TQBCF88uROCVAJbSIVghVQvwgUMmBaZs49Zl1HNvxuV7HfrzdEIQoZaVPO56defs89igvKqyjBGXdoChrbNOcsUSyTZEybmR7oFGoG4laSmS7EnGSHZ7/YqQLQxGjbTvopS+oqsbESikjJuVhV6rDMMCg1zE+0vcDRlrVCwSbWiYH7PICFd2CBWmWGydeGMEn+kwf7Pc9m++zJ6OKryeiLeq6NbekWfHbYZzsUDLWxdpd1/XGxUMYwdCXuLYSBG6DNAYcu4TPONkAq2UOZ1VyL2AvcgSeNPltNNwjFgP7KejYDSZXG9wtG0TBAPu2gsNY+ANurNzkA3cP3+kQcl97/T8J3R6bkIK+0+L7TDvsKEhXixoec1EICu4opFgICF72P9Y0sBEUdFcSV18jeKvWuLIp6P6KrXlKcQquQdFjY3/gIYciUHQYQ4SdmaHvSjYoM6vFrZ5hjTu7gr3Us1WcFz+WgDn/E53Ptvxtzl/P8fLRF7x9NsXp8xnGT75h/HSCj2MLH97MDRcnU7x/NcHlmYXPFze4fPfD1B45ezHDyeMpPp1f4ydPSOs4kZlzJgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d6e605e4fa7c48798595cfab3c64347b/263a4/modifyTable_blur.webp 480w,
/static/d6e605e4fa7c48798595cfab3c64347b/a7032/modifyTable_blur.webp 808w&quot; sizes=&quot;(max-width: 808px) 100vw, 808px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d6e605e4fa7c48798595cfab3c64347b/9aebd/modifyTable_blur.png 480w,
/static/d6e605e4fa7c48798595cfab3c64347b/4a1c5/modifyTable_blur.png 808w&quot; sizes=&quot;(max-width: 808px) 100vw, 808px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d6e605e4fa7c48798595cfab3c64347b/4a1c5/modifyTable_blur.png&quot; alt=&quot;월별 As-is 정산 데이터 보정 건수&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;&amp;lt;월별 As-is 정산 데이터 보정 건수&amp;gt;&lt;/figcaption&gt;
&lt;/div&gt;
&amp;nbsp;
&amp;nbsp;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto; text-align:center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 879px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/816bbf49fdf87b6d70439b29d76dcafa/00c79/modifyGraph_withoutY.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.91666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAABNElEQVR42pVS2XKDMAzk//+xL33IJAUDNr6vrWQCoZPSJp5ZZFv2slqrizHiDCEG/JX/DR1ORipArXhrpJTQVbq1odwZplnh47PHVy8QQmh7x3Nn2AlLKQ2VkHKF1BHGeijjUXLe8//hibCUisWVppSm0J4V15fU/VDIl3jYWJt3G4khwlzeJGRV2iUMk4FcLHnmiYTKrwXWZ3rpdf5yyawnBCIcBMZxhFKq/Y2fOJJcE/KdsO7YH3HfWwlb22wLHF/74OnDx3KKzbJd4dGH45oVX64DLrcRNlQ48jjm1VdGzKBOCLjeegxirbDTWsMYg2VZWuSSrbWQUra9eRopaupNSXkLMSnMyqIXsnnOPTuMCkJM8N6j4w83r3PuKbInnOfY1rQfvEOKa4wUOZ8SnaM8l/wNyeKwA1XpHqIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/816bbf49fdf87b6d70439b29d76dcafa/263a4/modifyGraph_withoutY.webp 480w,
/static/816bbf49fdf87b6d70439b29d76dcafa/d0a37/modifyGraph_withoutY.webp 879w&quot; sizes=&quot;(max-width: 879px) 100vw, 879px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/816bbf49fdf87b6d70439b29d76dcafa/9aebd/modifyGraph_withoutY.png 480w,
/static/816bbf49fdf87b6d70439b29d76dcafa/00c79/modifyGraph_withoutY.png 879w&quot; sizes=&quot;(max-width: 879px) 100vw, 879px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/816bbf49fdf87b6d70439b29d76dcafa/00c79/modifyGraph_withoutY.png&quot; alt=&quot;월별 As-is 정산 데이터 보정 추이&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;&amp;lt;월별 As-is 정산 데이터 보정 추이&amp;gt;&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;특히, 온라인몰 거래 건수가 꾸준히 증가하는 상황에서 정산 개편이 이루어지지 않았다면, 2024년 9월 세일 첫날에 하루 동안 약 320건의 보정 작업이 필요했을 것으로 예상됩니다.&lt;/p&gt;
&lt;p&gt;이러한 결과는 정산 개편이 단순히 프로세스를 개선하는 데 그치지 않고, 운영 업무의 부담을 획기적으로 줄이며 효율성을 극대화하는 데 크게 기여했다는 점을 보여줍니다.&lt;/p&gt;
&lt;h3 id=&quot;2-데이터&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%8D%B0%EC%9D%B4%ED%84%B0&quot; aria-label=&quot;2 데이터 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;2. 데이터&lt;/h3&gt;
&lt;p&gt;개편 전 환경에서는 여러 프로세스 간의 복잡한 상호작용으로 인해 정산 데이터의 정합성 불일치가 빈번하게 발생했습니다. 이로 인해 불일치 데이터를 제거하고 수정하는 데 많은 시간과 노력이 소요되었습니다.&lt;/p&gt;
&lt;p&gt;하지만 개편 후 환경에서는 통일된 계산식을 모든 상품 단위에 적용하고, BigDecimal 라이브러리를 활용하여 정확한 소수점 계산을 가능하게 함으로써, 주문번호별 데이터의 안정성을 완전히 확보할 수 있었습니다.&lt;/p&gt;
&lt;div style=&quot;max-width: 700px; width: 100%; display:block; margin-left: auto; margin-right: auto; text-align:center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 579px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/3680e7f918974a0f58d4fdaa574f44c0/ff2d6/bigDecimalEx.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 24.375000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABAElEQVR42o2NTUvDQBRF8xNFcF1KcSUoQkFw2R9mUWOrCxWrVDcFNVqb5qtJpulMZsbjmKq4cOHi8N6993Gf1+testfyaW8c0dns/5vtrT77bZ/DnQt2Wyd0O+f0DgZ4Q/+Rh7uM8ShlfPMLp+8b/sgaEm6vI06PJ4yuQob+E4OzCV5eFlSyQtUKbTRa62bWWiGVRNbSZRLTeDXG6ub2E2vfyfLS+ZplJQmjBG86iwleQ6ZvMWlasMgFeSHIFqU7yJg74niBWK5+ssjpolxitCUMhSvXJMmK4MUVzsKEJM2Zx6kjQ6m6+Wyt/WK9G7Pm269diTHGPRNUlUIIxXMQ8QEuNGEd9tKlAwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3680e7f918974a0f58d4fdaa574f44c0/263a4/bigDecimalEx.webp 480w,
/static/3680e7f918974a0f58d4fdaa574f44c0/96ba3/bigDecimalEx.webp 579w&quot; sizes=&quot;(max-width: 579px) 100vw, 579px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3680e7f918974a0f58d4fdaa574f44c0/9aebd/bigDecimalEx.png 480w,
/static/3680e7f918974a0f58d4fdaa574f44c0/ff2d6/bigDecimalEx.png 579w&quot; sizes=&quot;(max-width: 579px) 100vw, 579px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3680e7f918974a0f58d4fdaa574f44c0/ff2d6/bigDecimalEx.png&quot; alt=&quot;부동소수점 적용의 예&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;&amp;lt;부동소수점 적용의 예&amp;gt;&lt;/figcaption&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-10-11/settlement-floation-point/&quot;&gt;정산 스쿼드에서 경험했던 부동 소수점 이야기 보러가기 🚀&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;p&gt;또한, 정산 데이터 검증 과정에서 원천 데이터에 숨어 있던 이슈를 발견하고 이를 조치함으로써, 데이터의 신뢰도를 한층 높일 수 있었습니다. 결과적으로, 이러한 개선은 단순히 시스템의 안정성을 높이는 데 그치지 않고, 정산 업무의 효율성까지 크게 향상시키는 데 기여하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;3-정산운영-담당자-편의개선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%A0%95%EC%82%B0%EC%9A%B4%EC%98%81-%EB%8B%B4%EB%8B%B9%EC%9E%90-%ED%8E%B8%EC%9D%98%EA%B0%9C%EC%84%A0&quot; aria-label=&quot;3 정산운영 담당자 편의개선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;3. 정산운영 담당자 편의개선&lt;/h3&gt;
&lt;p&gt;정합성 향상으로 인해 재처리 업무가 사라졌을 뿐만 아니라, 정산 운영 담당자의 업무 편의성도 크게 향상되었습니다. 정산 개편 이전에는 마감 시 개발자와 운영 담당자 모두 긴장하며 데이터를 검증했으며, 언제 문제가 발생할지 몰라 마감 업무 종료까지 개발자가 대기하며 대응해야 했습니다.&lt;/p&gt;
&lt;p&gt;하지만 정산 개편 이후에는 운영 담당자가 업무 시간 내에 정산 마감을 자체적으로 처리할 수 있게 되었고, 마감 시간도 크게 단축되었습니다. 이제 개발자는 마감일에도 별도의 업무를 진행하며 정산 마감에 얽매이지 않는 환경을 누리고 있습니다.&lt;/p&gt;
&lt;p&gt;또한, 정산 담당자가 사용하는 정산 화면도 개선되었습니다. 혼용되던 화면을 통합하고, 불필요한 프로세스와 데이터를 제거하여 메뉴 조회 시간을 단축시켰습니다.(5분~10분이상 걸리던 화면을 30초 내외로 단축) 이를 통해 운영 효율성과 사용자 경험이 크게 개선되었습니다.&lt;/p&gt;
&lt;h3 id=&quot;4-개발인력-효율화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EA%B0%9C%EB%B0%9C%EC%9D%B8%EB%A0%A5-%ED%9A%A8%EC%9C%A8%ED%99%94&quot; aria-label=&quot;4 개발인력 효율화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;4. 개발인력 효율화&lt;/h3&gt;
&lt;p&gt;정산 개편의 성과는 정산 인력의 효율적인 재배치에서도 나타났습니다. 정산 담당 개발자들의 인력은 더이상 운영에 치중되지 않게 되었고, 새로운 기능을 개발하고 개선점을 더 찾아 갈 수 있는 시간이 확보되어 올리브영의 정산 시스템이 견고해질 수 있게 되었습니다. 인력 운용의 최적화를 통해 정산뿐만 아니라 조직 운영 전반의 효율성도 크게 향상시켰습니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;이번 정산개편 프로젝트는 단순히 시스템 개선을 넘어서, 우리의 업무 효율성을 높이고, 더 나아가 투명한 데이터 처리를 가능하게 만들었습니다. 과거의 방식에서 벗어나 새로운 방식으로 발전하는 과정은 쉽지 않았지만, 그만큼 값진 성과를 가져왔습니다. 향후 지속적인 모니터링과 개선을 통해 더욱 완성도 높은 시스템을 구축하고, 이를 바탕으로 고객에게 더 나은 서비스를 제공할 수 있기를 기대합니다. 이제, 우리가 만든 시스템은 더 이상 단순한 정산을 넘어서, 다양한 비즈니스 전략을 지원하는 중요한 요소가 될 것입니다. 앞으로도 더 나은 결과를 만들어 나가겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[올리브영 서비스에 사용되는 컴포넌트를 모아놓은 그곳!]]></title><description><![CDATA[안녕하세요. 올리브영에서 UI…]]></description><link>https://oliveyoung.tech/2024-11-22/designsystem-development/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-11-22/designsystem-development/</guid><pubDate>Fri, 29 Nov 2024 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영에서 UI를 개발하고 있는 시공입니다.&lt;br/&gt;
현재 올리브영 서비스에 사용되는 컴포넌트들의 디자인 시스템 가이드를 제작하고 있어, 작업을 시작하게 된 이유와 진행 과정에 대해 공유하고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;목차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%EC%B0%A8&quot; aria-label=&quot;목차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목차&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%B4%EB%9E%80&quot;&gt;디자인 시스템이란&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%A0%84%EC%97%AD-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%9D%98-%EC%A0%81%EC%9A%A9&quot;&gt;전역 스타일의 적용&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot;&gt;컴포넌트 만들기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#emotion%EC%9D%98-%EC%B6%94%EA%B0%80%EC%A0%81%EC%9D%B8-%EA%B8%B0%EB%8A%A5&quot;&gt;Emotion의 추가적인 기능&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot;&gt;올리브영 서비스에 디자인 시스템 적용하기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;디자인-시스템이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%B4%EB%9E%80&quot; aria-label=&quot;디자인 시스템이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;디자인 시스템이란&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;디자인 시스템&lt;/strong&gt;이란 웹이나 각종 서비스 UI 디자인에서 재사용할 수 있는 컴포넌트와 패턴을 정의한 가이드라인이나 규칙을 말합니다.&lt;/p&gt;
&lt;p&gt;올리브영 서비스 운영 시 공통 원칙을 준수한 디자인 패턴을 컴포넌트화하여 디자인 및 개발 작업에서 재사용한다면, 작업자 간의 지식 수준을 일치시켜 보다 통일성과 효율성을 높인 작업이 가능해집니다. 이는 우리에게 꼭 필요한 요소라 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;따라서 서비스에 필요한 컴포넌트들을 별도의 저장소로 분리하여 체계적으로 관리하고, 컴포넌트 옵션 변경 시 모양과 동작을 확인할 수 있는 가이드라인 문서를 구축하는 것이 이번 작업의 목표였습니다.&lt;/p&gt;
&lt;p&gt;이를 위해 Vite + React + TypeScript 환경에서 storybook과 emotion을 사용해 디자인 시스템을 구축했습니다.
&lt;code class=&quot;language-text&quot;&gt;storybook&lt;/code&gt;은 컴포넌트를 모아서 문서화로 보여주는 오픈소스 툴로, 독립적인 환경에서 컴포넌트의 구조와 형태를 쉽게 파악할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;emotion&lt;/code&gt;은 JS로 CSS를 작성하도록 설계된 라이브러리입니다. 스타일 적용을 위해서 styled-component를 사용해도 무방하지만, 리액트와 함께 사용할 수 있는 &lt;code class=&quot;language-text&quot;&gt;@emotion/react&lt;/code&gt; 패키지를 선택했습니다.&lt;/p&gt;
&lt;p&gt;(세팅 방법은 공식 문서에 친절하게 나와 있기 때문에 퀵하게 어떻게 컴포넌트를 구현했는지 설명하겠습니다.)&lt;/p&gt;
&lt;br/&gt;
&lt;h2 id=&quot;전역-스타일의-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%84%EC%97%AD-%EC%8A%A4%ED%83%80%EC%9D%BC%EC%9D%98-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;전역 스타일의 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;전역 스타일의 적용&lt;/h2&gt;
&lt;p&gt;storybook을 설치하고 처음 화면을 띄우면 가장 기본적인 화면을 마주하게 됩니다.
저희가 작업하는 컴포넌트들은 모바일 환경에서 사용되기 때문에 pc의 storybook 환경에서 잘 보이도록 컨테이너와 image에 스타일을 적용하는 작업이 필요했습니다.
이때, preview.tsx 파일에서 Global 컴포넌트를 사용하여 스타일을 쉽게 적용할 수 있습니다. CSS 리셋 등의 storybook 내에서 전반적으로 적용되어야 하는 전역 스타일을 Global을 사용해 관리하면 사이트의 통일성을 유지할 수 있습니다.&lt;/p&gt;
&lt;p&gt;.storybook &gt; preview.tsx&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;&amp;lt;Global
	styles=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token selector&quot;&gt;css`
		*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.innerZoomElementWrapper&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;min-height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 70px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;.sb-story &gt; div &gt; div&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token selector&quot;&gt;img&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100%&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  `&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
/&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;h2 id=&quot;컴포넌트-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;컴포넌트 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;컴포넌트 만들기&lt;/h2&gt;
&lt;p&gt;Text 같은 기본적인 컴포넌트가 만들어져 있다는 가정하에 조금 더 복잡한 Radio 컴포넌트를 스토리로 만들어 보겠습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 677px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/71c8e2ad9da09837dbd08a806f20bbd9/3c503/img00.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.750000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuUlEQVR42q1Ryw6CMBDs//+HMfFqjP9hNDEYDyYEkEep0Ae0hbGgYEAOHpxkkna3u5mZEq01pJSw1mIJbdv2/BWE0hy+70MphX+AqKqGUBVqp3QJh+MJZ++y2BNORBDF4EJ+Fpbu0lG6pXOrHVbrDba7/aQ2BFBwAe96A3uUY53QnCEII1BKwRibZNmd4yRFmlFkLhr9djEsLkqOMLq7Nwk45y+FTdP0gx2NMV+2rDVjb/4546wxo5AnJezV62oH4ogAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/71c8e2ad9da09837dbd08a806f20bbd9/263a4/img00.webp 480w,
/static/71c8e2ad9da09837dbd08a806f20bbd9/fac04/img00.webp 677w&quot; sizes=&quot;(max-width: 677px) 100vw, 677px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/71c8e2ad9da09837dbd08a806f20bbd9/9aebd/img00.png 480w,
/static/71c8e2ad9da09837dbd08a806f20bbd9/3c503/img00.png 677w&quot; sizes=&quot;(max-width: 677px) 100vw, 677px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/71c8e2ad9da09837dbd08a806f20bbd9/3c503/img00.png&quot; alt=&quot;Radio 컴포넌트 피그마&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;[Radio 컴포넌트 디자인]&lt;/figcaption&gt;
&lt;p&gt;화면에 띄워질 stories 파일들의 경로를 main.ts 파일 아래에 설정해 두고 해당 폴더(여기서는 src) 아래 작성할 컴포넌트 파일을 생성해 줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;ts&quot;&gt;&lt;pre class=&quot;language-ts&quot;&gt;&lt;code class=&quot;language-ts&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//.storybook &gt; main.ts&lt;/span&gt;
stories&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;../src/**/*.mdx&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../src/**/*.stories.@(js|jsx|mjs|ts|tsx)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 181px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/a54ae9517611b305c8f7937db3a2c42c/0b1d8/img01.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.773480662983424%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB2UlEQVR42pVTXW8SQRTlRU2jppVlF/Z7ZxbS2kJI7WZRPnYJKARZYozgFkhAQ0Ib3u2br77oP/FHHmduAsHGxPJw5+5kZs4959y7GduxwRwPPPQRTEIwn6FaraJcLqNQKBwcGddz4TMf5+0LXP+cofKqjGSYoNfrwTAMaJp2GGA+nweFokFXLRimJUBUAtN1HYwxygcBlkolFIsc7z+lWN99x+XVFW7Wa8xmM2w2G9TrdWIqi/wXUC5bBi7jKDIPhqYIZhyu68KyLDpTFAXZbPZhgBSGicLxEZ5/+Iaj6Q+cehZaUYzBYIBWq4UwDImpVPQwQMHSyL3A0+gzHr/5iKJn4+27HkajETWo0WggiiKSrqoq5e33fpEdoC4im9cQ2Ryjl1WYjgPP88gOzjk9tm2bmiQ9d8S5tMT3/Z0tfwGawvAnx8/wa/AVv6d3OKtc4PbmFovFAqvVCv1+H/P5HJPJBGmaYjweo9vtYrlcot1uI5fLUfHMvn5NUOeGhTPT2UnahrwsH0l5pmkSQ8lMZrn/p4e6coKTOMWjzhe8Di5xPZ0iSRJi1Gw2MRwOUavVqMBufu81aY+hGB1NMDgPoVSa4u9xEcUxOp0OSZJdjsU+CALsj9r9of8DmEpWhkv/O3UAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a54ae9517611b305c8f7937db3a2c42c/7f82f/img01.webp 181w&quot; sizes=&quot;(max-width: 181px) 100vw, 181px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a54ae9517611b305c8f7937db3a2c42c/0b1d8/img01.png 181w&quot; sizes=&quot;(max-width: 181px) 100vw, 181px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a54ae9517611b305c8f7937db3a2c42c/0b1d8/img01.png&quot; alt=&quot;Radio 컴포넌트 파일 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;[Radio 컴포넌트 파일 구조]&lt;/figcaption&gt;
&lt;p&gt;세 가지 파일이 컴포넌트 하나를 구성하는 기본 세트입니다.&lt;/p&gt;
&lt;h3 id=&quot;indexts&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#indexts&quot; aria-label=&quot;indexts permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[index.ts]&lt;/h3&gt;
&lt;p&gt;Radio 컴포넌트를 모듈로 내보내기 위한 파일입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Radio &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;./Radio&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Radio&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;radiotsx&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#radiotsx&quot; aria-label=&quot;radiotsx permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[Radio.tsx]&lt;/h3&gt;
&lt;p&gt;Radio 컴포넌트에 대한 파일입니다.
emotion을 사용하기 위해 &lt;code class=&quot;language-text&quot;&gt;@emotion/react&lt;/code&gt;를 import 해줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; css &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@emotion/react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이어서 Radio 컴포넌트 내용물을 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RadioProps&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;InputHTMLAttributes&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;HTMLInputElement&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  className&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  label&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  children&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactNode&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  onChange&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ChangeEvent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;HTMLInputElement&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Radio&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  className&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  label&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  onChange&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;attr
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; RadioProps&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;className&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;htmlFor&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;data-label&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt;
          &lt;span class=&quot;token spread&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;attr&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;radio&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;onChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;span&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Radio&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;emotion을 사용하면 아래와 같이 css prop을 직접 컴포넌트에 전달할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt;
  	&lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;className&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token attr-name&quot;&gt;htmlFor&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token attr-name&quot;&gt;data-label&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
		display: flex;
		position: relative;
		cursor: pointer;
	&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;emotion의 장점 중 하나는 컴포넌트의 상태나 props에 따라 동적 스타일링을 적용할 수 있다는 점입니다. 조건부 스타일링이 가능하여, props 값을 기반으로 스타일을 입맛대로 설정할 수 있습니다. 예를 들어 Radio의 &lt;code class=&quot;language-text&quot;&gt;disabled&lt;/code&gt; prop이 &lt;code class=&quot;language-text&quot;&gt;true&lt;/code&gt; 일 경우에만 스타일을 적용하고 싶다면 아래처럼 추가하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt;
	&lt;span class=&quot;token attr-name&quot;&gt;htmlFor&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token attr-name&quot;&gt;data-label&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
		display: flex;
		position: relative;
		cursor: pointer;

		&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;disabled &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
          cursor: auto;
		&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;checked&lt;/code&gt;와 &lt;code class=&quot;language-text&quot;&gt;disabled&lt;/code&gt; 상태에 대한 제어도 개발에서 가능하도록 Props에 추가해 주겠습니다.&lt;/p&gt;
&lt;p&gt;아래는 Radio.tsx 의 최종 형태입니다.
Props 정의 아래에 주석으로 텍스트를 입력하면 스토리에 컴포넌트에 대한 설명을 추가할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; css &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@emotion/react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; colors &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../Color&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;../&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Props&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  className&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  label&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  checked&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  disabled&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  children&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ReactNode&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  onChange&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ChangeEvent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/**
 * 리스트에서 항목을 선택을 하거나, 설정을 켜거나 끌 수 있도록 해주는 용도의 컨트롤입니다.&amp;lt;br/&gt;
 * - 옵션 중 한가지를 선택해야 하는 경우 사용 합니다.&amp;lt;br/&gt;
 * - 한 가지 옵션을 선택할 경우, 다른 옵션들은 자동적으로 해제됩니다.&amp;lt;br/&gt;
 * - **20px * 20px** 크기가 디폴트입니다.
 */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Radio&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  className&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  label&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  checked&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  disabled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  onChange&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;attr
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Props&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;label&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;htmlFor&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;data-label&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
        display: flex;
        position: relative;
        cursor: pointer;
        &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;disabled &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
          cursor: auto;
        &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;
        &lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
          position: relative;
          width: 20px;
          height: 20px;
          flex-shrink: 0;
        &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &amp;lt;input
          &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;attr&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          id=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          name=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          type=&quot;radio&quot;
          checked=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;checked&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          disabled=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;disabled&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          onChange=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;onChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          css=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
            width: 100%;
            height: 100%;
            border: 1px solid;
            border-radius: 50%;
            appearance: none;
            border-color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gray30&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;

            :checked {
              border-color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gray100&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
              :after {
                content: &quot;&quot;;
                position: absolute;
                top: 50%;
                left: 50%;
                width: calc(100% - 8px);
                height: calc(100% - 8px);
                border-radius: 50%;
                background-color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gray100&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
                transform: translate(-50%, -50%);
              }
            }

            :disabled {
              border-color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gray40&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
              background-color: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;colors&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gray20&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;;
            }
          &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        /&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;span&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
          &lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
            margin-left: 10px;
          &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;label&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Radio&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;스타일 적용까지 모두 끝이 났다면, 이제는 스토리를 띄워 보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;radiostoriestsx&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#radiostoriestsx&quot; aria-label=&quot;radiostoriestsx permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;[Radio.stories.tsx]&lt;/h3&gt;
&lt;p&gt;화면에 띄울 Radio 컴포넌트를 import하고, 페이지의 경로와 이름, props에 대한 설명을 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Radio &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Components/Radio&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// storybook 경로&lt;/span&gt;
  component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Radio&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  argTypes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    className&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;클래스 이름&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;내용&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    checked&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;체크&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    disabled&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;비활성&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    onChange&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;onChange 함수&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    children&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      control&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;하위 컴포넌트&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;가장 기본적인 형태의 Radio를 보여주기 위해 Default 상태를 정의하고, 체크 상태를 제어하는 props checked를 true로 정의하여 checked 상태의 스토리를 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Default &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Checked &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  args&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    checked&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 Control 영역에서 props를 제어하면 각 상태일 때 컴포넌트가 변경되는 모습과 소스를 바로 확인 할 수 있습니다.
여기에 더하여 스토리안에서도 useState 등으로 상태 값에 따라 컴포넌트의 변화되는 모습을 보여줄 수 있으며 이를 활용해서 더 복잡한 컴포넌트들도 구현이 가능합니다.&lt;/p&gt;
&lt;div style=&quot;width:100%; display:flex;&quot;&gt;
    &lt;div style=&quot;width:50%;height:100%;padding-right: 5px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1531px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/21dee963fd42a20347dbc52411445d0e/14af7/img02.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 79.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAIAAACZeshMAAAACXBIWXMAAAsTAAALEwEAmpwYAAABYklEQVR42pWTwW6DMAyG+yS9VjtMoo/SQ6UdQH2XVepbrEg77yX2FJt232BVIRsJiU1CNoe0wChM2y8LOTFfHGwzO+T52yEtSixRa2Os/fqjrLWz4wfPCsnBCKVpK8vznDEKGK/aaYqfCS6UglrXlJfWSirOBZylKIZox+Tghxe4f8L42Ty+Gn+ZX1L17+zgpECyd2GYdDDnPE3T47SyLEuShDHmYKwMNFYqpPXtdrtYXC2D5fWEgiCYz+e73c7BEo03oSpa3+336/XNZrMJwzCKovBCFFqtVnEc/4BVZdqPaZzO9/ttnbpqD2CttS8YFV8CtAxt0pOiute/EZii5NBTAQyKfOp826oBjFhBw9AppZSDxlLTycihA8Yzk3ySwXjQpgQUUjl4IjMS408puLDnEmrjLgtYlQq6IRmBm2tTZvZZlNKNZ/PDtPl749nCUJ3qZJpXqS7O6TVpZLYH8L/0DTVLc9y1UgDuAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/21dee963fd42a20347dbc52411445d0e/263a4/img02.webp 480w,
/static/21dee963fd42a20347dbc52411445d0e/a6361/img02.webp 960w,
/static/21dee963fd42a20347dbc52411445d0e/b33ea/img02.webp 1531w&quot; sizes=&quot;(max-width: 1531px) 100vw, 1531px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/21dee963fd42a20347dbc52411445d0e/9aebd/img02.png 480w,
/static/21dee963fd42a20347dbc52411445d0e/a91f8/img02.png 960w,
/static/21dee963fd42a20347dbc52411445d0e/14af7/img02.png 1531w&quot; sizes=&quot;(max-width: 1531px) 100vw, 1531px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/21dee963fd42a20347dbc52411445d0e/14af7/img02.png&quot; alt=&quot;스토리북 화면 - Radio 컴포넌트 Default&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;width:50%;height:100%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1058px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/28213768705341b03ea68a79ab8b6312/e5cc9/img03.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 69.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAIAAACgpqunAAAACXBIWXMAAAsTAAALEwEAmpwYAAABRUlEQVR42o1Sy07DMBD0/584cYIDn8CBS6/8AVQqRyJaCTVV3cSv9e7aZppICNFWymilRLbHMztr870/bvuu2+5Gn0JWF1OMSbWoKuXMIvhvF8AiNsxxCHtrDyfnSXzW0ceTtbXWtgDGh9AfBxc5cQmkWbQthhl9OFhY5pg1kBBrKbXVWvCpFeav0uoEMzjfg0yChlGnpFtXbCrEjJ5jSldbwCLSMJQSuaFVnSvnHBDYdJ4ZeUm9AVgzrzt+3thVx6svffnUbpAmBN9TpGeU2zBPH/Kwzo8bvV/r3Zu+91yZZqOQhfgscgncaqpKK/JrG+djjHOfLBg1z8ldhfGkKDfVSJqygAwCyBgbxn4r8POoMKG/RSzDOMIw9lLmgx1u6kL5HxnKzrn5ScL26KKWslQ5Evd9z5Py3HldqIxHgj4XPuzW2g+7KjSBJl7+uQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/28213768705341b03ea68a79ab8b6312/263a4/img03.webp 480w,
/static/28213768705341b03ea68a79ab8b6312/a6361/img03.webp 960w,
/static/28213768705341b03ea68a79ab8b6312/1de89/img03.webp 1058w&quot; sizes=&quot;(max-width: 1058px) 100vw, 1058px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/28213768705341b03ea68a79ab8b6312/9aebd/img03.png 480w,
/static/28213768705341b03ea68a79ab8b6312/a91f8/img03.png 960w,
/static/28213768705341b03ea68a79ab8b6312/e5cc9/img03.png 1058w&quot; sizes=&quot;(max-width: 1058px) 100vw, 1058px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/28213768705341b03ea68a79ab8b6312/e5cc9/img03.png&quot; alt=&quot;스토리북 화면 - Radio 컴포넌트 Checked&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;[스토리북 화면 - Radio 컴포넌트 Default/Checked]&lt;/figcaption&gt;
&lt;h2 id=&quot;emotion의-추가적인-기능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#emotion%EC%9D%98-%EC%B6%94%EA%B0%80%EC%A0%81%EC%9D%B8-%EA%B8%B0%EB%8A%A5&quot; aria-label=&quot;emotion의 추가적인 기능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Emotion의 추가적인 기능&lt;/h2&gt;
&lt;br/&gt;
&lt;p&gt;위 컴포넌트 작업에는 사용되지 않았지만, emotion의 추가적인 기능들이 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;keyframes&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#keyframes&quot; aria-label=&quot;keyframes permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Keyframes&lt;/h3&gt;
&lt;p&gt;emotion은 keyframes를 통해 복잡한 애니메이션을 모듈형으로 만들고, 이를 컴포넌트에 적용되도록 제공합니다.&lt;/p&gt;
&lt;img src=&quot;/7a5aa346a3d91242c13d443bbc476c8b/img04.gif&quot; alt=&quot;emotion keyframes&quot;&gt;
&lt;figcaption&gt;[keyframes 애니메이션이 적용된 Tooltip 컴포넌트]&lt;/figcaption&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; css&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; keyframes &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@emotion/react&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tooltipEnter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; keyframes&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  0% { transform: scale(0); }
  100% { transform: scale(100%); }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Tooltip&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;css&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
        animation: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;tooltipEnter&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; 0.25s;
      &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      tooltip
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;media-queries&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#media-queries&quot; aria-label=&quot;media queries permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Media Queries&lt;/h3&gt;
&lt;p&gt;emotion에서도 css에서처럼 미디어쿼리를 사용할 수 있으나 단순 적용이 아닌 중단점을 배열에 넣어 재사용하는 방법도 제공합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; breakpoints &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;280&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;320&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;768&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; mq &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; breakpoints&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bp &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@media (min-width: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;bp&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;px)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;css&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;black&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mq&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;red&apos;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mq&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;blue&apos;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mq&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;green&apos;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      text
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br/&gt;
&lt;h2 id=&quot;올리브영-서비스에-디자인-시스템-적용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%A6%AC%EB%B8%8C%EC%98%81-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;올리브영 서비스에 디자인 시스템 적용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;올리브영 서비스에 디자인 시스템 적용하기&lt;/h2&gt;
&lt;p&gt;지금까지 만들어진 디자인 시스템은 머지않아 github packages로 배포되고, 배포 후에는 실제 마티니 프로젝트에 적용될 예정입니다. Text 컴포넌트를 사용해야 하는 경우를 예로 들어 사용법을 말씀드리자면 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;패키지-설치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%A8%ED%82%A4%EC%A7%80-%EC%84%A4%EC%B9%98&quot; aria-label=&quot;패키지 설치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;패키지 설치&lt;/h3&gt;
&lt;h4 id=&quot;npm&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#npm&quot; aria-label=&quot;npm permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;npm&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; @oy-alldev/ui&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;yarn&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#yarn&quot; aria-label=&quot;yarn permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;yarn&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; @oy-alldev/ui&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;mtn적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mtn%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;mtn적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;mtn적용&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Text &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@oy-alldev/ui&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;생략&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;p&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;텍스트&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 emotion을 활용하여 storybook에서 컴포넌트를 구현하고, 패키징 화하여 사용하는 방법까지 둘러보았습니다.
아주 작은 요소에서부터, 다수의 Icon이나 swith, chip 등 앞으로도 필요한 컴포넌트들이 계속해서 업데이트될 것입니다.
디자인 시스템을 지켜봐 주시고 많이 사용되길 바라며 이만 인사드리겠습니다.
긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;div style=&quot;width:100%; display:flex;&quot;&gt;
    &lt;div style=&quot;width:33.33%;height:100%;padding-right: 5px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1021px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/39a937ed868eeeff298ce7d9df91dd77/d8785/img04.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 79.58333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAIAAACZeshMAAAACXBIWXMAAAsTAAALEwEAmpwYAAABiUlEQVR42pWSi46CMBBF+/9/ZqIoix+AiVmFYqG8HwJ76LjEmOhmJ1jvTHvncVv1fb2mWTbP8zRN8z9NpWkaxfHNmFhrgDFp3/f3JxvH8S05z3OdJJm1ZVUVRVnyV9VN09T1Y23b9i3Z2oUMa3Kdv9gfbVPApGnX9xSk6vKVldTnGz/yFVN1XcdKnfHX7u7DXoqvrgDlFBk/pH8hrBPBUvhFUSAbgHUYBjayLGNFKgQkboxhDECSJHIF1tqlMgng2DwHkAUyADIr4wiHuGhOFuLcpdbxQuY3OCNKHTbAAFyuKndJYZKIXrTWUuxwODzIi7J1DWAPAtt0JRUkTiPSQhRFpKZt+ORSMqrMwCoz0+ezFgCSAniO7HJG4g/B5DQjyQhhGOJCyNyzp8nz+QyIy3AYO25QhFRSB0dE7pxdLhd6gyxJERnAgZvRXdeSHZcDS+W2aURMGZhDtIe7zryqzcuTS5a4Op1O2+2Wxo7H436//3LmeV4QBL7vEyHOLsEF+IfA2Waz2e28H3LpnaQAQnf6AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/39a937ed868eeeff298ce7d9df91dd77/263a4/img04.webp 480w,
/static/39a937ed868eeeff298ce7d9df91dd77/a6361/img04.webp 960w,
/static/39a937ed868eeeff298ce7d9df91dd77/c76c1/img04.webp 1021w&quot; sizes=&quot;(max-width: 1021px) 100vw, 1021px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/39a937ed868eeeff298ce7d9df91dd77/9aebd/img04.png 480w,
/static/39a937ed868eeeff298ce7d9df91dd77/a91f8/img04.png 960w,
/static/39a937ed868eeeff298ce7d9df91dd77/d8785/img04.png 1021w&quot; sizes=&quot;(max-width: 1021px) 100vw, 1021px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/39a937ed868eeeff298ce7d9df91dd77/d8785/img04.png&quot; alt=&quot;스토리북 화면 - Icon 컴포넌트&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;width:33.33%;height:100%;padding-right: 5px;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1083px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/37bad72c08c19d6ebd541a5e95732216/41289/img05.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.79166666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAABMklEQVR42qWSvU7DMBSF/SAlCwNLuyFUxHNFatPHKyKROhUWhmQoG6oT7DiJf5K4nNhQVAGlFZ/sq6vkHjvKOaRpms3m5Y2x3fkQYwyltCiKqq6rCuUDUQ38IbbWcs5pnmMNFIWvjDGIu647JsbG3HZLvYbSnHHetu1x2dfNWhuFbQw0rg4yi+X4TYkXpO9trYVpNZre0Z0GJgkOeO2eW9uc9Z/9dxEhxKPj6WTW6/VDHJdlSZIkuZlOp7d3kzNZrVbkfrm8vBqPJ9dBEFyczGg0iuOYZFk2jxbRYjH/JIqi+SH7J76ZzWZhGKZpSnb/gMBbhFE7YDJSyXmpD0H+UaVUiK9ySCnhFnzuodkfhl4q9bM91iJA3qQhFLAKXSkEcuUnGikZL7/LfFVKD3rr4mTtO7RN8pmhKMNEAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/37bad72c08c19d6ebd541a5e95732216/263a4/img05.webp 480w,
/static/37bad72c08c19d6ebd541a5e95732216/a6361/img05.webp 960w,
/static/37bad72c08c19d6ebd541a5e95732216/fea0c/img05.webp 1083w&quot; sizes=&quot;(max-width: 1083px) 100vw, 1083px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/37bad72c08c19d6ebd541a5e95732216/9aebd/img05.png 480w,
/static/37bad72c08c19d6ebd541a5e95732216/a91f8/img05.png 960w,
/static/37bad72c08c19d6ebd541a5e95732216/41289/img05.png 1083w&quot; sizes=&quot;(max-width: 1083px) 100vw, 1083px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/37bad72c08c19d6ebd541a5e95732216/41289/img05.png&quot; alt=&quot;스토리북 화면 - Swith 컴포넌트&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;width:33.33%;height:100%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1107px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5951673bc3b8c1b1559873b7d81c79ab/3fa5a/img06.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 44.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA4ElEQVR42pWQzwqCQBDG9yG8eUkkJFLwWj50QX8fIBV8hjq2m7V6MUVdp5pdSzoU5Y9hmGHmm29ZAgBZlgkh7v0hKNsfDnF8xuL2AgeYQdG2n8VN03CeHCk9xWeepjJ4gpGkaXa95nleVdVXMR6u6xod8IoQmEAosHrfu32C4KAoisuFt4/EGwBd2QIq5OCZAQ3kmhRTymazed/fks5lWUZR5Pt+EIZBEOwUvqItutyB7Wa7ZYwRSqnnTUa2Mx67lmUZxsD4hWmamqYtFkvCGJ16nu249sgZ/gd66Lq+Wq0f32qpJksqAewAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5951673bc3b8c1b1559873b7d81c79ab/263a4/img06.webp 480w,
/static/5951673bc3b8c1b1559873b7d81c79ab/a6361/img06.webp 960w,
/static/5951673bc3b8c1b1559873b7d81c79ab/a8a52/img06.webp 1107w&quot; sizes=&quot;(max-width: 1107px) 100vw, 1107px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5951673bc3b8c1b1559873b7d81c79ab/9aebd/img06.png 480w,
/static/5951673bc3b8c1b1559873b7d81c79ab/a91f8/img06.png 960w,
/static/5951673bc3b8c1b1559873b7d81c79ab/3fa5a/img06.png 1107w&quot; sizes=&quot;(max-width: 1107px) 100vw, 1107px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5951673bc3b8c1b1559873b7d81c79ab/3fa5a/img06.png&quot; alt=&quot;스토리북 화면 - Chips 컴포넌트&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;figcaption&gt;[스토리북 화면의 일부 - Icon/Swith/Chips 컴포넌트]&lt;/figcaption&gt;</content:encoded></item><item><title><![CDATA[올리브영은 왜 선물하기를 개편했을까? Part - 1]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2024-11-28/gift-renewal/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-11-28/gift-renewal/</guid><pubDate>Thu, 28 Nov 2024 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 올리브영 선물/기프트카드 스쿼드에서 백엔드 개발을 담당하고 있는 기프트데이입니다.&lt;/p&gt;
&lt;p&gt;여러분은 선물에 대해 어떻게 생각하시나요?&lt;/p&gt;
&lt;p&gt;선물은 마음을 전하고, 기쁨을 나누며, 순간을 특별하게 만드는 좋은 방법입니다.&lt;/p&gt;
&lt;p&gt;요즘에는 편리한 선물하기 기능 덕분에 언제 어디서든 따뜻한 마음을 전할 수 있게 되었습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e8bc84151b7d90bbae0286211051eea9/33266/gift_Old_01.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAGQABAQADAQAAAAAAAAAAAAAABAABAgUD/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAH214fXw7wbZ1NmHo0yZBm5yFlzf//EAB8QAAICAQQDAAAAAAAAAAAAAAECAAMEERITFSAhIv/aAAgBAQABBQIZwnZJOxSVh3l1atj3Y4VqsPinA+84RY6/QvbfzNNPewEhPD//xAAXEQEAAwAAAAAAAAAAAAAAAAAQARES/9oACAEDAQE/AadS/wD/xAAZEQACAwEAAAAAAAAAAAAAAAAAAQIQEiH/2gAIAQIBAT8BlLN5Gjp//8QAJhAAAQIDBgcAAAAAAAAAAAAAAQACETJRAxASEzFBICEiYpGhov/aAAgBAQAGPwKOU7ypHKRy6dlRwFEAKLk/0iczXtUTa/N2EgUW10cOhqpeD//EAB4QAAMAAgIDAQAAAAAAAAAAAAABESExUWFBwfHw/9oACAEBAAE/IWZsdD6SPtGAp8ELklK4DvHqW277Km0uDTVl07+cjfDO+SWgTbS7omGfBEtrdlxYh7kXsfsR/9oADAMBAAIAAwAAABAj205kH//EABkRAQEAAwEAAAAAAAAAAAAAAAEAEBFBcf/aAAgBAwEBPxATJb7e2f/EABsRAQACAgMAAAAAAAAAAAAAAAEAERAhMUFR/9oACAECAQE/EBpZzhL1gj3UH1P/xAAhEAEAAgEEAgMBAAAAAAAAAAABABEhMUFRgWFxEMHR8f/aAAgBAQABPxAAEpY63V/FH8bFLsDJnrzBQVKruUU03jQIquDMVWKuQxXcUDiUB6Wo/wBrbMeCDIuerSTXIIVwX6RIU4Ga0f2LtbL4q3i7jV8GRQlc6wRoQ1Rn7m6dkbvc/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e8bc84151b7d90bbae0286211051eea9/263a4/gift_Old_01.webp 480w,
/static/e8bc84151b7d90bbae0286211051eea9/a6361/gift_Old_01.webp 960w,
/static/e8bc84151b7d90bbae0286211051eea9/95f20/gift_Old_01.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e8bc84151b7d90bbae0286211051eea9/a3e66/gift_Old_01.jpg 480w,
/static/e8bc84151b7d90bbae0286211051eea9/fb816/gift_Old_01.jpg 960w,
/static/e8bc84151b7d90bbae0286211051eea9/33266/gift_Old_01.jpg 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e8bc84151b7d90bbae0286211051eea9/33266/gift_Old_01.jpg&quot; alt=&quot;올영 메인 선물하기&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
   &lt;div style=&quot;width: 2%;&quot;&gt;&lt;/div&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/de7e24cd8e2b2140e8cc7082aaf5aee9/33266/gift_Old_02.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAGgAAAwADAQAAAAAAAAAAAAAAAAEFAgMEBv/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHrwlaT0BLK7inlEksAmmMA/8QAHRAAAgICAwEAAAAAAAAAAAAAAQIEEgMUABMhMP/aAAgBAQABBQLc8WapIkk8CvQWVssjJbVPXpEqYNvl/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwFf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwFf/8QAIxAAAgECBAcAAAAAAAAAAAAAAAECESEDEzOREiIwMTJxof/aAAgBAQAGPwLTe5RYcqstgTY5LxRVd0W5TL43T0JZr2NX4WiXXQ//xAAfEAEAAwEAAQUBAAAAAAAAAAABABExIYEQQWGhwXH/2gAIAQEAAT8hD18uJaETLhdlZyLa8k2A6/yL7VrCNTVvPfuKTUe08StoqeeS9NgDTQjyABRMx+/U/Y/sJ//aAAwDAQACAAMAAAAQW8y8IA//xAAYEQADAQEAAAAAAAAAAAAAAAABEDEhAP/aAAgBAwEBPxAbEMnSP//EABoRAAICAwAAAAAAAAAAAAAAAAABETEgIVH/2gAIAQIBAT8QiLNdG27w/8QAHxABAAMAAgIDAQAAAAAAAAAAAQARITFBUZFhceGB/9oACAEBAAE/EGgUVro9wDXwAKs0craiWNJ7IJek1o64VMJTtRtWt9wzXR7h+dmEQdi5ca8v2IjXIh3zexuBAQcH9gWwadjDymskaRdmWKm2bFap/Es8nNc9wp4brJ2npHb7n//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/de7e24cd8e2b2140e8cc7082aaf5aee9/263a4/gift_Old_02.webp 480w,
/static/de7e24cd8e2b2140e8cc7082aaf5aee9/a6361/gift_Old_02.webp 960w,
/static/de7e24cd8e2b2140e8cc7082aaf5aee9/95f20/gift_Old_02.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/de7e24cd8e2b2140e8cc7082aaf5aee9/a3e66/gift_Old_02.jpg 480w,
/static/de7e24cd8e2b2140e8cc7082aaf5aee9/fb816/gift_Old_02.jpg 960w,
/static/de7e24cd8e2b2140e8cc7082aaf5aee9/33266/gift_Old_02.jpg 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/de7e24cd8e2b2140e8cc7082aaf5aee9/33266/gift_Old_02.jpg&quot; alt=&quot;선물하기 구 메인화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
   &lt;div style=&quot;width: 2%;&quot;&gt;&lt;/div&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/bbea3012992ff4b02a7df3892dfdecb2/33266/gift_Old_03.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAGQABAQADAQAAAAAAAAAAAAAAAAECBAUD/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAEC/9oADAMBAAIQAxAAAAHt5c/0Tdaia2FiZoqpSg//xAAcEAACAQUBAAAAAAAAAAAAAAABAwIAEBESIDH/2gAIAQEAAQUC2F2mKiWGMFt2jiicXNDzr//EABcRAAMBAAAAAAAAAAAAAAAAAAABESD/2gAIAQMBAT8BpHn/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AV//xAAdEAABBAIDAAAAAAAAAAAAAAABAAIQIhEwMUFh/9oACAEBAAY/ApyxlndgK0coeyNH/8QAHhABAAICAgMBAAAAAAAAAAAAAQARIVExQRBhwaH/2gAIAQEAAT8hzINp0QKXUqU8iVBfskw45mQRHUweWYCD21AK4Ip+6lF9PEUdzF3fg+x+wn//2gAMAwEAAgADAAAAEEvojSMP/8QAGBEBAQADAAAAAAAAAAAAAAAAEQABEGH/2gAIAQMBAT8QI3OxO//EABkRAAMAAwAAAAAAAAAAAAAAAAABESBBUf/aAAgBAgEBPxCFXR3WH//EAB4QAQACAwACAwAAAAAAAAAAAAEAESExQVFxkdHw/9oACAEBAAE/EB21iwtI9BLZ7cr+WEFWrrFfWcy3SkYGdbPBFqgaaGGj7iB6vOIxGajDcNhkbwBHplxbaJK970wAA0YlhXZrfZ8hW+wbuuTqeI69z//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bbea3012992ff4b02a7df3892dfdecb2/263a4/gift_Old_03.webp 480w,
/static/bbea3012992ff4b02a7df3892dfdecb2/a6361/gift_Old_03.webp 960w,
/static/bbea3012992ff4b02a7df3892dfdecb2/95f20/gift_Old_03.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bbea3012992ff4b02a7df3892dfdecb2/a3e66/gift_Old_03.jpg 480w,
/static/bbea3012992ff4b02a7df3892dfdecb2/fb816/gift_Old_03.jpg 960w,
/static/bbea3012992ff4b02a7df3892dfdecb2/33266/gift_Old_03.jpg 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bbea3012992ff4b02a7df3892dfdecb2/33266/gift_Old_03.jpg&quot; alt=&quot;선물하기 구 브랜드 랭킹&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;올리브영에서는 소중한 분들께 감사와 따뜻한 마음을 전할 수 있도록 &lt;strong&gt;선물하기관&lt;/strong&gt;을 운영하고 있습니다.&lt;/p&gt;
&lt;p&gt;선물하기관은 고객이 선물하기에 적합한 상품을 쉽게 찾을 수 있도록 설계된 특별한 공간입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;다양한 상품 목록&lt;/strong&gt;과 &lt;strong&gt;맞춤형 큐레이션&lt;/strong&gt;을 제공하여, 모바일 올리브영 홈에서 &lt;strong&gt;선물하기 아이콘을 클릭&lt;/strong&gt;하면 선물하기관을 만나볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;저희 스쿼드는 선물하기관을 더욱 편리하고 빠르게 이용할 수 있도록 개편을 진행했습니다.&lt;/p&gt;
&lt;p&gt;이제 올리브영의 선물하기관 개편에 대해 자세히 소개해 드리겠습니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;왜-선물하기관을-개편하려고-했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-%EC%84%A0%EB%AC%BC%ED%95%98%EA%B8%B0%EA%B4%80%EC%9D%84-%EA%B0%9C%ED%8E%B8%ED%95%98%EB%A0%A4%EA%B3%A0-%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;왜 선물하기관을 개편하려고 했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;왜 선물하기관을 개편하려고 했나요?&lt;/h2&gt;
&lt;p&gt;기존 선물하기관은 성능 문제로 인해 고객들이 불편을 겪는 경우가 잦았습니다.&lt;/p&gt;
&lt;p&gt;앱에 접속했을 때 빈 화면이 오래 보이면, 서비스에 문제가 있다고 느끼지 않으신가요?&lt;/p&gt;
&lt;p&gt;특히 이벤트 기간에는 화면이 빈 상태로 노출되는 &lt;strong&gt;&apos;백화 현상&apos;&lt;/strong&gt; 이 자주 발생하여,&lt;/p&gt;
&lt;p&gt;저희 스쿼드에서는 성능 문제 해결을 최우선 과제로 삼고 원인 분석을 시작했습니다.&lt;/p&gt;
&lt;br/&gt;
&lt;div style=&quot;display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/b76c44cd99110e45ee59197b980c965f/33266/gift_Loading.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAe/NDLQAAjNKD//EABYQAAMAAAAAAAAAAAAAAAAAABARQP/aAAgBAQABBQIKD//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BX//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8BX//EABQQAQAAAAAAAAAAAAAAAAAAAED/2gAIAQEABj8CF//EABoQAAICAwAAAAAAAAAAAAAAAAABEcEQMDH/2gAIAQEAAT8hEo5rSiVhWOxH/9oADAMBAAIAAwAAABBDAAAjD//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QX//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8QX//EAB8QAAICAQQDAAAAAAAAAAAAAAABETFBMFFhgXGR8P/aAAgBAQABPxBJpWvQme5zuQ+b0uRXHZyK47E05h0ZHAZeT//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b76c44cd99110e45ee59197b980c965f/263a4/gift_Loading.webp 480w,
/static/b76c44cd99110e45ee59197b980c965f/a6361/gift_Loading.webp 960w,
/static/b76c44cd99110e45ee59197b980c965f/95f20/gift_Loading.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b76c44cd99110e45ee59197b980c965f/a3e66/gift_Loading.jpg 480w,
/static/b76c44cd99110e45ee59197b980c965f/fb816/gift_Loading.jpg 960w,
/static/b76c44cd99110e45ee59197b980c965f/33266/gift_Loading.jpg 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b76c44cd99110e45ee59197b980c965f/33266/gift_Loading.jpg&quot; alt=&quot;선물하기 구 메인화면&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;주요 문제를 정리하면 다음과 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DB 자원 과다 사용으로 인한 성능 저하&lt;/strong&gt;: 페이지 렌더링 시 DB 호출에 크게 의존해, 사용자 수가 몰리는 시간대에는 DB 리소스 부족과 속도 저하 문제가 발생했었습니다. 일부 페이지에는 Redis 캐시를 적용해 개선했지만, 대부분의 페이지는 여전히 실시간 DB 호출에 의존하고 있었습니다. 이로 인해 트래픽이 급증하는 이벤트나 프로모션 기간 동안 사용자 경험에 불편을 주었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;일 단위 배치 작업으로 인한 정보 업데이트 지연&lt;/strong&gt;: 기존 선물하기관 데이터는 일 단위 배치 작업을 통해 업데이트 되고 있었습니다. 이로 인해 상품 가격 변동이나 품절 상태가 실시간으로 반영되지 않아, 고객이 상품 상세 페이지로 이동해야 최신 정보를 확인할 수 있는 불편이 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배치 작업의 복구 절차 미흡&lt;/strong&gt;: 앞서 언급한 일 단위 배치 작업에서는 오류 발생 시 자동화된 복구 절차가 마련되어 있지 않았습니다. 이는 오류 발생 시 자동 복구 절차가 마련되어 있지 않아 담당자가 수동으로 데이터를 재 처리해야 했고, 이로 인해 고객에게 오류 데이터가 노출되거나 휴먼 에러 발생 가능성도 존재했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 이슈들은 서비스 신뢰도에 부정적인 영향을 줄 수 있다고 판단했습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; justify-content: center; text-align: center&quot;&gt;
   &lt;div style=&quot;width: 90%;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1663px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d75be0cdeae508a23aba8af5c29f2907/064b1/gift_as_is.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.208333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAd+oSD//xAAWEAEBAQAAAAAAAAAAAAAAAAAAAUH/2gAIAQEAAQUCbH//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAWEAADAAAAAAAAAAAAAAAAAAAAEDH/2gAIAQEABj8CKv/EABgQAAMBAQAAAAAAAAAAAAAAAAABESEx/9oACAEBAAE/IdKsdQSLOH//2gAMAwEAAgADAAAAEAgf/8QAFREBAQAAAAAAAAAAAAAAAAAAEQD/2gAIAQMBAT8QSb//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPxAn/8QAGxABAAICAwAAAAAAAAAAAAAAAQARITFBYZH/2gAIAQEAAT8Qumj2LIoy1Wu5hEHACqn/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d75be0cdeae508a23aba8af5c29f2907/263a4/gift_as_is.webp 480w,
/static/d75be0cdeae508a23aba8af5c29f2907/a6361/gift_as_is.webp 960w,
/static/d75be0cdeae508a23aba8af5c29f2907/7ea65/gift_as_is.webp 1663w&quot; sizes=&quot;(max-width: 1663px) 100vw, 1663px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d75be0cdeae508a23aba8af5c29f2907/a3e66/gift_as_is.jpg 480w,
/static/d75be0cdeae508a23aba8af5c29f2907/fb816/gift_as_is.jpg 960w,
/static/d75be0cdeae508a23aba8af5c29f2907/064b1/gift_as_is.jpg 1663w&quot; sizes=&quot;(max-width: 1663px) 100vw, 1663px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d75be0cdeae508a23aba8af5c29f2907/064b1/gift_as_is.jpg&quot; alt=&quot;선물하기 구 구조도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figure style=&quot;color: #afafaf&quot;&gt;&amp;lt;선물하기 개편 이전 AS-IS&amp;gt;&lt;/figure&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;선물하기관은-어떻게-개편되었나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%A0%EB%AC%BC%ED%95%98%EA%B8%B0%EA%B4%80%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%9C%ED%8E%B8%EB%90%98%EC%97%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;선물하기관은 어떻게 개편되었나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;선물하기관은 어떻게 개편되었나요?&lt;/h2&gt;
&lt;p&gt;올리브영의 전시 전략과 선물하기 기능이 따로 운영되면서, &lt;strong&gt;사용자 경험을 개선&lt;/strong&gt;하고 &lt;strong&gt;서비스의 일관성을 유지&lt;/strong&gt;하기 위해 두 기능을 통합할 필요가 있었습니다. 통합이 필요했던 사유는 위와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;일관된 사용자 경험 제공&lt;/strong&gt;: 전시 전략과의 통합을 통해, 선물하기관 이용 시 올리브영의 다른 서비스와 일관된 경험을 제공할 수 있어야 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;효율적인 자원 관리&lt;/strong&gt;: 서비스가 동일한 전시 전략을 따르도록 해, 전체 트래픽 증가 상황에서도 안정적인 성능을 유지할 수 있게 해야 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 동기화&lt;/strong&gt;: 전시 전략과 통합하여 선물하기관과 다른 전시 페이지 간의 데이터 동기화를 원활히 함으로써 최신 정보 제공과 실시간 반영이 가능해야 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유지보수 효율성 강화&lt;/strong&gt;: 동일한 전략을 기반으로 개발하여 신규 기능 개발 및 유지보수의 효율성을 높이고 오류 발생 시 신속하게 대응할 수 있도록 만들어야 했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;올리브영 전시 전략을 바탕으로, 선물하기는 리플렉션 기법을 사용하여 새로운 기능 추가나 서비스 방향에 따라 신속하게 개발하고 적용할 수 있도록 했습니다. &lt;/p&gt;
&lt;p&gt;아래의 불편한 점을 개선하기 위해 해당 기법을 적용하기로 하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;@CircuitBreaker 애너테이션과 @Cacheable 애너테이션을 선언하고 연관된 값을 일일히 설정해야 했습니다.&lt;/li&gt;
&lt;li&gt;스프링에서 관리하는 캐시 매니저를 사용하여 레디스 캐시명과 TTL 등의 설정이 서로 다른 위치로 분리되어 파편화되어 있었습니다.&lt;/li&gt;
&lt;li&gt;키값이나 fallback을 지정할 때 &quot;문자열&quot;을 사용하여 IDE에서 문제를 검출하기 어려웠고 변경 시 실수 확률이 높아졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 불편한 점을 개선하고, 리플렉션을 사용하여 위와 같은 이점을 얻을 수 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache Stampede 방지를 위한 로직 등을 단일 포인트에서 &lt;strong&gt;공통로직으로 제어&lt;/strong&gt;하여, 쉽게 개선할 수 있도록 하였습니다.&lt;/li&gt;
&lt;li&gt;파라미터 처리에 AOP를 활용하여, 개발자가 비즈니스 로직에 집중할 수 있도록 이점 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;해당 코드와 같이 &lt;strong&gt;커스텀 어노테이션&lt;/strong&gt;을 바탕으로 전시 전략 바탕 아래 선물하기 서비스에 맞춘 전시 전략을 세울 수 있었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@CircuitBreaker&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;giftCircuitBreaker&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    fallbackMethod &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fallbackGiftVersion&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@Cacheable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    cacheManager &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;giftManager&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;gift:Page&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{#giftPageNumber}&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    unless &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;#result == null or #result.isEmpty()&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchGiftVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
   giftPageNumber&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GiftReturnDto &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 로직 실행&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code class=&quot;language-text&quot;&gt;@GiftCaching&lt;/code&gt;과 &lt;code class=&quot;language-text&quot;&gt;@GiftCachingKey&lt;/code&gt;어노테이션은 스프링 AOP와 Kotlin 리플렉션을 활용하여 캐싱 기능을 구현한 커스텀 어노테이션이며, 설계 과정과 발생했던 이슈를 해결해나간 자세한 이야기들은 2부에서 다룰 예정입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token annotation builtin&quot;&gt;@GiftCaching&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    gnbRedisCacheInfo &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; GIFT_PAGE_KEY&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    dateTimeKeySuffix &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LOCAL_DATE
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchGiftVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token annotation builtin&quot;&gt;@GiftCachingKey&lt;/span&gt; giftPageNumber&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Long
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GiftReturnDto &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 로직 실행&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;선물하기 개편에 적용된 올리브영의 전시 전략은 아래 기술 블로그 글들을 참고해 주세요.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-10-17/oliveyoung-mall-home-new-architecture/&quot;&gt;올리브영 전시 아키텍처 개편&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-01-04/oliveyoung-discovery-mongodb/&quot;&gt;몽고DB를 활용한 전시 시스템&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-08-31/circuitbreaker-inventory-squad/&quot;&gt;Circuitbreaker를 사용한 장애 전파 방지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2023-11-07/ranking-system/&quot;&gt;랭킹 시스템 개편기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-10-30/application_warmup_algorithm/&quot;&gt;커스텀 어노테이션과 리플렉션으로 구현한 Spring Boot 웜업 로직 최적화&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;개편을-통해-무엇이-달라졌나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%ED%8E%B8%EC%9D%84-%ED%86%B5%ED%95%B4-%EB%AC%B4%EC%97%87%EC%9D%B4-%EB%8B%AC%EB%9D%BC%EC%A1%8C%EB%82%98%EC%9A%94&quot; aria-label=&quot;개편을 통해 무엇이 달라졌나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;개편을 통해 무엇이 달라졌나요?&lt;/h2&gt;
&lt;p&gt;이번 개편은 선물하기관의 성능 및 신뢰성을 개선하는 데 중점을 두었습니다.
DB 리소스 점유 문제와 실시간 정보 반영, 오류 데이터 문제를 해결함으로써 고객분들이 보다 빠르고 정확하게 상품 정보를 확인하고 선물하기 기능을 이용할 수 있게 되었습니다.&lt;/p&gt;
&lt;div style=&quot;display: flex; justify-content: center; text-align: center&quot;&gt;
   &lt;div style=&quot;width: 90%;&quot;&gt;
        &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1511px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/5c2c5ea9868113823c3bd8d11f141ec0/4e466/gift_to_be.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 42.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAECAwX/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAAB7sqF1GJ//8QAGRAAAgMBAAAAAAAAAAAAAAAAAAEDITIR/9oACAEBAAEFAmzlWSZej//EABURAQEAAAAAAAAAAAAAAAAAAAEQ/9oACAEDAQE/ASf/xAAVEQEBAAAAAAAAAAAAAAAAAAAQQf/aAAgBAgEBPwGH/8QAGhAAAgIDAAAAAAAAAAAAAAAAAAECECFxgf/aAAgBAQAGPwIeXXUT1X//xAAeEAACAQMFAAAAAAAAAAAAAAAAAREQMUEhgaHB8P/aAAgBAQABPyFyshwZiG8I3LXjU4nun//aAAwDAQACAAMAAAAQMy//xAAXEQADAQAAAAAAAAAAAAAAAAAAAREh/9oACAEDAQE/ENOEP//EABcRAAMBAAAAAAAAAAAAAAAAAAABESH/2gAIAQIBAT8QeUU//8QAHRABAAICAgMAAAAAAAAAAAAAARExACEQUUFx8P/aAAgBAQABPxAIsUvqc3QImFrXrDxXHgznwunEBRn/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5c2c5ea9868113823c3bd8d11f141ec0/263a4/gift_to_be.webp 480w,
/static/5c2c5ea9868113823c3bd8d11f141ec0/a6361/gift_to_be.webp 960w,
/static/5c2c5ea9868113823c3bd8d11f141ec0/ad42e/gift_to_be.webp 1511w&quot; sizes=&quot;(max-width: 1511px) 100vw, 1511px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5c2c5ea9868113823c3bd8d11f141ec0/a3e66/gift_to_be.jpg 480w,
/static/5c2c5ea9868113823c3bd8d11f141ec0/fb816/gift_to_be.jpg 960w,
/static/5c2c5ea9868113823c3bd8d11f141ec0/4e466/gift_to_be.jpg 1511w&quot; sizes=&quot;(max-width: 1511px) 100vw, 1511px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5c2c5ea9868113823c3bd8d11f141ec0/4e466/gift_to_be.jpg&quot; alt=&quot;선물하기 신 구조도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
        &lt;figure style=&quot;color: #afafaf&quot;&gt;&amp;lt;선물하기 개편 이후 TO-BE&amp;gt;&lt;/figure&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;div style=&quot;display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width: 40%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 415px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/48e26067278cda05dee24a0bf40ba820/80028/gift_as_is_speed.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 38.795180722891565%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB10lEQVR42lVQ30tTcRS//0g+iyEEkhEIYfYSgeZLEewtIejBV/XBSB+G6IuEFFFQGGYOXIITJmIkqFEjZeiIpXNz7e7H3b27d7tz997t3n063yMz+8Dhe87nfM7nnnMlz/PQbDbh0isg8lZchqg97zxafTHbmhew7Dok13W5cF0PsV9xIu3zmoSR/Si+bu/iW+QncgWFeUXVcHySvPjIZbChSBqNBkae+9HW2Y0/cpabtu1gcmYWw6PP0N59CwuBIPODvsfwPRnm/CiRxF70AHWa/89Q03W8/xhAz91BJJIpiOuFoUAmm0PfwEOUTROr4Q3ORyf82NzaxvW+exj3z7CGDS0HUmvtmmWht//BxTm2U+d3cnoWY2SQp5NvkIHYWGy58z2C+74hNtRKxr8N80WdzByoRF692YtEKo3V9S9YWgnR/9JxrecOfh8nkM7ImH7xCo+GnpLuNpbXNjD3dh5XOroQDIXZ0KxakI5SMtK5IoWKl+8WcJpVsPh5DYvBEPYO43jzYQllEmpGlYf2Y3GshDfxIxrDyMQUXs9/wqmsIK8akAslSKphQilVOKy6B0WvwKSNKzUHWrlKnMvivGYgU9CIO+O+bp7BbjRRo36RPAqlMuv+AmcDKQ/vJBTdAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/48e26067278cda05dee24a0bf40ba820/c93a9/gift_as_is_speed.webp 415w&quot; sizes=&quot;(max-width: 415px) 100vw, 415px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/48e26067278cda05dee24a0bf40ba820/80028/gift_as_is_speed.png 415w&quot; sizes=&quot;(max-width: 415px) 100vw, 415px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/48e26067278cda05dee24a0bf40ba820/80028/gift_as_is_speed.png&quot; alt=&quot;선물하기 개편 이전 속도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;div style=&quot;width: 2%;&quot;&gt;
   &lt;/div&gt;
 &lt;div style=&quot;width: 40%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 423px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/1bccb95d8452ff8108b6c7948cc67ec0/6fe3b/gift_to_be_speed.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 39.71631205673759%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuklEQVR42m2Qy28SURjF+VdduOqmURtbESOlrQ22JsZoYhcmaozBRAJVi7bSOgGtIjaWpPhKuzAVaBlgmGF4zDD8/O7FRtN4k5PzPc45c+eGfH/IYOAx8AYMh+Nas+ehdoyQPsDzfU60QRCcwoiRQNWhjtul3+8zGonzP8fzfC389yjtaagTCIdUUdors5pZ5/v+Aam1V1SqNdLCu3vfqNUt6maD98UdtvLvSD7LYNuO3HZ8O/VB9Qc6MPgTWKkdMR1d4LWR48q1JWaii0yGY8wt36HnwY3bK0xFYuyUykxMhXn8NM3Fq4uE5+LcvZ8gGr/Jy6yhQ3XgVu4tFyKzfP2xz6VYnLOT05yPzDO7dItsrsCZiXM6KJHKkEiucu9RQvYLzMSuc3l+WXYRqePjQE8een3TYOVBgucbBg+fpCmWvginyOYLGNtFDqvHEvyBTQk/+PmLj7tl3mx/Il/4rDn5YoO1bI6G1SF0WDOptxza7oCG3aXp9HTf6vQx2y6m5VKptziWmSmGqmlx1LRF09GzusyUT6Eq7x2y3Z4ki7hli8HWrHtrzApNW0xtR6PZ/lufQPtEZzldfgMo+TWJiLSKiAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1bccb95d8452ff8108b6c7948cc67ec0/008d7/gift_to_be_speed.webp 423w&quot; sizes=&quot;(max-width: 423px) 100vw, 423px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1bccb95d8452ff8108b6c7948cc67ec0/6fe3b/gift_to_be_speed.png 423w&quot; sizes=&quot;(max-width: 423px) 100vw, 423px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1bccb95d8452ff8108b6c7948cc67ec0/6fe3b/gift_to_be_speed.png&quot; alt=&quot;선물하기 개편 이후 속도&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;width: 100%; text-align: center;&quot;&gt;
    &lt;span style=&quot;color: #afafaf&quot;&gt;&amp;lt; 선물하기 개편 이전 / 개편 이후 평균 속도 &amp;gt;&lt;/span&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;br/&gt;
&lt;div style=&quot;display: flex; justify-content: center;&quot;&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 628px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7fba0cf316dc811094fbe82acac27831/cbb7d/gift_New_01.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAIDBAUB/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAezOKJojOxk6DMNHty2C0f/EAB4QAAEEAwADAAAAAAAAAAAAAAEAAgMREhMhIDJB/9oACAEBAAEFAsuhDqtfY/QyBbQmyWHXl1R3iY2XqYhGweH/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/AV//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/AV//xAAiEAABAgQHAQAAAAAAAAAAAAAAASEQMTKhAhEgIkFRYXH/2gAIAQEABj8CXw+wxOI/A65r2LsKCQsHKbkrjJo//8QAHxABAAIBBAMBAAAAAAAAAAAAAQARITFBUWGBocGx/9oACAEBAAE/IbU/iMo8pTlGlk0eoXpeksApteUuFsOHuUI7IWCueYsNdWppy+5crF3EJXlgrkSm9zD7H7Def//aAAwDAQACAAMAAAAQ7C5PMA//xAAXEQEAAwAAAAAAAAAAAAAAAAABECAh/9oACAEDAQE/EEjaf//EABcRAQEBAQAAAAAAAAAAAAAAAAEAESD/2gAIAQIBAT8QHYteP//EAB8QAQADAQABBQEAAAAAAAAAAAEAESExUUFxgZGxwf/aAAgBAQABPxCsFVt3vtsD0sa3+8lNQFNbcyRD1/UVMqcYtfMJKyAVaaTBRcp8TmBNG8PqPFCUqAXhCGV2NRlfcdiQZ4PYihCmnT9xIVG1SsfHZWhGyWdnqnif0n//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7fba0cf316dc811094fbe82acac27831/263a4/gift_New_01.webp 480w,
/static/7fba0cf316dc811094fbe82acac27831/189c6/gift_New_01.webp 628w&quot; sizes=&quot;(max-width: 628px) 100vw, 628px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7fba0cf316dc811094fbe82acac27831/a3e66/gift_New_01.jpg 480w,
/static/7fba0cf316dc811094fbe82acac27831/cbb7d/gift_New_01.jpg 628w&quot; sizes=&quot;(max-width: 628px) 100vw, 628px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7fba0cf316dc811094fbe82acac27831/cbb7d/gift_New_01.jpg&quot; alt=&quot;선물하기 신 테마&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
   &lt;div style=&quot;width: 2%;&quot;&gt;&lt;/div&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1440px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e5a6839ba4d2961dda3eff3bc9c683e9/33266/gift_New_02.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAMBAgQF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAe5POcbBBF65pt1iSG0cWBYP/8QAHRAAAgICAwEAAAAAAAAAAAAAAQIAEQMSEBMhIP/aAAgBAQABBQL2Dgb9pLxC2trLWgVgxHsGELjTHQ2m3l/H/8QAFhEBAQEAAAAAAAAAAAAAAAAAECEA/9oACAEDAQE/ASaP/8QAFhEAAwAAAAAAAAAAAAAAAAAAARAg/9oACAECAQE/AUY//8QAIBAAAgEDBAMAAAAAAAAAAAAAAAERAhAhEiAxMiIzQf/aAAgBAQAGPwK9VL1RzJKnHw81DG5R2RhobnBppt1PW+dv/8QAHRABAQEAAgMBAQAAAAAAAAAAAREAIUExUWHBcf/aAAgBAQABPyE+jcD7oZd60B69ZFFI4TBz6+ONxLg95/DZbnHK/juxy0yYAqLxLrtir61R5XXUXw65+4o8OP3P7jf/2gAMAwEAAgADAAAAEOMnMDMP/8QAGBEAAwEBAAAAAAAAAAAAAAAAAAEhIDH/2gAIAQMBAT8QcKdK4f/EABkRAAMAAwAAAAAAAAAAAAAAAAEQEQAhMf/aAAgBAgEBPxACrTmUv//EACAQAQEAAgICAgMAAAAAAAAAAAERACExQVFxgaEQkdH/2gAIAQEAAT8QS4Er1e/eHU7W3f8AXBC7/eHkCiUVTbpuzGqXaFvb6uXxpuQenA6totITneGnVgAS+M4KTFDnLwV0AqXn5mWdSFhcWeZk5YG0XgPowAQ/CkXer8feJVgM6Q+3Wb6Twmdvxe3vP//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e5a6839ba4d2961dda3eff3bc9c683e9/263a4/gift_New_02.webp 480w,
/static/e5a6839ba4d2961dda3eff3bc9c683e9/a6361/gift_New_02.webp 960w,
/static/e5a6839ba4d2961dda3eff3bc9c683e9/95f20/gift_New_02.webp 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e5a6839ba4d2961dda3eff3bc9c683e9/a3e66/gift_New_02.jpg 480w,
/static/e5a6839ba4d2961dda3eff3bc9c683e9/fb816/gift_New_02.jpg 960w,
/static/e5a6839ba4d2961dda3eff3bc9c683e9/33266/gift_New_02.jpg 1440w&quot; sizes=&quot;(max-width: 1440px) 100vw, 1440px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e5a6839ba4d2961dda3eff3bc9c683e9/33266/gift_New_02.jpg&quot; alt=&quot;선물하기 신 랭킹&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
   &lt;div style=&quot;width: 2%;&quot;&gt;&lt;/div&gt;
   &lt;div style=&quot;width: 30%;&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 628px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/87baa4aea4aa4ad22cc8df7ebcf0f812/cbb7d/gift_New_03.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 203.75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAApABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAEDBAIF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAfZmjRBIom7k6dKxtaKWhX//xAAdEAEBAAICAwEAAAAAAAAAAAABAgADECEREhMx/9oACAEBAAEFAvK8d47I1V7GSrjKtSuT+dDVSGbB+llOvQM6uf/EABYRAAMAAAAAAAAAAAAAAAAAABAgMf/aAAgBAwEBPwEVP//EABYRAAMAAAAAAAAAAAAAAAAAABARIP/aAAgBAgEBPwGEP//EAB8QAAEDBAMBAAAAAAAAAAAAAAEAAiEQERIxICJBUf/aAAgBAQAGPwLza0tK2NsvgoYUWQrkTFGkJsdkGu2I4f/EAB8QAQACAgEFAQAAAAAAAAAAAAEAESExURBBYXHBof/aAAgBAQABPyEvFLTDASq/UMC8piglfIxDfFzKK00Z3MYWHaajlGqKYamy2lupg35QRBGx7yrql58Qi9la6NiPsfsJ/9oADAMBAAIAAwAAABAcOry3D//EABkRAAIDAQAAAAAAAAAAAAAAAAABEBEhQf/aAAgBAwEBPxB4WPhwnUf/xAAXEQEAAwAAAAAAAAAAAAAAAAABABAx/9oACAECAQE/ENgQAaAZX//EAB8QAQACAgIDAQEAAAAAAAAAAAEAESExQVFhcYGh0f/aAAgBAQABPxDE8LuE8+Y2qG3a1MlkGQWWb5NE1bRu2O0cq04ISkK2Bp3jXqWRFkVeWaxubDRgdPqLUtt9VGia8C75ZTytVbHyGyBYGxO4/CpRrL+SpVDQNhSk/ZQICqihBwnypynSOXuf/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/87baa4aea4aa4ad22cc8df7ebcf0f812/263a4/gift_New_03.webp 480w,
/static/87baa4aea4aa4ad22cc8df7ebcf0f812/189c6/gift_New_03.webp 628w&quot; sizes=&quot;(max-width: 628px) 100vw, 628px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/87baa4aea4aa4ad22cc8df7ebcf0f812/a3e66/gift_New_03.jpg 480w,
/static/87baa4aea4aa4ad22cc8df7ebcf0f812/cbb7d/gift_New_03.jpg 628w&quot; sizes=&quot;(max-width: 628px) 100vw, 628px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/87baa4aea4aa4ad22cc8df7ebcf0f812/cbb7d/gift_New_03.jpg&quot; alt=&quot;선물하기 신 서브 페이지&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
   &lt;/div&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;이번 개편을 통해 선물하기관은 더 빠르고 정확한 정보를 제공하는 서비스로 개편되었습니다.&lt;/p&gt;
&lt;p&gt;1부에서는 기존 선물하기관의 문제점과 개편의 필요성, 개선된 상태를 소개해 드렸습니다.&lt;/p&gt;
&lt;p&gt;2부에서는 개편 과정의 세부 기술과 구현 과정을 더 자세히 다룰 예정입니다.&lt;/p&gt;
&lt;p&gt;많은 관심과 기대 부탁드립니다!&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[재고의 변동을 시계열 데이터로?!]]></title><description><![CDATA[…]]></description><link>https://oliveyoung.tech/2024-11-15/inventory-changed-stocks-function-with-redis-stream/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-11-15/inventory-changed-stocks-function-with-redis-stream/</guid><pubDate>Fri, 15 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 인벤토리 스쿼드 백엔드 개발을 담당하고 있는 한첨지입니다! &lt;br&gt;
앞서, 올여우님께서 &lt;a href=&quot;https://oliveyoung.tech/2023-10-04/inventory-project/&quot;&gt;신규 재고 시스템 구축을 위한 개발 여정&lt;/a&gt;을 소개해 주셨는데요, &lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;신규 구축 이후로도 인벤토리 스쿼드에서는 올리브영의 타 시스템에서 재고를 빠르고 효율적으로 가져가실 수 있도록 끊임없이 개선의 과정을 거치고 있습니다.&lt;br&gt;
이번에 그중 하나인 Redis Stream으로 특정 기간 변동 재고 제공 사례를 소개해 드리고자 합니다! &lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;배경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%EA%B2%BD&quot; aria-label=&quot;배경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;배경&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;올리브영의 일부 시스템에서는, 특정 주기를 가지고 올리브영 모든 매장, 모든 상품의 재고 수량 정보를 가져가 각자의 시스템에 맞게 색인하여 활용합니다.&lt;br&gt;
매장의 상품 단위를 SKU(Stock Keeping Unit)라고 하는데, 올리브영은 &lt;b&gt;1,000만 SKU&lt;/b&gt;가 넘습니다.&lt;/p&gt;
&lt;p&gt;일정 시간 단위 주기로 1,000만 건 이상의 대량 데이터를 가져가기 때문에 주기마다 Redis CPU 및 API Latency가 급격히 증가하는 현상이 발생하였는데요,&lt;/p&gt;
&lt;p&gt;먼저 현재 인벤토리 Redis의 구조에 대해 살펴봅시다. &lt;br&gt;
인벤토리 스쿼드에서는 1개 SKU 조회 기준으로 고성능 API 속도를 유지하기 위해 Redis에 Hash 타입으로 올리브영 전체 SKU의 재고 수량 정보를 저장합니다.&lt;br&gt;&lt;/p&gt;
&lt;table class=&quot;tg&quot;&gt;
&lt;thead&gt;
   &lt;tr&gt;
      &lt;th class=&quot;tg-0pky&quot; rowspan=&quot;2&quot;&gt;Key&lt;/th&gt;
      &lt;th class=&quot;tg-0pky&quot; colspan=&quot;2&quot;&gt;Value&lt;/th&gt;
   &lt;/tr&gt;
   &lt;tr&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;field&lt;/th&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;value&lt;/th&gt;
   &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
   &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;상품키&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;수량&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;10&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;상품키&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;...&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;...&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;...&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;...&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;...&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;p&gt;위 구조에서는 항상 최신의 재고 수량만 보유하기 때문에, 타 시스템에서 재고를 색인하려면 Redis의 모든 데이터를 &lt;b&gt;탈탈&lt;/b&gt; 털어 전체 재고를 가져가야 하는 비효율이 발생하였는데요,&lt;/p&gt;
&lt;p&gt;이런 비효율을 개선하기 위해 매장 내 POS, 물류 관리 시스템, 백오피스를 비롯해 여러 시스템에서 발생하는 상품의 판매, 입고, 반품, 폐기 등 모든 재고 변동 이벤트를 Redis Stream 데이터로 기록하기로 했습니다.&lt;br&gt;
Redis Stream이 무엇이고 Redis Stream을 선택한 이유를 알아볼까요?&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/294463dc80ad738e9848f68c5b51ebd0/3e7c2/inventory.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABM0lEQVR42n1Ry07DMBD0z8EPIT4Aid/oqdx759ZrD1wrcWyBNiJxEifxI7Ez7G6S8hCqpdV6HzOe9aqYErwP6PseMUYsZxxHBO8lP1IPx+Ocv3ZUIMDb+wcKXcI6zxApJCJpu05MCIcB6c+j/xIykEl7AsQhTkrIWNnr0xovjw+oj0e0ux3KzQZB62WESy9bSpNXMSZR1rQdBiakQqKCp3Gf7++wvr1Btt/DbLfIVyu4LMPABKSU1RrTIC80TNMKqUpEGEIPH4IQLuP2FDdlCX0+YyRyczpBHw5wxsiDrJDFdNahplzbWcEpXoojgKUCj81/mGYF4Jg9kWOOE9nPxUwjf3vFsqva4DMvZNtL80DAqqpQ17UY3w0p6WhJV5fy+2PTpcB3a60Y/6dzTnxgtVcIvwCZ7m178JP32QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/294463dc80ad738e9848f68c5b51ebd0/263a4/inventory.webp 480w,
/static/294463dc80ad738e9848f68c5b51ebd0/a6361/inventory.webp 960w,
/static/294463dc80ad738e9848f68c5b51ebd0/0b34d/inventory.webp 1920w,
/static/294463dc80ad738e9848f68c5b51ebd0/25e94/inventory.webp 2206w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/294463dc80ad738e9848f68c5b51ebd0/9aebd/inventory.png 480w,
/static/294463dc80ad738e9848f68c5b51ebd0/a91f8/inventory.png 960w,
/static/294463dc80ad738e9848f68c5b51ebd0/ac7a9/inventory.png 1920w,
/static/294463dc80ad738e9848f68c5b51ebd0/3e7c2/inventory.png 2206w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/294463dc80ad738e9848f68c5b51ebd0/ac7a9/inventory.png&quot; alt=&quot;inventory&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;uredis-stream이란u&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#uredis-stream%EC%9D%B4%EB%9E%80u&quot; aria-label=&quot;uredis stream이란u permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;u&gt;Redis Stream이란?&lt;/u&gt;&lt;/h4&gt;
&lt;p&gt;Redis Stream은 로그 데이터를 처리하기 위해 Redis 5.0부터 도입된 자료 구조입니다.
&lt;br&gt;
Redis Stream은 크게 두 가지 특성이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;append-only의 특징을 가진 시계열 데이터 처리&lt;/li&gt;
&lt;li&gt;기록된 데이터 소비(메시징 시스템, 영속성을 가진 메시지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 특성 덕분에 Redis Stream은 실시간 데이터 처리, 로그 수집, 메시지 브로커 및 이벤트 드리븐 아키텍처 등에 널리 활용됩니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;아래 표는 Redis Stream의 기본 명령어입니다.&lt;/p&gt;
&lt;table class=&quot;tg_1&quot;&gt;
&lt;thead&gt;
   &lt;tr&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;종류&lt;/th&gt;
      &lt;th class=&quot;tg-0pky&quot;&gt;설명&lt;/th&gt;
   &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
   &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;XADD&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;스트림에 새 Entry 추가합니다.&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;XREAD&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;주어진 위치에서 시작하여 시간 순서대로 앞으로 이동하면서 하나 이상의 항목을 읽습니다.&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;XRANGE&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;두 개의 제공된 Entry ID 사이의 범위에 해당하는 Entry를 반환합니다.&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
      &lt;td class=&quot;&quot;&gt;XLEN&lt;/td&gt;
      &lt;td class=&quot;&quot;&gt;스트림의 Length를 반환합니다.&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;br&gt;
&lt;p&gt;이 밖의 Redis Stream 명령어가 더 궁금하시다면 &lt;a href=&quot;https://redis.io/docs/latest/commands/?group=stream&quot;&gt;여기&lt;/a&gt;를 참고해 주세요!&lt;/p&gt;
&lt;p&gt;추가적인 인프라 구성없이 시계열 데이터를 처리할 수 있다는 점이 Redis Stream을 선택한 이유입니다.&lt;br&gt;
이제 구현 과정을 살펴볼까요~&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h2 id=&quot;목표-및-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A9%ED%91%9C-%EB%B0%8F-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;목표 및 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;목표 및 구현&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;기능을 제공하기에 앞서 변동 재고를 주기 위해 다음과 같은 목표를 세웠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;처리 순서 보장&lt;/li&gt;
&lt;li&gt;빠른 조회 속도 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;u처리-순서-보장u&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#u%EC%B2%98%EB%A6%AC-%EC%88%9C%EC%84%9C-%EB%B3%B4%EC%9E%A5u&quot; aria-label=&quot;u처리 순서 보장u permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;u&gt;처리 순서 보장&lt;/u&gt;&lt;/h4&gt;
&lt;br&gt;
Redis Stream은 데이터 생성 시 자동으로 고유한 Entry ID를 생성합니다.
&lt;br&gt;&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;저장된 데이터를 Entry라고 하는데, 각 Entry는 고유의 Entry ID를 가집니다.&lt;/li&gt;
&lt;li&gt;별도 설정이 ID를 지정해 주지 않으면 millisecondsTime-sequenceNumber 형태로 저장됩니다.&lt;/li&gt;
&lt;li&gt;sequenceNumber는 동일한 밀리초에 생성된 경우 0,1,2, ... 순차적으로 증가합니다.
&lt;ul&gt;
&lt;li&gt;sequenceNumber는 64비트 범위이므로 사실상 동일 밀리초에 생성할 수 있는 항목 수에 제한이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; XADD inventory-20241030 * store-id STORE_A product-id GOODS_A stock-count 10
&quot;1730271083140-0&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;각 순서는 다음 값을 의미합니다.
&lt;ul&gt;
&lt;li&gt;XADD [스트림 Key] [Entry ID] [필드-값]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-text&quot;&gt;*&lt;/code&gt;는 Entry ID 자동 생성을 트리거하는 것을 의미하며, 명시적으로 지정할 수 있습니다.(권장하진 않는다고 합니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; XADD inventory-explicit-id 0-1 store-id STORE_A product-id GOODS_A stock-count 10
&quot;0-1&quot;

&gt; XADD inventory-explicit-id 0-1 store-id STORE_A product-id GOODS_B stock-count 5
&quot;(error) ERR The ID specified in XADD is equal or smaller than the target stream top item&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
Redis Stream은 Entry ID를 기준으로 범위 쿼리를 지원합니다.
&lt;br&gt;&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;XRANGE 명령어는 Entry ID를 기준으로 범위를 조회합니다.&lt;/li&gt;
&lt;li&gt;-는 최소 Entry ID, +는 최대 Entry ID를 의미합니다.&lt;/li&gt;
&lt;li&gt;XREVRANGE 명령어로 역순 조회도 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&gt; XRANGE inventory-20241030 - +
1) 1) &quot;1730271083140-0&quot;
   2) 1) &quot;store-id&quot;
      2) &quot;STORE_A&quot;
      3) &quot;product-id&quot;
      4) &quot;GOODS_A
      5) &quot;stock-count&quot;
      6) &quot;10&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
&lt;p&gt;XRANGE 명령어를 통해 반환된 데이터는 자동으로 생성된 Entry ID로 그 순서를 보장할 수 있습니다. &lt;br&gt;
명령어만으로 설명하면 심심하니 Redisson 라이브러리를 적용한 예제 코드도 살펴볼까요?&lt;/p&gt;
&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// XADD&lt;/span&gt;
fun &lt;span class=&quot;token function&quot;&gt;addStockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; storeId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; productId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stockCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    redissonClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getStream&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;let &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token class-name&quot;&gt;StreamAddArgs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;token function&quot;&gt;mapOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;token string&quot;&gt;&quot;store-id&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;storeId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;token string&quot;&gt;&quot;product-id&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;productId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;token string&quot;&gt;&quot;stock-count&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;stockCount&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 호출&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;addStockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;inventory-1030&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;STORE_A&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GOODS_A&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;addStockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;inventory-1030&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;STORE_A&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GOODS_B&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
여기서 잠깐!!&lt;br&gt;
지금까지 예제에서 쓰인 Stream Key 뒤에 왜 날짜로 구별할까요? &lt;br&gt;
&lt;br&gt;
&lt;p&gt;변동 재고의 경우, 재고를 동기화하는 시스템에 API로 제공하는 것을 목표로 하므로 영구적으로 데이터를 보관할 필요가 없습니다. &lt;br&gt;
이 때문에 당일의 변동 재고를 저장하는 Key는 &quot;StreamKey-날짜&quot; 형식으로 설계하였습니다.&lt;/p&gt;
&lt;br&gt;
Stream Key에 TTL을 주면, TTL이 만료된 이후에 내부적으로 데이터가 정리되어 지난 날짜들의 더미 데이터를 신경 쓰지 않아도 되는 이점이 있습니다.&lt;br&gt;
&lt;br&gt;
add 함수 다음에 expire 함수로 TTL 기간을 지정해 주면 됩니다.
&lt;br&gt;&lt;br&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;expire&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ofDays&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;br&gt;&lt;br&gt;
이어서 범위를 조회하기 위한 XRANGE 예제도 살펴봅니다~
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// XRANGE&lt;/span&gt;
fun &lt;span class=&quot;token function&quot;&gt;findStockHistoriesIn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; startMilli&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; endMilli&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;StockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; redissonClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getStream&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;token class-name&quot;&gt;StreamMessageId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;startMilli&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// included&lt;/span&gt;
                &lt;span class=&quot;token class-name&quot;&gt;StreamMessageId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;endMMilli&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// excluded&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;let &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;entries&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; entry &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
                    &lt;span class=&quot;token class-name&quot;&gt;StockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;domainFromMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;br&gt;
여기서 또 의문이 한 가지 생깁니다. &lt;br&gt;
&lt;p&gt;시간 범위를 의미하는 startMilli와 endMilli는 어떤 값을 넣어줘야 하나요? &lt;br&gt;
이제부터 XRANGE 명령어의 함정에 관해서 얘기해 보겠습니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Redis는 inmemory를 데이터 저장소로 사용하여 고성능 데이터 처리가 가능하도록 설계되어 있는데요,
&lt;br&gt;
내부적으로는 싱글 스레드로 돌아가기 때문에 한 명령어의 처리 시간이 길어지면 자칫 장애로 이어질 수 있습니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;위험한 명령어로는 KEYS, HGETALL 등이 있습니다.&lt;br&gt;
이 두 명령어는 O(n)의 시간 복잡도를 가지는데요, 저장된 key 또는 field의 개수가 많아질수록 처리 시간이 오래 걸립니다.&lt;br&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;XRANGE 의 시간복잡도는 O(log(N)+M)으로, 데이터가 많을수록 한 번에 조회하는 것은 매우 위험합니다.
&lt;br&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;N : 스트림에 저장된 Entry 개수&lt;/li&gt;
&lt;li&gt;M : 반환될 Entry 개수&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;p&gt;그렇다면 어떻게 빠르고 안전한 조회 속도를 보장했을까요? &lt;br&gt;
이어서 가보겠습니다~!
&lt;br&gt;&lt;br&gt;&lt;/p&gt;
&lt;h4 id=&quot;u빠른-조회-속도-보장u&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#u%EB%B9%A0%EB%A5%B8-%EC%A1%B0%ED%9A%8C-%EC%86%8D%EB%8F%84-%EB%B3%B4%EC%9E%A5u&quot; aria-label=&quot;u빠른 조회 속도 보장u permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;&lt;u&gt;빠른 조회 속도 보장&lt;/u&gt;&lt;/h4&gt;
&lt;br&gt;
&lt;p&gt;KEYS, HGETALL를 써야 할 때 대체할 수 있는 명령어가 있습니다.&lt;br&gt;
바로 SCAN, HSCAN인데요, 간단하게 설명해 드리면 두 명령어는 스캔 범위(count)를 설정하여 해당 count만큼 끊어서 조회합니다.&lt;/p&gt;
&lt;br&gt;
&lt;p&gt;XRANGE를 쓰기 위해 비슷한 방식으로 적절하게 나누어 병렬로 조회합니다.
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1221px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0b0195e3301f12e9c6f35be248822bd6/a7b22/changed-stocks.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.75000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABIElEQVR42pVS2YqDQBD0/39MIS+GBA8QEuOZROMtGo9aqtkJSzYbNgXFtENPd3W1Gr6xLAumacK6rvgv7vc7XNdF13WPO00FwzAgTVP0fS+F/8I8zxjH8RH7vo+qqn4XJLIsk24/E55xvV5xPp8lh/lsTjFqOo0BE+I4hmVZCIIAjuOgLEs0TSOs61rI+HK5wLZtHA4HHI9HEVAUhZAWiEI+ZlGSMUdnIkci6S+p/GXO7XaTJoS6F4VMzPP8YSxPfvP+Fdq2FZX0kc2eoTHhdDohDEMkSSInyXGiKBIrqEg1oIp3f4LGYoZhYLvdYr/fY7PZYLfbQdd1mKYJz/Pebv2lQhpKPziuOqmKZPzJv6nxAdevNsWYhegjjedmPyn4BUOdBwaIO2WtAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0b0195e3301f12e9c6f35be248822bd6/263a4/changed-stocks.webp 480w,
/static/0b0195e3301f12e9c6f35be248822bd6/a6361/changed-stocks.webp 960w,
/static/0b0195e3301f12e9c6f35be248822bd6/3e01c/changed-stocks.webp 1221w&quot; sizes=&quot;(max-width: 1221px) 100vw, 1221px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0b0195e3301f12e9c6f35be248822bd6/9aebd/changed-stocks.png 480w,
/static/0b0195e3301f12e9c6f35be248822bd6/a91f8/changed-stocks.png 960w,
/static/0b0195e3301f12e9c6f35be248822bd6/a7b22/changed-stocks.png 1221w&quot; sizes=&quot;(max-width: 1221px) 100vw, 1221px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0b0195e3301f12e9c6f35be248822bd6/a7b22/changed-stocks.png&quot; alt=&quot;changed stocks&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;예시 코드를 살펴봅시다!&lt;/p&gt;
&lt;p&gt;조회 시에는 RedissonReactiveClient를 사용하면 리액티브 프로그래밍으로 조회 성능을 끌어올릴 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
    timeSplitList : 조회 범위를 밀리초로 변경하여 100ms 간격으로 나눈 리스트
    ex. [1709701855083, 1709701855183, 1709701855283, 1709701855383,...]
**/&lt;/span&gt;
fun &lt;span class=&quot;token function&quot;&gt;findListOfStockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamKey&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; timeSplitList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;StockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Flux&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fromIterable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;timeSplitList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; time &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
            reactiveMemoryDBConnectionFactory&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getStream&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;streamKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Stream-Key&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;token class-name&quot;&gt;StreamMessageId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;time&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// included&lt;/span&gt;
                    &lt;span class=&quot;token class-name&quot;&gt;StreamMessageId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;time &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 100ms, excluded&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                    it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;entries&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; entry &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
                        &lt;span class=&quot;token class-name&quot;&gt;StockHistory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;domainFromMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMapIterable &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; it &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ofSeconds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;collectList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listOf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;b&gt;각 시스템에 맞게 가장 적절한 조회 범위를 찾는 것&lt;/b&gt;이 가장 핵심 내용입니다.&lt;/p&gt;
&lt;p&gt;일반적으로는, 조회 범위가 길어 반환할 Entry 수가 많아지면 시간 복잡도가 증가하여 한 명령어 당 조회 성능이 저하되지만, 조회 범위가 너무 짧으면 명령어 호출 횟수가 그만큼 증가하여 자원을 낭비할 수 있습니다.&lt;/p&gt;
&lt;p&gt;시스템상 데이터가 시간의 흐름에 따라 어떤 분포도를 가지는지 파악하고 그에 맞는 조회 범위를 찾아야 합니다.&lt;br&gt;
시간의 흐름에 따른 스트림에 적재하기 유리한 분포도를 그려보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1908px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/9869de28f025d338b3d5afa39b1ef81d/41afc/graph.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 22.083333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAoUlEQVR42m2PvQqEMBCE8/4PpY2FnWlSJhEUQRMiiij+zjEp5LxzYMOQfDu7EcYY9H0P6rquv/rVG/PNiaqqMAzD4/E4DrxpXVdIKe+AaZrQtu2DETwYmiQJ0jSFUgp5nse7pmkwjiP2fcd5nvDeI8sydF2HEAK01iiKInoutW0bBKfxy2VZwlqLuq5jwzzPsQgxjBw9t2Swcy56DluW5eY+2SMzSOLbEjoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9869de28f025d338b3d5afa39b1ef81d/263a4/graph.webp 480w,
/static/9869de28f025d338b3d5afa39b1ef81d/a6361/graph.webp 960w,
/static/9869de28f025d338b3d5afa39b1ef81d/ad2ae/graph.webp 1908w&quot; sizes=&quot;(max-width: 1908px) 100vw, 1908px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9869de28f025d338b3d5afa39b1ef81d/9aebd/graph.png 480w,
/static/9869de28f025d338b3d5afa39b1ef81d/a91f8/graph.png 960w,
/static/9869de28f025d338b3d5afa39b1ef81d/41afc/graph.png 1908w&quot; sizes=&quot;(max-width: 1908px) 100vw, 1908px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9869de28f025d338b3d5afa39b1ef81d/41afc/graph.png&quot; alt=&quot;graph&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;위 분포도처럼 Redis Stream에 쌓인 Entry의 시간대별 분포를 분석하면 적절한 조회 범위를 찾을 수 있습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;마치며..&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;이번 시간에는 Redis Stream의 시계열 데이터 처리 특성을 중점적으로 개선 과정을 소개하였는데요! &lt;br&gt;
향후 개선 과정에 있어서 Redis Stream을 메시징 시스템으로도 사용하게 된다면, 이어서 소개해 드리겠습니다~ :)&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;style type=&quot;text/css&quot;&gt;
.tg  {border-collapse:collapse;border-spacing:0; width: 30%;}
.tg td{border-color:black;border-style:solid;border-width:1px;font-size:14px;
  overflow:hidden;padding:10px 5px;word-break:normal;}
.tg th{border-color:black;border-style:solid;border-width:1px;font-size:14px; background-color:#d8e0e7;
  font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal; font-weight: bold}
.tg .tg-0pky{border-color:inherit;text-align:middle;vertical-align:top}
.tg .tg-0pky:first-child {font-weight: bold}
.tg tr td:second-child {text-align:middle;}
&lt;/style&gt;
&lt;style type=&quot;text/css&quot;&gt;
.tg_1  {border-collapse:collapse;border-spacing:0; width: 80%;}
.tg_1 td{border-color:black;border-style:solid;border-width:1px;font-size:14px;
  overflow:hidden;padding:10px 5px;word-break:normal;}
.tg_1 th{border-color:black;border-style:solid;border-width:1px;font-size:14px; background-color:#d8e0e7;
  font-weight:normal;overflow:hidden;padding:10px 5px;word-break:normal; font-weight: bold}
.tg_1 .tg-0pky{border-color:inherit;text-align:middle;vertical-align:top}
.tg_1 .tg-0pky:first-child {font-weight: bold}
.tg_1 tr td:second-child {text-align:middle;}
&lt;/style&gt;</content:encoded></item><item><title><![CDATA[Debezium MSK Connect로 Failover 구현하여 서비스 안정성 높이기]]></title><description><![CDATA[안녕하세요. 카탈로그서비스개발팀에서 백엔드 개발을 담당하는 빅토르입니다. Debezium MSK Connect를 운영하며 안정성을 고민했던 경험과 Connector 중단 시 해결 방법에 대해 공유해보겠습니다. Debezium MSK Connect…]]></description><link>https://oliveyoung.tech/2024-11-07/cdc-failover/</link><guid isPermaLink="false">https://oliveyoung.tech/2024-11-07/cdc-failover/</guid><pubDate>Thu, 07 Nov 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 카탈로그서비스개발팀에서 백엔드 개발을 담당하는 빅토르입니다.&lt;/p&gt;
&lt;p&gt;Debezium MSK Connect를 운영하며 안정성을 고민했던 경험과 Connector 중단 시 해결 방법에 대해 공유해보겠습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 429px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/c73c11467973b213b0ceee1dede8c18b/7cc24/start.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAAFaUlEQVR42hWTW2zT5xnGfbFJW6+m0ZtpEl01VVulwTqY6NqKolWQaSBAFTBYxkTXkhQaDiGhISSQ0EDi2EkcHMeH2PHZ8Tm2Yzs+H2PHcUxCEjskECBoXbcpW8sqbdq0i+m3PxeP9L7vxaPfp+d7RFHDHXKmHjLmXhLmPlImMXFTP1H9bWHuI2uTkLaISQmKG24L9x6mtd1EtV2E1R345VeZlLXgll7E3vsxopRtCPOEEb/5LkXXEHnHCw0y45KTdiuxqqXYtTJsmkEc48NYlGImNFIU4k7UQ59hGe1FLelAK2ln5HYroviEHEfAQ8SpFiikTOklRKwyFqaULPkHSFtvk7HeIaLvYlp3g9BoK0FVGyFBgZFmwspWge4cTvFZgbAB0T2/kqpvmJRnDJfHRsEzypx/jGpQxvNqiH9/scTTxRiP5sN8Xs3wl2qStayD9ZyDZ3NeVpNmVhMmVqLjVCNjiPI+DWaHFVXqHqOzD5FrNSTdGh5My/nycQVnYg6d1YXMMonDN8VyOU/Mbydk17K2UGS5UiQeCVLOp1it5BAZvD46JyLc1FqRh3KIIxUkYzpqfilfbsyxvlwhE/ExHfQym4kwX5ohH/ESnbSyMBMnHXJzftjEsFzGctqLqBh3E4iFuCmRcKGji06xFNfEOJtZPVurOeaXVygWMhTnS6TyeZ5Wy3giMSKxKLVClFTYS9uIHpVCRi3rF0Jxj7F31+vsfv1V6t7dw9H33uTY/rfwKtvZWstjdAcwj2uwuVwMDA5STk4xoLXQJwC8mFfnklRiLhZSk1QLYUSHDrzLtQtnsBtUJHxGKtNGepobkH1ymFrOz7O1RR6vzPHnR/dZmE3xcK1KKR1hLhtjUdhTLh12eTeO4ZssJNyITLpBnv9tnR07d6MT/lit5Ocnu35OYPg6KykvwwJNh1RB0OPgqlSFx2ZkcSbG+mKBh4IqST8x0xA5r46lF0/+79fr/O8f6wR1UsqWYf4UNRDoa+WfSwHWAwru9vdhU/QT97txWgykI35WyjmK2QTFTIzKbJZgOMT9uRwPSlFE/6lOwbM4XxesGH/8Q9Tf+jaliyfYivYQuX6Ysv4WDu0IcSHlF2EszBdYKWUIu424hcYErFoalW7kGg2b9xKICr4xHvUdpzZyiuz5XxI9+gbzvQfxf7AD1faX0J85wKnmz4QUFTSofYyPj1GbTZIOOgjohZpGJhmwenC7bDyuCIZ/VHbysO41Uru2YTv0Kua67fRs+ybXRSL027chbaynv6eLuHMcrdXKTGKKzZUSD8ppqrMJapU8IY+VSjbMxrxgWHvnFbbe/xn/Ormbp/t+QOmnL2P6zjeQ7XiF7iNvobxcz5SkmaTlLmmnQFeMsLGYZ3O5yFolSyUfJ5sIE04mWLxXQDT3i+/z+XADXw3+li3V75i//DbR+p1oLxyhdc/3GPr9Xuydv2Fa+hGpkfNkR5uYUV2goG4ir/qEjOJjQY3Yhpow9ZxGdF/XxVfXj/L3S/vIHPsR8cYduHZ+F23jMYyXf012UoVTI8Yq68Cn7sI+0IzhVgOqT+uRXzrGwPnD9J79FQNnDyA+s09oSibNA+lVCntfJlj3Np43XiLa2oC9vxv91eNMW2Qoui/R23QSg6QNo+RTRm80omipx3DtNKNXTtL5wUFufHiI9tP7EZVdvawX3Sz5tOg6/kCwv4VpwyDdp/bTe2IP6ivHMXV/hL5dMGg7gemasLe9j6XtKNa2w5haD6JvrmP8Uh1jTe8h+muonedpCRvOThK3jvBF+A6bzjbKI/VUjQ2smRp5ZD3HE+dlNt1X2PS08MzbIszNwu0iT+xNPJk4x2PLWTbMH/J/4eZoTtwg8ScAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c73c11467973b213b0ceee1dede8c18b/0d06c/start.webp 429w&quot; sizes=&quot;(max-width: 429px) 100vw, 429px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c73c11467973b213b0ceee1dede8c18b/7cc24/start.png 429w&quot; sizes=&quot;(max-width: 429px) 100vw, 429px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c73c11467973b213b0ceee1dede8c18b/7cc24/start.png&quot; alt=&quot;start&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;[ChatGPT로 생성된 이미지입니다.]&lt;/figcaption&gt;
&lt;/div&gt;
&lt;p&gt;Debezium MSK Connect 최초 도입기는 같은 팀 빽곰님의 지난 3월 글을 확인해 주세요~&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://oliveyoung.tech/2024-03-11/msk-cdc-debezium/&quot;&gt;상품데이터 Pipeline을 위한 Debezium MSK Connect&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;모니터링-및-알람-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EB%B0%8F-%EC%95%8C%EB%9E%8C-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;모니터링 및 알람 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;모니터링 및 알람 설정&lt;/h2&gt;
&lt;p&gt;MSK 시스템의 안정성을 확보하기 위해 모니터링은 필수적입니다. 모니터링은 서비스 가용성, 성능 최적화, 데이터 정합성 보장 등 다양한 측면에서 중요합니다. 예를 들어, Connector에서 이상이 발생할 경우 이를 조기에 감지하여 신속하게 대응해야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;알람-시스템-구성-및-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EB%9E%8C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%84%B1-%EB%B0%8F-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;알람 시스템 구성 및 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;알람 시스템 구성 및 관리&lt;/h3&gt;
&lt;p&gt;저희 팀은 이러한 필요성을 인식하고, Connector 이상 발생 시 메시지를 감지하기 위해 Topic Heartbeat 기능을 활용하였습니다. 이 기능은 주기적으로 연결 상태를 확인하고, 문제가 발생할 경우 Slack을 통해 오류 메시지를 발송과 온콜 시스템을 가동합니다. 이를 통해 운영팀은 실시간으로 시스템 상태를 모니터링할 수 있으며 신속하게 문제를 해결할 수 있는 기반을 마련할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Connector 설정 시 &lt;code class=&quot;language-text&quot;&gt;heartbeat.interval.ms&lt;/code&gt; 옵션과 &lt;code class=&quot;language-text&quot;&gt;topic.heartbeat.prefix&lt;/code&gt;를 사용하여 Heartbeat 메시지를 수신합니다. 저희는 이 메시지를 주기적으로 저장하고 있으며 이전 Heartbeat와의 시간 차이가 발생할 경우 알림을 받도록 설정하여 시스템의 안정성을 모니터링하고 있습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1350px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/653e9dd30fc0be7647d90be77983274f/cad38/slack.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.75000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAKABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQCAwX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHehYCY4H//xAAaEAACAgMAAAAAAAAAAAAAAAABAgADEBES/9oACAEBAAEFAtwk81Pay5//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAaEAACAgMAAAAAAAAAAAAAAAAAAQIRICEi/9oACAEBAAY/Ah1tncKeH//EABsQAAICAwEAAAAAAAAAAAAAAAERACEQMXGB/9oACAEBAAE/IbD1QAwoBXYeG+1Es//aAAwDAQACAAMAAAAQc8//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAwEBPxAn/8QAFREBAQAAAAAAAAAAAAAAAAAAARD/2gAIAQIBAT8QZ//EABwQAQEAAQUBAAAAAAAAAAAAAAERIQAxQVGREP/aAAgBAQABPxACKJl25nGnFThNwYPdA/OQXJDO/d80CoBW4+//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/653e9dd30fc0be7647d90be77983274f/263a4/slack.webp 480w,
/static/653e9dd30fc0be7647d90be77983274f/a6361/slack.webp 960w,
/static/653e9dd30fc0be7647d90be77983274f/cbeb9/slack.webp 1350w&quot; sizes=&quot;(max-width: 1350px) 100vw, 1350px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/653e9dd30fc0be7647d90be77983274f/a3e66/slack.jpg 480w,
/static/653e9dd30fc0be7647d90be77983274f/fb816/slack.jpg 960w,
/static/653e9dd30fc0be7647d90be77983274f/cad38/slack.jpg 1350w&quot; sizes=&quot;(max-width: 1350px) 100vw, 1350px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/653e9dd30fc0be7647d90be77983274f/cad38/slack.jpg&quot; alt=&quot;slack&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;msk-connector가-정상적으로-작동하지-않는다면-어떻게-해야-할까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#msk-connector%EA%B0%80-%EC%A0%95%EC%83%81%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%9E%91%EB%8F%99%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;msk connector가 정상적으로 작동하지 않는다면 어떻게 해야 할까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;MSK Connector가 정상적으로 작동하지 않는다면 어떻게 해야 할까요?&lt;/h2&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 602px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/0fd5769e60710ecbc94739d57aed0757/d1b94/full_image.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.875%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB5klEQVR42o1TTY/TMBDtr+YIJ64c+RnsAXFccUBC5WMRq6qs2EWC7iZNk+w2bRzH9jj2Y+y02a90wVI0k3H8ZvLe80RbB+c8uodP5xBWXt7g5HSOVhmQtVCGONdotYHt7p8xjDUhLo4t53rAj9MTvHj5CkVe4OLikmOJPM/RturRmdDgICB8XzdEMTaNwvvjS2RL2dd5Uu/9YcAHe9i/BkBjDLTRWCxWqCoR60S3gCF6Nzoh58wTAn9ZAsgGUmvm06JhDrOqwVq0O0A7PmHdWMxnaxRlg7o1EKsSxEJcTX9BZdcw2+1wMPlTQUqKjXUQxXYxP/+2xfRIRGEmUlmczTPkRYNtSxBZCbHe4HSWYlNu0BV55CNwNvuaYLWsIrhipcl0Mf90vMCb1z95QlY5SC3514bxlYJmFdPzM8g0hd1sIp9E/MvZb9R19YjDsBc4NtQdUJl54y6RTxNyBF8CR2+XSNJ2UDlYK9SJBGT9mX06APoR1/jhYG8bwrPnM0y/5MGl7EPN37joDiFukKYf2PDqCR8OynWRfMUiSCkYSHITy3kblQ6I2noo8jwhc2h3V+x/VpjmrlMcv3TWILlWePedoMn9e8InG+yiYLv9WNS9yndvyp7N/SSjz719H2PH4nhno7H/AnqRQtZmpij7AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0fd5769e60710ecbc94739d57aed0757/263a4/full_image.webp 480w,
/static/0fd5769e60710ecbc94739d57aed0757/3abe5/full_image.webp 602w&quot; sizes=&quot;(max-width: 602px) 100vw, 602px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0fd5769e60710ecbc94739d57aed0757/9aebd/full_image.png 480w,
/static/0fd5769e60710ecbc94739d57aed0757/d1b94/full_image.png 602w&quot; sizes=&quot;(max-width: 602px) 100vw, 602px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0fd5769e60710ecbc94739d57aed0757/d1b94/full_image.png&quot; alt=&quot;Debezium MSK Connect Failover&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;Offset을 사용해 복구할 수 있지만, 이마저도 불가능한 상황에서는 Connector를 새로 생성해야 합니다. 저희 팀이 고민한 해결 방법을 공유해 드리겠습니다.&lt;/p&gt;
&lt;p&gt;저희는 기존 테이블 구조와 다른 신규 시스템 테이블로 데이터를 전송해야 해서, Sink Connector 대신 데이터 변환과 매핑을 직접 수행하고 있습니다. 이를 위해 별도의 컨슈머 워커(Consumer Worker)를 구성하여 MSK에서 수신한 메시지를 변환한 뒤 신규 시스템에 저장합니다. 새로운 Connector 생성 시에는 서비스 로직에서 기존 토픽의 데이터를 원하는 기간만큼 재발행하여 데이터 보정을 합니다.&lt;/p&gt;
&lt;p&gt;또한 비동기 메시지 처리에서는 AOP를 적용해 메시지 수정 시간을 비교하고, 최신 메시지일 경우에만 처리가 되도록 설정했습니다.&lt;/p&gt;
&lt;p&gt;전체적인 흐름은 아래와 같습니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1200px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/2f4d2c9c78890fb8f5df0c23f982ac38/03ffe/image_2.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.916666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3oAH/8QAFhABAQEAAAAAAAAAAAAAAAAAACEB/9oACAEBAAEFAqrH/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFhABAQEAAAAAAAAAAAAAAAAAAAEx/9oACAEBAAY/Amq//8QAGxABAAICAwAAAAAAAAAAAAAAAQAhEVFBYaH/2gAIAQEAAT8hDk+QKZHdQpbe5//aAAwDAQACAAMAAAAQc8//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAWEQADAAAAAAAAAAAAAAAAAAABECH/2gAIAQIBAT8QNX//xAAcEAEAAgEFAAAAAAAAAAAAAAABESEAMUFxgcH/2gAIAQEAAT8QCUtvTYwtpA0hLmultJlXnGf/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2f4d2c9c78890fb8f5df0c23f982ac38/263a4/image_2.webp 480w,
/static/2f4d2c9c78890fb8f5df0c23f982ac38/a6361/image_2.webp 960w,
/static/2f4d2c9c78890fb8f5df0c23f982ac38/cbd37/image_2.webp 1200w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2f4d2c9c78890fb8f5df0c23f982ac38/a3e66/image_2.jpg 480w,
/static/2f4d2c9c78890fb8f5df0c23f982ac38/fb816/image_2.jpg 960w,
/static/2f4d2c9c78890fb8f5df0c23f982ac38/03ffe/image_2.jpg 1200w&quot; sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2f4d2c9c78890fb8f5df0c23f982ac38/03ffe/image_2.jpg&quot; alt=&quot;image 2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;이전-offset으로의-복구&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B4%EC%A0%84-offset%EC%9C%BC%EB%A1%9C%EC%9D%98-%EB%B3%B5%EA%B5%AC&quot; aria-label=&quot;이전 offset으로의 복구 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;이전 Offset으로의 복구&lt;/h3&gt;
&lt;p&gt;MSK Connector나 Kafka와 같은 메시지 스트리밍 시스템에서 장애가 발생하거나 데이터 불일치 문제가 생길 때, 데이터 일관성과 연속성을 확보하기 위한 중요한 기술적 전략입니다. 이를 활용하면 시스템의 장애나 오류 발생 시 특정 시점으로 되돌아가 메시지를 재처리하여 데이터 정합성을 유지할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;효율적인 장애 복구와 데이터 불일치 해결&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;시스템 장애나 오류 발생 시, 특정 Offset부터 데이터를 재처리함으로써 전체 데이터를 처음부터 처리하지 않아도 되어 복구 시간이 단축됩니다. 이를 통해 운영 비용 절감과 빠른 복구가 가능하며, 데이터 불일치 문제도 해결할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;연속적 데이터 처리와 안정성 보장&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offset을 사용해 특정 시점부터 데이터를 재처리하면, 시스템이 데이터를 놓치지 않고 연속적으로 처리할 수 있어 데이터 흐름의 일관성을 유지할 수 있습니다. 이는 특히 데이터 연속성이 중요한 서비스에서 안정적인 운영을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;신규-connector-생성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A0%EA%B7%9C-connector-%EC%83%9D%EC%84%B1&quot; aria-label=&quot;신규 connector 생성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;신규 Connector 생성&lt;/h3&gt;
&lt;p&gt;현재 사용중인 Offset을 확인합니다. 아래는 akhq를 사용한 토픽 확인 방법입니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/6164a1348b2b1080deb0402946e2388d/438a9/topics.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 20.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAEABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHJkLBP/8QAFhAAAwAAAAAAAAAAAAAAAAAAAAEQ/9oACAEBAAEFAoz/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAY/An//xAAYEAEBAQEBAAAAAAAAAAAAAAABADERIf/aAAgBAQABPyHQW85kAcv/2gAMAwEAAgADAAAAEAAf/8QAFREBAQAAAAAAAAAAAAAAAAAAAAH/2gAIAQMBAT8QR//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABgQAQEBAQEAAAAAAAAAAAAAAAERACEx/9oACAEBAAE/ELACpVcHSMeAdB83/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6164a1348b2b1080deb0402946e2388d/263a4/topics.webp 480w,
/static/6164a1348b2b1080deb0402946e2388d/a6361/topics.webp 960w,
/static/6164a1348b2b1080deb0402946e2388d/0b34d/topics.webp 1920w,
/static/6164a1348b2b1080deb0402946e2388d/da28f/topics.webp 2880w,
/static/6164a1348b2b1080deb0402946e2388d/c69ba/topics.webp 3406w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6164a1348b2b1080deb0402946e2388d/a3e66/topics.jpg 480w,
/static/6164a1348b2b1080deb0402946e2388d/fb816/topics.jpg 960w,
/static/6164a1348b2b1080deb0402946e2388d/aaf92/topics.jpg 1920w,
/static/6164a1348b2b1080deb0402946e2388d/1d134/topics.jpg 2880w,
/static/6164a1348b2b1080deb0402946e2388d/438a9/topics.jpg 3406w&quot; sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6164a1348b2b1080deb0402946e2388d/aaf92/topics.jpg&quot; alt=&quot;topics&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;운영중인 토픽을 확인하고, 신규 Connector를 해당 Offset 정보로 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;properties&quot;&gt;&lt;pre class=&quot;language-properties&quot;&gt;&lt;code class=&quot;language-properties&quot;&gt;&lt;span class=&quot;token key attr-name&quot;&gt;value.converter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;org.apache.kafka.connect.json.JsonConverter&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;key.converter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;org.apache.kafka.connect.json.JsonConverter&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Connector Offset 설정&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;offset.storage.topic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;__amazon_msk_connect_offsets_test-connector&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 30MB 최대 요청 크기&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;producer.max.request.size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;31457280&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#클러스터에서 설정 정보를 저장하는 토픽의 복제 계수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;config.storage.replication.factor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3   &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#상태 정보를 저장하는 토픽의 파티션 수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;status.storage.partitions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3           &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#상태 정보를 저장하는 토픽의 복제 계수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;status.storage.replication.factor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3   &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#Offset 정보를 저장하는 토픽의 파티션 수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;offset.storage.partitions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3           &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#Offset 정보를 저장하는 토픽의 복제 계수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;offset.storage.replication.factor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이와 같이 설정하면 Connector의 장애 발생 후에도 정상적으로 재가동될 수 있으며, 이전 오프셋 정보를 바탕으로 데이터의 연속성을 보장할 수 있습니다. 이러한 절차는 시스템의 안정성을 높이는 데 중요한 역할을 하며, 데이터 처리의 신뢰성을 강화합니다.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;failover-발생-시나리오&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#failover-%EB%B0%9C%EC%83%9D-%EC%8B%9C%EB%82%98%EB%A6%AC%EC%98%A4&quot; aria-label=&quot;failover 발생 시나리오 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Failover 발생 시나리오&lt;/h2&gt;
&lt;h3 id=&quot;failover의-정의-및-필요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#failover%EC%9D%98-%EC%A0%95%EC%9D%98-%EB%B0%8F-%ED%95%84%EC%9A%94%EC%84%B1&quot; aria-label=&quot;failover의 정의 및 필요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Failover의 정의 및 필요성&lt;/h3&gt;
&lt;p&gt;Failover는 시스템의 주요 구성 요소가 장애를 일으킬 경우, 이를 자동으로 대체하여 서비스 연속성을 유지하는 기술입니다. 이 프로세스는 고가용성을 목표로 하며, 서비스 중단 시간을 최소화하고 시스템 안정성을 높이는 데 필수적입니다. 장애가 발생해도 사용자가 서비스 중단을 느끼지 못하도록 서비스가 지속적으로 운영되어야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서비스 중단 최소화: 장애가 발생해도 빠르게 전환하여 서비스가 지속되도록 보장합니다.&lt;/li&gt;
&lt;li&gt;데이터 손실 방지: 주요 데이터가 손실되지 않도록 보호해 데이터 무결성을 유지합니다.&lt;/li&gt;
&lt;li&gt;고객 신뢰성 유지: 지속적인 가용성은 사용자 신뢰를 강화하고, 시스템의 신뢰도를 높입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;failover-발생-시-조치-사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#failover-%EB%B0%9C%EC%83%9D-%EC%8B%9C-%EC%A1%B0%EC%B9%98-%EC%82%AC%ED%95%AD&quot; aria-label=&quot;failover 발생 시 조치 사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Failover 발생 시 조치 사항&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 700px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/34677707e0605121b1cde5533fdae589/40619/connector_10.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 110.20833333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAWABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAe+Q0gAA/8QAFBABAAAAAAAAAAAAAAAAAAAAMP/aAAgBAQABBQIf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAMP/aAAgBAQAGPwIf/8QAFhABAQEAAAAAAAAAAAAAAAAAASAx/9oACAEBAAE/ISDJ/9oADAMBAAIAAwAAABBQBzz/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAbEAEAAgIDAAAAAAAAAAAAAAABABEQITFB0f/aAAgBAQABPxDk+5TTAmkt7hbdwKlT/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/34677707e0605121b1cde5533fdae589/263a4/connector_10.webp 480w,
/static/34677707e0605121b1cde5533fdae589/84c6d/connector_10.webp 700w&quot; sizes=&quot;(max-width: 700px) 100vw, 700px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/34677707e0605121b1cde5533fdae589/a3e66/connector_10.jpg 480w,
/static/34677707e0605121b1cde5533fdae589/40619/connector_10.jpg 700w&quot; sizes=&quot;(max-width: 700px) 100vw, 700px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/34677707e0605121b1cde5533fdae589/40619/connector_10.jpg&quot; alt=&quot;connector 10&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 724px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/72199815796b56dd50e68f9b67e5aa12/f8db4/failover_event.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABPElEQVR42nWS0a6CQAxE9/8/jwchAVGIoCKgokZAtDenyRhfLsmk3Xa37UwJeZ5b27Z2OBy+iOPYmqbxOPZ0On3tfyB/uVwsbLdbq+vadrudlWVpfd97oc1mY+RAlmVWFIXnFWeQXx/7eDwsvN9vA5/Px/iwdOMSnWlwPB7dns9n67rOfeWIyS7LYoHHgCB0h2H4Tnm9Xr0AVIhxxgfcUwMVnefZwv1+N/B8Ph0ESUIxTVO3+/3exnF06N6vrzNMA/oREG1ARy0EC2WxYLqqqpwNMUkl6UIURZYkiYMufDSR0Ov12sG0LAe7Wq3cZ3LukWdhDBaYAB2grU5MiF7oRPx2u3kMSEPybBWf95yZNtBF4jKhKIs20BK0IHy2rJzuvl4vCxSZpskhDdGGfwwq6AUdgA9ElcIU0Xs0/AMHb/LGTj6xaQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/72199815796b56dd50e68f9b67e5aa12/263a4/failover_event.webp 480w,
/static/72199815796b56dd50e68f9b67e5aa12/a7369/failover_event.webp 724w&quot; sizes=&quot;(max-width: 724px) 100vw, 724px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/72199815796b56dd50e68f9b67e5aa12/9aebd/failover_event.png 480w,
/static/72199815796b56dd50e68f9b67e5aa12/f8db4/failover_event.png 724w&quot; sizes=&quot;(max-width: 724px) 100vw, 724px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/72199815796b56dd50e68f9b67e5aa12/f8db4/failover_event.png&quot; alt=&quot;failover event&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;이러한 절차를 통해 데이터 정합성을 유지하고, 각 단계의 검토 및 처리를 통해 데이터 신뢰성을 강화하고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;failover-상황에서-발생한-데이터-오류-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#failover-%EC%83%81%ED%99%A9%EC%97%90%EC%84%9C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%98%A4%EB%A5%98-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;failover 상황에서 발생한 데이터 오류 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;Failover 상황에서 발생한 데이터 오류 처리&lt;/h2&gt;
&lt;p&gt;메시지를 소비할 때 순차적으로 처리하는 경우 메시지의 순서 보장이 되지 않아 문제가 발생할 수 있습니다. MSK는 기본적으로 메시지 순서를 보장하지 않기 때문에 의도치 않은 순서로 메시지가 처리될 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;잘못된-데이터-처리-및-복구-절차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%98%EB%AA%BB%EB%90%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%B2%98%EB%A6%AC-%EB%B0%8F-%EB%B3%B5%EA%B5%AC-%EC%A0%88%EC%B0%A8&quot; aria-label=&quot;잘못된 데이터 처리 및 복구 절차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;잘못된 데이터 처리 및 복구 절차&lt;/h3&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7843d3f7b2f35644ddf7df1cc05822a8/dface/failover-flow.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 16.458333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAIAAAAcOLh5AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAaklEQVR42k2NWQrFIBAEvf8N9QYJGkHc1xQTeLz6aKa1Z1rtvWutOWd0Cb331ho658SOMZhR7Bf+4EXxHUKw1nrvuxBj1Fo750hzJKVkjLmuewiPQJ4+xbFfA/M5h/1SCuX/Fshg0SmstV4/Za5F8xFbyAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7843d3f7b2f35644ddf7df1cc05822a8/263a4/failover-flow.webp 480w,
/static/7843d3f7b2f35644ddf7df1cc05822a8/411c4/failover-flow.webp 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7843d3f7b2f35644ddf7df1cc05822a8/9aebd/failover-flow.png 480w,
/static/7843d3f7b2f35644ddf7df1cc05822a8/dface/failover-flow.png 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7843d3f7b2f35644ddf7df1cc05822a8/dface/failover-flow.png&quot; alt=&quot;failover flow&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h4 id=&quot;a-custom-plugin&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#a-custom-plugin&quot; aria-label=&quot;a custom plugin permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;a. Custom plugin&lt;/h4&gt;
&lt;p&gt;Amazon MSK 신규 Connector를 생성하게 되면, 사용할 plugin을 설정합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 900px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/d1e30bb6120548ab9f7c486a516c7fe6/7293b/connector_1.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.083333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAMABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAECAwX/xAAVAQEBAAAAAAAAAAAAAAAAAAABAP/aAAwDAQACEAMQAAAB25IhlIP/xAAYEAACAwAAAAAAAAAAAAAAAAABAhEgQf/aAAgBAQABBQLKFoP/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFX/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAGRAAAgMBAAAAAAAAAAAAAAAAAAEQESEx/9oACAEBAAE/IQuGS6g//9oADAMBAAIAAwAAABCn3//EABURAQEAAAAAAAAAAAAAAAAAAAAB/9oACAEDAQE/EEj/xAAVEQEBAAAAAAAAAAAAAAAAAAABAP/aAAgBAgEBPxCSX//EABsQAAIDAAMAAAAAAAAAAAAAAAABESExYXGh/9oACAEBAAE/EGloSlvw7DwkZEiOT//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d1e30bb6120548ab9f7c486a516c7fe6/263a4/connector_1.webp 480w,
/static/d1e30bb6120548ab9f7c486a516c7fe6/d0742/connector_1.webp 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d1e30bb6120548ab9f7c486a516c7fe6/a3e66/connector_1.jpg 480w,
/static/d1e30bb6120548ab9f7c486a516c7fe6/7293b/connector_1.jpg 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d1e30bb6120548ab9f7c486a516c7fe6/7293b/connector_1.jpg&quot; alt=&quot;connector 1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;h4 id=&quot;b-connector-properties&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#b-connector-properties&quot; aria-label=&quot;b connector properties permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;b. Connector properties&lt;/h4&gt;
&lt;p&gt;Connector를 생성할 때, Failover 대상 데이터를 처리하기 위해 &lt;code class=&quot;language-text&quot;&gt;snapshot.select.statement.overrides&lt;/code&gt; 옵션을 설정합니다. 이 옵션을 통해 특정 테이블의 스냅샷 쿼리를 커스터마이징할 수 있어, Failover 시 필요한 데이터만 선택적으로 가져오는 데 유용합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 900px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/7c2aa13adf1cc9716aded16c30055fe6/7293b/connector_5.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3VBB/8QAFhAAAwAAAAAAAAAAAAAAAAAAABEg/9oACAEBAAEFAhT/AP/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABgQAQEAAwAAAAAAAAAAAAAAAAEAEFFh/9oACAEBAAE/IdR1hhm//9oADAMBAAIAAwAAABDAz//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABoQAAMBAAMAAAAAAAAAAAAAAAABEUExUcH/2gAIAQEAAT8QfAZPA1NpkZ2M6f/Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7c2aa13adf1cc9716aded16c30055fe6/263a4/connector_5.webp 480w,
/static/7c2aa13adf1cc9716aded16c30055fe6/d0742/connector_5.webp 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7c2aa13adf1cc9716aded16c30055fe6/a3e66/connector_5.jpg 480w,
/static/7c2aa13adf1cc9716aded16c30055fe6/7293b/connector_5.jpg 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7c2aa13adf1cc9716aded16c30055fe6/7293b/connector_5.jpg&quot; alt=&quot;connector 5&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;properties&quot;&gt;&lt;pre class=&quot;language-properties&quot;&gt;&lt;code class=&quot;language-properties&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;#테이블의 스냅샷(초기 데이터 로딩) 시에 사용할 커스텀 SQL 쿼리를 정의&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;snapshot.select.statement.overrides.customers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;SELECT * FROM customers where change_dt &gt;= &apos;2024-10-10&apos;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#테이블의 스냅샷을 할 때 커스텀 쿼리를 사용하도록 지정&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;snapshot.select.statement.overrides&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;customers&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#테이블만 변경사항을 추적&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;table.include.list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;customers&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#Debezium이 테이블에서 감지한 이벤트를 기본 토픽 대신 failover-topic이라는 새로운 토픽으로 재배치합니다.&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;transforms.Reroute.topic.replacement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;failover-topic&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;c-worker-configuration&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#c-worker-configuration&quot; aria-label=&quot;c worker configuration permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;c. Worker configuration&lt;/h4&gt;
&lt;p&gt;기본 설정으로 아래와 같이 생성합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 540px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/e699cf2fef9fc0519520f3b6b1a55cf6/ceaae/connector_6.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 148.74999999999997%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAeABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAe5WiKMazSg//8QAFBABAAAAAAAAAAAAAAAAAAAAMP/aAAgBAQABBQJP/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAMP/aAAgBAQAGPwJP/8QAGRAAAgMBAAAAAAAAAAAAAAAAAREAEDFh/9oACAEBAAE/IRpsbb7Y2NQFz//aAAwDAQACAAMAAAAQw888/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPxAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPxAf/8QAHRABAAIBBQEAAAAAAAAAAAAAAQARITFBUXGBkf/aAAgBAQABPxAmh7kjd4Cu2F8H2A3F+zTcg3pBLVa4jSVdkxywtlXqJ2QBdT//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e699cf2fef9fc0519520f3b6b1a55cf6/263a4/connector_6.webp 480w,
/static/e699cf2fef9fc0519520f3b6b1a55cf6/caa95/connector_6.webp 540w&quot; sizes=&quot;(max-width: 540px) 100vw, 540px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e699cf2fef9fc0519520f3b6b1a55cf6/a3e66/connector_6.jpg 480w,
/static/e699cf2fef9fc0519520f3b6b1a55cf6/ceaae/connector_6.jpg 540w&quot; sizes=&quot;(max-width: 540px) 100vw, 540px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e699cf2fef9fc0519520f3b6b1a55cf6/ceaae/connector_6.jpg&quot; alt=&quot;connector 6&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;properties&quot;&gt;&lt;pre class=&quot;language-properties&quot;&gt;&lt;code class=&quot;language-properties&quot;&gt;&lt;span class=&quot;token key attr-name&quot;&gt;key.converter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;org.apache.kafka.connect.json.JsonConverter&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;value.converter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;org.apache.kafka.connect.json.JsonConverter&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;key.converter.schemas.enable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;value.converter.schemas.enable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 30MB 최대 요청 크기&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;producer.max.request.size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;31457280&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#클러스터에서 설정 정보를 저장하는 토픽의 복제 계수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;config.storage.replication.factor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3   &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#상태 정보를 저장하는 토픽의 파티션 수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;status.storage.partitions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3           &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#상태 정보를 저장하는 토픽의 복제 계수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;status.storage.replication.factor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3   &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#Offset 정보를 저장하는 토픽의 파티션 수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;offset.storage.partitions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3           &lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#Offset 정보를 저장하는 토픽의 복제 계수&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;offset.storage.replication.factor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;d-review-and-create&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#d-review-and-create&quot; aria-label=&quot;d review and create permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;d. Review and create&lt;/h3&gt;
&lt;p&gt;해당 테이블의 데이터를 CDC로 처리하는 Connector를 생성합니다.
이때 스냅샷 상태인 r 상태로 데이터가 구성되며, 지정된 토픽으로 전송됩니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 900px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/102657952f9e95ffe268ba0056d5cfe1/7293b/connector_9.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 37.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAdywQD//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAY/An//xAAXEAEAAwAAAAAAAAAAAAAAAAARAAEQ/9oACAEBAAE/IRhWf//aAAwDAQACAAMAAAAQA8//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAaEAEAAQUAAAAAAAAAAAAAAAABABFBUWGh/9oACAEBAAE/EKQW5NkQzP/Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/102657952f9e95ffe268ba0056d5cfe1/263a4/connector_9.webp 480w,
/static/102657952f9e95ffe268ba0056d5cfe1/d0742/connector_9.webp 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/102657952f9e95ffe268ba0056d5cfe1/a3e66/connector_9.jpg 480w,
/static/102657952f9e95ffe268ba0056d5cfe1/7293b/connector_9.jpg 900w&quot; sizes=&quot;(max-width: 900px) 100vw, 900px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/102657952f9e95ffe268ba0056d5cfe1/7293b/connector_9.jpg&quot; alt=&quot;connector 9&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 400px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/457dc87fb22c4c508efbf4b64a129cc9/a6407/wee-woo.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAUABQDASIAAhEBAxEB/8QAGQABAAMBAQAAAAAAAAAAAAAAAAECAwQF/8QAFwEBAAMAAAAAAAAAAAAAAAAAAgEDBP/aAAwDAQACEAMQAAAB67xiZloD1uIeSKtH/8QAHRAAAgIBBQAAAAAAAAAAAAAAAQIAERIDEyMxMv/aAAgBAQABBQLJZlUZaOp0oDtuQDjZQof1/8QAGBEAAgMAAAAAAAAAAAAAAAAAARARMUH/2gAIAQMBAT8BJm1q/8QAGBEAAwEBAAAAAAAAAAAAAAAAAAERQlH/2gAIAQIBAT8BS4RmSn//xAAeEAACAQMFAAAAAAAAAAAAAAAAARECITESUWFx4f/aAAgBAQAGPwLDZLjSeErA3LgtTYS5N+xn/8QAHRAAAgMAAwEBAAAAAAAAAAAAAREAITFBUWFxof/aAAgBAQABPyEBD2LuB6AZexisN8ozkKWUbEThDfY4oZ4T7K0dF+k3Y+p//9oADAMBAAIAAwAAABBzMIP/xAAcEQADAAEFAAAAAAAAAAAAAAAAAREhMVFxkfD/2gAIAQMBAT8QqynuSb3tjSsWh//EABoRAQEAAgMAAAAAAAAAAAAAAAEAESFRYaH/2gAIAQIBAT8QEFF2HkOllzf/xAAdEAEBAAIDAQEBAAAAAAAAAAABEQAhMUFRYXGx/9oACAEBAAE/EIPoTzT1xrr3I20gDt8D+uBAKQ857XBSVOGEtvSZCrFKvPdU0TRPmMg0RRUnvj8woGWFBXn0ckhY6tB+HBiUjsqCbz//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/457dc87fb22c4c508efbf4b64a129cc9/2276a/wee-woo.webp 400w&quot; sizes=&quot;(max-width: 400px) 100vw, 400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/457dc87fb22c4c508efbf4b64a129cc9/a6407/wee-woo.jpg 400w&quot; sizes=&quot;(max-width: 400px) 100vw, 400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/457dc87fb22c4c508efbf4b64a129cc9/a6407/wee-woo.jpg&quot; alt=&quot;wee woo&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;figcaption&gt;[ChatGPT로 생성된 이미지입니다.]&lt;/figcaption&gt;
&lt;/div&gt;
&lt;h3 id=&quot;주의점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9D%98%EC%A0%90&quot; aria-label=&quot;주의점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;svg aria-hidden=&quot;true&quot; focusable=&quot;false&quot; height=&quot;16&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot;&gt;&lt;path fill-rule=&quot;evenodd&quot; d=&quot;M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/a&gt;주의점&lt;/h3&gt;
&lt;p&gt;Connector 생성 시 주의해야 할 두 가지 옵션이 있습니다. &lt;code class=&quot;language-text&quot;&gt;snapshot.locking.mode&lt;/code&gt; = none, &lt;code class=&quot;language-text&quot;&gt;snapshot.mode&lt;/code&gt; = schema_only 로 설정하고 있습니다. 이 옵션에 따라 수행되는 기능이 달라지므로 각 옵션의 의미를 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 726px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/70deb9c03f999a8d0dda83f9b10e1d77/c04c8/snapshot_option.png&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.25%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABdUlEQVR42o2TCY+CQAyF+f//zoBXPFAUQeXwwAMUar5ualZidpmkmel05s3ra8dZLpcymUwkiiJZr9cSx7HM53MZjUa63m63MpvNNMZ6v9+L53kyHA41HoahrFYrAYeYUxSFbnKYmUPsBUGgdjgc1D+fz2Jjt9vpI4AAttls9N79fhenaZr3QVtfLhdxXVdZcLksS8nzXJ7Pp8azLFPGZjx6vV6lqqpPQBu32+2DEYcBMcDFYiHj8Vgf7ff7MhgMpNfrSZIk3wHbe20frbA0Td+GD5FOgEiAxmjEIH0YkyqZMAOKNE5d12IGEGb+4/HQGSAuoRGDAgAKAKyI8yjntcom7ul00gu2x0UO4lMcYw5b3/dlOp1qpdGU1jsejz8p/zaGvWjpt+O0EyCwhATsWSvDLhpyica3lCkAPkbvwp71n0X51p82kILGpqFJmTUyoHcnhrxMFdHIfgoa8kuY+arE/20bm2lYGhidLGUKgm96Uzja5gWwseZ6r31uTAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/70deb9c03f999a8d0dda83f9b10e1d77/263a4/snapshot_option.webp 480w,
/static/70deb9c03f999a8d0dda83f9b10e1d77/b0d8b/snapshot_option.webp 726w&quot; sizes=&quot;(max-width: 726px) 100vw, 726px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/70deb9c03f999a8d0dda83f9b10e1d77/9aebd/snapshot_option.png 480w,
/static/70deb9c03f999a8d0dda83f9b10e1d77/c04c8/snapshot_option.png 726w&quot; sizes=&quot;(max-width: 726px) 100vw, 726px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/70deb9c03f999a8d0dda83f9b10e1d77/c04c8/snapshot_option.png&quot; alt=&quot;snapshot option&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;위 절차대로 Connector 를 생성하면, 스냅샷이 생성되면서 op=&quot;r&quot; 타입의 데이터가 발행됩니다. 이 부분을 제어하려고 합니다.&lt;/p&gt;
&lt;div style=&quot;text-align: center;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 349px; &quot;&gt;
      &lt;a class=&quot;gatsby-resp-image-link&quot; href=&quot;/static/ff1229fcb33ba233898c34cbce3bbf7f/3cb7d/connector_11.jpg&quot; style=&quot;display: block&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.89684813753581%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAANABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAd1aAD//xAAVEAEBAAAAAAAAAAAAAAAAAAARIP/aAAgBAQABBQIr/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAGBAAAwEBAAAAAAAAAAAAAAAAAAERIRD/2gAIAQEAAT8hgWqk5MEf/9oADAMBAAIAAwAAABDDz//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABwQAQACAgMBAAAAAAAAAAAAAAEAETFxECGRof/aAAgBAQABPxDJn1hCXyU4MKu7gort23P/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ff1229fcb33ba233898c34cbce3bbf7f/be10f/connector_11.webp 349w&quot; sizes=&quot;(max-width: 349px) 100vw, 349px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ff1229fcb33ba233898c34cbce3bbf7f/3cb7d/connector_11.jpg 349w&quot; sizes=&quot;(max-width: 349px) 100vw, 349px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ff1229fcb33ba233898c34cbce3bbf7f/3cb7d/connector_11.jpg&quot; alt=&quot;connector 11&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
  &lt;/a&gt;
    &lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://debezium.io/documentation/reference/2.5/connectors/mysql.html#mysql-update-events&quot;&gt;Debezium Documentation Connectors MySQL - update events&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; 
  &lt;span class=&quot;token property&quot;&gt;&quot;schema&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ... &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;payload&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;before&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token null keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;after&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1004&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;first_name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Anne Marie&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;last_name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Kretchmar&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;annek@noanswer.org&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;source&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;2.5.4.Final&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mysql-server-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;connector&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mysql&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mysql-server-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;t