<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[물음표 엔지니어]]></title><description><![CDATA[백엔드 엔지니어로 일하며 마주한 기술적 트레이드오프와 설계 고민, 그리고 성장 과정을 기록하는 개발 블로그. 실무에서 얻은 경험과 인사이트를 공유합니다.]]></description><link>https://jeongkyun-dev.kr</link><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 11:11:03 GMT</lastBuildDate><atom:link href="https://jeongkyun-dev.kr/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[리텐션이 0에 수렴해서 데이터부터 다시 들여다봤더니]]></title><description><![CDATA[Foundry는 백엔드 엔지니어를 위한 기초 지식 학습 플랫폼입니다. 시험을 보고, 틀린 문제를 오답노트에 정리하고, 개념을 복습하는 서비스인데요. 베타 오픈 후 커뮤니티에 올려서 유저도 좀 모았고, 기능도 하나하나 잘 만들어놨다고 생각했습니다.
그런데 GA4를 열어보니 현실은 달랐거든요.
문제: 숫자가 말해주는 현실
GA4 리포트를 열어봤더니 대시보드 페]]></description><link>https://jeongkyun-dev.kr/0</link><guid isPermaLink="true">https://jeongkyun-dev.kr/0</guid><category><![CDATA[AI]]></category><category><![CDATA[Retrospective]]></category><category><![CDATA[side project]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 15 Mar 2026 08:03:56 GMT</pubDate><content:encoded><![CDATA[<p>Foundry는 백엔드 엔지니어를 위한 기초 지식 학습 플랫폼입니다. 시험을 보고, 틀린 문제를 오답노트에 정리하고, 개념을 복습하는 서비스인데요. 베타 오픈 후 커뮤니티에 올려서 유저도 좀 모았고, 기능도 하나하나 잘 만들어놨다고 생각했습니다.</p>
<p>그런데 GA4를 열어보니 현실은 달랐거든요.</p>
<h2>문제: 숫자가 말해주는 현실</h2>
<p>GA4 리포트를 열어봤더니 대시보드 페이지뷰는 429인데, 시험 결과 페이지뷰는 고작 몇십 건이었습니다. 오답노트 페이지는 거의 방문이 없었고요.</p>
<p>"리텐션이 안 좋은 것 같다"는 느낌은 있었는데, 정확히 어디서 얼마나 빠지는지 모르고 있었습니다. 그래서 이번에 제대로 파보기로 했습니다.</p>
<h2>데이터 파이프라인부터 만들었습니다</h2>
<p>보통은 GA4 화면을 캡처해서 공유하고, DB는 SQL 쿼리를 하나하나 돌리는데요. 이번엔 다른 방식을 써봤습니다. Claude Code(AI 코딩 에이전트)가 직접 데이터를 조회하고 분석할 수 있도록 파이프라인을 구성했거든요.</p>
<h3>Supabase DB 직접 조회</h3>
<p>Supabase는 REST API를 제공하고 있어서, Service Role Key만 있으면 바로 쿼리가 가능합니다.</p>
<pre><code class="language-bash">curl -s "https://{url}/rest/v1/exams?select=user_id,status,created_at" \
  -H "apikey: {SERVICE_ROLE_KEY}" \
  -H "Authorization: Bearer {SERVICE_ROLE_KEY}"
</code></pre>
<h3>GA4 Data API 연동</h3>
<p>GA4는 서비스 계정을 만들고 Python <code>google-analytics-data</code> 라이브러리로 접근했습니다. 트래픽, 이벤트 퍼널, 트래픽 소스 같은 데이터를 프로그래밍으로 뽑을 수 있게 됐거든요.</p>
<p>이렇게 세팅해두니까 "가입 전환율이 몇 %야?", "오답노트 사용률은?" 같은 질문에 바로 데이터로 답할 수 있게 되었습니다. 감이 아니라 숫자로 판단할 수 있는 환경이 된 거죠.</p>
<h2>퍼널을 뜯어봤더니</h2>
<p>GA4 이벤트 데이터와 DB 데이터를 교차 분석해서 전체 퍼널을 그려봤습니다.</p>
<pre><code class="language-plaintext">방문 131명 → 가입 40명(31%) → 시험시작 28명 → 시험완료 22명 → 재방문 3명(11%)
</code></pre>
<p>이벤트 기반으로 더 상세하게 보면</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>유저 수</th>
<th>전환율</th>
</tr>
</thead>
<tbody><tr>
<td>CTA 클릭</td>
<td>71명</td>
<td>-</td>
</tr>
<tr>
<td>로그인 시도</td>
<td>49명</td>
<td>69%</td>
</tr>
<tr>
<td>가입 완료</td>
<td>29명</td>
<td>59% (로그인 시도 대비)</td>
</tr>
<tr>
<td>시험 시작</td>
<td>29명</td>
<td>100%</td>
</tr>
<tr>
<td>시험 완료</td>
<td>20명</td>
<td>69%</td>
</tr>
</tbody></table>
<p>여기서 눈에 띄는 건 두 가지였는데요.</p>
<p>첫째, CTA를 클릭한 71명 중 가입 완료는 29명뿐이었습니다. 카카오 로그인 과정에서 42%가 이탈하고 있었거든요.</p>
<p>둘째, 가입하면 시험은 거의 100% 시작합니다. 근데 그 다음이 문제였습니다.</p>
<h2>DB를 파보니 더 심각했습니다</h2>
<p>DB에서 유저별 행동 패턴을 분류해봤더니 이런 그림이 나왔습니다.</p>
<table>
<thead>
<tr>
<th>유형</th>
<th>비율</th>
</tr>
</thead>
<tbody><tr>
<td>가입 후 시험 미완료</td>
<td>32%</td>
</tr>
<tr>
<td>시험 1번 보고 영구 이탈</td>
<td>43%</td>
</tr>
<tr>
<td>당일 2-3번 보고 이후 미복귀</td>
<td>14%</td>
</tr>
<tr>
<td>다른 날 재방문</td>
<td>11% (3명)</td>
</tr>
</tbody></table>
<p>75%가 첫날 이후 돌아오지 않는 거였습니다. 그리고 오답노트 데이터를 보면 더 놀라운데요.</p>
<blockquote>
<p>오답노트 126개 생성, 6개 열람(5%), 메모 작성은 4개(3%)</p>
</blockquote>
<p>핵심 기능이라고 생각했던 오답노트를 아무도 안 쓰고 있었습니다.</p>
<p>한 가지 더 확인한 건 점수와 이탈의 상관관계였는데요. 이탈 유저 평균 60점, 재방문 유저 평균 62점으로 유의미한 차이가 없었습니다. 시험을 못 봐서 떠나는 게 아니었던 거죠.</p>
<h2>진단: 새는 양동이에 물 붓기</h2>
<p>데이터를 종합해보니 사용자 여정이 이렇게 흘러가고 있었습니다.</p>
<pre><code class="language-plaintext">랜딩 방문 → 가입 → 시험 시작 → 시험 완료 → 점수 확인 → "그래서?" → 이탈
</code></pre>
<p>문제의 핵심은 시험 완료 후 "다음 행동"이 없다는 거였습니다. 점수를 확인하고 나면 할 게 없거든요. 오답노트 페이지는 별도로 찾아가야 했고, 재시험을 보려면 처음부터 새로 만들어야 했습니다.</p>
<p>처음에는 "마케팅을 더 열심히 해야 하나?" 싶었는데요. 데이터를 보니 정반대였습니다.</p>
<blockquote>
<p>지금 상태로 마케팅 예산을 2배 써서 262명이 와도, 재방문하는 건 6명뿐입니다. 전환율은 변하지 않거든요.</p>
</blockquote>
<p>리텐션을 해결하지 않고 마케팅을 늘리는 건 새는 양동이에 물을 더 붓는 거였습니다.</p>
<h2>실행 계획: P0부터 차근차근</h2>
<p>분석 결과를 토대로 우선순위를 매겼습니다. 기준은 간단했는데요 - 퍼널에서 가장 많이 새는 곳부터 막는 겁니다.</p>
<h3>P0: 활성화 루프 만들기 (가장 시급)</h3>
<p>시험 완료 후 "다음에 뭘 해야 하지?"를 해결하는 게 1순위입니다.</p>
<p><strong>결과 화면 강화</strong> - 현재는 점수와 오답 리스트만 보여주는데, 약점 토픽 분석("인덱싱에서 0/2 틀림")과 함께 "이 개념 복습하기" 링크로 학습 가이드에 바로 연결하려고 합니다.</p>
<p><strong>오답노트 인라인 표시</strong> - 별도 페이지로 이동해야 보이던 오답노트를 결과 화면 하단에 바로 보여줍니다. 사용자가 "찾아가는" 게 아니라 "자동으로 보이는" 경험으로 바꾸는 거죠.</p>
<p><strong>오답만 재시험</strong> - 틀린 문제만 모아서 바로 다시 풀 수 있게 합니다. 전부 맞출 때까지 반복하면 당일 학습 루프가 완성되는 구조입니다.</p>
<h3>P1: 랜딩 전환율 개선 (31% → 50%)</h3>
<p>CTA 클릭 71명 중 가입은 29명이었는데요. 카카오 로그인 허들이 큰 게 문제였습니다. 그래서 로그인 없이 3문제를 체험할 수 있게 하고, 결과를 보려면 가입하도록 유도하려 합니다.</p>
<h3>P2: 재방문 동기 만들기</h3>
<p>학습 현황 대시보드(토픽별 마스터율 시각화), 약점 기반 시험 추천 같은 것들입니다. P0가 해결되고 나서 진행할 예정입니다.</p>
<h2>AI를 분석 도구로 쓴 소감</h2>
<p>이번에 인상적이었던 건, AI한테 "리텐션이 낮은데 어떡해?"라고 물어본 게 아니라 GA4와 DB 데이터를 직접 조회할 수 있는 환경을 만들어줬다는 점입니다.</p>
<p>Claude Code에 Supabase REST API 접근 권한과 GA4 서비스 계정을 연결해두니, "유저별 시험 횟수 분포 보여줘", "이벤트 퍼널 그려봐" 같은 요청에 즉시 데이터를 뽑아서 분석해주더라고요. 사람이 GA4 화면을 캡처해서 공유하는 것보다 훨씬 빠르고, 교차 분석도 바로 가능했습니다.</p>
<p>핵심은 AI한테 "답을 달라"고 한 게 아니라 "데이터를 볼 수 있는 눈을 줬다"는 것인데요. 데이터에 접근할 수 있으니 추측이 아닌 근거 기반 분석이 나온 거죠.</p>
<h2>정리하며</h2>
<p>느낌으로 "리텐션이 안 좋다"를 알고 있는 것과, 데이터로 "75%가 첫날 이탈하고 오답노트 사용률은 5%"를 확인하는 건 전혀 다른 문제였습니다.</p>
<p>전자는 "마케팅을 더 해볼까?" 같은 막연한 대응으로 이어지는 반면, 후자는 "시험 결과 화면 다음에 할 게 없어서 나간다"는 구체적인 진단이 되거든요.</p>
<p>사이드 프로젝트에서 데이터 파이프라인을 만드는 게 거창하게 느껴질 수 있는데요. Supabase REST API + GA4 Data API 조합이면 생각보다 간단하게 세팅할 수 있었습니다. 비슷한 상황에 계신 분들 참고가 되면 좋겠습니다.</p>
]]></content:encoded></item><item><title><![CDATA[현대 아키텍쳐에서 DTO Pattern이 중요한 이유]]></title><description><![CDATA[DTO Pattern은 어디서 시작됐을까?
DTO Pattern은 Sun Microsystems의 J2EE Core Patterns에서 공식적으로 정리된 개념이다. (2001년 초판: Core J2EE Patterns) 최초 DTO는 분산 환경(EJB, RMI)에서 원격 호출 횟수를 줄이기 위한 목적으로 정의되었다.
이 개념을 2002년 마틴 파울러가 Patterns of Enterprise Application Architecture 책에서 ...]]></description><link>https://jeongkyun-dev.kr/dto-pattern</link><guid isPermaLink="true">https://jeongkyun-dev.kr/dto-pattern</guid><category><![CDATA[DTO Pattern]]></category><category><![CDATA[dto]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 01 Feb 2026 06:21:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769954442626/e4adcfae-f352-44ff-8295-4527c7625850.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-dto-pattern">DTO Pattern은 어디서 시작됐을까?</h2>
<p>DTO Pattern은 <code>Sun Microsystems의 J2EE Core Patterns</code>에서 공식적으로 정리된 개념이다. (2001년 초판: Core J2EE Patterns) 최초 DTO는 분산 환경(EJB, RMI)에서 원격 호출 횟수를 줄이기 위한 목적으로 정의되었다.</p>
<p>이 개념을 2002년 마틴 파울러가 <code>Patterns of Enterprise Application Architecture</code> 책에서 DTO Pattern을 소개했다. (마틴 파울러는 창시자 보다는 전파자임)</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In the field of programming a <strong>data transfer object</strong> (<strong>DTO</strong><a target="_self" href="https://en.wikipedia.org/wiki/Data_transfer_object?utm_source=chatgpt.com#cite_note-msdn-1"><sup>[1][2]</sup></a><a target="_self" href="https://en.wikipedia.org/wiki/Data_transfer_object?utm_source=chatgpt.com#cite_note-fowler-2">) i</a>s an object that carries data between processes. The motivation for its use is that communication between processes is usually done resorting to remote interfaces (e.g., web services), where each call is an expensive operation.<a target="_self" href="https://en.wikipedia.org/wiki/Data_transfer_object?utm_source=chatgpt.com#cite_note-fowler-2"><sup>[2]</sup></a> Because the majority of the cost of each call is related to the round-trip time between the client and the server, one way of reducing the number of calls is to use an object (the DTO) that aggregates the data that would have been transferred by the several calls, but that is served by one call only.</div>
</div>

<p><a target="_blank" href="https://en.wikipedia.org/wiki/Data_transfer_object?utm_source=chatgpt.com">wikiedia에서 발췌한 글</a>이다. 핵심만 뽑아보자면 다음과 같다.</p>
<blockquote>
<p>DTO는 프로세스 간 데이터를 전달하기 위한 객체이며,<br />원격 인터페이스 기반 통신에서 발생하는 비싼 호출 비용을 줄이기 위해 사용된다.</p>
</blockquote>
<p>핵심만 정리하면 이렇다.</p>
<ul>
<li>프로세스 간 통신은 보통 원격 인터페이스(Web Service 등)를 사용한다</li>
</ul>
<ul>
<li><p>호출 1회마다 비용이 크다</p>
<ul>
<li><p>네트워크 왕복 시간</p>
</li>
<li><p>직렬화 / 역직렬화</p>
</li>
<li><p>인증, 로깅, 레이트 리밋 같은 부가 오버헤드</p>
</li>
</ul>
</li>
<li><p>그래서 여러 호출로 나뉠 데이터를 DTO 하나로 묶어 한 번에 전달한다</p>
</li>
</ul>
<p>즉, 아래로 요약해볼 수 있겠다</p>
<ul>
<li><p>DTO → 데이터를 전달하기 위한 그릇</p>
</li>
<li><p>DTO Pattern → 여러 번의 호출을 한 번의 호출로 줄이기 위해 데이터를 묶어서 전달하는 패턴</p>
</li>
</ul>
<h2 id="heading-dto">DTO 본질에 집중하기</h2>
<p>DTO는 클라이언트가 필요로 하는 데이터를, 가장 효율적인 형태로 전달하기 위해 존재한다.</p>
<p>그런데 우리는 DTO를 설계할 때 종종 이런 고민을 한다.</p>
<ul>
<li><p>이 필드는 어느 Aggregate에 속하지?</p>
</li>
<li><p>이 DTO가 여러 도메인을 참조해도 괜찮을까?</p>
</li>
<li><p>이 구조가 도메인 경계를 침범하는 건 아닐까?</p>
</li>
</ul>
<p>물론 이런 고민 자체는 나쁘지 않다고 생각한다. (장 단점이 꽤나 명확한거같음)<br />다만, 이 고민이 DTO 설계를 멈추게 만들거나, 불필요하게 복잡한 구조를 낳고 있다면 필자는 잠깐 멈추고 생각해볼 필요가 있다고 생각한다.</p>
<ul>
<li><p>DTO는 도메인 경계를 설명하기 위한 객체가 아니다.</p>
</li>
<li><p>DTO는 도메인 모델을 보호하기 위한 객체도 아니다.</p>
</li>
<li><p><strong><em>DTO는 계약(contract)이다.</em></strong></p>
</li>
</ul>
<p>그리고 필자는 그 계약은 상대방이 우선되어 설계되어야 한다고 생각한다</p>
<h2 id="heading-7zie7iuk7j2yiouepo2kuoybjo2brouklcdsnbtsg4hsoihsnbtsp4ag7jwk64uk">현실의 네트워크는 이상적이지 않다</h2>
<p>우리 시스템의 고객(병원)은 네트워크 환경이 상상 이상으로 느리다.<br />(가끔 느린 수준이 아니라, 병원 열 곳 중 아홉곳은 버벅임 때문에 정상 사용이 어려울 정도임)</p>
<p>이런 환경에서 도메인 모델을 닮은 DTO를 여러 개 쪼개서 내려보내는 것은, 아키텍쳐적으로는 아름다울 수 있어도 매우 비효율적이다.</p>
<h2 id="heading-6re4656y7iscioyasoumrouklcdsnbtrn7ag7isg7yod7j2eio2vnoulpa">그래서 우리는 이런 선택을 한다</h2>
<ul>
<li><p>여러 도메인의 데이터를 하나의 DTO를 묶는다</p>
</li>
<li><p>화면 하나에 필요한 데이터를 한 번의 호출로 내려준다</p>
</li>
<li><p>도메인 간 의존성은 DTO 레벨에서 허용한다</p>
</li>
</ul>
<p>도메인 간 의존성, 트랜잭션 경계, 비지니스 규칙은 여전히 서버 내부에서 관리된다. 그래서 DTO는 그 결과를 외부에 전달하는 표현일 뿐, 도메인 모델처럼 순수해야 한다고 생각하기 시작하면 DTO는 본래 목적을 잃고, API는 호출 횟수가 늘어나며, 성능 문제를 유발할 수 있다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769930858313/fc856904-1ddf-4cce-b9b4-d68663cccfb4.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769931002587/68aa0910-48be-4000-9862-a86541461651.png" alt class="image--center mx-auto" /></p>
<p>참고로, 위와 같이 현업 상황 상 백엔드 리소스가 없다는것을 알고, (센스있게) 백엔드 엔지니어 의존없이 진행하기위해 FE엔지니어 판단하에 여러 API를 조합해서 매꾸는 선택도 자주 발생할 수 있는데 이것은 여러가지 비효율적인 문제가 있긴하지만 (최고보다는) 최선의 결정이라고 생각한다.</p>
<p>다만, 이 선택에 대한 공유만 잘 이루어지고 백로그 테스크만 잘 잡아둔다면 될거같다. 이 백로그 작업을 하는것은 더 낮은 레이턴시로 고객 경험을 올리고, 시스템의 불필요한 부하도 줄일 수 있기때문에 챕터 미션으로만 지속적으로 관리되면 될거같다는 의견이다.</p>
<p>이 내용을 토대로 팀 내 FE 개발자분들과 이야기를 나눠보려고한다. 아마 모두가 동일한 생각과 기준을 갖고있겠지만 현실은 너무 바쁘고, 정신없기때문에 우리가 놓친게 있는지 되새김질을 하기 위해서말이다.</p>
]]></content:encoded></item><item><title><![CDATA[이벤트 스토밍은 비싸다.]]></title><description><![CDATA[서론
이번 글 에서는 최근 KOS(자사 제품 명, 이하 KOS) 제품의 일본 런칭을 위해 필수 기능이였던, Stripe 결제 플랫폼과의 연동 통합 시스템을 구축하면서 시도해봤던 이벤트 스토밍 준비와 실제 결과까지의 내용을 다뤄보려고한다. 필자가 이벤트 스토밍 세션을 준비하면서 고민했던것과 실제 좋지않은 결과가 나왔던 이유를 장시간 기억하고싶어 남겨본다.
우선 이벤트 스토밍 세션 자료 준비 과정은 이랬다.
DDD 관련 서적과 이벤트 스토밍 관련 ...]]></description><link>https://jeongkyun-dev.kr/event-storming-is-expensive</link><guid isPermaLink="true">https://jeongkyun-dev.kr/event-storming-is-expensive</guid><category><![CDATA[Event Storming]]></category><category><![CDATA[DDD]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 01 Feb 2026 06:19:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769954349025/cfac9d07-7ba4-4d72-80c0-2223e76c44a8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-7isc66gg">서론</h1>
<p>이번 글 에서는 최근 KOS(자사 제품 명, 이하 KOS) 제품의 일본 런칭을 위해 필수 기능이였던, Stripe 결제 플랫폼과의 연동 통합 시스템을 구축하면서 시도해봤던 이벤트 스토밍 준비와 실제 결과까지의 내용을 다뤄보려고한다. 필자가 이벤트 스토밍 세션을 준비하면서 고민했던것과 실제 좋지않은 결과가 나왔던 이유를 장시간 기억하고싶어 남겨본다.</p>
<h3 id="heading-7jqw7isgioydtouypo2kucdsiqtthqdrsi0g7is47iwyioyekoujjcdspidruyqg6ro87kcv7j2aioydtoueroulpc4">우선 이벤트 스토밍 세션 자료 준비 과정은 이랬다.</h3>
<p>DDD 관련 서적과 이벤트 스토밍 관련 여러 아티클들을 조합하여 보다 정석적인(FM) 프로세스를 만들어본 뒤, 현재의 도메인 모델 상황과 팀원들의 도메인 지식 수준을 반영하여 아래와 같은 프레임 워크를 만들었다.</p>
<hr />
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769922197234/0865340a-2654-4b27-8995-b0e7e03987a2.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-7j2067kk7yq4ioykpo2goouwjeydgcdrrltsl4fsnbjqsia">이벤트 스토밍은 무엇인가?</h3>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>비지니스에서 실제로 “무슨 일이 일어나는지”를 중심으로 시스템을 이해하고 설계하는 협업 워크숍이다.</strong> 스트라이프를 KOS(자사 제품, 이하 KOS)에 통합하는 과정에서 기능이나 화면부터 논의하는 대신 언제, 어떤 일이, 어떤 순서로 일어나는가? 를 중심으로 전체 흐름을 함께 그려본다.</div>
</div>

<h3 id="heading-7j2067kk7yq4ioykpo2goouwjeydgcdrp6tsmrag67me7iu464uk">이벤트 스토밍은 매우 비싸다</h3>
<ul>
<li><p>이벤트 스토밍은 절대 싸지 않다</p>
</li>
<li><p>여러 직군(PO, PD. Engineer, Sales 등)이 같은 시간에 모여, 깊이 있는 사고와 결정이 필요한 논의를 한다</p>
</li>
<li><p>즉, 이 시간에는 사람의 시간과 집중력 모두 비싸다</p>
<ul>
<li>자칫하면 단순히 포스트잇 끄적인 회의로 끝날 수 있다</li>
</ul>
</li>
<li><p>따라서 우리는 이 시간에 <code>최대한 많은걸 논의하는것이 아닌, 가장 비싼 오해를 가장 빨리 발견하는것에 집중한다</code></p>
</li>
</ul>
<h3 id="heading-7jqw66as64quioyzncdsnbtrsqttirgg7iqk7yag67cn7j2eio2vmoukloqwgd8">우리는 왜 이벤트 스토밍을 하는가?</h3>
<ol>
<li><p>수납(Purchase) 도메인 모델과 Stripe 결제 연동에 대한 이해를 얼라인한다</p>
<ol>
<li><p><strong>각자가 알고있는 도메인 모델과 연동 흐름의 차이를 드러낸다</strong></p>
</li>
<li><p><strong>얼라인 된 이해를 바탕으로 BC 내 보편 언어(UL)을 정의한다</strong></p>
</li>
</ol>
</li>
<li><p>각자의 머릿속에 있는 이해가 서로 다르다는 사실을 빠르게 발견한다</p>
</li>
<li><p>말로 설명하기 어려운 흐름을 하나의 시각적 모델로 맞춰본다</p>
</li>
<li><p>얼라인된 결과물을 공식 산출물로 남긴다</p>
<ol>
<li><p>이벤트 스토밍 결과를 수납(Purchase) 도메인 모델 문서에 반영한다</p>
</li>
<li><p>이후 설계, 모델링, 구현에 활용한다</p>
</li>
</ol>
</li>
</ol>
<h2 id="heading-7kee7zajiouwqeylnq">진행 방식</h2>
<h3 id="heading-20">도메인 이벤트 나열 (20분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li>스트라이프 결제 연동 과정에서 발생하는 모든 도메인 이벤트 최대한 많이 수집</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>이벤트는 반드시 과거형으로 작성한다</p>
</li>
<li><p>이 단계에서는 이벤트의 순서, 맞고 틀림, 구현/기술 전혀 상관없음</p>
</li>
<li><p>그냥 발산한다</p>
</li>
<li><p>질보다 양이 더 중요한 단계임</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-align-20">이벤트 흐름 정렬(Align) (20분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li><p>이벤트들을 시간 흐름에 따라 정렬한다</p>
</li>
<li><p>정상 케이스와 비정상 케이스의 윤곽을 파악한다</p>
</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>이 순서가 맞는지 정렬에 대한 의문을 계속 제기한다</p>
</li>
<li><p>만약 병렬과 분기 가능성이 있다면 분리해서 표시한다</p>
</li>
<li><p>만약 논쟁 길어지면 즉석에서 결론 내지말고, 핫스팟으로 표시해준다</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-10">액터/ 트리거 식별 (10분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li>각 이벤트가 누가 / 무엇에 의해 발생했는지 명확히 하기</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>액터는 명확히 구분</p>
<ul>
<li><p>사용자</p>
</li>
<li><p>내부 시스템</p>
</li>
<li><p>관리자 (eg. 수납 담당자)</p>
</li>
<li><p>외부 시스템 (eg. Stripe)</p>
</li>
</ul>
</li>
<li><p>트리거는 반드시 3종으로만 정의한다</p>
<ul>
<li><p>시간 기반</p>
</li>
<li><p>외부 입력(eg. Stripe webhook)</p>
</li>
<li><p>내부 시스템 정책</p>
</li>
</ul>
</li>
<li><p>책임의 경계를 흐리게 두지않는다</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-15">커맨드 연결 (15분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li>이벤트를 발생시키는 의도(행위)를 드러낸다</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>커맨드는 현재형과 의도형으로 정의한다</p>
</li>
<li><p>하나의 이벤트가</p>
<ul>
<li><p>여러 커맨드에서 발생하는가?</p>
<ul>
<li><p>eg. <code>결제가 취소되었다</code></p>
<ul>
<li><p>커맨드1: <code>사용자가 결제를 취소한다</code></p>
</li>
<li><p>커맨드2: <code>관리자가 결제를 취소한다</code></p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>자동 처리로도 발생하는가? (사람의 커맨드 없이)</p>
<ul>
<li><p>eg.</p>
<ul>
<li><p>시간 기반: 만료/타임아웃/정산일 도래</p>
</li>
<li><p>외부 입력 기반: Stripe webhook 수신</p>
</li>
<li><p>내부 정책 기반: 리트라이/상태 전이 규칙</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>사용자 액션과 시스템 반응의 경계를 분명히 한다</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-20-1">정책 / 도메인 불변식 정리 (20분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li>이벤트 사이에 숨어 있는 정책, 조건, 분기를 드러낸다</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>언제, 어떤 조건에서를 반드시 질문하면서 밝혀낸다</p>
</li>
<li><p>합의되지 않은 정책은 억지로 결정하지않는다</p>
<ul>
<li>대신 왜 어려운지, 누가 결정해야 하는지를 명확히 남긴다</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="heading-10-1">컴포넌트 매핑(애그리거트 / 리드모델) (10분)</h3>
<ul>
<li><p><strong>목표</strong></p>
<ul>
<li><p>이벤트와 커맨드가 어디의 상태를 변경하는지 드러낸다</p>
</li>
<li><p>이후 설계/모델링의 초기 구조 후보를 만든다</p>
</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>이 단계에서 결정하는 애그리거트 / 리드모델은 확정이 하닌 후보 정도로 취급한다 <strong><em>(오늘 스트라이프 스토밍에서는 이미 정의가 되어있기때문에 이 규칙은 적용하지않는다)</em></strong></p>
</li>
<li><p>정책과 불변식에서 드러난 내용을 근거로만 매핑한다</p>
</li>
<li><p>논쟁이 생기면 억지로 결론 내리지않고 핫스팟으로 남긴다</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-10-2">핫스팟 정리 (10분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li>오늘 세션에서 불확실성이 가장 큰 지점을 명확히 남겨보기</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>핫스팟 유형 구분</p>
<ul>
<li><p>eg.</p>
<ul>
<li><p>정책 미결정</p>
</li>
<li><p>Stripe 제약 조건</p>
</li>
<li><p>참여자 간 해석 차이</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="heading-10-3">최종 정리 + 다음 액션 (10분)</h3>
<ul>
<li><p>목표</p>
<ul>
<li>오늘 결과를 이후 작업으로 안전하게 넘길 수 있는 상태 만들기</li>
</ul>
</li>
<li><p>규칙</p>
<ul>
<li><p>다음을 명확히 남긴다</p>
<ul>
<li><p>합의된 이벤트 흐름</p>
</li>
<li><p>추가 논의가 필요한 지점</p>
</li>
<li><p>수납(Purchase) 도메인 모델 반영 범위</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-7ko87jquioybkoy5mq">주요 원칙</h2>
<h3 id="heading-7zmv7iug7j20ioyxhuqxsoucmcdrqqjrpbtriptqsbqg7iio6riw7keaiounkoqzocdrk5zrn6zrgrjri6q">확신이 없거나 모르는건 숨기지 말고 드러낸다</h3>
<ul>
<li><p>아래 내용들 드러내라.</p>
<ul>
<li><p>이건 잘 모르겠다.</p>
</li>
<li><p>확신이 안선다</p>
</li>
<li><p>Stripe가 이걸 보장해주나?</p>
</li>
<li><p>이 케이스 실제로 발생할 수 있는건가?</p>
</li>
</ul>
</li>
<li><p>이것들은 모두 문제 제기가 아니라 이벤트 스토밍의 핵심 산출물이다</p>
</li>
<li><p>이 때 확신 없는 이벤트와 정책은</p>
<ul>
<li><p>추측으로 채우지말고</p>
</li>
<li><p>핫스팟으로 남겨두고 최종 과정에서 논의한다</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-7j2067kk7yq4ioykpo2goouwjsdqsrdqs7zripqg7iuc7iqk7ywcioyepoqzhoqwgcdslytri4jri6q">이벤트 스토밍 결과는 시스템 설계가 아니다</h3>
<ul>
<li><p>결과물은 구현 명세, API 스펙이 아니다</p>
</li>
<li><p>단지 작업자들의 이해가 얼라인 된 상태를 기록한 흔적이다</p>
<ul>
<li>이 결과물을 토대로 설계, 모델링, 구현에 활용한다</li>
</ul>
</li>
</ul>
<h3 id="heading-64z7j287zwcioydmouvuoulvcdqsidsp4qg7j2067kk7yq4ly7pounqoutncdspjhrs7ug7kcv7j2y66w8io2xioyaqe2vnoulpa">동일한 의미를 가진 이벤트/커맨드 중복 정의를 허용한다</h3>
<ol>
<li><p>동일한 의미를 가지더라도 중복을 막지않는다</p>
</li>
<li><p>중복을 즉시 합치지 않는다</p>
</li>
<li><p>중복 자체를 정보로 활용한다</p>
</li>
</ol>
<ul>
<li><p>이 때 왜 허용해야하는가?</p>
<ul>
<li><p>같은 이벤트라도, 표현이 다르거나 범위가 다르가나 의미가 다를 수 있음</p>
</li>
<li><p>eg.</p>
<ul>
<li><p>결제가 실패했다</p>
</li>
<li><p>카드 승인에 실패했다</p>
</li>
<li><p>Stripe 결제가 실패했다</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>그럼 언제 정리함?</p>
<ul>
<li><p>이벤트 나열 단계에서 정리하지않고</p>
</li>
<li><p>정리는 이벤트 흐름 정렬 단계에서 한다</p>
<ul>
<li><p>완전한 동일한 의미의 이벤트는 한 위치에 모아둔다</p>
</li>
<li><p>표현만 다른 경우, 더 도메인에 적합한 이름으로 통합</p>
</li>
<li><p>의미가 다를 가능성이 있다면, 분리 유지하거나 핫스팟 표시해두고 넘어가도록한다</p>
</li>
</ul>
</li>
<li><p><code>이벤트 나열 단계의 목적은 수렴이 아니라 발산임</code></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-7j2067kk7yq464quiouphouployducdqtidsojdsl5dshjwg7j2y664ioyeiouklcdsgqzsi6trp4wg64uk66os64uk">이벤트는 도메인 관점에서 의미 있는 사실만 다룬다</h3>
<ul>
<li><p>이벤트는 화면 변화, API 호출, 단순 로그가 아니다</p>
</li>
<li><p>도메인에서 무슨 일이 생겼는가에 집중한다</p>
</li>
<li><p>만약 <code>Stripe 결제 승인 웹훅이 수신되었다</code> 와 같은 사건이 정의되었다면 이것은 도메인 이벤트가 아닌, 도메인 이벤트를 유발하는 트리거로 구분한다</p>
<ul>
<li><p>외부 입력(Trigger/Signal): <code>Stripe 결제 승인 웹훅이 수신되었다</code></p>
<ul>
<li>관측된 사실일뿐임</li>
</ul>
</li>
<li><p>도메인 이벤트: <code>결제가 승인 되었다</code></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-7y647j2y7iobio2vmoucmouhncdrrynsuzjsp4ag7jwk64qu64uk">편의상 하나로 뭉치지 않는다</h3>
<ul>
<li><p>너무 큰 이벤트는 추후 정책/ 분기에서 터질 가능성이 높다</p>
</li>
<li><p>헷갈리면 되도록 쪼개는 쪽을 택한다</p>
<ul>
<li><p>eg. 결제 처리가 완료되었다 (X)</p>
<ul>
<li><p>결제가 승인되었다</p>
</li>
<li><p>결제가 실패되었다</p>
</li>
<li><p>결제가 취소되었다</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>정제되지 않은 과잉 표현이 뭉개진 추상화보다 보통 낫다</p>
</li>
</ul>
<hr />
<h2 id="heading-7jyeiousuoyenoulvcdsnphshlhtlzjrqbtshjwg6rog66865cy7jei642y6rkd7j2a">위 문서를 작성하면서 고민되었던것은</h2>
<h3 id="heading-7jqw66as64quioydtouvucdsmrdrpqzsnzgg64e66mu7j24iouqqounuoydhcdrhijrrlqg7j6yioyvjoqzooyeioulpa">우리는 이미 우리의 도메인 모델을 너무 잘 알고있다</h3>
<p>무슨 말이냐면, 스트라이프 통합 과정에서 필요한 애그리거트와 리드모델, 이벤트 등 이미 어떻게 동작하는지, 누가 액터이고 명령을 수행하는지에 대해 팀원 모두 너무 잘 알고있다.</p>
<p>위 전제로 인해, 진행 방식 순서에 대한 고민이 많았다. 보통 다같이 이벤트 발산과 정렬한 뒤 여러 컴포넌트들을 통합해가며 애그리거트를 도출하는데, 이미 애그리거트와 정책은 사전 플래닝 과정에서 얼라인 되어있었기때문이다.</p>
<p>그래서 아래 항목이 가장 고민이 많이됐다.</p>
<h3 id="heading-7jqw66as64quioyzncdsnbtrsqttirgg7iqk7yag67cn7j2eio2vmoukloqwgd8-1">우리는 왜 이벤트 스토밍을 하는가?</h3>
<ol>
<li><p>수납(Purchase) 도메인 모델과 Stripe 결제 연동에 대한 이해를 얼라인한다</p>
<ol>
<li><p><strong>각자가 알고있는 도메인 모델과 연동 흐름의 차이를 드러낸다</strong></p>
</li>
<li><p><strong>얼라인 된 이해를 바탕으로 BC 내 보편 언어(UL)을 정의한다</strong></p>
</li>
</ol>
</li>
<li><p>각자의 머릿속에 있는 이해가 서로 다르다는 사실을 빠르게 발견한다</p>
</li>
<li><p>말로 설명하기 어려운 흐름을 하나의 시각적 모델로 맞춰본다</p>
</li>
<li><p>얼라인된 결과물을 공식 산출물로 남긴다</p>
<ol>
<li><p>이벤트 스토밍 결과를 수납(Purchase) 도메인 모델 문서에 반영한다</p>
</li>
<li><p>이후 설계, 모델링, 구현에 활용한다</p>
</li>
</ol>
</li>
</ol>
<p>위 내용에서 새로운 애그리거트를 도출하기보다는 각자 알고있는 지식의 수준을 얼라인 하고 보편 언어를 정의하는데 필요한 도구라는것을 강조하고싶었다. 실제 스토밍 진행 전에 이 내용을 강조도 했었고, 팀원들도 동의하는 바였다.</p>
<h3 id="heading-7zwy7kea66emioqysoqzvoukla">하지만 결과는</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769924410030/d75ac510-b939-4820-bbba-8c997d353302.png" alt class="image--center mx-auto" /></p>
<p>이것은 약 2시간 정도의 시간을 진행했을 때 나온 결과물이다. 이벤트 발산과 정렬까지 꽤나 밀도있게 진행했다고 생각은 들지만 시간 투자 대비 결과가 너무 만족스럽지못했다.</p>
<p>왜냐면, 아래 거래 이벤트쪽을 자세히 살펴보면</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769924516059/f492e73e-af7b-49bd-ad47-25e1123c2c5b.png" alt class="image--center mx-auto" /></p>
<p>각자 많은 도메인 이벤트를 발산한거까지는 좋았지만, 막상 그룹핑 해보니 이미 우리는 도메인 모델을 너무 잘 알고있고 용어도 크게 다를게 없었다.</p>
<p>필자는 이 과정에서 도메인 이벤트 → 발산 → 정렬 하는 과정에서 보편 언어와 예상치 못한 사건을 발견할 수 있기를 기대했다. 하지만, 결과는 이미 모두 같은 언어를 사용하고있었고 (결제와 거래의 표현은 약간 달랐지만, 매우 사소했음) 이미 사전 플래닝에서 나온 이벤트만 정의가 되었던것이다.</p>
<p>물론 이 과정에서 약간의 다른 표현과 이벤트의 트리거 조건, 액터는 누구인가를 밝히는 과정에서 새롭게 알게된 지식 + 얼라인 된 것은 매우 좋았지만 시간 투자 대비 결과가 너무 아쉽다는 생각이 들었다.</p>
<p>매우 좋았던 부분은 사실 요청 흐름을 시퀀스 다이어그램으로 표현하고, 여기서 필요한 명령을 기준으로 정책을 뽀개보는게 더 밀도 높게 진행할것이란 생각이 스쳤고 팀원들과 이것에 대해 이야기해보니 모두 비슷한 생각이였다. (ROI가 맞지않다고 판단함)</p>
<blockquote>
<p>이때, 우리는 ‘탐색이 필요한 상태’가 아니라, ‘정렬이 끝난 상태’에 가까웠다고 생각했다</p>
</blockquote>
<p>그래서 과감하게 우리는 이벤트 스토밍 세션을 중단하고 아래와 같이 바로 시퀀스를 그려보고 도메인 모델 문서로 넘어갔다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769924963122/f38d9e9e-fe22-4fad-9020-950939036466.png" alt class="image--center mx-auto" /></p>
<p>오히려 더 뾰족하게 우리가 지금 더 고민해야 하는 영역에 집중할 수 있었고 약 2시간 가까이 집중해서 그레이 영역을 모두 밝혀냈다.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769925114858/df4e3c4d-9b7f-4a01-8fe8-67e2c27b40f0.png" alt class="image--center mx-auto" /></p>
<p>그 결과, 위와 같이 보편 언어(UL)와 정책 또한 빠른 시간 내에 깔끔하게 정리가 되었다. 처음부터 이벤트 스토밍이 아니고 위 과정을 바로 들어갔다면, 플래닝의 ½ 정도의 시간을 실제 시스템 설계에 더 투자했을 것이라 생각된다.</p>
<h2 id="heading-6re4656y7iscio2vhoyekoydmcdstzzsooug6rkw66gg7j2a">그래서 필자의 최종 결론은</h2>
<p>필자가 속한 KOS 팀은 DDD를 효과적으로 쓰기 위해 모두가 노력중이고, 계속 배워나가고있다. 엔지니어만 노력하는게 아닌 비 엔지니어(PO, PD, Sales) 모두가 말이다.</p>
<p>이벤트 스토밍은 DDD를 활용하고있는 팀에서만 사용하는 도구는 아니긴 하지만, 관련된 개념들을 접목시켜 효과적으로 도메인 사건을 도출하고 식별하며 맥락 경계(bounded context) 와 집합 (aggregate)의 지식을 맞춰볼 수 있는 행위라 생각한다.</p>
<p>하지만 이번 이벤트 스토밍 세션은 DDD를 더 잘하고, 효과적으로 사용하고싶은 팀, 구성원에게 아직은 어색한 도구를 익힌다는 과정에서 큰 도움은 되었지만, 실제 제품 기여 레벨로 봤을 때 모두의 비싼 시간을 허비한게 너무 컸었다.</p>
<p>필자는 그래서 이벤트 스토밍이라는 도구는 백지 상태에서 도메인 모델을 정의해야할 때 다시 조심히 꺼내볼거같다.</p>
<p>이미 그림이 그려져 있는 스케치북에서 이벤트 스토밍이라는 비싼 활동으로 색을 더해 가는 것은, 오히려 문제를 선명하게 만들기보다 밀도를 낮출 수도 있다는 것을 이번에 배웠다.</p>
<p>특히</p>
<ul>
<li><p>핵심 애그리거트와 정책이 이미 정리되어있고</p>
</li>
<li><p>남은 문제가 새로운 도메인 (모델) 발견이 아니라 흐름/ 조건/ 책임의 정리이며</p>
</li>
<li><p>팀의 도메인 (모델) 이해 수준이 이미 충분히 높다면</p>
</li>
</ul>
<p>이벤트 스토밍은 탐색의 도구가 아니라 (이미 얼라인 된) 확인과 반복의 도구로만 작동할 가능성이 크다는것을 느끼고 배웠다.</p>
<p>이번 경험을 통해 필자는 <strong>이벤트 스토밍을 잘하는 방법</strong> 보다는 <strong>언제 쓰지 말아야하는지</strong> 에 대한 판단 기준을 얻었다고 생각한다.</p>
<p>마지막으로 이 경험을 함께 만들고 솔직하게 방향 전환에 동의해준 KOS팀에 감사하며 이 글을 마친다.</p>
]]></content:encoded></item><item><title><![CDATA[관측가능성(Observability)이란 무엇인가?]]></title><description><![CDATA[(아래 자료는 팀 내 세션 진행을 위해 제작한 글입니다)
관측 가능성 (Observabliity)

💡
“시스템의 내부 상태를, 외부에서 관측 가능한 데이터만으로 얼마나 잘 이해할 수 있는가”를 의미한다



시스템 안에서 무슨 일이 벌어지고있는지

문제가 생겼을 때 왜 그런 일이 발생했는지

지금 상태가 정상인지 / 비정상인지


위와 같이 로그 메트릭, 트레이스 같은 신호(signal)를 통해 추론할 수 있는가를 말한다.
(≠ 모니터링한다...]]></description><link>https://jeongkyun-dev.kr/improving-observability-with-datadog</link><guid isPermaLink="true">https://jeongkyun-dev.kr/improving-observability-with-datadog</guid><category><![CDATA[observability]]></category><category><![CDATA[Datadog]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 25 Jan 2026 06:38:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769954585342/8bfae99e-cdf2-4911-988a-4fba34132363.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>(아래 자료는 팀 내 세션 진행을 위해 제작한 글입니다)</p>
<h2 id="heading-observabliity">관측 가능성 (Observabliity)</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">“시스템의 내부 상태를, 외부에서 관측 가능한 데이터만으로 얼마나 잘 이해할 수 있는가”를 의미한다</div>
</div>

<ul>
<li><p>시스템 안에서 무슨 일이 벌어지고있는지</p>
</li>
<li><p>문제가 생겼을 때 왜 그런 일이 발생했는지</p>
</li>
<li><p>지금 상태가 정상인지 / 비정상인지</p>
</li>
</ul>
<p>위와 같이 로그 메트릭, 트레이스 같은 신호(signal)를 통해 추론할 수 있는가를 말한다.</p>
<p>(≠ 모니터링한다와는 다름. 관측 가능성은 한단계 더 높은 개념임)</p>
<h2 id="heading-66qo64ui7ysw66ebiokjocdqtidsukeg6rca64ql7isx">모니터링 ≠ 관측 가능성</h2>
<h3 id="heading-monitoring">모니터링(Monitoring)</h3>
<ul>
<li><p>미리 정한 지표를 보고</p>
</li>
<li><p>미리 정한 임계치(threshold)를 넘으면 알림</p>
<ul>
<li><p>CPU가 N% 넘었는가?</p>
</li>
<li><p>에러율이 기준(정한 수치)보다 높은가?</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-observability">관측 가능성(Observability)</h3>
<ul>
<li><p>사전에 예상하지 못한 문제도</p>
</li>
<li><p>데이터를 통해 원인을 탐색하고 설명 가능</p>
<ul>
<li><p>“이 장애는 왜 발생했나요?”</p>
</li>
<li><p>“어떤 요청 경로에서 문제가 시작된건가요?”</p>
</li>
</ul>
</li>
</ul>
<p>⇒ 모니터링은 <code>알려진 문제를 감지</code> , 관측가능성은 <code>알려지지 않은 문제를 이해</code> 에 집중하는것임.</p>
<h2 id="heading-6rsa7lih6rca64ql7isx7j2yioykkeyaloyesq">관측가능성의 중요성</h2>
<ul>
<li><p>현재 KOS 시스템은 MSA / MFA로 많은 서비스를 운영중임</p>
</li>
<li><p>단일 서비스로 운영하고있지않기때문에, 어디서 문제가 시작됐는지 감으로는 파악이 아예 불가능함</p>
</li>
<li><p>또한 시스템 장애는 예상치못하게 언젠가는 발생함</p>
<ol>
<li><p>이 때, 얼마나 빨리 원인을 파악하고</p>
</li>
<li><p>얼마나 정확하게 복구하느냐</p>
</li>
</ol>
</li>
</ul>
<ul>
<li>매우 중요쓰</li>
</ul>
<ul>
<li><p>추가로, <code>관측 데이터</code> 는 단순히 서비스의 상태만이 아닌 아래 중요도도 가짐</p>
<ul>
<li><p>사용자 요청의 흐름 (eg. RUM)</p>
</li>
<li><p>특정 기능의 성능</p>
</li>
<li><p>릴리즈 이후 변화</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-3-three-pillars">관측가능성의 3대 핵심 요소(Three Pillars)</h2>
<h3 id="heading-metrics">메트릭 (Metrics)</h3>
<ul>
<li><p><code>수치 기반의 시계열 데이터</code></p>
<ul>
<li><p>eg.</p>
</li>
<li><p>CPU, 메모리</p>
</li>
<li><p>요청 수, 에러율, 지연 시간(latency)</p>
</li>
</ul>
</li>
<li><p>상태 변화 감지에 최적임</p>
</li>
</ul>
<h3 id="heading-logs">로그 (Logs)</h3>
<ul>
<li><p>사건(events) 단위의 상세 기록</p>
<ul>
<li><p>eg.</p>
</li>
<li><p>에러 스택 트레이스</p>
</li>
<li><p>비지니스 이벤트 로그</p>
</li>
</ul>
</li>
<li><p>맥락이 풍부하고, 무슨 일이 있었는지를 파악할 수 있음</p>
</li>
</ul>
<h3 id="heading-traces">트레이스 (Traces)</h3>
<ul>
<li><p>하나의 요청이 시스템을 통과하는 전체 경로</p>
<ul>
<li><p>eg.</p>
</li>
<li><p>Root Client → Application API BFF → Purchase API → MongoDB</p>
</li>
</ul>
</li>
<li><p>분산 시스템에서 병목과 원인 지점을 정확히 추적 가능함</p>
</li>
</ul>
<h2 id="heading-6re4656y7iscioq0goy4oeqwgoukpeyeseydgcdrs7tthrug7ja065a76rkmioq0goumro2vqd8">그래서 관측가능성은 보통 어떻게 관리함?</h2>
<h3 id="heading-instrumentation">계측(instrumentation)</h3>
<ul>
<li>Application과 Infra에 메트릭 수집 / 로그 표준화 / 트레이싱 심기 를 함</li>
</ul>
<h3 id="heading-amp-collector">중앙 수집 &amp; 저장 (Collector)</h3>
<ul>
<li><p>모든 로그, 메트릭, 트레이스를 하나의 플랫폼 or 통합된 시스템으로 수집</p>
<ul>
<li>eg. <a target="_blank" href="https://www.elastic.co/kr/elastic-stack">ELK</a>, Datadog, 별도의 Application 구현</li>
</ul>
</li>
</ul>
<h3 id="heading-amp">시각화 &amp; 탐색</h3>
<ul>
<li><p>대시보드</p>
</li>
<li><p>검색</p>
</li>
<li><p>서비스 맵</p>
</li>
<li><p>요청 흐름 분석</p>
</li>
</ul>
<h3 id="heading-amp-1">알림 &amp; 대응</h3>
<ul>
<li><p>단순 임계치 알림을 넘어성</p>
</li>
<li><p>이상 징후 감지 (anomaly detection), SLO 기반 알림 구축</p>
</li>
</ul>
<h2 id="heading-7jqw66asio2mgoydgcdslrtrlrvqsowg6rsa7lihioqwgoukpeyeseydhcdqtidrpqzspjhsnoq">우리 팀은 어떻게 관측 가능성을 관리중임?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322144520/8825201e-d19d-40c6-8951-e9de7be60529.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-642w7j207ysw64fpw">데이터독?</h2>
<ul>
<li><p>위 관측가능성을 올리기 위한 요구사항들을 다루기 시작하면서 등장한 SaaS 플랫폼</p>
<ul>
<li><p>관측가능성의 Three Pillars를 하나의 플랫폼에서 통합</p>
</li>
<li><p>Infra → Application → UX 까지 연결 가능</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-features">데이터독 핵심 Features</h2>
<h3 id="heading-infrastructure-monitoring">Infrastructure Monitoring</h3>
<ul>
<li><p>서버 / 컨테이너 / k8s / 클라우드 리소스 상태 수집</p>
</li>
<li><p>CPU / Memory / Disk / Network 등 메트릭 기반 가시화</p>
</li>
</ul>
<h3 id="heading-apm-application-performance-monitoring">APM (Application Performance Monitoring)</h3>
<ul>
<li><p>요청 단위 트레이싱을 통해 서비스 간 호출 흐름 추적 가능</p>
</li>
<li><p>병목 구간, 지연 원인, 에러 발생 지점 파악</p>
</li>
<li><p>어디서 문제가 시작됐는지 확인 가눙</p>
</li>
</ul>
<h3 id="heading-log-management">Log Management</h3>
<ul>
<li><p>Application 및 Infra 로그 중앙 수집 기능</p>
</li>
<li><p>구조화 된 로그 검색 및 필터링</p>
</li>
<li><p>메트릭/ 트레이스와 로그를 연결해 맥락(context) 있는 분석 가능</p>
</li>
</ul>
<h3 id="heading-rum-real-user-monitoring">RUM (Real User Monitoring)</h3>
<ul>
<li><p>실제 사용자의 요청 흐름과 성능을 브라우저 단에서 관측함</p>
</li>
<li><p>사용자 경험 관점에서의 지연, 에러, 이탈 지점 파악 가능</p>
</li>
<li><p>서버 관점 아닌, 사용자 관점의 관측 가능성을 제공함</p>
</li>
</ul>
<h3 id="heading-amp-2">대시보드 &amp; 알림</h3>
<ul>
<li><p>메트릭, 로그, 트레이스를 하나의 화면에서 시각화해줌</p>
</li>
<li><p>임계치 기반 알림을 넘어, 이상 징후(anomaly) 감지</p>
</li>
<li><p>SLO 기반 운영 지표 구성 가능</p>
</li>
</ul>
<h2 id="heading-642w7j207ysw64f7j2aioywtouwuqyjcdsijjsp5eglydsoidsnqxsnyqg7zwg6rmmpw">데이터독은 어떻게 수집 / 저장을 할까?</h2>
<ol>
<li><p>애플리케이션에서 메트릭 발생</p>
</li>
<li><p>Datadog Agent가 메트릭을 수집</p>
<ol>
<li>Datadog Agent: 메트릭, 로그, 트레이스를 수집하는 경량 데몬 프로세스임</li>
</ol>
</li>
</ol>
<ul>
<li>보통 애플리케이션과 같은 호스트에서 실행함</li>
</ul>
<ol start="3">
<li><p>Datadog 플랫폼으로 전송 및 저장</p>
</li>
<li><p>그 뒤 대시보드, 알림, 분석에 활용</p>
</li>
</ol>
<h2 id="heading-service-map">Service Map을 보면</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322178322/97f45bd6-4463-4db1-a4a6-1b7cb1c42248.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>모든 서비스들이 Agent(Datadog)을 의존하고있는것을 확인할 수 있음</p>
</li>
<li><p><strong>왜 의존하지? 어떻게 관측 데이터 처리를 하는거지? (→ 의문이 들었음. 이걸 알아보자)</strong></p>
</li>
</ul>
<h2 id="heading-642w7j207ysw64f7j20ioyduo2uhoudvoyxkcdslrtrlrvqsowg7is47yyf65cy7ja07j6i7j2e6rmmpw">데이터독이 인프라에 어떻게 세팅되어있을까?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322265096/4d6c767d-b59e-4b02-ab0d-590a793cf47a.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Datadog Agent</p>
<ul>
<li><p><a target="_blank" href="https://kubernetes.io/ko/docs/concepts/workloads/controllers/daemonset/">Kubernetes DaemonSet</a>으로 띄움</p>
<ul>
<li><p>노드 메트릭</p>
</li>
<li><p>컨테이너 메트릭</p>
</li>
<li><p>로그 파일</p>
</li>
<li><p>APM Span 수신</p>
</li>
<li><p>→ 수집된 데이터를 Datadog Saas로 전송함</p>
</li>
</ul>
</li>
<li><p>즉, 모든 노드마다 Agent Pod가 1개씩 실행됨</p>
<ul>
<li><p>예시)</p>
</li>
<li><p>Node</p>
<p>  ㄴ ticket-api Pod (Java)</p>
<p>  ㄴ purchase-api Pod (Java)</p>
<p>  ㄴ Datadog Agent Pod</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Datadog Cluster Agent</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322280383/7cfe23e7-9798-4cd8-99a9-545dd72e9178.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>노드에 붙어있지않고, Cluster 단위로 존재함</p>
</li>
<li><p>클러스터 전체를 관리하는 컨트롤 플레인 역할을 함</p>
<ul>
<li><p>Pod 생성 시 Datadog 주입 (Admission Controller) - 아래 순서대로 진행함</p>
<ol>
<li><p>kubectl apply / ArgoCD sync</p>
</li>
<li><p>Kubernetes API Server</p>
</li>
<li><p>Datadog Admission Controller (Mutating Webhook)</p>
<ul>
<li><p>Mutating Webhook?</p>
<blockquote>
<p>Kubernetes가 리소스를 생성하기 직전에, 그 리소스의 spec을 자동으로 수정할 수 있게 해주는 훅(hook)</p>
</blockquote>
<ul>
<li><p>개발자가 작성한 YAML을</p>
</li>
<li><p>Kubernetes API Server가 최종 확정하기 전에</p>
</li>
<li><p>제3의 컨트롤러가 고쳐서 넘길 수 있게 하는 메커니즘</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Pod Spec 수정 (Mutate)</p>
</li>
</ol>
</li>
</ul>
</li>
</ul>
<p>        <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322302740/2d23538f-7667-4df6-9bba-c513b1e495ad.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li><p>Pod 생성</p>
<ul>
<li><p>이미 떠있는 Pod를 건드리지않고, 앞으로 생성 될 Pod의 spec만 수정함</p>
</li>
<li><p>Deployment/Service/Pod 상태 수집</p>
</li>
</ul>
</li>
<li><p>Replicas 수, Ready 여부</p>
</li>
<li><p>이벤트(Failed, CrashLoopBackOff 등)</p>
</li>
</ol>
<h2 id="heading-6rcbioy7to2prouejo2kuoulvcdthrxtlantlbtshjwg67o066m0">각 컴포넌트를 통합해서 보면</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322388842/cbb7e88a-f177-42f2-8641-0a1463b381ed.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Datadog Cluster Agent는 Pod 생성 시 Pod Spec을 mutate하여</p>
</li>
<li><p>APM tracer(dd-java-agent, dd-trace 등)를 주입하고</p>
</li>
<li><p>애플리케이션 Pod에서 발생한 stdout/stderr 로그는 컨테이너 런타임(eg. containerd, docker)에 의해 노드의 로그 파일로 기록된다</p>
</li>
<li><p>(Span은 데이터독 에이전트로 바로 로컬 통신으로 전달함)</p>
</li>
<li><p>Datadog Agent는 각 노드에서 이 로그 파일을 비동기적으로 tailing하고</p>
</li>
<li><p>Span/Metric은 애플리케이션으로부터 네트워크로 직접 수신하여</p>
</li>
<li><p>내부 버퍼링·샘플링 후 HTTPS를 통해 Datadog SaaS로 전송한다</p>
</li>
</ul>
<h2 id="heading-monitors">(제일 얘기하고싶었던) 데이터독 Monitors 소개</h2>
<ul>
<li><p>Application / Infra에서 데이터가 발생하고</p>
</li>
<li><p>Datadog Agent가 이를 수집해서</p>
</li>
<li><p>Datadog SaaS에 저장하는것까지 이해함</p>
</li>
<li><p><strong><em>우린 이제 저장된 데이터를 잘 활용해서 관측가능성을 올려야한다</em></strong></p>
</li>
</ul>
<h2 id="heading-datadog-monitors">Datadog Monitors</h2>
<p>[관측 데이터]</p>
<p>↓</p>
<p>[조건(Condition)]</p>
<p>↓</p>
<p>[임계치 / 패턴]</p>
<p>↓</p>
<p>[상태 판단]</p>
<p>↓</p>
<p>[알림(Notification)]</p>
<ul>
<li><p>데이터독 모니터는 요런 동작 모델임 (쏘 단순)</p>
<ul>
<li><p>eg.</p>
<ul>
<li><p>조건: <code>ticket-api</code>의 HTTP 5xx 비율</p>
</li>
<li><p>임계치: 5분 평균 3% 초과</p>
</li>
<li><p>결과: Slack 알림 발송</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="heading-66em7jw9iouqqoulio2esoqwgcdsl4bsl4jri6trqbq">만약 모니터가 없었다면?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769322488519/2896df7b-a837-4945-88f1-6c5b640284a7.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>위와 같은 OOM Killed Event를 감지 못했을것임</p>
</li>
<li><p>HPA 등을 걸어놓아 리소스가 부족했을 때 스케일링도 중요하겠지만 OOM 발생이 정말 처리량이 많아져서 리소스가 부족해졌을 수 있지만</p>
<ul>
<li>메모리 릭이나 예상하지못한 처리로 인해 문제가 발생할 수 있다</li>
</ul>
</li>
<li><p><strong>그렇기떄문에 우리는 이런 비정상 사건(events)에 대해 꼭 인지할 수 있도록 관측 가능성을 올려야함</strong></p>
</li>
</ul>
<h2 id="heading-15">이제 각자 모니터 만들어봐요. (15분동안)</h2>
<ul>
<li><p>마지막엔 프론트, 백엔드 엔지니어 모두 최근 발생했던 이슈나 개발하면서 감지하고싶었던것을 15분동안 만들어보기</p>
</li>
<li><p>그 후 각자 짧게 소개해보기</p>
</li>
</ul>
<h1 id="heading-64gdlg">끝.</h1>
]]></content:encoded></item><item><title><![CDATA[극한 프로그래밍?]]></title><description><![CDATA[XP(Extreame Programming, 이하 XP)는 애자일 방법론 중 하나이다. 고객의 요구가 자주 변하는 환경에서 소프트웨어 품질을 높이고, 변화에 빠르게 대응하기 위해 고안된 개발 방법을 말한다.
1990년대, 켄트 백(kent back)이 chrysler c3 프로젝트에서 처음 체계화했다고하며, 짧은 개발 주기와 강한 피드백 루프, 협업 중심 문화를 특징으로 한다.
XP는 “가치를 극대화하려면 좋은 활동들을 극단으로 끌어올리자”라는...]]></description><link>https://jeongkyun-dev.kr/7iuk7jqp7kcb7j24io2uhouhnoq3uouemouwjeqzvcdqsidsnqug6rca6rmm7jq0iouwqeuyleuhod8</link><guid isPermaLink="true">https://jeongkyun-dev.kr/7iuk7jqp7kcb7j24io2uhouhnoq3uouemouwjeqzvcdqsidsnqug6rca6rmm7jq0iouwqeuyleuhod8</guid><category><![CDATA[extreme programming]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sat, 13 Dec 2025 05:59:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769954569759/043a5030-9012-4012-9dd5-991372b3f566.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>XP(Extreame Programming, 이하 XP)는 애자일 방법론 중 하나이다. 고객의 요구가 자주 변하는 환경에서 소프트웨어 품질을 높이고, 변화에 빠르게 대응하기 위해 고안된 개발 방법을 말한다.</p>
<p>1990년대, 켄트 백(kent back)이 chrysler c3 프로젝트에서 처음 체계화했다고하며, 짧은 개발 주기와 강한 피드백 루프, 협업 중심 문화를 특징으로 한다.</p>
<p>XP는 “가치를 극대화하려면 좋은 활동들을 극단으로 끌어올리자”라는 철학을 기반으로 한다. 예를 들어, 코드 리뷰가 좋다면 “항상 함께 코딩한다(Pair Programming)”, 테스트가 중요하다면 “테스트를 먼저 작성한다(TDD)”처럼 실천 활동들을 극단적으로 적용한다.</p>
<h2 id="heading-xp">XP의 목적</h2>
<p>XP의 목적은 크게 세 가지로 요약할 수 있다.</p>
<ol>
<li>변화에 대한 민첩한 대응</li>
</ol>
<p>요구사항이 자주 바뀌어도 짧은 개발 사이클과 지속적 개선, 고객과의 긴밀한 소통을 통해 빠르고 정확하게 대응하는 것을 목표로 한다.</p>
<ol start="2">
<li>높은 품질의 소프트웨어 제공</li>
</ol>
<p>TDD, 지속적 통합(CI), 리팩터링 등을 통해 기술적 부채를 줄이고 코드 품질을 지속적으로 유지한다.</p>
<ol start="3">
<li>팀의 생산성 및 행복 증진</li>
</ol>
<p>지속 가능한 속도, 협업 중심 문화, 명확한 피드백 구조를 통해 개발자가 지치지 않고 이끌어갈 수 있는 환경을 만든다</p>
<h2 id="heading-xp-core-values">XP의 핵심 가치(Core Values)</h2>
<p>XP는 크게 5가지의 핵심 가치를 기반으로 한다</p>
<ol>
<li>의사소통(Communication)</li>
</ol>
<p>팀원 간 지속적이고 솔직한 소통을 강조한다</p>
<ol start="2">
<li>단순성(Simplicity)</li>
</ol>
<p>지금 필요한 것만 구현하고, 과도한 설계를 지양한다</p>
<ol start="3">
<li>피드백(Feedback)</li>
</ol>
<p>테스트, 고객, 팀 내부 등 다양한 피드백 루프를 통해 개발을 조정한다</p>
<ol start="4">
<li>용기(Courage)</li>
</ol>
<p>불필요한 코드를 지우거나 설계를 바꾸는 데 두려움을 갖지 않는다</p>
<ol start="5">
<li>존중(Respect)</li>
</ol>
<p>팀원 간 상호 존중을 통해 신뢰 기반의 작업 환경을 만든다</p>
<h2 id="heading-xp-practices">XP의 주요 실천 방법(Practices)</h2>
<p>XP를 구성하는 대표적 실천 기법은 다음과 같다고한다.</p>
<ol>
<li>페어 프로그래밍(Pair Programming)</li>
</ol>
<p>두 명의 개발자가 한 컴퓨터 앞에서 함께 코딩하며, 지속적인 리뷰와 지식을 공유한다.</p>
<ol start="2">
<li>테스트 주도 개발(TDD)</li>
</ol>
<p>테스트를 먼저 작성하고, 테스트를 통과시키는 최소한의 코드만 작성한 뒤 리팩터링을 반복한다</p>
<ol start="3">
<li>지속적 통합(CI)</li>
</ol>
<p>코드를 자주 통합하고 테스트가 자동 실행되도록 하여 오류 발생 시 빠르게 잡는다</p>
<ol start="4">
<li>리팩터링(Refactoring)</li>
</ol>
<p>동작을 유지하면서 코드 구조를 개선하는 활동을 지속적으로 수행한다</p>
<ol start="5">
<li>작은 릴리즈 (Small Releases)</li>
</ol>
<p>기능을 작은 단위로 구현하고, 빠르게 배포/피드백을 받는다</p>
<ol start="6">
<li>고객 상주(On-site Customer)</li>
</ol>
<p>프로젝트 동안 실제 고객이 개발 팀과 밀접하게 소통할 수 있도록 한다</p>
<ol start="7">
<li>플래닝 게임(Planning Game)</li>
</ol>
<p>고객과 개발자가 함께 우선순위를 결정하고, 다음 반복(Iteration)의 범위를 정하는 기법이다</p>
<h2 id="heading-xp-1">XP의 장점</h2>
<p>XP는 특히 요구사항이 변화가 잦은 프로젝트에서 높은 효율을 보인다.</p>
<ol>
<li>변화 대응력 강화</li>
</ol>
<p>짧은 반복 주기와 고객 피드백으로 변화 요구를 빠르게 반영할 수 있다</p>
<ol start="2">
<li>높은 코드 품질</li>
</ol>
<p>TDD, 리팩터링, 페어 프로그래밍 등은 결과적으로 결함을 줄이고, 유지보수성을 높인다</p>
<ol start="3">
<li>위험 감소</li>
</ol>
<p>지속적 통합과 반복적인 기능 구현으로 리스크를 초기 단계에서 발견할 수 있다</p>
<ol start="4">
<li>팀 역량 향상</li>
</ol>
<p>페어 프로그래밍, 공동 소유자 전통(Collective Ownership) 등이 지식 공유와 팀 역량 상승에 기여한다</p>
<ol start="5">
<li>고객 만족도 증가</li>
</ol>
<p>고객이 개발 과정에 참여하기 때문에 전달되는 결과물과 요구의 일치도가 높다</p>
<h2 id="heading-xp-2">XP의 단점</h2>
<p>XP는 여러 이점을 제공하지만 모든 상황에 적합하진느 않다</p>
<ol>
<li>팀 문화 성숙도가 필요</li>
</ol>
<p>소통 중심 문화가 약하거나, 갈등이 많은 팀에선느 XP의 효과가 반감될 수 있다</p>
<ol start="2">
<li>고객 참여 요구가 높음</li>
</ol>
<p>고객이 지속적으로 참여해야 하기 때문에 부담이 크거나 참여가 어려운 고객에게는 적용하기 힘들 수 있다</p>
<ol start="3">
<li>페어 프로그래밍 비용 증가</li>
</ol>
<p>두 사람이 한 작업에 투입되므로 단기적으로는 투입 인력 대비 생산성이 낮아 보일 수 있다</p>
<ol start="4">
<li>큰 조직, 복잡한 프로젝트 적용 어려움</li>
</ol>
<p>대규모 조직에서는 XP가 요구하는 높은 유연성과 직접적인 커뮤니케이션 구조를 유지하기 어렵다</p>
<hr />
<p>이처럼 XP는 단순함, 피드백, 용기, 그리고 협업을 기반으로 하는 실천 중심의 방법론이다. 모든 프로젝트에 적합하지는 않지만 변화 대응과 코드 품질 측면에서 큰 강점을 제공하며, 팀의 문화가 이를 지지한다면 강력한 개발 방식이 될 수 있다.</p>
]]></content:encoded></item><item><title><![CDATA[Gateway의 Connection Pool 고갈로 인한 장애, 원인은 다운 스트림의 GC Pause였다]]></title><description><![CDATA[서론
최근 Gateway가 간헐적으로 요청을 처리하지 못하는 장애가 발생했다. 짧은 시간이었지만, 특정 시점에 에러가 집중적으로 터지면서 실제 사용자 영향도 발생했다.
처음에는 단순한 네트워크 이슈나 일시적인 트래픽 문제라고 생각했다. 하지만 로그와 지표를 하나씩 따라가다 보니, 문제의 원인은 예상보다 훨씬 안쪽에 있었다.
이번 글에서는 이 장애를 어떻게 인지했고, 어떤 가설을 세웠으며, 왜 최종적으로 다운스트림의 GC Pause와 Event ...]]></description><link>https://jeongkyun-dev.kr/gateway-connection-pool-gc-pause</link><guid isPermaLink="true">https://jeongkyun-dev.kr/gateway-connection-pool-gc-pause</guid><category><![CDATA[troubleshooting]]></category><category><![CDATA[node js]]></category><category><![CDATA[webclient]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Mon, 22 Sep 2025 16:40:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760280511553/76e8f5fa-f710-4534-80a7-3c42e72a3274.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-7isc66gg">서론</h2>
<p>최근 Gateway가 간헐적으로 요청을 처리하지 못하는 장애가 발생했다. 짧은 시간이었지만, 특정 시점에 에러가 집중적으로 터지면서 실제 사용자 영향도 발생했다.</p>
<p>처음에는 단순한 네트워크 이슈나 일시적인 트래픽 문제라고 생각했다. 하지만 로그와 지표를 하나씩 따라가다 보니, 문제의 원인은 예상보다 훨씬 안쪽에 있었다.</p>
<p>이번 글에서는 이 장애를 어떻게 인지했고, 어떤 가설을 세웠으며, 왜 최종적으로 다운스트림의 GC Pause와 Event Loop Delay에 도달하게 되었는지를 순서대로 정리해보려고 한다. 단기적인 대응부터 장기적인 관점에서 무엇을 고민해야 했는지까지 함께 다뤄보려고한다.</p>
<h2 id="heading-66y47kccioydge2zqq">문제 상황</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758555556736/ee1eab99-fcfb-41f9-8f8e-f4a38569c00a.png" alt class="image--center mx-auto" /></p>
<p>모니터링 채널에 많은 알람이 찍히고있어, 빠르게 모니터링을 진행했다.</p>
<p>서비스를 라우팅하는 주 서비스인 Gateway의 에러 그래프를 보니, 특정 시점에 수천 건의 에러가 짧은 시간 동안 집중적으로 발생한것을 확인했다.</p>
<blockquote>
<h5 id="heading-gateway-errors-graph"><strong>Gateway Errors Graph 이미지 첨부</strong></h5>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760280302155/a8a2c29c-08ac-4316-9521-5dcc2ba3c0b7.png" alt class="image--center mx-auto" /></p>
<p>로그를 확인해보니 아래 메시지가 반복적으로 찍히고 있었다. 그것도 이틀 연속, 간헐적으로 짧은 시간 동안 장애가 발생했다.</p>
<pre><code class="lang-plaintext">org.springframework.web.reactive.function.client.WebClientRequestException: 
Pending acquire queue has reached its maximum size of 500
</code></pre>
<p>처음에는 단순한 네트워크 문제인가로 접근했지만, 해당 로그는 사실 WebClient의 Connection Pool 대기열이 꽉 찼다는 뜻이었다. 즉, Gateway에서 Account API(인증 서비스)로 요청을 보냈지만 커넥션을 빌릴 수 없어서 요청 자체가 실패한 상황이었다.</p>
<h3 id="heading-kirsm5dsnbgg7lau7kcbkio"><strong>원인 추적</strong></h3>
<p>그럼 왜 커넥션 풀 고갈이 일어났을까?<br />에러 시점을 기준으로 Account API 지표를 확인했다.</p>
<blockquote>
<p><strong>Account API Rumtime Metrics 이미지 첨부</strong></p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758556616621/4f58dbcb-79d5-4acc-8942-be692ca2261f.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>응답 지연</strong>: 특정 시간대에 N초 이상 증가됨</p>
</li>
<li><p><strong>Event Loop Delay</strong>: 200ms 이상 급증함</p>
</li>
</ul>
<p>즉, Account API 자체가 제때 응답을 돌려주지 못했고, 이로 인해 Gateway에서 커넥션이 반환되지 못한 것이다.</p>
<h3 id="heading-6re867o4ioybkoydua">근본 원인</h3>
<p>문제는 트래픽 패턴에 있었다.</p>
<p>이 서비스는 특정 시간대에 이용량이 몰리는 경우는 있었지만, 예기치 못한 트래픽 급증이 발생하는 서비스는 아니었다. 그런데 주간 트래픽 그래프를 보니 평소 대비 <strong>6배 이상 급증한 흔적</strong>이 있었다.</p>
<blockquote>
<p><strong>Gateway 주간 트래픽 그래프</strong></p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760254265697/192dae6f-799b-469d-bef1-73c8127ccd7f.png" alt class="image--center mx-auto" /></p>
<p>(물론, 병원에서 가장 많이 이용하는 시간대는 있지만, 예상치못한 트래픽이 갑작스레 급증하거나 하진 않는다)</p>
<p>Gateway의 Weekly 트래픽 지표를 토대로 급증한 요청을 집계해보니, 16일 늦은 저녁, 계정의 상태 변경을 최신화하기위해 클라이언트의 폴링 로직이 배포된 영향이였다.</p>
<h3 id="heading-64ya7j2rioylnoyekq">대응 시작</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760254539744/a1cb0e6c-8500-4e63-becc-ec1070cb89d5.png" alt class="image--center mx-auto" /></p>
<p>일단 문제 상황을 팀원분들께 공유했고, 세번째 프로덕션 장애를 막기 위해 다양한 논의를 스레드에서 진행했다. 이 논의 과정에서 빠르게 단기적 방어 전략을 세웠고, 응급 조치를 진행했다. (단기적/ 장기적 전략을 어떻게 세웠는지는 아래에서 구체적으로 다룰 예정)</p>
<h2 id="heading-event-loop-delay">왜 Event Loop Delay가 그렇게 크게 발생했을까?</h2>
<p>이제부터 Node.js 런타임 내부 동작, GC Pause, 그리고 Event Loop Delay에 대해 살펴보려한다.</p>
<p>문제의 본질을 이해하기위해서는 Node JS의 구조적 특성을 이해해야된다 생각한다</p>
<h4 id="heading-nodejs">Node.js 동작 원리</h4>
<p>Node.js 애플리케이션은 단일 스레드 기반 이벤트 루프 모델로 동작한다.</p>
<p>즉, 모든 요청 처리는 결국 이벤트 루프라는 한 줄짜리 실행 트랙 위에서 순차적으로 흘러간다.</p>
<blockquote>
<p>“이벤트 루프가 잠깐이라도 중단되면, 동시에 들어오는 요청들이 모두 영향을 받는다.”</p>
</blockquote>
<p>이번 장애 상황의 핵심도 바로 이 부분이었다.</p>
<h3 id="heading-libuv">libuv와 이벤트 루프의 내부를 열어보면</h3>
<p>Node.js는 구글의 V8 자바스크립트 엔진 위에서 동작하며, 비동기 I/O 처리는 libuv라는 C 언어 레벨의 이벤트 루프 라이브러리가 담당한다</p>
<p>libuv의 루프를 코드에서 단순화해보면 아래와 같다</p>
<pre><code class="lang-c"><span class="hljs-comment">// libuv 내부 구조를 단순화해보면</span>
<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">uv_run</span><span class="hljs-params">(<span class="hljs-keyword">uv_loop_t</span>* loop, uv_run_mode mode)</span> </span>{
    <span class="hljs-keyword">while</span> (loop-&gt;active_handles &gt; <span class="hljs-number">0</span>) {
        uv__update_time(loop);        <span class="hljs-comment">// 현재 tick 시간 갱신</span>
        uv__run_timers(loop);         <span class="hljs-comment">// setTimeout / setInterval 콜백 실행</span>
        uv__run_pending(loop);        <span class="hljs-comment">// I/O 콜백 실행</span>
        uv__run_idle(loop);           <span class="hljs-comment">// Idle 핸들 실행</span>
        uv__io_poll(loop, timeout);   <span class="hljs-comment">// epoll/kqueue/select 대기 및 이벤트 감지</span>
        uv__run_check(loop);          <span class="hljs-comment">// setImmediate 콜백 실행</span>
        uv__run_closing_handles(loop);<span class="hljs-comment">// Close 이벤트 처리</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>이 루프가 바로 Node.js 이벤트 루프의 핵심이다.</p>
<p>V8이 자바스크립트로 코드를 실행하면, 비동기 호출(<code>fs.readFile</code>, <code>fetch</code>, <code>setTimeout</code> 등)은 libuv의 큐에 작업으로 등록된다. 그리고 libuv는 OS의 이벤트 시스템(epoll, kqueue, IOCP 등)을 활용해 이벤트를 감시하고, 완료된 작업을 다시 V8으로 콜백 형태로 전달한다.</p>
<h3 id="heading-phase">각 Phase의 실제 흐름은</h3>
<p>위 루프를 JS 개발자 입장에서 좀 더 가깝게 표현하면 다음과 같다.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// pseudo-code로 작성하면</span>
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  timersPhase();         <span class="hljs-comment">// setTimeout / setInterval</span>
  ioCallbacksPhase();    <span class="hljs-comment">// I/O 콜백 (예: DNS, FS 등)</span>
  idlePreparePhase();    <span class="hljs-comment">// 내부 준비 작업</span>
  pollPhase();           <span class="hljs-comment">// 이벤트 감시 및 실행 (핵심)</span>
  checkPhase();          <span class="hljs-comment">// setImmediate 실행</span>
  closePhase();          <span class="hljs-comment">// 소켓 close, cleanup</span>
}
</code></pre>
<p>이 과정에서 <code>pollPhase()</code> 가 핵심인데, <code>pollPhase()</code> 순서에서 실제 네트워크 소켓이나 파일 I/O 결과를 기다린다.</p>
<p>이 과정은 크게 다음 두 가지 상태를 오간다.</p>
<ol>
<li><p>이벤트가 존재할 때: 즉시 콜백 실행</p>
</li>
<li><p>이벤트가 없을 때: 대기 (idle state)</p>
</li>
</ol>
<p>이 대기 시간은 곧 이벤트 루프가 쉬는 시간이며, 만약 이 시점에서 CPU-bound 작업이 들어오면 (eg. json parsing, encryption, 대용량 연산 등)이 들어오면 루프가 한 tick 동안 돌아오지 못하고 <strong>Event Loop Delay</strong>가 발생한다.</p>
<h3 id="heading-tick-delay">tick과 delay의 관계는</h3>
<p>Node.js 내부적으로는 매 tick마다 현재 시간(<code>uv__update_time</code>)을 기록하고, 다음 tick 시작 시점과 비교하여 지연을 측정한다.</p>
<p>아래와 같은 방식으로 Event Loop Delay를 실험해볼 수 있다</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> start = performance.now();

<span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> delay = performance.now() - start - <span class="hljs-number">100</span>;
  <span class="hljs-keyword">if</span> (delay &gt; <span class="hljs-number">50</span>) <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Event loop delayed by <span class="hljs-subst">${delay.toFixed(<span class="hljs-number">2</span>)}</span>ms`</span>);
}, <span class="hljs-number">100</span>);

<span class="hljs-comment">// cpu bound 작업 수행</span>
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> end = <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">500</span>; <span class="hljs-comment">// 500ms 동안 block 해보기</span>
  <span class="hljs-keyword">while</span> (<span class="hljs-built_in">Date</span>.now() &lt; end);
}, <span class="hljs-number">2000</span>);
</code></pre>
<p>이 코드를 실행하면 약 2초 후부터 “Event loop delayed by ~500ms” 같은 로그가 찍히는것을 확인할 수 있다.<br />즉, JS 스레드가 500ms 동안 block 되었기 때문에, 루프가 제때 돌아오지 못한 것이다.</p>
<h3 id="heading-v8-libuv">V8 ↔ libuv의 협업 구조는</h3>
<p><strong>이벤트 루프는 단순한 무한 반복문이 아니다.</strong></p>
<p>사실은 V8과 libuv의 협업 구조로 이루어져 있다.</p>
<pre><code class="lang-javascript">+------------------------------------------------------+
|                   JavaScript Code                    |
|  (<span class="hljs-keyword">async</span>/<span class="hljs-keyword">await</span>, <span class="hljs-built_in">Promise</span>, callbacks 등)                |
+-------------------------------┬----------------------+
                                │
                                ▼
+------------------------------------------------------+
|                      V8 Engine                       |
| - 실행 컨텍스트 관리 (Execution Context)                 |
| - 메모리 / Heap 관리 (Garbage Collection)              |
| - JS 코드 → 바이트코드 컴파일 및 실행                       |
+-------------------------------┬----------------------+
                                │
                                ▼
+------------------------------------------------------+
|                      libuv Layer                     |
| - Event Loop (uv_run)                                |
| - Timer Queue (<span class="hljs-built_in">setTimeout</span>, <span class="hljs-built_in">setInterval</span>)              |
| - Async I/O Polling (epoll, kqueue, IOCP)            |
| - Thread Pool (비동기 CPU-bound 작업 처리)               |
+-------------------------------┬----------------------+
                                │
                                ▼
+------------------------------------------------------+
|                  OS Kernel &amp; System Calls            |
| - 네트워크 소켓, 파일 I/O, 타이머, 시그널 관리                |
| - epoll/kqueue/select 기반 이벤트 감시                  |
+------------------------------------------------------+
</code></pre>
<p>즉, Node.js에서 비동기 I/O는 실제로 JS 스레드가 직접 처리하지 않는다.</p>
<p>libuv의 이벤트 루프가 OS 커널 이벤트를 감시하고 완료된 이벤트를 콜백 형태로 V8에 전달한다.</p>
<p>하지만 중요한 점은, GC Pause나 CPU-bound 연산은 V8 엔진 내부에서 발생하기 때문에, 이때는 libuv가 아무리 준비되어 있어도 JS 스레드가 멈추기 때문에 콜백을 처리할 수 없다. 그게 바로 Event Loop Delay의 근본 원리이다.</p>
<h3 id="heading-7lwc7kkf7jy866gcioydtouypo2kucdro6jtlitsnzgg7z2q66ae7j2eioyaloyvve2vmouptcdslytrnpjsmyag6rcz64uklg">최종으로 이벤트 루프의 흐름을 요약하면 아래와 같다.</h3>
<ol>
<li><p>V8이 JS 코드를 실행한다</p>
</li>
<li><p>비동기 작업은 libuv의 큐로 위임된다</p>
</li>
<li><p>libuv는 OS 레벨에서 이벤트를 기다린다</p>
</li>
<li><p>완료된 작업을 다시 JS 콜백 큐에 push한다</p>
</li>
<li><p>GC, CPU 작업 등으로 JS 스레드가 막히면, 다음 tick이 지연된다</p>
</li>
</ol>
<p><strong>여기서 지연된 시간이 바로 Event Loop Delay 이다.</strong></p>
<p>이 사이클은 보통 몇 밀리초도 걸리지않는데, 특정 시점에 한 tick이 수백 밀리초 이상 늦어진다면, 그 순간 들어오는 모든 요청이 줄줄이 밀리 시작한다. (이게 이번 장애의 핵심이였음)</p>
<h4 id="heading-event-loop-delay-tick">그래서 Event Loop Delay는 이벤트 루프가 제때 tick을 돌지 못했을 때 발생한다.</h4>
<p>예를 들어,</p>
<ul>
<li><p>원래는 10ms 단위로 루프가 돌아야 하는데</p>
</li>
<li><p>어떤 작업이 200ms를 점유했다면</p>
</li>
<li><p>그 순간 event loop delay는 190ms로 기록되게되는것이다</p>
</li>
</ul>
<p>즉, delay 지표는 <code>이벤트 루프가 얼마나 늦게 돌고 있는가</code>를 보여주는 신호(signal)이다.</p>
<p>이번 Account API의 장애 시점에서 이 값이 200ms 이상 치솟은 건, 루프가 그만큼 멈췄다는 의미인것이다.</p>
<h3 id="heading-v8-gc-event-loop-gc-pause">그래서 V8의 GC가 Event Loop를 왜 멈춘게 한건데? (GC Pause)</h3>
<p>Node.js의 자바스크립트 실행은 V8 엔진 위에서 수행된다. V8은 단순히 JS를 실행하는 엔진이 아니라, 실행 컨텍스트와 메모리(Heap)를 직접 관리하는 런타임 시스템이다.</p>
<p>즉, V8은 다음 두 가지 책임을 동시에 가진다.</p>
<ol>
<li><p>JS 코드를 빠르게 실행한다.</p>
</li>
<li><p>사용이 끝난 객체를 찾아서 메모리를 회수한다.</p>
</li>
</ol>
<p>이 두 번째 단계, 바로 <code>Garbage Collection(GC)</code>이 이번 장애의 숨은 주범이었다.</p>
<h3 id="heading-v8-heap">V8의 Heap 구조</h3>
<p>V8의 메모리는 크게 두 세대(Generation)로 나뉜다.</p>
<pre><code class="lang-plaintext">+-----------------------------------------+
|                 Heap                    |
+-------------------+---------------------+
| Young Generation  |  Old Generation     |
| (새 객체, 임시값) | (오래된 객체)             |
+-------------------+---------------------+
</code></pre>
<ol>
<li><p><strong>Young Generation</strong></p>
<ul>
<li><p>새로 생성된 객체들이 저장되는 공간이다.</p>
</li>
<li><p>메모리가 작고, GC가 자주 발생하지만 빠르다.</p>
</li>
<li><p>여기서 수행되는 GC를 <strong>Minor GC</strong>라고 한다.</p>
</li>
</ul>
</li>
<li><p><strong>Old Generation</strong></p>
<ul>
<li><p>여러 번의 GC를 견뎌 살아남은 객체들이 옮겨지는 공간이다.</p>
</li>
<li><p>용량이 크고, GC는 느리지만 횟수는 적다.</p>
</li>
<li><p>여기서 수행되는 GC를 Major GC (또는 Mark-Sweep-Compact)라고 한다.</p>
</li>
</ul>
</li>
</ol>
<p>V8의 GC는 단순히 쓰지 않는 메모리를 지운다 수준으로 그치지않는다.</p>
<p>실제로는 아래와 같은 단계를 거친다.</p>
<ol>
<li><h4 id="heading-mark-phase">Mark Phase (마킹 단계)</h4>
</li>
</ol>
<ul>
<li><p>루트 객체(Global, Stack 등)에서 참조 가능한 모든 객체를 따라가며 표시(mark)한다.</p>
</li>
<li><p>즉, “아직 살아있는 객체”를 식별한다.</p>
</li>
</ul>
<ol start="2">
<li><h4 id="heading-sweep-phase">Sweep Phase (제거 단계)</h4>
</li>
</ol>
<ul>
<li>마킹되지 않은 객체(참조되지 않는 객체)를 찾아 메모리에서 제거한다.</li>
</ul>
<ol start="3">
<li><h4 id="heading-compact-phase">Compact Phase (압축 단계)</h4>
</li>
</ol>
<ul>
<li>남은 객체들을 Heap의 앞부분으로 모아 메모리 단편화(Fragmentation)를 줄인다.</li>
</ul>
<p>이 Compact 단계가 바로 Stop-the-World(STW)의 핵심이다<br />이때 JS 실행이 완전히 멈춘다. 왜냐면, 객체의 위치가 바뀌면 모든 참조 포인터를 다시 계산해야 하기 때문이다.</p>
<h3 id="heading-stop-the-worldstw">Stop-the-World(STW)란?</h3>
<p>STW는 GC가 실행되는 동안 JS 실행이 완전히 중단되는 구간을 말한다.<br />즉, 그 순간 이벤트 루프도 멈춘다.</p>
<pre><code class="lang-plaintext">JS 실행 중 → [GC Start] → (모든 JS 코드 중단) → [GC End] → JS 실행 재개
</code></pre>
<p>아래는 간단히 그 시점을 시각화한 예시다</p>
<pre><code class="lang-plaintext">|---- JS 실행 ----|■■■■■■■■■■■■■■|---- JS 실행 ----|
                  ↑
               GC Pause 구간
</code></pre>
<p>이 Pause가 50ms 정도라면 거의 체감이 안 된다.<br />하지만 200ms~500ms 이상 길어지면, <strong>Event Loop Delay</strong>로 바로 이어진다.</p>
<h3 id="heading-delay">실제로 어떻게 Delay가 측정되었나?</h3>
<p>V8 엔진은 내부적으로 GC 수행 시간을 측정하며,<br />Node.js는 <code>eventLoopDelay</code> 또는 <code>gcDuration</code> 같은 메트릭으로 이를 노출한다.</p>
<p>장애 당시 Account API의 지표에서는 다음 패턴이 관측되었다.</p>
<ul>
<li><p><strong>GC Pause Duration</strong>: 40~50ms</p>
</li>
<li><p><strong>Event Loop Delay</strong>: 200ms 이상</p>
</li>
</ul>
<p>이는 단순히 GC 하나가 느린 게 아니라, 여러 GC Pause가 연속적으로 발생하면서 이벤트 루프의 tick 주기를 밀어버린 상황이었던것이다.</p>
<h3 id="heading-major-gc">그런데 왜 Major GC가 자주 발생했나?</h3>
<p>V8이 Major GC를 수행하는 이유는 단순하다. 바로 Old Generation 영역이 꽉 찼기 때문이다.</p>
<p>이 영역은 주로 다음 이유로 가득 찬다.</p>
<ol>
<li><p><strong>짧은 주기의 대량 객체 생성</strong></p>
<ul>
<li><p>JSON 직렬화/역직렬화</p>
</li>
<li><p>DTO 변환, 복사 등</p>
</li>
</ul>
</li>
<li><p><strong>GC가 도달하지 못하는 객체 참조 유지</strong></p>
<ul>
<li>전역 캐시나 클로저 내부의 오래된 참조</li>
</ul>
</li>
<li><p><strong>메모리 해제 타이밍 불일치</strong></p>
<ul>
<li>JS는 객체 참조가 사라져야만 GC 대상이 됨</li>
</ul>
</li>
</ol>
<p>즉, 트래픽이 급증하고 JSON 변환이 반복되는 상황에서는 메모리 할당/해제가 폭증하면서 Major GC가 더 자주 일어나게 된다.</p>
<h3 id="heading-gateway">gateway 흐름을 도식으로 살펴보면</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760253741771/d78ac23f-ca9d-4f2a-9c8c-730c1b233de2.png" alt class="image--center mx-auto" /></p>
<p>이걸 앞선 Gateway 문제와 연결해보면 이렇게 된다.</p>
<ol>
<li><p>Account API에서 GC Pause → Event Loop Delay 발생</p>
</li>
<li><p>응답이 지연됨 → Gateway WebClient가 커넥션을 붙잡고 대기</p>
</li>
<li><p>Pool에 여유 커넥션이 없음 → Pending Queue로 요청 밀림</p>
</li>
<li><p>Queue까지 한계치 도달 → <code>Pending acquire queue has reached its maximum size</code> 에러 발생</p>
</li>
<li><p>클라이언트의 요청(Request)은 실패 응답(5xx)를 받게됨</p>
</li>
</ol>
<p>즉, Gateway에서 본 에러의 근본 원인은 Account API 내부의 이벤트 루프 지연이었다.</p>
<h3 id="heading-account-api"><strong>그런데 Account API는</strong></h3>
<p>Account API는 서비스의 모든 요청이 반드시 거쳐야 하는 계정/보안 서비스이다. 여기서 이벤트 루프가 멈추면, 사실상 서비스 전체가 멈추는 것과 같다.</p>
<p>일반적인 데이터 집중적인 마이크로 서비스라면 Circuit Breaker로 요청을 차단하는 선택을 할 수 있겠지만, Account API는 그럴 수조차 없었다. (물론 회로 차단을 세밀히 조정하면 효과는 볼 수 있겠지만)</p>
<p>결국 GC Pause와 Event loop Delay를 줄이는것이 서비스 안정성의 핵심 과제라고 생각됐다.</p>
<h3 id="heading-kirri6jquldsoihsnbgg64ya7j2rioyghouetsoq"><strong>단기적인 대응 전략</strong></h3>
<p>이번 장애에서 취한 응급 대응은 크게 두 가지였다.</p>
<ol>
<li><p><strong>타임아웃 단축 (fail fast)</strong></p>
<ul>
<li><p>기존 Gateway WebClient의 타임아웃이 5초였다.</p>
</li>
<li><p>이 설정 때문에 Account API가 응답하지 못하면 커넥션을 최대 5초 동안 붙잡게 되었고, 그 결과 커넥션 풀 고갈로 이어졌다.</p>
</li>
<li><p>타임아웃을 단축해 Fail-Fast 전략으로 전환함으로써, 지연이 전체 시스템에 전파되는 걸 우선적으로 차단하도록했다.</p>
</li>
<li><p>요 전략은 에러율은 순간적으로 증가할 수 있지만, 풀 고갈로 인한 연쇄 장애를 막는 데는 효과적이 방어수단이라고 생각했다.</p>
</li>
</ul>
</li>
<li><p><strong>Account API Scale-out</strong></p>
<ul>
<li><p>Account API는 모든 서비스 요청의 관문 역할을 하기 때문에, 단일 인스턴스에서 Event Loop가 멈추는 순간 전체 서비스 장애로 이어질 수 있다</p>
</li>
<li><p>따라서, Account API의 Pod 수를 기존 대비 2배로 확장해 트래픽을 분산했다</p>
</li>
<li><p>이를 통해 특정 인스턴스에서 GC Pause가 발생하더라도, 그 영향이 전체 장애로 확산되는 상황은 방지할 수 있었다</p>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>참고로 이 시점에서 HPA를 적용하는 선택지도 고려할 수 있다.</p>
<p>다만, 이번 상황에서는 이미 장애가 발생한 이후였고 event loop delay는 cpu 사용률처럼 즉각적인 scale 지표로 삼기엔 애매한 특성이 있어 수동 scale out으로 빠르게 대응하는 쪽을 선택했다.</p>
</blockquote>
<h3 id="heading-7j6l6riw7kcb7j24ioqwnoyeocdsoitrnru">장기적인 개선 전략</h3>
<p>단기적인 대응으로 일시적인 안정성은 확보했지만, Event Loop Delay와 GC Pause는 언젠가/ 언제든지 다시 반복될 수 있다. 따라서 장기적으로 아래와 같은 접근이 필요하다고 판단했다.</p>
<ol>
<li><p><strong>이벤트 루프 지연에 대한 관측성 확보</strong></p>
<ul>
<li>Event Loop Delay 지표를 Monitor Alert 로 추가하여, 100ms 이상의 지연이 발생할 경우 즉시 인지할 수 있도록한다</li>
</ul>
</li>
<li><p><strong>메모리 프로파일링 및 최적화</strong></p>
<ul>
<li><p>heap snapshot, heamdump 등을 활용해 어떤 객체가 주로 메모리를 점유하는지 파악한다</p>
</li>
<li><p>특히 json 직렬화/역직렬화 과정에서 불필요한 객체 생성/ 복사가 있다면 개선 대상으로 본다</p>
</li>
</ul>
</li>
<li><p><strong>GC 튜닝</strong></p>
<ul>
<li><p>Node.js 실행 시 <code>—max-old-space-size</code> 옵션으로 Heap 크기를 조정해 Major GC 발생 빈도를 줄인다</p>
<ul>
<li><p>다만 heap 크기를 무작정 키우면 STW 시간이 함께 늘어날 수 있기때문에, 애플리케이션이 어떤 객체를 얼마나 자주 생성하고, 그 객체들이 얼마나 오래 살아남는지와 같은 메모리 할당 패턴을 파악해보고 heap 크기를 조정하는것이 필요하다</p>
</li>
<li><p>이를 위해, 관측성 도구에 내장된 기능(eg. datadog profiling) 또는 pprof 툴같은걸 이용해서 실제 메모리 할당 그래프와 GC 발생 주기의 상관관계를 추적하는 것이 중요하다 (필자는 datadog 기능 이용하였음)</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>스케일링 전략 (HPA 관점)</strong></p>
<ol>
<li><p>CPU 사용률 기반 HPA는 GC Pause나 Event Loop Delay를 직접적으로 해결하지는 못한다</p>
</li>
<li><p>하지만 지속적인 트래픽 증가 상황에서는 완충 역할을 할 수 있다</p>
</li>
<li><p>장기적으로 아래 내용을 고려해보고있다</p>
<ol>
<li><p>CPU / Memory 기반 HPA로 기본적인 트래픽 대응</p>
</li>
<li><p>Event Loop Delay는 알람 및 운영 판단 지표로 활용</p>
</li>
<li><p>Delay 증가 시 캐싱, 트래픽 차단, 수동 스케일 아웃과 같은 대응 시나리오 준비</p>
</li>
</ol>
</li>
</ol>
</li>
<li><p><strong>캐싱 전략</strong></p>
<ul>
<li><p>동일한 계정 검증, IP 확인, MFA 상태 조회 등이 반복되기때문에, 캐시 계층을 두어 불필요한 CPU 및 메모리 소모를 줄인다</p>
<ul>
<li>실제로 모든 API들이 Account API를 거치면서, 최소 3회 이상의 DB Query가 발생하고 있었기 때뭉네 이 부분은 GC 부담을 줄이는 데에도 직접적인 효과가 있을 것으로 판단했다</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="heading-66ei66y066as">마무리</h2>
<p>이번 장애를 겪으면서 다시 한 번 느낀 건, Node.js의 단일 스레드 모델은 단순해 보이지만 결코 단순하지 않다는 점이었다.</p>
<p>이벤트 루프의 한 틱이 밀리는 순간, 그 영향은 생각보다 빠르게, 그리고 넓게 퍼진다. (게다가 인증 서비스였어서) 겉으로는 Gateway의 커넥션 풀 문제처럼 보였지만, 실제 원인은 훨씬 아래 계층에서 조용히 쌓이고 있었다.</p>
<p>장애를 마주했을 때 무작정 스케일을 늘리는 선택도 필요할 수 있다. 하지만 그보다 더 중요한 건, 내가 만든 서비스가 내부에서 어떻게 돌아가고 있는지 이해하고 있는가라는 질문이라고 생각한다.</p>
<p>이번 경험이, Event Loop Delay나 GC Pause를 지표에서 처음 마주했을 때, “왜 이런 현상이 나오는 걸까?”를 한 번 더 고민해보는 계기가 되었으면 좋겠다는 생각을 하며 마무리한다.</p>
]]></content:encoded></item><item><title><![CDATA[아키텍처 특성을 식별하라]]></title><description><![CDATA[아키텍처에서 가장 흔한 안티패턴 중 하나 101

\=> 모든 아키텍처 특성을 지원하는 제네릭 아키텍처를 설계하려는것.

그래서 우선 도메인 관심사에서 아키텍처 특성을 도출해야한다.

도메인 이해관계자들과 아키텍트는 서로 다른 말을 한다. 그래서 도메인 관심사를 아키텍처 특성을 옮겨야한다. 아래는 일반적인 도메인 관심사와 이를 뒷받침 하는 아키텍처 특성을 정리하는 표이다.






도메인 관심사아키텍처 특성



인수 합병상호 운용성, 확장성...]]></description><link>https://jeongkyun-dev.kr/7jwe7ykk7ywn7lkyio2kueyeseydhcdsi53rs4ttlzjrnbw</link><guid isPermaLink="true">https://jeongkyun-dev.kr/7jwe7ykk7ywn7lkyio2kueyeseydhcdsi53rs4ttlzjrnbw</guid><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 17 Aug 2025 08:35:14 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p><strong>아키텍처에서 가장 흔한 안티패턴 중 하나 101</strong></p>
</blockquote>
<p>\=&gt; 모든 아키텍처 특성을 지원하는 제네릭 아키텍처를 설계하려는것.</p>
<ol>
<li><p>그래서 우선 도메인 관심사에서 아키텍처 특성을 도출해야한다.</p>
</li>
<li><p>도메인 이해관계자들과 아키텍트는 서로 다른 말을 한다. 그래서 도메인 관심사를 아키텍처 특성을 옮겨야한다. 아래는 일반적인 도메인 관심사와 이를 뒷받침 하는 아키텍처 특성을 정리하는 표이다.</p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td>도메인 관심사</td><td>아키텍처 특성</td></tr>
</thead>
<tbody>
<tr>
<td>인수 합병</td><td>상호 운용성, 확장성, 적응성, 신장성</td></tr>
<tr>
<td>출시 시기</td><td>민첩성, 시험성, 배포성,</td></tr>
<tr>
<td>유저 만족</td><td>성능, 가용성, 내고장성, 배포성, 민첩성, 보안</td></tr>
<tr>
<td>경쟁 우위</td><td>민첩성, 배포성, 확장성, 가용성, 내고장성</td></tr>
<tr>
<td>시간 및 예산</td><td>단순성, 실행성</td></tr>
</tbody>
</table>
</div><ol start="4">
<li>암묵적 아키텍처 특성</li>
</ol>
<ul>
<li><p>가용성 =&gt; 유저는 언제든지 접속할 수 있어야한다.</p>
</li>
<li><p>신뢰성 =&gt; 시스템을 문제없이 사용하려면 반드시 필요하다.</p>
<ul>
<li>구입하려고 사이트 접속했는데 자꾸 접속 끊기면 리텐션 없을거임.</li>
</ul>
</li>
<li><p>보안 =&gt; 모든 시스템에 공통적인 암묵적 특성</p>
<ul>
<li>안전하지않은 소프트웨어를 원하는 사람은 아무도 없다.</li>
</ul>
</li>
</ul>
<p>아키텍처 특서은 서로 연관되어 움직이므로 중요도(Criticality)에 따라 우선순위는 달라질 수 있다. 아키텍트는 보안이 설계의 구조적인 측면에 영향을 미치거나 애플리케이션에 매우 중대한 요소라고 판단할 경우 아키텍처 특성으로 간주한다.</p>
]]></content:encoded></item><item><title><![CDATA[가면 갈수록 일이 재밌어져요. 하루하루 정말 빡센데 말이죠. 여러분은 요즘 어때요?]]></title><description><![CDATA[문득 생각해봤는데, 저는 강남언니 팀에 합류하고 단 하루도 안 바쁜 적이 없었던 것 같아요.
아마 정말 많은 분들이 본인이 만들고 있는 제품/ 팀이 시장에서 가능성을 증명하기 위해 각자의 레일에서 최선을 다해 달리고 있을 거예요. 또는 입사 초기라면 팀에게 본인의 효용을 증명하는 시간을 보내고 계실 수도 있을 것 같고요.
저는 KOS팀(KOS: 만들고 있는 제품명, 스쿼드명)에 합류한 지도 벌써 3년을 향해 가고있어요. 그런데도 여전히 서투르고...]]></description><link>https://jeongkyun-dev.kr/6rca66m0ioqwioyimouhnsdsnbzsnbqg7j6s67cm7ja07kc47jqulidtlzjro6jtlzjro6gg7kcv66eqiou5oeyevounscdrp5dsnbtso6auwqdsl6zrn6zrtotsnyag7jqu7kayioywtouvjoyald8k</link><guid isPermaLink="true">https://jeongkyun-dev.kr/6rca66m0ioqwioyimouhnsdsnbzsnbqg7j6s67cm7ja07kc47jqulidtlzjro6jtlzjro6gg7kcv66eqiou5oeyevounscdrp5dsnbtso6auwqdsl6zrn6zrtotsnyag7jqu7kayioywtouvjoyald8k</guid><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sat, 19 Jul 2025 15:04:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752937532066/fb17d790-129f-4935-bf4a-e36bbc6c4e77.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>문득 생각해봤는데, 저는 강남언니 팀에 합류하고 단 하루도 안 바쁜 적이 없었던 것 같아요.</p>
<p>아마 정말 많은 분들이 본인이 만들고 있는 제품/ 팀이 시장에서 가능성을 증명하기 위해 각자의 레일에서 최선을 다해 달리고 있을 거예요. 또는 입사 초기라면 팀에게 본인의 효용을 증명하는 시간을 보내고 계실 수도 있을 것 같고요.</p>
<p>저는 KOS팀(KOS: 만들고 있는 제품명, 스쿼드명)에 합류한 지도 벌써 3년을 향해 가고있어요. 그런데도 여전히 서투르고, 자주 넘어지고, 많은 부분에서 깨지고 있어요. (하하..)</p>
<p>가끔은 무슨 말이든 찰떡같이 이해하고, 솔루션을 척척 만들어내는 사람이면 좋겠다고 생각하지만, 그런 역량이 제게는 없더라고요.   그래서 입사 초반에는 효능감을 잃는 느낌이 싫고, 속상하기도 했어요.</p>
<p>그런데 왜 저는 시간이 지나면 지날수록 일이 재밌고, 이 팀의 미래가 기대될까요?</p>
<p>내린 답은 생각보다 단순했는데요. "여전히 저를 서투르게 만들고, 넘어지게 하는 팀원들이 있기 때문이었어요."</p>
<p>KOS팀은 각자가 믿는 가치에 따라 거리낌 없이 도전하는 팀이에요.   때로는 치열하게 부딪히기도 하지만, 그만큼 더 나은 (최선의) 결과로 이어져요.</p>
<p>더 놀라운 건, 자신의 주장이 옳다고 끝까지 밀어붙이던 사람도, 다양한 인풋을 함께 고민하고 나면 “그게 더 낫겠다”고 말하며 기꺼이 팀의 방향에 헌신한다는 점이에요.</p>
<p>저는 여전히 이런 팀이 좋고, 함께하고 있는 것이 감사해요.</p>
<p>배경과 맥락을 먼저 이해하고, 근본적인 원인부터 짚어가며 ‘최선’의 결과를 만들어내는 KOS팀에 조금 더 많이 기여하고 싶어요.</p>
<p>저처럼 이런 문화를 좋아하는 분이라면, 혹은 작게나마 관심이 생기셨다면 언제든 편하게 커피챗을 요청해주세요. 저도 아직 많이 배우는 중이라, 다양한 분들과 이야기 나누는 게 제게도 큰 배움이 될 거라 생각해요 🙂</p>
<p>애정 가득한 우리 KOS팀 단체 커버 사진으로 마무리합니다 ㅎㅎ</p>
]]></content:encoded></item><item><title><![CDATA[본질은 결국 하나이구나]]></title><description><![CDATA[시스템도 지속적인 통합과 전달을 통해 성숙해진다. 인간도 지속적인 통합과 전달을 통해 성숙해진다.
다만, 그 과정에서 얼마나 의미있는 알맹이를 가지고있느냐가 중요한거같다. 또한, 통합과 전달을 하는 매개체가 올바르지않다면, 아무리 통합과 전달을 주기적으로 잘 실행해도 성숙해지지 않을 수 있다.]]></description><link>https://jeongkyun-dev.kr/67o47kei7j2aioqysoq1rsdtlzjrgpjsnbtqtazrgpg</link><guid isPermaLink="true">https://jeongkyun-dev.kr/67o47kei7j2aioqysoq1rsdtlzjrgpjsnbtqtazrgpg</guid><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sat, 19 Jul 2025 15:02:57 GMT</pubDate><content:encoded><![CDATA[<p>시스템도 지속적인 통합과 전달을 통해 성숙해진다. 인간도 지속적인 통합과 전달을 통해 성숙해진다.</p>
<p>다만, 그 과정에서 얼마나 의미있는 알맹이를 가지고있느냐가 중요한거같다. 또한, 통합과 전달을 하는 매개체가 올바르지않다면, 아무리 통합과 전달을 주기적으로 잘 실행해도 성숙해지지 않을 수 있다.</p>
]]></content:encoded></item><item><title><![CDATA[Subdomain에는 어떤 유형들이 있나요?]]></title><description><![CDATA[서론
DDD의 전략적 설계 안에는 여러가지 개념들이 있다.
그 개념중 하나로는 Subdomain이 있는데, 이 개념안에는 몇가지 유형들로 나뉘어진다.
유형으로 나눈 목적과 그로 얻고자 하는 가치가 매우 확고하여, 유형들의 구분은 매우 중요하다고 생각된다.
반버논이 말한 팩터들을 가지고 내용을 정리해본다.
Subdomain이 뭐죠?
Subdomain은 전체 비지니스 도메인의 하위 부분을 말한다.
대부분의 비지니스 도메인은 전체를 포괄적으로 바라보...]]></description><link>https://jeongkyun-dev.kr/subdomain</link><guid isPermaLink="true">https://jeongkyun-dev.kr/subdomain</guid><category><![CDATA[Subdomian]]></category><category><![CDATA[DDD]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Tue, 03 Jun 2025 08:33:20 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-7isc66gg">서론</h2>
<p>DDD의 전략적 설계 안에는 여러가지 개념들이 있다.</p>
<p>그 개념중 하나로는 <code>Subdomain</code>이 있는데, 이 개념안에는 몇가지 유형들로 나뉘어진다.</p>
<p>유형으로 나눈 목적과 그로 얻고자 하는 가치가 매우 확고하여, 유형들의 구분은 매우 중요하다고 생각된다.</p>
<p>반버논이 말한 팩터들을 가지고 내용을 정리해본다.</p>
<h2 id="heading-subdomain">Subdomain이 뭐죠?</h2>
<p><code>Subdomain</code>은 전체 비지니스 도메인의 하위 부분을 말한다.</p>
<p>대부분의 비지니스 도메인은 전체를 포괄적으로 바라보기에는 너무 크고 복잡하기 마련이다.</p>
<p>그래서 거대하고 복잡한, 프로젝트 내에서 문제 영역을 논리적으로 쪼개는데 사용되는것이 <code>Subdomain</code>이다.</p>
<p>결국 비지니스의 핵심 영역에 대해 관심사를 나누는것이다. 그리고 <code>Subdomain</code>을 개발하는데, DDD를 사용했다면 매우 명확한 Bounded Context를 만들어낼 수 있다. (반버논은 DDD를 사용할 때, Bounded Context와 Subdomain은 일대일 관계를 맺도록 권장한다. 다만 목표에 가깝고, 예외는 언제든지 있다)</p>
<h2 id="heading-subdomain-3">Subdomain의 유형은 3가지로 나뉜다</h2>
<h3 id="heading-7zw17iusiouphouploydua">핵심 도메인</h3>
<p>보편언어를 신중하게 만들기 위한 전략적 투자 영역으로, 주요 자원을 할당하는 명시적인 Bounded Context이다.</p>
<p>여기에는 잘 정의된 도메인 모델이 존재한다. 이 핵심 도메인은 다른 경쟁자들에 대한 차별화를 만들 영역이기 때문에 프로젝트 목록에서 매우 높은 우선순위를 갖는다.</p>
<p>기업은 경쟁사와 모든 것을 차별화 할 수 없기 때문에, 핵심 도메인은 기업이 특히 더 뛰어나야하는 부분에 대한 경계를 구분해준다.</p>
<p>따라서, 핵심 도메인은 소프트웨어에서 가장 큰 투자가 필요한 곳이다.</p>
<h3 id="heading-7kea7juqioyenou4jouphouploydua">지원 서브도메인</h3>
<p>이미 존재하는 제품으로 해결할 수 없는 맞춤 제작 개발이 필요한 모델링 영역을 말한다.</p>
<p>여기에는 핵심 도메인에서와 같은 투자 방식을 동일하게 따를 필요는 없다. 전략적 차별화를 위해 투자한 것에 실패하지않으면서도 지원 형태의 Bounded Context에 너무 큰 투자를 하지 않기위해 아웃소싱을 고려해볼 수 있다.</p>
<p>하지만, 알겠지만 지원 서브도메인 없이는 핵심 도메인을 성공시킬수는 없다. 지원 서브도메인이 존재해야 핵심 도메인을 성공시킬 수 있다. 즉, 지원 서브도메인도 중요한 소프트웨어 모델이다.</p>
<h3 id="heading-7j2867cyioyenou4jouphouploydua">일반 서브도메인</h3>
<p>기존 제품 구매를 통해 바로 충족시킬 수 있는 경우에 해당한다. 아웃소싱을 할 수도있고, 핵심 도메인 또는 좀 더 작은 지원 서브도메인에 할당된 엘리트 개발자가 없는 팀에서 직접 개발을 할 수 있는 영역이다.</p>
<p>따라서, 일반 서브도메인을 핵심 도메인으로 오해하지않아야한다. 오해하게된다면, 핵심 도메인의 투자를 일반 도메인에 할 수 있기때문이다.</p>
<hr />
<p>위 세가지 유형을 현재 내가 기여하고있는 제품에 접목시켜보면, 생각보다 쉽게 유형 구분이 가능하다. 놀라운것은 팀원 모두가 위 유형을 명확하게 표현만 안했지, 서브 도메인들을 설명하고 개선이 필요할 때 암묵적으로 유형들을 나뉘어 설명하곤했다.</p>
<p>요즘 개인적으로, 표현할 때 암묵적으로, 느낌적인 느낌(?)으로, 감각적으로 등의 설명을 정의된 용어를 찾았을 때 작은 희열을 느낀다. 우리는 대개 전략적으로 보편 언어를 사용하게되는 이점은 매우 뚜렷한것을 알지만, 제품 관련이 아니라면 보편언어 선정을 고려하지않는 경우가 많다. 개념에 대한 명확한 확립이 안되면, 목적은 동일하지만 표현 차이로 불필요한 비용이 발생할 수 있는데 이런 부분들을 하나씩 해소해나가고자 이번 글을 작성했다.</p>
]]></content:encoded></item><item><title><![CDATA[Domain Storytelling 기법 이해하기]]></title><description><![CDATA[누구나 누군가에게 본인 머리에 있는 생각을 전달하는것은 어려워한다
특히 어떤 일에 대한 착상이나 구상인 Idea에 대해 이야기를 전달해야한다면, 더더욱 어렵다.
이럴 때, 먼저 함께 이해하고 쓸 수 있는 방법과 용어 같은 “공통 기반”을 마련하면 좋다.

A가 B에게 비지니스에 관해 이야기할 때, 어떤 용어를 사용하며, 그 용어가 뭘 의미하는지 설명하고

지금 얘기하고있는것은 어떤 업무처리 과정과 관련이 있는지 설명하고

업무처리 과정에서 중요...]]></description><link>https://jeongkyun-dev.kr/domain-storytelling</link><guid isPermaLink="true">https://jeongkyun-dev.kr/domain-storytelling</guid><category><![CDATA[DDD]]></category><category><![CDATA[#Domain-Driven-Design]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Mon, 05 May 2025 06:12:33 GMT</pubDate><content:encoded><![CDATA[<h3 id="heading-kirriitqtazrgpgqkidriitqtbdqsidsl5dqsowg67o47j24iouououmroyxkcdsnojripqg7iod6rcb7j2eioyghoulro2vmoukloqygydgcdslrtroktsm4ztlzzri6q"><strong>누구나</strong> 누군가에게 본인 머리에 있는 생각을 전달하는것은 어려워한다</h3>
<p>특히 <code>어떤 일에 대한 착상이나 구상인 Idea에 대해 이야기를 전달해야한다면</code>, 더더욱 어렵다.</p>
<p>이럴 때, 먼저 함께 이해하고 쓸 수 있는 방법과 용어 같은 <code>“공통 기반”</code>을 마련하면 좋다.</p>
<ul>
<li><p>A가 B에게 비지니스에 관해 이야기할 때, <strong>어떤 용어를 사용하며, 그 용어가 뭘 의미하는지 설명하고</strong></p>
</li>
<li><p>지금 얘기하고있는것은 <strong>어떤 업무처리 과정과 관련이 있는지 설명하고</strong></p>
</li>
<li><p>업무처리 과정에서 <strong>중요한 단계는 무엇일지 설명하고</strong></p>
</li>
<li><p><strong>누가 그 업무처리 과정에 관련</strong>이 있을지 설명한다</p>
</li>
</ul>
<p><strong><em>이런 일련의 과정을 어떻게 설명하면 좋을까?</em></strong></p>
]]></content:encoded></item><item><title><![CDATA[Domain Event와 Integration Event를 잘 구분짓고 있나요?]]></title><description><![CDATA[배경
EDA(Event-Driven Architecuture) 환경을 설계할 때 Event에 대해서 여러 종류가 존재한다는것을 대부분 알고있을것이다.
필자가 최근 기여하고있는 제품은 다양한 이벤트들을 생산(produce)하고 소비(consume)하여 많은 도메인 서비스 컴포넌트 간 데이터를 동기화하고 일관성을 최종적으로 확보하는 구조이다.
팀 내에서 제품을 만들 때 DDD(Domain-Driven Design) 방법론을 적극적으로 활용하고 있음...]]></description><link>https://jeongkyun-dev.kr/domain-event-integration-event</link><guid isPermaLink="true">https://jeongkyun-dev.kr/domain-event-integration-event</guid><category><![CDATA[Domain Events]]></category><category><![CDATA[Intergation Events]]></category><category><![CDATA[DDD]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sat, 01 Mar 2025 12:08:40 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-67cw6rk9">배경</h2>
<p>EDA(Event-Driven Architecuture) 환경을 설계할 때 <code>Event</code>에 대해서 여러 종류가 존재한다는것을 대부분 알고있을것이다.</p>
<p>필자가 최근 기여하고있는 제품은 다양한 이벤트들을 생산(produce)하고 소비(consume)하여 많은 도메인 서비스 컴포넌트 간 데이터를 동기화하고 일관성을 최종적으로 확보하는 구조이다.</p>
<p>팀 내에서 제품을 만들 때 DDD(Domain-Driven Design) 방법론을 적극적으로 활용하고 있음에도, 욕구적으로 도메인 이벤트라 정의했는데도 불구하고 애그리것의 상태만 담긴것이 아닌 다른 컴포넌트에서 처리까지를 기대하여 개념에 위배되는 이벤트 설계를 하는 좋지않은 사고가 생긴다 생각되어 이번 기회에 명확하게 개념들을 구분지어보려한다.</p>
<p>개념들은 크게는 <code>도메인 이벤트(Domain Event)</code>, <code>통합 이벤트(Integration Event)</code>, <code>외부 이벤트(External Event)</code>로 나눌 수 있다. 외부 이벤트는 정말 외부에서 호출되는 훅이나 API 요청을 말하는것이기때문에 이번 글에서 생략한다.</p>
<h2 id="heading-7j2067kk7yq4ioq4souwmcdsi5zsiqtthzzsnyag7jmcioyxho2dne2vmoukloqxuoq5jd8">이벤트 기반 시스템은 왜 채택하는걸까?</h2>
<p>전통적인 모놀리식(monolithic) 아키텍처에서는 서비스 간 강한 결합 (tight coupling) 때문에 확장성과 유지보수성이 떨어지는 경우가 많았다. 이런 문제점을 해결하기 위해 서비스들을 분리했고, 단순 컴포넌트들을 분리만 한다고 위 문제들을 해결할 수 없기에 이벤트 기반 시스템(Event-Driven Architecture, EDA)을 구축해 비동기 메시징을 통해 서비스 간 결합도를 낮추고, 확장성을 높이는 방식을 택하면서 많은 기업에서 채택하기 시작했다.</p>
<p>(물론 비동기 프로세싱을 통해 동시 처리량을 높이는 이유도 있을 수 있겠지만, 이는 이번글에서 큰 관련이 없어 넘어간다)</p>
<p>그래서 이벤트 기반 시스템이 각광받는 이유를 정리해보면 다음과 같다.</p>
<blockquote>
<ol>
<li><strong>비동기 처리 및 확장성</strong></li>
</ol>
</blockquote>
<p>이벤트를 큐잉하여 병렬로 처리가 가능해진다. (e.g. 결제 승인 후 즉시 배송 준비 or 배치 잡에서 메시징을 이용해 동시 처리 성능을 올려야할 때 등)</p>
<blockquote>
<ol start="2">
<li><strong>마이크로서비스 간 결합도 감소</strong></li>
</ol>
</blockquote>
<p>직접적인 API 호출 없이 서비스 간 독립적인 비동기 통신이 가능해진다. 즉, 각 서비스들 간 직접 통신이 아닌 메시지 브로커를 통해 처리하는것이다.</p>
<blockquote>
<ol start="3">
<li><strong>최종적 일관성 (Eventual Consistency) 보장</strong></li>
</ol>
</blockquote>
<p>중앙 데이터베이스가 없더라도, 이벤트 기반으로 데이터 정합성을 맞출 수 있다.</p>
<blockquote>
<ol start="4">
<li>실시간 반응형 시스템 구축 용이</li>
</ol>
</blockquote>
<p>고객 행동 데이터를 실시간 분석해야하거나, 특정 도메인에서 사건이 발생했을 때 리액티브하게 알림을 처리하거나 등등의 유즈케이스에서 최적화를 할 수 있다.</p>
<p>그러나 여기서 말하는 이벤트들은 모두 동일한 책임과 관심사를 갖고있진않다. 이는 이벤트를 설계할 때 정말 중요한 요소이다. 하나씩 알아보자.</p>
<h2 id="heading-domain-event">도메인 이벤트 (Domain Event)</h2>
<p>도메인 이벤트는 도메인 모델 내부에서 발생하는 비지니스적으로 중요한 사건을 말한다. 주로 특정 애그리거트(Aggregate)에서 비지니스 규칙을 반영하고 같은 Bounded Context 내에서 동작한다.</p>
<h3 id="heading-64e66mu7j24ioydtouypo2kuouklcdslrtrlrvqsowg64z7j6r7zwy64ky7jqupw">도메인 이벤트는 어떻게 동작하나요?</h3>
<ul>
<li><h5 id="heading-aggregate">특정 Aggregate에서 상태 변화가 발생하면, 트랜잭션 내에서 이벤트를 발생시킨다.</h5>
</li>
<li><p>같은 Bounded Context 내에서 이벤트 핸들러를 통해 비지니스 로직을 확장하거나 후속 처리를 수행한다.</p>
</li>
<li><p>보통 실무에선 동기 또는 내부 비동기 메시징 방식으로 처리한다.</p>
</li>
</ul>
<h3 id="heading-7jii7iuc">예시</h3>
<p>일반적인 커머스에서 주문을 처리하는 과정을 묘사해보면, 주문 완료 시 재고가 할당되고, 결제가 요청될것이다.</p>
<p>이를 <code>주문됨(OrderPlaced)</code> =&gt; <code>재고 할당됨(ItemStockAllocated</code>) =&gt; <code>결제 요청됨(PaymentInitiated)</code> 순으로 후속 트리거 되어 주문의 프로세스가 처리된다.</p>
<p>이 때, 주의를 해야하는것은 주문됨 사건은 Order Aggregate의 도메인 이벤트임으로 외부 서비스(e.g. 배송)에 대한 내용을 담으면 안된다. 예시 코드로 보면 다음과 같다.</p>
<h3 id="heading-7j6y66q765ccioyyioylna">잘못된 예시</h3>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderPlaced</span></span>(
    <span class="hljs-keyword">val</span> orderId: String,
    <span class="hljs-keyword">val</span> userId: String,
    <span class="hljs-keyword">val</span> shippingAddress: String,  <span class="hljs-comment">// X 배송 관련 정보가 포함되어 있다</span>
    <span class="hljs-keyword">val</span> deliveryDate: LocalDate   <span class="hljs-comment">// X 주문 서비스의 도메인 이벤트에서 배송 컨텍스트가 포함되어있다</span>
)

<span class="hljs-meta">@Service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> applicationEventPublisher: ApplicationEventPublisher) {

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">placeOrder</span><span class="hljs-params">(orderId: <span class="hljs-type">String</span>, userId: <span class="hljs-type">String</span>, shippingAddress: <span class="hljs-type">String</span>)</span></span> {
        <span class="hljs-comment">// place order domain logic...</span>
        println(<span class="hljs-string">"주문이 생성되었습니다. 주문 ID: <span class="hljs-variable">$orderId</span>"</span>)

        <span class="hljs-comment">// 여기서 주문됨 이벤트를 생산할 때, 배송 서비스의 처리를 위해 배송 컨텍스트가 주문 애그리것 도메인 이벤트에 포함이 되어있다.</span>
        <span class="hljs-keyword">val</span> event = OrderPlaced(orderId, userId, shippingAddress, LocalDate.now().plusDays(<span class="hljs-number">3</span>))
        applicationEventPublisher.publishEvent(event)
    }
}
</code></pre>
<p>주문을 할 때 보통 배송지와 같은 정보를 입력한다. 이 때, 배송지에 대한 컨텍스트가 주문 애그리거트의 속성인지 아닌지를 이벤트 설계할 때 판단해야한다. 단순하게 해당 이벤트를 브로커를 통해 발행해 배송 컴포넌트에서 처리가 되어야하기에 넣는다면, 이는 도메인 이벤트가 아닌 통합이벤트가 된다.</p>
<p>대게 주문의 생성과 배송의 생성은 별개이다. 주문이 생성되었다고 반드시 배송이 생성된다는 보장이 없으며, 위 논리라면 배송 서비스가 OrderPlaced 도메인 이벤트에 강하게 의존되며 <strong>주문 서비스가 배송 서비스의 변경에 취약해진다.</strong></p>
<h3 id="heading-orderservice">올바르게 수정한 OrderService</h3>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderPlaced</span></span>(<span class="hljs-keyword">val</span> orderId: String, <span class="hljs-keyword">val</span> userId: String)

<span class="hljs-meta">@Service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> applicationEventPublisher: ApplicationEventPublisher,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> kafkaTemplate: KafkaTemplate&lt;String, OrderCreatedIntegrationEvent&gt;
) {

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">placeOrder</span><span class="hljs-params">(orderId: <span class="hljs-type">String</span>, userId: <span class="hljs-type">String</span>, shippingAddress: <span class="hljs-type">String</span>)</span></span> {
        <span class="hljs-comment">// place order domain logic...</span>
        println(<span class="hljs-string">"주문이 생성되었습니다. 주문 ID: <span class="hljs-variable">$orderId</span>"</span>)

        <span class="hljs-keyword">val</span> domainEvent = OrderPlaced(orderId, userId)
        applicationEventPublisher.publishEvent(domainEvent)

        <span class="hljs-keyword">val</span> integrationEvent = OrderCreatedIntegrationEvent(orderId, userId, shippingAddress, LocalDate.now().plusDays(<span class="hljs-number">3</span>))
        kafkaTemplate.send(<span class="hljs-string">"order-created-topic"</span>, integrationEvent)
    }
}
</code></pre>
<h3 id="heading-shippingservice">ShippingService</h3>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderCreatedIntegrationEvent</span></span>(
    <span class="hljs-keyword">val</span> orderId: String,
    <span class="hljs-keyword">val</span> userId: String,
    <span class="hljs-keyword">val</span> shippingAddress: String,
    <span class="hljs-keyword">val</span> deliveryDate: LocalDate
)

<span class="hljs-meta">@Component</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShippingService</span> </span>{

    <span class="hljs-comment">// 주문 애그리것의 도메인 이벤트가 아닌 통합 이벤트를 구독하고있다.</span>
    <span class="hljs-meta">@KafkaListener(topics = [<span class="hljs-meta-string">"order-created-topic"</span>], groupId = <span class="hljs-meta-string">"shipping-service"</span>)</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">handleOrderCreatedEvent</span><span class="hljs-params">(event: <span class="hljs-type">OrderCreatedIntegrationEvent</span>)</span></span> {
        println(<span class="hljs-string">"배송 준비 시작! 주문 ID: <span class="hljs-subst">${event.orderId}</span>, 주소: <span class="hljs-subst">${event.shippingAddress}</span>"</span>)
    }
}
</code></pre>
<p>그래서 위와 같이 생성된 Order Aggregate의 변화만 담기는 것이 중요하다. 도메인 이벤트와 통합 이벤트를 잘 구분지어 발행 논리를 설계하고 구현해야한다. 외부 서비스에선 통합 이벤트에 집중할 뿐이다. 이 때, 해당 애그리것의 전역 식별자(Aggregate Root Identifier)를 통해 추가 정보는 조회하여 얻어 처리하는 방식을 택할 수 있다.</p>
<h2 id="heading-integration-event">통합 이벤트 (Integration Event)</h2>
<p>위 도메인 이벤트 설명에서 말했듯 통합 이벤트는 다른 서비스와 데이터를 공유할 때 사용된다.</p>
<p>주문이 완료되면 결제 요청이 이루어지며 결제가 완료 된 이후에 배송 요청이 되어야한다는 프로세스의 가정으로 이벤트를 설계하면 다음과 같이 도출될것이다.</p>
<p><strong>주문 서비스</strong></p>
<ul>
<li><p>OrderPlacedEvent (도메인 이벤트) 발생</p>
</li>
<li><p>OrderCreatedIntegrationEvent (통합 이벤트) 발행 → Kafka → 결제 서비스 전달</p>
</li>
</ul>
<p><strong>결제 서비스</strong></p>
<ul>
<li><p>OrderCreatedIntegrationEvent를 구독하여 결제 진행</p>
</li>
<li><p>PaymentCompletedIntegrationEvent (통합 이벤트) 발행 → Kafka → 배송 서비스 전달</p>
</li>
</ul>
<p><strong>배송 서비스</strong></p>
<ul>
<li>PaymentCompletedIntegrationEvent를 구독하여 배송 시작</li>
</ul>
<h3 id="heading-kirqt7zrjbag7ya17zwpioydtouypo2kuouklcdsmzwg7iks7jqp7zwy64qu6rkd7j286rmmpyoq"><strong>근데 통합 이벤트는 왜 사용하는것일까?</strong></h3>
<p>도메인 이벤트를 소비하는 시점에 비지니스 요구사항에 맞춰 다른 서비스를 호출하면 될텐데 왜 통합 이벤트를 굳이 발행해서 처리하는것일까?</p>
<p>위에서 언급한 EDA가 각광받는 이유에 답이 있다. 메시징을 이용하지않으면 각 서비스들을 아무리 잘 분리시켜놔도 결합도를 느슨하게 만들기 힘들다. 즉, 서비스들을 마이크로하게 가져간 이득이 사라지는셈이다. 오히려 시스템 관리 복잡도와 코스트만 올라가고 MSA의 이점을 보기힘들어진다.</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">placeOrder</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// ... order domain logic</span>

    <span class="hljs-comment">// 주문 생성 후 결제 API 호출</span>
    paymentService.processPayment(orderId, amount)

    <span class="hljs-comment">// 결제 후 배송 API 호출</span>
    shippingService.startShipping(orderId)
}
</code></pre>
<p>통합 이벤트를 메시징 하지않으면 위와 같이 동기이던 비동기 요청이던 각 서비스들이 서로간 강한 결합이 유지된다. 그런데 통합 이벤트를 이용하여 메시징 처리를 하면 서비스들은 브로커를 의존하게 되어 서비스들 간의 결합도를 낮출 수 있고, 브로커의 특성에 따라 다르긴하지만 대게 순차처리, 파티셔닝 등을 통해 동시 처리량도 높게 확보할 수 있어진다.</p>
]]></content:encoded></item><item><title><![CDATA[Command Bus는 반드시 비동기를 지원해야할까?]]></title><description><![CDATA[배경
CQRS(Command Query Responsibility Segregation) Pattern은 명령(Command)과 조회(Query)를 분리하여 각각 독립적으로 최적화할 수 있는 강력한 아키텍처 패턴이다. 이를 통해 시스템의 확장성과 유지보수성을 높일 수 있다.
CQRS에서 명령을 처리하는 과정에서 Command Bus라는 개념이 등장하는데, 많은 자료에서 이를 비동기 방식으로 처리하는 예시를 주로 다루고 있다. 그러나 반드시 비동...]]></description><link>https://jeongkyun-dev.kr/command-bus-have-to-be-asynchronous</link><guid isPermaLink="true">https://jeongkyun-dev.kr/command-bus-have-to-be-asynchronous</guid><category><![CDATA[CommandBus]]></category><category><![CDATA[#CQRS]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 09 Feb 2025 15:25:39 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-67cw6rk9">배경</h2>
<p><strong><em>CQRS(Command Query Responsibility Segregation) Pattern</em></strong>은 명령(Command)과 조회(Query)를 분리하여 각각 독립적으로 최적화할 수 있는 강력한 아키텍처 패턴이다. 이를 통해 시스템의 확장성과 유지보수성을 높일 수 있다.</p>
<p>CQRS에서 명령을 처리하는 과정에서 <code>Command Bus</code>라는 개념이 등장하는데, 많은 자료에서 이를 비동기 방식으로 처리하는 예시를 주로 다루고 있다. 그러나 반드시 비동기 처리가 요구되는 것은 아니며, 오히려 동기 방식을 기본으로 시작하는 것이 더 적절할 수 있다.</p>
<p>이번 글에서는 Command Bus의 동기 및 비동기 방식에 대한 선택 기준을 알아보고, 어떤 경우에 비동기 방식을 도입하는 것이 적절한지에 대해 다뤄보려한다.</p>
<h2 id="heading-67cy65oc7iuciou5houpmeq4soulvcdsp4dsm5dtlbtslbztlzjriptqsia">반드시 비동기를 지원해야하는가?</h2>
<p><strong>CQRS 패턴에서 Command Bus가 반드시 비동기로 동작해야 하는 것은 아니다.</strong></p>
<p>실제로 많은 시스템에서 Command Bus는 기본적으로 동기적으로 동작하며, 필요에 따라 비동기 방식으로 전환하는 것이 더 바람직하다.</p>
<p><code>반 버논(Vaughn Vernon)의 Implementing Domain-Driven Design</code> 책에서는 다음과 같이 언급된다.</p>
<blockquote>
<p>"커맨드를 비동기식 메시지로 보내며, 이는 전용 스타일로 설계된 핸들러로 전달된다. 이는 각 커맨드 처리기 컴포넌트가 특정 타입의 메시지를 받도록 할 뿐만 아니라, 커맨드 프로세싱 부하를 처리할 수 있도록 주어진 타입의 처리기를 추가할 수 있다. 그러나 이 접근법에는 좀 더 복잡한 설계가 필요하기 때문에 이를 기본으로 사용하면 안 된다. 일단은 동기식 커맨드 처리기로 선택해 시작하자. 확장성(Scalability)의 요구가 있을 때에만 비동기식으로 변경하자."</p>
</blockquote>
<p>즉, <strong>비동기 처리는 기본값이 아니라 필요할 때 적용하는 것이 적절하다.</strong></p>
<h2 id="heading-6re466ch64uk66m0ioq4souzuoyggeycvouhncdrj5nqulag67cp7iud7j20ioyggeygio2vncdsnbtsnkdripqg66y07jeh7j286rmmpw">그렇다면 기본적으로 동기 방식이 적절한 이유는 무엇일까?</h2>
<h3 id="heading-kioq7j286rsa7isxioycooyngoqwgcdsmqnsnbttlzjri6qqkio"><strong><em>일관성 유지가 용이하다</em></strong></h3>
<ul>
<li><p>동기적으로 커맨드를 처리하면, 명령 실행 후 즉시 조회 모델이 최신 상태로 유지될 가능성이 높아진다.</p>
</li>
<li><p>이는 사용자가 커맨드 실행 후 변경 사항을 즉시 확인할 수 있음을 의미한다.</p>
<ul>
<li>예를들어, 고객이 쇼핑몰에서 주문하면 주문 정보가 즉시 반영되어 보여주기를 기대한다. 만약 비동기 방식이라면, 메세지 큐에 의해 주문이 나중에 처리될 수 있으므로 즉시 조회했을 때 아직 데이터가 반영되지 않았을 가능성이 존재한다.</li>
</ul>
</li>
</ul>
<h3 id="heading-kioq7yq4656c7j6t7iwyioq0goumroqwgcdri6jsijztlzjri6qqkio"><strong><em>트랜잭션 관리가 단순하다</em></strong></h3>
<ul>
<li><p>동기 방식에서는 하나의 트랜잭션 내에서 명령을 실행하고 필요 시 쉽게 롤백할 수 있다.</p>
<ul>
<li>예를들어, 은행 계좌 이체를 수행할 때 하나의 트랜잭션으로 송금과 입금을 처리하면, 실패 시 쉽게 복구할 수 있다.</li>
</ul>
</li>
<li><p>반면 비동기 시스템에서는 비동기 방식에서는 분산 트랜잭션(Distributed Transaction) 처리가 필요해지는 복잡성이 초래된다.</p>
</li>
</ul>
<h3 id="heading-kioq65su67ke6rmf7j20ioyaqeydto2vmoulpcoqkg"><strong><em>디버깅이 용이하다</em></strong></h3>
<ul>
<li><p>동기 방식에서는 문제가 발생한 시점과 원인을 즉시 파악할 수 있다. 요청을 보낸 후 응답이 즉시 오기 때문에, 에러 발생 시 트레이스를 쉽게 따라갈 수 있다.</p>
</li>
<li><p>반면 비동기 시스템에서는 메시지 지연이나 실패 시 추적이 어려울 수 있다.</p>
</li>
</ul>
<h2 id="heading-6re4658ioywuoygncdruytrj5nqulag67cp7iud7j2eioqzoougpo2vtoyvvo2vooq5jd8">그럼 언제 비동기 방식을 고려해야할까?</h2>
<h3 id="heading-kioq7zmv7j6l7isx7j20ioyaloq1rouqocdrlywqkio"><strong><em>확장성이 요구될 때</em></strong></h3>
<ul>
<li><p>사용자가 많아지면 동기 방식의 커맨드 처리는 서버 부하를 증가시키고, 응답 속도가 느려질 수 있다.</p>
</li>
<li><p>커맨드 처리를 메세징 서비스(e.g. Kafka)를 통해 비동기로 분산하면 수평 확장이 쉬워지고, 부하를 분산할 수 있다.</p>
</li>
</ul>
<h3 id="heading-kioq6rog7isx64ql7j20ioyaloq1rouqmouklcdsi5zsiqtthzzsnbwg65wmkioq"><strong><em>고성능이 요구되는 시스템일 때</em></strong></h3>
<ul>
<li><p>대량의 트래픽을 처리하는 시스템에서는 동기 방식으로 인해 성능 병목(Bottleneck)이 발생할 수 있다.</p>
<ul>
<li>예를들어, 실시간 데이터 스트리밍이 필요한 시스템에서는 빠른 응답을 위해 비동기 방식이 적절할 수 있다. 대규모 로그 처리 시스테에서 Kafka와 같은 서비스를 이용하여 이벤트를 비동기로 수집하는 사례까 대표적이다.</li>
</ul>
</li>
</ul>
<h3 id="heading-kirsponqsihsoihsnbgg7j2r64u17j20io2vhoyalo2vmoyngcdslyrsnyag6rk97jqwkio"><strong>즉각적인 응답이 필요하지 않은 경우</strong></h3>
<ul>
<li>일부 도메인에서는 명령 실행 후 즉시 결과를 제공할 필요없는 경우가 있다.</li>
</ul>
<p>위 케이스 외에도 더 다양한 트레이드 오프가 존재할것이라 생각한다. 만약 비동기로 명령을 처리하고 프론트쪽에서는 폴링 기법을 활용하여 처리가 완료될 때 까지 조회를 진행하는 방식을 채택할 수도 있다.</p>
<p>CQRS Pattern는 다양한 비지니스 요구사항을 만족할 수 있는 좋은 기법이라 생각하지만, 이번글에서 다룬 명령 처리 부분 외에도 읽기 모델(Read Model)을 가공하는 부분에서 대게 결과적 일관성 방식으로 구현되어 지연(Lag)과 실패에 대한 핸들링이 필요해진다.</p>
]]></content:encoded></item><item><title><![CDATA[당신의 시스템에서 단일장애지점은 어디인가요?]]></title><description><![CDATA[배경
이번 글에서는 필자가 기여하고있는 B2B SaaS 제품(이하 KOS)에서 단일 장애지점을 파악해보려한다. 이슈 지점에 대해 낙관적으로 바라보고 솔루션을 강구하지않으면 정말 ‘문제’가 되기때문에 솔직 담백하게 문제라고 생각되는 부분들을 정리해보고 항목 별로 액션 아이템을 도출해보려한다.
근데, 단일 장애 지점이 뭔가요?
단일 장애 지점(Single Point Of Failure, 이하 SPOF)은 시스템에서 하나의 구성 요소가 실패하면 전체...]]></description><link>https://jeongkyun-dev.kr/where-is-the-single-point-of-failure-in-your-system</link><guid isPermaLink="true">https://jeongkyun-dev.kr/where-is-the-single-point-of-failure-in-your-system</guid><category><![CDATA[SPOF]]></category><category><![CDATA[Single Point of Failure]]></category><category><![CDATA[architect]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Tue, 28 Jan 2025 16:21:04 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-67cw6rk9">배경</h2>
<p>이번 글에서는 필자가 기여하고있는 B2B SaaS 제품(이하 KOS)에서 단일 장애지점을 파악해보려한다. 이슈 지점에 대해 낙관적으로 바라보고 솔루션을 강구하지않으면 정말 ‘문제’가 되기때문에 솔직 담백하게 문제라고 생각되는 부분들을 정리해보고 항목 별로 액션 아이템을 도출해보려한다.</p>
<h2 id="heading-6re8642wlcdri6jsnbwg7j6l7jwgioyngoygkoydtcdrrztqsidsmpq">근데, 단일 장애 지점이 뭔가요?</h2>
<p>단일 장애 지점(Single Point Of Failure, 이하 SPOF)은 시스템에서 하나의 구성 요소가 실패하면 전체 시스템이 중단되는 취약점을 의미한다. 마치 의존도가 높은 다리의 한 기둥이 무너질 경우, 다리가 전체적으로 무너질 위험이 있는것과 같다. 특정 구성 요소가 실패했을 때 대체하거나 복구할 수 없는 구조를 가진 경우 해당 요소가 단일 장애 지점으로 간주된다.</p>
<h2 id="heading-64uo7j28ioyepeyvocdsp4dsojag7iud67oe7zwy6riw">단일 장애 지점 식별하기</h2>
<p>KOS 서비스는 Kubernetes 환경에서 서비스들이 오케스트레이션되는 구조이다. 즉, 스케일링과 회복성이 뛰어나다. 어플리케이션이 모종의 이유로 다운되더라도, ReplicaSet 설정에 따라 새로운 파드(pod)가 자동으로 생성된다.</p>
<blockquote>
<h3 id="heading-6re4658io2yhoyercdslrttlizrpqzsvidsnbtshzjsnzgg7lwc64yaioyymoumroufisdrsi8g6rca7jqpio2xioyaqey5mouklcdslrtripdsojxrj4tsnbjqsia">그럼 현재 어플리케이션의 최대 처리량 및 가용 허용치는 어느정도인가?</h3>
</blockquote>
<p>현재 운영되고있는 서비스들의 정량적 가용 지표를 파악하고 있지않았다. 특정 중요 서비스들은 성능 테스트 도구가 아닌 매뉴얼로 스크립트를 만들어 레이턴시 측정은 진행된적이 있으나 서비스별로 부하 테스트를 통해 최대 처리량에 대한 지표측정이 필요하다.</p>
<h4 id="heading-7jmcioq3uoupmeyvicdsp4dtkzwg7lih7kcv7j2eioyvio2wioucmd8">왜 그동안 지표 측정을 안했나?</h4>
<p>대규모 트래픽에 예민한 시스템이였다면 필수적으로 진행했겠으나, B2B 제품 특성상 스파크성 트래픽이 발생하지않아 데일리로 모니터링 도구를 통해 어플리케이션 CPU, Memory 지표등을 파악하여 스케일링을 하고있었다. 그러나 곧 정말 큰 규모의 고객이 두달내 입점할 예정이기에 이젠 코어 서비스들의 최대 처리량 파악을 통해 가시성을 올리는 액션 아이템이 필요할것같다.</p>
<p>[] 코어 서비스를 우선으로 스트레스 테스트 진행하기</p>
<p>[] 테스트를 통해 처리량 지표 파악하기</p>
<blockquote>
<h3 id="heading-642w7j207ysw67kg7j207iqk64quioq0noywruucmd8">데이터베이스는 괜찮나?</h3>
</blockquote>
<p>현재 KOS는 MongoDB를 메인 데이터베이스로 사용하고있다.</p>
<p>MongoDB는 고가용성과 데이터 무결성을 보장하기위해 최소 3개의 노드(Primary-Secondary)를 구성해야한다. 따라서, Primary 노드가 죽더라도 Secondary 노드가 Primary로 승격되면서 페일오버가 진행한다.</p>
<h4 id="heading-spof">그럼 데이터베이스는 SPOF가 아닌건가?</h4>
<p>데이터베이스(Shared Database)는 단일 장애지점이 아닐 수 없다. (Shread Database 환경이라면 더더욱) 페일오버를 통해 회복탄력성이 빠르게 보장이 되는것뿐이지 승격되는 과정에서 데이터 유실이 발생할 수 있고, 특정 타이밍에 따라 복구가 불가능한 상황이 발생할수도 있다. 따라서 잠재적인 단일 장애 지점으로 볼 수 있다.</p>
<p>그러나 현재 초기단계의 제품 사이즈에서는 데이터베이스의 물리적 확장 방식인 샤딩이나 노드 수를 확장하는 전략을 취하기보다는 데이터 접근에 대한 쿼리에 대한 Observability에 더 집중하는게 맞다고 생각된다.</p>
<p>정상적인 인덱싱 작업이 되어있지않은 무거운 쿼리가 발생한다던가, 불필요한 쿼리(e.g. n+1)가 발생하고있다던가 등의 Query Observability를 확보하여 이를 어플리케이션에서 개선시키는 과정이 더 중요할것같다.</p>
<p>또한, 현재 Write, Query가 모두 Primary에서 이루어지고있는데, 강한 일관성이 필요하지않는 부분에 대해서 Query는 Secondary를 바라보도록 하는 Read Concern 설정을 통해 Query I/O를 분산하는 작업도 필요한 상황이다.</p>
<p>[] Read Concern을 통해 DB I/O 분산시키기</p>
<p>[] Database Observability 확보</p>
<blockquote>
<h3 id="heading-api-gateway">API Gateway는 괜찮나?</h3>
</blockquote>
<p>KOS의 Gateway는 Spring Webflux로 이루어져있다. API Gateway Pattern을 기반으로 모든 도메인 서비스들의 프록시 역할과 인가 역할도 진행하며 BFF에 대한 책임도 갖고있다.</p>
<p>Gateway는 번역 그대로 ‘통로, 연결지점’의 역할을 지니고있기에 이 서비스가 다운되면 전면 장애는 피할 수 없는 사실이다.</p>
<h4 id="heading-spof-1">그럼 어떻게 SPOF를 방지할 수 있을까?</h4>
<p>Observability 도구를 통해 주기적인 점검과 지표에 맞는 스케일링을 통해 사전 예방을 하고있다. 또한 Kubernetes의 Liveness/ Readness Probe 설정을 통해 장애 감지 및 자동 복구가 진행된다.</p>
<p>그러나 다운스트림 서비스들로 라우팅할 때 특정 서비스가 정상적인 응답을 주지못하고 지연이 걸리거나 타임아웃등이 발생하면 Gateway Service에 또한 작업 스레드가 밀려 장애가 전파될 수 있다.</p>
<p>위 상황을 방지하기위해 Rate Limitting과 Circuit Breaker에 대한 Handling이 필요한 상황이라 생각한다. Circuit Breaker를 통해 다운 스트림 장애 시 원격 서비스 대상의 회복 시간을 제공하고, 요청을 차단하여 폴백을 통해 빠른 피드백을 전달하여 스레드를 오래 잡지않도록 할 수 있다.</p>
<p>또한, 어뷰저가 악의성을 갖고 과도한 요청(e.g. DoS)을 통해 Gateway Service를 과부하 상태로 만들 수 있다. 이는 Rate Limitting을 통해 방지할 수 있어야하겠다.</p>
<p>[] Circuit Breaker 구성</p>
<p>[] Rate Limitting 구성</p>
<h3 id="heading-66ei7lmy66mw">마치며</h3>
<p>기여하고있는 시스템에서의 단일 장애 지점을 식별하는 작업은 매우 중요하다 생각한다.</p>
<p>이번 글에서 나온 액션 아이템들은 (처절한..) 우선순위에 따라 하나씩 해결나갈 예정이고, 그 과정에서 공유할만한 트러블 슈팅이 생긴다면 꼭 남겨보도록 하겠다.</p>
]]></content:encoded></item><item><title><![CDATA[신사업에서 Event Sourcing Pattern을 왜 사용했나요?]]></title><description><![CDATA[서론
소프트웨어 개발에서는 데이터 저장 방식이 시스템의 확장성과 유지보수성에 큰 영향을 미친다. 특히, 변화가 빈번하고 복잡한 비즈니스 요구사항이 있는 환경에서는 단순한 CRUD 기반의 상태 저장 방식으로는 한계에 봉착하곤한다.
필자가 현재 기여하고있는 신사업 B2B SaaS 제품(이하 KOS)도 마찬가지였다. 병원 내에서는 고객의 내원부터 귀가까지 다양한 이벤트가 발생하며, 이 과정에서 실시간으로 예약 변경, 시술 추가, 환불 처리 등 여러 ...]]></description><link>https://jeongkyun-dev.kr/why-using-event-sourcing-pattern</link><guid isPermaLink="true">https://jeongkyun-dev.kr/why-using-event-sourcing-pattern</guid><category><![CDATA[Event Sourcing]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[DDD]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sun, 05 Jan 2025 16:29:05 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-7isc66gg">서론</h2>
<p>소프트웨어 개발에서는 데이터 저장 방식이 시스템의 확장성과 유지보수성에 큰 영향을 미친다. 특히, 변화가 빈번하고 복잡한 비즈니스 요구사항이 있는 환경에서는 단순한 CRUD 기반의 상태 저장 방식으로는 한계에 봉착하곤한다.</p>
<p>필자가 현재 기여하고있는 신사업 B2B SaaS 제품(이하 KOS)도 마찬가지였다. 병원 내에서는 고객의 내원부터 귀가까지 다양한 이벤트가 발생하며, 이 과정에서 실시간으로 예약 변경, 시술 추가, 환불 처리 등 여러 상황이 동시다발적으로 이루어진다. 이러한 복잡한 프로세스를 모두 데이터로 기록하고, 추적 가능한 방식으로 관리하는 것은 병원 운영의 효율성을 높이고 직원들의 업무 평가에 활용할 수 있는 중요한 과제였다.</p>
<blockquote>
<h3 id="heading-7j2067kk7yq4ioygjoylst8">이벤트 소싱?</h3>
</blockquote>
<p><strong>모든 상태 변화를 이벤트(Event)라는 형태로 기록하고, 이를 기반으로 시스템의 현재 상태를 재화(Rehydration)시키는 방식이다. 단순한 CRUD 방식의 상태(State) 저장 방식과 달리, 과거의 모든 이벤트를 영속적으로 유지하며 감사 로그(Audit Log)와 데이터 추적을 용이하게 만들어 준다.</strong></p>
<h2 id="heading-7zie7iukioyeuoqzhoydmcdrrljsojzripqg7iod6rcb67o064ukiounlcdrs7xsnqhtlojri6q">현실 세계의 문제는 생각보다 더 복잡했다</h2>
<p>미용의료 병원에서 고객의 내원 ~ 귀가 프로세스를 바라보면, <code>병원 내원 -&gt; 데스크 접수 -&gt; 시술 -&gt; 귀가</code> 정도로 보일 수 있겠지만, 이 과정을 좀 더 톺아보면 정말 많은 과정들이 숨어있다. 하나의 예시로는 시술을 받는 과정에서 업셀링을 통해 새로운 시술을 실시간으로 추가하여 받을 수 있고, 받고 나와서 서비스가 불만족스러워 환불 처리를 밟을 수도 있다. 또는 피부 케어 서비스가 들어가 추가 시술들이 언제든지 추가될 수 있다.</p>
<p>무엇보다 이 복잡한 모든 과정을 데이터로 영속하여 사실에 기반한 데이터로 통계를 내고, 해당 통계를 기반하여 병원의 서비스적인 품질과 다른 제품에선 제공할 수 없는 여러 인사이트를 혁신적으로 제공하는것이 우리 팀이 그린 KOS 제품의 최종적인 모습이였다.</p>
<h2 id="heading-64e7j6f7zwy7jesioygno2sio2mgoydtcdslrvsnyag6rca7lmy64qupw">도입하여 제품팀이 얻은 가치는?</h2>
<p>신사업에서 많은 레퍼런스가 없는 이벤트 소싱을 도입하는것이 챌린징적인 요소이긴했지만, 도입 이후 결과적으론, 사실 기반의 정량적 데이터로 효과적인 결과를 거둘 수 있었다. (특히, Product Owner와 Product Designer가 제품의 새로운 문제 영역을 식별하고 정책을 마련하는데있어 가장 크게 도움이 되었던것같다)</p>
<h2 id="heading-64e7j6f7zwy7jesioqzooqwneydtcdslrvsnyag6rca7lmy64qupw">도입하여 고객이 얻은 가치는?</h2>
<p><img src="https://blog.kakaocdn.net/dn/bmUe9t/btsG5FttDT6/h0snIMuNZ8xPdlKc0gpUaK/img.gif" alt="이벤트 소싱을 사용한 이유, 그리고 적용하면서 겪은 문제들 (feat. CQRS) - 목차 - 적용하면서 겪은 문제들" /></p>
<p>병원의 담당 부서마다 얻은 가치가 다르긴하지만 가장 반응이 좋았던것은 병원 관리자가 누가, 왜, 언제, 어떤 작업을 수행한것인지 파악하는게 가장 큰 골칫덩어리였는데 하나의 Stream에서 발생한 모든 사건(Event)를 기록하고있기때문에 이는 자연스럽게 해결할 수 있었다. 예를들어 다음과 같이 말이다.</p>
<p><strong>e.g. 어딧로그</strong></p>
<blockquote>
<p>병원 CS 담당자: 1.6일 내원한 안정균 고객님의 예약 정보를 누가 변경한것인지 알 수 있을까요? 가능하면, 변경한 시각도 알고싶어요 !</p>
</blockquote>
<h3 id="heading-kos-view">KOS 제품의 예약 현황 View</h3>
<p><img src="https://blog.kakaocdn.net/dn/cz38MJ/btsG3zgVNCB/2WMQtwfklqJQHufKd6wmM1/img.png" alt="이벤트 소싱을 사용한 이유, 그리고 적용하면서 겪은 문제들 (feat. CQRS) - 목차 - 적용하면서 겪은 문제들" /></p>
<blockquote>
<p>KOS: 예약 현황 or 예약 상세에서 어딧 로그 기능을 제공하고있습니다. (위 사진 참고)</p>
<p>병원 담당자: 혹시 그렇다면 예약별로 ‘어느’ 시술을 받는데 ‘얼마나’ 시간이 소요되었고, ‘언제’ 귀가했는지를 알 수있을까요?</p>
<p>KOS: 네, 예약 통계에서 요청 주신 데이터 모두 확인가능합니다.</p>
<p>병원 담당자: 와우!</p>
</blockquote>
<p>위 과정이 가능하게된것은 병원에서 수행된 모든 이벤트가 기록되어있음으로, 어떤 상품으로 시술을 받았고, 언제부터 시술을 받았고, 종료되었는지, 또한 귀가는 언제했는지 등 모든것을 제공할 수 있었다.</p>
<h2 id="heading-event-sourcing">그럼 Event Sourcing를 사용안했다면 해결못하는 문제였나?</h2>
<p>(당연히) 아니다. 문제를 식별하고 이를 해결하는 과정에는 팀의 환경, 구성원들의 역량, 제품 특성 등 모든것을 고려하여 다양한 솔루션을 내세울 수 있다. 이 과정에서 KOS 제품이 Event Sourcing을 진행하기에 적합한 환경이였을뿐이다.</p>
<p>적합했던 이유라하면, KOS 제품은 도메인 주도 설계(Domain-Driven Design) 방법론에서 여러 전략, 개념들을 이용하여 제품 설계를 초기부터 진행했었기에, 명확한 명령(Commands), 사건(Events), 집합(Aggregates)가 정의를 해놓은 상태였고 필요 시 이벤트 스토밍도 진행하여 필요한 재료들은 세팅이 되어있었기 때문인것같다.</p>
<h2 id="heading-7kcb7jqp7zwy66m07isciouwnoydne2vncdrrljsojzripq">적용하면서 발생한 문제는?</h2>
<h3 id="heading-rehydration"><em>이벤트 재화(Rehydration)에 따른 성능 비용</em></h3>
<p>이벤트 소싱은 모든 상태(State)를 이벤트의 흐름(Event Stream)으로부터 재구성해야 한다. 그러나 다수의 이벤트가 포함된 스트림을 조회할 때, 각 이벤트를 순차적으로 재생하는 작업이 성능 병목을 초래했다. 특히 예약 현황을 조회하거나 변경 이력을 확인하는 기능에서 조회 시간이 급격히 늘어나는 문제가 발생했다.</p>
<p>e.g. 예약현황 뷰</p>
<p><img src="https://blog.kakaocdn.net/dn/cz38MJ/btsG3zgVNCB/2WMQtwfklqJQHufKd6wmM1/img.png" alt="이벤트 소싱을 사용한 이유, 그리고 적용하면서 겪은 문제들 (feat. CQRS) - 목차 - 적용하면서 겪은 문제들" /></p>
<p>위와 같이 예약현황이 있고, 만약 각 내원객의 예약마다 100개의 이벤트가 존재한다면, 예약 수 x 이벤트의 재화 연산을 거쳐야한다. 단순하게 예약이 100개 있고, 각 예약마다 100번의 이벤트가 발생했었다면 최소한 예약 현황을 제공하기위해 10,000번의 연산이 애플리케이션에서 이루어져야한다는 말이다. (또한 Stream마다 Query I/O 비용도 추가로 발생한다)</p>
<p><strong>이러한 문제를 CQRS 패턴을 도입하여, 재화 연산 비용에 대한 문제를 해결했다.</strong></p>
<p>명령과 조회를 분리하여 이벤트 소스 데이터는 Write Model로 유지하고, 조회는 별도의 Read Model을 통해 처리하는 방식으로 개선하였다. 이때 Read Model은 필요한 데이터를 이벤트로부터 미리 연산하여 물리적 테이블로 저장해 두는 방식으로 성능 최적화를 진행했다.</p>
<p>CQRS 패턴 도입 외 또 다른 솔루션으론 특정 시점의 상태를 스냅샷으로 저장하고, 이후 이벤트만 재생시켜 최종 상태를 재구성하는 전략도 있었으나, 실시간으로 여러 Read Model을 구성해야하는 요구사항이 있어 CQRS 패턴을 택하게되었다.</p>
<h3 id="heading-7j2067kk7yq4ioyinoyencdrs7tsnqu">이벤트 순서 보장</h3>
<p>이벤트 기반 아키텍쳐(Event-Driven Architecture)에서도 중요하게 다뤄지는 이벤트의 순서 보장에 대한 내용이다.</p>
<p>이벤트가 시스템에 저장되거나 소비될 때 순서가 보장되지 않으면, 잘못된 상태가 만들어질 수 있다. 예를 들어, “예약 변경됨”이라는 이벤트가 “예약 생성됨“ 이벤트보다 먼저 처리되는 경우 비지니스 로직이 올바르게 동작하지 않을것이다.</p>
<h4 id="heading-7zw06rkw7jwi">해결안</h4>
<p>이러한 문제를 방지하기위해 Message Broker의 파티션 키를 이용하여 순차처리하도록 진행하였다.</p>
<p>Message Broker로는 AWS Kinesis Data Stream을 사용하였고, 순서 보장과 더불어 온디멘드 샤딩을 지원하여 Stream 처리량이 매우 뛰어난 서비스이기에 채택하게 되었다.</p>
<h3 id="heading-7ksr67o1iouployeuoyngcdsspjrpqw">중복 메세지 처리</h3>
<p>메세징 시스템을 구성에 있어 메세지 전달 전략들이 여러가지가 있는데, 가장 쉽게 구성할 수 있는것이 At Least Once 방식이다. 말그대로 최소한 한번 이벤트가 발생할 수 있다는 말이다. 그냥 한번만 생산(Produce)하면 되는것아닌가? 싶을 수 있는데, 이것은 이번 주제와 무관하다 생각하여 자세한 이야기는 생략한다. (<s>이벤트 전달 전략은 조만간 작성해 볼 예정</s>)</p>
<h4 id="heading-7zw06rkw7jwi-1">해결안</h4>
<p>중복 메세지가 소비될 수 있는 부분에 대해선 소비자 워커(Consumer Worker)에서 멱등성 논리를 구성하였다. 모든 소비자에 멱등성 논리가 들어간것은 아니고, 요구사항에 따라 소비자 모델의 고유 ID를 통해 이미 처리된 이벤트는 무시되도록 설계한 방식도 존재한다.</p>
<h3 id="heading-7lwc7kkf7kcbioydvoq0goyeseycvouhncdsnbjtlzwg64z6riw7zmu">최종적 일관성으로 인한 동기화</h3>
<p>이벤트를 생산(Produce)하고 Write Model은 수행을 끝낸다. Read Model의 워커에서 비동기로 이벤트를 소비하여 데이터를 처리한다. 즉, 강한 일관성(strong consistency)을 보장하지못하며, 비동기로 인해 최종적으로 데이터 일관성을 보장한다.</p>
<p>이러한 최종적 일관성에 대해 사용자는 다음과 같은 불편감이 생길 수 있다.</p>
<blockquote>
<p>병원 담당자: 예약 생성 버튼 클릭 → 성공 ! → 응? 성공인데 왜 화면에 생성이 안되는거지? (버그인가?)</p>
</blockquote>
<p>시스템적으로 보았을 땐, 당연한 이야기처럼 보이지만 사용자 입장에선 황당한 일이고 버그로 인지하기 딱 좋은 케이스이다.</p>
<h4 id="heading-7zw06rkw7jwi-2">해결안</h4>
<p>위와 같은 이슈를 해결하기 위해 WebSocket을 지원하는 이벤트 워커를 구현하여, 생산된 이벤트를 워커에서 소비하면 WebSocket에 연결되어있는 모든 웹 브라우저에게 변경된 사항을 실시간으로 전달하도록했다.</p>
<h3 id="heading-64z7iuc7isxiousuoygna">동시성 문제</h3>
<p>여러 사용자가 동시에 하나의 스트림(e.g. 예약 데이터)에 수정을 가하면 갱신 손실 문제가 발생할 수 있다. 제품 특성 상 병원 데스크에서 동일한 예약 정보에 동시에 변경을 가하는일이 간혹 일어난다. 이런 상황에서 동시성 문제가 제대로 관리되지않으면 고객은 예상치 못한 혼란스러움을 겪을 수 있다.</p>
<p>e.g. 동시성 상황</p>
<blockquote>
<p><strong>[동시 요청]</strong><br />(데스크 직원 A) a, b, c 시술 테스크 저장</p>
<p>(데스크 직원 B) c, d, e 시술 테스크 저장</p>
</blockquote>
<p>시술 테스크의 Sequence Key는 애플리케이션에서 1씩 증가하는 논리를 갖고 있다. 하지만 동시 요청이 발생하면, 동일한 Sequence Key를 가진 테스크가 저장될 가능성이 있다. 이는 데이터 충돌을 초래하며, 결과적으로 잘못된 시술 테스크 정보가 저장될 위험이 있다.</p>
<h4 id="heading-7zw06rkw7jwi-3">해결안</h4>
<p>동시성 문제를 제어하는 방법은 요구사항마다 다르긴하지만, KOS에서 이벤트 소싱을 다루고있는 영역안에선 Event Schema에 이벤트가 쌓일 때마다 1씩 증가하는 version이라는 속성을 유니크 색인으로 정의하여 낙관적 잠금(<a target="_blank" href="https://en.wikipedia.org/wiki/Optimistic_concurrency_control">Optimistic Lokcing</a>)을 통해 제어하고있다. 이벤트 소싱에선 이벤트가 쌓인 순서가 매우 중요한 요소이기때문에 version이 꼭 필요하다. 그런데 만약 제품 특성 상 동시성 문제가 빈번하게 일어나며 이로인해 고객의 서비스 품질이 저하된다 생각하면 다른 잠금 전략들을 고려하였을것이다.</p>
<h2 id="heading-66ei7lmy66mw">마치며</h2>
<p>이벤트 소싱을 신사업 B2B SaaS 제품에 적용하면서 얻은 가치와 직면했던 문제들을 정리해보았다. 이벤트 소싱은 복잡한 도메인 문제를 해결하고, 과거 데이터를 기반으로 명확한 인사이트를 제공하는 데 강력한 도구가 될 수 있었다.</p>
<p><strong>그러나 이벤트 소싱은 제시한 모든 문제를 해결하는 은탄환이 절대 아니다.</strong></p>
<p>결국, 기술의 선택은 팀 또는 개인의 환경과 목표에 따라 달라진다 생각한다. 이벤트 소싱이 적합한 상황에서는 강력한 도구가 될 수 있지만, 단순한 상태 저장이나 CRUD 중심의 애플리케이션에서는 오히려 과도한 설계 복잡성을 초래할 수 있다.</p>
]]></content:encoded></item><item><title><![CDATA[EDA 환경에서 Event Payload는 어떻게 설계하는게 좋을까?]]></title><description><![CDATA[서론

최근 회사 동료분과 Database Sharding 관련해서 실시간 이벤트 동기화 방식에 대해 이야기를 나누다가 Thin, Fat Payload 방식에 대해 의견을 나누는 시간이 있었는데, 이 과정에서 두 전략 모두 명확한 트레이드 오프를 갖고있다는 것을 다시 한번 느꼈고, 휘발되기전에 내용들을 정리해보려한다.
Thin Payload (aka. Event Notification, Zero Payload)

위 예시로 그려본 도식처럼 Th...]]></description><link>https://jeongkyun-dev.kr/how-to-design-event-payloads</link><guid isPermaLink="true">https://jeongkyun-dev.kr/how-to-design-event-payloads</guid><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[Microservices]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Wed, 01 Jan 2025 15:19:19 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-7isc66gg">서론</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735734966995/cec66246-2844-4087-b9d5-97ee3f348fad.png" alt class="image--center mx-auto" /></p>
<p>최근 회사 동료분과 Database Sharding 관련해서 실시간 이벤트 동기화 방식에 대해 이야기를 나누다가 Thin, Fat Payload 방식에 대해 의견을 나누는 시간이 있었는데, 이 과정에서 두 전략 모두 명확한 트레이드 오프를 갖고있다는 것을 다시 한번 느꼈고, 휘발되기전에 내용들을 정리해보려한다.</p>
<h2 id="heading-thin-payload-aka-event-notification-zero-payload">Thin Payload (aka. Event Notification, Zero Payload)</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735748571723/9950859f-d02e-4fd2-9c9d-fbc2475c852a.png" alt class="image--center mx-auto" /></p>
<p>위 예시로 그려본 도식처럼 Thin Payload는 이벤트가 발생했다는 사실만을 알리는 방식으로 최소한의 정보만을 포함한다. 주로 이벤트 타입과 관련된 식별자만을 포함하며, 구체적인 데이터(속성)는 포함하지않는다.</p>
<p>따라서, Thin Payload는 소비자가 필요한 데이터를 이벤트 원천 소스로부터 조회를 통해 필요한 값들을 획득하는 특징이 있다.</p>
<h3 id="heading-7j6l7kcq">장점</h3>
<ul>
<li><p>상태의 식별값 외 속성값을 포함하고있지않아 메세지 사이즈가 작다.</p>
<ul>
<li>따라서, Network 대역폭과 Storage 사용량이 적다.</li>
</ul>
</li>
<li><p>상태 모델이 확장 되더라도 소비자의 이벤트 워커에 부작용(Side Effect)이 발생하지않는다.</p>
</li>
</ul>
<h3 id="heading-64uo7kcq">단점</h3>
<ul>
<li><p>이벤트 워커에서 수신할 때마다, 수신자가 생산자 API를 조회하는 Network I/O가 발생한다.</p>
<ul>
<li>따라서, 이벤트 원천 서비스의 가용성과 성능에 강한 의존성이 생긴다. (생산자 API 장애 발생 시 수신자 이벤트 워커에도 장애가 전파된다)</li>
</ul>
</li>
</ul>
<h2 id="heading-fat-payload-aka-event-carried-state-transfer">Fat Payload (aka. Event Carried State Transfer)</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735743690229/7ab41472-2f83-4512-8375-254b27e1f020.png" alt="Fat Payload Diagram" class="image--center mx-auto" /></p>
<p>위 예시로 그려본 도식처럼 Fat Payload는 이벤트와 관련된 상태 정보를 포함한 방식이다. 소비자가 이벤트와 관련된 모든 필요한 데이터를 페이로드에 넣어 별도로 생산자 API를 조회하지않고도 처리할 수 있도록 설계하는것이 특징이다.</p>
<h3 id="heading-7j6l7kcq-1">장점</h3>
<ul>
<li><p>페이로드에 상태의 속성 값들이 모두 들어있어, 수신자가 생산자 API 조회 비용없이 이벤트 워커의 작업을 수행할 수 있다.</p>
<ul>
<li>따라서, 소비자가 생산자에 대한 물리적 의존성을 느슨하게 만들 수 있다.</li>
</ul>
</li>
</ul>
<h3 id="heading-64uo7kcq-1">단점</h3>
<ul>
<li><p>상태의 속성 값들이 모두 들어있어, 메세지 사이즈가 크다.</p>
<ul>
<li>따라서, Network 대역폭과 Storage 사용량이 크다.</li>
</ul>
</li>
<li><p>모델의 확장 또는 변경에 따라 생산자와 소비자 모두 부작용(Side-Effect)이 발생한다.</p>
<ul>
<li><p>이벤트 생산 시 상태의 확장된 부분을 이벤트 페이로드에 반영해야한다.</p>
</li>
<li><p>만약 모든 속성이 표기 되어있지 않은 Fat이라면, 소비자가 생산자에게 이벤트 확장을 요구하는 논의가 필요해진다.</p>
</li>
</ul>
</li>
<li><p>이벤트 생산 시점의 데이터를 신뢰하고 사용하기때문에, 소비자 워커 작업 시 최신 상태의 데이터가 아닐 수 있다. (일관성 이슈)</p>
</li>
</ul>
<h2 id="heading-7isg7yodioq4soykgoydtcdtla3sg4eg66qf7zmv7zwgioyimcdsnojriptqsbtqsia">선택 기준이 항상 명확할 수 있는건가?</h2>
<p>아니다. 뻔한 얘기일 수 있지만, 당연하게 시스템 요구사항에 따라 다르다.</p>
<p>하지만.. 필자는 대게 일반적인 상황이라면 Fat Payload 방식을 우선 고민하는편인것같다.</p>
<p>이유는 필자에겐 꽤나 결정적이라고 생각하는데, Thin Payload 방식은 Fat Payload 방식을 제공할 수 없지만, Fat Payload는 Thin Payload 방식을 제공할 수 있다. 이를 공식적인 포함 관계로 표현하기는 조금 위험해보이지만, Fat에도 식별값이 들어있기때문에 기능을 지원하는 영역에선 포함관계라고 생각된다.</p>
<p>그리고 무엇보다, Thin Payload 방식은 수신 시 생산자 API를 콜백하는 방식으로 강하게 의존하는 형태이기때문에 API 지연이 걸릴 수 있는 시간적 결함문제가 존재한다. (Fat Payload도 필요하다면 생산자에게 API를 콜백할 수 있다)</p>
<h3 id="heading-fat-payload">그럼 Fat Payload가 적합하지않은 상황은 어떤것들이 있을까?</h3>
<p>가장 단순하게는 <strong>상태의 모든 속성들이 필요하지않은 경우</strong>이다.</p>
<p>제어가능한 서비스에서 소비자가 필요로하는 데이터가 모든 속성이 아니라면, 굳이 네트워크 대역폭을 오염시킬 필요가 없다. 앞에 “제어가능한”을 붙인 이유는 EDA 특성 상 생산자의 입장에서 소비자들의 컨텍스트는 알 필요가 없지만, 엔지니어가 이를 필요로 하는 소비자들에 대해 화이트 박스인 상황이고, 확장의 여지가 없거나 확장이 되더라도 제어가 가능하다면 <a target="_blank" href="https://martinfowler.com/eaaCatalog/dataTransferObject.html">DTO(Data Transfer Object)</a> 패턴을 통해 프로젝션하여 필요한 데이터만 조회하는것이 더 나은 판단일 수 있다.</p>
<p>두번째로는 <strong>대역폭이 낮게 설정된 인프라를 사용하고있는 경우</strong>라면 Fat Payload이 제한될 수 있다. Fat 방식은 상태의 특징에 따라 이벤트 사이즈가 기하급수적으로 증가할 가능성이 있기때문에 정말 무거워질 수 있다. 예를들어, 필자가 만들고있는 시스템에는 <code>TicketGroup</code> 이라는 도메인 엔터티가 존재하는데, 내부에 <code>Tickets</code> 이라는 배열 속성이 있다. 이는 1:N 관계로 <code>Ticket</code>은 상황에 따라 100개가 넘어갈수도 있다. 만약 AWS SQS를 사용하고있다면, 현재 <a target="_blank" href="https://docs.aws.amazon.com/ko_kr/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html">SQS의 Size Max</a>가 262,144바이트(256KiB)이기때문에 발행에 실패할 수 있다.</p>
<p>세번째로는 <strong>이벤트 워커 작업 시 최신 데이터를 보장해야하는 경우</strong>라면 이벤트 원천 서비스와 직접 상호작용하는 방식인 Thin Payload를 선택하는것이 적합하다. 물론 위에서 언급했듯이 Fat Payload도 식별값을 들고있기때문에 최신 데이터를 조회할 수는 있지만, 발행 시점의 상태를 포함하여 이벤트 소비자 간 독립성을 높이는 Fat Payload의 장점이 희석될 수 있다.</p>
<p>일반적으로 소비자 입장에선 모든 데이터가 이벤트에 포함되면 처리하는데 용이하겠지만, 모든 객체 그래프를 포함시키는것은 느슨하게 결합된 Bounded Context에서 애그리거트 간 외부 엔터티를 얼마나 포함하는지 도메인 모델의 속성들을 이벤트에 모두 노출하는 것과 동일할 수 있기때문에 고민이 꼭 필요하다.</p>
<p>또한, Thin Payload의 가장 큰 장점중에 하나가 내부 도메인 모델링의 확장 또는 변경이 되더라도 소비자들은 식별값으로 조회하기때문에 확장에 대해 사이드 이펙트가 없다. 따라서, 이벤트 생산자를 담당하고있는 작업자와 다른 팀이여도 Fat Payload에 비해 커뮤니케이션이 적게들어가는 이점이 있다. (* 없는것은 아님!)</p>
<h2 id="heading-66ei7lmy66mw">마치며</h2>
<p>이번 포스팅에서는 Thin Payload와 Fat Payload의 개념과 각 트레이드 오프에 대해서 알아보았다. 결국 이 두가지 전략의 선택은 시스템 요구사항에 따라 달라질 수 밖에 없다. 이 두 전략을 공부하면서 느낀 한가지 분명한 점은 이벤트 설계는 단순한 데이터 전달을 넘어 시스템 아키텍쳐링의 설계 철학까지 고려해야한다는 점이였다.</p>
<p>필자가 기여하고있는 제품에선 많은 서비스에서 이벤트들을 소싱하고있는데, 초반엔 시스템 특성, 요구사항을 우선 고려하기보단 큰 고민 없이 이벤트를 설계하면서 여러번 넘어진 경험이 있어 소개해보고싶었으며, 무엇보다 가장 중요한것은 완벽한 설계가 아닌, 팀과 시스템에 가장 적합한 설계를 찾아가는 과정인것같다.</p>
]]></content:encoded></item><item><title><![CDATA[테스트 대역을 어떻게 활용하고있나요?]]></title><description><![CDATA[시작하며
필자는 현재 B2B SaaS 제품(이하 KOS)을 만들고있다. 제품을 만들면서 테스트에선 분명 성공했지만 운영에서 장애를 발생시킨 경험을 하고 난 이후 테스트 대역을 어떻게 바라보고 사용하는지에 대해 소개해보려한다.
테스트 대역이 습관화 되는것이 해롭다는것은 많은 개발자들이 인지하고있는 사실이다. 그러나 안타깝게도 테스트를 작성하다보면 테스트 대역이 필요한 순간은 있기마련이다. 이에 대해 필자는 어떤 기준을 갖고 테스트 대역을 활용하고...]]></description><link>https://jeongkyun-dev.kr/how-to-use-test-doubles</link><guid isPermaLink="true">https://jeongkyun-dev.kr/how-to-use-test-doubles</guid><category><![CDATA[test doubles]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Fri, 27 Dec 2024 09:28:46 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-7iuc7j6r7zwy66mw">시작하며</h2>
<p>필자는 현재 B2B SaaS 제품(이하 KOS)을 만들고있다. 제품을 만들면서 테스트에선 분명 성공했지만 운영에서 장애를 발생시킨 경험을 하고 난 이후 테스트 대역을 어떻게 바라보고 사용하는지에 대해 소개해보려한다.</p>
<p>테스트 대역이 습관화 되는것이 해롭다는것은 많은 개발자들이 인지하고있는 사실이다. 그러나 안타깝게도 테스트를 작성하다보면 테스트 대역이 필요한 순간은 있기마련이다. 이에 대해 필자는 어떤 기준을 갖고 테스트 대역을 활용하고있는지에 대해 소개한다.</p>
<h2 id="heading-67cw6rk9">배경</h2>
<p>KOS의 아키텍처는 Mircroservices 환경으로 이루어져있고, 여러 컴포넌트 간 동기 방식의 HTTP 통신 또는 <a target="_blank" href="https://jeongkyun.hashnode.dev/what-does-message-mean-in-messaging-services">메세지</a>를 발행하여 Message Broker 등을 활용하는 여러 통신 방식이 존재한다. 그리고 메인 데이터베이스로는 MongoDB를 사용하고있다.</p>
<p>시스템에서 복잡한 비지니스 요구사항을 만족하기위해서 하나의 명령(Command)이 실행 됐을 때 여러 서비스 컴포넌트 간 데이터 일관성을 위해 다양한 방식의 통신이 발생하고 이를 제어하기 위해선 하나의 유즈케이스에 많은 의존성이 필요해질 때가 있다.</p>
<p>다음과 같이 복잡한 비즈니스 요구사항과 시스템 제약사항을 만족하기위해 다양한 의존 관계가 형성되었을 때, 어떻게 시스템을 설계하여 테스트 대역을 설정하는지에 대한 경험을 KOS 예약 시스템의 예제 코드 기반으로 공유하고자한다.</p>
<blockquote>
<h3 id="heading-kirsi5zsiqtthzwg7jqu6rws7iks7zwtkio"><strong>시스템 요구사항</strong></h3>
<ul>
<li><p>동일한 내원객(Visitor)은 동 시간대 중복 예약을 할 수 없다.</p>
</li>
<li><p>예약 완료 시 해당 내원객에게 예약 확정 SMS 문자가 발송되어야한다.</p>
</li>
<li><p>예약 완료 시 내원객의 스케줄 내역의 읽기 모델이 만들어져야한다 (ref. CQRS)</p>
<ul>
<li>위 조건을 만족하기 위해, KOS에서는 여러 도메인 서비스 에서 변경이 발생한 사건들(Domain Events)을 발행하여 Downstream 서비스 에서 요구사항에 맞추어 처리하고있다.</li>
</ul>
</li>
</ul>
</blockquote>
<h3 id="heading-kirsmijslb0g7jqu6rws7iks7zwt7j2eiounjoyhse2vmoq4scdsnittlzwg7jwe7ykk7ywn7lkyioq1royeseuphcoq"><strong>예약 요구사항을 만족하기 위한 아키텍처 구성도</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735285891987/cbdac78f-d97a-4c15-a758-935c4aa3e919.png" alt class="image--center mx-auto" /></p>
<p>(위 이미지는 요구사항을 이해하기 위한 보충 설명일 뿐이니, 깊게 이해를 필요로하지않는다.)</p>
<p>스케줄 B.C 에서 예약 성공 후 발행되는 <code>Reserved</code> 도메인 이벤트를 Notification B.C 와 스케줄의 리드모델 이벤트 처리기 등에서 이벤트를 소비하여 요구사항을 만족하는 과정을 추상적으로 도식화해보면 위와 같다.</p>
<p>이제 위 요구사항을 만족하는 예제 코드를 구현해보면 아래와 같이 작성할 수 있다.</p>
<h3 id="heading-as-is"><strong>예약 유즈케이스 구현 (As-Is)</strong></h3>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReserveUseCase</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ScheduleEventStore eventStore;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> VisitorClient visitorClient;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ReservationOverlapValidator reservationOverlapValidator;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ReserveUseCase</span><span class="hljs-params">(
        ScheduleEventStore eventStore,
        VisitorClient visitorClient,
        ReservationOverlapValidator reservationOverlapValidator
    )</span> </span>{
        <span class="hljs-keyword">this</span>.eventStore = eventStore;
        <span class="hljs-keyword">this</span>.visitorClient = visitorClient;
        <span class="hljs-keyword">this</span>.reservationOverlapValidator = reservationOverlapValidator;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">execute</span><span class="hljs-params">(Reserve command)</span> </span>{
        Visitor visitor = visitorClient.read(command.getVisitorId());

        <span class="hljs-keyword">if</span> (
            !reservationOverlapValidator.validate(
                visitor.getId(),
            command.getStartDateTime(),
            command.getEndDateTime()
        )) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(<span class="hljs-string">"overlapped reservation"</span>);
        }

        eventStore.collectEvents(
              List.of(<span class="hljs-keyword">new</span> Reserved(...))
        );
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ScheduleEventStore</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> IScheduleEventRepository mongoRepository;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> IMessageBus kinesisClient;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">collectEvents</span><span class="hljs-params">(String streamId, Iterable&lt;Object&gt; events)</span> </span>{
          mongoRepository.save(...)
          kinesisClient.send(...)
          <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;Object&gt; <span class="hljs-title">queryEvents</span><span class="hljs-params">(String streamId)</span> </span>{
          <span class="hljs-keyword">return</span> mongoRepository.findAll(...)
          <span class="hljs-comment">// ...</span>
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VisitorClient</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> WebClient webClient;

    <span class="hljs-function">Visitor <span class="hljs-title">read</span><span class="hljs-params">(String visitorId)</span> </span>{
        <span class="hljs-keyword">return</span> webClient.get(...)
        <span class="hljs-comment">// ...</span>
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReservationOverlapValidator</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> JedisPool redis;

    <span class="hljs-function">Boolean <span class="hljs-title">validate</span><span class="hljs-params">(
        String visitorId,
        OffsetDateTime requestedStartDateTime,
        OffsetDateTime requestedEndDateTime
    )</span> </span>{
          redis.exists(key)
          <span class="hljs-comment">// ...</span>
    }
}
</code></pre>
<blockquote>
<p><strong>VisitorClient</strong></p>
</blockquote>
<p>내원객의 식별값(VisitorId)으로 내원객 정보를 읽어오는 Reader이다</p>
<p>⇒ <strong>Spring Webflux의 <em>WebClient</em></strong>를 이용하여 Visitor Component를 호출한다</p>
<blockquote>
<p><strong>ReservationOverlapValidator</strong></p>
</blockquote>
<p>중복 예약 방지 요구사항을 만족하기 위해 필요한 내원객의 식별값(VisitorId)과 예약 시간을 파라미터로 받는 중복 예약 검증기이다</p>
<p>⇒ 예약 시 <strong><em>Redis</em></strong>에 캐싱하여 내원객 + 예약 시간 기반으로 중복 예약인지 검증한다</p>
<blockquote>
<p><strong>EventStore</strong></p>
</blockquote>
<p>KOS는 Event Sourcing을 사용하고있어 명령이 발생하면 관련된 도메인 이벤트들을 스토어에 적재하고있는데, EventStore는 도메인 이벤트들을 관리하는 스토어이다</p>
<p>⇒ <strong>MongoDB</strong>와 <strong>AWS Kinesis Data Stream</strong>을 이용하여 Message의 Store And Forward 역할을 한다</p>
<h3 id="heading-test-as-is">Test (As-Is)</h3>
<pre><code class="lang-java"><span class="hljs-meta">@SpringBootTest</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> class <span class="hljs-title">ReserveUseCaseTest</span><span class="hljs-params">()</span> </span>{

    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ScheduleEventStore eventStore;

    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> VisitorClient visitorClient;

    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ReservationOverlapValidator reservationOverlapValidator;

      <span class="hljs-meta">@Test</span>
      <span class="hljs-keyword">void</span> `Sut는 내원객은 동 시간대 중복 예약을 할 수 없다`() {
            <span class="hljs-comment">//Arrange</span>
            <span class="hljs-keyword">var</span> sut = <span class="hljs-keyword">new</span> ReserveUseCase(
                eventStore,
                visitorClient,
                reservationOverlapValidator
            );

            <span class="hljs-keyword">var</span> command = <span class="hljs-keyword">new</span> Reserve(...);

            <span class="hljs-comment">//Act &amp; Assert</span>
            assertThrows(RuntimeException.class, () -&gt; sut.execute(command));
      }

  <span class="hljs-comment">//...</span>
}
</code></pre>
<p>현재의 구조에서는 테스트를 실행하기 위해서 Redis, WebClient, AWS Kinesis Data Stream, Mongo 등 많은 의존성이 필요한 상황이다. 즉, 모든 인프라 계층의 자원들에게 강결합이 되어있는 지금은 Docker와 같은 컨테이너 서비스를 이용하여 Redis, MongoDB, LocalStack 등의 컨테이너를 띄워서 테스팅을 진행할 수 밖에없는 상황인것이다.</p>
<p>게다가 Docker를 사용하기 힘든 환경이라면, 개발자들은 로컬 장비에 MongoDB, Redis를 설치받아야하고 AWS와 같은 자원은 테스트를 위한 서비스 계정을 추가로 관리해야할 수 있다.</p>
<h3 id="heading-q"><strong>Q. 그렇다면 테스팅을 할 때 실제 인프라 자원들을 실행시키는것이 좋지않은것인가?</strong></h3>
<p>아니다. (<strong>강력</strong>) 몇가지 필요 조건에 부합하기만 한다면 실제 인프라 자원과 객체를 사용하는것이 우선되어야한다고 생각한다.</p>
<p><strong>필자가 고려하는 테스트 대역을 사용해야하는 조건은 크게 아래와 같다.</strong></p>
<ul>
<li><p>실제 구성 요소 제어가 불가능한 의존성인가?</p>
</li>
<li><p>실제 구성 요소의 출력이 비일관적인가?</p>
</li>
<li><p>실제 구성 요소가 테스트 실행 시 속도가 느린가?</p>
</li>
</ul>
<p><strong>위 조건에 N개라도 부합한다면, 필자는 고민없이 필요 인프라 자원을 실행시키고, 실제 운영 객체</strong>(<a target="_blank" href="http://xunitpatterns.com/DOC.html"><strong>depended-on component</strong></a><strong>, 이하 DOC</strong>)<strong>를 사용할 것이다. 그렇게 운영이 가능하면 당연하게도 실제 운영에서도 동일한 동작을 할것이기때문에 실패하는 거짓 음성(False-Nagative)의 상황을 제거할 수 있다.</strong></p>
<p>테스트를 작성하는 이유에는 몇 가지 이유들이 있지만 필자는 안정감을 유지하기 위해 꼭 필요한 도구라고 생각한다. 만약 가짜 객체를 주입시키고, 가짜 인프라를 실행시키면 분명 자동화 테스트에서 다 성공해서 운영에 배포했는데, 시스템 장애를 맞이할 수 있는것이다.</p>
<p>(실제로 필자는 Redis TTL 제어가 필요한 유즈케이스 논리에서 대역을 사용했다가 운영에서 TTL에 음수가 들어갈 수 있는 상황이 발생해서 장애를 맞이한적이 존재한다 🫨 만약 실제 Redis 의존성을 주입해서 사용했다면, 테스트에서 잡혔을것이다.)</p>
<p>이제 위 대역 조건을 통해, 선정해보면 AWS Kinesis는 실행 속도가 느리고, 자원 세팅이 번거롭다. (협업자들의 자원 세팅의 비용도 중요하다 생각한다.) 그리고 Visitor API를 제어하는 WebClient는 제어가 불가능하다고 생각했다. Visitor API에 대한 논리를 요청자가 제어하는것은 불가능하다.</p>
<p>반면, Redis와 MongoDB는 실행 속도가 빠르고 입력에 따른 출력이 일관적이다. 제어도 주어진 인터페이스 내에선 가능하다고 생각했다. 이제 위의 결정대로 의존성을 주입할 수 있는 설계가 되도록 리팩터해보자.</p>
<h3 id="heading-to-be"><strong>예약 유즈케이스 구현 (To-Be)</strong></h3>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReserveUseCase</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> EventStore eventStore;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> VisitorReader visitorReader;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ReservationOverlapValidator reservationOverlapValidator;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ReserveUseCase</span><span class="hljs-params">(
        EventStore eventStore,
        VisitorReader visitorReader,
        ReservationOverlapValidator reservationOverlapValidator
    )</span> </span>{
        <span class="hljs-keyword">this</span>.eventStore = eventStore;
        <span class="hljs-keyword">this</span>.visitorReader = visitorReader;
        <span class="hljs-keyword">this</span>.reservationOverlapValidator = reservationOverlapValidator;
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">execute</span><span class="hljs-params">(Reserve command)</span> </span>{
        Visitor visitor = visitorReader.read(command.getVisitorId());

        <span class="hljs-keyword">if</span> (
            !reservationOverlapValidator.validate(
                  visitor.getId(),
              command.getStartDateTime(),
              command.getEndDateTime()
          )
        ) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(<span class="hljs-string">"overlapped reservation"</span>);
        }

        eventStore.collectEvents(
            List.of(<span class="hljs-keyword">new</span> Reserved(...))
        );
    }

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">EventStore</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">store</span><span class="hljs-params">(String streamId, Iterable&lt;Object&gt; events)</span></span>;
    <span class="hljs-function">List&lt;Object&gt; <span class="hljs-title">queryEvents</span><span class="hljs-params">(String streamId)</span></span>;
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">VisitorReader</span> </span>{
    <span class="hljs-function">Visitor <span class="hljs-title">read</span><span class="hljs-params">(String visitorId)</span></span>;
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ReservationOverlapValidator</span> </span>{
    <span class="hljs-function">Boolean <span class="hljs-title">validate</span><span class="hljs-params">(
        String visitorId,
        OffsetDateTime requestedStartDateTime,
        OffsetDateTime requestedEndDateTime
    )</span></span>;
}
</code></pre>
<p>예약 유즈케이스를 기존 인프라스트럭쳐 레벨의 의존성들을 의존하던것들을 제거하고 순수한 인터페이스만을 의존하고있는 구조로 변경하였다. 이제 리팩터링 한 내용을 기반으로 테스트를 작성해보면 다음과 같다. (도메인 모델 입장에서 순수한 인터페이스를 의존하고 다양한 의존성을 주입할 수 있는 환경을 만드는게 주 목적이다)</p>
<h3 id="heading-test-to-be">Test (To-Be)</h3>
<pre><code class="lang-java"><span class="hljs-meta">@SpringBootTest</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> class <span class="hljs-title">ReserveUseCaseTest</span><span class="hljs-params">()</span> </span>{  
    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> JedisPool redis;

    <span class="hljs-meta">@Autowired</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> ScheduleMongoRepository mongoRepository;

  <span class="hljs-meta">@Test</span>
  <span class="hljs-keyword">void</span> `Sut는 동일한 내원객은 동 시간대 중복 예약을 할 수 없다`() {
        <span class="hljs-comment">//Arrange</span>
        <span class="hljs-keyword">var</span> sut = <span class="hljs-keyword">new</span> ReserveUseCase(
            <span class="hljs-keyword">new</span> ScheduleEventStore(
                mongoRepository,
                <span class="hljs-keyword">new</span> MessageBusStub()
            ),
            VisitorReaderStub(),
            <span class="hljs-keyword">new</span> ReservationOverlapValidator(redis)
        );

        <span class="hljs-keyword">var</span> command = <span class="hljs-keyword">new</span> Reserve(...)

        <span class="hljs-comment">//Act &amp; Assert</span>
        assertThrows(RuntimeException.class, () -&gt; sut.execute(command));
  }

  //...
}

<span class="hljs-keyword">public</span> class VisitorReaderStub implements VisitorReader {
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> Visitor <span class="hljs-title">read</span><span class="hljs-params">(...)</span> </span>{ }
}

<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MessageBusStub</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">IMessageBus</span> </span>{
    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">send</span><span class="hljs-params">(...)</span> </span>{ }
}
</code></pre>
<p>현재 테스트 대상(System Under Test, 이하 SUT)의 의존성을 조립하는 부분을 보면, 인터페이스를 구현한 테스트 대역들을 주입하고있는 것을 확인할 수 있다. 이 때, Redis와 Mongo와 관련된 의존성인 실제 운영 객체를 주입하고있는것도 볼 수 있다.</p>
<p>테스트 대역에는 Stub, Fake, Spy, Mock, Dummy 와 같은 많은 방법들이 있는데, 이번엔 예시 코드에서 사용한 개념들에 대해서만 예시 코드로 알아본다.</p>
<h2 id="heading-test-double"><strong>테스트 대역(Test Double)</strong></h2>
<blockquote>
<p>테스트 환경에서 실제 객체를 대신하여 사용되는 객체들을 의미한다. 이는 테스트의 독립성을 유지하고, 외부 의존성에 대한 제어를 통해 다양한 시나리오를 검증하는데 유용하게 사용된다</p>
</blockquote>
<h3 id="heading-fake">Fake</h3>
<blockquote>
<p>SUT가 의존하는 실제 구성 요소(DOC)를 대체하여 동일한 기능을 간단하게 구현한 객체이다. 검증 목적으로 사용되지는 않으며, 제어점이나 관찰점으로 사용되지 않는다</p>
</blockquote>
<p>e.g.</p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InMemoryEventStoreFake</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">EventStore</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> Map&lt;String, List&lt;Object&gt;&gt; store;

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">InMemorySaasEventStore</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">this</span>.store = <span class="hljs-keyword">new</span> HashMap&lt;&gt;();
    }

    <span class="hljs-meta">@Overrride</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">collectEvents</span><span class="hljs-params">(String streamId, Iterable&lt;Object&gt; events)</span> </span>{
        store.computeIfAbsent(
            streamId,
            x -&gt; <span class="hljs-keyword">new</span> ArrayList&lt;&gt;()
        )
        .addAll(events);
    }

    <span class="hljs-meta">@Override</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;Object&gt; <span class="hljs-title">queryEvents</span><span class="hljs-params">(String streamId)</span> </span>{
      <span class="hljs-keyword">return</span> store.getOrDefault(streamId, Collections.emptyList());
  }
}
</code></pre>
<p>위 시나리오에서는 실제 구성 요소(DOC)를 사용하고있지만, 만약 메인으로 검증하고자 하는 DB가 아니라면, 위와 같이 구현하여 사용할 수 있다.</p>
<h3 id="heading-stub">Stub</h3>
<blockquote>
<p>SUT가 의존하는 실제 구성 요소(DOC)를 대체하여, 미리 정의된 응답이나 동작을 제공하는 객체이다. SUT가 특정 조건이나 상황에서 어떻게 동작하는지를 테스트하기 위해 사용되며, 내부 동작이나 호출된 메서드를 기록하지는 않는다</p>
</blockquote>
<p><strong>e.g.</strong></p>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">VisitorReaderStub</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">VisitorReader</span> </span>{
        <span class="hljs-keyword">private</span> Visitor visitor;

        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">VisitorReaderStub</span><span class="hljs-params">(Visitor visitor)</span> </span>{
            <span class="hljs-keyword">this</span>.visitor = visitor;
        }

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> Visitor <span class="hljs-title">read</span><span class="hljs-params">(String visitorId)</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Visitor(
            visitor.getId(),
            visitor.getName()
        ); 
    }
}
</code></pre>
<ul>
<li><p>내부 간접 입력을 통해 미리 정해진 결과를 반환하는 VisitorReaderStub의 구현체이다.</p>
</li>
<li><p>만약 실제 구성 요소 객체(DOC)를 SUT에 주입했다면, WebClient를 이용하여 Visitor Component로 HTTP 요청을 보내야한다. 그러나 Stub 객체를 읽어 간접 출력한 값으로 테스트를 진행하도록 했다.</p>
</li>
</ul>
<h3 id="heading-spy">Spy</h3>
<blockquote>
<p>실제 구성 요소(DOC)를 대체하면서, 호출된 메서드나 전달된 인자를 기록하는 객체이다. SUT의 상호작용을 검증하는 데 사용되며, Stub처럼 미리 정의된 동작을 제공할 수도 있다.</p>
</blockquote>
<pre><code class="lang-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EventStoreSpy</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">EventStore</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> List&lt;Object&gt; eventLogs = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();

    <span class="hljs-meta">@Override</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">collectEvents</span><span class="hljs-params">(Iterable&lt;Object&gt; events)</span> </span>{
        <span class="hljs-keyword">this</span>.eventLogs.addAll(events);
    }

    <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;Object&gt; <span class="hljs-title">getEvents</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.eventLogs;
    }
}
</code></pre>
<ul>
<li><p>위와 같이 Spy는 실제 해당 메서드가 호출(출력)되었는지 관찰하고 싶을 때 사용한다.</p>
</li>
<li><p>만약 유즈케이스가 실행되었을 때, EventStore의 collectEvents가 호출되었는지, 그리고 예상한 형태의 이벤트가 수집되었는지 검증하고자 할 때 사용한다.</p>
<ul>
<li>대게 Spy는 명령이 몇 번 실행되었는지, 또는 메세지가 정상적으로 발행을 했는지 등을 검증할 때 사용한다.</li>
</ul>
</li>
</ul>
<h1 id="heading-classists-vs-mockists">Classists vs Mockists</h1>
<p>테스트 주도 개발(Test-Driven Development, TDD)이 널리 확산되면서, 테스트 작성 방법에 대한 다양한 철학과 접근 방식이 등장했다. 이 중에서도 Classists와 Mockists는 테스트의 초점과 목적에 따라 나뉘는 두 가지 대표적인 테스트 철학이다.</p>
<p>두 학파 중 어떤 방식을 선택하느냐는 테스트하려는 시스템의 복잡성과 팀의 철학에 따라 달라질 수 있다고 생각하나.. 최근엔 전체적인 시장에서도 Classists쪽으로 많이 기운것같다. (기울게 된 이유도 명확하다 생각한다)</p>
<h3 id="heading-classists">Classists</h3>
<p>실제 객체를 사용하여 검증하고, 통합된 시스템의 동작을 확인하며 상태 검증(State Verification)에 중점을 둔다. 시스템이 의도한 대로 동작하는지를 검증하기 위해 <strong><em>실제 객체(DOC)를 사용</em></strong>하거나 ‘간혹 상황에 따라’ 이를 대체하는 테스트 대역을 활용한다.</p>
<h3 id="heading-mockists">Mockists</h3>
<p>특정 메서드 호출이나 객체 간 상호작용을 검증한다. 행위 검증(Behaviour Verification)에 주로 중점을 두고, 객체 간의 상호작용이 올바르게 이루어졌는지 확인한다.</p>
<h3 id="heading-7ja065akiouwqeylneydhcdrjzqg7isg7zi47zwy64ky7jqupw">어떤 방식을 더 선호하나요?</h3>
<p>필자는 Classists 방식을 선호한다.</p>
<p>Mockists의 접근 방식은 행위 검증을 통해 간단한 반환값을 특정하여 메서드 호출의 정확성을 확인할 수 있는 장점이 있지만, 상태 검증이 부족하여 거짓 음성(False Nagative)이 발생할 수 있기때문이다.</p>
<p>이로 인해, 테스트는 통과하나 실제 구성 요소(DOC) 동작은 실패하는 경험을 할 수 있다. 그래서 가능하다면 실제 객체를 사용하고있으며, 외부 시스템과 협력이 필요한 경우에는 테스트 대역(Test Double)을 사용하여 Classists 방식의 테스팅을 진행하고있다.</p>
<h2 id="heading-66ei7lmy66mw">마치며</h2>
<p>테스트를 작성하는 가장 큰 이유가 안정감이라고 생각한다.</p>
<p>그 안정감을 유지하기 위해선 거짓 음성, 거짓 양성을 최소화해야한다. 그러기위해서 우리는 테스트 대역을 지양해야하고, 실제 객체를 사용하는것이 좋다. 이번 글에선 대역에 대한 기준을 정하고, 코드를 재설계하여 필요한 의존성을 주입해서 사용하는 과정을 소개해보았다.</p>
]]></content:encoded></item><item><title><![CDATA[Messaging Services에서 Message의 의미가 무엇인가요?]]></title><description><![CDATA[서론
흔히 Microservices, Miniservices와 같이 분산 시스템 환경에서 컴포넌트 간 데이터를 전달하기 위해 Message를 생산하고 소비한다.
이번 포스팅에선 Message가 무엇인지, 그리고 Command, Query, Event의 각 개념과 차이점은 무엇인지에 대해서 작성해보려고한다.
Message의 본질
Message는 시스템 간 통신의 기본 단위로, 정보를 주고받는 매개체 역할을 한다. 그리고 데이터를 담은 컨테이너일 ...]]></description><link>https://jeongkyun-dev.kr/what-does-message-mean-in-messaging-services</link><guid isPermaLink="true">https://jeongkyun-dev.kr/what-does-message-mean-in-messaging-services</guid><category><![CDATA[#EventDrivenArchitecture]]></category><category><![CDATA[Messaging Service]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Fri, 27 Dec 2024 05:14:46 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-7isc66gg">서론</h2>
<p>흔히 Microservices, Miniservices와 같이 분산 시스템 환경에서 컴포넌트 간 데이터를 전달하기 위해 <code>Message</code>를 생산하고 소비한다.</p>
<p>이번 포스팅에선 Message가 무엇인지, 그리고 Command, Query, Event의 각 개념과 차이점은 무엇인지에 대해서 작성해보려고한다.</p>
<h2 id="heading-message">Message의 본질</h2>
<p>Message는 시스템 간 통신의 기본 단위로, 정보를 주고받는 매개체 역할을 한다. 그리고 데이터를 담은 컨테이너일 뿐만 아니라, 전송 의도와 의미를 전달한다.</p>
<p>이 때, 의도에 따라 Message는 아래와 같이 세가지 유형으로 분류 해볼 수 있다.</p>
<p>Message</p>
<p>ㄴ Command</p>
<p>ㄴ Event</p>
<p>ㄴ Query</p>
<h3 id="heading-command">명령(Command)</h3>
<p><code>명시적으로 특정 동작을 수행하라는 요청</code>을 뜻한다.</p>
<p>시스템에선 상태의 변경을 발생시키는 요청이라고 할 수 있다. 예를들어, 결제를 요청(<code>ProcessPaymentCommand</code>)하거나 예약을 하는것(<code>ReserveCommand</code>)처럼 일반적으로 상태를 변경하기 위한 요청이라고 볼 수 있다.</p>
<p>주요 특징으로는 요청자가 수신자(Consumer)를 의존한다. 예약을 요청한다는것은, 요청자가 수신자를 알고 Message를 요청한다는것과 같다.</p>
<h3 id="heading-event">사건(Event)</h3>
<p><code>이미 발생한 사실</code>을 뜻한다.</p>
<p>시스템에선 과거의 상태 변경을 전달하고, 수신자가 이를 기반으로 적절히 반응(Reaction)할 수 있다. 예를 들어, 결제 완료됨(<code>PaymentCompleted</code>)예약됨(<code>Reserved</code>)처럼 과거에 발생한 사실의 정보가 담긴 값이다.</p>
<p>주요 특징으로는 수신자가 특정되지않고, 발행자(Producer)는 수신자(Consumer)의 존재 여부에 의존하지않는다.</p>
<h3 id="heading-query">조회(Query)</h3>
<p><code>데이터를 요청하거나 조회하기 위한 요청</code>을 뜻한다.</p>
<p>시스템에선 상태에 변경을 가하지않고, 단순히 정보를 반환하는데 초점이 맞춰져있다. 예를들어, 결제 내역 조회(<code>PaymentsQuery</code>)를 하거나 예약 내역 조회(<code>ReservationsQuery</code>)처럼 시스템 상태를 유지하면서 필요한 정보를 얻는데 사용된다.</p>
<h2 id="heading-7jii7iuc66gcioyvjoyvhouztoupta">예시로 알아보면</h2>
<p>대게 커머스 주문 시스템에선 사용자가 상품을 주문하면, 다양한 서비스들이 협력하여 주문 생성, 결제 처리, 재고 차감 등을 수행하는 구조를 갖게된다. 이 과정에서 <code>Message</code>를 어떻게 구분 하는지에 대해 알아보면 다음과 같다.</p>
<p>(<s>물론 시스템 설계자에 따라 의존 방향, 이벤트 설계는 다를 수 있다.</s>)</p>
<ol>
<li><h4 id="heading-command-1">주문 생성 명령 (Command)</h4>
</li>
</ol>
<p>클라이언트가 Order Service로 주문 생성 명령(<code>CreateOrderCommand</code>)을 전달한다. 이 때, 내부 논리를 통해 Order Entity가 생성될 것이다.</p>
<p>이 때, Order Serivce로 요청자가 수신자를 알고 요청하는(의존하는) 형태가 되는것을 알 수 있다.</p>
<ol start="2">
<li><h4 id="heading-event-1"><strong>주문 생성됨 사건 (Event)</strong></h4>
</li>
</ol>
<p>주문 생성이 완료되면 Order Service에서는 주문 생성됨(<code>OrderCreated</code>)의 실제 발생한 사건에 대해 발행한다.</p>
<p>이 때, 일반적으로 해당 주문이 완료되면 결제가 진행되어야하고 상품 인벤토리의 개수가 차감되어야한다. 그리고 사용자에게 주문이 완료되었다는 알림을 보내야한다.</p>
<p>각 Payment Service, Inventory Service, Notification Service는 해당 이벤트를 구독하고 있는 상태라면, 사건이 발생한것을 반응하여 Payment Service에서는 결제 프로세스를 진행하고 Inventory Service에서는 해당 상품의 재고 차감을 진행할 것이다. 그리고 Notification Service는 주문 완료되었다는 고객의 번호 또는 이메일로 완료 내용을 알릴 것이다.</p>
<p>이 때, Order Service는 주문 생성을 완료하고 해당 사건에 대해 발행만 했을 뿐, 구독자와 이벤트를 소비한 주체가 누군지 모른다. 즉, Event의 의존방향은 Command와 달리 발행자는 수신자를 모르는 형태가 된다.</p>
<ol start="3">
<li><h4 id="heading-query-1">주문 조회 (Query)</h4>
</li>
</ol>
<p>주문이 완료된 내역을 조회(<code>OrderQuery</code>)한다.</p>
<p>상태의 부작용을 갖고있지않기에 특별한 복잡성 없이 주문의 식별값을 통해 구체적인 상태의 정보를 취득할 수 있다.</p>
<h2 id="heading-66ei7lmy66mw">마치며</h2>
<p>Messaging Services에서 Message는 Command, Query, Event를 포괄한 넓은 개념이라는것을 알아보았다. 대게 이를 명확하게 구분지어 표현할 때가 얻는 이점이 더 많을 때가 있기때문에 각 개념들을 정확히 이해하고 사용하는것이 중요하다고 생각한다.</p>
]]></content:encoded></item><item><title><![CDATA[MongoDB를 왜 메인 데이터베이스로 사용하고있나요?]]></title><description><![CDATA[배경
필자는 현재 B2B SaaS 신사업팀에서 백엔드 엔지니어로 일 하고있다.
메인 데이터베이스로 MongoDB를 사용하고있는데, 그 이유와 사용 방법에 대해 소개해보려한다. (실제로 질문도 많이 받았었다)
왜 MongoDB를 메인 데이터베이스로 채택했나요?
MongoDB는 세부적인 보안 설정을 컬렉션 별로 지원하기때문이다.
왜 세부적인 보안 설정이 필요하고, 이를 어떻게 활용한 계획인지 설명하기 위해선 우선 Multi-Tenancy에 대해 간...]]></description><link>https://jeongkyun-dev.kr/why-use-mongodb</link><guid isPermaLink="true">https://jeongkyun-dev.kr/why-use-mongodb</guid><category><![CDATA[Multi tenancy]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[database]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Wed, 18 Dec 2024 15:47:45 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-67cw6rk9">배경</h2>
<p>필자는 현재 B2B SaaS 신사업팀에서 백엔드 엔지니어로 일 하고있다.</p>
<p>메인 데이터베이스로 MongoDB를 사용하고있는데, 그 이유와 사용 방법에 대해 소개해보려한다. (실제로 질문도 많이 받았었다)</p>
<h2 id="heading-mongodb">왜 MongoDB를 메인 데이터베이스로 채택했나요?</h2>
<p>MongoDB는 세부적인 보안 설정을 컬렉션 별로 지원하기때문이다.</p>
<p>왜 세부적인 보안 설정이 필요하고, 이를 어떻게 활용한 계획인지 설명하기 위해선 우선 Multi-Tenancy에 대해 간략히 설명이 필요할것같다.</p>
<blockquote>
<h3 id="heading-multi-tenancy">Multi-Tenancy가 뭔가요?</h3>
</blockquote>
<p>Multi-Tenancy는 단일 인스턴스에서 여러 테넌트(고객 또는 조직)를 독립적으로 지원하는 아키텍처이다.</p>
<p><img src="https://mblogthumb-phinf.pstatic.net/MjAyMDAxMjBfODgg/MDAxNTc5NDk0ODQ2OTM3.ojXj6Fg4AyZhoXT2uy8tXlhkETmqoz479v_WPyj7cugg.zdmOSUhnn9kXIFEtBWVdAOoW11RrkSoIrCCFe6q-ZLwg.PNG.ki630808/multitenancy2.PNG?type=w800" alt /></p>
<p>위 이미지처럼 Multi-Tenancy는 소프트웨어 개발과 유지보수 비용을 공유하기 때문에 경제적이다. 따라서, 공급자는 업데이트를 한 번만 하면 다중 테넌트에게 업데이트 된 서비스를 제공할 수 있다.</p>
<p>(Single-Tenant라면, 공급자는 여러 소프트웨어의 인스턴스에 모두 업데이트가 필요하다.)</p>
<p><img src="https://velog.velcdn.com/images/jongil512/post/3cbc5ce4-b0b3-479b-bdb8-6975e50e39b1/image.png" alt /></p>
<p>그런데 각 테넌트마다 독립적인 Database Server를 사용하면, 고객사가 10000개라면, 10000개의 DB Server를 관리해야하는 비용이 발생한다. 그래서 서비스 목적성에 맞추어 Multi-Tenancy Model을 채택하여 설계한다.</p>
<p>내가 속한 팀의 Multi-Tenancy 구조는 3번이다. 하나의 인스턴스에서 논리적으로 테넌트를 분리하고 Database Server는 한대만 기동하고있다.</p>
<p>물론, 엔터프라이즈 고객의 요구에 따라 Database Server와 어플리케이션 인스턴스를 독립적으로 분리할 수 있는 인프라 세팅도 준비되어있다.</p>
<blockquote>
<h3 id="heading-tenant">어떻게 Tenant 별로 데이터를 관리하고있나요?</h3>
</blockquote>
<p>위에서 말했듯이, 어플리케이션에서 논리적으로 테넌트들을 분리하고있다. 논리적으로 분리된 테넌트는 각 독립된 MongoDB Collection에 저장이된다.</p>
<p>이를 코드로 나타내면 아래와 같다.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReservationRepositoryImpl</span></span>(
    database: MongodbConfiguration,
) : ReservationRepository {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> entityType: Class&lt;ReservationDataModel&gt; = ReservationDataModel::<span class="hljs-keyword">class</span>.java
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> collection: MongoTemplate = MongoTemplate(database.mongoDatabaseFactory)

    <span class="hljs-keyword">fun</span> create(tenantId: String, entity: ReservationDataModel) {
        <span class="hljs-keyword">val</span> collectionName = <span class="hljs-string">"reservations_<span class="hljs-variable">$tenantId</span>"</span>
        collection.insert(entity, collectionName)
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">findById</span><span class="hljs-params">(tenantId: <span class="hljs-type">String</span>, reservationId: <span class="hljs-type">String</span>)</span></span>: ReservationDataModel? {
        <span class="hljs-keyword">val</span> collectionName = <span class="hljs-string">"reservations_<span class="hljs-variable">$tenantId</span>"</span>
        <span class="hljs-keyword">return</span> collection.findById(reservationId, entityType, collectionName)
    }
}
</code></pre>
<p>최초 고객이 로그인할 때, 테넌트를 설정할 수 있고 접속하면 URL에서 tenantId를 관리하고있고, 도메인 서비스에게 보내는 요청의 Request Param에 tenantId가 모두 포함되어있는 형태이다.</p>
<p>따라서, Domain Service들의 API Endpoint에는 <code>.../tenants/{tenant}/...</code>가 붙어있다. 이러한 요청을 통해 구분된 TenantId는 MongoDB의 CollectionName에 Suffix에 붙여 사용한다.</p>
<p>e.g. collection name</p>
<ul>
<li><p>reservations_{tenantId}</p>
</li>
<li><p>products_{tenantId}</p>
</li>
</ul>
<p>따라서 각 테넌트별로 독립적인 컬렉션을 갖고있는 형태가된다. 독립적이기때문에 얻을 수 있는 이점은 컬렉션 별 보안 설정을 할 수 있다. 물론, 특정 테넌트에 잘못된 데이터가 Hard Updating 되었을 때 다른 테넌트들은 장애 전파가 안되기도하지만 이는 매우 특별한 케이스이긴하다.</p>
<blockquote>
<h3 id="heading-6re4656y7iscioykkeyalo2vncdrs7tslyjsnyag7ja065a76rkmioyepoygleydhcdtlzjqs6dsnojrgpjsmpq">그래서 중요한 보안은 어떻게 설정을 하고있나요?</h3>
</blockquote>
<p>MongoDB는 컬렉션 수준 보안 설정 기능을 제공한다.</p>
<blockquote>
<p><strong>역할 기반 액세스 제어 (RBAC) 기능</strong></p>
</blockquote>
<ul>
<li><p>역할 기반 액세스 제어를 통해 DB의 특정 컬렉션에 대한 권한을 세밀하게 부여할 수 있다.</p>
<ul>
<li><p>관리자는 사용자 정의 역할(User Defined Role)을 생성하여 각 테넌트의 컬렉션에 대한 읽기, 쓰기, 업데이트 등의 권한을 개별적으로 설정할 수 있다.</p>
</li>
<li><p>권한의 수준 범위는 아래와 같다.</p>
<ul>
<li><p>데이터 베이스 수준</p>
<ul>
<li>특정 테넌트 전용의 DB에만 접근 권한만 부여할 수 있다 (하나의 테넌트가 N개의 DB에 접근할 수도 있다)</li>
</ul>
</li>
<li><p>컬렉션 수준</p>
<ul>
<li>테넌트의 컬렉션만 접근 가능하도록 제한이 가능하다</li>
</ul>
</li>
<li><p>필드 수준</p>
<ul>
<li>특정 필드에도 read, write의 제한이 가능하다</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>사용자 정의 역할(User Defined Role)</p>
</blockquote>
<ul>
<li><p>MongoDB 관리자(제품팀)는 특정 컬렉션이나 DB에 대해 사용자 정의 역할을 생성할 수 있다.</p>
</li>
<li><p>각 역할 별 read, write, update와 같은 세부 권한을 커스텀 할 수 있고, 특정 테넌트의 컬렉션에만 적용되도록 제한할 수도 있다.</p>
<ul>
<li><p>e.g.</p>
<ul>
<li><p>reservation_{tenantA}에서 <code>병원 IT 관리자1</code> 에게 해당 컬렉션의 read 권한을 줄 수 있다.</p>
</li>
<li><p>제품팀 개발자들의 책임에 따라 관리자가 책임별로 세부 권한을 나누어 관리할 수 있다.</p>
<ul>
<li>SaaS 제품 특성 상 병원의 데이터를 제품 개발자라고 모두 볼 수 있는것은 법적으로 안된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>위의 MongoDB RBAC 특성을 통해, 잘못된 쿼리나 운영 실수로 인해 다른 테넌트에 데이터가 노출되거나, 수정되는것을 방지할 수 있게되어 데이터 격리 수준을 한층 강화할 수 있다.</p>
<pre><code class="lang-javascript">db.createRole({
  <span class="hljs-attr">role</span>: <span class="hljs-string">"tenant_admin_reader"</span>,
  <span class="hljs-attr">privileges</span>: [
    {
      <span class="hljs-attr">resource</span>: { <span class="hljs-attr">db</span>: <span class="hljs-string">"myDatabase"</span>, <span class="hljs-attr">collection</span>: <span class="hljs-string">"reservations_tenantA"</span> },
      <span class="hljs-attr">actions</span>: [<span class="hljs-string">"find"</span>]
    }
  ],
  <span class="hljs-attr">roles</span>: []
});

<span class="hljs-comment">// 2. 사용자에게 역할 할당</span>
db.createUser({
  <span class="hljs-attr">user</span>: <span class="hljs-string">"tenantAUser"</span>,
  <span class="hljs-attr">pwd</span>: <span class="hljs-string">"pw1234"</span>,
  <span class="hljs-attr">roles</span>: [<span class="hljs-string">"tenant_admin_reader"</span>]
});
</code></pre>
<p>위 스크립트처럼 RBAC 설정들을 모두 DB Query를 제공하고 있어서, 백오피스를 구축하면 쉽게 권한 분리도 가능하다.</p>
<h2 id="heading-66ei7lmy66mw">마치며</h2>
<p>속한 SaaS팀에서 Multi-Tenancy 환경을 구축하면서 MognoDB를 메인 데이터베이스로 사용하고있는 이유에 대해서 다뤄보았다. 포스트에서 유의할점은 RDBMS에서도 RBAC에 준하는 권한 관리가 가능하다.</p>
<p>그러나 MongoDB는 동적 데이터 구조 관리(Dynamic Data Structure Management) + 스키마 리스(Schema-less)하여 보다 현재 속한 신사업 팀이 보다 빠르고 간단하게 Multi-Tenancy를 구축하는데 도움이 되었던 내용을 공유한것이니 <code>MongoDB 라서 가능했다!는 아니라는것을 유의바란다.</code></p>
<p>(실제로, Mongo Atlas를 사용하고 있었기때문에 Atlas Search 기능을 통해 보다 쉽게 검색 엔진을 도입할 수 있었던 사례도 있는데 이것도 추후 기회가 되면 작성해보도록 하겠다.)</p>
]]></content:encoded></item><item><title><![CDATA[Domain-Driven Design을 왜 학습해야할까요?]]></title><description><![CDATA[최근 Domain-Driven Design(이하 DDD) 개념이 많은 IT 종사자들에게 퍼지고있다.
이번 포스팅에선 필자의 현업에서 경험한 내용을 바탕으로 DDD에 대한 이야기를 작성해보려한다.
작성자 배경
미용의료 병원용 B2B SaaS 제품(이하 KOS)을 만들고있으며, 총 인원 10-15명 정도의 신사업팀에서 백엔드 엔지니어를 담당하고있고, 팀 내엔 DDD에 익숙한, 익숙하지 않은 이들이 모여있다.
엔지니어는 총 8명이 있고, 백엔드는 필...]]></description><link>https://jeongkyun-dev.kr/domain-driven-design</link><guid isPermaLink="true">https://jeongkyun-dev.kr/domain-driven-design</guid><category><![CDATA[#Domain-Driven-Design]]></category><dc:creator><![CDATA[Jeongkyun An]]></dc:creator><pubDate>Sat, 14 Dec 2024 08:37:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769954762413/b5db8521-d0d5-4843-a0fc-83ea1c4e0c13.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h5 id="heading-domain-driven-design-ddd-it">최근 <strong>Domain-Driven Design</strong>(이하 DDD) 개념이 많은 IT 종사자들에게 퍼지고있다.</h5>
<h5 id="heading-ddd">이번 포스팅에선 필자의 현업에서 경험한 내용을 바탕으로 DDD에 대한 이야기를 작성해보려한다.</h5>
<h2 id="heading-7j6r7isx7j6qiouwsoqyvq">작성자 배경</h2>
<p>미용의료 병원용 B2B SaaS 제품(이하 KOS)을 만들고있으며, 총 인원 10-15명 정도의 신사업팀에서 백엔드 엔지니어를 담당하고있고, 팀 내엔 DDD에 익숙한, 익숙하지 않은 이들이 모여있다.</p>
<p>엔지니어는 총 8명이 있고, 백엔드는 필자 포함 2명이다. 다른 이들은 PO(Product Owner), PD (Product Desginer), Tech Sales (Domain Expert 1) 분들이 계신다.</p>
<h2 id="heading-ddd-1">DDD를 사용해서 좋은점이 뭔가요?</h2>
<blockquote>
<h3 id="heading-kirtjiag64k0iouphouployducdsp4dsi53snyqg7zgc7ksa7zmu7zwy6rogiouqhe2zle2vncdshozthrxsnyqg7zwgioyimcdsnojri6qqkg"><strong>팀 내 도메인 지식을 표준화하고 명확한 소통을 할 수 있다</strong></h3>
</blockquote>
<p>팀 내 이해관계자들(Stakeholders)간 원활한 의사소통은 가장 필요로 하는것 중 하나이다.</p>
<p>DDD에는 크게 두가지로 전술적 패턴과 전략적 패턴이 존재한다.</p>
<p>전략적 패턴에 보편 언어(Uniquitous Language)라는 개념이 존재하는데, 주 목적은 모든 팀 구성원이 동일한 언어로 도메인을 이해하고 이야기 할 수 있는것이다.</p>
<p>이 때, 보편 언어를 정의할 때는 제품 사용성에 대해 집중해야한다. 제품 사용자들이 실제 사용하는 언어, 행위 등과 준 동치되는 개념으로 투영되도록 정의하는게 중요하다는것이다.</p>
<p>그래서 팀 내 도메인 전문가(Domain Expert)를 초빙하여, 의사소통에 필요한 개념들의 표준화를 다같이 정의한다. 물론 이 보편 언어는 단방에 정의할 수는 없다. 실제 DDD Guide에서도 한번에 정의내리는것이 아닌 여러 *지식 탐구(Crunching Knowledge) 시간을 여러번 가지며 정의내릴 수 있다고 말한다.</p>
<p>ref. 지식탐구? - <a target="_blank" href="https://www.dddcommunity.org/wp-content/uploads/files/books/chapter01.pdf">https://www.dddcommunity.org/wp-content/uploads/files/books/chapter01.pdf</a></p>
<h3 id="heading-7jii7iuc66gciouztoupta">예시로 보면</h3>
<p><strong>보편언어가 정해져있지 않을 때</strong></p>
<blockquote>
<p>팀원: 고객이 병원에 워크인해서 데스크에 말한 뒤 예약을 생성할 수 있어요.</p>
</blockquote>
<p>위 내용을 도메인 전문가 또는 팀원에서 보편 언어를 설정하면 다음과 같이 변경할 수 있다.</p>
<p><strong>보편 언어를 정한 뒤에 소통할 때</strong></p>
<blockquote>
<p>팀원: 고객이 워크인해서 접수한 뒤 예약을 할 수 있어요.</p>
</blockquote>
<p>좀 더 간단하고 명확한 표현으로 정리된다. 이는 아래에서 다루겠지만, 엔지니어들에게도 매우 중요한 변경사항이다.참고로 필자의 팀 내에선 노션으로 보편 언어 문서를 관리한다. (아래 이미지 참고)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734163480189/da101ce4-d4bd-43ee-bd0e-a07a5bd7ed70.png" alt class="image--center mx-auto" /></p>
<p>위 노션 문서에는 제품의 많은 보편언어들이 정리가 되어있다.</p>
<p>위에서 언급했듯이 보편 언어는 엔지니어들에게도 중요한 변경사항이다.</p>
<h3 id="heading-7jmciouzto2oucdslrjslrtqsiag6rcc67cc7j6q7zwc7ywmioykkeyalo2vnoqwgoyald8">왜 보편 언어가 개발자한테 중요한가요?</h3>
<p>DDD의 또 하나의 장점은 기술과 비지니스의 경계를 허무는것이다. 즉, 비지니스 가치를 중심으로 개발을 할 수 있다는 것이다.</p>
<p>최초 보편언어가 정해져있지않을 때 예약 서비스를 만든다면 아래와 같이 도메인 이벤트, 명령들을 정의할 수 있을것같다.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateReservationCommand</span></span>(...) {} <span class="hljs-comment">// 예약을 생성하는 DTO</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReservationCreated</span></span>(...) {} <span class="hljs-comment">// 예약 생성됨을 뜻하는 도메인 이벤트</span>
</code></pre>
<p>데이터적인 사고를 배제하면 예약을 생성한다는 어색하다. 실제 비지니스에서 보더라도 병원 관리자가 고객에게 “예약 생성하시고 오셨나요?“라고 물어보진 않을것이다.</p>
<p>그래서 실제 비지니스의 행위에 맞는 보편언어를 정했을 때 System에도 그대로 투영 되도록 설계하는것이 중요하다.</p>
<p><strong>보편언어를 정한 이후엔 아래와 같이 정의할 수 있다.</strong></p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ReserveCommand</span></span>(...) {} <span class="hljs-comment">// 예약 DTO</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Reserved</span></span>(...) {} <span class="hljs-comment">//예약됨을 뜻하는 도메인 이벤트</span>
</code></pre>
<p>우리가 해결하고자 하는 비지니스 문제, 상황에 맞추어 클래스, 메서드, 변수 등을 정의내릴 수 있다. 이러한 행위들은 결국 시스템이 커질수록 빛을 바랄 수 있다.</p>
<p>제품을 만들다보면, 비스무리하지만 살짝 다른 개념들이 정말 많이 나온다. 이럴 때일 수록 명확하게 보편언어를 팀내에서 설정하고 만들다보면 스파게티가 될 확률이 매우 높아진다. 자연스레 코드에서도 스파게티 개념들을 녹이다보면 유지보수성이 매우 떨어질 수 있다.</p>
<blockquote>
<h3 id="heading-67o17j6h7zwcio2yhoylpoyeuoqzhoydmcdrrljsojzrpbwg66qf7zmv7zwcioqyveqzhoulvcdrgpjriitslrqg7zw06rkw7zwgioyimcdsnojri6q">복잡한 현실세계의 문제를 명확한 경계를 나누어 해결할 수 있다</h3>
</blockquote>
<p>이는 DDD에서 전략적 설계의 Bounded Context에 해당되는 이야기이다.</p>
<p>소프트웨어 개발 과정에서 현실세계의 문제를 그대로 코드로 옮기다보면 복잡도가 급격하게 증가한다. 특히, 여러 도메인이 얽혀있는 경우 의미가 다른 개념들이 혼재되어 혼란을 야기할 수 있다.</p>
<p>(필자가 만들고있는 제품에서 식별한 도메인만 해도 10개가 넘는 Bounded Context가 있다)</p>
<blockquote>
<p><strong>Bounded Context? (이하 B.C)</strong><br />‘하나의 시스템을 여러개의 독립적인 컨텍스트로 나누어 관리하는 개념’</p>
</blockquote>
<ul>
<li><p>특정 도메인 모델이 유효한 경계(boundary)를 정의하며, 각 B.C 내에서 도메인 모델은 일관성을 유지한다.</p>
</li>
<li><p>B.C 간의 경계를 명확히 함으로써 모델의 충돌을 피하고 각 컨텍스트 내에서 독립적으로 모델을 발전시킬 수 있다.</p>
</li>
<li><p>설계를 통해 B.C를 도출하고 연관 관계를 맺었다면, 그 관계를 유지하기 위해 어떻게든 우회해서 유지하는게 좋다.</p>
</li>
</ul>
<p>예를 들어, 필자가 개발중인 제품에는 다양한 도메인 요구사항이 존재한다.</p>
<ul>
<li><p>스케줄(예약) 관리</p>
</li>
<li><p>수납 관리</p>
</li>
<li><p>시술권 관리</p>
</li>
</ul>
<p>DDD를 하고있다면 이 각각의 영역들의 경계 컨텍스트를 명확히 정의내리고, 각 경계별로 어떤 모델들을 가지는지, 의존성은 어떻게 관리하는지 등에 대해 정리를 해야한다. (아래 이미지 참고)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734259448224/3bd8624b-1d86-4ce0-92a3-b9590e6efa3f.png" alt="Bounded Context Dependecy" class="image--center mx-auto" /></p>
<p>위와 같이 각 B.C 별로 의존성 관계를 설정하고, 앞으로 확장되는 요구사항이 생길 때마다 각 경계의 책임에 맞추어 설계 및 개발이 이루어져야한다. 만약, 제품의 확장성, 유지보수성을 고려하지않고 경계를 정의하지않고 서로 양방향 의존성이 생기도록 개발이 되면 추후 확장되면서 점차 감당할 수 없는 논리들이 마구마구 생겨날 것이다.</p>
<p>이렇게 잘 정의된 B.C는 모듈화를 촉진하고, 재사용성을 높일 수 있게된다.</p>
<blockquote>
<h4 id="heading-bc">B.C.를 나누지않으면 무슨 문제들이 발생할까?</h4>
</blockquote>
<ul>
<li><p><strong>모델 충돌 및 혼란</strong></p>
<ul>
<li>도메인 모델이 하나의 컨텍스트 안에서 혼재해 있을 때, 동일한 개념이 다른 의미로 사용되거나 서로 다른 용어가 동일한 의미를 가지는 경우가 발생할 수 있다.</li>
</ul>
</li>
<li><p><strong>비지니스 로직의 분산</strong></p>
<ul>
<li><p>도메인 로직이 여러곳에 분산되어있을 경우, 비지니스 로직을 이해하고 수정하는데 어려움이 발생한다.</p>
</li>
<li><p>특정 로직이 어디에 위치하는지 찾기가 어려워지고, 이는 개발자의 생산성과 시스템의 일관성을 저해한다.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-ddd-2">DDD는 모든 상황에 적합한 방법론이 아니다</h2>
<p>(<s>세상에 트레이드 오프 없는 방법론이 존재할까🤔</s>)</p>
<p>DDD는 복잡한 도메인을 다루기 위한 강력한 설계 방법론으로 많은 장점을 제공하지만, 모든 상황에 적합하지는 않다. 특히, 팀의 경험과 조직 환경, 도메인의 복잡성에 따라 DDD가 기대만큼 효과를 발휘하지 못할 수 있다. 이와 같은 트레이드 오프를 이해하고 상황에 따라 적절히 활용하는것이 중요하다 생각된다.</p>
<blockquote>
<h3 id="heading-7yya7j2yioqyve2xmoyxkcdrlldrnbwg6rkw6ro86rcaioyynoywqounjouzhoydtoulpa">팀의 경험에 따라 결과가 천차만별이다</h3>
</blockquote>
<p>DDD의 성공 여부는 팀의 역량과 경험에 크게 의존한다.</p>
<p>DDD는 단순한 방법론이 아니며, 도메인 중심의 사고방식과 이를 구현하기 위한 전략적·전술적 설계 모두에 깊은 이해가 필요하다. 하지만 팀 내 DDD에 대한 경험이 부족한 경우, 오히려 복잡한 설계로 인해 효율성이 떨어질 수 있다.</p>
<p>e.g.</p>
<ul>
<li><p>Bounded Context를 잘못 정의하면 경계 간 의존성이 얽히면서 시스템이 복잡해지고, 이는 유지보수성에 악영향을 미친다.</p>
</li>
<li><p>또한, 전술적 빌딩 블록(애그리거트, 엔터티, 값 객체, 도메인 이벤트, …)을 잘못 적용하면 모델이 지나치게 세밀하거나, 반대로 지나치게 추상적이게 될 수 있다.</p>
</li>
</ul>
<blockquote>
<h3 id="heading-ddd-3">팀원 모두가 DDD를 학습해야한다 (러닝 커브 비용)</h3>
</blockquote>
<p>DDD를 팀에 도입하려면 모든 구성원이 DDD의 개념과 이를 실무에 적용하는 방법을 이해해야한다.</p>
<p>DDD는 도메인 모델링, 보편 언어, 바운디드 컨텍스트, 컨텍스트 매핑 등 많은 새로운 개념을 학습해야한다. 엔지니어뿐만 아니라, PO, PD, Domain Expert 등 비개발자 구성원도 도메인 중심의 사고방식에 익숙해져야 하기 때문에 학습 범위가 넓다. 따라서 이러한 학습 과정에서 생산성이 일시적으로 하락할 가능성이 높아지며, 팀원들의 반발을 살 수 있다.</p>
<blockquote>
<h3 id="heading-64e66mu7j24ioyghousuoqwgcdtmzxrs7tqsiag7ja066c164uk">도메인 전문가 확보가 어렵다</h3>
</blockquote>
<p>DDD의 가장 큰 핵심은 도메인 모델링이며, 이를 위해 도메인 전문가(Domain Expert)와의 긴밀한 협력이 필수적이다.</p>
<p>도메인 전문가가 없다면, 개발자가 도메인을 완전히 이해하지못하고 추측에 의존하여 설계를 하게된다. 또는 도메인 전문가가 있다고 하더라도, 그들의 시간을 확보하거나 협업을 이끌어내는 일이 어려울 수 있다.</p>
<p>특히, 소규모 팀일 수록 도메인 전문가를 따로 두기 어려운 경우가 많다. 이로인해, 도메인의 전문 지식이 산발적으로 흩어져 있을 때 도메인 모델링의 품질이 낮아질 수 있고, 도메인 전문가가 모델링 과정에 충분히 참여하지못하면 개발자와 도메인 간의 괴리가 생기고 잘못된 설계로 이어질 가능성이 커진다.</p>
<p>그래서 도메인 전문가를 확보하기 어려운게 일반적인 환경이다보니, 팀 내 PO, PD, Sales, Developer 등 모두가 내부의 지식을 최대한 축적하기 위해 시장 고객과 이야기를 하며 전문성을 키워나가는것이 중요하다 생각한다. (이 때, 문서화, 위키를 적극 활용하는것이 중요하다.)</p>
<blockquote>
<h3 id="heading-7kgw7kebiousuo2zloyzgoydmcdrtodtlantlzjsp4dslyrsnlzrqbqg7kcb7jqp7j20ioywtougteulpa">조직 문화와의 부합하지않으면 적용이 어렵다</h3>
</blockquote>
<p>DDD는 단순한 기술적 도구가 아니라, 비지니스와 기술의 경계를 허물고 팀의 협업을 중심으로 하는 접근 방식이다.</p>
<p>기존에 빠르게 결과물을 내놓는데 익숙한 팀이거나, 개발 속도를 중시하는 조직에서는 DDD의 점진적 설계 방식이 받아들여지기 어려울 수 있다. 특히 비지니스와 기술 간의 장벽이 존재하거나, 팀 간의 협업이 원활하지 않은 환경에서는 DDD가 성공적으로 정착하기 어렵다.</p>
<p>e.g.</p>
<ul>
<li><p>도메인 모델을 정의하고, 보편언어를 설정하는 과정(지식 탐구)에 시간 투자하는것을 조직이 낭비로 간주할 가능성이 있다.</p>
</li>
<li><p>팀 간 소통 문화가 부족하거나, 개발자들이 비지니스에 관심을 두지 않은 경우 DDD의 효과는 크게 줄어들 수 밖에 없다.</p>
</li>
</ul>
<p>필자는 개인적으로 조직의 특성, 목적, 목표을 존중하는것도 팀원의 몫이라고 생각한다. DDD를 활용하고싶지만 조직의 목표에 맞지않는다면 가감히 포기하는것도 나쁘지않다 생각한다. 그런데 팀의 목표를 이루기위해 DDD가 적절하다고 생각된다면, 조직의 리더쉽과 기대효과에 대해 많은 논의를 거치며 설득해 나가는 과정을 겪는것도 좋다 생각한다.</p>
<blockquote>
<h3 id="heading-ddd-4">복잡하지 않은 시스템에 DDD는 오히려 복잡도가 증가한다. (오버 엔지니어링)</h3>
</blockquote>
<p>DDD는 복잡한 비즈니스 문제를 해결하기 위해 고안된 강력한 설계 방법론이다.</p>
<p>단순한 비지니스 로직이나 작은 규모의 시스템에서는 DDD를 도입하는것이 오히려 오버 엔지니어링이 될 수 있다. 복잡하지 않은 시스템에서는 CRUD 중심의 데이터 접근 방식이 훨씬 더 효율적일 수 있다.</p>
<p>단순한 시스템에 적용하다보면 아래와 같은 문제를 야기할 수 있다.</p>
<p><strong>불필요한 복잡도 증가</strong></p>
<ul>
<li>도메인을 세분화하고 경계를 나누는 작업은 비용이 많이 들며, 작은 시스템에서는 그 자체가 불필요한 복잡도를 초래한다.</li>
</ul>
<p><strong>생산성 저하</strong></p>
<ul>
<li>빠르게 배포하고 피드백을 받아야 하는 초기 MVP 단계에서 DDD를 도입하면 개발 속도가 저하될 수 있다.</li>
</ul>
<p><strong>관리 부담</strong></p>
<ul>
<li>전술적 설계(Aggregate, Entity, Value-Object 등)를 적용하려는 시도가 오히려 코드를 이해하기 어렵게 만들 수 있다.</li>
</ul>
<h2 id="heading-66ei7lmy66mw">마치며</h2>
<p>DDD는 강력한 도구이지만, 이를 효과적으로 활용하려면 팀과 조직이 충분히 납득하고 준비되어있어야한다. 팀의 경험 부족, 학습 곡선의 높음, 도메인 전문가의 확보 어려움 등 DDD 적용이 어려워질 수 있다. 하지만 DDD의 특성과 장점들을 속한 조직에 투영시켜보고 도움이 된다 판단된다면, 점진적으로 팀원들과 합심하여 도입해보는것이 좋을것같다 생각한다.</p>
<p>마지막으로 <code>DDD는 만능 해결책이 아니다</code>라는 점을 강조하며 글을 끝내본다.</p>
]]></content:encoded></item></channel></rss>