<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>G마켓 기술블로그</title>
    <link>https://dev.gmarket.com/</link>
    <description>G마켓의 기술과 경험을 공유합니다.</description>
    <language>ko</language>
    <pubDate>Tue, 5 May 2026 10:43:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>G마켓 기술블로그</managingEditor>
    <image>
      <title>G마켓 기술블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/4067742/attach/339440313972411f88d1816a3a4ef145</url>
      <link>https://dev.gmarket.com</link>
    </image>
    <item>
      <title>iOS에서 이벤트 기반 URL 요청이 잘 전송되는지 확인하기 (feat. 광고 트래킹.. 제대로 가고 있나요?)</title>
      <link>https://dev.gmarket.com/119</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;안녕하세요 지마켓 Mobile Application Team 강수진입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오늘은 iOS에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;특정 이벤트에 대한 URL 요청이 정상적으로 이루어졌는지&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;확인하는 방법에 대해 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;들어가기 전에&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모든 서비스에서 광고는 중요합니다. 왜냐하면 수익과 직결되기 때문이죠  &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지마켓도 곳곳에 다양한 유형의 광고가 포함되어 있는데요! 일례로 사용자가 광고 상품을 클릭하면, 해당 이벤트가 광고 처리 시스템으로 전송되어 광고가 집계되고, 이에 따라 비용이 청구될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/me4Fg/btsLDvHNKTM/v3mAMMHBQrOSdhdkdTxcl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/me4Fg/btsLDvHNKTM/v3mAMMHBQrOSdhdkdTxcl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/me4Fg/btsLDvHNKTM/v3mAMMHBQrOSdhdkdTxcl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fme4Fg%2FbtsLDvHNKTM%2Fv3mAMMHBQrOSdhdkdTxcl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2474&quot; height=&quot;2160&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;요구 사항&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;광고 트래킹은 수익과 직결되기 때문에 문제가 생기면 최우선 순위로 대응해야 하는 이슈 중 하나입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 코드 수정을 하다가 기존의 트래킹 코드가 동작을 안 하는 상황이 발생한다면요..?? 심지어 이런 데이터 트래킹의 이슈는 일반적인 QA로 발견되기 어려운데요...? 벌써 아찔하죠..?  &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그렇다면 배포 전 테스트를 통해 '광고 트래킹 이벤트가 발생했을때, 해당 URL 요청을 제대로 보내고 있는지' 확인할 수 없을까요??&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번 프로젝트는 이러한 필요성으로부터 시작됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;어떻게 하면 좋을까?&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;앞서 말했듯 '광고 이벤트가 발생하면, 광고 처리 시스템 URL 로 트래킹을 보낸다' 이것이 광고 시스템 처리에 대한 기본 전제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 트리거 이벤트가 발생했을때 진짜로 광고 URL 요청을 보냈는지 확신할 수 있나요? 정말요?? 이 사실을 어떻게 검증할 수 있을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blrT3V/btsLFZGLn0z/pvm44rWQXhDcDHyRLZnlvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blrT3V/btsLFZGLn0z/pvm44rWQXhDcDHyRLZnlvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blrT3V/btsLFZGLn0z/pvm44rWQXhDcDHyRLZnlvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblrT3V%2FbtsLFZGLn0z%2Fpvm44rWQXhDcDHyRLZnlvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2332&quot; height=&quot;834&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기존에 트래킹 이벤트를 아래와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;sendTracking&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에서 처리한다고 할때&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;protocol AdTracker {
 &amp;nbsp;func sendTracking()
}
​
struct AdTrackingClient: AdTracker {
 &amp;nbsp;func sendTracking() {
 &amp;nbsp; &amp;nbsp;// URL Request 보냄
 &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;단순히 Fake 객체를 만들고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;sendTracking&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;isTrackingSent&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;플래그를 변경하는 방식으로 트래킹이 되었는지 검증하면 될까요?&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;struct FakeAdTrackingClient: AdTracker {
 &amp;nbsp;var isTrackingSent = false
 &amp;nbsp;func sendTracking() {
 &amp;nbsp; &amp;nbsp;isTrackingSent = true
 &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이와 같은 방식은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;sendTracking&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수가 호출되었는지 여부만 확인할 수 있을 뿐, 실제로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;URLRequest&lt;/span&gt;&lt;span&gt;를 통해 네트워크 요청이 이루어졌는지는 확인할 수 없습니다. 실제&lt;span&gt;&amp;nbsp;&lt;/span&gt;AdTrackingClientAdTrackingClient의 &lt;/span&gt;&lt;span&gt;sendTracking&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;print(&quot;hello world&quot;)만&lt;/span&gt;&lt;span&gt; 구현되어 있을지도 모르는 일이죠!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 저희는 실제 URL 요청을 캐치할 수 있는&lt;span&gt;&amp;nbsp;&lt;b&gt;proxy&lt;/b&gt;&amp;nbsp;를&lt;/span&gt; 만들어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;이벤트를 가로채기&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;로 했습니다. 그리고 여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;가로챈 URL 정보&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;는 저장소에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;별도로 저장해 둡니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C7EY8/btsLFyizsUP/GHIAxwEfTeJ66VuKeELHsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C7EY8/btsLFyizsUP/GHIAxwEfTeJ66VuKeELHsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C7EY8/btsLFyizsUP/GHIAxwEfTeJ66VuKeELHsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC7EY8%2FbtsLFyizsUP%2FGHIAxwEfTeJ66VuKeELHsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3632&quot; height=&quot;1272&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;광고 트래킹 이벤트를 트리거 시킬 때 URL 요청을 제대로 보낸다면, 해당 URL 정보는 proxy에 의해 저장소에 저장될 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 우리는 이벤트 트리거 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;URL 이 저장소에 있냐 / 아니냐&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;에 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;이벤트에 따른 URL 요청 여부를 판단&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l5SOu/btsLEhISpKT/1S97KYjKmwAtwV8Tvdz0a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l5SOu/btsLEhISpKT/1S97KYjKmwAtwV8Tvdz0a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l5SOu/btsLEhISpKT/1S97KYjKmwAtwV8Tvdz0a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl5SOu%2FbtsLEhISpKT%2F1S97KYjKmwAtwV8Tvdz0a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3034&quot; height=&quot;542&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;URL 요청 가로채기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 먼저 URL 요청을 가로채는 proxy를 만들 거다!라고 했을 때, iOS에서는 실제로 어떻게 구현을 할 수 있을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ptiAU/btsLEbvbVvJ/FuFKkVbTbK4BZW4fOjrwuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ptiAU/btsLEbvbVvJ/FuFKkVbTbK4BZW4fOjrwuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ptiAU/btsLEbvbVvJ/FuFKkVbTbK4BZW4fOjrwuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FptiAU%2FbtsLEbvbVvJ%2FFuFKkVbTbK4BZW4fOjrwuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3632&quot; height=&quot;1272&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;바로&lt;span&gt;&amp;nbsp;&lt;b&gt;URLProtocol&lt;/b&gt;&amp;nbsp;을&lt;/span&gt; 사용하면 됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;URLProtocol&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;은 네트워크 연결을 열고 요청을 작성하며 응답을 읽어오는 기본 작업을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;URLProtocol&lt;/span&gt;&lt;span&gt;은 URL 로딩 시스템에 대한 확장성을 제공하기 위해 서브클래싱할 수 있도록 설계되었는데요, HTTPS와 같이 일반적인 프로토콜을 위한 서브클래스는 기본으로 제공됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 우리가 원하는 건 일반적인 네트워크 요청이 아니죠! 따라서&lt;span&gt;&amp;nbsp;URLProtocol&amp;nbsp;를&lt;/span&gt; 상속받는 subclass를 정의하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;startLoading()&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 함수를 override 하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;요청을 가로챈&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;필요한 처리 및 임의의 응답을 반환&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;final class CustomURLProtocol: URLProtocol {
 &amp;nbsp; &amp;nbsp;// 요청을 시작할 때 호출되는 메서드
 &amp;nbsp; &amp;nbsp;override func startLoading() {
 &amp;nbsp;      // ✅ 이곳에서 요청을 가로채고, 저장소에 URL 저장 할 것
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// URLProtocolClient 프로토콜을 통해 시스템에 진행 상황을 전달 가능
 &amp;nbsp; &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;동작에 대한 좀 더 자세한 내용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/videos/play/wwdc2018/417/?time=494&quot;&gt;&lt;span&gt;[WWDC18] Testing Tips &amp;amp; Tricks 영상&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/documentation/foundation/urlprotocol&quot;&gt;&lt;span&gt;URLProtocol 문서,&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/documentation/foundation/url_loading_system&quot;&gt;&lt;span&gt;URL Loading System 문서&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;를 참고하세요.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3JyIU/btsLDLjqrWG/8hqaPUyf1FkNijI2x2hxL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3JyIU/btsLDLjqrWG/8hqaPUyf1FkNijI2x2hxL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3JyIU/btsLDLjqrWG/8hqaPUyf1FkNijI2x2hxL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3JyIU%2FbtsLDLjqrWG%2F8hqaPUyf1FkNijI2x2hxL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3840&quot; height=&quot;2160&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다시 본론으로 돌아와서, 우리가 원하는 동작을 달성하기 위해 아래 코드는 요청을 시작할 때 호출되는 메서드인&lt;span&gt;&amp;nbsp;startLoading()&amp;nbsp;을&lt;/span&gt; override 하여&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;저장소에 요청 URL을 저장하고&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;임의의 응답을 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt; // CustomURLProtocol.swift
​
 &amp;nbsp; &amp;nbsp;// 요청을 시작할 때 호출되는 메서드
 &amp;nbsp; &amp;nbsp;override func startLoading() {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;guard let url = request.url else { return }
 &amp;nbsp; &amp;nbsp; &amp;nbsp; 
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;do {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// URL을 텍스트로 변환하여 저장소에 기록
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;try AdTrackingURLStore.writeDataToSharedFolder(text: url.absoluteString)
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;} catch {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// URL 기록에 실패했을 경우 오류 메시지 출력
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;print(error.localizedDescription)
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;}
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 네트워크 요청을 모방하여 임의의 응답을 생성
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 응답 객체를 클라이언트에게 전달
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed)
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 요청 완료를 클라이언트에게 알림
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;self.client?.urlProtocolDidFinishLoading(self)
 &amp;nbsp; &amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 외에도 URLProtocol을 상속받는 subclass에서는&amp;nbsp;&lt;/span&gt;&lt;span&gt;startLoading&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;외에도 필수로 구현해야 하는 몇 가지 함수가 있기 때문에 추가로 구현해 주었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;final class CustomURLProtocol: URLProtocol {
 &amp;nbsp; &amp;nbsp;override class func canInit(with request: URLRequest) -&amp;gt; Bool {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;return true
 &amp;nbsp; &amp;nbsp;}
 &amp;nbsp;
 &amp;nbsp; &amp;nbsp;override func startLoading() {
 &amp;nbsp; &amp;nbsp; &amp;nbsp;....
 &amp;nbsp; &amp;nbsp;}
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;override class func canonicalRequest(for request: URLRequest) -&amp;gt; URLRequest {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;return request
 &amp;nbsp; &amp;nbsp;}
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;override func stopLoading() {
 &amp;nbsp; &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 정의한&lt;span&gt;&amp;nbsp;CustomURLProtocol&amp;nbsp;는&lt;/span&gt;&amp;nbsp;URLSessionConfiguration의&amp;nbsp;protocolClasses에 설정하면 됩니다. 물론 광고 트래킹을 보내는 용도의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;URLSession&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 따로 정의되어있기 때문에 아래와 같은 코드 세팅이 가능했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;let sessionConfiguration = URLSessionConfiguration.ephemeral
if isTest {
 &amp;nbsp;// 테스트 환경일때 CustomURLProtocol 에서 요청을 가로채서 처리
 &amp;nbsp;sessionConfiguration.protocolClasses = [CustomURLProtocol.self]
}
// 광고 트래킹 URLSession
let adTrackingSession = URLSession(configuration: sessionConfiguration)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 아래와 같이 프록시 역할을 위해 정의된&lt;span&gt;&amp;nbsp;CustomURLProtocol&amp;nbsp;에서는&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;URL 요청을 가로채고, 저장소에 URL을 저장&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3632&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WuVZn/btsLE2dlQqR/8ZkK12kDVHjbdpPReqd7D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WuVZn/btsLE2dlQqR/8ZkK12kDVHjbdpPReqd7D1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WuVZn/btsLE2dlQqR/8ZkK12kDVHjbdpPReqd7D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWuVZn%2FbtsLE2dlQqR%2F8ZkK12kDVHjbdpPReqd7D1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3632&quot; height=&quot;1272&quot; data-origin-width=&quot;3632&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;저장된 URL 불러오기 (feat. 어떤 저장소를 사용할까?)&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 우리는 저장소에 광고 URL 이 제대로 저장되어 있는지 확인함으로써, 광고 이벤트에 따른 URL 요청이 보내졌는지 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lnP8o/btsLErYZqqU/bN3KYkH19qeqiMB8HUStiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lnP8o/btsLErYZqqU/bN3KYkH19qeqiMB8HUStiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lnP8o/btsLErYZqqU/bN3KYkH19qeqiMB8HUStiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlnP8o%2FbtsLErYZqqU%2FbN3KYkH19qeqiMB8HUStiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1504&quot; height=&quot;532&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉,&lt;span&gt;&amp;nbsp;CustomURLProtocol&amp;nbsp;에서&lt;/span&gt; 요청을 가로채고 URL을 저장해 두었다면&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;// CustomURLProtocol.swift
try AdTrackingURLStore.writeDataToSharedFolder(text: url.absoluteString) //저장소에 URL 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;테스트 코드에서는 광고 트래킹 이벤트를 발생시킨 후, 아래와 같이 저장소에 원하는 URL 존재하는지 확인함으로써 URL 요청 여부를 판단할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;// AdTrackingUITest.swift
let textCount = try AdTrackingURLStore.numberOfTextInSharedFolder(text: expectedURL) // 저장소에 특정 URL 존재 여부 확인
XCTAssertEqual(textCount, 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 여기서 근본적인 의문이 드는데요,, 과연 우리는&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;'어떤 저장소'를&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt; 사용하고 있느냐 대한 문제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3632&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Givy/btsLFAHr8tW/lUv9MsmrkGKjeTG6rvQh31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Givy/btsLFAHr8tW/lUv9MsmrkGKjeTG6rvQh31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Givy/btsLFAHr8tW/lUv9MsmrkGKjeTG6rvQh31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Givy%2FbtsLFAHr8tW%2FlUv9MsmrkGKjeTG6rvQh31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3632&quot; height=&quot;1272&quot; data-origin-width=&quot;3632&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;iOS에서 데이터를 저장하는 방법은 여러 가지가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음에는 단순히 아래와 같은 방식으로 하나의 객체에 url 저장소를 만든 후 (App target에서) 쓰기 / (UI Test target 에서) 읽기를 시도했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;// App &amp;amp; UI Test Target 모두 포함
class AdTrackingURLStore {
 &amp;nbsp;static let shared: AdTrackingURLStore()
 &amp;nbsp;var storedUrls: [String] = []
}
​
// App Target
class ViewController: UIViewController {
 &amp;nbsp; func clickAdButton() {
 &amp;nbsp; &amp;nbsp; AdTrackingURLStore.shared.storedUrls.append(&quot;adTrackingUrl&quot;)
 &amp;nbsp;}
}
​
// UI test target
class AdTrackingTest: XCTestCase {
 &amp;nbsp;func test_장바구니_클릭시_광고_클릭_url_저장() {
 &amp;nbsp; &amp;nbsp;// 장바구니 클릭 tap
 &amp;nbsp; &amp;nbsp;let isUrlStored = AdTrackingURLStore.shared.storedUrls.contains(&quot;adTrackingUrl&quot;)
 &amp;nbsp; &amp;nbsp;XCTAssertTrue(isUrlStored)
 &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 UI Test Target에서 제대로 값을 불러오지 않는 문제가 발생하는데요! 그 이유는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/09-ui_testing.html&quot;&gt;&lt;span&gt;User Interface Testing&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;에서 엿볼 수 있듯, UI TestCode는 별도의 프로세스로 실행되기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;class AdTrackingTest: XCTestCase {
 &amp;nbsp;func test_장바구니_클릭시_광고_클릭_url_저장() {
 &amp;nbsp; &amp;nbsp;// 장바구니 클릭 tap
 &amp;nbsp; &amp;nbsp;let isUrlStored = AdTrackingURLStore.shared.storedUrls.contains(&quot;adTrackingUrl&quot;) //   false
 &amp;nbsp; &amp;nbsp;XCTAssertTrue(isUrlStored)
 &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서&lt;span&gt;&amp;nbsp;&lt;b&gt;App&lt;/b&gt;&amp;nbsp;과&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;UI Test&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Target 양쪽에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;공통적으로 사용&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;데이터 저장소&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;가 필요했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhnQHO/btsLDVMO9H0/rmtZHmRvKLCc5vgiaOGsJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhnQHO/btsLDVMO9H0/rmtZHmRvKLCc5vgiaOGsJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhnQHO/btsLDVMO9H0/rmtZHmRvKLCc5vgiaOGsJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhnQHO%2FbtsLDVMO9H0%2FrmtZHmRvKLCc5vgiaOGsJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;454&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;그리고 이를 File System을 통해 해결하기로 했는데요,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;SIMULATOR_SHARED_RESOURCES_DIRECTORY라는&lt;/span&gt;&lt;span&gt; 환경 변수를 통해, 시뮬레이터의 공유 리소스 디렉터리 Path 얻고 접근할 수 있습니다. 이런 식으로 말이죠!&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;let simulatorSharedDir = ProcessInfo.processInfo.environment[&quot;SIMULATOR_SHARED_RESOURCES_DIRECTORY&quot;]
​
// output
/Users/계정이름/Library/Developer/XCTestDevices/시뮬레이터-UDID/data&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 가져온 공유 폴더 경로에 파일 경로까지 (ex.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;storedUrls.txt&lt;/span&gt;&lt;span&gt;) 추가해 주면!? 우리가 read / write 할 파일의 경로가 완성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt; &amp;nbsp; &amp;nbsp;private static func fileURL() throws -&amp;gt; URL {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 공유 폴더의 URL을 가져옴
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;let folderURL = try sharedFolderURL()
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 파일 이름을 추가하여 최종 파일 경로 생성
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;let fileName = &quot;storedUrls.txt&quot;
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;return folderURL.appendingPathComponent(fileName)
 &amp;nbsp; &amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 아래와 같이 생성되어 있는 걸 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2644&quot; data-origin-height=&quot;1164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bffG0v/btsLEI7eB4S/5kTVIHRVNoktyJhip5OpR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bffG0v/btsLEI7eB4S/5kTVIHRVNoktyJhip5OpR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bffG0v/btsLEI7eB4S/5kTVIHRVNoktyJhip5OpR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbffG0v%2FbtsLEI7eB4S%2F5kTVIHRVNoktyJhip5OpR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2644&quot; height=&quot;1164&quot; data-origin-width=&quot;2644&quot; data-origin-height=&quot;1164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;/Users/계정이름/Library/Developer/XCTestDevices/시뮬레이터-UDID/data&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s7pXf/btsLBxxPs5o/0wYk5srEHH9fX0ZGiqti6K/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/s7pXf/btsLBxxPs5o/0wYk5srEHH9fX0ZGiqti6K/img.png&quot;&gt;&lt;/span&gt;/Users/계정이름/Library/Developer/XCTestDevices/시뮬레이터-UDID/data&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;물론&lt;span&gt;&amp;nbsp;공유폴더/storedUrls.txt&amp;nbsp;처럼&lt;/span&gt; 폴더 하위에 바로 txt 파일을 생성하지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;공유폴더/Library/Caches/&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;와 같이 path를 더 붙임으로써 캐시 디렉토리 하위로 파일 경로를 지정할 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;어찌 됐건 저장소를 시뮬레이터의 공유 폴더 하위 파일로 설정했기 때문에, App / UI Test 어느 타겟에서나 접근 가능해졌죠?!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3756&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d06Cnz/btsLFKpxVoH/Pw1lMYk879jxG2iDy2XTb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d06Cnz/btsLFKpxVoH/Pw1lMYk879jxG2iDy2XTb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d06Cnz/btsLFKpxVoH/Pw1lMYk879jxG2iDy2XTb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd06Cnz%2FbtsLFKpxVoH%2FPw1lMYk879jxG2iDy2XTb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3756&quot; height=&quot;1272&quot; data-origin-width=&quot;3756&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;해당 파일에 접근하여 읽고 쓸 수 있도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;AdTrackingURLStore&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에는 아래와 같은 함수들이 포함되어있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;참고로, 단순히 저장소 내 URL의 포함 여부를 확인하는 것이 아니라 URL의 개수를 반환하는 함수를 사용하는 이유는, 광고가 중복 적재되지 않아야 하는 상황에서 중복 적재되거나, 모듈 재노출 시 다시 적재되어야 하는 상황에서 적재되지 않는 등 에러 상황을 판단하기 위함입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;struct AdTrackingURLStore {
 &amp;nbsp; &amp;nbsp;private static func fileURL() throws -&amp;gt; URL {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; // 시뮬레이터 환경 변수에서 공유 리소스 디렉토리 경로를 가져오고, 공유 폴더 경로와 파일 이름 추가하여 최종 파일 경로 생성
 &amp;nbsp; &amp;nbsp;}
 &amp;nbsp;
 &amp;nbsp; &amp;nbsp;static func writeDataToSharedFolder(text: String) throws {
 &amp;nbsp; &amp;nbsp; &amp;nbsp;// 파일에 입력된 text 쓰기
 &amp;nbsp; &amp;nbsp;}
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;static func numberOfTextInSharedFolder(text: String) throws -&amp;gt; Int {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; // 파일에서 데이터 읽어와서 특정 텍스트가 있는지 확인
 &amp;nbsp; &amp;nbsp;}
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;static func clearFileInSharedFolder() throws {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; // 파일 내용 삭제
 &amp;nbsp; &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;광고 이벤트 트리거 하기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 광고 이벤트가 전송될 때, 해당 요청을 가로채어 저장소에 저장하고, 읽어낼 준비까지 모두 완료되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 이 모든 과정의 시작인, 광고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;이벤트의 전송&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;을 어떻게 해야 할까요? 개발자가 일일이 버튼 클릭, 스크롤 등의 트래킹 시나리오를 재현한 뒤, 저장소에 URL 이 제대로 기록되었나 확인해야 할까요?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3722&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2Du4a/btsLFWJ2yto/Sca1TsJFKZpkz3y7za3gbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2Du4a/btsLFWJ2yto/Sca1TsJFKZpkz3y7za3gbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2Du4a/btsLFWJ2yto/Sca1TsJFKZpkz3y7za3gbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2Du4a%2FbtsLFWJ2yto%2FSca1TsJFKZpkz3y7za3gbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3722&quot; height=&quot;1272&quot; data-origin-width=&quot;3722&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사람이 일일이 테스트하는 방법은 당연히 말도 안 됩니다  &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;잠깐 위에서도 언급했지만, 우리는 UITest를 통해 사용자의 광고 이벤트를 트리거하고, URL 요청을 잘 보냈는지 검증할 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 UnitTest가 아닌 UITest를 선택한 이유는, 실제 사용자 환경에서 광고 이벤트가 발생하는 흐름을 보다 정확하게 재현하고자 했기 때문입니다. 단순히&lt;span&gt;&amp;nbsp;clickButton&amp;nbsp;과&lt;/span&gt; 같은 함수 호출을 검증하는 것이 아니라, 앱과 사용자가 상호작용하는 과정에서 이벤트가 정상적으로 처리되고, 필요한 URL 요청이 제대로 전송되는지를 확인하려는 의도입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;UITest 작성을 시작하기 전에 알아야 할 가장 중요한 점은 바로, UITest에서는 UI 요소를 찾기 위해&lt;span&gt;&amp;nbsp;&lt;b&gt;accessibilityIdentifier&lt;/b&gt;&amp;nbsp;를&lt;/span&gt; 사용하기 때문에, 상호 작용하려는 요소에 반드시 해당 값이 설정되어 있어야 한다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지마켓 서비스도 접근성을 꾸준히 대응하고 있지만, 이를 위해 주로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;accessibilityLabel&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 설정되어 있고,&amp;nbsp;accessibilityIdentifier는 되어있지 않은 상황이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 큰 틀에서의 컨벤션은 아래와 같이 정하고 실제 테스트 코드 작성 시 어려움이 생긴다면 수정해 나가기로 결정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;자신의 identifier를 초기화 함수 등에서 자체적으로 설정할 수 있다면 타입 명을 따라간다 (&lt;/span&gt;&lt;span&gt;ex. MyItemCell&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;UIView&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등 기본 타입으로 정의되어 있다면 property 명을 따라간다 (&lt;/span&gt;&lt;span&gt;ex. defaultBackgroundView&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;ViewController&amp;nbsp;기준으로&amp;nbsp;enum을&amp;nbsp;나눠&amp;nbsp;정의하되 공통적인&amp;nbsp;요소가&amp;nbsp;있다면&amp;nbsp;Common에&amp;nbsp;선언한다&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 UITest에서는 설정한 identifier와 타입을 조합해 상위 요소부터 상호 작용하고자 하는 요소까지 찾아 들어가면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;let element = app.otherElements[viewControllerIdentifier].collectionViews.cells[cellIdentifier].buttons[cartButtonIdentifier]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;참고로 자주 element와 상호작용하기 위해 자주 사용한 함수로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;waitForExistence(timeout:)&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;tap()&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;swipeLeft()&lt;/span&gt;&lt;span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;typeText()&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등이 있는데요, 더 많은 내용은&lt;span&gt;&amp;nbsp;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/documentation/xctest/xcuielement&quot;&gt;XCUIElement&lt;/a&gt;&amp;nbsp;를&lt;/span&gt; 참고하세요.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 아래와 같이 자동으로 요소를 찾아서 클릭하고, 동작에 따라 url 이 잘 저장되었는지 검증하는 코드를 작성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;func test_MyCell에서_장바구니_버튼_클릭시_광고_url을_호출한다() throws {
 &amp;nbsp; &amp;nbsp;// given
 &amp;nbsp; &amp;nbsp;// ... 검색 결과 페이지까지 이동...
 &amp;nbsp; &amp;nbsp;let clickExpectedURL = &quot;광고 처리 시스템 url&quot;
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;// when
 &amp;nbsp; &amp;nbsp;let element = app.otherElements[viewControllerIdentifier].collectionViews.cells[cellIdentifier].buttons[cartButtonIdentifier]
 &amp;nbsp; &amp;nbsp;element.tap()
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;// then
 &amp;nbsp; &amp;nbsp;do {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;let textCount = try AdTrackingURLStore.numberOfTextInSharedFolder(text: clickExpectedURL)
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;XCTAssertTrue(textCount == 1, &quot;\(expectedURL) URL이 \(textCount)개 존재합니다.&quot;)
 &amp;nbsp; &amp;nbsp;} catch {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;XCTFail(&quot;URL을 찾을 수 없습니다.&quot;)
 &amp;nbsp; &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;광고 데이터 세팅하기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 거의 다 왔습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마지막으로 저희가 고민해야 할 사항은 테스트 시 어떤 데이터를 사용할 거냐,, 에 대한 부분입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3632&quot; data-origin-height=&quot;1272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cB1wYd/btsLE4CefDJ/6QqjloIYBy317lmQArZtW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cB1wYd/btsLE4CefDJ/6QqjloIYBy317lmQArZtW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cB1wYd/btsLE4CefDJ/6QqjloIYBy317lmQArZtW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcB1wYd%2FbtsLE4CefDJ%2F6QqjloIYBy317lmQArZtW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3632&quot; height=&quot;1272&quot; data-origin-width=&quot;3632&quot; data-origin-height=&quot;1272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &amp;zwj;♀️ : 귀찮은데 그냥 이대로 돌리면 안돼여?&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &amp;zwj;  : 흠,, 그렇다는 건 네트워크를 통해 데이터를 동적으로 받아온다는 얘기인데,, 그럼 이런 상황에선 어떻게 하실 건가요?&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;네트워크 상황이 좋지 않아서 데이터를 불러오는데 시간이 오래 걸리고, 결과적으로 테스트 실행이 느려지거나 실패하는 경우는요?&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;상황에 따라 테스트가 필요한 모듈이 노출되지 않을 수도 있는데요?&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;동일한 모듈이라 하더라도, 일반 상품인지 / 광고 상품인지에 따라, 광고 처리 시스템 URL의 포함 여부가 달라질 수 있잖아요? 이때 URL 기록 결과로 성공 여부를 판단하고 있는 현재 상황에서는, 모듈에서 트래킹 이벤트를 처리하지 않아 실패할 가능성이 있을 뿐만 아니라, API에서 값이 전달되지 않아 테스트가 실패할 위험도 존재합니다. 즉, 상황에 따라 동일 모듈에 대한 테스트 결과가 달라집니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;특정 모듈의 트래킹 이벤트 여부를 정확히 판단하기 위해, 단순히 테스트 성공 여부를 트래킹 이벤트 전송 여부로 판단하지 않고, 어떤 URL로 요청을 보냈는지의 값을 확인하고 있습니다. 그러나 데이터가 동적으로 들어오면 어떤 URL 값을 성공 기준으로 봐야 하는지 알 수 없습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &amp;zwj;♀️ : 아 알겠어여;;; Fake 객체 만들어서 쓰면 되잖아요 ㅜ 근데..........어떻게 해요? ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 Protocol과 의존성 주입의 중요성이 등장합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음과 같이 protocol에 수행해야 할 동작을 명시하고, 네트워크 요청을 통해 결과를 받아오는 구현체와 JSON 파일을 로딩하여 결과를 받아오는 구현체를 각각 구분하여 작성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;protocol SearchResultRepository {
 &amp;nbsp; &amp;nbsp;func fetchItemList(keyword: String) async -&amp;gt; [Item]
}
​
class SearchResultRepositoryImp: SearchResultRepository {
 &amp;nbsp; &amp;nbsp;func fetchItemList(keyword: String) async -&amp;gt; [Item] {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// 네트워크 요청
 &amp;nbsp; &amp;nbsp;}
}
​
class FakeSearchResultRepository: SearchResultRepository {
 &amp;nbsp; &amp;nbsp;func fetchItemList(keyword: String) async -&amp;gt; [Item] {
 &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;// Items.json 로딩해서 리턴
 &amp;nbsp; &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 protocol을 사용하면, 함수를 호출하는 쪽에서 코드 변경 없이, 실제 환경과 UITest 환경에 따라 주입된 Concrete type의 동작을 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;private let searchResultRepository: any SearchResultRepository
​
init(searchResultRepository: any SearchResultRepository) {
 &amp;nbsp;self.searchResultRepository = searchResultRepository
}
​
func search(keyword: String) async {
 &amp;nbsp;let items = await searchResultRepository.fetchItemList(keyword: keyword)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;물론, 실제로는 모든 코드가 예시처럼 정석적으로 분리되어 있진 않았지만, 주입을 통해 구현체를 교체한다는 큰 개념은 차용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 모든 게 해결된 걸까요? 일반적인 상황에서는 그럴 수 있겠지만, 저희의 경우 테스트 케이스마다 테스트할 모듈을 다르게 세팅해야 했습니다. 즉, 상황에 따라 사용해야 하는 JSON 파일이 각기 달랐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1736139306715&quot; class=&quot;swift&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;class MockSearchResultRepository: SearchResultRepository {
    func fetchItemList(keyword: String) async -&amp;gt; [Item] {
        // case 1. A 모듈.json 로딩해서 리턴
        // case 2. B 모듈.json 로딩해서 리턴
        // case 3. C 모듈.json 로딩해서 리턴
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3386&quot; data-origin-height=&quot;2160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3Ks10/btsLEdft4UH/eTcufiQza6us8bOWVOyCK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3Ks10/btsLEdft4UH/eTcufiQza6us8bOWVOyCK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3Ks10/btsLEdft4UH/eTcufiQza6us8bOWVOyCK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3Ks10%2FbtsLEdft4UH%2FeTcufiQza6us8bOWVOyCK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3386&quot; height=&quot;2160&quot; data-origin-width=&quot;3386&quot; data-origin-height=&quot;2160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 해결하기 위해 XCUIApplication의&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/documentation/xctest/xcuiapplication/1500427-launchenvironment&quot;&gt;&lt;span&gt;launchEnvironment&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;프로퍼티를 사용했는데요! 얘가 뭔고,, 하니 말 그대로 앱 실행 시에 전달되는 환경 변수로 key - value 쌍을 지정할 수 있습니다. 이렇게 말이죠!&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;swift&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;// UITest target
class AdTrackingTest: XCTestCase {
 &amp;nbsp;func test_모듈_A_장바구니_클릭시_광고_클릭_url_저장() {
 &amp;nbsp; &amp;nbsp;// ✅ 1. 세팅할 Fake resonse 지정
 &amp;nbsp; &amp;nbsp;let environmentKey = &quot;FakeSearchResultResponse&quot;
 &amp;nbsp; &amp;nbsp;app.launchEnvironment[environmentKey] = &quot;ModuleA.json&quot;
 &amp;nbsp; &amp;nbsp;
 &amp;nbsp; &amp;nbsp;// 2. 장바구니 클릭 tap
 &amp;nbsp; &amp;nbsp;// 3. url 저장 검증
 &amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 앱에서는&lt;span&gt;&amp;nbsp;&lt;a style=&quot;color: #4183c4;&quot; href=&quot;https://developer.apple.com/documentation/foundation/processinfo/1408734-processinfo&quot;&gt;processInfo&lt;/a&gt;&amp;nbsp;에서&lt;/span&gt; 동일한 key 값을 이용해 값을 불러올 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #333333; text-align: left;&quot;&gt;&lt;code&gt;// App target
let environmentKey = &quot;FakeSearchResultResponse&quot;
let fakeResponseFileName = ProcessInfo.processInfo.environment[environmentKey] // &quot;ModuleA.json&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;물론 실제 코드는 좀 더 리팩토링 되어있긴 하지만,, 큰 개념은 아시겠죠?!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 테스트 코드를 돌려보면~~?! 완성되었습니다~&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;2556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cENMiO/btsLFaPNBs0/lcCJoZpebnIMSYNZSvjc71/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cENMiO/btsLFaPNBs0/lcCJoZpebnIMSYNZSvjc71/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cENMiO/btsLFaPNBs0/lcCJoZpebnIMSYNZSvjc71/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cENMiO/btsLFaPNBs0/lcCJoZpebnIMSYNZSvjc71/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1178&quot; height=&quot;2556&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;2556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;지금까지 iOS에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;특정 이벤트에 대한 URL 요청이 정상적으로 이루어졌는지&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;확인하는 방법에 대해 알아보았는데요!&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;전체적인 구조와 각 단계에서 사용된 주요 개념은 아래 그림과 같이 정리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3848&quot; data-origin-height=&quot;1514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EWT76/btsLEfqH2g1/dcrG6BEkBZQVX8ucwTnS90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EWT76/btsLEfqH2g1/dcrG6BEkBZQVX8ucwTnS90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EWT76/btsLEfqH2g1/dcrG6BEkBZQVX8ucwTnS90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEWT76%2FbtsLEfqH2g1%2FdcrG6BEkBZQVX8ucwTnS90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3848&quot; height=&quot;1514&quot; data-origin-width=&quot;3848&quot; data-origin-height=&quot;1514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;먼저, 테스트 환경에 따라서 세팅된 가짜 json 데이터가 로드됩니다&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;다음으로 UITest를 통해서 버튼 클릭과 같은, 사용자의 광고 이벤트를 트리거합니다. 이렇게 URL 요청이 나가면,&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;proxy 가 이를 가로채고&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;공유 디렉토리 저장소에 URL을 저장합니다&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;&lt;span&gt;마지막으로, 이 URL 이 저장소에 있냐 아니냐에 따라서 이벤트에 따른 URL 요청 여부를 판단합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기까지 읽으신 분들 중에 혹시 '  엥,, 테스트 그렇게 하는 거 아닌데용,,' 하는 분들 계신가요?! 그렇다면 언제든 댓글로 의견 나눠주시면 압도적 감사,, 하겠습니다 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그럼 테스트와 함께 행복한 2025년 되세요! 그럼 20000!  &lt;/span&gt;&lt;/p&gt;</description>
      <category>Mobile</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/119</guid>
      <comments>https://dev.gmarket.com/119#entry119comment</comments>
      <pubDate>Mon, 6 Jan 2025 14:03:52 +0900</pubDate>
    </item>
    <item>
      <title>RSS로 누구보다 빠르게, 그리고 자동으로 새 소식 가져오기</title>
      <link>https://dev.gmarket.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;br /&gt;&lt;br /&gt;Pricing Tech Engineering팀원이자&amp;nbsp;&lt;br /&gt;해당&amp;nbsp;테크블로그의&amp;nbsp;에디터장을&amp;nbsp;담당하고&amp;nbsp;있는&amp;nbsp;김민우입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본격적인 글에 앞서 이번에 작업한 테크블로그 UI 변경 건을 잠깐 소개해드립니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 87.5584%; height: 2225px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 2208px;&quot;&gt;
&lt;td style=&quot;width: 100%; text-align: left; height: 2208px;&quot;&gt;&lt;span&gt;&lt;br /&gt;&amp;nbsp;G마켓 테크블로그는 티스토리를 기반으로 제작되었으며,&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp;티스토리에서 제공하는 HTML과 CSS를 직접 수정하는 '스킨 편집'을 통해 홈페이지를 변경하였습니다.&lt;br /&gt;&lt;/span&gt;&amp;nbsp;(아래는 개편 이전의 스킨입니다.)&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;2234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCIYA/btsLCGaGM5K/9oR4XAgNOQB1Xnm6usL2K0/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCIYA/btsLCGaGM5K/9oR4XAgNOQB1Xnm6usL2K0/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCIYA/btsLCGaGM5K/9oR4XAgNOQB1Xnm6usL2K0/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCIYA%2FbtsLCGaGM5K%2F9oR4XAgNOQB1Xnm6usL2K0%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;679&quot; height=&quot;530&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;2234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp;1.&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;전체적인 홈 디자인 변경&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp; &amp;bull;&lt;span&gt;&amp;nbsp;신규&amp;nbsp;&lt;/span&gt;로고 제작 후 적용&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WyotT/btsLBJTtoWZ/ZIvkY0KXY2S5mJvdKFqQc0/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WyotT/btsLBJTtoWZ/ZIvkY0KXY2S5mJvdKFqQc0/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WyotT/btsLBJTtoWZ/ZIvkY0KXY2S5mJvdKFqQc0/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWyotT%2FbtsLBJTtoWZ%2FZIvkY0KXY2S5mJvdKFqQc0%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;235&quot; height=&quot;178&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;span&gt;&lt;br /&gt;&amp;nbsp; &amp;bull;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;카테고리 상단 추가 및 썸네일 이미지 및 애니메이션 추가로 콘텐츠 가독성 강화.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Jan-02-2025 17-16-58.gif&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGBAgX/btsLDxxlDkT/cUuHvzPtDKpmHFU8QUnNOK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGBAgX/btsLDxxlDkT/cUuHvzPtDKpmHFU8QUnNOK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGBAgX/btsLDxxlDkT/cUuHvzPtDKpmHFU8QUnNOK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bGBAgX/btsLDxxlDkT/cUuHvzPtDKpmHFU8QUnNOK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1780&quot; height=&quot;1036&quot; data-filename=&quot;Jan-02-2025 17-16-58.gif&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;기존에 다소 밋밋했던 블로그 메인 페이지를 전면적으로 개편하였습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&amp;nbsp; -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;특히, 썸네일 이미지를 활용해 글의 주요 내용을 간략히 요약할 수 있도록 구성하여 독자들이 원하는 콘텐츠를 한눈에 파악할 수 있도록 개선했습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp;2.&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Footer 업데이트&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp; &amp;bull;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;기존의 기본 footer 제거하고, 지마켓 연락처 및 패밀리 사이트로 연결되는 정보를 추가.&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctT8KZ/btsLBZ9EeUl/bFR7N2WglpaBDtqejiZYB0/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctT8KZ/btsLBZ9EeUl/bFR7N2WglpaBDtqejiZYB0/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctT8KZ/btsLBZ9EeUl/bFR7N2WglpaBDtqejiZYB0/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctT8KZ%2FbtsLBZ9EeUl%2FbFR7N2WglpaBDtqejiZYB0%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;617&quot; height=&quot;250&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;티스토리 기본 디자인을 배제하고, 회사와 관련된 사이트를 홍보하는 용도로 활용됩니다.&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;댓글로 소통이 원활하지 않았던 점을 보완하고자 Contact Us에 블로그 운영진의 이메일을 넣어 독자들이 직접 연락할 수 있도록 하였습니다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp;3.&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;댓글 정리 및 정책 변경&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp; &amp;bull;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;스팸 댓글 일괄 제거 및 비밀글 옵션 html 제거&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1022&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0cn92/btsLCZHQVN0/uJSiPzPG1Yq5XoR4BOduhk/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0cn92/btsLCZHQVN0/uJSiPzPG1Yq5XoR4BOduhk/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0cn92/btsLCZHQVN0/uJSiPzPG1Yq5XoR4BOduhk/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0cn92%2FbtsLCZHQVN0%2FuJSiPzPG1Yq5XoR4BOduhk%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;407&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1022&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;관련 없는 홍보성 댓글들로 채워진&amp;nbsp;&lt;/span&gt;&lt;span&gt;비밀글 도배로 인해 불편을 초래했던 댓글들을 정리하고, 자동 스팸기능을 통해 작성이 불가능하도록 조치하였습니다.&lt;/span&gt;&lt;br /&gt;&amp;nbsp; &lt;br /&gt;&amp;nbsp; -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;회사명 변경과 티스토리 기본 디자인 배제를 위해 블로그 공식 도메인을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://dev.gmarket.com/&quot;&gt;https://dev.gmarket.com/&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;으로 설정하고 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;이에 따라, 비밀글을 쓰기 위해 티스토리 로그인 후 댓글 쓸 경우 CORS 에러로 정상적으로 등록되지 않는 문제를 방지하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;비로그인 댓글만 허용&lt;/b&gt;하는 정책으로 변경하였습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp; G마켓 테크블로그는 꾸준히 개선 중이며, 더 나은 사용자 경험을 제공하기 위해 노력하고 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이어서 본격적인 글을 시작하겠습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;들어가기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;게시판에서 선착순으로 댓글을 작성해야 하는 상황을 겪어본 적이 있나요?&lt;br /&gt;저희 회사에서는 가끔 사내게시판에 선착순으로 댓글을 쓰면 당첨되는 이벤트성 글들이 올라옵니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9bhzl/btsLDfKtw4l/0YkFDXuagbwqkBoPNVAEDk/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9bhzl/btsLDfKtw4l/0YkFDXuagbwqkBoPNVAEDk/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9bhzl/btsLDfKtw4l/0YkFDXuagbwqkBoPNVAEDk/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9bhzl%2FbtsLDfKtw4l%2F0YkFDXuagbwqkBoPNVAEDk%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;199&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 문제는 이러한 방식은 게시판을 자주 들락거리는 사람들이 유리하고, 꾸준히 업무에 집중하고 있는 사람들은 이런 기회를 잡기가 어렵다는 점입니다. 알람 기능이 있었더라면 누구든 공평하게 선착순으로 댓글을 달 수 있을 텐데 말이죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 불합리함을 해결해 보고자 개발자다운 방법을 고민하기 시작했고&amp;nbsp;&amp;ldquo;디버깅을 해보면 뭔가 단서가 있지 않을까?&amp;rdquo;라는&lt;span&gt;&amp;nbsp;&lt;/span&gt;생각이 들었습니다. 개발자 도구를 열고 페이지의 소스를 하나하나 들여다보며&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RSS&lt;/b&gt;라는 단어를 발견했습니다. 처음엔 이게 뭔가 싶었지만, 조금만 더 찾아보니 RSS 리더 앱을 활용하면 해당 RSS 주소를 통해 게시판 업데이트를 실시간으로 확인할 수 있다는 사실을 알게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기술적으로 보면 RSS는&lt;span&gt;&amp;nbsp;주로는&amp;nbsp;&lt;/span&gt;&lt;b&gt;XML 형식의 데이터&lt;/b&gt;로 구성되어 있는데, 이를 통해 새로운 게시글의 제목, 링크, 요약 등을 정리한 데이터를 RSS 리더라는 앱이나 프로그램에서 확인할 수 있습니다. 직접 들어가 보면 다음과 같이 기존 게시판 페이지와는 사뭇 다르게 필요한 정보만 모인 페이지가 뜹니다. 이를 활용하여 리더 앱은 전체 html을 긁어올 필요 없이 알람에 필요할만한 정보만 빼서 알림을 만들게 되는 셈입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;769&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Swx0/btsLDM8SFZR/jjgdIP7Wg2cGJeHDSOq1RK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Swx0/btsLDM8SFZR/jjgdIP7Wg2cGJeHDSOq1RK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Swx0/btsLDM8SFZR/jjgdIP7Wg2cGJeHDSOq1RK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Swx0%2FbtsLDM8SFZR%2FjjgdIP7Wg2cGJeHDSOq1RK%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;607&quot; height=&quot;439&quot; data-origin-width=&quot;769&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RSS 란?&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RSS는 1999년에 처음 개발되었으며, 웹사이트의 콘텐츠를 효율적으로 배포하기 위한 기술로 시작되었습니다. 여기서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;론 스워츠(Aaron Swartz)라는 천재 개발자를 언급 안 할 수가 없는데, RSS를 창시하&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;며 모든 웹의 정보를 더 자유롭고 접근하기 쉽게 만들고자&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;도입하였고, 또한 RSS를 기반으로 만들어진&lt;span&gt;&amp;nbsp;&lt;/span&gt;'레딧(reddit)'이라는 미국 최대 커뮤니티 사이트의 공동 설립자로도 알려져 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;그가 RSS를 도입할 때에도 &amp;lsquo;정보의 민주화&amp;rsquo;라는 철학이 깔려 있었습니다. 정보를 특정한 사람들만 독점하지 않고, 누구나 공평하게 접근할 수 있도록 하겠다는 취지입니다. 이런 점은 앞서 말씀드린 선착순 댓글 이벤트로 인해 게시판을 주기적으로 확인할 시간적인 여유가 안 되는 저와 같은 입장의 사람들에게도 기술적 의의가 있다는 점에서 일맥상통합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBXEas/btsLDwd8tnK/HmQp6SNwsSKcwoLbXxg2uK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBXEas/btsLDwd8tnK/HmQp6SNwsSKcwoLbXxg2uK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBXEas/btsLDwd8tnK/HmQp6SNwsSKcwoLbXxg2uK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBXEas%2FbtsLDwd8tnK%2FHmQp6SNwsSKcwoLbXxg2uK%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;220&quot; height=&quot;186&quot; data-origin-width=&quot;1320&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RSS의 가장 큰 장점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정보를 자동으로 받아볼 수 있다&lt;/b&gt;는 점입니다. 굳이 웹사이트를 직접 방문하지 않아도 RSS 리더를 통해 필요한 정보만 쏙쏙 받아볼 수 있죠. 이런 방식 덕분에 여러 사이트를 돌아다니며 시간을 낭비할 필요가 없고, 중요한 정보를 놓치는 일도 줄어듭니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RSS는 새로운 정보가 올라오는 거의 즉시 확인할 수 있다는 점&lt;/b&gt;에서도 큰 강점을 가집니다. RSS 리더는 정해진 시간 간격마다 피드를 확인하고 업데이트를 받아오는데, 이 시간을 사용자가 조정할 수 있습니다. 예를 들어, 확인 주기를 1분으로 설정하면, 불규칙적으로 사이트에 들어가는 사용자보다 훨씬 빠르게 정보를 받아볼 수 있는 것이죠. 이런 방식은 중요한 공지나 업데이트를 기다리는 상황에서 특히 유용합니다. RSS를 통해 정보가 업데이트되는 즉시 알림을 받는다면, 빠르게 정보를 확인하고 대응할 수 있게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 RSS의 또 다른 큰 장점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;표준화된 형식 덕분에 여러 사이트의 정보를 한 곳에서 통합 관리할 수 있다는 점&lt;/b&gt;입니다. 예를 들어, 뉴스 사이트 A, 블로그 B, 커뮤니티 C의 업데이트를 확인하려면 각각 들어가서 알림을 설정하거나 직접 확인해야 하지만, RSS 리더를 사용하면 이 모든 정보를 한 번에 받아볼 수 있습니다. 사이트가 RSS 피드를 제공하기만 하면, 사용자는 별도의 설정 없이도 링크만 추가해 관리할 수 있어, 번거롭게 사이트별 알림을 켜고 끌 필요 없이 간편하게 최신 정보를 모아볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RSS 알림을 받는 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, RSS 리더기를 다운로드해야 합니다. 맥북을 사용한다면 App Store, 윈도우를 사용한다면 Microsoft Apps에서 다운로드할 수 있습니다. 모바일에서 알림을 받고 싶다면, 각 OS에서 지원하는 공식 스토어(Android는 Google Play, iOS는 App Store)를 통해 앱을 설치하는 것이 가장 안전합니다. 크롬 브라우저를 사용 중이라면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://chromewebstore.google.com/search/rss&quot;&gt;Chrome 웹 스토어&lt;/a&gt;에서 RSS 관련 확장 프로그램을 검색해 설치할 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 제가 위에서 설명한 사내 게시판과 같이 인증(ID/PW)으로 접속을 해야 볼 수 있는 사이트들은 인증 기능을 지원하는 RSS 피드를 사용 중이라면, 이를 지원하는 리더 앱을 찾아야 합니다. 이런 경우, 공식 스토어 외에도 구글링을 통해 적합한 스펙의 앱을 직접 찾아볼 수 있습니다. 다만, 안전을 위해 신뢰할 수 있는 소스를 통해 다운로드하는 것을 권장합니다. 실습 전, 어떤 RSS 리더를 사용했다고 특정 지으면 광고가 될 수도 있기 때문에 설치 과정은 생략하도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RSS 리더가 제대로 작동하는지 확인하기 위해 글쓴이인 저의 개인 블로그를 예시로 사용하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(참고로, 해당 테크블로그도 RSS를 지원하지만, 테스트 목적으로 글을 올릴 수 없으므로 개인 블로그를 예로 듭니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제 블로그 주소는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://blog.naver.com/rlaalsdn456456&quot;&gt;https://blog.naver.com/rlaalsdn456456&lt;/a&gt;입니다. 블로그 하단에 RSS 버튼이 있으며, 이를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;a href=&quot;https://rss.blog.naver.com/rlaalsdn456456.xml&quot;&gt;https://rss.blog.naver.com/rlaalsdn456456.xml&lt;/a&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 RSS 주소를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NnSlo/btsLDU6FUGK/6eUCE1VBg9UIbuhCYzSnEk/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NnSlo/btsLDU6FUGK/6eUCE1VBg9UIbuhCYzSnEk/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NnSlo/btsLDU6FUGK/6eUCE1VBg9UIbuhCYzSnEk/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNnSlo%2FbtsLDU6FUGK%2F6eUCE1VBg9UIbuhCYzSnEk%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;227&quot; height=&quot;117&quot; data-origin-width=&quot;227&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주소를 다운받은 RSS 리더 앱에 입력하면 설정이 끝입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/evFH4q/btsLC0z19sK/10lGEEvIEd974X889hO7rK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/evFH4q/btsLC0z19sK/10lGEEvIEd974X889hO7rK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/evFH4q/btsLC0z19sK/10lGEEvIEd974X889hO7rK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FevFH4q%2FbtsLC0z19sK%2F10lGEEvIEd974X889hO7rK%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;439&quot; height=&quot;678&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;제가 사용하는 앱의 경우, 체크 주기(Frequency)를 초 단위로 설정할 수 있습니다. 시연을 위해 10초로 설정하고 테스트를 진행해 보겠습니다. 이 설정은 10초마다 새 글이 있는지 확인하고, 업데이트가 있으면 알림을 띄워줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일단 제 블로그니까 간단한 글 하나 바로 올려볼게요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;1198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JneSL/btsLDPdrO9z/pepaGwyQkA4iDXzdKHMcK1/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JneSL/btsLDPdrO9z/pepaGwyQkA4iDXzdKHMcK1/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JneSL/btsLDPdrO9z/pepaGwyQkA4iDXzdKHMcK1/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJneSL%2FbtsLDPdrO9z%2FpepaGwyQkA4iDXzdKHMcK1%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;1198&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;1198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;약 1분 정도 기다려보면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RSS 리더 앱&lt;/b&gt;에서 새로운 글에 대한 알림이 뜨는 것을 확인할 수 있습니다. 이를 통해 RSS가 정상적으로 작동하는지 테스트할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Dec-18-2024 14-05-01.gif&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deLBEX/btsLDdTsIHN/ogS57kKKKuu3QrwCA99oE1/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deLBEX/btsLDdTsIHN/ogS57kKKKuu3QrwCA99oE1/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deLBEX/btsLDdTsIHN/ogS57kKKKuu3QrwCA99oE1/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeLBEX%2FbtsLDdTsIHN%2FogS57kKKKuu3QrwCA99oE1%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;390&quot; height=&quot;144&quot; data-filename=&quot;Dec-18-2024 14-05-01.gif&quot; data-origin-width=&quot;390&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;눈치채신 분들은 이제야 RSS의 문제점을 보시게 될 겁니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RSS 의 치명적인 한계&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RSS의 최대 장점이자 단점이 될 수 있는 점은 정보 공급자인 서비스 업체에서 운영하는 사이트가 아니라 정보 수요자인&amp;nbsp;&lt;b&gt;사용자에게 주도권이 있다는 구조라는 점&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;549&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HBVO0/btsLBLqeonK/QtrjqT0Z26lA7Ta3keG7ok/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HBVO0/btsLBLqeonK/QtrjqT0Z26lA7Ta3keG7ok/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HBVO0/btsLBLqeonK/QtrjqT0Z26lA7Ta3keG7ok/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHBVO0%2FbtsLBLqeonK%2FQtrjqT0Z26lA7Ta3keG7ok%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;937&quot; height=&quot;549&quot; data-origin-width=&quot;937&quot; data-origin-height=&quot;549&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그림만 보셔도 아시다시피 정보 수요자는 RSS 리더를 통해 계속해서 정보 공급자인 사이트에 RSS를 요구합니다. 1명이 이러는 것도 부하가 갈 텐데, 여러 사용자가 이렇게 반복적이면서도 의미 없는 메아리를 계속하는 것은 사용자 입장에서는 편해도 서비스를 운영하는 서버 입장에서는 필요 이상으로 xml을 계속해서 만들어주고 있으니, RSS는 &lt;b&gt;쓸데없는 부하를 일으키는 주범&lt;/b&gt;이 됩니다. (물론, 일반적인 크롤링보다 요청당 데이터 용량이 적기 때문에 부담이 덜하긴 하지만, 근본적으로는 서버 자원을 소모시키는 구조라는 점에서 여전히 문제가 됩니다.) 제 경우처럼 10초마다 피드를 확인한다는 점에서, 여러 명이 동시에 이런 환경에서 RSS를 구축해 놓으면 타겟 서버 입장에서는 일종의 DDoS 공격 부하를 경험하게 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 이유로, 최근의 RSS 리더 앱들은 호출 빈도를 길게 설정하도록 제한하고 있습니다. 예를 들어, RSS 기능이 탑재된 Microsoft Outlook은 기본적으로 1시간 단위로 피드를 확인하도록 설정되어 있습니다. 하지만 이 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실시간성이 떨어지는 문제&lt;/b&gt;가 발생하며, RSS의 큰 장점 중 하나를 희생해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이와 이어지는 이유로 서비스 사이트들이 RSS 피드를 지원하지 않게 되었습니다. 서버 부하와 실시간성 부족 문제로 인해 많은 사이트들은 RSS를 지원하는 대신, 자체 앱과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;이벤트 기반(event-driven)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;방식을 도입하고 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;예를 들어, 페이스북, 인스타그램, 유튜브 등은 자체 앱을 통해 푸시 알림을 전송하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;정보 공급자가 주도권을 가지고 실시간성을 확보&lt;/b&gt;하는 구조를 채택했습니다. 이 방식은 덤으로 사용자 맞춤형 콘텐츠와 광고 노출을 통해 수익 창출까지 가능하게 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UkSIE/btsLBKZai2i/e1mMrIX6AI0XpDKVHRhK90/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UkSIE/btsLBKZai2i/e1mMrIX6AI0XpDKVHRhK90/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UkSIE/btsLBKZai2i/e1mMrIX6AI0XpDKVHRhK90/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUkSIE%2FbtsLBKZai2i%2Fe1mMrIX6AI0XpDKVHRhK90%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;880&quot; height=&quot;339&quot; data-origin-width=&quot;880&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결국, RSS는 사용자 주도권이라는 장점이 있지만, 대량의 요청이 사이트에 부하를 주고, 이를 포기하면 실시간성이 떨어진다는 단점이 있습니다. 이로 인해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Polling&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;방식처럼 구식으로 인식되는 경향이 있으며, 실시간 정보 전달을 요구하며 필요한 자원만 쓰려는 이벤트 트리거 방식을 선호하는 현대의 환경에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Stream&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;과 같은 방식이 선호되고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그럼에도 여전히 가치 있는 RSS&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 RSS의 장점을 설명한 것 중 하나인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;표준화된 형식 덕분에 여러 사이트의 정보를 한 곳에서 통합 관리할 수 있다는 점&lt;/b&gt;은 아직 대체 불가한 장점입니다.&amp;nbsp;실제로 몇몇 독자 분들은 직접 이 사이트를 방문한 것이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RSS를 통해 기술 블로그 글들을 모아놓은 사이트&lt;/b&gt;에서 이 글에 도달한다는 방문자 통계도 찍힙니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;844&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEt5C8/btsLCDycxRN/fG7EbQ8vi19TSR7EIP4RF1/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEt5C8/btsLCDycxRN/fG7EbQ8vi19TSR7EIP4RF1/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEt5C8/btsLCDycxRN/fG7EbQ8vi19TSR7EIP4RF1/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEt5C8%2FbtsLCDycxRN%2FfG7EbQ8vi19TSR7EIP4RF1%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;551&quot; height=&quot;442&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;844&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bShwqd/btsLDNNtHxn/6ZO4DqILeeKJImRgZvSROK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bShwqd/btsLDNNtHxn/6ZO4DqILeeKJImRgZvSROK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bShwqd/btsLDNNtHxn/6ZO4DqILeeKJImRgZvSROK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbShwqd%2FbtsLDNNtHxn%2F6ZO4DqILeeKJImRgZvSROK%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;566&quot; height=&quot;223&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1639&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히, 취업 준비생이나 기술 트렌드를 빠르게 익히고 싶은 분들에게는 RSS가 더없이 유용합니다. 왜냐하면, 어떤 기술 블로그는 티스토리, 어떤 곳은 미디엄(Medium), 또 다른 곳은 GitHub Pages를 사용하고, 어떤 사이트는 자체 서버를 통해 운영됩니다. 이처럼 플랫폼이 다양하지만, 여전히 블로그와 같은&amp;nbsp;&lt;b&gt;대부분의 게시글 서비스의 사이트들은 관례적으로 RSS를 지원&lt;/b&gt;하고 있기 때문에, RSS는 위 사이트들처럼 여전히 정보를 한 곳에 모아보거나 알람을 바로바로 받고 싶지만 일일이 각각 앱들을 깔아서 알람 환경설정도 불필요하기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 사이트는 티스토리 플랫폼에서 운영되고 있습니다. 따라서, 티스토리 앱을 설치하면 해당 블로그를 구독하고 티스토리 자체 앱에서 알림을 받을 수 있는 구조로 설계되어 있습니다. 하지만 대부분의 블로그들이 그렇듯,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RSS 기능도 함께 제공&lt;/b&gt;하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;끝으로&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 블로그의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RSS 주소&lt;/b&gt;를 바로 드리겠습니다. 이 주소를 RSS 리더 앱에 추가하고 알림을 설정해 보세요.&lt;/p&gt;
&lt;pre id=&quot;code_1735802923418&quot; class=&quot;html xml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;https://dev.gmarket.com/rss&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현직 개발자들이 경험하고 있는 기술 트렌드를 누구보다 빨리 읽고, 경쟁력을 높일 수 있는 기회가 될 것입니다.&lt;br /&gt;&lt;br /&gt;RSS로 누구보다 빠르게 G마켓의 기술과 경험을 공유합니다.&lt;br /&gt;읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>Backend</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/118</guid>
      <comments>https://dev.gmarket.com/118#entry118comment</comments>
      <pubDate>Thu, 2 Jan 2025 16:39:15 +0900</pubDate>
    </item>
    <item>
      <title>Windows Container 에 대해 알아보기</title>
      <link>https://dev.gmarket.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요&amp;nbsp;&lt;br /&gt;Dev&amp;nbsp;Platform&amp;nbsp;&amp;amp;&amp;nbsp;Corporate&amp;nbsp;IT팀&amp;nbsp;팀&amp;nbsp;박진규입니다.&lt;br /&gt;이번 포스팅에서는 Windows Container에 대한 내용을 공유드리려 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;제가 담당하는 서비스들 중에서는 Windows OS에 종속적인 서비스들이 존재하는데,&lt;br /&gt;(ex. net framework)&lt;br /&gt;어느 날 이러한 서비스들을 Container 위에서 동작시킬 수 없을까 의문이 들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;일반적으로 Container는 Linux 기반이다 보니,&lt;br /&gt;Windows에서는 이를 어떻게 해결할 수 있을까 찾아보던 도중&lt;br /&gt;Windows Container에 대해 알게 되었고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이에 대한 내용을 정리해서 공유드립니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Windows Container 란?&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows 어플리케이션을 Windows Server 환경에서 격리하여 실행하기 위한 기능입니다.&lt;br /&gt;&lt;b&gt;Windows Server 2016부터 지원&lt;/b&gt;되고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Linux 컨테이너와 유사하게 애플리케이션을 독립된 환경에서 실행하지만&lt;br /&gt;&lt;/span&gt;&lt;span&gt;Windows 컨테이너는 Windows 커널을 기반으로 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhueoB/btsLeKLlhnh/FIlgRR9uzvY4a5zPJefNl0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhueoB/btsLeKLlhnh/FIlgRR9uzvY4a5zPJefNl0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhueoB/btsLeKLlhnh/FIlgRR9uzvY4a5zPJefNl0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhueoB%2FbtsLeKLlhnh%2FFIlgRR9uzvY4a5zPJefNl0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1116&quot; height=&quot;297&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Windows Container 특이점&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 기본 이미지&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows Container 는 Microsoft에서 제공해 주는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;4가지 종류의 기본 이미지&lt;/b&gt;를 사용해서 빌드가 가능합니다.&lt;br /&gt;각 이미지는 포함된 Windows 기능들이 다르기 때문에 상황에 따라 필요한 이미지를 선택하여 사용하시면 됩니다.&lt;br /&gt;(포함된 기능이 많을 수록 이미지 크기가 커집니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxWAye/btsLdmEfrV4/OnvuERMxttsWbGLGFXfrV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxWAye/btsLdmEfrV4/OnvuERMxttsWbGLGFXfrV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxWAye/btsLdmEfrV4/OnvuERMxttsWbGLGFXfrV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxWAye%2FbtsLdmEfrV4%2FOnvuERMxttsWbGLGFXfrV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;815&quot; height=&quot;484&quot; data-origin-width=&quot;815&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Microsoft에서는 대부분의 사용자에게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Windows Server Core&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;및&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Nanoserver&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 추천하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아래는 직접 docker pull로 기본 이미지를 받았을 때&lt;br /&gt;이미지 사이즈를 정리한 내용입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;917&quot; data-origin-height=&quot;68&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mV8En/btsLehbNFiR/WP1GL80uS6tNnK3x5ekHd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mV8En/btsLehbNFiR/WP1GL80uS6tNnK3x5ekHd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mV8En/btsLehbNFiR/WP1GL80uS6tNnK3x5ekHd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmV8En%2FbtsLehbNFiR%2FWP1GL80uS6tNnK3x5ekHd0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;917&quot; height=&quot;68&quot; data-origin-width=&quot;917&quot; data-origin-height=&quot;68&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;이미지 종류&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;이미지 사이즈&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;특이사항&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Nano&amp;nbsp;Server&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 300 MB&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Server&amp;nbsp;Core&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 5 GB&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Windows&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 9 GB&lt;/td&gt;
&lt;td&gt;windows server 2019 까지만 지원합니다. (ltsc2019)&lt;br /&gt;windows server 2022 부터는 Windows Server 이미지를 사용해야 합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Windows&lt;span&gt;&amp;nbsp;&lt;/span&gt;Server&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 10 GB&lt;/td&gt;
&lt;td&gt;windows server 2022 부터 지원합니다. (ltsc2022)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 호환성&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows Container를 구동하기 위해서는 별도의 컨테이너 호환성 요구 사항이 존재합니다. Windows에서는 Linux와 다르게 사용자 모드와 커널 모드가 긴밀하게 결합되어 있고&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이로 인해 Windows Container 는 Host 서버의 Windows OS 버전에 따라 구동이 불가능한 경우가 생깁니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 Windows 10 환경에서는 Windows Server 2022 이미지의 Container를 구동하는 게 불가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk2mqJ/btsLfP5WLp5/G9MTtrk1ta8kf12d4KDtUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk2mqJ/btsLfP5WLp5/G9MTtrk1ta8kf12d4KDtUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk2mqJ/btsLfP5WLp5/G9MTtrk1ta8kf12d4KDtUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk2mqJ%2FbtsLfP5WLp5%2FG9MTtrk1ta8kf12d4KDtUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;870&quot; height=&quot;365&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;365&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제로 Windows 10 환경에서 Windows Server 2022 이미지의 Container를 실행할 경우 아래와 같은 오류가 발생합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649512&quot; class=&quot;angelscript&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker: a Windows version 10.0.20348-based image is incompatible with a 10.0.19043 host.&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;.Net Framework App을 Windows Container로 실행해 보기&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저는 아래의 조건에서 Windows Container를 실행해 보았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Host OS : Windows 10&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;Container OS : windowsservercore-ltsc2022&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;실행 Application : .net framework 4.5.2 기반의 ASP.NET Application&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 Application 만들기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) .net framework 4.5.2 기반의 ASP.NET Application을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;646&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cu0ZHt/btsLe6ABzpy/FQdDxTeliJQkPjugoSqVnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cu0ZHt/btsLe6ABzpy/FQdDxTeliJQkPjugoSqVnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cu0ZHt/btsLe6ABzpy/FQdDxTeliJQkPjugoSqVnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcu0ZHt%2FbtsLe6ABzpy%2FFQdDxTeliJQkPjugoSqVnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;938&quot; height=&quot;646&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;646&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 생성한 ASP.NET Application 이 잘 동작하는 것을 확인한 후, 게시합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;deploy.PNG&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;231&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SP7mp/btsLfOTwqvD/FrD5MfWnsKhC54Dae5n920/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SP7mp/btsLfOTwqvD/FrD5MfWnsKhC54Dae5n920/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SP7mp/btsLfOTwqvD/FrD5MfWnsKhC54Dae5n920/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSP7mp%2FbtsLfOTwqvD%2FFrD5MfWnsKhC54Dae5n920%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;231&quot; data-filename=&quot;deploy.PNG&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;231&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Windows 10 Host에 Windows Server 2022 가상 컴퓨터 구축하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 이야기드린 것처럼&lt;br /&gt;Windows 10에서는 windowsservercore-ltsc2022 이미지를 구동할 수 없습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 Hyper-V를 사용해서 Windows Server 2022 가상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;컴퓨터를&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;구축한 뒤&lt;br /&gt;해당 가상환경 위에서 windowsservercore-ltsc2022 이미지를 container로 구동했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) windows_server_2022 평가판 ISO 다운로드합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://www.microsoft.com/ko-kr/evalcenter/evaluate-windows-server-2022&quot;&gt;https://www.microsoft.com/ko-kr/evalcenter/evaluate-windows-server-2022&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 다운로드한 ISO와 Hyper-V를 사용해서 가상 컴퓨터를 생성합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;가상 컴퓨터 설정 요약&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZI3fW/btsLgfwwh6o/cJwe8o2OcAopTQ1RooUNI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZI3fW/btsLgfwwh6o/cJwe8o2OcAopTQ1RooUNI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZI3fW/btsLgfwwh6o/cJwe8o2OcAopTQ1RooUNI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZI3fW%2FbtsLgfwwh6o%2FcJwe8o2OcAopTQ1RooUNI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;781&quot; height=&quot;339&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;가상 컴퓨터를 실행 후 연결하여 정상적으로 설치되었는지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcC4D4/btsLgb1X2fe/EZYE2bTHiBzTDviBior9KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcC4D4/btsLgb1X2fe/EZYE2bTHiBzTDviBior9KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcC4D4/btsLgb1X2fe/EZYE2bTHiBzTDviBior9KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcC4D4%2FbtsLgb1X2fe%2FEZYE2bTHiBzTDviBior9KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;758&quot; height=&quot;300&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) Host 컴퓨터 가상 컴퓨터를 대상으로 중첩 가상화 설정을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649514&quot; class=&quot;cmake&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set-VMProcessor -VMName &quot;windows-server-2022&quot; -ExposeVirtualizationExtensions $true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4) 가상 컴퓨터에 Container 관련 Windows Feature를 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649514&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Install-WindowsFeature -Name Hyper-V
Install-WindowsFeature -Name Containers&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5) 가상 컴퓨터에 Windows Server 용 Docker CE를 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649514&quot; class=&quot;cmake&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Invoke-WebRequest -UseBasicParsing &quot;https://raw.githubusercontent.com/microsoft/Windows-Containers/Main/helpful_tools/Install-DockerCE/install-docker-ce.ps1&quot; -o install-docker-ce.ps1
.\install-docker-ce.ps1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;docker 명령어 정상 실행을 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;38&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cybI1n/btsLerkXdlB/5BWekEvKwFPkNby9IqJCVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cybI1n/btsLerkXdlB/5BWekEvKwFPkNby9IqJCVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cybI1n/btsLerkXdlB/5BWekEvKwFPkNby9IqJCVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcybI1n%2FbtsLerkXdlB%2F5BWekEvKwFPkNby9IqJCVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;394&quot; height=&quot;38&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;38&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;가상 컴퓨터에서 Docker 빌드하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) &quot;실행 Application 만들기&quot; 단계에서 게시했던 ASP.NET Application의 결과물을 가상 컴퓨터로 이동합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2) 가상 컴퓨터에 Dockerfile을 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649514&quot; class=&quot;clean&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# windowsservercore-ltsc2022 이미지에 ASP.NET 프로그램을 실행할 수 있도록 가공된 이미지를 받아옵니다.
## 설치된 내용
### Windows Server Core as the base OS
### IIS 10 as Web Server
### .NET Framework (multiple versions available)
### .NET Extensibility for IIS
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022

# IIS의 기본 웹 루트 폴더를(/inetpub/wwwroot) 작업 디렉토리로 설정합니다.
WORKDIR /inetpub/wwwroot

# ASP.NET Application 의 게시 결과물을 현재 디렉토리에 복제합니다.
COPY ./{ASP.NET Application 의 게시 결과물이 존재하는 폴더}/ .

# IIS 기본 포트(80번)를 외부로 노출합니다.
EXPOSE 80&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) Dockerfile을 사용해서 빌드합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649515&quot; class=&quot;applescript&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker build -t my-aspnet-app .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/97yrB/btsLeNabS8b/Em2fXCv7jaSecV8gIXKiZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/97yrB/btsLeNabS8b/Em2fXCv7jaSecV8gIXKiZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/97yrB/btsLeNabS8b/Em2fXCv7jaSecV8gIXKiZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F97yrB%2FbtsLeNabS8b%2FEm2fXCv7jaSecV8gIXKiZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;259&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxouV/btsLfOMKLn1/YoBOv7drmIrM3Wtr8OATv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxouV/btsLfOMKLn1/YoBOv7drmIrM3Wtr8OATv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxouV/btsLfOMKLn1/YoBOv7drmIrM3Wtr8OATv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmxouV%2FbtsLfOMKLn1%2FYoBOv7drmIrM3Wtr8OATv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;51&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4) 빌드된 이미지를 실행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1733902649515&quot; class=&quot;angelscript&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -p 80:80 -d my-aspnet-app:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUXMGt/btsLefyjbGV/SeRSHaykuzEvVIXuh930ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUXMGt/btsLefyjbGV/SeRSHaykuzEvVIXuh930ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUXMGt/btsLefyjbGV/SeRSHaykuzEvVIXuh930ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUXMGt%2FbtsLefyjbGV%2FSeRSHaykuzEvVIXuh930ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;916&quot; height=&quot;114&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5) 80 포트로 정상 접근 확인합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CYRgn/btsLfGuDXvl/wmJIjXR5c4qmqmSjzi8gS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CYRgn/btsLfGuDXvl/wmJIjXR5c4qmqmSjzi8gS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CYRgn/btsLfGuDXvl/wmJIjXR5c4qmqmSjzi8gS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCYRgn%2FbtsLfGuDXvl%2FwmJIjXR5c4qmqmSjzi8gS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1188&quot; height=&quot;692&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows Container의 경우 Linux Container에 비해서는 아직 부족한 점이 있지만&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;무언가의 이유로 인해 Windows에서 벗어날 수 없는 경우에는 Windows OS를 사용하면서 Container 환경으로 갈 수 있는 좋은 방법 중 하나가 될 수 있을 것으로 보입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예제에서는 간단하게 설명하기 위해 Dockerfile을 간소화했지만,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows COM 모듈 설치나, IIS 설정, Windows registry 설정 등&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Application 실행을 위해 필요한 작업을 미리 Dockerfile 내부에서 정의해 놓을 수 있다면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows 개발 / 운영 환경 구축 및 관리에 필요한 리소스를 크게 단축할 수 있을 것으로 보입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Windows 개발자 분들께 도움이 되길 바라며 이만 글을 마치겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-2022%2Cwindows-11&quot;&gt;https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/version-compatibility?tabs=windows-server-2022%2Cwindows-11&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/containerd&quot;&gt;https://learn.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/containerd&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/set-up-environment?tabs=dockerce&quot;&gt;https://learn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/set-up-environment?tabs=dockerce&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/building-sample-app&quot;&gt;https://learn.microsoft.com/en-us/virtualization/windowscontainers/quick-start/building-sample-app&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://hub.docker.com/r/microsoft/windows&quot;&gt;https://hub.docker.com/r/microsoft/windows&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/archive/msdn-magazine/2017/april/containers-bringing-docker-to-windows-developers-with-windows-server-containers&quot;&gt;https://learn.microsoft.com/en-us/archive/msdn-magazine/2017/april/containers-bringing-docker-to-windows-developers-with-windows-server-containers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Infra</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/117</guid>
      <comments>https://dev.gmarket.com/117#entry117comment</comments>
      <pubDate>Wed, 11 Dec 2024 16:40:49 +0900</pubDate>
    </item>
    <item>
      <title>Redis Vs Mongo DB By Item View Count (이 상품 몇명이 보고 있어요)</title>
      <link>https://dev.gmarket.com/116</link>
      <description>&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요 저는 VI Engineering 팀 김윤제입니다.&lt;br /&gt;Gmarket Mobile Web Vip(View Item Page = 상품 상세)를 담당하고 있는 Backend Engineer 입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이번 블로그에서는 개인적으로 상품 상세 페이지에 넣고 싶었던&lt;br /&gt;현재 이 상품 몇 명이 보고 있어요 기능을 혼자 공부하며 개발해보는데 있어서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;어떻게 설계를 해야 최적의 성능을 낼 수 있을지 고민하였고 그 과정을 설명드리려고 합니다.&lt;br /&gt;자세한 내용은 아래에서 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h1 style=&quot;color: #000000; text-align: justify;&quot;&gt;동작 과정&lt;/h1&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;요구사항은 다음과 같았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상품 별로 중복되지 않은 사용자가 몇 명이 보고 있는지 실시간으로 집계하여 보여준다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;현재 이 상품 몇 명이 보고 있어요 기능의 동작 과정은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 웹 또는 앱을 통하여 상품 상세 페이지에 접속한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;상품 상세 서버에서는 상품 번호와 사용자 인식 정보를 데이터베이스에 저장한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;상품 상세 서버에서는 데이터베이스에서 해당 상품번호에 몇 명의 사용자가 있는지 검색한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;데이터베이스에서 검색한 사용자 수를 현재 사용자에게 반환한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;접속 중인 사용자 (웹 또는 앱)는 주기적으로 상품 상세 서버에 현재 몇명이 이 상품을 보고 있는지 요청한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;사용자가 이탈 (상품을 벗어남)하면 데이터베이스에서 해당 상품번호에 저장된 해당 사용자 인식 정보를 제거합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;5번에서 주기적으로 상품 상세 서버에 요청을 해야 하는데 웹 / 앱이 동일한 방법을 써야 관리하기가 쉬울 것 같다는 점에서&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;웹소켓 / 소켓이 아닌 API 요청을 받는 게 좋다고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;실시간으로 쏟아지는 트래픽 속에서 집계를 해야 하는 상황이라 RDB로는 성능이 안 나올 것 같아 NoSql 데이터베이스를 선택했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제 Nosql 데이터베이스 중 Redis와 MongoDb 사이에서 선택해야 했는데요.&lt;br /&gt;아래에서 각 데이터베이스 별로 장단점을 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h1 style=&quot;color: #000000; text-align: justify;&quot;&gt;Redis&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4ysNP/btsJMiVPfdX/Kfvrvym03FfWMX7HJ5GKH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4ysNP/btsJMiVPfdX/Kfvrvym03FfWMX7HJ5GKH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4ysNP/btsJMiVPfdX/Kfvrvym03FfWMX7HJ5GKH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4ysNP%2FbtsJMiVPfdX%2FKfvrvym03FfWMX7HJ5GKH0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;633&quot; height=&quot;234&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1727242575306&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Redis란?
Redis는 Remote Dictionary Server의 약자로 키(Key) - 값(Value) 쌍의 해시 맵과 같은 구조를 가진 
비관계형(NoSQL) 데이터베이스 관리 시스템(DBMS)입니다.
Redis는 오픈 소스 기반으로인-메모리(In-memory) 데이터 구조 저장소로 메모리에 데이터를 저장합니다.

따라서 별도의 쿼리문이 필요로 하지 않고, 인-메모리에 저장되기 때문에 
상당히 빠른 속도로 처리할 수 있습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727242575306&quot; class=&quot;erlang&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Redis의 특징 및 장단점

1. 성능
모든 Redis 데이터는 메모리에 저장되어 대기 시간을 낮추고 처리량을 높입니다.
평균적으로 읽기 및 쓰기의 작업 속도가 1ms로 디스크 기반 데이터베이스보다 빠릅니다.

2. 유연한 데이터 구조
Redis의 데이터는 String, List, Set, Hash, Sorted Set, Bitmap, JSON 등
다양한 데이터 타입을 지원합니다.
따라서, 애플리케이션의 요구 사항에 알맞은 다양한 데이터 타입을 활용할 수 있습니다.

3. 개발 용이성
Redis는 쿼리문이 필요로 하지 않으며, 단순한 명령 구조로 데이터의 저장, 조회 등이 가능합니다.
또한, Java, Python, C, C++, C#, JavaScript, PHP, Node.js, Ruby 등을 비롯한 
다수의 언어를 지원합니다.

4. 영속성
Redis는 영속성을 보장하기 위해 데이터를 디스크에 저장할 수 있다. 
서버에 치명적인 문제가 발생하더라도 디스크에 저장된 데이터를 통해 복구가 가능합니다.

5. 싱글 스레드 방식
Redis는 싱글 스레드 방식을 사용하여 한 번에 하나의 명령어만을 처리합니다. 
따라서 연산을 원자적으로 처리하여 Race Condition(경쟁 상태)가 거의 발생하지 않습니다.
하지만, 멀티 스레드를 지원하지 않기 때문에 
시간 복잡도가 O(n)인 명령어의 사용은 주의해서 사용해야 합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Item View Count (지금 이 상품 몇 명이 보고 있어요) 기능은 실시간 집계가 필요하며 성능적으로도 빨라야 해서&lt;br /&gt;고속의 인 메모리 기반의 데이터베이스 레디스를 생각하게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 레디스로 구현을 해당 기능을 올바르게 구현하기 위해서는 자료구조를 어떤 것을 선택하는지가 중요합니다.&lt;br /&gt;&lt;/b&gt;아래에서 레디스에서 자료구조 선택하는 데 있어서 했던 고민들을 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;자료 구조 선택의 과정&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Set (1)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;가정:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;제일 먼저 생각했던 방법입니다. (굉장히 간단할 것이라고 생각했습니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Set 자료구조에 Key로 상품번호를 넣고, Value에 사용자 인식 정보를 넣는다면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ex (key = &quot;123456789&quot;, value=&quot;rlanwl&quot;)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;사용자는 중복되지 않을 것이고, Insert, Select 시간 복잡도 O(1) 성능 이슈 X&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;앱은 라이프 사이클에 따라 웹은 브라우저 종료 감지로 사용자 이탈 시 제거하면 럭키 비키합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;현실:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;파이어 폭스 같은 브라우저는 자바스크립트의 종료 감지 이벤트가 안될 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 이슈로 종료 감지를 못하면?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Set에 저장된 데이터는 영영 삭제되지 않는 문제가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;유감.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Set (2)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 방식으로 저장하되 각 키에 ExpireTime (60초)를 설정&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그럴 경우 만약 1번 상품에 대하여 60초 동안 아무도 안 본다면 1번 상품을 레디스에서 삭제.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;레디스에서 삭제가 되려면 60초 동안 각 상품을 안 봐야 하는데... 새벽에나 가능하지 않을까&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한 최대한 사용자 수를 동기화를 해야 하는데 너무 오차가 크다는 문제가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;유감.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Set X Hash&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;가정:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Set 자료구조에 Key로 상품번호를 넣고, Value에 사용자 인식 정보를 넣고..&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Hash에는 모든 상품과 사용자 정보, 현재 시간을 넣어서 별도의 Batch을 통해 이탈 감지 실패한 케이스들을 삭제합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Insert, Select 시간복잡도 O(1)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;현실:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Hash에 저장된 데이터를 Batch 을 통해 지울 경우 모든 상품을 검사하며&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;저장된 모든 유저들의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;저장시간을 비교해야 하는데&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;해시에 저장된 모든 키 조회로 인한 레디스 부하 -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;O(N)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(Hash의 필드 수)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;키 리스트를 순회하며 유저 조회 후 날짜 비교 어플리케이션 부하&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Sorted Set&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;가정:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Sorted Set에는 Key, Value, Score을 저장할 수 있으니&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Key에는 상품번호, Value에는 사용자 인식 정보, Score에는 현재 시간을 저장합니다. -&amp;gt; 시간 복잡도 O(log n)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;중복된 Value를 허용하지 않으며 동일한 데이터 입력 시 Score를 업데이트할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;현재 상품을 몇 명이 보고 있는지는 해당 Key에 저장된 길이를 구하면 됩니다. -&amp;gt; 시간 복잡도 O(1)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한 Sorted Set에는 ZREMRANGEBYSCORE라는 명령어가 있는데 이 것을 활용하면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;일정 시간 동안 Score가 업데이트되지 않은 사용자 정보를 제거 할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;현실:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;가장 이상적인 자료구조 조합이지만&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Batch를 돌려서 일정 시간 동안 Score가 업데이트 되지 않은 사용자 정보를 제거해야 하는데&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SortedSet에 저장된 모든 키를 다 찾아야 합니다. =&amp;gt; ZRANGE 시간 복잡도 O(log n + m)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그 키 별로 ZREMRANGEBYSCORE 명령어를 실행해야 하기 때문에 =&amp;gt; 시간 복잡도 O(log n + m)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;총 시간 복잡도는 O(log n + m) + 상품 수 * O(log n + m)으로 레디스의 부하가 예상되나&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;SortedSet에 저장된 상품별 사용자 정보에서 얼마나 많은 수의 이탈 감지 실패가 발생했는지가 관건&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Batch도 언제 돌릴지가 관건이었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;(하지만 이탈 감지 실패는 그렇게 많지 않을 것으로 보이기 때문에 이 자료구조를 선택하였습니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;위 Set&amp;nbsp; X Hash 구조와 성능적으로는 크게 차이가 나지 않지만 확실히 SortedSet 자료구조가 가져다주는 편리한 이점이 있어서 해당 기능을 사용하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h1 style=&quot;color: #000000; text-align: justify;&quot;&gt;MongoDB&lt;/h1&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 몽고 DB에서는 어떨지 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAc7zQ/btsJLtqwSSS/xQV88wvi1TmeMGiNLc5CBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAc7zQ/btsJLtqwSSS/xQV88wvi1TmeMGiNLc5CBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAc7zQ/btsJLtqwSSS/xQV88wvi1TmeMGiNLc5CBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAc7zQ%2FbtsJLtqwSSS%2FxQV88wvi1TmeMGiNLc5CBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;342&quot; height=&quot;100&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1727242575310&quot; class=&quot;mipsasm&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;MongoDB는 고성능, 고가용성 및 쉬운 확장성을 제공하는 NoSQL, Document 지향 데이터베이스입니다.

데이터를 배열 및 중첩 Document와 같은 복잡한 데이터 유형을 효율적으로 저장할 수 있는

유연한 JSON과 유사한 형식인 BSON(Binary JSON)으로 저장합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727242575310&quot; class=&quot;erlang&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;몽고DB의 주요 특징으로는 스키마리스 구조, 고성능, 고가용성, 확장성 등이 있습니다.
이러한 특징들은 개발자가 더 빠르고 유연하게 애플리케이션을 개발할 수 있게 돕습니다.

왜냐하면 몽고DB는 동적 스키마를 지원하기 때문에, 애플리케이션의 데이터 구조가 변경되어도
데이터베이스를 수정할 필요가 없습니다.
이는 개발 시간을 단축시키는 큰 이점입니다.

또한, 몽고DB는 내장된 샤딩 기능을 통해 데이터베이스의 수평 확장이 용이하며,
이는 대규모 데이터베이스 관리에 필수적인 기능입니다.

고가용성을 위한 복제 세트와 자동 장애 복구 기능은 몽고DB를 더욱 신뢰할 수 있는
데이터 저장소로 만듭니다.

왜냐하면 이러한 기능들은 데이터의 안정성과 서비스의 지속 가능성을 보장하기 때문입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;몽고 디비에서는 아래와 같이 2가지 설계를 생각해 보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째, 다음과 같이 컬렉션 구조를 가져갔습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;userProductView&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;{
    itemNo: &quot;1&quot;,
    loginId: &quot;abcd&quot;,
    timestamp: &quot;2024-09-17T04:06:19.474+00:00&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Flow는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;userProductView -&amp;gt; 상품 별로 사용자 인식 정보와 현재 시간을 upsert한다. -&amp;gt; 시간 복잡도 O(log n)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;위 userProductView에 저장된 상품 번호 별 document 수를 구한다. -&amp;gt; 시간 복잡도 O(n)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;위 2번에서 구한 상품 별 ViewCount를 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이 구조는 생각보다 레디스보다 간단해 보이지만 숨은 내용이 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;검색 속도 개선을 위한 인덱스 설정&lt;br /&gt;몽고 디비를 사용할 경우 userProductView 컬렉션에 itemNo(상품 번호) 필드에 index를 걸어야 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;사용자 수 동기화를 위한 timestamp에 expire를 설정&lt;br /&gt;몽고 디비를 사용할 경우 각 도큐먼트 별로 expire를 설정할 수 있기 때문에&lt;br /&gt;레디스처럼 별도의 Job을 구성하여 이탈 감지 실패 케이스들에 대하여 검사를 할 필요가 없습니다.&lt;br /&gt;하지만 정확한 만료 시간 보장이 어렵다는 단점이 있으며, 성능 저하가 있을 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째, 다음과 같이 하나의 컬렉션 구조를 가져갑니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;{
    itemNo: &quot;1&quot;,
    &quot;users&quot;: [
        {
            loginId: &quot;abcd&quot;,
            timestamp: &quot;2024-09-17T04:06:19.474+00:00&quot;
        }    
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;동작 Flow는 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상품 조회 =&amp;gt; 인덱스가 있는 경우 O(log n) 또는 인덱스가 없는 경우 O(n)&lt;/b&gt;&lt;br /&gt;사용자가 상품을 조회할 때, 해당 itemNo에 대한 Document를 찾아야 합니다.&lt;br /&gt;사용자 정보를 users 배열에 추가하여 timestamp를 갱신합니다.&lt;br /&gt;해당 itemNo에 expire 설정&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;상품에 속한 users 배열을 순회하며 특정 시간 기준으로 활동하지 않은 user라 판단하는 비교 로직을 수행해야 하므로, 배열의 크기에 따라 O(n)시간이 걸립니다.&lt;/li&gt;
&lt;li&gt;배열에 사용자 추가 =&amp;gt; O(1)&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 이 방식에도 문제가 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;상품 번호에 expire가 설정되어 있기 때문에 실제로 자주 보는 상품이라면 삭제가 되지 않을 수 있어서 데이터의 양이 엄청나게 커질 수 있습니다. 그럴 경우 상품 전체 조회 O(n) + user 배열 순회 O(n) + 배열 삭제 및 업데이트 O(n)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;이에 따라 또다시 Batch Job을 구성해 삭제해야 하는...&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h1 style=&quot;color: #000000; text-align: justify;&quot;&gt;어떤 게 더 좋은 거지?&lt;/h1&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;레디스와 몽고 DB를 전체 Flow에 시간복잡도를 비교해 보았을 때 테이블로 보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cL8bF2/btsJLWMwd34/YNi5DhChrtNuc1ehq4QX9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cL8bF2/btsJLWMwd34/YNi5DhChrtNuc1ehq4QX9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cL8bF2/btsJLWMwd34/YNi5DhChrtNuc1ehq4QX9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcL8bF2%2FbtsJLWMwd34%2FYNi5DhChrtNuc1ehq4QX9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1002&quot; height=&quot;619&quot; data-origin-width=&quot;1002&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMRCIE/btsJLVz505m/fkQokKqVhgUbyyBlaMfAe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMRCIE/btsJLVz505m/fkQokKqVhgUbyyBlaMfAe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMRCIE/btsJLVz505m/fkQokKqVhgUbyyBlaMfAe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMRCIE%2FbtsJLVz505m%2FfkQokKqVhgUbyyBlaMfAe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;976&quot; height=&quot;393&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;해당 기능을 구현하기에 있어서 실시간이라는 점, 여러 도메인에 조회 기능을 제공해야 한다는 점에서 보면 성능적으로 레디스가 나을 듯합니다.&lt;br /&gt;하지만 레디스는 가격이 비싸다는 가장 큰 문제가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;복잡성과 유지보수면에서는 Batch Job이 없는 몽고 DB가 더 나아 보이지만,&lt;br /&gt;몽고 DB는 Index 설정, Expire 설정 등 따져야 할 게 몇 가지 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;각 데이터베이스별로 장단점이 있어서 어떤 것을 사용하는 게 좋을지는 면밀히 검토해봐야 할 듯합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h1 style=&quot;color: #000000; text-align: justify;&quot;&gt;끝으로&lt;/h1&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 간단할 것이라 판단하였지만 생각보다 따져볼 것이 많아 복잡했습니다.&lt;br /&gt;만들고 싶었던 기능을 이것저것 따져가며 여러 데이터베이스와 비교를 해보는 것도 꽤 재미있는 경험이 되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이제는 주니어보단 시니어 개발자에 가까운 연차가 되어가네요.&lt;br /&gt;꾸준히 노력하여 부끄럽지 않은 개발자가 되도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;</description>
      <category>Infra</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/116</guid>
      <comments>https://dev.gmarket.com/116#entry116comment</comments>
      <pubDate>Wed, 25 Sep 2024 14:38:20 +0900</pubDate>
    </item>
    <item>
      <title>오픈마켓 여행 플랫폼의 실전 API 연동 노하우</title>
      <link>https://dev.gmarket.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. Vertical Engineering 팀의 이지민입니다.&lt;br /&gt;지마켓의 여행 플랫폼은 작년에 포스팅한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://dev.gmarket.com/91&quot;&gt;오픈마켓에서 여행 플랫폼으로 살아남기&lt;/a&gt;에서 소개드린 바 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행 플랫폼은 지마켓의 커머스 시스템과 OTA(온라인 여행사 제휴업체) 서버의 API를 통합하여 서비스를 제공합니다. 이처럼 여러 서버와 의존성이 높아, 서비스의 복잡도가 높습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히, 실시간 API 연동을 요구하여, 견고하게 API를 호출해야 하는 비즈니스 도메인은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;여행 상품 상세 페이지&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;실시간 예약&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 여행 상품 상세 페이지와 실시간 예약의 주요 특징과 고려 사항에 대해 설명하고자 합니다.&lt;/p&gt;
&lt;h1 style=&quot;color: #333333; text-align: start;&quot;&gt;(1) 여행 상품 상세 페이지&lt;/h1&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행 상품 상세 페이지는 내결함성(fault tolerance)이 높아야 하는 화면입니다. 이러한 요건이 생긴 배경과 개발 고려 사항은 다음과 같습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행 상품 상세 페이지는 지마켓 여행 플랫폼, 지마켓 커머스 시스템, OTA (온라인 여행사 제휴업체) 서버의 API를 조합하여 기능을 제공합니다. 한 화면에서 사용하는 API의 수만 10개 이상이고, 이를 책임지는 팀과 회사도 다양합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음은 상품 상세 페이지를 구성하기 위해 관여하는 서버를 추상화한 그림입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1_연계시스템.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2DO5W/btsIKio9coq/Ia6SB9ej4jRupQSKMBh151/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2DO5W/btsIKio9coq/Ia6SB9ej4jRupQSKMBh151/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2DO5W/btsIKio9coq/Ia6SB9ej4jRupQSKMBh151/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2DO5W%2FbtsIKio9coq%2FIa6SB9ej4jRupQSKMBh151%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;750&quot; data-filename=&quot;1_연계시스템.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행플랫폼의 API에서 문제가 발생할 경우, 여행팀 내부에서 신속하게 대응할 수 있습니다. 반면, 외부에서 제공받은 API는 직접 제어하기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특히 일부 OTA (온라인 여행사 제휴업체)는 타사의 API를 직접 연동하기도 합니다. 이 경우, API 문제 발생 시, 대응이 더 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하나의 상품 상세 페이지를 완성하는 과정은 여러 팀과 회사의 서버가 협력하는 복잡한 작업입니다. 그래서 다수의 API가 항상 완벽하게 동작하기를 기대하기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 특징으로 여행 상품 상세 페이지는 내결함성(fault tolerance)이 높은 시스템을 만들어야 한다는 미션이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;fault&amp;nbsp;tolerance:&amp;nbsp;시스템을&amp;nbsp;구성하는&amp;nbsp;부품의&amp;nbsp;일부에서&amp;nbsp;결함(fault)&amp;nbsp;또는&amp;nbsp;고장(failure)이&amp;nbsp;발생하여도&amp;nbsp;정상적&amp;nbsp;혹은&amp;nbsp;부분적으로&amp;nbsp;기능을&amp;nbsp;수행할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;시스템&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;API 연동 고려 사항&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(a) 타임아웃 시간을 case by case으로 설정하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일부 API의 응답 지연이 여행 상품 상세 페이지에 영향을 미치지 않게 하려면, 타임아웃을 적절하게 설정해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 두 가지 특징을 고려하여, API 별로 적절한 타&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;임아웃을 설정해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;중요도&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;평균 응답시간&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중요도가 낮은 API는타임아웃을 적절하게 설정하여, 응답 지연이 전체로 전파되지 않도록 해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면, 응답시간이 오래 걸리더라도 중요도가 높은 API은 최대한 기다려야 합니다. 대표적인 예로 가격과 재고 조회 API가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행 도메인에서 모든 실가격과 재고를 데이터베이스에 적재하는 것은 어렵습니다. 가격과 재고는 &amp;ldquo;상품 x 옵션 x 연령별 요금제 x 날짜 x 시간&amp;rdquo; 등등의 조합을 통해 결정됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모든 조합을 사전에 저장해두는 것은 많은 비용이 들기 때문에 실시간으로 조회하여 연산해야 합니다. 이 경우, 오랜 시간 기다려야 합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(b) fallback: 결함이 발생하지 않은 척하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;특정 API에서 오류가 발생하는 경우, 일시적으로 기능을 제공하지 않는 방법으로 내결함성을 높일 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 할인된 가격을 다루는 API에서 이슈가 발생하는 경우, 원가를 노출하는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2_fallback.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HJ6mV/btsIHE1XUYz/6Jm5Brxtk6vOv7iOZwnQ6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HJ6mV/btsIHE1XUYz/6Jm5Brxtk6vOv7iOZwnQ6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HJ6mV/btsIHE1XUYz/6Jm5Brxtk6vOv7iOZwnQ6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHJ6mV%2FbtsIHE1XUYz%2F6Jm5Brxtk6vOv7iOZwnQ6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;284&quot; data-filename=&quot;2_fallback.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://resilience4j.readme.io/&quot;&gt;reslience4j&lt;/a&gt;을 사용하면 api 에러에 대한 fallback처리를 손쉽게 할 수 있습니다.&lt;br /&gt;다음은 CircuitBreaker에 fallbackMethod를 적용한 샘플코드입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@CircuitBreaker(name = &quot;discount&quot;, fallbackMethod = &quot;getDiscountPriceFallback&quot;)
public DiscountPrice getDiscountPrice(String productId, BigDecimal originPrice)
    ...
}

public DiscountPrice getDiscountPriceFallback(String productId, BigDecimal originPrice, Throwable t) {
    return DiscountPrice.builder()
    .price(originPrice)
    .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1 style=&quot;color: #333333; text-align: start;&quot;&gt;(2) 실시간 예약&lt;/h1&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실시간 예약 처리는 최종 일관성과 멱등성을 보장해야 합니다. 이러한 요건이 생긴 배경과 개발 고려 사항은 다음과 같습니다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;고객의 결제가 승인되면, 예약을 확정하는 &amp;ldquo;실시간 예약&amp;rdquo;이 이루어집니다. &amp;ldquo;실시간 예약&amp;rdquo;은 지마켓 예약 플랫폼, 커머스 시스템, OTA (온라인 여행사 제휴업체)가 실예약을 계약하는 과정입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 과정이 중요한 이유는 크게 2가지입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;금전 거래와 관련 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;고객은 비용을 지불하고, 판매자는 대금에 대한 정산을 받는다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;실시간 예약이 완료되는 시점부터 취소 위약금이 부과될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;고객이 예약한 서비스를 확실하게 보장해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;만약 연동이 원활하지 않으면, 고객은 여행지에서 예약한 서비스를 제대로 제공받지 못하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;각 시스템은 자신만의 고유한 상태를 지닙니다. 그리고 각 상태는 결제, 대금정산, 여행 서비스 제공 등의 기반 데이터로 사용됩니다. 그래서 세 개의 시스템은 일관된 상태로 동기화되어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3_시스템과_상태.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwbtLy/btsIJxN4rYq/opDxBGvgFBjKqifLryNB90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwbtLy/btsIJxN4rYq/opDxBGvgFBjKqifLryNB90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwbtLy/btsIJxN4rYq/opDxBGvgFBjKqifLryNB90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwbtLy%2FbtsIJxN4rYq%2FopDxBGvgFBjKqifLryNB90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;321&quot; data-filename=&quot;3_시스템과_상태.png&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 특징에 의하여, 두 가지 비기능요건이 요구됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;최종일관성 (eventual consistency)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;일시적으로 상태가 어긋나더라도, 결국 동일한 단계의 상태로 동기화가 되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;멱등성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;동일한 연산을 여러 번 수행해도 결과가 동일해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;API 연동 고려 사항&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(a) 상태머신 (state machine) 만들기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상태머신을 만들면, 복잡한 상태 연동 과정이 단순해집니다. 그리고 상태 동기화 함수의 재사용성도 높아집니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상태머신을 설계할 때에는 다음 두 가지가 사전에 정의되어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;각 시스템 별 상태 정의하기&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;시스템 간 동일한 단계 매칭하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 그림은 여행 플랫폼, 지마켓 커머스 시스템, 제휴사 시스템의 각 예약과 주문 상태에 대해 정의한 상태 다이어그램입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4_상태다이어그램.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf1zEK/btsIJwBFvms/P4R4l0VELsNnLfxTmUkobK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf1zEK/btsIJwBFvms/P4R4l0VELsNnLfxTmUkobK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf1zEK/btsIJwBFvms/P4R4l0VELsNnLfxTmUkobK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf1zEK%2FbtsIJwBFvms%2FP4R4l0VELsNnLfxTmUkobK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;310&quot; data-filename=&quot;4_상태다이어그램.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행플랫폼에서는 이처럼 상태의 흐름과 규칙을 사전에 정의한 후, 상태를 전이시키는 상태머신을 구현하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(b) 대사배치 만들기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일시적인 장애로 인하여, 상태 연동 처리에 실패하는 경우가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 각 시스템의 예약 상태를 확인하고 재연동하는 대사 작업을 통해 이를 보강할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;오케스트레이터 역할을 하는 배치 프로그램이 각 시스템의 상태를 확인합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 상태가 어긋난 경우, 상태머신을 통해 상태를 재연동합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5_상태배치.png&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTBlJa/btsIKe1k2TI/5nvIlJDWdAOur0C4uYtqM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTBlJa/btsIKe1k2TI/5nvIlJDWdAOur0C4uYtqM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTBlJa/btsIKe1k2TI/5nvIlJDWdAOur0C4uYtqM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTBlJa%2FbtsIKe1k2TI%2F5nvIlJDWdAOur0C4uYtqM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1099&quot; height=&quot;343&quot; data-filename=&quot;5_상태배치.png&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 상태가 일시적으로 어긋나더라도, 결국 일치하게 되는 최종일관성을 얻을 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(c) 멱등성을 고려하여, 재시도하기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예약 상태 연동 과정에서 API 호출 에러가 발생하는 경우, 재시도로 문제를 해결할 수 있습니다. 그러나 예약 플랫폼에서는 에러응답을 받았으나, 연계 시스템에서는 정상적으로 접수된 상태일 수도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, 타임아웃 및 네트워크 오류에 의하여 클라이언트는 에러가 났지만, 서버의 트랜잭션은 정상 종료가 된 경우가 그러합니다. 이 경우, API 호출을 재시도하면 연계 시스템 입장에서는 중복 요청이 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중복 요청 시, 연계시스템에서 에러를 리턴하는 경우가 있습니다. 이때 예약 플랫폼은 요청이 정상으로 접수된 것으로 인지할 수 있게 예외처리를 해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;(c-2) 중복 요청 시, 명시적인 에러코드가 반환되는 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;6_중복에러처리_에러코드.png&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2sWj/btsIJTXqR7R/qlDhAM6KhyVqwouRzmsm2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2sWj/btsIJTXqR7R/qlDhAM6KhyVqwouRzmsm2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2sWj/btsIJTXqR7R/qlDhAM6KhyVqwouRzmsm2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2sWj%2FbtsIJTXqR7R%2FqlDhAM6KhyVqwouRzmsm2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;249&quot; data-filename=&quot;6_중복에러처리_에러코드.png&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중복 요청 에러 코드가 내려오는 경우는 해피한 케이스입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;해당 에러 코드가 내려오면 예약 플랫폼은 이를 성공으로 간주합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;(c-2) 명시적인 에러코드는 없지만, 상태 조회 API를 제공하는 경우&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;7_중복에러처리_상태조회.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kirhl/btsIJHJHamb/PBSbuqeNRODdGfiw0fHwKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kirhl/btsIJHJHamb/PBSbuqeNRODdGfiw0fHwKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kirhl/btsIJHJHamb/PBSbuqeNRODdGfiw0fHwKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKirhl%2FbtsIJHJHamb%2FPBSbuqeNRODdGfiw0fHwKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;229&quot; data-filename=&quot;7_중복에러처리_상태조회.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;때로는 중복요청으로 에러가 발생하더라도, 에러코드로 이를 식별할 수 없는 경우가 있습니다. 이 경우, 연계 시스템의 상태 조회 API를 활용하여 중복 호출 가능 여부를 판단할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;때로는 연계 시스템의 상태가 다음 상태로 전이될 때까지 기다려야 하는 경우도 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때에는&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.baeldung.com/spring-retry&quot;&gt;spring-retry&lt;/a&gt;의 exception, backoff 옵션을 사용하여, 일정시간 대기 이후 재시도를 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #333333; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Override
@Retryable(value = NotReadyReservationException.class, backoff = @Backoff(delay = 5000), maxAttempts = 3)
public void reserve(String id) {
    if(!isReservationReady(id)){
        throw new NotReadyReservationException(id);
    }
    ...    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(c) 보상 트랜잭션: 재시도로 해결할 수 없다면 깔끔하게 놓아주자.&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;재시도로 해결할 수 없는 문제도 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;중복 호출 횟수만큼 실 데이터가 중복으로 접수되는 경우&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;배치에서 일정 기간 동안 재시도를 했음에도 해결되지 않는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우, 보상 트랜잭션을 통해 데이터를 원상태로 돌려주어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8_보상트랜잭션_응답처리.png&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byzgri/btsIJCuRtop/6DU27QEsqfXwFG1iDrbNd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byzgri/btsIJCuRtop/6DU27QEsqfXwFG1iDrbNd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byzgri/btsIJCuRtop/6DU27QEsqfXwFG1iDrbNd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbyzgri%2FbtsIJCuRtop%2F6DU27QEsqfXwFG1iDrbNd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;248&quot; data-filename=&quot;8_보상트랜잭션_응답처리.png&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;422&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보상 트랜잭션을 매끄럽게 처리하려면, 롤백을 하기 쉬운 순서대로 외부 시스템을 연동해야 합니다. 제어하기 어렵고, 금전 거래와 관련된 트랜잭션은 가장 나중에 호출하는 것이 좋습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어, OTA (온라인 여행 제휴업체)와 실예약을 계약하는 시점부터 취소가 불가능하거나 취소위약금이 부과되는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;9_취소환불규정사례.png&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baRxE3/btsIJ96NsjS/5463LGEo6PpK5HfDkhXBoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baRxE3/btsIJ96NsjS/5463LGEo6PpK5HfDkhXBoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baRxE3/btsIJ96NsjS/5463LGEo6PpK5HfDkhXBoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaRxE3%2FbtsIJ96NsjS%2F5463LGEo6PpK5HfDkhXBoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1848&quot; height=&quot;498&quot; data-filename=&quot;9_취소환불규정사례.png&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 OTA (온라인 여행 제휴업체)와 실예약을 한 이후, 지마켓 내부 시스템 연동에 실패한다면 상황을 되돌리기 어렵습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;10_보상트랜잭션_워스트케이스.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhj6od/btsIJEe8HiJ/GOTt9HeaynifdNdq88T9K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhj6od/btsIJEe8HiJ/GOTt9HeaynifdNdq88T9K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhj6od/btsIJEe8HiJ/GOTt9HeaynifdNdq88T9K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdhj6od%2FbtsIJEe8HiJ%2FGOTt9HeaynifdNdq88T9K0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;263&quot; data-filename=&quot;10_보상트랜잭션_워스트케이스.png&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이 경우, 상대적으로 대응이 쉬운 시스템을 먼저 연동하는 것이 좋습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 돌이킬 수 없는 요청은 최하단에 배치하는 것이 좋습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;11_보상트랜잭션_해피케이스.png&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvhFsj/btsII1BSenZ/fLTCOknkVxd4a8cn8k5a6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvhFsj/btsII1BSenZ/fLTCOknkVxd4a8cn8k5a6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvhFsj/btsII1BSenZ/fLTCOknkVxd4a8cn8k5a6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhFsj%2FbtsII1BSenZ%2FfLTCOknkVxd4a8cn8k5a6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;239&quot; data-filename=&quot;11_보상트랜잭션_해피케이스.png&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본 글을 통해 여러 시스템과 API를 연동하며, 고려했던 점들을 공유드렸습니다. 본 글이 API 연동 과정에서 어려움을 겪는 분들께 도움이 되었으면 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://item.gmarket.co.kr/Item?goodscode=2519411763&quot;&gt;[도서] 이벤트 기반 마이크로서비스 구축: 8장. 마이크로서비스 워크플로 구축&lt;/a&gt;&lt;br /&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/expedia-group-tech/four-key-differences-between-online-travel-and-online-shopping-e-commerce-8512cc47b0bf&quot;&gt;[아티클] Four Key Differences Between &amp;ldquo;Online Travel&amp;rdquo; and &amp;ldquo;Online Shopping&amp;rdquo; E-commerce&lt;/a&gt;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/115</guid>
      <comments>https://dev.gmarket.com/115#entry115comment</comments>
      <pubDate>Tue, 23 Jul 2024 10:59:22 +0900</pubDate>
    </item>
    <item>
      <title>jcenter, 이제 문 닫습니다</title>
      <link>https://dev.gmarket.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;안녕하세요. Mobile Application 팀 전계원입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;어느 날 Android 개발 중 jcenter 에 있는 경고 문구를 확인하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;이에 궁금증을 가지고 jcenter 와 관련하여 찾아본 이런저런 내용들에 대해 공유드리고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;0. 프롤로그 - jcenter() 코드에 그어진 한 개의 줄&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r5gBS/btsIBkvm8qz/ytcr4HUQftnKDykXscducK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r5gBS/btsIBkvm8qz/ytcr4HUQftnKDykXscducK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r5gBS/btsIBkvm8qz/ytcr4HUQftnKDykXscducK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr5gBS%2FbtsIBkvm8qz%2Fytcr4HUQftnKDykXscducK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;498&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;position: absolute;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Android Studio 의 다크테마 속에서 jcenter() 코드가 노랗게 반짝이고 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마우스를 올려보니 &quot;jcenter maven repository 는 update 를 제공하지 않는다&quot; 라고 쓰여있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;247&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btGX4C/btsICdCg66u/0OtLoqEH7KKyTb5741qTQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btGX4C/btsICdCg66u/0OtLoqEH7KKyTb5741qTQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btGX4C/btsICdCg66u/0OtLoqEH7KKyTb5741qTQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtGX4C%2FbtsICdCg66u%2F0OtLoqEH7KKyTb5741qTQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;267&quot; height=&quot;247&quot; data-filename=&quot;2.png&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;247&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;jcenter 에 대체 무슨 일이 있었던 것일까요?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. Gradle 저장소에서 사라진 jcenter 의 흔적&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mllRC/btsICT4embq/ZFRjseUFAVlUMXqrLxfi21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mllRC/btsICT4embq/ZFRjseUFAVlUMXqrLxfi21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mllRC/btsICT4embq/ZFRjseUFAVlUMXqrLxfi21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmllRC%2FbtsICT4embq%2FZFRjseUFAVlUMXqrLxfi21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;692&quot; data-filename=&quot;3.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://docs.gradle.org/current/userguide/declaring_repositories.html&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7fkEW/btsICgy0Aym/na3oF3DpR07FOUkNh7ql01/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/7fkEW/btsICgy0Aym/na3oF3DpR07FOUkNh7ql01/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/declaring_repositories.html&quot;&gt;https://docs.gradle.org/current/userguide/declaring_repositories.html&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러고 보니 gradle 공식문서를 살펴보면 과거에는 Gradle 저장소에는 mavenCentral(), jcenter(), google() 이렇게 3가지가 있다고 쓰여있었는데, 최근 문서를 확인해 보면 jcenter 는 어디론가 사라져 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_4.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;883&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZJrfe/btsICFrxYF4/f5rIqJ3zgW49eTWQ9kR5IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZJrfe/btsICFrxYF4/f5rIqJ3zgW49eTWQ9kR5IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZJrfe/btsICFrxYF4/f5rIqJ3zgW49eTWQ9kR5IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZJrfe%2FbtsICFrxYF4%2Ff5rIqJ3zgW49eTWQ9kR5IK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;883&quot; data-filename=&quot;edited_4.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;883&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://docs.gradle.org/current/userguide/declaring_repositories.html&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oJEqY/btsIA98E8VY/RLgsjzN0xCFQ7lFnRKA1m0/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/oJEqY/btsIA98E8VY/RLgsjzN0xCFQ7lFnRKA1m0/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/declaring_repositories.html&quot;&gt;https://docs.gradle.org/current/userguide/declaring_repositories.html&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 mavenCentral() 은 java 프로젝트에서의 오픈소스 라이브러리들을 받기 위한 저장소이며, google() 은 Android SDK 에 포함되어 있는 Android 라이브러리를 가져오기 위한 maven 저장소입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. 우리식당 정상 영업합니다&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;jcenter 는 bintray 에서 호스팅 하고 있는 Java 와 관련한 라이브러리와 패키지 등이 저장된 오픈소스 라이브러리 공개 저장소입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_5.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csFqw0/btsIBAEJZv5/8CRzx1xFT3GlVexRG1e831/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csFqw0/btsIBAEJZv5/8CRzx1xFT3GlVexRG1e831/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csFqw0/btsIBAEJZv5/8CRzx1xFT3GlVexRG1e831/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsFqw0%2FbtsIBAEJZv5%2F8CRzx1xFT3GlVexRG1e831%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;616&quot; data-filename=&quot;edited_5.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqqBDr/btsIBePwl2r/WFDWehgO7NYBVKxf1esXt1/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bqqBDr/btsIBePwl2r/WFDWehgO7NYBVKxf1esXt1/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot;&gt;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어느 날 bintray 에서 21년 2월에 jcenter 을 지원중단할 것이며, jcenter 사용자들은 새로운 호스팅 솔루션으로 migration 하기를 권하였고, 21년 4월부터는 jcenter 의 새로운 업데이트는 받지 않는다는 공식 포스트가 업로드되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_6.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOiaYg/btsICxG5Ar0/QEfzPFDZOetKfcqxNvd2V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOiaYg/btsICxG5Ar0/QEfzPFDZOetKfcqxNvd2V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOiaYg/btsICxG5Ar0/QEfzPFDZOetKfcqxNvd2V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOiaYg%2FbtsICxG5Ar0%2FQEfzPFDZOetKfcqxNvd2V1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;353&quot; data-filename=&quot;edited_6.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFu9iq/btsIDemJ5Hm/PkLMeorazXIEK7FQSd04n0/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bFu9iq/btsIDemJ5Hm/PkLMeorazXIEK7FQSd04n0/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot;&gt;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다행히 jcenter repository 가 지원중단되지만, 업데이트가 되지 않을 뿐 서버가 문을 닫는 것은 아니었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSWH1e/btsIBA5OIns/NNZ5XdXSeWute3pZU56OS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSWH1e/btsIBA5OIns/NNZ5XdXSeWute3pZU56OS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSWH1e/btsIBA5OIns/NNZ5XdXSeWute3pZU56OS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSWH1e%2FbtsIBA5OIns%2FNNZ5XdXSeWute3pZU56OS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1156&quot; height=&quot;746&quot; data-filename=&quot;7.png&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 우리는 지원 중단된 jcenter 를 여전히 사용할 수 있으며, 갑자기 빌드가 실패하는 문제를 겪지 않을 수 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. jcenter 문 닫습니다 (2024년 8월 15일 예정)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_8.png&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;2060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdCCyj/btsIAOqdJeP/89Bwak5mM8JJbT7rBLigeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdCCyj/btsIAOqdJeP/89Bwak5mM8JJbT7rBLigeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdCCyj/btsIAOqdJeP/89Bwak5mM8JJbT7rBLigeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdCCyj%2FbtsIAOqdJeP%2F89Bwak5mM8JJbT7rBLigeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1662&quot; height=&quot;2060&quot; data-filename=&quot;edited_8.png&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;2060&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkFtP3/btsICuDzMc1/JWfjYbDJdf9HcYcfFPouLk/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bkFtP3/btsICuDzMc1/JWfjYbDJdf9HcYcfFPouLk/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot;&gt;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2024년 7월 2일. binray 는 read-only 로 저장소를 유지한 지 3년이 지났으니 maven central 로 옮길 수 있도록 지원하기 위해 jcenter 를 완전히 종료하겠다는 공식 포스트를 업로드하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가로 갑자기 서버를 내리면 이를 참조하고 있던 수많은 프로젝트들이 혼돈에 겪을 것이기에 bintray 는 다음과 같은 Brownout Schedule 을 발표합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버 내림 일정 (한국시간 기준)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;7월 30일 16:00 ~ 17:00 (1시간)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;7월 31일 03:00 ~ 04:00 (1시간)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;8월 05일 16:00 ~ 20:00 (4시간)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;8월 06일 03:00 ~ 07:00 (4시간)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;8월 07일 22:00 ~ 8월 08일 22:00 (24시간)&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;8월 15일 09:00 ~ (문 닫습니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. jcenter 가 문을 닫으면 우리 같은 개발자들은 어떻게 하라는 겐가?&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_9.png&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFplvI/btsIDciaTRM/14afRyWOZO7vxUSiPZrgA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFplvI/btsIDciaTRM/14afRyWOZO7vxUSiPZrgA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFplvI/btsIDciaTRM/14afRyWOZO7vxUSiPZrgA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFplvI%2FbtsIDciaTRM%2F14afRyWOZO7vxUSiPZrgA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1614&quot; height=&quot;564&quot; data-filename=&quot;edited_9.png&quot; data-origin-width=&quot;1614&quot; data-origin-height=&quot;564&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK47R0/btsIDdVGi7s/kOrMKHcd1uCpMQkZu4PZnk/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/bK47R0/btsIDdVGi7s/kOrMKHcd1uCpMQkZu4PZnk/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot;&gt;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버가 문을 닫게 되면 jcenter 를 향한 모든 request 들은 자동으로 모두 maven central(&lt;a href=&quot;https://repo1.maven.org/maven2/&quot;&gt;https://repo1.maven.org/maven2/&lt;/a&gt;) 로 redirect 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 현존하는 project 들이 영향을 받을 수 있으며, maven central 과 호환되지 않는 버전을 참조하고 있다면 빌드시점에 오류가 발생할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하는 방법은 간단합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;code&gt;...

dependencies {
    api &quot;com.android.volley:volley:1.1.0&quot;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선 jcenter() 를 지우고 mavenCentral() 로 변경합니다.&lt;br /&gt;그리고 volley 를 예시로 하였을 때 위와 같이 오래된 라이브러리가 존재하면 라이브러리를 찾지 못한다고 이 시점에서 에러가 발생할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_10.png&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHEeHo/btsIDeAhvPA/I4HZ3WwfSOjGmBC31rvOYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHEeHo/btsIDeAhvPA/I4HZ3WwfSOjGmBC31rvOYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHEeHo/btsIDeAhvPA/I4HZ3WwfSOjGmBC31rvOYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHEeHo%2FbtsIDeAhvPA%2FI4HZ3WwfSOjGmBC31rvOYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1410&quot; height=&quot;590&quot; data-filename=&quot;edited_10.png&quot; data-origin-width=&quot;1410&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;https://repo.maven.apache.org/maven2/com/android/volley/volley/&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6R77N/btsIAR76lQV/xAOqERckqz82GQVXltDSL0/img.png&quot; data-url=&quot;https://blog.kakaocdn.net/dn/6R77N/btsIAR76lQV/xAOqERckqz82GQVXltDSL0/img.png&quot;&gt;&lt;/span&gt;&lt;a href=&quot;https://repo.maven.apache.org/maven2/com/android/volley/volley/&quot;&gt;https://repo.maven.apache.org/maven2/com/android/volley/volley/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 MavenCentral 에서 호환되는 버전을 확인 후 알맞은 버전으로 업데이트하면 해결할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. TL;DR&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;jcenter 는 bintray 에서 호스팅 하는 java 오픈소스 라이브러리 공개저장소입니다&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;2021년 2월에 지원 중단했지만 서버를 내린 것은 아니었습니다&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;2024년 8월 15일에 서버를 내릴 예정입니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;서버 내리면 jcenter 로 향한 모든 요청은 mavenCentral 로 redirect 됩니다&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc;&quot;&gt;mavenCentral 저장소에는 없는데 jcenter 에만 있는 구버전 라이브러리를 사용한다면 mavenCentral 에서 호환하는 라이브러리 버전으로 업데이트가 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;6. 참고자료&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JCenter Sunset on August 15th, 2024 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://jfrog.com/blog/jcenter-sunset/&quot;&gt;https://jfrog.com/blog/jcenter-sunset/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Into the Sunset on May 1st: Bintray, GoCenter, and ChartCenter :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&quot;&gt;https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Gradle 공식문서 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/declaring_repositories.html&quot;&gt;https://docs.gradle.org/current/userguide/declaring_repositories.html&lt;/a&gt;&lt;/p&gt;</description>
      <category>Mobile</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/114</guid>
      <comments>https://dev.gmarket.com/114#entry114comment</comments>
      <pubDate>Wed, 17 Jul 2024 10:53:57 +0900</pubDate>
    </item>
    <item>
      <title>Redis Stream 적용기</title>
      <link>https://dev.gmarket.com/113</link>
      <description>&lt;div id=&quot;SE-2c9b3139-ac56-4d2d-850c-9c900a9815a4&quot; data-compid=&quot;SE-2c9b3139-ac56-4d2d-850c-9c900a9815a4&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-2c9b3139-ac56-4d2d-850c-9c900a9815a4&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-741470b1-f120-49c5-87ee-5590a6ea400a&quot;&gt;
&lt;p id=&quot;SE-357ed5d0-792c-4e3b-8e49-ae1c6930c179&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;안녕하세요 Data Product 팀 박상우입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-21a56774-3805-4eb6-a441-8b03225e0767&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-ca8cdf9c-9bec-49fe-8444-8b20e8ef3c1f&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이번에 제가 소개해드릴 내용은 팀 내 session Info data 적재 및 API 서비스 구축에 적용한 Redis Stream에 대한 이야기입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-2edf5846-d99e-4b1d-942a-824a88ef7faa&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-6d909383-ff9b-4d35-aae4-be5d72b77c65&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;저희 팀에서는 User의 행동 정보를 수집하는 프레임워크 중 하나인 montelena receiver를 통해 수집한 데이터 (view, event, impression 등)를 post Processor라는 데이터 파이프라인 application을 통해 적재, 가공해서 각종 지표 트래킹 및 분석에 활용할 수 있도록 제공하고 있습니다.&lt;/p&gt;
&lt;p id=&quot;SE-02e93e82-be43-4a45-a3f1-db95b554df20&quot; style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-468d39bb-314c-46ca-83cd-0af12095f990&quot; data-compid=&quot;SE-468d39bb-314c-46ca-83cd-0af12095f990&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-468d39bb-314c-46ca-83cd-0af12095f990&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-468d39bb-314c-46ca-83cd-0af12095f990&quot;&gt;
&lt;div data-unitid=&quot;SE-468d39bb-314c-46ca-83cd-0af12095f990&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;353&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SBPOd/btsIwoKfyaN/2JJfk81k3oxKCKPy1Dezy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SBPOd/btsIwoKfyaN/2JJfk81k3oxKCKPy1Dezy1/img.png&quot; data-alt=&quot;Post Processor Data Pipeline&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SBPOd/btsIwoKfyaN/2JJfk81k3oxKCKPy1Dezy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSBPOd%2FbtsIwoKfyaN%2F2JJfk81k3oxKCKPy1Dezy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;936&quot; height=&quot;353&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;353&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Post Processor Data Pipeline&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-5a4bd3d0-0d12-4aad-bf3a-3c3dcce13a6f&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-e075356b-38e3-41eb-a3a2-b7858c9beba2&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그중 유니크한 active user를 식별하기 위해 session_id를 발급하고, 그 히스토리를 남겨 광고에 활용하고 있는데,&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-0b48a89c-f2df-44d8-8d78-7edc34e87c1d&quot; data-compid=&quot;SE-0b48a89c-f2df-44d8-8d78-7edc34e87c1d&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-0b48a89c-f2df-44d8-8d78-7edc34e87c1d&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-0493ecca-7da0-42b9-87b5-31418a7ef390&quot;&gt;
&lt;p id=&quot;SE-c7fb2763-0ab4-4ee5-b9aa-fa5fdbc41797&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;'Big Smile Day' (지마켓 최고의 연례행사인 빅스마일데이, 이하 BDS)&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-55b4456d-f930-4b5a-b8cb-4254427f8342&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;'User Targeting Content' (이하 UTC) Push 발송 등 유저의 유입이 급증해서 트래픽이 대폭 증가할 경우 데이터 처리가 지연되는 현상이 발생하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-a3dd4089-12f6-48d5-a70f-211da8bc84dc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-6168f497-1f4a-40e0-adb6-8600e573040e&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 문제를 해결하기 위해서는 부하를 발생시키는 로직을 분리해서 별도로 처리하도록 하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000; text-align: start;&quot;&gt;application&lt;/span&gt;의 개발이 요구되는 상황이었고,&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-411d4691-6532-4300-9a58-0e1b4db2dbcd&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이에 Redis stream을 사용해서 session_id 히스토리 적재 로직을 수행하는 신규 consumer를 개발했던 과정을 간단하게나마 공유해보고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-9aebcf12-3a31-4257-aa78-23b2b573f787&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-412c07c7-1fbf-45e3-bc6e-655cf98e9f72&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-b2f0ee19-e189-4a18-bcb3-58df216da2f7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-8c1cf4fc-bedd-4308-ba8a-d7746f88e176&quot; data-compid=&quot;SE-8c1cf4fc-bedd-4308-ba8a-d7746f88e176&quot; data-a11y-title=&quot;소제목&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-8c1cf4fc-bedd-4308-ba8a-d7746f88e176&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-0b356118-4562-40a6-970c-366cf868c876&quot;&gt;
&lt;h2 id=&quot;SE-5b922977-36c3-495c-87b4-f690741f3b32&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis Stream의 특징과 장점&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;2018년 10월 17일, Redis 5.0 버전이 출시되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div id=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-compid=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-9291e0a4-c0af-46d2-b6b9-77ebb077ab5f&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-compid=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-unitid=&quot;SE-19dc2e32-9404-454a-b69c-0eaa465d7b9a&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJtN2h/btsIv8Vbi5J/y3xzOpcLp6Yw69kOCkbDU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJtN2h/btsIv8Vbi5J/y3xzOpcLp6Yw69kOCkbDU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJtN2h/btsIv8Vbi5J/y3xzOpcLp6Yw69kOCkbDU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJtN2h%2FbtsIv8Vbi5J%2Fy3xzOpcLp6Yw69kOCkbDU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;471&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이전&amp;nbsp;버전에서&amp;nbsp;많은&amp;nbsp;부분이&amp;nbsp;개선되었지만&amp;nbsp;그중&amp;nbsp;가장&amp;nbsp;중요한&amp;nbsp;기능&amp;nbsp;추가&amp;nbsp;중&amp;nbsp;하나가&amp;nbsp;바로&amp;nbsp;Redis&amp;nbsp;stream&amp;nbsp;이였는데요.&lt;br /&gt;고가용성&amp;nbsp;데이터&amp;nbsp;스트리밍&amp;nbsp;처리가&amp;nbsp;도입되면서,&amp;nbsp;데이터의&amp;nbsp;일관성과&amp;nbsp;안정성을&amp;nbsp;보장하면서&amp;nbsp;대용량&amp;nbsp;데이터&amp;nbsp;스트림을&amp;nbsp;실시간으로&amp;nbsp;처리할 수&amp;nbsp;있게&amp;nbsp;되었습니다.&lt;br /&gt;동시에 inmemory 기반으로 동작하는 key value 기반의 캐시를 사용하기 때문에 속도가 빠르다는 장점으로 사내에서도 저장소로&amp;nbsp;널리&amp;nbsp;사용되고 있죠.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-401d2a2f-1385-43dc-95ac-240605a5cff7&quot; data-compid=&quot;SE-401d2a2f-1385-43dc-95ac-240605a5cff7&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-401d2a2f-1385-43dc-95ac-240605a5cff7&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-27f964f0-3e80-4fbe-b415-5335fd52026c&quot;&gt;
&lt;p id=&quot;SE-476d05cb-8a69-4e82-980c-9d7baca9f1d0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;SE-d9e7b0e4-6c6a-4932-be60-96638bca8718&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;왜 Redis Stream을 선택했는가?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-662bd44e-9cac-4554-b87a-9572250e9bc2&quot; data-compid=&quot;SE-662bd44e-9cac-4554-b87a-9572250e9bc2&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-662bd44e-9cac-4554-b87a-9572250e9bc2&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-1132e013-65d2-4ece-808b-f2633ffe9d50&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-270a3284-7ae1-4297-8f36-ab7624d57043&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;처음에는 메시지 큐로 kafka나 MQ를 생각했었는데, 새로운 플랫폼을 적용해야 하다 보니 개발 공수도 늘어나고 리소스도 많이 소모될 거라는 결론을 내렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-a2903419-78c8-42eb-9aed-0d5f54ca6b90&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;거기다 BSD가 얼마 남지 않은 시점이라 그전에 개발을 완료해야 된다는 시간적인 제한도 허들이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-d4b227e2-63f1-4f92-b04d-9d26a05c9b5a&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이미 session_id 저장소로 redis를 사용하고 있었고, 최대한 기존 로직을 건드리지 않으면서, 빠르게 히스토리를 적재할 수 있는 방법을 찾던 중,&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-ce036b84-2517-4774-84e0-23908edcd43f&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;kafka와 유사한 기능들을 제공하면서 사내 openshift 환경의 여러 개 pod에서 구동해도 데이터 중복이나 유실 없이 처리가 가능한 Redis stream을 선택하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-9c0b7bfc-2769-4411-a8bd-8a73884e7abc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-8d5fb2d8-06ac-47a2-9fb9-449b0eff6d50&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-6ae2d4c0-877b-4b62-8629-b4015e0ec3f0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-b28477fc-4501-4d0b-919a-48011b3b7d95&quot; data-compid=&quot;SE-b28477fc-4501-4d0b-919a-48011b3b7d95&quot; data-a11y-title=&quot;소제목&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-b28477fc-4501-4d0b-919a-48011b3b7d95&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-edc77a9b-6756-4678-91c4-e7f6fa5a5040&quot;&gt;
&lt;h2 id=&quot;SE-1eedcb1a-3744-4dc2-b8f0-66f9dc8cb113&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis Pub/Sub과 Redis Stream?&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-b2bfe4c8-3516-4379-8f5a-0d3343890166&quot; data-compid=&quot;SE-b2bfe4c8-3516-4379-8f5a-0d3343890166&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-b2bfe4c8-3516-4379-8f5a-0d3343890166&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-2b73999d-9408-4f92-adfd-337b6364c533&quot;&gt;
&lt;p id=&quot;SE-e11ef9e3-5704-4fe1-a45d-aa93b4dc4061&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-3e062e88-28c2-41be-a19b-d0f1ca15cf58&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;일반적으로 Redis를 이용해 메시지를 Broadcasting 할 때는 pub/sub을 많이 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-afd4ed17-640d-417e-ac6e-a48c5896ea5d&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;하지만 이 방식은 publisher가 메시지를 발행했을 때 subscriber가 존재하지 않거나 애플리케이션에 이슈가 발생하면 수신 여부에 관계없이 메시지가 휘발되는 단점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-9ae4edb6-538a-47e2-9e74-226c6df845d1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;또한, 여러 개의 subscriber를 구동하면 모두에게 동일한 메시지를 발행해 데이터가 중복되는 이슈가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-compid=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot;&gt;
&lt;div id=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-compid=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot;&gt;
&lt;div data-unitid=&quot;SE-8deef978-4efa-41b3-83e6-09075a0ff7d8&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djuNL5/btsIvc44dSR/nwOBrr8w29oi21zVE3rBw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djuNL5/btsIvc44dSR/nwOBrr8w29oi21zVE3rBw1/img.png&quot; data-alt=&quot;Redis Pub / Sub&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djuNL5/btsIvc44dSR/nwOBrr8w29oi21zVE3rBw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjuNL5%2FbtsIvc44dSR%2FnwOBrr8w29oi21zVE3rBw1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;271&quot; data-origin-width=&quot;764&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Pub / Sub&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-d8914789-fae5-40df-b819-d93e5536cc54&quot; data-compid=&quot;SE-d8914789-fae5-40df-b819-d93e5536cc54&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-d8914789-fae5-40df-b819-d93e5536cc54&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-b1a053cd-7310-4f7d-ba53-b9259578498e&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-afc6e0ac-5e90-4898-900a-5375c334b416&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #202124; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;반면, Redis Stream은 휘발성이 아니라 Kafka의 offset 개념처럼 마지막으로 수신한 record id를 저장하고 XADD, XREADGROUP, XACK, XPENDING, XCLAIM으로 이어지는 처리 프로세스를 통해 메시지를 컨트롤할 수 있는 다양한 방법을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-compid=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-38ffbcef-62f9-4b87-a300-74b1c2961990&quot;&gt;
&lt;div id=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-compid=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot;&gt;
&lt;div data-unitid=&quot;SE-9e58f812-3103-4700-8642-45867dc03830&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5VRLC/btsIuJbk6De/jgDkLxka0URSNnkpQGfjKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5VRLC/btsIuJbk6De/jgDkLxka0URSNnkpQGfjKK/img.png&quot; data-alt=&quot;Redis Stream&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5VRLC/btsIuJbk6De/jgDkLxka0URSNnkpQGfjKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5VRLC%2FbtsIuJbk6De%2FjgDkLxka0URSNnkpQGfjKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;340&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Stream&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-1685d92e-3fd6-4e11-ae8e-7f7010b4f3d0&quot; data-compid=&quot;SE-1685d92e-3fd6-4e11-ae8e-7f7010b4f3d0&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-1685d92e-3fd6-4e11-ae8e-7f7010b4f3d0&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-6c282846-05f4-450b-b3f8-299d639bd94c&quot;&gt;
&lt;p id=&quot;SE-a1ce989c-92e9-434c-a046-4009660c5ae8&quot; style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출처 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot; data-href=&quot;https://jybaek.tistory.com/935&quot;&gt;&lt;a href=&quot;https://jybaek.tistory.com/935&quot;&gt;https://jybaek.tistory.com/935&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-ab0cbd7c-2853-41b6-9179-101ca31033ec&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-d054cfc8-c06c-429e-960a-fad4ad846a75&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-b4aeaa0a-5405-4c79-9c69-9afaa5447348&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;또한, Redis Stream은 consumer group을 지원하기 때문에 producer가 발행한 메시지를 여러 개의 consumer가 하나의 그룹을 형성해서 중복 없이 순차적으로 병렬 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-c899530e-7b99-4159-b0dc-d2c6ddba796e&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 XACK 명령어를 사용해 메시지 처리 여부를 확인할 수 있으며, 일정 시간 동안 처리되지 못한 메시지들도 Pending Entries List를 이용해서 재처리할 수 있는 방법을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-e16ffa64-c312-4d25-9143-50cf67592c37&quot; data-compid=&quot;SE-e16ffa64-c312-4d25-9143-50cf67592c37&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-e16ffa64-c312-4d25-9143-50cf67592c37&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-e16ffa64-c312-4d25-9143-50cf67592c37&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-unitid=&quot;SE-e16ffa64-c312-4d25-9143-50cf67592c37&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcWlQp/btsIvuj7Zcb/EubKOQ3WOv7XN1Bnj0H2gK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcWlQp/btsIvuj7Zcb/EubKOQ3WOv7XN1Bnj0H2gK/img.png&quot; data-alt=&quot;Redis Stream&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcWlQp/btsIvuj7Zcb/EubKOQ3WOv7XN1Bnj0H2gK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcWlQp%2FbtsIvuj7Zcb%2FEubKOQ3WOv7XN1Bnj0H2gK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;384&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Stream&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-288e280d-6a52-41e8-8499-823d02f251b6&quot; data-compid=&quot;SE-288e280d-6a52-41e8-8499-823d02f251b6&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-288e280d-6a52-41e8-8499-823d02f251b6&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-fb3932b5-b95d-4b11-8b07-5ceaf287aefb&quot;&gt;
&lt;p id=&quot;SE-9e6367df-0387-4fba-a980-1fc1075b9da8&quot; style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출처 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot; data-href=&quot;https://jybaek.tistory.com/935&quot;&gt;&lt;a href=&quot;https://jybaek.tistory.com/935&quot;&gt;https://jybaek.tistory.com/935&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-a1d3405d-9bf0-44fc-8335-d72f94d05895&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-f7885cbb-4667-4e34-a91e-a6b61bc7ba9d&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;SE-c2091a53-27c5-4981-819b-8363f55aedce&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Redis stream Development&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-707de02d-f65b-48a9-b6f0-3b9a443cc77b&quot; data-compid=&quot;SE-707de02d-f65b-48a9-b6f0-3b9a443cc77b&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-707de02d-f65b-48a9-b6f0-3b9a443cc77b&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-b9beb496-17c7-4639-8501-98cfa7c601c5&quot;&gt;
&lt;p id=&quot;SE-848be57f-56e6-4915-9825-93836706a052&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-eb31c105-8a1b-48fe-bce2-91a3d4629279&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이제 실제로 코드를 보며 Redis stream으로 어떻게 개발을 진행했는지 간단히 알아보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-4c006b2e-08de-451c-a726-2c5558f7b92a&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;프로세스는 심플한 구조로 아래 flow chart를 참고해 주시면 되겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-compid=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-b8fbb9b6-067f-4fd3-82b2-513c47cfb988&quot; data-compid=&quot;SE-b8fbb9b6-067f-4fd3-82b2-513c47cfb988&quot; data-a11y-title=&quot;소제목&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-b8fbb9b6-067f-4fd3-82b2-513c47cfb988&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-7bf1eb01-c5e9-4567-affa-b6a9ad6384de&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-compid=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-unitid=&quot;SE-4bb6012c-751a-4442-8b38-e7bfa5169dfc&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;327&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cADQ90/btsIv7IKSSA/ZAki0Xm51oKpkSICbkZZ6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cADQ90/btsIv7IKSSA/ZAki0Xm51oKpkSICbkZZ6k/img.png&quot; data-alt=&quot;Redis Stream flow&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cADQ90/btsIv7IKSSA/ZAki0Xm51oKpkSICbkZZ6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcADQ90%2FbtsIv7IKSSA%2FZAki0Xm51oKpkSICbkZZ6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;327&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;327&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Stream flow&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-32a10410-a65a-4dbd-9633-7a1305807528&quot; data-compid=&quot;SE-32a10410-a65a-4dbd-9633-7a1305807528&quot; data-a11y-title=&quot;소제목&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-32a10410-a65a-4dbd-9633-7a1305807528&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-32848fa8-69dd-4c86-b11f-7450c4f671c7&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-84113c7f-be28-4bb0-a7c5-40373116ca6b&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&amp;lt;Publisher&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-edac2506-b47b-4324-ad33-123a418238b6&quot; data-compid=&quot;SE-edac2506-b47b-4324-ad33-123a418238b6&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-edac2506-b47b-4324-ad33-123a418238b6&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-b23e679d-2030-4156-af61-4fa1da515d1f&quot;&gt;
&lt;p id=&quot;SE-61ccbf26-4173-4f6e-a7c1-c285af9f72d2&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;먼저 Post Processor에서 트래픽 데이터를 가공/처리한 후, app에서 정의한 streamKey를 통해서 생성한 session 객체를 캡슐화해서 Redis Stream 메시지로 발행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-77887dee-5a6a-4f32-86cb-ac6c538be831&quot; data-compid=&quot;SE-77887dee-5a6a-4f32-86cb-ac6c538be831&quot; data-a11y-title=&quot;코드&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-77887dee-5a6a-4f32-86cb-ac6c538be831&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ObjectRecord&amp;lt;String, String&amp;gt; record = StreamRecords.newRecord()
				.ofObject(GsonUtil.gson().toJson(session))
				.withStreamKey(streamKey);

RecordId recordId = gmktRedisTemplate.opsForStream().add(record);
if (Objects.isNull(recordId)) {
    // Redis Stream record 처리에 실패했을 경우 로직 추가
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-cfc7fcb3-d839-45cd-b87e-f298b5026795&quot; data-compid=&quot;SE-cfc7fcb3-d839-45cd-b87e-f298b5026795&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-cfc7fcb3-d839-45cd-b87e-f298b5026795&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-2e782fc2-0ab5-4abf-914b-08864a729e76&quot;&gt;
&lt;p id=&quot;SE-ee92c833-7004-4aff-88a3-56d74510ede1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;opsForStream().add() 메서드를 통해서 메시지를 발행하면 recordId를 리턴하는데, 이 값은 각각의 메시지가 스트림에 추가될 때 Redis가 생성해 주는 고유의 id 값으로 보시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-1c7c2ef7-8c54-49ad-a8c7-2f2b3c4ae6a9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;저는 stream key / value 형태의 ObjectRecord 객체를 사용해서 개발했지만,&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-c5453b6a-83fb-4f14-bffb-455d21798805&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이외에도 value 값으로 Map&amp;lt;K, V&amp;gt; 형태로 데이터를 저장하고 읽을 수 있는 MapRecord라는 메서드도 지원하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-9fc73dac-0da6-4c26-8cb9-86d12ca9d6f1&quot; data-compid=&quot;SE-9fc73dac-0da6-4c26-8cb9-86d12ca9d6f1&quot; data-a11y-title=&quot;코드&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-9fc73dac-0da6-4c26-8cb9-86d12ca9d6f1&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;public interface MapRecord&amp;lt;S, K, V&amp;gt; extends Record&amp;lt;S, Map&amp;lt;K, V&amp;gt;&amp;gt;, Iterable&amp;lt;Map.Entry&amp;lt;K, V&amp;gt;&amp;gt; {

	static &amp;lt;S, K, V&amp;gt; MapRecord&amp;lt;S, K, V&amp;gt; create(S stream, Map&amp;lt;K, V&amp;gt; map) {

		Assert.notNull(stream, &quot;Stream must not be null&quot;);
		Assert.notNull(map, &quot;Map must not be null&quot;);

		return new MapBackedRecord&amp;lt;&amp;gt;(stream, RecordId.autoGenerate(), map);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-98145847-a32a-42c0-8da6-7a148159ce2b&quot; data-compid=&quot;SE-98145847-a32a-42c0-8da6-7a148159ce2b&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-98145847-a32a-42c0-8da6-7a148159ce2b&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-5360de94-3060-40bc-bd73-ed580458aa65&quot;&gt;
&lt;p id=&quot;SE-185d8051-2d3f-4daf-9a0f-55cf30e9408c&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-1a9ed55e-cb22-4642-95bb-0d9530ccefa9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;&amp;lt;Consumer&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-454f8cb7-0b4f-4483-acfb-02371449862b&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;다음은 발행된 메시지를 컨슈밍 하고 처리하는 코드를 보여드리겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-d1e0074a-b694-45b6-9d8b-bc0d867cb4b3&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;먼저 Spring boot 프로젝트에서 'spring-boot-starter-data-redis' 의존성을 추가하고 Redis 서버 설정을 완료합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-45b12fc3-7f7a-4a11-80aa-18f4bb1a4dd7&quot; data-compid=&quot;SE-45b12fc3-7f7a-4a11-80aa-18f4bb1a4dd7&quot; data-a11y-title=&quot;코드&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-45b12fc3-7f7a-4a11-80aa-18f4bb1a4dd7&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-2be1ad25-43fa-4716-992e-de27eeb244f1&quot; data-compid=&quot;SE-2be1ad25-43fa-4716-992e-de27eeb244f1&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-2be1ad25-43fa-4716-992e-de27eeb244f1&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-1fa8afaa-6160-4826-b530-b3a37368dd9b&quot;&gt;
&lt;p id=&quot;SE-2a544bfe-68be-43bc-a2d1-b56886883422&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그다음 'StreamListener' 인터페이스를 구현하고, afterPropertiesSet() 메서드를 @Orverride 해서 Redis Stream 메시지를 소비하는 consumer Group과 Listener Container를 설정하고 초기화합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-ef18516e-30ed-4bb5-a556-a87a00b1826f&quot; data-compid=&quot;SE-ef18516e-30ed-4bb5-a556-a87a00b1826f&quot; data-a11y-title=&quot;코드&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-ef18516e-30ed-4bb5-a556-a87a00b1826f&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;@Override
public void afterPropertiesSet() throws Exception {
    // Consumer Group 설정
    createStreamConsumerGroup(streamKey, consumerGroupName);

    // StreamMessageListenerContainer 설정
    this.listenerContainer = StreamMessageListenerContainer.create(
				redisTemplate.getConnectionFactory(),
				StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
				.targetType(String.class)
				.pollTimeout(Duration.ofSeconds(2))
				.build()
		);

    // Subscription 설정
    this.subscription = this.listenerContainer.receive(
            Consumer.from(this.consumerGroupName, consumerName),
            StreamOffset.create(streamKey, ReadOffset.lastConsumed()),
            this
    );

    // Redis listen 시작
    this.listenerContainer.start();
}

public void createStreamConsumerGroup(String streamKey, String consumerGroupName){
	if (!redisTemplate.hasKey(streamKey)){
		RedisClusterAsyncCommands commands = (RedisClusterAsyncCommands) this.redisTemplate
				.getConnectionFactory()
				.getClusterConnection()
				.getNativeConnection();
		
        CommandArgs&amp;lt;String, String&amp;gt; args = new CommandArgs&amp;lt;&amp;gt;(StringCodec.UTF8)
				.add(CommandKeyword.CREATE)
				.add(streamKey)
				.add(consumerGroupName)
				.add(&quot;0&quot;)
				.add(&quot;MKSTREAM&quot;);

		commands.dispatch(CommandType.XGROUP, new StatusOutput(StringCodec.UTF8), args);
	}
	else{
		if(!isStreamConsumerGroupExist(streamKey, consumerGroupName)){
			this.redisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from(&quot;0&quot;), consumerGroupName);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-c5eab8de-6a72-4a4f-8e32-4af6a44dacff&quot; data-compid=&quot;SE-c5eab8de-6a72-4a4f-8e32-4af6a44dacff&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-c5eab8de-6a72-4a4f-8e32-4af6a44dacff&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-6d43d598-d620-4aca-bba0-c3cb7590ae47&quot;&gt;
&lt;p id=&quot;SE-2e564dd0-5c7c-4245-95d3-6c566ea24e15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Publisher에서 설정한 StreamKey와 Consumer Name을 세팅해서 Consumer Group을 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-f1cfaf02-4aa6-4f2a-9580-6b70e6f42e99&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 Consumer 객체를 생성, Offset 설정, Subscriber 옵션 등 세부적인 설정을 완료한 뒤, Listener Container를 시작하면 Redis Stream 메시지를 비동기적으로 Listner에게 전달하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-c7bafcff-d90c-412c-a794-02cf3171546a&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 과정을 통해서 여러 개의 Consumer Group을 사용해서 하나의 Redis Stream을 병렬로 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-ecc68fbd-e19e-432f-b6a1-d69aa5666fdb&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-094e67ba-5494-4454-af2b-94dc1d8a9cc4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이렇게 전달되는 메시지들은 마찬가지로 'StreamListener' 인터페이스가 제공하는 onMessage 메서드를 구현해서 처리하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-3a982061-e5b4-4038-ae19-04ba484914aa&quot; data-compid=&quot;SE-3a982061-e5b4-4038-ae19-04ba484914aa&quot; data-a11y-title=&quot;코드&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-3a982061-e5b4-4038-ae19-04ba484914aa&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;@Override
public void onMessage(ObjectRecord&amp;lt;String, String&amp;gt; message) {
    
    String stream = message.getStream();
    String recordId = message.getId().getValue();
    
    try {
        // 처리할 로직 구현
        if (StringUtils.isNotEmpty(message.getValue())) {
            // To-Do
        }
        // 이후, ack stream
        this.redisTemplate.opsForStream().acknowledge(streamKey, consumerGroupName, recordId);
        
    } catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();
        this.redisOperator.delete(stream, recordId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-a27a1193-0356-49c7-b518-59125a518ed4&quot; data-compid=&quot;SE-a27a1193-0356-49c7-b518-59125a518ed4&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-a27a1193-0356-49c7-b518-59125a518ed4&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-77401bbc-f306-4983-8687-2811adf476f6&quot;&gt;
&lt;p id=&quot;SE-b9079962-3214-42fa-b68b-dbab64b1f572&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;메시지를 처리하고 나면 'ackStream' 메소드를 호출해서 Redis Stream 메시지가 성공적으로 처리되었음을 알리고, 해당 메시지를 대기열에서 제거합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-86d29336-8939-4552-b232-12fbb8d88559&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Exception 이 발생할 경우 recordId를 통해 메시지를 삭제 처리할 수도 있고, 다양한 시나리오로 처리가 가능하니 용도에 맞게 사용하시면 되겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-b526e745-0b93-41e8-aca0-da6e37a27e6f&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-54f0a52c-4c94-4a55-b41e-e5d4511a6e19&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-e30ea8ea-4591-4bea-aa95-20d8826a6409&quot; data-compid=&quot;SE-e30ea8ea-4591-4bea-aa95-20d8826a6409&quot; data-a11y-title=&quot;소제목&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-e30ea8ea-4591-4bea-aa95-20d8826a6409&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-03626274-fc83-43bf-9233-551862ac372c&quot;&gt;
&lt;h2 id=&quot;SE-a27eef85-6824-4d86-b7d4-b2b29d03ebeb&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;Redis stream 적용 시 고려할 점들&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-87337d3c-5368-4f1e-9f65-b37a21afde18&quot; data-compid=&quot;SE-87337d3c-5368-4f1e-9f65-b37a21afde18&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-87337d3c-5368-4f1e-9f65-b37a21afde18&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-a4e50485-301a-4a9e-9d0b-6663d0d4c93c&quot;&gt;
&lt;p id=&quot;SE-64809c52-18b2-4908-b60a-2a8c36fed53a&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-70df0afc-f520-4bb5-b4b7-3d8a0de56a52&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis Stream은 파티션 개념이 없기 때문에 하나의 stream을 여러 개의 consumer가 병렬 처리하는 구조로 동작합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-862d46d6-c66f-472d-a7cd-05838df12568&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 Redis cluster 구조에서 sharding 되어 있는 node에 메시지를 고르게 보내려면 추가적으로 N개의 stream을 각각의 node에 할당하도록 추가 개발이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-70c5363c-6600-405c-9acd-098e8440ce5f&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이러한 특징 때문에 produce 된 순서대로 메시지를 처리한다는 보장이 없어지는데, 이는 실서비스에 적용할 때 반드시 고려해야 할 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-compid=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot;&gt;
&lt;div id=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-compid=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot;&gt;
&lt;div data-unitid=&quot;SE-e29805df-493d-4a32-8789-5a611b6e7ad0&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSL4Sg/btsIvO3JfJn/csBbQKFXKKAG9EE5OCjOKK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSL4Sg/btsIvO3JfJn/csBbQKFXKKAG9EE5OCjOKK/img.jpg&quot; data-alt=&quot;Partition을 지원하는 Kafka cousumer group의 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSL4Sg/btsIvO3JfJn/csBbQKFXKKAG9EE5OCjOKK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSL4Sg%2FbtsIvO3JfJn%2FcsBbQKFXKKAG9EE5OCjOKK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;249&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Partition을 지원하는 Kafka cousumer group의 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-compid=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot;&gt;
&lt;div id=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-compid=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot;&gt;
&lt;div data-unitid=&quot;SE-608c567f-3e1e-4174-ad08-3ccd114e6363&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npYh8/btsIu22KWqw/RkzpHbeirIf34M98HJ1QPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npYh8/btsIu22KWqw/RkzpHbeirIf34M98HJ1QPk/img.jpg&quot; data-alt=&quot;단일 Stream Redis stream cousumer group의 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npYh8/btsIu22KWqw/RkzpHbeirIf34M98HJ1QPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpYh8%2FbtsIu22KWqw%2FRkzpHbeirIf34M98HJ1QPk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;190&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;단일 Stream Redis stream cousumer group의 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-e9465d19-1241-4b4f-95c5-50a2277c372b&quot; data-compid=&quot;SE-e9465d19-1241-4b4f-95c5-50a2277c372b&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-e9465d19-1241-4b4f-95c5-50a2277c372b&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-65d6d43d-9c7f-4a33-add0-3e989e0afe0a&quot;&gt;
&lt;p id=&quot;SE-2c787147-9cc6-4a3e-b992-addc26e3ae16&quot; style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;출처 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot; data-href=&quot;https://mattwestcott.org/blog/redis-streams-vs-kafka&quot;&gt;&lt;a href=&quot;https://mattwestcott.org/blog/redis-streams-vs-kafka&quot;&gt;https://mattwestcott.org/blog/redis-streams-vs-kafka&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-bbe73690-fbfd-49d0-b88d-a389de44d439&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-d7e416de-66bf-4f23-becc-cfc8c0e1abb0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-a4680370-73c5-4265-aeb4-8c3c8cca5851&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그리고 또 한 가지.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-24b0340e-ccac-4dfa-a27a-6f2b4b3c5c75&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;in-memory 기반의 저장소이기 때문에 memory 관리에 신경을 많이 써야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-b3a1c6b3-6467-41ec-ac68-ee4aba251ae9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;예를 들어, XACK를 받지 못한 pending 메시지를 다시 처리하지 않으면, 그 메시지들은 점점 쌓여가면서 Redis cluster의 memory를 위태롭게 만들 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-bdf5cc9a-642c-4ce3-b23b-140131123daa&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그래서 저도 1분마다 스케줄을 돌며 pending 메시지를 처리하는 로직을 추가해 memory full 이슈를 방지하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-c0fd154c-f351-44b0-842a-3321011e41e6&quot; data-compid=&quot;SE-c0fd154c-f351-44b0-842a-3321011e41e6&quot; data-a11y-title=&quot;코드&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-c0fd154c-f351-44b0-842a-3321011e41e6&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot;&gt;&lt;code&gt;@Scheduled(fixedRate = 60000)
public void PendingMessagesSummaryScheduler() {
    PendingMessagesSummary pendingSummary = this.redisOperator.pendingSummary(streamKey, consumerGroupName);

    // 로그 출력
    pendingSummary.getPendingMessagesPerConsumer().forEach((consumer, count) -&amp;gt; {
        log.info(&quot;Consumer: &quot; + consumer + &quot;, Pending Messages: &quot; + count);
    });

    // pendingSummary와 TotalPendingMessages 체크 및 메시지 처리
    if (pendingSummary != null &amp;amp;&amp;amp; pendingSummary.getTotalPendingMessages() &amp;gt; 0) {
        PendingMessages pendingMessages = this.redisOperator.pending(streamKey, consumerGroupName);
        pendingMessages.toList().stream()
            .filter(pendingMessage -&amp;gt; !ObjectUtils.isEmpty(pendingMessage))
            .forEach(pendingMessage -&amp;gt; {
                List&amp;lt;ObjectRecord&amp;lt;String, String&amp;gt;&amp;gt; messages = this.redisOperator.read(streamKey, consumerGroupName, consumerName, pendingMessage.getIdAsString());
                messages.stream()
                    .filter(recordMessage -&amp;gt; !ObjectUtils.isEmpty(recordMessage))
                    .forEach(recordMessage -&amp;gt; {
                        // 메시지 처리 로직

                    });
                // 메시지 ACK
                this.redisTemplate.opsForStream().acknowledge(streamKey, consumerGroupName, pendingMessage.getIdAsString());
            });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-42274b60-2da6-44b5-ad35-b53dacb09eb2&quot; data-compid=&quot;SE-42274b60-2da6-44b5-ad35-b53dacb09eb2&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-42274b60-2da6-44b5-ad35-b53dacb09eb2&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-c7cb55f7-fe1f-4819-bfc7-802c1132f4ae&quot;&gt;
&lt;p id=&quot;SE-693159ae-8306-4082-a9ec-3c18d5f8f95d&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-47cb3487-82b5-4ae5-9341-c0c88e5a4946&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-b83569b2-1c27-42da-8743-44d9dc71ad3f&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-10c8da58-aef9-4e4e-96a5-5ff41f728b27&quot; data-compid=&quot;SE-10c8da58-aef9-4e4e-96a5-5ff41f728b27&quot; data-a11y-title=&quot;소제목&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-10c8da58-aef9-4e4e-96a5-5ff41f728b27&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-b594a902-f03d-4223-b683-2e9c5487e358&quot;&gt;
&lt;h2 id=&quot;SE-71dc92dd-b306-42bd-bbdd-9dc5b4d5ddeb&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-compid=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-ca80826c-94e0-4ff5-9146-d700c00e5dbb&quot;&gt;
&lt;div id=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-compid=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot;&gt;
&lt;div data-unitid=&quot;SE-f2b009b7-1f8d-4ff8-a0e7-505550c6290a&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blcKht/btsIv8t7Obt/B9tykHX6n0BhZ1UzhNlbSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blcKht/btsIv8t7Obt/B9tykHX6n0BhZ1UzhNlbSK/img.png&quot; data-alt=&quot;Redis Stream 적용 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blcKht/btsIv8t7Obt/B9tykHX6n0BhZ1UzhNlbSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblcKht%2FbtsIv8t7Obt%2FB9tykHX6n0BhZ1UzhNlbSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;267&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Stream 적용 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-5a48b168-41af-4e85-a534-1895384ac2c7&quot; data-compid=&quot;SE-5a48b168-41af-4e85-a534-1895384ac2c7&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-5a48b168-41af-4e85-a534-1895384ac2c7&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-5a48b168-41af-4e85-a534-1895384ac2c7&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-unitid=&quot;SE-5a48b168-41af-4e85-a534-1895384ac2c7&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UKiVE/btsIwTCYMz8/zxfAyKBSfE97ad8RswKHSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UKiVE/btsIwTCYMz8/zxfAyKBSfE97ad8RswKHSk/img.png&quot; data-alt=&quot;Redis Stream 적용 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UKiVE/btsIwTCYMz8/zxfAyKBSfE97ad8RswKHSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUKiVE%2FbtsIwTCYMz8%2FzxfAyKBSfE97ad8RswKHSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;765&quot; height=&quot;268&quot; data-origin-width=&quot;765&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Stream 적용 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;SE-7ee1f2b0-a714-4669-9e65-8b30e4d5826f&quot; data-compid=&quot;SE-7ee1f2b0-a714-4669-9e65-8b30e4d5826f&quot; data-a11y-title=&quot;본문&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-7ee1f2b0-a714-4669-9e65-8b30e4d5826f&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-02edf51b-92e7-4e2f-bcb8-c6a57e6e9e2d&quot;&gt;
&lt;p id=&quot;SE-bf6bfcd0-4542-4cca-970d-19af03efff87&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;데이터 파이프라인 applicaion에 Redis Stream을 적용 후,&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-2db906b3-8936-4cbe-a53e-8800aa493289&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;UTS Push 발송 시 지연되던 트래픽 처리가 완전하게 해소되어, BDS 행사기간에도 원활하게 서비스가 가능하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-6cd6cb97-2cab-4575-8841-e6b0855fee35&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Redis stream은 분산 처리 환경에서 쉽고 빠르게 실시간 데이터 처리를 가능하게 만들어주는 아주 효율적인 도구입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-38a26d28-e877-4047-813d-9546cac74fc1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;이 포스팅을 통해서 Redis Stream의 개념과 동작 방식에 대해서 간단하게나마 파악하고, 추후 Redis Stream을 이용한 애플리케이션을 구축하는데 작은 도움이 되었으면 하는 바람입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-86da73f1-502d-4546-a181-08e1b1fcbe7e&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-a64c4b1e-fe02-424f-a7ee-df4d8150007e&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-c3dc5a9e-a343-44ac-9683-70e65485d380&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-d007608b-6eaf-49e9-993b-dd36f1741769&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-3d107c9b-9460-42e7-8b3e-b431203876e4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-1d5667f3-9023-4457-aa24-aecad03020d0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;SE-589e507f-4171-4d24-b177-f44a9b0c8033&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&amp;lt;참고문서&amp;gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-e467964e-8d0c-4f7d-ad03-deea9d4649f1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot; data-href=&quot;https://nimasrn.medium.com/introduction-to-redis-streams-1d6a95ab141&quot;&gt;&lt;a href=&quot;https://nimasrn.medium.com/introduction-to-redis-streams-1d6a95ab141&quot;&gt;https://nimasrn.medium.com/introduction-to-redis-streams-1d6a95ab141&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-8c5f199d-1b03-40e3-b2fd-a17fc3134293&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot; data-href=&quot;https://redis.io/docs/latest/develop/data-types/streams/&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/data-types/streams/&quot;&gt;https://redis.io/docs/latest/develop/data-types/streams/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p id=&quot;SE-e042d8ec-bd31-4584-9faa-9ccdc721abbb&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot; data-href=&quot;https://jybaek.tistory.com/935&quot;&gt;&lt;a href=&quot;https://jybaek.tistory.com/935&quot;&gt;https://jybaek.tistory.com/935&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://kingjakeu.github.io/springboot/2022/02/10/spring-boot-redis-stream/&quot;&gt;https://kingjakeu.github.io/springboot/2022/02/10/spring-boot-redis-stream/&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/113</guid>
      <comments>https://dev.gmarket.com/113#entry113comment</comments>
      <pubDate>Thu, 11 Jul 2024 15:55:03 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 오퍼레이터를 Java로 개발해보기</title>
      <link>https://dev.gmarket.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스트:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.gmarket.com/102&quot;&gt;쿠버네티스 오퍼레이터를 Golang으로 개발해보기&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Cloud Strategy팀 박규민입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;지난번에 Golang으로 쿠버네티스 오퍼레이터를 간단하게 만들어 봤습니다. 하지만 국내에서는 아무래도 Golang보다는 Java의 수요가 압도적으로 많은데요. 이번 포스트로 Java로 오퍼레이터를 구현하는 과정을 보여드리겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Java Operator SDK&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;logo-2.png&quot; data-origin-width=&quot;2361&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7MgTk/btsIizyEI7x/DTAZ1JCOopIkxTktCrtwXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7MgTk/btsIizyEI7x/DTAZ1JCOopIkxTktCrtwXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7MgTk/btsIizyEI7x/DTAZ1JCOopIkxTktCrtwXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7MgTk%2FbtsIizyEI7x%2FDTAZ1JCOopIkxTktCrtwXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2361&quot; height=&quot;475&quot; data-filename=&quot;logo-2.png&quot; data-origin-width=&quot;2361&quot; data-origin-height=&quot;475&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Java Operator SDK는 Kubernetes Client Java API인&lt;span&gt;&amp;nbsp;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/fabric8io/kubernetes-client&quot;&gt;fabric8io&lt;/a&gt;를&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기반으로 작성되어 있습니다. 이는 세부적으로 쿠버네티스와 상호 작용하기 위한 Low Level 단에서의 코드 작성 걱정 없이 개발자에게 친숙한 Java API를 사용하여 오퍼레이터를 쉽게 작성할 수 있도록 설계되어 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Architecture&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;JavaOperatorSDKArch.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;1456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOuXMQ/btsIjLyocZ6/RXLIPTAB75ZtgPHVqr6eoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOuXMQ/btsIjLyocZ6/RXLIPTAB75ZtgPHVqr6eoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOuXMQ/btsIjLyocZ6/RXLIPTAB75ZtgPHVqr6eoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOuXMQ%2FbtsIjLyocZ6%2FRXLIPTAB75ZtgPHVqr6eoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1542&quot; height=&quot;1456&quot; data-filename=&quot;JavaOperatorSDKArch.png&quot; data-origin-width=&quot;1542&quot; data-origin-height=&quot;1456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Operator&lt;/b&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스의 집합이며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스가 Kubernetes 단일 리소스를 조정(Reconciling)해주는 역할을 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EventSourceManager&lt;/b&gt;가 Controller와 관련된 여러&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EventSource&lt;/b&gt;들의 수명 주기를 관리해 줍니다. 여기에서의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Event&lt;/b&gt;는 리소스 조정을 유발하는 사건을 의미합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EventSource&lt;/b&gt;에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EventProcessor&lt;/b&gt;에 전파되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Event&lt;/b&gt;를 생성합니다. (Controller와 관련된 기본 리소스의 변경 사항을 감시할 때는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ControllerResourceEventSource&lt;/b&gt;를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Event&lt;/b&gt;를 전파하여 관련 상태를 캐싱합니다)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Event&lt;/b&gt;를 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EventProcessor&lt;/b&gt;에서 리소스가 아직 처리되지 않은 경우, 적절한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Reconciler&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드로 호출하여 전달해 주는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ReconcilerDispatcher&lt;/b&gt;를 호출하여 필요한 모든 정보를 차례대로 전달합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reconciler&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드가 끝났을 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EventProcessor&lt;/b&gt;가 다시 호출되어 실행을 완료하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Controller&lt;/b&gt;의 상태를 업데이트합니다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Reconciler&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드에서 반환한 결과에 따라서 필요한 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ReconcilerDispatcher&lt;/b&gt;는 Kubernetes API 서버에 호출합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;EventProcessor&lt;/b&gt;는 요청 재시도를 해야 할지, 그리고 동일한 리소스에 대해 수신된 후속&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Event&lt;/b&gt;가 없는지 확인합니다. 이 중 어느 것도 일어나지 않으면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Event&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;처리가 완료됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;직접 구현해야 할 포인트는?&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;개발자들에게는 다음과 같은 구성 요소들을 Java Class로 만들 필요가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Primary Resource&lt;/b&gt;: k8s 클러스터에 배포할 CRD&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spec, Status&lt;/b&gt;: CRD에 필요한 내부 구성 요소
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Spec&lt;/b&gt;: 사용자가 CR에 적용할 상태 정의&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Status&lt;/b&gt;: CR의 현재 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Reconciler&lt;/b&gt;: Primary Resource의 변경사항을 감지하고 조정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;KubernetesDependentResource&lt;/b&gt;: CRD 배포의 결과로 클러스터에서 만들고 싶은 각 K8s 리소스(DeploymentConfig, Service, Ingress 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Project 생성&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 Spring Boot 기반으로 프로젝트를 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Intellij 기준으로 Spring Initializr Generators 메뉴를 통해 프로젝트를 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;731&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ei7rHX/btsIhcY0LLB/g1j6tVaY0qkPf4P9RVpQck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ei7rHX/btsIhcY0LLB/g1j6tVaY0qkPf4P9RVpQck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ei7rHX/btsIhcY0LLB/g1j6tVaY0qkPf4P9RVpQck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fei7rHX%2FbtsIhcY0LLB%2Fg1j6tVaY0qkPf4P9RVpQck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;729&quot; height=&quot;731&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;731&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSep4Q/btsIhvqtKD4/xmNwjs1kS4nPFl4Mildcc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSep4Q/btsIhvqtKD4/xmNwjs1kS4nPFl4Mildcc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSep4Q/btsIhvqtKD4/xmNwjs1kS4nPFl4Mildcc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSep4Q%2FbtsIhvqtKD4%2FxmNwjs1kS4nPFl4Mildcc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;734&quot; height=&quot;735&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;build.gradle에서 다음과 같이 수정합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719796610958&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
  id 'java'
  id 'org.springframework.boot' version '3.3.1'
  id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
  toolchain {
    languageVersion = JavaLanguageVersion.of(21)
  }
}

repositories {
  mavenCentral()
}

dependencies {
  annotationProcessor 'io.fabric8:crd-generator-apt:6.13.0'

  implementation 'org.springframework.boot:spring-boot-starter'
  implementation 'io.javaoperatorsdk:operator-framework-spring-boot-starter:5.5.0'

  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'io.javaoperatorsdk:operator-framework-spring-boot-starter-test:5.5.0'
  testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
  useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Custom Resource 작성&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;customresources라는 package를 만든 다음, 하위에 Spec, Status, Primary Resource Class를 만듭니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Spec Class&lt;/h3&gt;
&lt;pre id=&quot;code_1719796610959&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.customresources;

// Lombok @Data로도 적용 가능
public class PetclinicSpec {
  private String image;
  private Integer size;

  public String getImage() {
    return image;
  }

  public void setImage(String image) {
    this.image = image;
  }

  public Integer getSize() {
    return size;
  }

  public void setSize(Integer size) {
    this.size = size;
  }

  public Integer getPort() {
    return port;
  }

  public void setPort(Integer port) {
    this.port = port;
  }

  private Integer port;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Status Class&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;ObservedGenerationAwareStatus 클래스를 확장합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이는 k8s의 controller가 Petclinic CR에 대한 변경사항을 추적할 수 있도록 Petclinic 오브젝트가 매번 변경될 때마다 Petclinic CR 내에 observedGeneration status 값을 증가시킵니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719796610963&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.customresources;

import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus;

public class PetclinicStatus extends ObservedGenerationAwareStatus {
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Primary Resource Class&lt;/h3&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Petclinic CR을 Class로 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;CustomResource 추상클래스를 확장할 때 PetclinicSpec과 PetclinicStatus 클래스를 참조합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719796610964&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.customresources;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;

@Group(&quot;spring.my.domain&quot;)
@Version(&quot;v1&quot;)
public class Petclinic extends CustomResource&amp;lt;PetclinicSpec, PetclinicStatus&amp;gt; implements Namespaced {

  @Override
  public String toString() {
    return &quot;Petclinic{spec=&quot; + spec + &quot;, status=&quot; + status + &quot;}&quot;;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Dependent Resources 작성&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;dependentresources라는 package를 생성한 다음, 하위에 KubernetesDependentResource 클래스들을 만듭니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;CRUDKubernetesDependentResource 추상클래스를 확장하여 CRD에 필요한 k8s 리소스들의 manifest를 class로 정의합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;@KubernetesDependent 어노테이션을 통해 Petclinic CR의 변화에 대응하여 해당 k8s 리소스의 수명 주기를 관리합니다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Deployment&lt;/h3&gt;
&lt;pre id=&quot;code_1719796610966&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.dependentresources;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class PetclinicDeploymentResource extends CRUDKubernetesDependentResource&amp;lt;Deployment, Petclinic&amp;gt; {

  public PetclinicDeploymentResource() {
    super(Deployment.class);
  }

  @Override
  protected Deployment desired(Petclinic petclinic, Context&amp;lt;Petclinic&amp;gt; context) {
    final ObjectMeta petclinicMetadata = petclinic.getMetadata();
    final String petclinicName = petclinicMetadata.getName();

    return new DeploymentBuilder()
        .editMetadata()
          .withName(petclinicName)
          .withNamespace(petclinicMetadata.getNamespace())
          .addToLabels(&quot;app&quot;, petclinicName)
          .endMetadata()
        .editSpec()
          .withSelector(new LabelSelectorBuilder()
            .addToMatchLabels(&quot;app&quot;, petclinicName)
            .build())
          .withReplicas(petclinic.getSpec().getSize())
          .withTemplate(new PodTemplateSpecBuilder()
            .editMetadata()
            .addToLabels(&quot;app&quot;, petclinicName)
            .endMetadata()
            .editSpec()
            .withContainers(new ContainerBuilder()
                .withName(petclinicName + &quot;-container&quot;)
                .withImage(petclinic.getSpec().getImage())
                .addToPorts(new ContainerPortBuilder()
                  .withContainerPort(petclinic.getSpec().getPort())
                  .build())
                .build())
            .endSpec()
            .build())
        .endSpec()
        .build();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Service&lt;/h3&gt;
&lt;pre id=&quot;code_1719796610968&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.dependentresources;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import io.fabric8.kubernetes.api.model.*;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class PetclinicServiceResource extends CRUDKubernetesDependentResource&amp;lt;Service, Petclinic&amp;gt; {

  public PetclinicServiceResource() {
    super(Service.class);
  }

  @Override
  protected Service desired(Petclinic petclinic, Context&amp;lt;Petclinic&amp;gt; context) {
    final ObjectMeta petclinicMetadata = petclinic.getMetadata();
    final String petclinicName = petclinicMetadata.getName();

    return new ServiceBuilder()
        .editMetadata()
          .withName(petclinicName)
          .withNamespace(petclinicMetadata.getNamespace())
          .addToLabels(&quot;app&quot;, petclinicName)
        .endMetadata()
        .editSpec()
          .withType(&quot;NodePort&quot;)
          .addToSelector(&quot;app&quot;, petclinicName)
          .addToPorts(new ServicePortBuilder().withName(&quot;http&quot;).withPort(petclinic.getSpec().getPort()).withProtocol(&quot;TCP&quot;).withTargetPort(new IntOrStringBuilder().withValue(petclinic.getSpec().getPort()).build()).build())
        .endSpec()
        .build();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;Ingress&lt;/h3&gt;
&lt;pre id=&quot;code_1719796610970&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.dependentresources;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.networking.v1.*;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent
public class PetclinicIngressResource extends CRUDKubernetesDependentResource&amp;lt;Ingress, Petclinic&amp;gt; {

  public PetclinicIngressResource() {
    super(Ingress.class);
  }

  @Override
  protected Ingress desired(Petclinic petclinic, Context&amp;lt;Petclinic&amp;gt; context) {
    final ObjectMeta petclinicMetadata = petclinic.getMetadata();
    final String petclinicName = petclinicMetadata.getName();

    return new IngressBuilder()
        .editMetadata()
          .withName(petclinicName)
          .withNamespace(petclinicMetadata.getNamespace())
          .addToLabels(&quot;app&quot;, petclinicName)
        .endMetadata()
        .editSpec()
          .withIngressClassName(&quot;nginx&quot;)
          .withRules(new IngressRuleBuilder()
            .withHttp(new HTTPIngressRuleValueBuilder()
              .withPaths(new HTTPIngressPathBuilder()
                .withPath(&quot;/&quot;)
                .withPathType(&quot;Prefix&quot;)
                .withBackend(new IngressBackendBuilder()
                  .withService(new IngressServiceBackendBuilder()
                    .withName(petclinicName)
                    .withPort(new ServiceBackendPortBuilder()
                      .withNumber(petclinic.getSpec().getPort())
                      .build())
                    .build())
                  .build())
                .build())
              .build())
            .build())
        .endSpec()
        .build();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Reconciler 작성&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Petclinic Primary Resource의 변경사항을 감지하고 조정을 해주는 Reconciler 클래스를 작성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;@ControllerConfiguration 어노테이션을 붙여 dependents 속성을 통해 @KubernetesDependent 어노테이션을 붙인 KubernetesDependentResource 클래스를 @Dependent 어노테이션으로 여기에 연결합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719796610972&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava;

import com.example.petclinicoperatorjava.customresources.Petclinic;
import com.example.petclinicoperatorjava.dependentresources.PetclinicDeploymentResource;
import com.example.petclinicoperatorjava.dependentresources.PetclinicIngressResource;
import com.example.petclinicoperatorjava.dependentresources.PetclinicServiceResource;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import org.springframework.stereotype.Component;

@ControllerConfiguration(
    dependents = {
        @Dependent(type = PetclinicDeploymentResource.class),
        @Dependent(type = PetclinicServiceResource.class),
        @Dependent(type = PetclinicIngressResource.class)
    })
public class PetclinicReconciler implements Reconciler&amp;lt;Petclinic&amp;gt;, ErrorStatusHandler&amp;lt;Petclinic&amp;gt;, Cleaner&amp;lt;Petclinic&amp;gt; {

  @Override
  public UpdateControl&amp;lt;Petclinic&amp;gt; reconcile(Petclinic petclinic, Context&amp;lt;Petclinic&amp;gt; context) {
    return UpdateControl.updateResourceAndPatchStatus(petclinic);
  }

  @Override
  public DeleteControl cleanup(Petclinic petclinic, Context&amp;lt;Petclinic&amp;gt; context) {
    return DeleteControl.defaultDelete();
  }

  @Override
  public ErrorStatusUpdateControl&amp;lt;Petclinic&amp;gt; updateErrorStatus(Petclinic petclinic, Context&amp;lt;Petclinic&amp;gt; context, Exception e) {
    return ErrorStatusUpdateControl.patchStatus(petclinic);
  }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Config 작성&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;해당 Reconciler 클래스와 직접 구현한 PetclinicOperator를 각각 Bean으로 등록하도록 Config 클래스를 config package 하위에 만듭니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719796610975&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava.config;

import com.example.petclinicoperatorjava.PetclinicReconciler;
import io.javaoperatorsdk.operator.Operator;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class PetclinicOperatorConfig {

  @Bean
  public PetclinicReconciler petclinicReconciler() {
    return new PetclinicReconciler();
  }

  @Bean(initMethod = &quot;start&quot;, destroyMethod = &quot;stop&quot;)
  public Operator operator(List&amp;lt;Reconciler&amp;lt;?&amp;gt;&amp;gt; controllers) {
    Operator operator = new Operator();
    controllers.forEach(operator::register);
    return operator;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;k8s 클러스터에 CRD 적용&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;io.fabric8:crd-generator-apt 라이브러리에 의해 프로젝트 컴파일을 진행할 시 자동으로 build 디렉토리 내부에서 classpath/META-INF/fabric8 디렉토리에 yml 형식으로 생성됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://dev.gmarket.com/102&quot;&gt;지난 시간&lt;/a&gt;에 구축한 kind 기반의 로컬 k8s 클러스터에 CRD로 등록합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719796610977&quot; class=&quot;stylus&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl apply -f ./build/classes/java/main/META-INF/fabric8/petclinics.spring.my.domain-v1.yml
customresourcedefinition.apiextensions.k8s.io/petclinics.spring.my.domain created&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;로컬 테스트&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;PetclinicOperatorJavaApplication에서 main 클래스를 실행합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FZqVj/btsIhTxCzvm/UPwNvF6vnDwoi4mVvyXXKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FZqVj/btsIhTxCzvm/UPwNvF6vnDwoi4mVvyXXKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FZqVj/btsIhTxCzvm/UPwNvF6vnDwoi4mVvyXXKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFZqVj%2FbtsIhTxCzvm%2FUPwNvF6vnDwoi4mVvyXXKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;215&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;kind cluster에 Petclinic manifest가 적용이 잘 되는지 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719796610978&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ kubectl config set-context --current --namespace=petclinic

$ kubectl apply -f - &amp;lt;&amp;lt;EOF
apiVersion: spring.my.domain/v1
kind: Petclinic
metadata:
  name: sample
spec:
  image: springio/petclinic
  size: 1
  port: 8080
EOF
petclinic.spring.my.domain/sample created

$ kubectl get all
NAME                          READY   STATUS    RESTARTS   AGE
pod/sample-594c8976df-5gmzr   1/1     Running   0          47s

NAME             TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/sample   NodePort   10.96.34.237   &amp;lt;none&amp;gt;        8080:32494/TCP   48s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample   1/1     1            1           48s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-594c8976df   1         1         1       48s

$ kubectl get ing
NAME     CLASS   HOSTS   ADDRESS     PORTS   AGE
sample   nginx   *       localhost   80      69s&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;http://xn--localhost-ji89a/&quot;&gt;http://localhost로&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;접속하여 Petclinic 메인 화면이 나오는지 확인합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFksUj/btsIiUvN2Mz/NyISI6WutrjNV6z85Q4fC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFksUj/btsIiUvN2Mz/NyISI6WutrjNV6z85Q4fC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFksUj/btsIiUvN2Mz/NyISI6WutrjNV6z85Q4fC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFksUj%2FbtsIiUvN2Mz%2FNyISI6WutrjNV6z85Q4fC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;448&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Integration Test&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;@EnableMockOperator 어노테이션을 통해 k8s 클러스터를 mocking 하여 직접 구현한 Operator의 CRD를 적용하여 전용 통합 테스트를 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719796610978&quot; class=&quot;css&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.petclinicoperatorjava;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.springboot.starter.test.EnableMockOperator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@EnableMockOperator(crdPaths = &quot;classpath:META-INF/fabric8/petclinics.spring.my.domain-v1.yml&quot;)
public class PetclinicOperatorUnitTest {

  @Autowired
  KubernetesClient k8sClient;

  @Test
  void whenContextLoaded_thenCrdApplied() {
    assertThat(
        k8sClient
            .apiextensions()
            .v1()
            .customResourceDefinitions()
            .withName(&quot;petclinics.spring.my.domain&quot;)
            .get()
    ).isNotNull();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;Operator 배포&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 gradle build task를 실행하여 jar 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OzmIc/btsIjIDEie7/dNVwb0vm9fo2IGK3NHQjG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OzmIc/btsIjIDEie7/dNVwb0vm9fo2IGK3NHQjG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OzmIc/btsIjIDEie7/dNVwb0vm9fo2IGK3NHQjG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOzmIc%2FbtsIjIDEie7%2FdNVwb0vm9fo2IGK3NHQjG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;206&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 프로젝트 밑에 k8s라는 디렉토리를 만들어 다음과 같이 Dockerfile(./k8s/Dockerfile)을 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719968440583&quot; class=&quot;dockerfile&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;FROM bellsoft/liberica-openjdk-alpine:21

COPY build/libs/*-0.0.1-SNAPSHOT.jar /opt/app/app.jar

EXPOSE 80

CMD [&quot;java&quot;, &quot;-showversion&quot;, &quot;-jar&quot;, &quot;/opt/app/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에서 docker build 명령어를 실행하여 petclinic java operator 이미지를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719968440583&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;# 개인 docker public registry에 petclinic java operator를 push합니다.
# 현재 로컬 kind cluster에서의 worker node는 linux/arm64 기반이어서 맞출 필요가 있음
$ docker build -f ./k8s/Dockerfile -t ycatt/petclinic-operator-java:0.0.1 . --platform &quot;linux/arm64&quot;

$ docker login

$ docker push ycatt/petclinic-operator-java:0.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kind 로컬 클러스터에 다음과 같은 매니페스트 파일을 배포합니다. (./k8s/petclinic-operator.yaml)&lt;/p&gt;
&lt;pre class=&quot;yaml&quot; style=&quot;background-color: #ffffff; color: #080808; text-align: start;&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;apiVersion: v1
kind: ServiceAccount
metadata:
  name: petclinic-operator

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: petclinic-operator
spec:
  selector:
    matchLabels:
      app: petclinic-operator
  replicas: 1
  template:
    metadata:
      labels:
        app: petclinic-operator
    spec:
      serviceAccountName: petclinic-operator
      containers:
        - name: petclinic-operator
          image: ycatt/petclinic-operator-java:0.0.1
          imagePullPolicy: Always
          ports:
            - containerPort: 80

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: petclinic-operator-admin
subjects:
  - kind: ServiceAccount
    name: petclinic-operator
    namespace: default
roleRef:
  kind: ClusterRole
  name: petclinic-operator
  apiGroup: &quot;&quot;

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: petclinic-operator
rules:
  - apiGroups:
      - &quot;&quot;
      - &quot;extensions&quot;
      - &quot;apps&quot;
    resources:
      - deployments
      - services
      - pods
      - pods/exec
    verbs:
      - '*'
  - apiGroups:
      - &quot;apiextensions.k8s.io&quot;
    resources:
      - customresourcedefinitions
    verbs:
      - '*'
  - apiGroups:
      - &quot;spring.my.domain&quot;
    resources:
      - petclinics
    verbs:
      - '*'
  - apiGroups:
      - &quot;networking.k8s.io&quot;
    resources:
      - ingresses
    verbs:
      - '*'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1719968440585&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;$ kubectl apply -f ./k8s/petclinic-operator.yaml
serviceaccount/petclinic-operator created
deployment.apps/petclinic-operator created
clusterrolebinding.rbac.authorization.k8s.io/petclinic-operator-admin created
clusterrole.rbac.authorization.k8s.io/petclinic-operator created

$ kubectl get pod -n default
NAME                                  READY   STATUS    RESTARTS   AGE
petclinic-operator-6dc9f97b96-qg84p   1/1     Running   0          20m

$ kubectl logs petclinic-operator-6dc9f97b96-qg84p -n default
openjdk version &quot;21.0.3&quot; 2024-04-16 LTS
OpenJDK Runtime Environment (build 21.0.3+10-LTS)
OpenJDK 64-Bit Server VM (build 21.0.3+10-LTS, mixed mode)

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.1)

2024-07-02T06:57:28.720Z  INFO 1 --- [petclinic-operator-java] [           main] c.e.p.PetclinicOperatorJavaApplication   : Starting PetclinicOperatorJavaApplication v0.0.1-SNAPSHOT using Java 21.0.3 with PID 1 (/opt/app/app.jar started by root in /)
2024-07-02T06:57:28.723Z  INFO 1 --- [petclinic-operator-java] [           main] c.e.p.PetclinicOperatorJavaApplication   : No active profile set, falling back to 1 default profile: &quot;default&quot;
2024-07-02T06:57:29.603Z  WARN 1 --- [petclinic-operator-java] [           main] ault ConfigurationService implementation : Configuration for reconciler 'petclinicreconciler' was not found. Known reconcilers: None.
2024-07-02T06:57:29.619Z  INFO 1 --- [petclinic-operator-java] [           main] ault ConfigurationService implementation : Created configuration for reconciler com.example.petclinicoperatorjava.PetclinicReconciler with name petclinicreconciler
2024-07-02T06:57:29.657Z  INFO 1 --- [petclinic-operator-java] [           main] io.javaoperatorsdk.operator.Operator     : Registered reconciler: 'petclinicreconciler' for resource: 'class com.example.petclinicoperatorjava.customresources.Petclinic' for namespace(s): [all namespaces]
2024-07-02T06:57:29.659Z  INFO 1 --- [petclinic-operator-java] [           main] io.javaoperatorsdk.operator.Operator     : Operator SDK 4.9.1 (commit: 135b239) built on Tue May 28 08:09:11 GMT 2024 starting...
2024-07-02T06:57:29.666Z  INFO 1 --- [petclinic-operator-java] [           main] io.javaoperatorsdk.operator.Operator     : Client version: 6.12.1
2024-07-02T06:57:29.667Z  INFO 1 --- [petclinic-operator-java] [linicreconciler] i.j.operator.processing.Controller       : Starting 'petclinicreconciler' controller for reconciler: com.example.petclinicoperatorjava.PetclinicReconciler, resource: com.example.petclinicoperatorjava.customresources.Petclinic
2024-07-02T06:57:30.233Z  INFO 1 --- [petclinic-operator-java] [linicreconciler] i.j.operator.processing.Controller       : 'petclinicreconciler' controller started
2024-07-02T06:57:30.360Z  INFO 1 --- [petclinic-operator-java] [           main] c.e.p.PetclinicOperatorJavaApplication   : Started PetclinicOperatorJavaApplication in 1.885 seconds (process running for 2.43)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE에서 실행한 Petclinic Operator Application을 Terminate한 뒤, 기존에 로컬에서 테스트했던 Petclinic CR을 삭제하고 재생성하여 정상적으로 메인 페이지 접속이 되는 지 확인합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1719968440585&quot; class=&quot;sql&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;$ kubectl delete petclinic --all
petclinic.spring.my.domain &quot;sample&quot; deleted

$ kubectl apply -f - &amp;lt;&amp;lt;EOF
apiVersion: spring.my.domain/v1
kind: Petclinic
metadata:
  name: sample
  namespace: petclinic
spec:
  image: springio/petclinic
  size: 1
  port: 8080
EOF
petclinic.spring.my.domain/sample created

$ kubectl get all -n petclinic
NAME                          READY   STATUS    RESTARTS   AGE
pod/sample-594c8976df-k4944   1/1     Running   0          8s

NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/sample   NodePort   10.96.140.108   &amp;lt;none&amp;gt;        8080:30502/TCP   8s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample   1/1     1            1           8s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-594c8976df   1         1         1       8s

$ kubectl get ing -n petclinic
NAME     CLASS   HOSTS   ADDRESS     PORTS   AGE
sample   nginx   *       localhost   80      44s

# curl로 확인
$ curl http://localhost:80 -v -o /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
&amp;gt; GET / HTTP/1.1
&amp;gt; Host: localhost
&amp;gt; User-Agent: curl/8.1.2
&amp;gt; Accept: */*
&amp;gt;
&amp;lt; HTTP/1.1 200
&amp;lt; Date: Tue, 02 Jul 2024 07:31:39 GMT
&amp;lt; Content-Type: text/html;charset=UTF-8
&amp;lt; Transfer-Encoding: chunked
&amp;lt; Connection: keep-alive
&amp;lt; Content-Language: en-US
&amp;lt;
{ [3168 bytes data]
100  3161    0  3161    0     0  10577      0 --:--:-- --:--:-- --:--:-- 10788
* Connection #0 to host localhost left intact&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;마치면서...&lt;/h2&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 Java를 사용하여 Kubernetes Operator를 직접 구현해 보았습니다. Golang에 비해 구조가 비교적 간단하고 친숙하여 더 쉽게 접근할 수 있다는 느낌이 있는데요. 기존 Java/Spring Boot 기반 서비스와의 연계성, 개발자 커뮤니티의 폭넓은 지원까지 생각해 본다면 Kubernetes Operator를 개발할 때 Java라는 언어로도 꽤나 매력적인 선택이 될 수 있습니다. Java Operator SDK 활용에 작은 도움이 되었기를 바라면서 글을 마치겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;감사합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;참조&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #24292f; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://javaoperatorsdk.io/docs&quot;&gt;https://javaoperatorsdk.io/docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/operator-framework/java-operator-sdk/tree/main/sample-operators&quot;&gt;https://github.com/operator-framework/java-operator-sdk/tree/main/sample-operators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/java-kubernetes-operator-sdk&quot;&gt;https://www.baeldung.com/java-kubernetes-operator-sdk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techtalk.11stcorp.com/2022/pdf/TECH-TALK-2022_SESSION-02.pdf&quot;&gt;https://techtalk.11stcorp.com/2022/pdf/TECH-TALK-2022_SESSION-02.pdf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/112</guid>
      <comments>https://dev.gmarket.com/112#entry112comment</comments>
      <pubDate>Mon, 1 Jul 2024 00:00:00 +0900</pubDate>
    </item>
    <item>
      <title>신규 서비스 &amp;quot;꿀템&amp;quot;을 만들기 위한 여정(네? 다음달까지요?) -2편</title>
      <link>https://dev.gmarket.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. Web Frontend팀 이민하입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 편에서 꿀템 서비스를 기획하고 필요한 개념들의 이름을 지어주며 이를 바탕으로 데이터베이스를 설계해 보았습니다. 이번 편에서는 어떤 기술 스택을 선택했는지 소개하도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;기술 스택 선택과 개발&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_Attraction Data Flow.png&quot; data-origin-width=&quot;1243&quot; data-origin-height=&quot;1266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfyI3c/btsIiqIljjt/9dRXV13rdEc1Xofxlkjg8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfyI3c/btsIiqIljjt/9dRXV13rdEc1Xofxlkjg8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfyI3c/btsIiqIljjt/9dRXV13rdEc1Xofxlkjg8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfyI3c%2FbtsIiqIljjt%2F9dRXV13rdEc1Xofxlkjg8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1243&quot; height=&quot;1266&quot; data-filename=&quot;edited_Attraction Data Flow.png&quot; data-origin-width=&quot;1243&quot; data-origin-height=&quot;1266&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;External 망에는 기존에 BSD 프론트엔드 영역 어플리케이션들이 있습니다. node.js와 경량 웹프레임워크인 fastify로 되어있습니다. 프론트에서 api를 호출하면 attraction 집계 어플리케이션이 메인 데이터 저장소인 Oracle DB에서 데이터를 조회해 옵니다. 구매내역, 링크루 등 외부 api를 호출한 결과도 전달해 줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장된 데이터는 Admin화면을 통해 관리할 수 있습니다. 누가 어떤 피드를 작성했고 누가 좋아요 버튼을 눌렀는지, 구매는 얼마나 일어났는지 등을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;프론트엔드는 별도의 서버를 구축하지 않고 플러그인 형태로 개발되었습니다.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;event&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/i&gt;&amp;nbsp;관련 어플리케이션은 프론트엔드 이름이 붙어있지만 여러 프로모션 영역의 비즈니스 로직이 많이 들어있는 어플리케이션입니다. 이번 BSD는 랭킹 탭 옆에 신규 탭으로 꿀템 탭이 신설될 것이므로 event 어플리케이션 안에 신규 생성한 attraction 어플리케이션을 번들 형태로 import 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z8xti/btsIjdIx4rI/wiAfSitPETSfPkJKYuglA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z8xti/btsIjdIx4rI/wiAfSitPETSfPkJKYuglA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z8xti/btsIjdIx4rI/wiAfSitPETSfPkJKYuglA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz8xti%2FbtsIjdIx4rI%2FwiAfSitPETSfPkJKYuglA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;371&quot; height=&quot;439&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Typescript, React 18.2&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tanstack&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;query (aka. react-query)&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;api&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;fetching&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기능을&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;꿀템의&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등록&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;편집&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;좋아요 처리에 개별적으로 상태를 관리하지 않아도&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;되서&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;개발에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;유리하였고요&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Tanstack&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;virtual을 이용해 무한스크롤 기능을 구현하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;event-fe와 마찬가지로 Fastify 프레임워크를 사용하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;Vite로 번들링 하여 사내 배포시스템을 통해 배포하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 아래의 특징을 갖고 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;attraction 집계 api 호출 및 사용자 입력값 검증 (xss 방어)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이벤트 프론트 저장소가 분리되어 있고 꿀템 저장소(attraction 프론트엔드)도 별도라서 꿀템 쪽 fastify용 코드는 attraction 프론트에서 개발하고 결과물을 fastify plugin 형태의 npm 패키지로 생성해서 이벤트 프론트 쪽에서 가져다 쓰는 방식으로 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;개발 중에는 attraction 프론트엔드에서 이벤트 프론트의 구성과 유사하게 dev server를 띄워서 개발하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;백엔드의 기술스택은 다음과 같습니다.&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1623&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9xMAI/btsIg5enoCW/BkfchCPLK2359lfY4jUPX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9xMAI/btsIg5enoCW/BkfchCPLK2359lfY4jUPX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9xMAI/btsIg5enoCW/BkfchCPLK2359lfY4jUPX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9xMAI%2FbtsIg5enoCW%2FBkfchCPLK2359lfY4jUPX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1623&quot; height=&quot;717&quot; data-origin-width=&quot;1623&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 영역에서는 C#&amp;nbsp;닷넷과&amp;nbsp;Node.js로 이루어진 어플리케이션이 많은데 팀 내에서 처음으로&amp;nbsp;Spring과&amp;nbsp;Kotlin을 적용하여 어플리케이션을 개발했습니다. 오라클을 사용하니 Stored Procedure&amp;nbsp;대신&amp;nbsp;jpa와&amp;nbsp;QueryDSL을 적용하였습니다.&lt;/li&gt;
&lt;li&gt;Admin 서비스는 스프링 멀티모듈로 만들어 웹 UI와 oracle에 접속할 api가 같은 도메인 모델을 공유하도록 하였습니다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사내에서 사용되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공통&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Admin 시스템은&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사용하는 모든 도메인이 관련 프론트엔드&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저장소 한 곳에&amp;nbsp;있다 보니&amp;nbsp;개발&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배포가 번거로울 때가 있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트가 무거워 빌드에도 시간이 꽤 걸리고 어플리케이션 별로 자체 스케일링하기 어렵습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;. 관련 어드민&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에서는 외부 시스템과의 연동을 위해&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SSO(Single Sign On)&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 지원합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이를 이용해 사내&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿠버네티스&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;시스템인&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fusion&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에 올릴 수 있는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;java/&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;kotlin&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;, node.js&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;등으로 변경하면 관리포인트가 줄어 유지보수에 매우 도움이 됩니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 닷넷&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프레임웍을&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;쓰지 않기 때문에 맥북으로 개발이 가능해집니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;하지만&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;사내 어드민&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;공통 기능은 자체적으로 구현해야 하고 별도의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;admin&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;qa&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 거쳐야 하므로 개발기간이 길어질 수 있어 개발과 유지보수의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;trade off&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;관계라고 생각합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;팀 내에서는 같은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기술스택이지만 다른&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;어드민&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;을 작업하고 있었기 때문에 수월하게 적용할 수 있었습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;HTMX +&amp;nbsp;&lt;span style=&quot;color: #000000;&quot;&gt;Thymeleaf&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;rarr; 마찬가지로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;cshtml&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 아닌&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;htmx&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 이용하여&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;admin&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ui&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;구성했습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;완성도보다는 빠르게 기능을 구현하는데 목적을 둔 어플리케이션이지만&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;htmx&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적용을 테크니컬 챌린지라고 말씀드릴 수 있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;. (&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;물론 일정이&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;촉박해질수록&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;후회를&amp;nbsp;안 할 수가&amp;nbsp;없었지만&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;..)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;HTMX에 대해 한마디 하자면...&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1819&quot; data-origin-height=&quot;786&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5jSUV/btsIjc3VwUN/s6saFkuUonBYG6RwuTW15K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5jSUV/btsIjc3VwUN/s6saFkuUonBYG6RwuTW15K/img.png&quot; data-alt=&quot;html만 봐도 이해하기 쉽습니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5jSUV/btsIjc3VwUN/s6saFkuUonBYG6RwuTW15K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5jSUV%2FbtsIjc3VwUN%2Fs6saFkuUonBYG6RwuTW15K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1819&quot; height=&quot;786&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1819&quot; data-origin-height=&quot;786&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;html만 봐도 이해하기 쉽습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;htmx&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 별도의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스크립트를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이용하지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;않고 서버와&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ajax&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;통신을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;도와주는&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;프론트엔드&amp;nbsp;웹&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프레임워크입니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기존에&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;통신하기&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위해&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수많은&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;js&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드를 만들어야&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;했는데 보일러 플레이트를 꽤&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;많이 없애&amp;nbsp;코드량&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자체가 줄어드는 효과가 있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;ldquo;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;hx&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;-&amp;rdquo;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프리픽스가&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;붙은 속성을&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;선언해 간편하게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원하는 데이터를 만들어 낼 수 있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요한 기능은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Extension&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;으로&amp;nbsp;만들 수 있어 사용성이 무궁무진합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또 빌드 단계도 별도로 없어 즉시 개발할&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수 있게 도와줍니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장단점이 아주 명확한데요&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;장점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서버 통신할 때&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;js&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정말&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용하지 않고&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;html&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;본연의 목적에 충실할 수 있다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;thymeleaf&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;와 결합하면 레이아웃 재사용으로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리액트의&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;컴포넌트 재사용과&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 효과를 가질 수 있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;백엔드&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;개발자 혼자서 프론트를 만들 수 있다는 것도 장점입니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단점은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;api&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;통신을 주고받았다 보니까 받았다 보니까&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;html 시맨틱&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;태그를 응답받는 것에 적응할 시간이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;필요하다는&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;것과&amp;nbsp; 간단한 통신을 해도 쉽지 않다는 것입니다. 특히 컨트롤러에서 @RequestBody만 주로 사용해 오다가 @RequestPart, @ModelAttribute로 바꾸고 data class가 바인딩되지 않는 이슈는 한참을 헤매게 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터를 주고받는 데이터&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;패킷량도&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;많아져 트래픽이 많이 몰리는 프론트 서비스에서는 사용이 부담스러울&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수 있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;무엇보다 필요한 기능은 결국&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;js&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를&amp;nbsp;써야 합니다 써야 합니다. Csv&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;엑셀 다운로드&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;, form reset&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이나&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;date-picker&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 사용하기&amp;nbsp; 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;공통 함수&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;작성&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;..&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;typescript&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 쓸 수 없다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;..는 것이 또 큰 단점이죠.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Htmx&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;레퍼런스가 많이 없어 거대한&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;js&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;덩어리를 디버깅해 가며 입맛에 맞게 수정했던 경험도 있네요&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;htmx에 대해 아주 신랄하게 까는 아티클이 공식 홈페이지에 게시되어 있습니다..(&lt;a href=&quot;https://htmx.org/essays/htmx-sucks/&quot;&gt;https://htmx.org/essays/htmx-sucks/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어플리케이션 구현할 때 기술스택에 있어서 정답은 없습니다. fastify 대신 express를 쓸 수도 있고 nest.js를 적용할 수도 있습니다. spring 대신에 ktor를 사용할 수도, htmx 대신에 handlebar나 rythm 템플릿, jsp를 쓸 수도 있고요. 다양한 기술들은 저마다의 다양한 장단점을 가지고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2주간 매일 스크럼을 진행했습니다.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;짧게는 1분 길게는 10분 정도의 데일리 스크럼을 진행했습니다. 현재 진행하고 있는 작업과 진척 상황을 공유했습니다. 데일리 스크럼을 통해 프로젝트의 불확실성을 조율할 수 있었고, 짧은 시간 동안만 진행하기 때문에 불필요한 추가 업무나 의논이 생기지 않았습니다. 또 별도의 회의실이 필요하지 않아 자리에 모여 진행할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byPaT4/btsIgN58gfC/GI4zwyatupJwuyrn74ya31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byPaT4/btsIgN58gfC/GI4zwyatupJwuyrn74ya31/img.png&quot; style=&quot;width: 37.1105%; margin-right: 10px;&quot; data-filename=&quot;blob&quot; data-widthpercent=&quot;37.55&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;971&quot; data-origin-width=&quot;619&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byPaT4/btsIgN58gfC/GI4zwyatupJwuyrn74ya31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyPaT4%2FbtsIgN58gfC%2FGI4zwyatupJwuyrn74ya31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;971&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b265Qm/btsIiRyXJGs/7KFWncvKYgvW45SBUKex20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b265Qm/btsIiRyXJGs/7KFWncvKYgvW45SBUKex20/img.png&quot; style=&quot;width: 61.7267%;&quot; data-filename=&quot;edited_blob&quot; data-widthpercent=&quot;62.45&quot; data-is-animation=&quot;false&quot; data-origin-height=&quot;580&quot; data-origin-width=&quot;615&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b265Qm/btsIiRyXJGs/7KFWncvKYgvW45SBUKex20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb265Qm%2FbtsIiRyXJGs%2F7KFWncvKYgvW45SBUKex20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;팀 내에서 자체 QA를 진행했습니다.&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스가 론칭되려면&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QA(&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Quality Assurance) 단계를 거쳐 서비스의 이상은 없는지 확인을 하는데요&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QE&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;팀과 담당&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PM&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;분들이 같이 진행을 합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;. 사내&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PM&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;분들의 리소스가 넉넉한 편이 아닙니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때 저희 팀원분들의 자체&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QA&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단계를 먼저 진행해 굉장히 많은 도움을 받을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;있었습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스의 대략적인 프로세스만 설명하고 별도의 가이드가 제공되지 않은 상태로 자체&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QA&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를 진행했는데요&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사소한 버그부터 불편하다고 생각하는 동선&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;혹은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기획 아이디어 등 수준 높은 피드백을 받을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수 있었습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한 참여하신 모두가&amp;nbsp;개발자이신 만큼&amp;nbsp;이를 개선할 방법을 제시해주시기도 하셨습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QA&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;시나리오에 없는 내용들도 꼼꼼하게 봐주셨기 때문에 서비스가 더욱 견고해질&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;있었던 이유라고 생각합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;론칭 준비 중인 쇼룸도 자체&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QA를&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;진행해서 굉장히 큰 도움을 받았는데요, 개인적으로 저는 이런 팀 문화가 매우 만족스럽습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;1409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSFANb/btsIhfOy67j/SBBVQKBuCLj5BNPXHbLfBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSFANb/btsIhfOy67j/SBBVQKBuCLj5BNPXHbLfBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSFANb/btsIhfOy67j/SBBVQKBuCLj5BNPXHbLfBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSFANb%2FbtsIhfOy67j%2FSBBVQKBuCLj5BNPXHbLfBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1495&quot; height=&quot;1409&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1495&quot; data-origin-height=&quot;1409&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발을 하던 와중에 마주한 몇 가지 이슈 상황이 있었는데요, 몇 가지 소개해보도록 하겠습니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Oracle DB에서 pk가 순차적(serial)으로 채번 되지 않는 이유가 있었습니다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mWaQd/btsIhgUdCDf/Avu9Khhs4WB7TvSk43khtK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mWaQd/btsIhgUdCDf/Avu9Khhs4WB7TvSk43khtK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mWaQd/btsIhgUdCDf/Avu9Khhs4WB7TvSk43khtK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/mWaQd/btsIhgUdCDf/Avu9Khhs4WB7TvSk43khtK/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;471&quot; height=&quot;474&quot; data-origin-width=&quot;471&quot; data-origin-height=&quot;474&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기능을 구현하고&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;dev&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;테스트를 진행해 보니&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;pk&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;가 순차적이지 않고&amp;nbsp;들쑥날쑥이었습니다들쑥날쑥 이었습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;저는 주로 사용했던&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PostgreSQL&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에서는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Serial&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터 타입이 존재해 기본적으로&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;pk&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;는 순서가 보장되었지만&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Oracle&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;은 뭔가 달랐습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;OracleDB&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RAC(Real Application Cluster)&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구조로 되어있습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;네트워크 단의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;L4, L7&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스위치 같은&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로드밸런서와&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;비슷한 역할을 하고 있는데요&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;, N&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Instance&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;를&amp;nbsp;통해 동일&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DB&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;에&amp;nbsp;접근할 수 있는 방법입니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;N&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개의&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Instance DB&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의 자원을 이용하기 때문에&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한&amp;nbsp;node&amp;nbsp;node의 부하를 분산시키는 효과를 갖습니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 RAC 구조에서 Sequence를 생성하게 되면 설정되어 있는 CACHE 옵션만큼 채번을 해 메모리로 올려둡니다. (CACHE 옵션이 20이라면 1번 node에서 1~20까지, 2번 node에서 21~40까지...)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실제 트랜잭션에서는 커넥션을 맺은 Instance의 채번 된 sequence를 가져오게 됩니다. 그래서 순서가 보장되지 않을 수 있는 것이죠 (1, 21, 41...) pk 채번이 비즈니스 적으로 반드시 순차적인 Sequence가 보장되어야 할 때(주문, 결제 관련 트랜잭션 등)는 ORDER 옵션을 사용해 순서를 보장할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나 RAC 구조에서&amp;nbsp;CACHE + ORDER 옵션을 설정하고 Sequence의 NEXTVAL()을 사용해 채번을 하게 되면 Data Dictionary가 자주 변경 되고 E&lt;span&gt;&lt;span&gt;xclusive instance SV lock을 필요로 하게 됩니다. 이는 동일 세션에서 Hang을 유발할 수 있기 때문에 DB에 부하를 주게 됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Cache 크기를 크게 잡는다면 메모리 휘발로 인해 오히려 사용되지 않을 수 있으므로 적절한 캐시 크기를 잡는 것이 중요합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 보통의 경우 유일함(Unique)만을 보장하고 싶다면 NO ORDER 옵션을 통해 Sequence를 채번하는 것이 좋습니다. 오라클에서도 CACHE 크기는 크게 잡고 NO ORDER 옵션을 권장합니다. (캐싱을 하지 않는 NO CACHE 옵션은 Oracle에서도 사용하지 말라고 권고) RAC에 대해 더 알고 싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://docs.oracle.com/en/database/oracle/oracle-database/19/racad/introduction-to-oracle-rac.html#GUID-D04AA2A7-2E68-4C5C-BD6E-36C62427B98E&quot;&gt;Oracle 공식 가이드&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;SP 대신 JPA, QueryDSL을 사용했습니다.&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MSSQL의 경우에는 SP(Stored Procedure)라고 불리는 방식의 쿼링을 하는데 native query를 미리 만들어두고 이를 검수, 배포하여 어플리케이션에서 사용합니다. 쿼리의 성능을 보장하고 전사적으로 모니터링을 하기 위함입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Oracle / MySQL의 경우에는 SP를 사용을 권장하지 않기 때문에 다른 DB팀의 검수 및 배포하지 않습니다. 대신 DB Consulting Jira를 통해 검수를 진행합니다. 사내에서 JPA, queryDSL 적용 가이드가 있었는데 다만 저희는 이러한 프로세스를 인지하지 못했었는데요, 실제로 dev 테스트 중에 DB Index를 설정하지 않아 조회 시 Full Scan이 일어났고 실행 시간이 매우 길어졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 후 실제 적용할 Query는 만들었으나 이를 queryDSL로 포팅하는 과정에서 애를 먹었습니다. Admin에서 구매건별 구매자, 구매 금액등을 검색조건과 매핑하기 위한 화면을 그릴 때 필요한 상황이 있었는데요, 구매데이터를 저희가 생성하는 게 아니라 링크루 서비스로부터 받아오기 때문에 join조건이 상당히 복잡한 쿼리였습니다. 그 와중에 from절과 join절에 서브 쿼리를 사용하는 것을 queryDSL에서 지원을 하지 않아 의도한 대로 쿼리를 만들어낼 수 없는 이슈였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러 자료를 찾다가 subQuery 자체를 마치 View처럼 Entity화 하는 방법을 발견했고 이를 적용해 해결할 수 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;1626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btOYPp/btsIgOqqvcT/OWJEHYQvgKSjgIBNThVbtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btOYPp/btsIgOqqvcT/OWJEHYQvgKSjgIBNThVbtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btOYPp/btsIgOqqvcT/OWJEHYQvgKSjgIBNThVbtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtOYPp%2FbtsIgOqqvcT%2FOWJEHYQvgKSjgIBNThVbtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1346&quot; height=&quot;1626&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1346&quot; data-origin-height=&quot;1626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;사내에선 다양한 도구를 활용해 프러덕을 만들어갑니다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecm9lG/btsIhArrUWA/WVZeDuwdz38blHp8SWnk8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecm9lG/btsIhArrUWA/WVZeDuwdz38blHp8SWnk8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecm9lG/btsIhArrUWA/WVZeDuwdz38blHp8SWnk8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fecm9lG%2FbtsIhArrUWA%2FWVZeDuwdz38blHp8SWnk8K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1116&quot; height=&quot;541&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사내에서 정말 다양한 개발도구를 이용해 업무를 진행합니다. it 인프라를 적극 활용하는 것이 빠르고 정확하게 서비스를 만드는 과정이라고 생각합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Git &amp;amp; Github&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발소스는 형상관리 툴 중하나인 Git을 이용하고 있으며 사내 GitHub에서 소스 관리하고 있습니다. 협업을 하기 위한 필수 도구로 여러 개발자가 한 어플리케이션을 개발할 때 작업 내용, history 등을 볼 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Jira&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모든 업무의 끝과 끝에는 Jira가 있습니다. 업무의 최소 단위이기에 History 관리가 잘되어야 합니다. 사내 Infra 요청에도 Jira를 사용하기에 익숙해지는 것이 좋을 것 같습니다. 각종 문의도 jira를 통합니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Saturn Initializr&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;세상에는 다양한 개발 도구와 이를 도와주는 라이브러리들이 있는데 보안을 이유로 사내에서만 사용하는 패키지들이 있습니다. 백엔드 어플리케이션을 생성할 때 필요한 사내 패키지를 주입해 만들어주는 Saturn Initializr를 통해 더욱 빠르게 만들 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Fusion &amp;amp; 사내 VM 툴&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사내 Cloud Native 오케스트레이션 툴인 Fusion은 RedHat이라는 회사에서 만든 Openshift라는 툴입니다. Github 주소로부터 개발소스를 불러와 빌드, 배포하고 라우팅 할 수 있는 강력한 도구입니다. BSD 같은 큰 이벤트를 앞두고 아주 쉽게 똑같은 서버를 복제해서 운영할 수 있기 때문에 개발자들에게 있어 필수로 다뤄야 하는 툴이라고 생각합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Cloud 자원이 아닌 가상 물리 서버인 Virtual Machine은 사내에선 다른 이름으로 제공해주고 있습니다. 저는 리눅스 서버를 다루는 게 편해서 해커톤이나 메세지큐 적용을 위한 poc를 해당 서비스를 통해 이용해 본 경험이 있습니다. 요새는 대부분의 툴들을 Docker 이미지로 받을 수 있으니 fusion을 이용하는 추세입니다. 사내에 docker 이미지 저장소에 원하는 이미지가 없다면 보안검수를 통해 등록하는 것이 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Kibana &amp;amp; 사내 로그 모니터링 시스템&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠버네티스 환경에서 최소 단위의 하드웨어, 즉 서버를 Pod라고 부르는데 이 Pod 개수가 수십, 수백 개가 되면 각 서버 내에 적재되는 로그를 찾기가 매우 어렵습니다. Pod에 쌓이는 로그를 Elastic Search라는 검색엔진을 통해 취합하고 이를 시각화하는 도구인 Kibana를 통해 타임라인별 로그를 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발자가 유의미하다고 판단되는 로그를 모아 볼 수 있게 한 사내 서비스도 있습니다. 개발 환경별로 패키지가 제공되어 매우 간편하며 fusion을 이용하지 않는 기존 C# 닷넷에서도 해당 로깅 시스템을 이용하고 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;nGrinder&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;부하 테스트를 위한 툴입니다. Naver에서 오픈소스로 만들었고 서버가 견딜 수 있는 트래픽을 의도적으로 부하를 주어 성능 테스트를 진행할 때 사용합니다. 이번 꿀템의 경우도 실제 서비스에 얼마나 많은 사람들이 방문할지 모르기 때문에 아주 많은 성능 테스트를 진행했습니다. ride가 10만 건, 100만 건 일 때 스트레스 테스트도 문제없었습니다. nGrinder 환경 세팅은 fusion을 통해 세팅할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Datadog&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사내에서 사용하는 APM(Application Performance Monitoring) 툴입니다. 운영 중인 어플리케이션의 요청이 기록으로 남기 때문에 타임라인 별로 트래픽을 비교하고 에러 건수를 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Em7AC/btsIjdhsHcD/HSJ156CrAKphjQATvPK00K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Em7AC/btsIjdhsHcD/HSJ156CrAKphjQATvPK00K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Em7AC/btsIjdhsHcD/HSJ156CrAKphjQATvPK00K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEm7AC%2FbtsIjdhsHcD%2FHSJ156CrAKphjQATvPK00K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;626&quot; height=&quot;893&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Dependency Map 등을 통해 에러가 어느 지점에서 발생했는지 유추할 수 있어 에러 추적에 용이합니다. 또한 경보 체계를 만들어 장애 발생 시 Teams나 Slack으로 alert이 갈 수 있도록 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;593&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmbIPX/btsIioRixsp/pkYUzebQvjywqEm8kqlp3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmbIPX/btsIioRixsp/pkYUzebQvjywqEm8kqlp3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmbIPX/btsIioRixsp/pkYUzebQvjywqEm8kqlp3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmbIPX%2FbtsIioRixsp%2FpkYUzebQvjywqEm8kqlp3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;306&quot; height=&quot;593&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;593&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;서비스를&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;런칭하면서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이런&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;의의가&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;있다고&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;생각합니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;급하게 POC를&amp;nbsp;진행했음에도 불구하고&amp;nbsp;빅스마일데이 기간 동안&amp;nbsp;장애 없이 서비스했다는&amp;nbsp;점&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발 조직으로부터 시작된 서비스 &amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아이디어를&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;제시하는&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;것&amp;nbsp;&lt;/span&gt;뿐아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구현까지&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단기간에&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이루어진&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;케이스&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;프러덕&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;디벨롭은&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;누구나 챌린지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;할 수&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;있다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;. &amp;rarr;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;긍정적인 영향 전파&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결국 꿀템이 오픈될 수 있었던 이유는 유관부서의 적극적인 협조가 있었기에 가능했습니다. 이 자리를 빌려 다시 한번 감사하다는 말씀드립니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;꿀템은 댓글 기능, 신고하기 기능 등 다양한 신규 기능을 가지고 한가위 빅세일에 다시 한번 진행할 예정입니다! 앞으로 성장할 꿀템 서비스를 잘 지켜봐 주세요!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Infra</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/111</guid>
      <comments>https://dev.gmarket.com/111#entry111comment</comments>
      <pubDate>Sun, 30 Jun 2024 19:09:53 +0900</pubDate>
    </item>
    <item>
      <title>신규 서비스 &amp;quot;꿀템&amp;quot;을 만들기 위한 여정(네? 다음달까지요?) -1편</title>
      <link>https://dev.gmarket.com/110</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjtYHr/btsIhLfkog7/C7WNooK1WGD670vhLOcSK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjtYHr/btsIhLfkog7/C7WNooK1WGD670vhLOcSK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjtYHr/btsIhLfkog7/C7WNooK1WGD670vhLOcSK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjtYHr%2FbtsIhLfkog7%2FC7WNooK1WGD670vhLOcSK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;838&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요. Web Frontend팀 이민하입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지난 빅스마일데이에 첫 론칭한 꿀템 피드 서비스! 이 서비스를 만들기 위한 여정을 여러분께 소개드리려고 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Intro&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시작은 디지털 빅세일이 끝난 2월 말.. 저희 팀장님의&amp;nbsp;한마디에서&amp;nbsp;시작했습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&quot;챗GBT를 우리만 보기에 아깝지 않아..!?&quot;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_chat-gbt.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7vprO/btsIjd9JQyx/FsL1dGmSfYq8tePq7vynRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7vprO/btsIjd9JQyx/FsL1dGmSfYq8tePq7vynRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7vprO/btsIjd9JQyx/FsL1dGmSfYq8tePq7vynRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7vprO%2FbtsIjd9JQyx%2FFsL1dGmSfYq8tePq7vynRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;803&quot; data-filename=&quot;edited_edited_chat-gbt.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;803&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;챗GBT(Gmarket Best iTem)는 임직원분들이 직접 추천하는 상품을 모아둔 페이지로 하나하나가 진짜 꿀템입니다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아깝다고 생각했습니다. 저도 GBT를 보며 많이 샀기 때문에 고객도 이 페이지를 같이 보면 구매가 많이 일어날 것이라고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추가로 고객들이 게시글을 작성할 수 있다면..? 그러면 고객들이 우리 사이트에서 놀 수 있는 공간이 되지 않을까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 사내에서도 우리 프러덕에 커뮤니티 기능을 넣어보자는 니즈가 있었다고 들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자신감이 생기기 시작했습니다. 우리 사이트에 새로운 활기를 불어넣어 줄 수 있을 것 같은 그런 자신감이 생겼습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만들 수 있을 것 같은 기대감에 하나 잊어버린 것이 있었습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&quot;아 맞다 나 신규 서비스 만들어 본 적 없지!&quot;&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;네. 저는 입사하고 이벤트 플랫폼, 한반도 기획전, 패턴6를 담당하면서 기존 서비스의 개선 작업을 주로 해왔고 &quot;쇼룸&quot;이라는 서비스를 준비 중이었으나 아직 신규 서비스를 론칭해 본 경험은 없었습니다.&amp;nbsp;그렇기 때문에 어디서부터 무엇을 작업해야 할지 머릿속에 그려지지 않았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 발표는 신규 서비스를 개발하려고 할 때 무엇을 준비해야 할지 막막한 &quot;개발자&quot;분들이 시행착오를 줄일 수 있도록 도움을 드리고자 준비하게 되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;지속 가능한 개발을 위한 설계&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지속 가능한 설계란 보통 환경의 수용능력 범위 내에서 개발하는 것을 말하는, 도시설계에서 사용하는 개념인데요, 어플리케이션 개발에 있어서 개발 비용이 너무 많이 소모되지 않으면서 꾸준히 디벨롭하고 개선할 수 있는 프러덕을 만드는 것과 일맥상통한다고 생각합니다. 그리고 이를 위해 확장성을 고려한 설계가 필요했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;첫 번째로 고민한 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;네이밍&quot;이었습니다.&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;kishore_programmer_hardest_job.jpg&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1091&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E2VtU/btsIipvUaqc/EMMTQ3NdzKisNDPBhwywC0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E2VtU/btsIipvUaqc/EMMTQ3NdzKisNDPBhwywC0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E2VtU/btsIipvUaqc/EMMTQ3NdzKisNDPBhwywC0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE2VtU%2FbtsIipvUaqc%2FEMMTQ3NdzKisNDPBhwywC0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;1091&quot; data-filename=&quot;kishore_programmer_hardest_job.jpg&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1091&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;개발자에게 가장 어려운 일들&quot; data-phocus=&quot;phocus&quot; data-url=&quot;https://blog.kakaocdn.net/dn/KYfFS/btsIiOB8Idy/NGDiipC3CHuwPZXQ0JQrw0/img.jpg&quot;&gt;&lt;/span&gt;개발자에게 가장 어려운 일들&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개발자에게 있어 이름을 짓는다는 행위는 단순히 부르기 위한 목적보다 더 큰 의미가 있습니다. 이름만 듣고도 직관적으로 만드는 의도를 이해하는데 도움이 되어야 합니다. 저희도 구현하고자 하는 개념을 포괄할 수 있어야 한다고 생각했습니다. 또한 기존 개념과 헷갈리지 않도록 사내에서 사용되지 않는 단어를 선택해야 했습니다.&amp;nbsp;그러다 문득&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;우리 사이트에서 놀 수 있어야 한다.. &amp;rarr; 즐길 거리!? 그러면 게시글을 &quot;놀이 기구&quot;에 비유하면 어떨까?&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;챗GBT는 Padlet이라는 공유 게시판 서비스를 이용하였는데 그 padlet안에 게시글을 포스팅할 수 있는 빈 보드에 해당하는 영역을 &quot;&lt;b&gt;Attraction&lt;/b&gt;&quot;이라 명명했습니다. 어트랙션은 놀이 공원의 놀이 기구라는 뜻으로 사용되곤 하지만 실제로는 매력,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;즐길 거리,&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;명소 등의 뜻을 가지고 있습니다. 부동산 개발 업계에서는 도시의 랜드마크나 코엑스의 별마당 도서관처럼 상업 시설 내 차별화를 두는 장소를 어트랙션이라고 부르기도 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Attraction에 들어갈 게시글 하나하나는 놀이기구,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;탈 것&lt;/b&gt;을 의미하는 &quot;&lt;b&gt;Ride&lt;/b&gt;&quot;로 부르기로 하였습니다. Attraction 하위에 존재하며 등록, 수정이 가능한&lt;span&gt;&amp;nbsp;&lt;b&gt;컨텐츠&lt;/b&gt;입니다.해당&lt;/span&gt; 프로젝트에서 가장 중요한 개념이며 Ride의 퀄리티에 따라 이 서비스의 흥행이 정해질 것이라 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 Ride를 업로드할 수 있는 사람을 지칭하는 개념이 필요했습니다. 이 사람은 지마켓, 옥션의 회원 일 수도, 셀러 고객일 수도, 임직원 일 수도 있기 때문에 이를 포괄하는 개념이어야 했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러므로 Ride의 탑승객을 의미하는 &quot;&lt;b&gt;Passenger&lt;/b&gt;&quot;로 명명했습니다.&amp;nbsp;그렇게&amp;nbsp;핵심 개념인&amp;nbsp;&lt;b&gt;Attraction&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Ride&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Passenger&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;가 탄생했고 자연스레 이 프로젝트의 이름은 지마켓의 놀이공원, &quot;&lt;b&gt;G-world&lt;/b&gt;&quot;가 되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;확장성을 염두에 두고 하위 속성을 정의하기 시작했습니다.&lt;/h4&gt;
&lt;div id=&quot;SE-7018a7de-76c5-48e8-a382-557a8c7a8fa1&quot; style=&quot;color: #333333; text-align: start;&quot; data-compid=&quot;SE-7018a7de-76c5-48e8-a382-557a8c7a8fa1&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-7018a7de-76c5-48e8-a382-557a8c7a8fa1&quot; data-direction=&quot;top&quot;&gt;
&lt;div id=&quot;SE-7018a7de-76c5-48e8-a382-557a8c7a8fa1&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-unitid=&quot;SE-7018a7de-76c5-48e8-a382-557a8c7a8fa1&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pAiKg/btsIiOvq2kI/hOJpRplG7xakZV6x816Qlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pAiKg/btsIiOvq2kI/hOJpRplG7xakZV6x816Qlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pAiKg/btsIiOvq2kI/hOJpRplG7xakZV6x816Qlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpAiKg%2FbtsIiOvq2kI%2FhOJpRplG7xakZV6x816Qlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;611&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;611&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트 이름을 정하고나서 머릿속에 있는 내용을 화이트보드에 정리해봤습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팀원들과 본격적으로 G-world의 얼개를 짜기 시작했습니다. 개발자가 기획하는 것이다 보니 어떤 식으로 접근해야 할지 몰랐습니다. 해당 페이지를 어디에 노출시킬 것이며 어떤 속성이 필요한지를 고민했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Attraction은 챗GBT처럼 저희 영업본부 임직원 분들이 올릴 수도 있고, 고객들도 업로드할 수 있어야 했고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;상품을 공유했을 때 구매 적립받을 수 있도록 Linkrew 와의 연동이 필요하다고 생각했습니다. (Linkrew 란 누구나 상품 링크를 공유해 수익을 셰어 받는 지마켓의 신규 제휴서비스입니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Ride는 상품은 구매 내역에서 불러오거나, 구매 내역 없이 단순히 상품을 추천하거나, 임직원이 올릴 수 있는 3가지 형태가 있을 수 있다고 판단했습니다. 그리고 구매 기록에 기반한 태그(첫 구매시기, 재구매 횟수 표시 등)를 게시하여 Ride의 리뷰 신뢰도를 높일 수 있는 방안을 생각해 보았습니다. 또한 Ride 내에 승객이 직접 해시태그를 넣고 이를 필터링할 수 있는 기능, 좋아요 표시를 통한 인기 Ride에 대한 리스팅 기능 등 정말 다양한 방안이 나왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ride의 상품 카테고리를 통해 특정 기획전 콘셉트에 맞는, 혹은 유저가 원하는 상품의 Ride를 모아볼 수 있는 필터링 기능도 넣었습니다. 댓글 기능도 들어가야 커뮤니티로써 기능한다고 생각했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Content Quality에 대한 우려가 가장 큰 고민이었는데, 이용약관 동의 하기 위한 동선, 동일 상품의 Ride가 올라올 경우 모아서 보여준다던지, 콘텐츠가 활발하게 업로드될 수 있도록 승인 대신 선 게시 후 모니터링이 필요하다고 생각했고 이를 위한 신고 기능도 개발할 필요가 있음을 인지했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사흘 동안 얼추 구상한 내용을 팀장님께 말씀드리고 피드백을 받았습니다.&amp;nbsp; 최초 아이디어를 팀장님이 주신만큼 개발 가능한 범위와 설계 타당성에 대한 검증을 해주시고 조언에 따라 수정을 거쳤습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;이번 빅스마일데이(5월)에 적용할 POC(Proof of Concept)를 만들어보자!&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 때는 3월 초. 그리고 저는 3월 중순에 오랜 기간 염원해 오던 미국 여행을 앞두고 있었습니다....&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여행 가서 노트북을 펼치지 않으려면 지금 빠르게 설계를 마쳐야 한다는 위기의식이&amp;nbsp;생겨났습니다. (&lt;s&gt;&lt;b&gt;네? 다음 달까지요?)&lt;/b&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;본격적으로 테이블을 설계할 때가 됐습니다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;435&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ISiLd/btsIhW8LPZL/Y00ZE4UeOjkCCOZGwR6XL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ISiLd/btsIhW8LPZL/Y00ZE4UeOjkCCOZGwR6XL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ISiLd/btsIhW8LPZL/Y00ZE4UeOjkCCOZGwR6XL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FISiLd%2FbtsIhW8LPZL%2FY00ZE4UeOjkCCOZGwR6XL0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;926&quot; height=&quot;435&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;435&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;저는 개인적으로 PostgreSQL을 자주 사용해왔습니다.&quot; data-phocus=&quot;phocus&quot; data-url=&quot;https://blog.kakaocdn.net/dn/clHrBt/btsIiy0xi0b/3HWffXMx8ayaffsyidtw1K/img.png&quot;&gt;&lt;/span&gt;저는 개인적으로 PostgreSQL을 자주 사용해 왔습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 해야 할 일은 &amp;ldquo;어떤 DBMS를 선택해야 하는가?&amp;rdquo;입니다. 사내에선 다양한 DBMS를 이용해 다양한 프러덕을 만들고 있습니다. 각 DB는 저마다의 장단점을 가지고 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장할 데이터 양이 많지는 않으면서도 POC 끝나고 데이터의 사용성을 고려했을 때 정합성이 가장 중요하다고 생각했습니다. 따라서 팀 내에선 성능과 안전성을 고려하면 Oracle을 사용하는 데에 이견이 없었습니다.&amp;nbsp;더불어 이번 BSD는 지마켓만 오픈하지만 옥션으로 확장할 수 있다는 생각도 있었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테이블 명은 ATTRACTION, RIDE, PASSENGER가 정해졌지만 테이블 내 속성을 의미하는 칼럼은 해당 키워드를 사용하기에 길다고 느껴졌습니다. 그래서 각 단어의 축약형이 필요했는데 보통 개발자들은 영어 단어의 첫 글자를 제외한 모음을 지워서 3~5글자로 만듭니다. Attraction의 모음을 없애면 Attr이죠. 제가 방금 네이밍에 있어서 기존 개념과 헷갈리지 않아야 한다고 말씀드렸습니다. 해당 단어를 현재 사내에서 사용하고 있는지 먼저 찾아볼 필요가 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/raUXU/btsIg221gxc/BhYTckFu5BRhlL7GKO4eZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/raUXU/btsIg221gxc/BhYTckFu5BRhlL7GKO4eZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/raUXU/btsIg221gxc/BhYTckFu5BRhlL7GKO4eZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FraUXU%2FbtsIg221gxc%2FBhYTckFu5BRhlL7GKO4eZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;792&quot; height=&quot;532&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span data-alt=&quot;사내 메타 데이터 관리 사이트&quot; data-phocus=&quot;phocus&quot; data-url=&quot;https://blog.kakaocdn.net/dn/KFe1Y/btsIjaSy1YO/PJ9kfxYH8Td6WLhIXjHQOk/img.png&quot;&gt;&lt;/span&gt;사내 메타 데이터 관리 사이트&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사내 Meta data를 관리하는 사이트에서 표준 단어로 검색을 해봅니다. 이는 &quot;속성&quot;을 뜻하는 Attribute의 축약형과 동일합니다. 따라서 이 경우에는 &quot;&lt;i&gt;ATRC&lt;/i&gt;&quot;로 축약형을 정하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ride는 글자 수가 적기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;Ride&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그대로 사용해도 무방하다고 판단했고, Passenger는 탑승객의 항공업계 공식 약어인 &quot;&lt;i&gt;PAX&lt;/i&gt;&quot;를 사용하기로 하였습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-edf0f392-5921-469f-8fbd-f17abe35c85f&quot; style=&quot;color: #333333; text-align: start;&quot; data-compid=&quot;SE-edf0f392-5921-469f-8fbd-f17abe35c85f&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-edf0f392-5921-469f-8fbd-f17abe35c85f&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-edf0f392-5921-469f-8fbd-f17abe35c85f&quot;&gt;
&lt;div data-unitid=&quot;SE-edf0f392-5921-469f-8fbd-f17abe35c85f&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;583&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTSUf5/btsIieuDaP9/goQwAD99fpdJccHkONTdS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTSUf5/btsIieuDaP9/goQwAD99fpdJccHkONTdS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTSUf5/btsIieuDaP9/goQwAD99fpdJccHkONTdS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTSUf5%2FbtsIieuDaP9%2FgoQwAD99fpdJccHkONTdS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;583&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;583&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테이블의 논리적인 칼럼명은 주로 한글 표기하고 물리적인 칼럼명은 영문으로 표기합니다. 칼럼의 Data Type은 DBMS 별로 조금씩 달라지긴 하나 보편적인 타입을 선언하였습니다. 그리고 해당 칼럼의 길이를 러프하게 정하고 Null 여부와 초기값 등을 설정하는 표를 만들었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Attraction을 기획하면서 구상한 내용을 담기 위해 테이블에 콘텐츠의 생성, 조회 권한 칼럼을 두었습니다. 이는 추후 유니버스 클럽 고객 만을 위한 Attraction을 만들 수도 있게 하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;디지털, 신선식품 등 특정 카테고리 상품군 만을 Ride로 올릴 수 있도록 Attraction Category 테이블을 설계했습니다. Attraction 테이블에 칼럼으로 넣지 않고 Category 테이블을 별도로 만든 이유는 Attraction이 기획전, 상품평, 코너 등에 사용될 수 있다고 판단하여 확장성을 고려한 설계가 필요하다고 느꼈기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 구조화하는 작업을 테이블의&quot;정규화&quot;라고 합니다. 중복 데이터를 없애기 위한 용도로도 사용되지만 여기서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터베이스의 구조 확장 시 재 디자인 하는 것을 최소화하기 위한 목적으로 사용&lt;/b&gt;되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;SE-277c7b5a-3ca0-4429-9688-91b54c0432ea&quot; style=&quot;color: #333333; text-align: start;&quot; data-compid=&quot;SE-277c7b5a-3ca0-4429-9688-91b54c0432ea&quot; data-a11y-title=&quot;사진&quot;&gt;
&lt;div&gt;
&lt;div data-unitid=&quot;&quot; data-compid=&quot;SE-277c7b5a-3ca0-4429-9688-91b54c0432ea&quot; data-direction=&quot;top&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;SE-277c7b5a-3ca0-4429-9688-91b54c0432ea&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-unitid=&quot;SE-277c7b5a-3ca0-4429-9688-91b54c0432ea&quot; data-compid=&quot;&quot; data-direction=&quot;top&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5q0cX/btsIg2IGGrl/B38UcYfAvDZ8P1KL9LoKU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5q0cX/btsIg2IGGrl/B38UcYfAvDZ8P1KL9LoKU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5q0cX/btsIg2IGGrl/B38UcYfAvDZ8P1KL9LoKU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5q0cX%2FbtsIg2IGGrl%2FB38UcYfAvDZ8P1KL9LoKU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;693&quot; height=&quot;457&quot; data-origin-width=&quot;936&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ride는 우리 사이트의 상품과 연동할 수 있어야 했습니다. 이번 BSD에서는 BSD 기간에 태그가 붙은 상품을 구매한 경우에만 구매 내역에서 불러와 Ride Goods로 등록할 수 있게 하였습니다. 그 외 좋아요를 저장하는 Ride Recommend 테이블과 신고할 수 있는 Ride Report 테이블, 댓글을 저장할 Ride Comment 테이블을 설계하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IvI1K/btsIhu5VM4R/cllKG7H66DWMeIDKbhLzR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IvI1K/btsIhu5VM4R/cllKG7H66DWMeIDKbhLzR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IvI1K/btsIhu5VM4R/cllKG7H66DWMeIDKbhLzR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIvI1K%2FbtsIhu5VM4R%2FcllKG7H66DWMeIDKbhLzR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;789&quot; height=&quot;486&quot; data-origin-width=&quot;789&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Passenger의 경우에는 여태까지 사내에 없던 정말 생소한 개념이기에 테이블 설계하는데 애를 먹었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 아래의 문제 상황이 존재했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;Gmarket의 id 중복 문제 &amp;rarr; 기본 id들이 있기 때문에 Gmarket은 유니크한 id 식별자를 봐야 함&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;G/A 간 회원 식별자가 다른 것 &amp;rarr; Auction 에는 회원 식별자가 없음&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal;&quot;&gt;Admin도 참여할 수 있는 서비스 &amp;rarr; Admin id가 G/A와 중복될 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; MyG, MyA 등의 내 정보와 연동하기 위해선 타 팀과의 논의가 필수불가결합니다. 하지만 커뮤니케이션 비용 절감 및 확장 가능성을 고려해 (지마켓 그 너머..) 유저아이디, 회원 식별자, 사이트 종류 세 가지 속성을 이용해 기존 지/옥/Admin 계정 정보와 논리적인 Relation을 맺는 것이 합리적이라 판단했습니다. 지마켓, 옥션, Admin 계정의 유저 id는 중복될 수가 있으므로 pax의 id 칼럼을 만들어 유저 id로 넣어두되 사이트 종류 칼럼으로 구분할 수 있습니다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;따라서 Passenger 테이블의 pk&lt;/span&gt;&lt;span&gt;&amp;nbsp;만을 보고 유일한 탑승객임을 보장할 수 있게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;추후 해당 승객의 등급, 뱃지 등 게이미픽케이션 요소를 넣기 위한 칼럼도 정의하였습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;해당 탑승객에 대한 정보는 Passenger 테이블에만 저장됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 모든 속성은 G-world 서비스에서만 이용하는 것이기 때문에 개인정보를 저장하지 않는다는 것이 중요했습니다. (개인정보 보호테이블이 되면 Meta System 내에서 별도로 관리되어 관리 포인트가 많아집니다.)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 모든 테이블에는 Insert Operator, Update Operator와 관련한 칼럼이 포함되어야 합니다. 그래서 해당 칼럼에 생성 승객과 수정 승객 번호를 넣으려고 했습니다. 그러나 담당 DA분께서 아래와 같은 가이드를 주셨습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;Insert Operator, Update Operator는 이 데이터를 (ROW) 만들거나 수정한 시스템(또는 플랫폼) 정보여야 합니다.&lt;br /&gt;즉, 시스템명, Application명, Host 정보명(Pod명) 등을 뜻하며, 고객의 개인정보(로그인 ID 등)를 저장해서는 안됩니다.&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 별도로 공통 칼럼을 만들어 모든 테이블에 전부 넣어주었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_common-column.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwfs1w/btsIhurkrDl/sGZFl2RK61fQFdyxSElxM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwfs1w/btsIhurkrDl/sGZFl2RK61fQFdyxSElxM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwfs1w/btsIhurkrDl/sGZFl2RK61fQFdyxSElxM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwfs1w%2FbtsIhurkrDl%2FsGZFl2RK61fQFdyxSElxM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;954&quot; height=&quot;471&quot; data-filename=&quot;edited_edited_common-column.png&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설계는 항상 꼼꼼해야 하지만 칼럼 추가나 수정은 개발되는 기간 내내 발생할 수 있기 때문에 너무 완벽함을 추구해선 안됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;설계를 마치고 테이블을 생성했습니다.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사내에서 주제영역으로 불리는 개념이 있는데 Database의 schema라고 생각해 주시면 이해가 잘 될 것 같습니다. 물리적인&amp;nbsp;스키마명을&amp;nbsp;통일해&amp;nbsp;사용하지만&amp;nbsp;실제&amp;nbsp;주제영역은&amp;nbsp;Meta&amp;nbsp;데이터&amp;nbsp;관리&amp;nbsp;시스템을&amp;nbsp;통해&amp;nbsp;관리됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;G-world는 신규 서비스이므로 Attraction이라는 이름의 새로운 주제영역을 신청하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_erd.png&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;1727&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQVQLO/btsIhNRLYd7/Q5LlfPnWe5RrgFCKZotKI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQVQLO/btsIhNRLYd7/Q5LlfPnWe5RrgFCKZotKI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQVQLO/btsIhNRLYd7/Q5LlfPnWe5RrgFCKZotKI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQVQLO%2FbtsIhNRLYd7%2FQ5LlfPnWe5RrgFCKZotKI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2360&quot; height=&quot;1727&quot; data-filename=&quot;edited_edited_erd.png&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;1727&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위처럼 Entity를 선언하고 Entity끼리의 Relationship을 맺어주면 ERD가 완성됩니다. 물리적, 논리적 ERD를 각각 구성해야 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;참고로 사내에서는 실제 테이블이 생성될 때 외래키(FK)를 반영하지 않습니다. 외래키 라고 하는 것은 두 테이블을 연결시켜 주는 다리라고 생각하면 이해가 빠른데 Ride 테이블에 상위 테이블인 Attraction의 키 값이 있거나 Ride Goods 테이블에 Ride의 키 값이 들어있는 것을 말합니다. 보통 관계형 데이터베이스에 대해 배울 때는 PK와 FK 설정을 통한 데이터 무결성, 정합성 보장을 한다고 하지만 성능, 개발, 유지보수에 있어서 trade off 관계이기 때문에 현업에서는 물리적인 fk는 정의하지 않는 편입니다. 그러므로 Meta 시스템을 통해 정합성을 보장하려고 하죠.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;이제 G-World를 소개하기로 했습니다.&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지난 일주일 간의 개발자들이 기획 / 설계한 것을 가지고 기획팀, 디자인팀과 자리를 마련해 팀장님이 처음으로 G-World에 대해 말씀드렸습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_g월드 회의.png&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bulzFh/btsIjxfTapb/NmK8ZUWiUO5GvkZpPOngo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bulzFh/btsIjxfTapb/NmK8ZUWiUO5GvkZpPOngo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bulzFh/btsIjxfTapb/NmK8ZUWiUO5GvkZpPOngo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbulzFh%2FbtsIjxfTapb%2FNmK8ZUWiUO5GvkZpPOngo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;446&quot; data-filename=&quot;edited_edited_g월드 회의.png&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;걱정반 기대반이었습니다. 기획 / 디자인 리소스가 부족하면 이번 BSD에 보여주기는 힘들 것 같았습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;해봅시다!&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다행히 긍정적으로 검토해 주시고 챌린지 해보자는 뜻이 모아졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 BSD는 5월 7일부터 시작했으므로 2달이 남아있었지만 QA 기간까지 끝내려면&amp;nbsp;&lt;b&gt;한 달&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에 기획 / 디자인 / 퍼블리싱 / 개발을 모두 마쳐야 했습니다.&lt;s&gt; (&lt;b&gt;네?! 다음 달 까지요!?&lt;/b&gt;)&lt;/s&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;BSD에 진행하는 것이 확정이 되고 전 예정대로 휴가를 가게 되었습니다. 그 사이 팀원분들이 울퉁불퉁한 설계도에 필요한 테이블과 칼럼을 추가해 주시면서 무사히 G-world의 데이터베이스가 생성될 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 편에는 전체적인 Data Flow와 기술스택에 대해 소개해드리겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Backend</category>
      <author>G마켓 기술블로그</author>
      <guid isPermaLink="true">https://dev.gmarket.com/110</guid>
      <comments>https://dev.gmarket.com/110#entry110comment</comments>
      <pubDate>Sun, 30 Jun 2024 19:05:49 +0900</pubDate>
    </item>
  </channel>
</rss>