<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발하는 고라니</title>
    <link>https://dev-gorany.tistory.com/</link>
    <description>천천히 차근차근</description>
    <language>ko</language>
    <pubDate>Tue, 11 Aug 2026 05:01:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>조용한고라니</managingEditor>
    <image>
      <title>개발하는 고라니</title>
      <url>https://tistory1.daumcdn.net/tistory/4369632/attach/5523960362324545883efb6e4de6a6e4</url>
      <link>https://dev-gorany.tistory.com</link>
    </image>
    <item>
      <title>Redis Keyspace Notification 과 지연 이벤트</title>
      <link>https://dev-gorany.tistory.com/375</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일전에 지연 이벤트를 구현하기 위해 batch를 이용한 polling 방식과 Rabbit MQ, 그리고 redis의 keyspace notification 을 알아보았습니다.&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;그중 제가 채택한 redis를 이용해서 어떻게 구현했고, 아쉬웠던 점과 놓쳤던 부분에 대해 버그가 발생했던 부분을 회고할 겸 글을 작성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Architecture&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 완성시킨 아키텍처에서 일부분을 도식화 하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ORall/dJMcadgf0CJ/TkQUPPVdecomekyKCNScuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ORall/dJMcadgf0CJ/TkQUPPVdecomekyKCNScuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ORall/dJMcadgf0CJ/TkQUPPVdecomekyKCNScuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FORall%2FdJMcadgf0CJ%2FTkQUPPVdecomekyKCNScuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2554&quot; height=&quot;900&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;900&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;A 에서 주문 등록 처리 후 Redis에 N분의 TTL과 함께 Key:Value를 등록&lt;/li&gt;
&lt;li&gt;N분 후 Key가 만료됨에 따라 Redis에서 Event를 발행&lt;/li&gt;
&lt;li&gt;각 B 서버(복수)에서 동일한 Event 수신&lt;/li&gt;
&lt;li&gt;Redis 분산락 획득 시도&lt;/li&gt;
&lt;li&gt;락을 획득한 서버만 주문이 결제가 되었는지 확인 후 안되었으면 주문 취소, 락을 획득하지 못한 서버는 아무 행동하지 않음&lt;/li&gt;
&lt;li&gt;주문 취소 되었으면 Kafka에 주문 취소 Event 발행&lt;/li&gt;
&lt;li&gt;C 서버에서 Kafka Event를 받아 후속 로직 처리 (예: 알림톡 발송 등)&lt;/li&gt;
&lt;/ol&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;h2 data-ke-size=&quot;size26&quot;&gt;분산락이 필요했나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 질문을 던질 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현 구조에서는 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis keyspace notification은 기본적으로 pub/sub 으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pub/sub은 메세지 큐 방식(point-to-point)과 다르게 &quot;Broadcast&quot; 방식을 사용합니다. 즉, 모든 Subscriber 에게 동일한 메세지를 전달합니다.&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;위의 그림에서 B는 단일 인스턴스가 아닌 다중 인스턴스로 구성되어있습니다. B가 5개라면 5개의 서버 모두 하나의 동일한 메세지(이벤트)를 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 Race Condition을 생각해야 합니다. 중복으로 처리가 되어선 안되기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Redlock을 획득한 한 대의 서버만 이벤트를 받았다고 판단하고 후속 조치를 할 수 있도록 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 방법은 없었을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 간단한 방법도 존재합니다. 위 아키텍처에서 Dispatcher 서버를 추가하는 것 입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2524&quot; data-origin-height=&quot;882&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0kka2/dJMcahbV3f0/SHmpbXQnzfLcwuUGkeH8J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0kka2/dJMcahbV3f0/SHmpbXQnzfLcwuUGkeH8J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0kka2/dJMcahbV3f0/SHmpbXQnzfLcwuUGkeH8J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0kka2%2FdJMcahbV3f0%2FSHmpbXQnzfLcwuUGkeH8J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2524&quot; height=&quot;882&quot; data-origin-width=&quot;2524&quot; data-origin-height=&quot;882&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;단일 인스턴스로 구성된 Dispatcher 서버를 추가하면 Race Condition에 대한 걱정을 없앨 수 있고, 아키텍쳐 또한 단순해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 단일 인스턴스의 취약점을 고려해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;고가용성(High Availablity)&quot; 과 &quot;SPOF(Single Point of Failure)&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 인스턴스로 구성했기 때문에 고가용성은 당연히 어려우며, SPOF 가 가장 두드러지게 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dispatcher 서버가 죽었거나 배포로 인한 Down Time이 발생하면, 치명적인 장애로 이어집니다.&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;redis pub/sub은 &quot;Fire and Forget&quot; 방식을 채택하고 있으므로 발행 후 수신하지 못한 이벤트는 유실됩니다.&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;따라서 Dispatcher 서버를 두어도 이중화에 대한 방안도 마련해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 &lt;b&gt;Active-Active&lt;/b&gt; 또는 &lt;b&gt;Standby-Active&lt;/b&gt; 서버 구성을 고려해볼 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, Active-Active 는 주로 분산락을 이용하니 기존 구성과 다를게 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Standby-Active는 하나의 리더를 두고 만약을 대비해 Failover가 가능한 서버를 준비해두는 것입니다.&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;즉, 처음 구조처럼 고려한 이유는 현재로서 Dispatcher 서버를 구성하고 별도로 두는 것이 오버 엔지니어링이라 판단했기에 Service Application 서버에서 레디스 이벤트를 수신하도록 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아쉬웠던 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은총알은 없다는 말을 아시나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산락을 이용하면 race condition에서 중복처리가 발생하지 않으리라 확신했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 발생하더군요... 다행히 치명적인 장애로 이어지진 않았지만, 결국 알림톡을 중복으로 수신한 고객이 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그 발생의 원인은 다음과 같이 복합적인 이유로 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. B에서 redis event 수신하는 시점이 100% 동일하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 분산락 획득 후 반납하기 까지 너무 빨랐음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 분산락 획득까지 대기 시간이 너무 길었음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3대의 B 서버가 있다는 가정하의 한 케이스를 제시해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;00:00:00 - Redis Event 발행됨&lt;/li&gt;
&lt;li&gt;00:00:01 1ms - B1 서버가 Event를 수신, 분산락 획득 시도&lt;/li&gt;
&lt;li&gt;00:00:01 1ms - B2 서버가 Event를 수신, 분산락 획득 시도&lt;/li&gt;
&lt;li&gt;00:00:01 2ms - B1 서버 분산락 획득&lt;/li&gt;
&lt;li&gt;&lt;b&gt;00:00:01 20ms - B2 서버 분산락 획득 포기 후 종료&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;00:00:01 30ms - B3 서버가 Event를 수신, 분산락 획득 시도&lt;/li&gt;
&lt;li&gt;&lt;b&gt;00:00:01 50ms - B1 서버 로직 처리 후 분산락 반납&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;00:00:01 50ms - B3 서버가 분산락 획득&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;00:00:01 90ms - B3 서버 로직 처리 후 분산락 반납&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 위와 같은 케이스는 아닐테지만, 어쨋든 의도치 않게 버그가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 저는 응급 처치로 B에서 분산락을 획득 했더라도 후속 로직을 처리하기 전 Redis에 특정 Key를 넣어 또 다시 중복처리를 방지하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B674t/dJMcafSJnV3/NUnwdNpqmPbDYmpwvmBbG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B674t/dJMcafSJnV3/NUnwdNpqmPbDYmpwvmBbG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B674t/dJMcafSJnV3/NUnwdNpqmPbDYmpwvmBbG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB674t%2FdJMcafSJnV3%2FNUnwdNpqmPbDYmpwvmBbG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2818&quot; height=&quot;894&quot; data-origin-width=&quot;2818&quot; data-origin-height=&quot;894&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;</description>
      <category>Restart</category>
      <category>keyspace notification</category>
      <category>Race Condition</category>
      <category>Redis</category>
      <category>지연이벤트</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/375</guid>
      <comments>https://dev-gorany.tistory.com/375#entry375comment</comments>
      <pubDate>Tue, 23 Dec 2025 01:48:55 +0900</pubDate>
    </item>
    <item>
      <title>Redis Keyspace Notification playground</title>
      <link>https://dev-gorany.tistory.com/374</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Local 환경 keyspace notification&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Redis는 성능을 이유로 이 설정이 off되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스에서 발생하는 이벤트를 발행하고 받으려면 아래의 설정값을 주어야한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;notify-keyspace-events&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 설정하는 2가지 방법을 간단하게 보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 1: redis-cli 로 즉시 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;로컬이나 개발 환경에서 빠르게 테스트할 때 유용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis-cli로 접속하여 간단히 즉시 적용할 수 있으나, redis 서버를 재시작하면 휘발됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1760262155468&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ redis-cli

# 'E'는 키스페이스 이벤트, 'x'는 만료 이벤트를 의미합니다.
&amp;gt; CONFIG SET notify-keyspace-events Ex

# 설정값 조회
&amp;gt; CONFIG GET notify-keyspace-events&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 2: redis.conf 파일에 영구 설정 -&amp;gt; 재시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;운영 환경에서는 설정 파일에 직접 추가하여 서버가 재시작되어도 설정이 유지되도록 해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760262208962&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis.conf

notify-keyspace-events Ex&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;그런데 설정 값을 &quot;Ex&quot;로 주었다. 이것이 무엇을 의미할까?&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정값 별 의미&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/#configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;redis 공식 문서&lt;/a&gt;에 자세히 나와있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1760262268806&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;K     Keyspace events, published with __keyspace@&amp;lt;db&amp;gt;__ prefix.
E     Keyevent events, published with __keyevent@&amp;lt;db&amp;gt;__ prefix.
g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
$     String commands
l     List commands
s     Set commands
h     Hash commands
z     Sorted set commands
t     Stream commands
d     Module key type events
x     Expired events (events generated every time a key expires)
e     Evicted events (events generated when a key is evicted for maxmemory)
m     Key miss events generated when a key that doesn't exist is accessed (Note: not included in the 'A' class)
n     New key events generated whenever a new key is created (Note: not included in the 'A' class)
o     Overwritten events generated every time a key is overwritten (Note: not included in the 'A' class)
c     Type-changed events generated every time a key's type changes (Note: not included in the 'A' class)
A     Alias for &quot;g$lshztdxe&quot;, so that the &quot;AKE&quot; string means all the events except &quot;m&quot;, &quot;n&quot;, &quot;o&quot; and &quot;c&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;K, E 는 각각 keyspace, keyevent를 의미한다고 하는데, 이게 무엇일까? AI는 아래와 같이 알려주었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sAwff/btsQ7a4zyJX/rzlWCx7vKKRTDmi192Fkdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sAwff/btsQ7a4zyJX/rzlWCx7vKKRTDmi192Fkdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sAwff/btsQ7a4zyJX/rzlWCx7vKKRTDmi192Fkdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsAwff%2FbtsQ7a4zyJX%2FrzlWCx7vKKRTDmi192Fkdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1508&quot; height=&quot;1422&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;1422&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;저는 특정 패턴의 레디스 키(Keyevent, E)에 관심이 있고, 만료(Expired, x)되었을 때 이벤트를 받고싶으므로 &quot;Ex&quot; 로 설정하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #292a2e; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-layout=&quot;default&quot; data-table-width=&quot;760&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;구분&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;Keyevent (추천)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;Keyspace (부적절)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;&lt;b&gt;구독 대상&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;__keyevent@*__:expired &lt;b&gt;(채널 1개)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;__keyspace@*__:waiting:called:* &lt;b&gt;(N개의 키)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;&lt;b&gt;처리 방식&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;이벤트 종류를 먼저 정하고, 어떤 키인지 나중에 필터링&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;키를 먼저 정하고, 어떤 이벤트인지 나중에 필터링&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;&lt;b&gt;효율성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;&lt;b&gt;매우 효율적&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;background-color: #ffffff; text-align: left;&quot; colspan=&quot;1&quot; rowspan=&quot;1&quot;&gt;&lt;span&gt;비효율적이고 구현이 복잡함&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Run command in redis-cli&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;1. keyspace notifications 설정을 ON 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760262667449&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis-1
&amp;gt; CONFIG SET notify-keyspace-events Ex&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;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;2. 다른 redis session을 열어 keyevent 만료 이벤트에 대해 구독합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760262677242&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis-2
&amp;gt; SUBSCRIBE __keyevent@0__:expired&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;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;3.&amp;rdquo;order:created:1&amp;rdquo; key로 아무 값이나 넣어 TTL을 5s 로 삽입합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760262687005&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis-1
&amp;gt; SET order:created:1 true EX 5&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;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;4.키가 만료되면 출력되는 이벤트를 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760262697603&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis-2
&amp;gt; SUBSCRIBE __keyevent@0__:expired
1) &quot;subscribe&quot; -- listening 명령어에 대한 결과 출력
2) &quot;__keyevent@0__:expired&quot;
3) (integer) 1
1) &quot;message&quot; -- 만료 이벤트 발생에 대한 출력
2) &quot;__keyevent@0__:expired&quot;
3) &quot;order:created:1&quot;&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;2288&quot; data-origin-height=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2ryBg/btsQ5z5eWH2/kr1kd6Fd1nRgUCxfueX3fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2ryBg/btsQ5z5eWH2/kr1kd6Fd1nRgUCxfueX3fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2ryBg/btsQ5z5eWH2/kr1kd6Fd1nRgUCxfueX3fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2ryBg%2FbtsQ5z5eWH2%2Fkr1kd6Fd1nRgUCxfueX3fk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2288&quot; height=&quot;860&quot; data-origin-width=&quot;2288&quot; data-origin-height=&quot;860&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Restart</category>
      <category>Redis</category>
      <category>redis keyspace notification</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/374</guid>
      <comments>https://dev-gorany.tistory.com/374#entry374comment</comments>
      <pubDate>Sun, 12 Oct 2025 18:59:35 +0900</pubDate>
    </item>
    <item>
      <title>Delayed Event(지연 이벤트)</title>
      <link>https://dev-gorany.tistory.com/373</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Event&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트라는 단어 참 친숙하지요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일상 생활부터 소프트웨어 개발까지, 이벤트는 셀 수 없이 많은 곳에서 다양하게 쓰이고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 특히 도메인 이벤트(Domain Event)라는 개념을 중요하게 사용합니다. 도메인 이벤트란 &quot;비즈니스적으로 의미 있는 어떤 사건이 발생했다&quot;는 사실 그 자체를 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 '주문이 생성되었다'거나 '회원 가입이 완료되었다'는 사실이 바로 도메인 이벤트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 사건을 중심으로 시스템을 설계하면, 비즈니스 로직 간의 &lt;b&gt;결합도를 낮추어&lt;/b&gt; 시스템을 훨씬 유연하게 만들 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Delayed Event&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 지연 이벤트에 대해 이야기 해보고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;지연 이벤트&quot;란 &lt;b&gt;시스템에서 어떤 일이 발생했을 때 그에 대한 후속 조치를 즉시 실행하지 않고 의도적으로 지정된 시간만큼 지연시킨 후 처리&lt;/b&gt;하는 이벤트 혹은 메세지를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활용 사례는 정말 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 예약 메세지 가 있겠네요. &quot;다음 날 아침 9시에 보고서 제출 메세지를 보내줘&quot; 처럼요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 주문에서도 쓰일 수 있습니다. 주문은 했지만 결제를 10분내 하지않은 주문 자동 취소 같이 쓰일 수 있겠네요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 구현할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법 또한 다양합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 크게 3가지로 설명하고자 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Batch 기반 Polling&lt;/li&gt;
&lt;li&gt;Message Queue&lt;/li&gt;
&lt;li&gt;Redis Keyspace Notification&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Batch 기반 Polling&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인의 변경이 일어났을 때 발행한 이벤트를 Database 혹은 Cache 같은 곳에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 &quot;주문이 발생됨&quot; 이라는 데이터와 주문 ID, 언제 소비(처리)가 되어야하는지 시간을 넣어두면 좋겠네요.&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;이제 배치 Job을 구성하고 매 1분마다 해당 데이터를 탐색합니다. 아직 소비가 되지 않았으면서 처리되어야 하는 시간이 현재시간에 일치하는 이벤트 데이터를 찾아 결제가 완료되었는지 검사하고 조건에 따라 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순하지만 비효율적이라 생각되지 않나요? 저는 가장 먼저 떠올렸지만 바로 생각을 접었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;확장성이 떨어집니다.&lt;/b&gt; 다른 종류의 지연 이벤트를 활용한 로직을 구성해야한다면, 또 다른 배치 Job을 만들어야합니다. 또한 데이터의 양이 늘어나면 스캔해야할 데이터의 범위도 늘어나게 됩니다. 이는 Database 의 부하에도 영향을 줄 수 있습니다.&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;2. &lt;b&gt;불필요한 자원이 소비됩니다.&lt;/b&gt; 실제로 처리할 이벤트가 없더라도 매 분 배치는 동작하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;초단위 까지 맞춰서 하기엔 무리가 있습니다.&lt;/b&gt; 위에서 말씀드렸듯, 배치를 1분마다 동작하게 합니다. 이를 1초마다 동작하게 한다면 (2)의 단점이 매우 부각될 뿐더러, 배치 Job이 밀리며 의도대로 동작하지 않을 리스크가 있습니다. (무엇보다 일반적으로 그렇게 하지 않습니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Message Queue&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메세지 큐로는 2가지를 알아보았습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS SQS&lt;/li&gt;
&lt;li&gt;RabbitMQ&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아래에서 이벤트와 메세지를 혼용해서 사용하고 있습니다. context에 따른 것이니 크게 개의치 않아도 됩니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQS (Simple Queue Service)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS에서 제공하는 SQS는 대표적으로 다음과 같은 특징을 지닙니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 처리(Async Processing)&lt;/li&gt;
&lt;li&gt;완전 관리형 서비스(Fully Managed Service)&lt;/li&gt;
&lt;li&gt;메세지 처리 보장&lt;/li&gt;
&lt;li&gt;메세지를 큐에 저장하여 프로듀서와 컨슈머를 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ나 Kafka와 같이 직접 구성하고 관리해야하는 인프라와 달리 SQS는 AWS에서 모두 관리를 해주기에 우리는 단순히 사용만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 메세지 큐가 필요하다면 바로 이용했겠지만, 아래와 같은 이슈들로 인해 후순위로 밀려났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. SQS는 &quot;지연 메세지&quot; 기능을 제공하지만, &lt;b&gt;최대 15분&lt;/b&gt; 까지만 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 지연 메세지 기능을 사용하면 &lt;b&gt;FIFO(First-In-First-Out) Queue&lt;/b&gt;를 사용할 수 없습니다. 이는 메세지 순서를 100% 보장할 수 없음을 의미합니다. (SQS는 Standard Queue를 기본적으로 사용합니다. 이는 순서를 보장하진 않습니다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2942&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brucdN/btsQ6esEgFI/9vldJHkZ6TFNQTp5bRUiG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brucdN/btsQ6esEgFI/9vldJHkZ6TFNQTp5bRUiG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brucdN/btsQ6esEgFI/9vldJHkZ6TFNQTp5bRUiG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrucdN%2FbtsQ6esEgFI%2F9vldJHkZ6TFNQTp5bRUiG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2942&quot; height=&quot;1120&quot; data-origin-width=&quot;2942&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;At Least Once Delivery&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS는 &quot;메세지가 최소 한 번 소비자에게 전달&quot; 을 보장합니다. 다시 말해 네트워크 이슈 등으로 두 번 이상 전달될 수 있다는 의미입니다. 따라서 멱등성 있는 설계가 요구됩니다.&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;4. &lt;b&gt;가시성 시간 초과(Visibillity Timeout)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소비자가 Queue에서 메세지를 가져가면, 그 &lt;b&gt;메세지는 다른 소비자에게 보이지 않는 &quot;처리 중&quot; 상태&lt;/b&gt;가 됩니다. 이처럼 숨겨져 있는 시간을 가시성 초과 시간이라 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가시성 시간 초과는 메세지 처리 시간보다 너무 길거나, 너무 짧지 않도록 적절히 설정해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에 고려해야할 점은 메세지의 크기 제한(1MB), DLQ 등이 있으나 크게 의미를 두진 않았습니다.&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;만약 SQS를 사용하기로 결정했다면 최대 15분 지연이라는 이슈를 커버하기 위해 아래와 같은 아키텍처를 설계했을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;1192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTG3Qo/btsQ73jtJfA/vJ1ag2CXXsbL36vQGQawgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTG3Qo/btsQ73jtJfA/vJ1ag2CXXsbL36vQGQawgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTG3Qo/btsQ73jtJfA/vJ1ag2CXXsbL36vQGQawgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTG3Qo%2FbtsQ73jtJfA%2FvJ1ag2CXXsbL36vQGQawgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2136&quot; height=&quot;1192&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;1192&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;메세지를 발행할 때 최대 15분의 지연 설정을 넣어 발행합니다. 비록 이 메세지가 30분 뒤에 소비되어야 한대도 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서도 주문을 예를 들었으니, 마찬가지로 주문을 이용해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메세지는 어떤 주문이고, 어느 시간에 처리가 되어야하는지 적혀있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N분 후 이 메세지는 Consumer에게 소비가 될 것 이고, 메세지를 읽은 컨슈머는 시간을 비교해 처리해야하는 시간보다 일찍 읽었다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 지연 설정을 넣어 SQS에 메세지를 발행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 제 시간에 읽었다면 결제가 되었는지 체크하고 조건에 따라 주문을 취소합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ + &lt;a href=&quot;https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq#installing-the-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Delayed Plugin&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 AMQP 프로토콜을 구현한 오픈 소스 메시지 브로커로, 시스템 간에 메시지를 안정적으로 전달하는 중간 다리 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같이 주요한 특징을 지닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유연한 라우팅 (Flexible Routing) &lt;/b&gt;&amp;nbsp;메시지를 단순히 큐에 넣는 것을 넘어, 'Exchange'라는 라우터를 통해 다양한 규칙(Direct, Topic, Fanout 등)으로 메시지를 여러 큐에 분배하거나 특정 큐에만 선별적으로 보낼 수 있습니다. 이를 통해 복잡한 메시징 시나리오를 쉽게 구현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 처리와 낮은 결합도 (Asynchronous &amp;amp; Decoupled)&lt;/b&gt;&amp;nbsp;메시지를 보내는 서비스(Producer)와 처리하는 서비스(Consumer)가 서로를 직접 알 필요 없이 독립적으로 동작합니다. 이는 한쪽 서비스의 장애가 다른 쪽으로 전파되는 것을 막아주며, 시스템 전체의 안정성과 확장성을 높여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 보증 및 안정성 (Data Guarantee &amp;amp; Reliability)&lt;/b&gt;&amp;nbsp;메시지를 디스크에 저장하는 지속성(Durability) 옵션과 소비자가 처리를 완료했음을 확인하는 메시지 확인(Acknowledgement) 메커니즘을 통해, 서버가 재시작되거나 장애가 발생해도 메시지가 유실되지 않도록 보장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고가용성 및 확장성 (High Availability &amp;amp; Scalability) &lt;/b&gt;여러 서버를 하나로 묶는 클러스터링(Clustering)과 큐의 데이터를 여러 노드에 복제하는 미러링(Mirroring) 기능을 통해 일부 서버에 장애가 발생해도 중단 없이 서비스를 운영할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;풍부한 기능과 성숙한 생태계 (Rich Features &amp;amp; Mature Ecosystem)&lt;/b&gt;&amp;nbsp;메시지 유효 시간(TTL), 데드 레터링(처리 실패 메시지 격리), 관리자 UI, 다양한 프로그래밍 언어 지원 등 오랜 기간 발전해 온 성숙한 기능들을 제공하여 개발 및 운영 편의성이 뛰어납니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에 &lt;a href=&quot;https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq#installing-the-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Delayed Plugin&lt;/a&gt;을 이용하면 안정적으로 지연 메세지를 전송하고 소비할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연 속성은 queue나 exchange 에 설정하는 것이 아닌 메세지에 설정하기에 메세지마다 3분, 5분, 10분 등 다양하게 설정하여 전송할 수 있습니다.&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;다만 현재 팀에서 RabbitMQ 를 운용하고 있지 않기에 배제하였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Keyspace Notification&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;Redis docs : &lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1575&quot; data-annotation-mark=&quot;true&quot; data-annotation-inline-node=&quot;true&quot; data-card-url=&quot;https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/&quot; data-inline-card=&quot;true&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span data-testid=&quot;hover-card-trigger-wrapper&quot;&gt;&lt;a style=&quot;background-color: #ffffff; color: #1868db;&quot; href=&quot;https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/&quot; data-testid=&quot;inline-card-resolved-view&quot;&gt;&lt;span data-testid=&quot;inline-card-icon-and-title&quot;&gt;&lt;span data-testid=&quot;icon-position-wrapper&quot;&gt;&lt;/span&gt;&lt;span&gt;Redis keyspace notifications&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt; &lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;Redis-Keyspace-Notifications-란?&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1579&quot; data-ke-size=&quot;size23&quot;&gt;Redis Keyspace Notifications 란?&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1612&quot; data-ke-size=&quot;size16&quot;&gt;Redis를 단순한 데이터 저장소를 넘어, 데이터에 변화가 생겼을 때 &lt;b&gt;먼저 우리에게 알려주는 똑똑한 이벤트 발행자(Publisher)로 만들어주는 기능&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1612&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1612&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;이 기능을 사용하면, 주기적으로 데이터를 확인하는 비효율적인 방식(Polling)을 없애고 실시간으로 데이터 변경에 반응하는 동적인 애플리케이션을 만들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;어떻게-동작하는가?&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1799&quot; data-ke-size=&quot;size23&quot;&gt;어떻게 동작하는가?&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1811&quot; data-ke-size=&quot;size16&quot;&gt;Redis의 &lt;b&gt;Pub/Sub (발행/구독)&lt;/b&gt; 모델을 기반으로 동작합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1852&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1854&quot; data-ke-size=&quot;size16&quot;&gt;데이터에 특정 이벤트가 발생하면, Redis는 정해진 채널로 메시지를 발행(Publish)합니다. 애플리케이션은 관심 있는 채널을 구독(Subscribe)하고 있다가, 메시지가 도착하면 즉시 알아채고 특정 동작을 수행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1760202761540&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;## 주요 이벤트

SET: 키에 새로운 값이 저장될 때
DEL: 키가 삭제될 때
EXPIRE: 키의 유효시간(TTL)이 만료되어 사라질 때 (활용도 높음)
HSET: 해시(Hash) 데이터가 변경될 때&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;1180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nqdMZ/btsQ4VgTWjD/Y52YVqqUphhFqzTKD3eKj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nqdMZ/btsQ4VgTWjD/Y52YVqqUphhFqzTKD3eKj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nqdMZ/btsQ4VgTWjD/Y52YVqqUphhFqzTKD3eKj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnqdMZ%2FbtsQ4VgTWjD%2FY52YVqqUphhFqzTKD3eKj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1886&quot; height=&quot;1180&quot; data-origin-width=&quot;1886&quot; data-origin-height=&quot;1180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;약간의 부하
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이벤트를 발행하는 과정에서 Redis 서버의 CPU를 아주 약간 더 사용합니다. 대부분의 경우 무시할 수준이지만, 초당 수십만 건의 이벤트가 발생하는 환경에서는 영향을 고려해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전달 보장
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;100% 전달을 보장하지는 않습니다. 구독 클라이언트가 잠시 네트워크 문제로 끊겼을 때 그 사이의 이벤트를 놓칠 수 있습니다. 따라서 반드시 처리되어야 하는 중요한 데이터는 RabbitMQ 같은 메시지 큐와 함께 사용하는 것이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 처리와 Race Condition
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;keyspace notification은 기본적으로 pub/sub 구조로 동작합니다. 즉, subscriber가 N 개이면 N번 소비하여 수행합니다.&lt;/li&gt;
&lt;li&gt;단 한번만 처리되어야하는 로직이라면 keyspace notifications외 다른 방법을 강구하거나, 동시성 제어를 해야합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 저는 지연 이벤트를 처리하기 위해 Redis의 Keyspace Notification을 이용하기로 했습니다.&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;이를 이용하면 위에서 언급했듯, pub/sub 으로 동작하기 때문에 subscribe 한 모든 서버에 이벤트가 전달되게 됩니다. 우리는 보통 운영환경에서 멀티서버 구조를 갖기에 반드시 중복으로 처리되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후속 글에서 이를 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Keyspace Notification 에 대해 좀더 자세히 알아보고, redis-cli를 통해 간단히 사용해보며 실제 Code로 구현해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 설계로 중복처리와 race condition을 방지했는지 말씀드리고 완성된 아키텍처를 설명드리겠습니다.&lt;/p&gt;</description>
      <category>Restart</category>
      <category>batch</category>
      <category>delayed event</category>
      <category>polling</category>
      <category>Rabbitmq</category>
      <category>Redis</category>
      <category>redis keyspace notification</category>
      <category>SQS</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/373</guid>
      <comments>https://dev-gorany.tistory.com/373#entry373comment</comments>
      <pubDate>Sun, 12 Oct 2025 02:22:26 +0900</pubDate>
    </item>
    <item>
      <title>간단하게 UnitTest와 IntegrationTest 분리하기 (with. kotlin gradle dsl)</title>
      <link>https://dev-gorany.tistory.com/371</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;specification]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;framework - spring boot 2.6.3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test - Junit 5&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 애플리케이션을 개발하는데 있어 테스트는 선택이 아닌 필수가 되었으며, 작성을 용이하게 해주는 라이브러리 또한 플랫폼에 따라 많이 나와있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그만큼 개발자에게 테스트는 비교적 저렴한 비용으로 나의 코드가 안전함을 보장하는 이점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 서적으로 Unit Test이 있고, 살짝 보았으나 역시 어려워 이해하는데 어려움이 있었다.&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;일반적으로 code review를 요청하기 위해 Github에 PR(Pull Request)을 올리는데, 이때 기존에 작성된 테스트 코드들이 깨지는지, 빌드가 깨지는지 등을 pipeline으로 정의해두는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때마다 통합테스트가 동작하면 시간도 오래걸리고, 환경에 따라 동작하지 않을 수도 있어 유닛테스트만 검증하는 편이다. (팀바팀이지만 통합 테스트는 개인 로컬에서 돌려보는 편이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 분류의 테스트를 어떻게 나눌 수 있을까?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Annotation - @Tag&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Junit 5에서는 테스트 클래스나, 테스트 메서드에 태그를 붙일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 다음과 같이 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1677516310386&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.gorany.user

import com.gorany.annotation.UnitTest
import org.apache.commons.lang3.StringUtils
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@Tag(&quot;ATestClass&quot;)
internal class UserTest {

    @Test
    @DisplayName(&quot;계정 생성시 패스워드는 8자 이상이어야한다.&quot;)
    @Tag(&quot;ATest&quot;)
    fun when_create_user_then_password_length_must_longer_than_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = &quot;0123456789&quot;
        
        //when
        val result = User.of(email, password, name)

        //then
        assertThat(result.password.length).isGreaterThanOrEqualTo(8)
    }

    @Test
    @DisplayName(&quot;계정 생성시 패스워드가 8자 미만이면 예외가 발생한다.&quot;)
    @Tag(&quot;BTest&quot;)
    fun throw_exception_when_password_length_is_under_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = &quot;0&quot;

        //when
        assertThatThrownBy { User.of(email, password, name) }
            .isInstanceOf(IllegalArgumentException::class.java)
            .hasMessage(&quot;비밀번호는 8자 이상이어야 합니다.&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간편하게 테스트 클래스와 메서드에 별명을 붙여준 것이라 보면된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Custom Annotation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 하면 매번 붙이고자 하는 클래스나 메서드에 모두 태그를 붙여줘야하는 번거로움이 있어 커스텀 어노테이션을 만들어보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들고자 하는 용도는&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Unit Test를 위한 어노테이션&lt;/li&gt;
&lt;li&gt;Integration Test를 위한 어노테이션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;pre id=&quot;code_1677516475804&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// UnitTest Annotation
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)
@Tag(&quot;unitTest&quot;)
@Retention(AnnotationRetention.RUNTIME)
annotation class UnitTest()


// IntegrationTest Annotation
import org.junit.jupiter.api.Tag
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@Tag(&quot;integrationTest&quot;)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
annotation class IntegrationTest()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 어노테이션을 만들어주었으면, 유닛 테스트를 모아놓은 테스트 클레스에 붙여보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1677516521551&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@UnitTest
internal class UserTest {

    @Test
    @DisplayName(&quot;계정 생성시 패스워드는 8자 이상이어야한다.&quot;)
    fun when_create_user_then_password_length_must_longer_than_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = &quot;0123456789&quot;
        
        //when
        val result = User.of(email, password, name)

        //then
        assertThat(result.password.length).isGreaterThanOrEqualTo(8)
    }

    @Test
    @DisplayName(&quot;계정 생성시 패스워드가 8자 미만이면 예외가 발생한다.&quot;)
    fun throw_exception_when_password_length_is_under_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = &quot;0&quot;

        //when
        assertThatThrownBy { User.of(email, password, name) }
            .isInstanceOf(IllegalArgumentException::class.java)
            .hasMessage(&quot;비밀번호는 8자 이상이어야 합니다.&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;이러면, UserTest라는 테스트 클레스는 &quot;UnitTest&quot; 라는 별명이 붙게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Test 명령어 실행하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 command를 치면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1677516637454&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle 설정을 해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1677516679935&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// build.gradle.kts
    tasks.withType&amp;lt;Test&amp;gt; {
        useJUnitPlatform()
    }

    tasks.test {
        useJUnitPlatform() {
            includeTags(&quot;unitTest&quot;)
            excludeTags(&quot;integrationTest&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;test 할때, &quot;unitTest&quot; 태그가 붙은 것을 포함하고, &quot;integrationTest&quot; 태그가 붙은 것을 제외한다는 설정이다.&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;pre id=&quot;code_1677516752412&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과연 유닛 테스트만 동작하는 것이 맞을까?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IntegrationTest 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증해보자. 우선 예외를 던지는 integrationTest 아무거나 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1677516800757&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@IntegrationTest
class SampleExceptionTest {

    @Test
    @DisplayName(&quot;예외가 발생하는 테스트&quot;)
    fun just_run_throw_exception() {

        throw RuntimeException(&quot;SAMPLE ERROR&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;만일, ./gradlew test 했을 때 &quot;unitTest&quot;가 붙은 테스트만 동작하는 것이 아니라면, 이 작업은 깨져야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1677516878488&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BUILD SUCCESSFUL in 8s
32 actionable tasks: 32 up-to-date&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;정상적으로 성공하는 것을 보니, 의도대로 잘 되고있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IntegrationTest만 수행하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유닛테스트만 동작하도록 분리했으니, 통합 테스트도 동작할 수 있도록 설정을 해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle의 test 명령어는 이미 unitTest만 동작하도록 점유하고 있으니, 'integration' 이라는 명령어로 통합테스트만 동작하도록 설정해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1677516971268&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    task&amp;lt;Test&amp;gt;(&quot;integration&quot;) {
        useJUnitPlatform() {
            excludeTags(&quot;unitTest&quot;)
            includeTags(&quot;integrationTest&quot;)
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 명령어 실행&lt;/p&gt;
&lt;pre id=&quot;code_1677516997708&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew integration&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1677517032508&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; Task :api:integration

SampleExceptionTest &amp;gt; 예외가 발생하는 테스트 FAILED
    java.lang.RuntimeException: SAMPLE ERROR
        at com.gorany.sample.SampleExceptionTest.just_run_throw_exception(SampleExceptionTest.kt:14)
        ...&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;의도한대로 SampleExceptionTest에서 테스트가 깨지는 것을 확인하였으니, integrationTest 태그가 붙은 친구를 동작시키는 것을 확인했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법 말고도, 디렉토리 구조로 나누는 방법 등 다양하게 있다. 하지만 나는 이처럼 태그를 가지고 하는 것이 가장 간편했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 편의를 위해 유닛/통합 테스트를 분리해야한다면 이 방법을 시도해보면 좋을 것 같다.&lt;/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;# Github&lt;/p&gt;
&lt;figure id=&quot;og_1677517294259&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[add-unit-test] by rhacnddl &amp;middot; Pull Request #6 &amp;middot; rhacnddl/monorepo&quot; data-og-description=&quot;[add-unit-test] unitTest / integrationTest 어노테이션 생성 UserTest 작성 gradle test 명령어 분리 (유닛/통합)&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/rhacnddl/monorepo/pull/6/files&quot; data-og-url=&quot;https://github.com/rhacnddl/monorepo/pull/6&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/QYxek/hyRMlX33Vi/SecB3PWiFGlJKrx8fMQxV0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/rhacnddl/monorepo/pull/6/files&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/rhacnddl/monorepo/pull/6/files&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/QYxek/hyRMlX33Vi/SecB3PWiFGlJKrx8fMQxV0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[add-unit-test] by rhacnddl &amp;middot; Pull Request #6 &amp;middot; rhacnddl/monorepo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[add-unit-test] unitTest / integrationTest 어노테이션 생성 UserTest 작성 gradle test 명령어 분리 (유닛/통합)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>integration test</category>
      <category>JUnit 5</category>
      <category>Spring boot</category>
      <category>tag</category>
      <category>test</category>
      <category>Unit Test</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/371</guid>
      <comments>https://dev-gorany.tistory.com/371#entry371comment</comments>
      <pubDate>Tue, 28 Feb 2023 02:01:51 +0900</pubDate>
    </item>
    <item>
      <title>[Flyway] Database의 이력을 관리해보자.</title>
      <link>https://dev-gorany.tistory.com/370</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;specification]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySql - 8.0.28&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle - 7.4&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flyway - 8.0.0&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다보면, Database에 생성해놓은 테이블의 컬럼명이 수정된다던지, 컬럼의 타입이 변경되거나, 컬럼이 추가/삭제되는 일이 빈번하진 않지만 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시간이 지나고, 우리가 이런 수정이 언제? 왜 생겼지?는 왠만해서 알 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 코드 또한 이러한 이유를 포함하여 VCS(Version Control System, ex.git) 을 사용하는 것이 아닌가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database의 변경이력을 파일로써 관리해보도록 하자.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 글에서는 &lt;b&gt;Flyway&lt;/b&gt;라는 '&lt;b&gt;오픈소스 데이터 마이그레이션&lt;/b&gt;' 툴을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하는 방법은 여러가지가 있지만. build.gradle.kts에 작성해서 사용해보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 필요한 플러그인과 의존성은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1677493465436&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.flywaydb.flyway&quot;) version &quot;8.0.0&quot;
}

dependencies {
    compileOnly(&quot;mysql:mysql-connector-java&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 database라는 모듈을 만들고(&lt;a href=&quot;http://2022.02.20%20- [Framework/Spring Boot] - [Spring Boot] Mono Repo, Multi Module (with. Gradle Kotlin DSL)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;멀티모듈 생성글&lt;/a&gt; 참조), 다음과 같은 디렉토리 구조를 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1677493609932&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src
  - main
    - resources
      - db
        - migration&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;174&quot; data-origin-height=&quot;121&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s8m91/btr02AT7MoZ/MI9kjfwOuVrMqLdelJOXb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s8m91/btr02AT7MoZ/MI9kjfwOuVrMqLdelJOXb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s8m91/btr02AT7MoZ/MI9kjfwOuVrMqLdelJOXb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs8m91%2Fbtr02AT7MoZ%2FMI9kjfwOuVrMqLdelJOXb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;174&quot; height=&quot;121&quot; data-origin-width=&quot;174&quot; data-origin-height=&quot;121&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;다음은, [migration] 디렉토리에 내가 작성하고 싶은 SQL 파일을 작성한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단, 다음과 같은 규칙이 존재한다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbg6Ov/btr0VKv2QZ9/NKrA6V1ByJsKJgrTM0ZbD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbg6Ov/btr0VKv2QZ9/NKrA6V1ByJsKJgrTM0ZbD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbg6Ov/btr0VKv2QZ9/NKrA6V1ByJsKJgrTM0ZbD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcbg6Ov%2Fbtr0VKv2QZ9%2FNKrA6V1ByJsKJgrTM0ZbD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;342&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) V1__init.sql&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;pre id=&quot;code_1677493794731&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# V1__add_table_user.sql
CREATE TABLE `user`
(
    `id`         bigint       NOT NULL AUTO_INCREMENT comment 'user ID',
    `role_id`    bigint       NOT NULL comment 'role ID',
    `email`      varchar(255) NOT NULL comment 'email',
    `password`   varchar(255) NOT NULL comment '비밀번호',
    `name`       varchar(255) NOT NULL comment '유저 이름',

    `created_at` timestamp    NOT NULL comment '생성일',
    `updated_at` timestamp    NOT NULL comment '수정일',
    `created_by` varchar(255) NOT NULL comment '생성주체',
    `updated_by` varchar(255) NOT NULL comment '수정주체',
    `is_deleted` bit(1)       NOT NULL DEFAULT b'0' comment '삭제여부',
    PRIMARY KEY (`id`),
    KEY          `user_idx_01` (`updated_at`),
    KEY          `user_idx_02` (`created_at`),
    KEY          `user_idx_03` (`role_id`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_general_ci;&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;SQL 파일까지 작성했다면, Database에 적용해줄 차례이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gradle 파일에 접근하고자 하는 DB 정보를 기입해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1677493868048&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.flywaydb.flyway&quot;) version &quot;8.0.0&quot;
}

flyway {
    url = &quot;jdbc:mysql://localhost:3306/mono_repo?allowPublicKeyRetrieval=true&amp;amp;zeroDateTimeBehavior=convertToNull&amp;amp;useUnicode=true&amp;amp;characterEncoding=utf8&amp;amp;useSSL=false&amp;amp;serverTimezone=Asia/Seoul&quot;
    user = &quot;root&quot;
    password = &quot;1234567890&quot;
}

dependencies {
    compileOnly(&quot;mysql:mysql-connector-java&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;command로 flyway를 실행할 수도 있지만, intelliJ를 이용해 가시적으로 사용해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY4Wgx/btr0KGnJ9kZ/nqEaxGPyieRPwAGzkRJ7b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY4Wgx/btr0KGnJ9kZ/nqEaxGPyieRPwAGzkRJ7b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY4Wgx/btr0KGnJ9kZ/nqEaxGPyieRPwAGzkRJ7b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY4Wgx%2Fbtr0KGnJ9kZ%2FnqEaxGPyieRPwAGzkRJ7b1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;286&quot; height=&quot;529&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 여기서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flywayClean&lt;/li&gt;
&lt;li&gt;flywayMigrate&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clean은 DB를 모두 비우는 것이고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Migrate는 내가 작성한 SQL 파일을 모두 적용하는 명령이다.&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;# Code&lt;/p&gt;
&lt;figure id=&quot;og_1677494156917&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[add-flyway] by poby-kr &amp;middot; Pull Request #5 &amp;middot; rhacnddl/monorepo&quot; data-og-description=&quot;[add-flyway]&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/rhacnddl/monorepo/pull/5/files#diff-b0480671c8f02ef160638046c98725b9bb5bb50b0f8a6a67af88c25edc2c0c88R5&quot; data-og-url=&quot;https://github.com/rhacnddl/monorepo/pull/5&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/x5UjX/hyRMjThR5b/2lZexkDNeq5kqqSt0n6Ro0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/rhacnddl/monorepo/pull/5/files#diff-b0480671c8f02ef160638046c98725b9bb5bb50b0f8a6a67af88c25edc2c0c88R5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/rhacnddl/monorepo/pull/5/files#diff-b0480671c8f02ef160638046c98725b9bb5bb50b0f8a6a67af88c25edc2c0c88R5&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/x5UjX/hyRMjThR5b/2lZexkDNeq5kqqSt0n6Ro0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[add-flyway] by poby-kr &amp;middot; Pull Request #5 &amp;middot; rhacnddl/monorepo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[add-flyway]&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>잡동사니</category>
      <category>DATABASE</category>
      <category>flyway</category>
      <category>gradle</category>
      <category>Open Source</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/370</guid>
      <comments>https://dev-gorany.tistory.com/370#entry370comment</comments>
      <pubDate>Mon, 27 Feb 2023 19:36:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] API Error message가 나오지 않을 때</title>
      <link>https://dev-gorany.tistory.com/369</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;specification]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot - 2.6.3&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API를 만들면 종종 Exception이 발생하기 마련이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client에서 요청을 했는데, 예외가 발생했을 때 구체적이고 명시적으로 어떤 에러, 무엇 때문에 발생했는지를 &quot;message&quot;에 담아서 보낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1677487700608&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun validateMoney(money: BigDecimal) {
  if (money &amp;lt; BigDecimal(0)) {
    throw MinusMoneyException(&quot;금액이 음수입니다. money : $money&quot;)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 그간 무심코 넘어갔지만 에러 메세지를 확인해보려니 다음과 같이 발생한 적이 있을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1677487753528&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;timestamp&quot;: &quot;2023-02-27T08:43:33.081+00:00&quot;,
    &quot;status&quot;: 500,
    &quot;error&quot;: &quot;Internal Server Error&quot;,
    &quot;path&quot;: &quot;/api/v1/samples/error&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 분명히 메세지를 전달했는데...?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;본론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 Error를 발생하는 API가 있다고 하자.&lt;/p&gt;
&lt;pre id=&quot;code_1677487887141&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
class SampleController {

    @GetMapping(&quot;/api/v1/samples/error&quot;)
    fun throwException(): ResponseEntity&amp;lt;*&amp;gt; {

        throw RuntimeException(&quot;sample 예외가 발생합니다.&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;이를 postman에서 호출했고, 나는 &lt;b&gt;&quot;sample 예외가 발생합니다&quot; 라는 메세지&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;2134&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bzf2b/btr01UkFsAx/LFinykdyfAgDK8RN2SArYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bzf2b/btr01UkFsAx/LFinykdyfAgDK8RN2SArYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bzf2b/btr01UkFsAx/LFinykdyfAgDK8RN2SArYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBzf2b%2Fbtr01UkFsAx%2FLFinykdyfAgDK8RN2SArYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2134&quot; height=&quot;1146&quot; data-origin-width=&quot;2134&quot; data-origin-height=&quot;1146&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;내 메세지는 어디갔을까.. 답은 baeldung에서 찾을 수 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Starting from the 2.3 version, Spring Boot doesn't include an error message on the default error page.&lt;/b&gt;&amp;nbsp;&lt;br /&gt;The reason is to reduce the risk of leaking information to a client&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring boot 2.3 버전 부터는 클라이언트에 정보가 누수될까봐, 기본 에러페이지에 에러메세지를 담지 않는다고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결법은 간단했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;YAML or properties 파일에 설정하나만 넣어주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1677488137804&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
server:
  error:
    include-message: always
    
# application.properties
server.error.include-message=always&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 위 경로로 호출해보니, 기대하던 결과가 반환되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1677488224741&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;timestamp&quot;: &quot;2023-02-27T08:56:40.461+00:00&quot;,
    &quot;status&quot;: 500,
    &quot;error&quot;: &quot;Internal Server Error&quot;,
    &quot;message&quot;: &quot;sample 예외가 발생합니다.&quot;,
    &quot;path&quot;: &quot;/api/v1/samples/error&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;# 코드&lt;/p&gt;
&lt;figure id=&quot;og_1677488353634&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;[add-error-message] by poby-kr &amp;middot; Pull Request #4 &amp;middot; rhacnddl/monorepo&quot; data-og-description=&quot;[add-error-message] 에러 API 생성 error message 노출여부 설정&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/rhacnddl/monorepo/pull/4/files#diff-b9b68fb79dbb518c662c934f2c0bdcb195dde9cb874459f25f59d4752dc4251dR37&quot; data-og-url=&quot;https://github.com/rhacnddl/monorepo/pull/4&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bzycwm/hyRKYXrWvB/gE3sg915RgjZdPK4nqwGAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/rhacnddl/monorepo/pull/4/files#diff-b9b68fb79dbb518c662c934f2c0bdcb195dde9cb874459f25f59d4752dc4251dR37&quot; data-source-url=&quot;https://github.com/rhacnddl/monorepo/pull/4/files#diff-b9b68fb79dbb518c662c934f2c0bdcb195dde9cb874459f25f59d4752dc4251dR37&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bzycwm/hyRKYXrWvB/gE3sg915RgjZdPK4nqwGAk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[add-error-message] by poby-kr &amp;middot; Pull Request #4 &amp;middot; rhacnddl/monorepo&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[add-error-message] 에러 API 생성 error message 노출여부 설정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# Reference&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-response-status-exception&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.baeldung.com/spring-response-status-exception&lt;/a&gt;&lt;/p&gt;</description>
      <category>Framework/Spring Boot</category>
      <category>error</category>
      <category>message</category>
      <category>Spring</category>
      <category>Spring boot</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/369</guid>
      <comments>https://dev-gorany.tistory.com/369#entry369comment</comments>
      <pubDate>Mon, 27 Feb 2023 17:59:40 +0900</pubDate>
    </item>
    <item>
      <title>Mono repo, Multi Module 위에서 DDD(Domain Driven Design) 를 써보자.</title>
      <link>https://dev-gorany.tistory.com/368</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 새로운 프로젝트를 시작하였는데, 이 도메인은 기존 팀에서 진행하던 도메인과 별개의 도메인으로 판단되어, 새 코드 레포지토리가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 최범균님의 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;'&lt;b&gt;도메인 주도 개발 시작하기&lt;/b&gt;&lt;/span&gt;' 책으로 스터디를 했고, 기존에 MVC 패턴으로만 개발하면서 느낀점들을 경험하고 싶지 않아 다른 아키텍쳐를 도입하고자 하였다.&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;사실 DDD를 공부하기 전, &lt;span style=&quot;background-color: #9feec3;&quot;&gt;&lt;b&gt;'Clean Architecture'&lt;/b&gt;&lt;/span&gt; 스터디를 먼저 했었다. 정말 어렵다. 초보자가 단숨에 하기에 벅찬 내용이고, 이걸 도입했다가는 아키텍쳐에 매몰되어 개발에 차질이 있을 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 클린 아키텍쳐는 좀 더 성장해서 써보기로 했다. 그래서 고른 것이 DDD 이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MVC 에서 아키텍쳐를 변경하게된 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;1. 유지보수가 힘들다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 이유이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC는 처음에 개발하기 굉장히 편리하고, 빠르고 쉽다. 하지만, 이 기능 저 기능 추가되고, 변경되고 하다보면 어플리케이션이 거대해진다.&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;크게 API Controller, Application Service, JPA Repository, JPA Entity를 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;controller는 간단하다. service를 호출해 원하는 동작을 실행하고, 결과를 반환하니 괜찮았다.&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;가장 큰 문제는 service 였다. 응용 서비스의 역할은 트랜잭션을 관리하고, 도메인 업무의 순서를 보장하는 것이라 생각한다. 하지만 도메인이 따로 있지 않았고 이에 따라 도메인 로직도 애매하게 JPA Entity에 존재하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 service의 로직이 비대해져서 조그마한 기능 하나 수정하려해도, 사이드 이펙트를 두려워 할 수 밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 테스트를 잘 작성해두었다면 맘 놓고 수정할 수 있었겠지만, 우리의 레거시는 그렇지 못했다. 변명을 하자면, 응용 서비스의 메서드 하나를 테스트 하기 위해서는 수많은 하위 로직을 모킹해야했고, 번거로웠다. (사실 변명이 되진 않는다...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨋든 위 이유로 유지보수가 굉장히 힘들다. 그래서 도메인을 코어로 명확히 분리하고 역할과 책임을 몰빵하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;2. 서비스 로직이 비대하다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한 내용에 해당하긴 한다. 서비스 로직이 repository를 호출하고, 어떨땐 다른 서비스를 호출하고, 역할과 책임이 모호해 중구난방으로 살이 붙다보면 로직 자체가 비대해져 나중엔 분리하기도 힘들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 레이어에 역할과 책임을 분명히 하고자 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;3. 응용 서비스는 기술 구현체를 몰라야한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 외부 API 통신을 할 일이 있다. 그래서 서비스에서 이 API 통신의 기능을 구현한 구현체를 직접 사용하게 될 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;, 구현/스펙이 달라진다면? 응용 서비스의 로직 또한 수정되어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 SRP (Single Responsibility Principle)을 위반한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(실제로, DDD나 클린 아키텍쳐 등 내용에서 SRP를 굉장히 중요하게 생각한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약하자면, 다음과 같다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;각 레이어별 역할과 책임을 명확히 분리한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이를 위해 레이어간 침범을 강제적으로 규제한다. (멀티 모듈의 의존 관리)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도메인을 코어로 두고, 핵심 도메인 로직을 갖는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응용 서비스는 트랜잭션을 관리하고, 도메인 로직의 순서를 보장하는 것이 역할이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;멀티 모듈 구조&lt;/h2&gt;
&lt;pre id=&quot;code_1676108764622&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;root
├── api                          - API (controller)
├── application                  - 응용 서비스
├── infrastructure               - 구현기술 (DB, HTTP, Queue)
└── domain                       - 도메인, 도메인 로직, repository

* 이해를 돕기위해 api는 하나만두고, 몇몇 모듈은 제외&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;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;[API / 표현 영역]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청을 응용 역역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용 영역의 응답을 HTTP 응답으로 변환하여 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;[Application / 응용 영역]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템이 사용자에게 제공해야할 기능을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 구현하기 위해 도메인 영역의 도메인 모델을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직을 직접 수행하기보다 도메인 모델에 로직 수행을 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용 서비스, Http client를 래핑한 Adapter interface 등이 이 영역에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;[Domain / 도메인 영역]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 모델은 도메인의 핵심 로직을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 주문 도메인은 '배송지 변경', '결제 완료', '주문 총액 계산'과 같은 핵심 로직을 모두 도메인 모델에서 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 Repository는 이 영역에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000;&quot;&gt;&lt;b&gt;[Infrastructure / 인프라스트럭처 영역]&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 기술에 대한 것을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS 연동, 메시징 큐에 메세지 전송하거나, 수신하는 기능을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SMTP를 이용한 메일 발송 기능, HTTP 클라이언트를 이용해 REST API 호출을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리적인 개념을 표현하기 보다 실제 구현을 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Entity, JPA Repository는 이 영역에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;도메인 / 응용 / 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라스트럭처 영역에서 제공하는 기능을 사용해 필요한 기능을 개발한다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모듈의 의존 관리&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH05FO/btrYTVTKo9G/Zu7yFUkkK0dn6cY7bMYsB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH05FO/btrYTVTKo9G/Zu7yFUkkK0dn6cY7bMYsB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH05FO/btrYTVTKo9G/Zu7yFUkkK0dn6cY7bMYsB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH05FO%2FbtrYTVTKo9G%2FZu7yFUkkK0dn6cY7bMYsB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;548&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;978&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 각 모듈(영역)의 의존성을 도식화한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표현 영역은 응용 영역을 의존한다. 그리고 런타임 시점에 인프라스트럭쳐를 주입 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이는 멀티 모듈 특성상 필요한 작업이다. 응용 영역에서 인프라스트럭처를 직접적으로 사용하지 않지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라스트럭처에 구현체가 있어서, 이를 Spring의 Bean으로 만들어 런타임 시점에 사용할 수 있게 해야한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용 영역은 도메인 영역을 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인은 POJO 객체로, 어느 것에도 의존해선 안된다.&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;인프라스트럭처 영역은 도메인에 의존하고, 응용 영역에 의존한다. (응용 영역에 정의된 interface를 구현하기 위함)&lt;/p&gt;
&lt;pre id=&quot;code_1676111246999&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
build.gradle.kts 중 일부
*/
...

project(&quot;:api&quot;) {
    dependencies {
        implementation(project(&quot;:application&quot;))
        runtimeOnly(project(&quot;:infrastructure&quot;))
    }
}

project(&quot;:application&quot;) {
    dependencies {
        implementation(project(&quot;:domain&quot;))
    }
}

project(&quot;:infrastructure&quot;) {
    dependencies {
        implementation(project(&quot;:domain&quot;))
        implementation(project(&quot;:application&quot;))
    }
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;각 모듈의 구성 및 흐름도&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2inz9/btrYUu9sOvx/iiVHHhHuLBMfZHTWBaS1RK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2inz9/btrYUu9sOvx/iiVHHhHuLBMfZHTWBaS1RK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2inz9/btrYUu9sOvx/iiVHHhHuLBMfZHTWBaS1RK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2inz9%2FbtrYUu9sOvx%2FiiVHHhHuLBMfZHTWBaS1RK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1858&quot; height=&quot;940&quot; data-origin-width=&quot;1858&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[Domain Repository]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인을 query/command 하는 리퍼지토리이다.&lt;/p&gt;
&lt;pre id=&quot;code_1676113199787&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface UserRepository {
  fun save(user: User)
}&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;[Domain &amp;lt;-&amp;gt; Entity]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Domain과 Entity를 분리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, User라는 도메인이 있으면 UserEntity라는 엔티티가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Infrastructure에 이 둘을 매핑(mapping) 해주는 Mapper를 두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1676113261105&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;object UserMapper {
  fun toDomain(entity: UserEntity): User {
  ...
  }
  
  fun toEntity(domain: User): UserEntity {
  ...
  }
}

//Domain
data class User(
  val id: Long,
  val name: String,
  val email: String,
  ...
)


//Entity - Infrastructure
@Entity
data class UserEntity(
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0L,
  val name: String,
  val email: String,
  ...
)&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;[Persistence Adapter]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Domain에 있는 Repository를 구현한 구현체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA Repository를 이용해 기능을 구현한다.&lt;/p&gt;
&lt;pre id=&quot;code_1676113355236&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class UserPersistenceAdapter (
  val userJpaRepository: UserJpaRepository,
) : UserRepository {

  override fun save(user: User) {
    val entity: UserEntity = UserMapper.toEntity(user)
    
    userJpaRepository.save(entity)
}&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;TODO..&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 개발해보니, 로직의 복잡도가 낮아지고 로직이 작아졌다. 그러다보니 가독성 또한 높아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇보다 단위 테스트 작성에 거리낌이 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 영역의 역할과 책임을 확실히 나누니, mocking 해야할 것이 줄었고 내가 테스팅 해야할 것에 집중할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무조건 좋은 점만 있는 것은 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보일러 플레이트 코드가 늘었고, 내가 만들어놓은 규제에 빠져 오랜 시간 고민이 필요하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 의미있는 고민이었고, 그 결과 현재의 내가 낼 수 있는 최고의 퍼포먼스를 냈다고 생각한다. 그간 공부해온 것을 쏟아부은 집합체라 할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;clean code, DDD, clean architecture(살짝), kotlin in action 그리고 effective kotlin&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년 전의 내가 짜던 코드와 비교하였을 때 확실히 성장한 것이 보여서 뿌듯했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기반으로 다음 공부하고 시도해볼 것은 헥사고날 아키텍처이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;여느 아키텍처 관련 서적에서 말하듯이, 정답인 아키텍쳐는 없다. 각 상황과 팀, 도메인의 복잡도에 따라 유연하게 적절한 아키텍쳐를 가져가면 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github]&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1677428262552&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - rhacnddl/monorepo: Mono Repo, Multi Module&quot; data-og-description=&quot;Mono Repo, Multi Module. Contribute to rhacnddl/monorepo development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/rhacnddl/monorepo&quot; data-og-url=&quot;https://github.com/rhacnddl/monorepo&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ynIpc/hyRKKYNKN0/ZBr38YkW4cHoQYpZRkOig1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/rhacnddl/monorepo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/rhacnddl/monorepo&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ynIpc/hyRKKYNKN0/ZBr38YkW4cHoQYpZRkOig1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - rhacnddl/monorepo: Mono Repo, Multi Module&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Mono Repo, Multi Module. Contribute to rhacnddl/monorepo development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 주도 개발 시작하기.최범균.한빛미디어&lt;/p&gt;</description>
      <category>잡동사니</category>
      <category>Architecture</category>
      <category>Clean Architecture</category>
      <category>ddd</category>
      <category>Kotlin</category>
      <category>Mono Repo</category>
      <category>multi module</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/368</guid>
      <comments>https://dev-gorany.tistory.com/368#entry368comment</comments>
      <pubDate>Sat, 11 Feb 2023 19:40:50 +0900</pubDate>
    </item>
    <item>
      <title>[백준][Kotlin] 단지번호붙이기 - 2667번</title>
      <link>https://dev-gorany.tistory.com/367</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/2667&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/2667&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1674974332111&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2667번: 단지번호붙이기&quot; data-og-description=&quot;&amp;lt;그림 1&amp;gt;과 같이 정사각형 모양의 지도가 있다. 1은 집이 있는 곳을, 0은 집이 없는 곳을 나타낸다. 철수는 이 지도를 가지고 연결된 집의 모임인 단지를 정의하고, 단지에 번호를 붙이려 한다. 여&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2667&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2667&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gDqqn/hyRq3Ksl5P/EKjF3C53Ya1RAvquO11yk0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2667&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2667&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gDqqn/hyRq3Ksl5P/EKjF3C53Ya1RAvquO11yk0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2667번: 단지번호붙이기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;그림 1&amp;gt;과 같이 정사각형 모양의 지도가 있다. 1은 집이 있는 곳을, 0은 집이 없는 곳을 나타낸다. 철수는 이 지도를 가지고 연결된 집의 모임인 단지를 정의하고, 단지에 번호를 붙이려 한다. 여&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_description&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;그림 1&amp;gt;과 같이 정사각형 모양의 지도가 있다. 1은 집이 있는 곳을, 0은 집이 없는 곳을 나타낸다. 철수는 이 지도를 가지고 연결된 집의 모임인 단지를 정의하고, 단지에 번호를 붙이려 한다. 여기서 연결되었다는 것은 어떤 집이 좌우, 혹은 아래위로 다른 집이 있는 경우를 말한다. 대각선상에 집이 있는 경우는 연결된 것이 아니다. &amp;lt;그림 2&amp;gt;는 &amp;lt;그림 1&amp;gt;을 단지별로 번호를 붙인 것이다. 지도를 입력하여 단지수를 출력하고, 각 단지에 속하는 집의 수를 오름차순으로 정렬하여 출력하는 프로그램을 작성하시오.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLUjcz/btrXqDUkoFG/EiMcmGjxYKK2GGpaL6yjOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLUjcz/btrXqDUkoFG/EiMcmGjxYKK2GGpaL6yjOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLUjcz/btrXqDUkoFG/EiMcmGjxYKK2GGpaL6yjOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLUjcz%2FbtrXqDUkoFG%2FEiMcmGjxYKK2GGpaL6yjOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;409&quot; height=&quot;192&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_input&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 줄에는 지도의 크기 N(정사각형이므로 가로와 세로의 크기는 같으며 5&amp;le;N&amp;le;25)이 입력되고, 그 다음 N줄에는 각각 N개의 자료(0혹은 1)가 입력된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출력&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_output&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 줄에는 총 단지수를 출력하시오. 그리고 각 단지내 집의 수를 오름차순으로 정렬하여 한 줄에 하나씩 출력하시오.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 입력 1&lt;span&gt;&amp;nbsp;&lt;/span&gt;복사&lt;/h2&gt;
&lt;/div&gt;
&lt;pre id=&quot;sample-input-1&quot; class=&quot;angelscript&quot;&gt;&lt;code&gt;7
0110100
0110101
1110101
0000111
0100000
0111110
0111000
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 출력 1&lt;span&gt;&amp;nbsp;&lt;/span&gt;복사&lt;/h2&gt;
&lt;/div&gt;
&lt;pre id=&quot;sample-output-1&quot; class=&quot;angelscript&quot;&gt;&lt;code&gt;3
7
8
9&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 : BFS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Map이라는 클래스와, House 클래스를 정의하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Map은 N x N 2차원 매트릭스을 갖는데, 원소 타입은 House이다. 즉, House 2차원 배열을 프로퍼티로 갖으며, Boundary(N)을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 각 단지에 번호를 매길 num을 1부터 갖고있다. (점점 증가한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. input 입력받기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. map 클래스 생성 및 초기화 (input 받은 것으로 동기화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. map의 각 단지에 numbering 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3-1. 각 단지의 numbering은 bfs를 이용해 번호를 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. output 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1674974531132&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*

class Map(private val boundary: Int) {
    private var num: Int = 1
    private val houses: Array&amp;lt;Array&amp;lt;House&amp;gt;&amp;gt; = Array(boundary) { Array(boundary) { House() } }

    fun init(array: Array&amp;lt;Array&amp;lt;Boolean&amp;gt;&amp;gt;) {
        for (i in 0 until boundary)
            for (j in 0 until boundary)
                if (array[i][j])
                    houses[i][j].upgrade(num)
    }

    private fun increase() = num++

    fun numbering(): Pair&amp;lt;Int, List&amp;lt;Int&amp;gt;&amp;gt; {
        var complexes = 0
        val houseCount = mutableListOf&amp;lt;Int&amp;gt;()

        for (i in 0 until boundary) {
            for (j in 0 until boundary) {
                if (houses[i][j].isValid()) {
                    houseCount.add(bfs(i, j))

                    complexes++
                    this.increase()
                }
            }
        }

        houseCount.sort()
        return complexes to houseCount
    }

    private fun bfs(y: Int, x: Int): Int {
        var count = 1
        val queue = LinkedList&amp;lt;Pair&amp;lt;Int, Int&amp;gt;&amp;gt;()

        houses[y][x].upgrade(num)
        houses[y][x].visit()
        queue.offer(y to x)

        while (!queue.isEmpty()) {
            val (currentY, currentX) =  queue.poll()

            for (i in 0..3) {
                val nextY = currentY + Y_BOUND[i]
                val nextX = currentX + X_BOUND[i]

                if (nextY &amp;lt; 0 || nextX &amp;lt; 0 || nextY &amp;gt; boundary - 1 || nextX &amp;gt; boundary - 1 || !houses[nextY][nextX].isValid()) {
                    continue
                }

                houses[nextY][nextX].visit()
                houses[nextY][nextX].upgrade(num)
                queue.offer(nextY to nextX)
                count++
            }
        }
        return count
    }

    private val Y_BOUND = arrayOf(0, 0, 1, -1)
    private val X_BOUND = arrayOf(1, -1, 0, 0)
}

class House(
    private var complex: Int = 0,
    private var check: Boolean = false,
) {
    fun upgrade(num: Int) {
        complex = num
    }

    fun isValid(): Boolean = complex &amp;gt; 0 &amp;amp;&amp;amp; !check

    fun visit() {
        check(!check)
        check = true
    }
}

fun main() {
    val boundary = readLine()!!.toInt()
    val array: Array&amp;lt;Array&amp;lt;Boolean&amp;gt;&amp;gt; = Array(boundary) { Array(boundary) {false} }

    for (i in 0 until boundary) {
        array[i] = readLine()?.split(&quot;&quot;)!!
            .filter { it != &quot;&quot; }
            .map { it == &quot;1&quot; }
            .toTypedArray()
    }

    val map = Map(boundary)
    map.init(array)
    val (complexes, counts) = map.numbering()

    println(complexes)
    counts.forEach { println(it) }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/백준</category>
      <category>BFS</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/367</guid>
      <comments>https://dev-gorany.tistory.com/367#entry367comment</comments>
      <pubDate>Sun, 29 Jan 2023 15:42:59 +0900</pubDate>
    </item>
    <item>
      <title>[백준][Kotlin] 미로 탐색 - 2178번</title>
      <link>https://dev-gorany.tistory.com/366</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/2178&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/2178&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1674902702253&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;2178번: 미로 탐색&quot; data-og-description=&quot;첫째 줄에 두 정수 N, M(2 &amp;le; N, M &amp;le; 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/2178&quot; data-og-url=&quot;https://www.acmicpc.net/problem/2178&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OPx0G/hyRq5ufsW2/12QafmQzOxLVhknPsRfKBk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2178&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/2178&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OPx0G/hyRq5ufsW2/12QafmQzOxLVhknPsRfKBk/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;2178번: 미로 탐색&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 두 정수 N, M(2 &amp;le; N, M &amp;le; 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_description&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N&amp;times;M크기의 배열로 표현되는 미로가 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미로에서 1은 이동할 수 있는 칸을 나타내고, 0은 이동할 수 없는 칸을 나타낸다. 이러한 미로가 주어졌을 때, (1, 1)에서 출발하여 (N, M)의 위치로 이동할 때 지나야 하는 최소의 칸 수를 구하는 프로그램을 작성하시오. 한 칸에서 다른 칸으로 이동할 때, 서로 인접한 칸으로만 이동할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예에서는 15칸을 지나야 (N, M)의 위치로 이동할 수 있다. 칸을 셀 때에는 시작 위치와 도착 위치도 포함한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_input&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째 줄에 두 정수 N, M(2 &amp;le; N, M &amp;le; 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;붙어서&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입력으로 주어진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출력&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_output&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째 줄에 지나야 하는 최소의 칸 수를 출력한다. 항상 도착위치로 이동할 수 있는 경우만 입력으로 주어진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 입력 1&lt;span&gt;&amp;nbsp;&lt;/span&gt;복사&lt;/h2&gt;
&lt;/div&gt;
&lt;pre id=&quot;sample-input-1&quot; class=&quot;basic&quot;&gt;&lt;code&gt;4 6
101111
101010
101011
111011
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 출력 1&lt;span&gt;&amp;nbsp;&lt;/span&gt;복사&lt;/h2&gt;
&lt;/div&gt;
&lt;pre id=&quot;sample-output-1&quot; class=&quot;angelscript&quot;&gt;&lt;code&gt;15&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 N x M 매트릭스에서의 BFS 문제이다.&lt;/p&gt;
&lt;pre id=&quot;code_1674902758293&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*
import java.util.stream.IntStream

val Y_BOUND = arrayOf(0, 0, 1, -1)
val X_BOUND = arrayOf(1, -1, 0, 0)
const val POSITION = &quot;position&quot;
const val COUNT = &quot;count&quot;

fun main() {
    val (row, col) = readLine()?.split(' ')!!.map { it.toInt() }.take(2)
    val vertexList: MutableList&amp;lt;MutableList&amp;lt;Vertex&amp;gt;&amp;gt; = mutableListOf()

    for (i in 0 until row) {
        vertexList.add(mutableListOf())
        readLine()?.forEachIndexed { j, it -&amp;gt;
            vertexList[i].add(j, Vertex(position = Position(i, j), possible = it == '1'))
        }
    }

    println(bfs(row, col, vertexList))
}

data class Vertex(
    val position: Position,
    var check: Boolean = false,
    val possible: Boolean,
) {
    fun visit() {
        check(!check) { &quot;(y, x) = (${position.y}, ${position.x})'s check is true&quot; }
        check = true
    }

    fun getNeighborPosition(): List&amp;lt;Position&amp;gt; = IntStream.range(0, 4)
        .toArray()
        .map {
            Position(position.y + Y_BOUND[it], position.x + X_BOUND[it])
        }
}

data class Position(val y: Int, val x: Int)

private fun bfs(row: Int, col: Int, vertexList: List&amp;lt;List&amp;lt;Vertex&amp;gt;&amp;gt;): Int {
    val queue = LinkedList&amp;lt;Map&amp;lt;String, Any&amp;gt;&amp;gt;()

    vertexList[0][0].visit()
    queue.offer(mapOf(POSITION to Position(0, 0), COUNT to 1))

    while (!queue.isEmpty()) {
        val poll = queue.poll()
        val (y, x) = poll[POSITION] as Position
        val currentCount = poll[COUNT] as Int

        if (y == row - 1 &amp;amp;&amp;amp; x == col - 1) {
            return currentCount
        }

        for ((nextY, nextX) in vertexList[y][x].getNeighborPosition()) {
            if (nextY &amp;lt; 0 ||
                nextX &amp;lt; 0 ||
                nextY &amp;gt; row - 1 ||
                nextX &amp;gt; col - 1 ||
                vertexList[nextY][nextX].check ||
                !vertexList[nextY][nextX].possible
            ) {
                continue
            }

            vertexList[nextY][nextX].visit()
            queue.offer(mapOf(POSITION to Position(nextY, nextX), COUNT to currentCount + 1))
        }
    }

    throw IllegalArgumentException(&quot;Wrong Logic.&quot;)
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/백준</category>
      <category>BFS</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/366</guid>
      <comments>https://dev-gorany.tistory.com/366#entry366comment</comments>
      <pubDate>Sat, 28 Jan 2023 19:46:21 +0900</pubDate>
    </item>
    <item>
      <title>[백준][Kotlin] DFS와 BFS - 1260번</title>
      <link>https://dev-gorany.tistory.com/365</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크 : &lt;a href=&quot;https://www.acmicpc.net/problem/1260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/1260&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1674897550579&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1260번: DFS와 BFS&quot; data-og-description=&quot;첫째 줄에 정점의 개수 N(1 &amp;le; N &amp;le; 1,000), 간선의 개수 M(1 &amp;le; M &amp;le; 10,000), 탐색을 시작할 정점의 번호 V가 주어진다. 다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다. 어떤 두 정점 사&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1260&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1260&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/1dd1c/hyRqYBQsi6/l7SR46xBwaGVKXPSDD1Bo1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1260&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1260&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/1dd1c/hyRqYBQsi6/l7SR46xBwaGVKXPSDD1Bo1/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1260번: DFS와 BFS&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;첫째 줄에 정점의 개수 N(1 &amp;le; N &amp;le; 1,000), 간선의 개수 M(1 &amp;le; M &amp;le; 10,000), 탐색을 시작할 정점의 번호 V가 주어진다. 다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다. 어떤 두 정점 사&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_description&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프를 DFS로 탐색한 결과와 BFS로 탐색한 결과를 출력하는 프로그램을 작성하시오. 단, 방문할 수 있는 정점이 여러 개인 경우에는 정점 번호가 작은 것을 먼저 방문하고, 더 이상 방문할 수 있는 점이 없는 경우 종료한다.&amp;nbsp;정점 번호는 1번부터 N번까지이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입력&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_input&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째 줄에 정점의 개수 N(1 &amp;le; N &amp;le; 1,000), 간선의 개수 M(1 &amp;le; M &amp;le; 10,000), 탐색을 시작할 정점의 번호 V가 주어진다. 다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다. 어떤 두 정점 사이에 여러 개의 간선이 있을 수 있다. 입력으로 주어지는 간선은 양방향이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;출력&lt;/h2&gt;
&lt;/div&gt;
&lt;div id=&quot;problem_output&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째 줄에 DFS를 수행한 결과를, 그 다음 줄에는 BFS를 수행한 결과를 출력한다. V부터 방문된 점을 순서대로 출력하면 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 입력 1&lt;span&gt;&amp;nbsp;&lt;/span&gt;복사&lt;/h2&gt;
&lt;/div&gt;
&lt;pre id=&quot;sample-input-1&quot; class=&quot;basic&quot;&gt;&lt;code&gt;4 5 1
1 2
1 3
1 4
2 4
3 4
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 출력 1&lt;span&gt;&amp;nbsp;&lt;/span&gt;복사&lt;/h2&gt;
&lt;/div&gt;
&lt;pre id=&quot;sample-output-1&quot; class=&quot;basic&quot;&gt;&lt;code&gt;1 2 4 3
1 2 3 4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 절차지향으로만 풀어왔는데, 완벽하진 않으나, 객체지향을 도입하여 풀어보기로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 정점을 가지는 Vertex 라는 클래스로 정의하여&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;num : 몇 번째 정점&lt;/li&gt;
&lt;li&gt;check : 방문 여부&lt;/li&gt;
&lt;li&gt;neighbors : 이웃한 정점 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 나타내며, 해당 정점을 방문할 때 사용되는 visit() 메서드와, 인접한 정점을 연결하는 connect() 메서드를 갖는다.&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;DFS / BFS 로직은 일반적이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre id=&quot;code_1674897776992&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*
import java.util.stream.IntStream

fun main() {
    val (vertexCount, edgeCount, startNum) = readLine()?.split(' ')!!
        .map { it.toInt() }
        .take(3)

    val originalVertexList: List&amp;lt;Vertex&amp;gt; = getVertexList(vertexCount, edgeCount)

    println(DFS(startNum, originalVertexList.map { it.copy() }).joinToString(&quot; &quot;))
    println(BFS(startNum, originalVertexList.map { it.copy() }).joinToString(&quot; &quot;))
}

private fun getVertexList(
    vertexCount: Int,
    edgeCount: Int,
): List&amp;lt;Vertex&amp;gt; {
    val originalVertexList: List&amp;lt;Vertex&amp;gt; = IntStream.rangeClosed(0, vertexCount)
        .toArray()
        .map { Vertex(it) }

    for (i in 0 until edgeCount) {
        val (vertex1, vertex2) = readLine()?.split(' ')!!
            .map { it.toInt() }
            .take(2)

        originalVertexList[vertex1].connect(vertex2)
        originalVertexList[vertex2].connect(vertex1)
    }

    originalVertexList.forEach { it.neighbors.sort() }

    return originalVertexList
}

data class Vertex(
    val num: Int,
    var check: Boolean = false,
    val neighbors: MutableList&amp;lt;Int&amp;gt; = mutableListOf(),
) {
    fun visit() {
        if (check) {
            throw IllegalStateException(&quot;$num's check is true.&quot;)
        }
        check = true
    }

    fun connect(neighbor: Int) {
        neighbors.add(neighbor)
    }
}

fun BFS(startVertex: Int, vertexList: List&amp;lt;Vertex&amp;gt;): List&amp;lt;Int&amp;gt; {
    val queue = LinkedList&amp;lt;Int&amp;gt;()
    val result = mutableListOf(startVertex)

    vertexList[startVertex].visit()
    queue.add(startVertex)

    while (!queue.isEmpty()) {
        val (_, _, neighbors) = vertexList[queue.remove()]

        neighbors.forEach {
            if (!vertexList[it].check) {
                vertexList[it].visit()
                result.add(it)
                queue.add(it)
            }
        }
    }

    return result
}

fun DFS(startVertex: Int, vertexList: List&amp;lt;Vertex&amp;gt;): List&amp;lt;Int&amp;gt; {
    val result = mutableListOf(startVertex)
    val current: Vertex = vertexList[startVertex]

    current.visit()

    current.neighbors.forEach {
        if (!vertexList[it].check) {
            result += DFS(it, vertexList)
        }
    }

    return result
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/백준</category>
      <category>BFS</category>
      <category>dfs</category>
      <category>Kotlin</category>
      <category>백준</category>
      <author>조용한고라니</author>
      <guid isPermaLink="true">https://dev-gorany.tistory.com/365</guid>
      <comments>https://dev-gorany.tistory.com/365#entry365comment</comments>
      <pubDate>Sat, 28 Jan 2023 18:23:13 +0900</pubDate>
    </item>
  </channel>
</rss>