<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>lim_dohyeon.log</title>
        <link>https://velog.io/</link>
        <description>성장을 즐기는 사람 🧍</description>
        <lastBuildDate>Sun, 04 Oct 2026 16:40:04 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>lim_dohyeon.log</title>
            <url>https://velog.velcdn.com/images/lim_dohyeon/profile/93f35dbe-dd45-4154-812b-de3bbab41160/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. lim_dohyeon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lim_dohyeon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Stateful , Stateless & Fan Out]]></title>
            <link>https://velog.io/@lim_dohyeon/Stateful-Stateless-Fan-Out</link>
            <guid>https://velog.io/@lim_dohyeon/Stateful-Stateless-Fan-Out</guid>
            <pubDate>Sun, 04 Oct 2026 16:40:04 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/d57d221a-3a1c-404e-9fa0-12c4daacdace/image.png" alt=""></p>
<h1 id="state">State</h1>
<p>백엔드 공부를 하며 각 서버가 state를 가지는가 ? 상태를 가지는가 ? 에 대한 고민을 해본적이 없었다.</p>
<p>아니 사실은 state가 뭔지도 잘 몰랐다. 스프링 공부를 깊게 하게 되면서 서버는 state를 가지는것이 좋은 서버가 아니라는것을 배우게 되면서 그렇다면 여태까지 내가 빌드한 서버들은 모두 state를 가지는가? 에 대한 물음표가 생기게 되었다.</p>
<blockquote>
<p>🍃 스프링의 Singleton 디자인패턴과 비슷한 느낌이다. 상태를 가지는 서버가 여러개있다면, 동시성 제어의 어려움부터 시작해 운영이 복잡해질 것이고 무엇보다 Scale-out 수평적 확장에 무리가 있기 때문에 좋은 설계가 아니다.</p>
</blockquote>
<h2 id="그렇다면-state란-무엇인가-">그렇다면 state란 무엇인가 ?</h2>
<p>state는 단어의 뜻 그대로 상태를 말한다. </p>
<p>싱글톤 패턴이 아니라 하나의 클래스에 대하여 여러개의 인스턴스가 존재할때, 각 인스턴스마다 고유한 상태 (변수 , 메타 데이터 등등 ) 을 가질 것이다.</p>
<p>클래스와 인스턴스 범위에서 보게되면 , 인스턴스의 로컬 변수가 state를 말할 수도 있고,</p>
<p>보다 넓은 서버 범위에서 보게되면 세션을 유지하는 경우 state를 가진다 말할 수 있고, <del>Scheduler  정해진 시각에 어떤 기능을 수행하는 경우도</del> state를 가진다고 말할 수 있을것이다.</p>
<blockquote>
<p><em><strong>State : 과거의 상호작용 결과에 따라 미래의 요청 처리 결과가 달라지게 만드는 정보</strong></em></p>
</blockquote>
<p>보통의 stateless 한 서버들은 상호작용의 결과를 서버에서 가지고 있는것이 아니라, 서버들간의 공유된 DB에 저장해두거나, Redis 와 같은 인메모리와 소통할 수도 있을것이다.</p>
<p>다시 두가지의 차이점을 정의해보자면</p>
<p><strong>Stateful Server</strong> : 해당 서버가 이전 요청에 대한 상태를 저장해, 장애가 발생했을때 다른 서버가 처리하기 힘든 서버.
<strong>Stateless Server</strong> : 해당 서버가 장애가 발생해도 다른 서버가 그 요청을 처리할 수 있는 서버.</p>
<h1 id="stateful">Stateful</h1>
<p>그렇다면 상태를 유지해야하는 stateful 서버로 구현해야하는 경우는 어떤 경우가 있을까 ? </p>
<ul>
<li><p>클라이언트가 세션을 유지해야하는 WebSocket 서버</p>
</li>
<li><blockquote>
<p>실시간 통신이 이루어지기 위해서는 클라이언트의 세션이 계속 유지되어야한다.</p>
</blockquote>
</li>
<li><p>대용량 파일 업로드 처리</p>
</li>
<li><blockquote>
<p>한번의 요청으로 처리하지 못할정도의 대용량 파일의 경우에도 클라이언트와 서버가 대용량 업로드가 끝날때까지 상태를 유지해야하는 경우다.</p>
</blockquote>
</li>
<li><p>Stream 처리 서버</p>
</li>
<li><blockquote>
<p>Kafka Streams, Flink 같은 시스템에서 지난 5분간 사용자별 요청 수, 누적 합계, window aggregation 등을 로컬 state store에 유지하면 Stateful processing이다.</p>
</blockquote>
</li>
</ul>
<ul>
<li>Scheduler 기능</li>
<li><blockquote>
<p>정해진 시간에 연산이 수행되는 경우, 모든 서버들이 연산을 수행하게 되면 N번 연산이 수행될 수 있을것이다. 그래서 Redis , DB와 같은 공유 시스템에 상태를 외부화해야한다. <em><strong>그래서 서버 자체는 stateless 상태가 존재하긴 하지만, 서버에는 상태가 없다.</strong></em></p>
</blockquote>
</li>
</ul>
<h1 id="scale-out-수평적-확장">Scale-Out 수평적 확장</h1>
<p>인프라를 구성할때에 수평적 확장을 항상 고려해야한다. </p>
<p>확장을 고려하지 않은 설계는 언제 트래픽이 몰려 서버를 확장해야할지 모르는데, 서비스의 성장을 전혀 염두에 두고 있지 않은 나쁜 설계라고 생각한다.</p>
<p>그리고 수평적 확장이라함은 서버 인스턴스 갯수를 늘린다는 것인데, 단순히 서버만 늘린다고 TPS Throughput 이 증가하지는 않을 것이다. 당연히 그에 상응하는 DB도 복제가 되야할 것이다. </p>
<p>DB 복제와 샤딩에 대해서는 이번 포스트에서 다루기에는 무거운 주제이니 인스턴스의 수평확장만을 고려해보면 , stateless 서버는 확장에 전혀 어려움이 없어 보인다. 서버들 모두가 상태를 저장하고 있지 않으니, 수평적 확장을 하여서 상태를 전달받거나 전처리를 해야하는 과정이 없다.</p>
<p>stateful 서버는 당연하게도 확장에서 신경써야할 부분이 많을 것이다.</p>
<blockquote>
<p><em>그렇다면 무조건 state, stateless 서버는 분리되어야하나 ?</em></p>
</blockquote>
<p>아니다. 모놀리식 서버에서 state , stateless 기능을 모두 다 가지고 있을 수 있다.</p>
<p>확장의 경우에는 서버 앞단의 API Gateway 에서 state, stateless 요청을 다르게 받아서 각 기능들의 요청에 따라 scale-out 을 결정하게 하는 구조로 두면 된다.</p>
<h1 id="thunder-heard--fan-out">Thunder Heard , Fan-out</h1>
<pre><code>Post Service
     │
     │ CommentCreated
     ▼
Event / Message Layer
     │
 ┌───┼───────────────┐
 ▼   ▼               ▼
WS GW 1            WS GW 2           WS GW 3
 │                   │                 │
 ├─ User A           ├─ User D         ├─ User G
 ├─ User B           ├─ User E         ├─ User H
 └─ User C           └─ User F         └─ User I

       각 Gateway 내부에서 Local Fan-out</code></pre><p>Websocket 기능을 가진 gateway를 통한 MSA 구조를 예를 들어 설명해보자.</p>
<p>어떤 Event가 발생하게 되면 그 이벤트의 구독자인 gateway가 이벤트가 발생한것을 &#39;Heard&#39; 듣게 되면,N개의 gateway가 DB Read연산을 수행해 갑작스러운 QPS 의 기하급수적 상승이 있을 수 있다. (Thunder Heard 문제)</p>
<p>그래서 EDA, Redi Stream을 활용한 Local-Fan-Out구조로 위의 문제를 해결하는 설계이다.</p>
<p>Event는 페이지를 새로고침하거나, 사용자에게 줘야하는 최소의 정보를 가진 payload를 담아 발행된다.</p>
<p>그럼 그 Event의 구독자인 WS gateway는 DB를 읽는 것이 아니라, Event Payload를 읽고 WS gateway와 연결된 N개의 클라이언트들에게 정보를 전달한다.</p>
<p>이 구조로 실질적인 DB 연산은 이벤트 발행에 따른 쓰기 연산 한번이 일어난 것이다.</p>
<p>이러한 설계는 DB 부하를 줄이는 것뿐 아니라 Stateful한 WebSocket 서버를 수평 확장할 때도 장점이 있다. 새로운 WebSocket Gateway 인스턴스가 추가되더라도 해당 인스턴스는 새롭게 연결되는 Socket만 관리하면 되고, 이벤트를 전달받은 뒤 자신이 보유한 연결에 대해서만 Fan-out하면 된다. 특정 사용자가 어느 Gateway에 연결되어 있는지를 중앙 DB에서 매번 조회할 필요가 없으며, Gateway 내부의 메모리 접근만으로 실제 전달 대상자를 결정할 수 있다.</p>
<h2 id="마무리">마무리</h2>
<p>Stateful 서버라고 해서 수평적 확장이 불가능한것은 아니다.</p>
<p>새로 생성된 인스턴스에게 세션을 나누어주고, Sticky Session을 유지시키는 오버헤드가 있어서 그렇지, 완전히 불가능한것은 아니다.</p>
<p>보통의 사이드 프로젝트에서 MSA 를 적용하는일은 거의 없기에, stateless, stateful을 잘 구분하여 수평적 확장에 용이한 설계를 해야겠다.</p>
<p>사실 마구잡이로 확장한다고해서 해결되는것은 아니며 사실 DB sharding , DB 복제 및 동기화 등 더 어려운 문제가 나를 기다린다 .. 🥹</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[EDA - Event Driven Architecture]]></title>
            <link>https://velog.io/@lim_dohyeon/EDA-Event-Driven-Architecture</link>
            <guid>https://velog.io/@lim_dohyeon/EDA-Event-Driven-Architecture</guid>
            <pubDate>Sat, 03 Oct 2026 08:58:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/e66afad9-f692-42ba-8613-505a89d99cb5/image.png" alt=""></p>
<h1 id="아키텍처에-대한-고민">아키텍처에 대한 고민</h1>
<p> UMC라는 연합동아리에서 2번의 백엔드 개발자로써 참여하게 되면서 백엔드 전체적인 흐름과 구조를 파악할 수 있게 되었고,</p>
<p> 최근 들어서는 큰 구조와 틀을 알게 되고 나서 , 이 구조와 틀을 다른 방식으로 구성해보고 싶다는 생각을 많이 하였다.</p>
<p>두번의 프로젝트 모두 하나의 서버에 모든 도메인 로직과 기능들이 구현되어있는 모놀리식 서버 구조였고, 하나의 HTTP 요청에 대하여 하나의 트랜잭션으로 모든 도메인들이 처리되는 구조였다.</p>
<blockquote>
<p>&quot;확장성 ❌ , 거대한 트랜잭션 안의 동기적 요청 , 도메인간 강결합&quot;</p>
</blockquote>
<p>모든 요청들이 동기적으로 연결되는 것에 대한 문제점을 인지하여 Event-Driven Architecture 에 관해 공부해보았다.</p>
<h1 id="확장성">확장성</h1>
<p>좋은 코드와 좋은 설계는 여러가지 기준이 있겠지만, </p>
<blockquote>
<p>확장에 열려 있으며, 변경에는 닫혀있다.
구현에 의존하지 말고, 추상화에 의존하라</p>
</blockquote>
<p>라는 것이 크게 가장 신경쓰는 부분들인것같다.
2번의 프로젝트에서의 MVC 아키텍처 , DDD 아키텍처 모두 도메인간의 결합을 최소화 하고자 Port , Adapter , Usecase간의 인터페이스를 이용한 도메인간 소통을 구현해 보았는데, 도메인이 다른 도메인을 호출해야하는 일들이 늘어날때마다 인터페이스를 늘려줘야한다는것은 해결하지 못하는 문제였다.</p>
<p><del>매번 동료의 usecase추가 요청이 지겨웠다.</del></p>
<h1 id="그래서-eda란-">그래서 EDA란 ?</h1>
<p>앞에서 서술한 문제점 , 서론에서 보았던 문제를 일부분 해결할 수 있는 Event-Driven Architecture이다.</p>
<p>도메인간의 소통의 방식을 설계한 Architecture이다.</p>
<p>가장 다른 점은 도메인에서 이벤트를 발행
그 이벤트를 동시 다발적으로 필요한 Consumer 도메인들이 그 이벤트를 소비한다.</p>
<p>그래서 Event의 Payload를 정해두고, 어떤 Event를 소비하고 발행할것인지 Contract를 정한 후 , Event Hamndler 을 도메인별로 정의해두면 , 도메인간의 결합을 이벤트를 통한 소통으로 변경하여 Loosely Coupling을 구현할 수 있게 되는것이다.</p>
<pre><code>EDA 관점

OrderCreated
   ↓
Message Broker
   ├─ Inventory Service Event Handler
   ├─ Notification Service Event Handler
   └─ Analytics Service Event Handler</code></pre><p>이 아키텍처의 가장 큰 장점은 비동기 이벤트 처리를 통해, 트랜잭션을 가볍게 가져가며, 반드시 처리되어야하는 후속 이벤트 처리들을 남겨둠으로써 사용자에게 보다 더 빠른 지연시간을 제공해줄 수 있다는 장점을 가진다.</p>
<h2 id="eda가-적합한-경우">EDA가 적합한 경우</h2>
<ol>
<li>하나의 이벤트에 대하여 여러개의 도메인이 반응해야하는경우.
다시말해, 주문 발행이라는 이벤트에 대하여 알림 도메인 , 광고 도메인 , 유저 도메인 등등 다양한 도메인에서 그 이벤트를 병렬적으로 처리해야하는 경우 적합하다.</li>
<li>서비스간의 통신에서 동기적으로 통신해야하는경우 (HTTP 요청 , gRPC 통신 , 다른 서비스의 응답값이 반드시 필요한)가 많은 경우에는 적합하지 않다.
Ex) 유저에게 즉시 결제 성공과 결제 실패를 반환해줘야하는 경우.<h1 id="eda--msa-">EDA = MSA ?</h1>
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/3da1615e-edfd-43c6-80fa-c25b6d4ecc27/image.png" alt=""></li>
</ol>
<p>처음에는 EDA 와 MSA를 동일한 설계구조라고 생각했다.</p>
<blockquote>
<p>🤔 당연히 MSA 마이크로 서비스 환경에서는 EDA가 강제되는 것 아닌가?</p>
</blockquote>
<p>하지만 분명히 두 아키텍처 설계의 목표는 다르다는 것.</p>
<p>EDA는 도메인간의 소통의 방식을 설계하고 , 도메인간의 결합을 Loosely하게 하기 위한 아키텍처</p>
<p>MSA는 서비스마다 서버를 독립적을 하나씩 구성해 서비스의 독립성을 높이고, 시스템 경계를 정확히 하는데에 목적이 있다. MSA를 통해 독립적인 서비스들의 확장을 가져갈 수 있다는 목적 또한 가진다.</p>
<p>그리고 MSA 마이크로 서비스에서 모두 EDA 를 통한 서비스간의 소통을 하는것은 아니라는것이다.</p>
<p>그 반대도 마찬가지이다. EDA도 모두 MSA로 독립된 서버에서 무조건 사용되는것이 아니라 모놀리식 서버에서 도메인간의 처리를 비동기 이벤트 기반으로 처리할 수 있게 설계할 수 있다.</p>
<p>이 집합관계의 차이를 이해하게 되면 그 차이와 아키텍처의 목적이 다르다는 것을 알 수 있다.</p>
<p><del>하지만 결국 이 둘은 같이 가는 느낌이기는 하다.</del></p>
<h1 id="message-broker">Message Broker</h1>
<p>EDA에서 필수 불가결하게 중요한 것이 이벤트들을 관리하는 기능을 추가해줘야한다는 것이다.</p>
<p>이 Message Broker 의 기능은 메세지 저장 및 이벤트 재발행 등의 책임들을 가진다.</p>
<p>이 Message Broker 의 대표적인 솔루션으로 언급되는것이 바로 RabbitQ, Kafka 이 두개이다.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/9f6a4164-a3b3-465a-8a75-84d1dbae6e94/image.jpeg" alt="">
그 중에서도 대규모 데이터 파이프라인에서 가장 주류를 이루는 것은 카프카.
카프카는 대용량 이벤트 스트림 처리, 메시지 보존 및 재처리, 수평 확장성의 장점을 가진다.</p>
<h2 id="message-broker-의-책임">Message Broker 의 책임</h2>
<ul>
<li><p>메시지 전달: Producer가 발행한 이벤트를 적절한 Consumer에게 전달
Producer–Consumer 결합도 감소: Producer는 누가 소비하는지 몰라도 됨</p>
</li>
<li><p>비동기 처리 지원: Consumer가 즉시 응답하지 않아도 메시지를 나중에 처리 가능</p>
</li>
<li><p>버퍼링: 순간적으로 이벤트가 몰려도 Consumer가 처리 가능한 속도로 소화할 수 있도록 완충</p>
</li>
<li><p>내구성 보장: 브로커에 따라 메시지를 디스크에 저장해 장애 후에도 재처리 가능
재전송 / 재처리 지원: Consumer 처리 실패 시 retry나 replay 가능</p>
</li>
<li><p>순서 보장: Kafka의 partition처럼 특정 범위 내 이벤트 순서를 보장할 수 있음</p>
</li>
<li><p>Consumer 분산 처리: 여러 Consumer 인스턴스에 메시지를 나눠 처리해 수평 확장 지원</p>
</li>
<li><p>Fan-out: 하나의 이벤트를 여러 Consumer가 각각 독립적으로 받을 수 있음</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flyway Migration 문제]]></title>
            <link>https://velog.io/@lim_dohyeon/Flyway-Migration-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@lim_dohyeon/Flyway-Migration-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 20 Jul 2026 16:55:17 GMT</pubDate>
            <description><![CDATA[<h1 id="마이그레이션이란">마이그레이션이란?</h1>
<blockquote>
<p>정의 : 기존의 데이터, 소프트웨어, 운영체제(OS) 등의 시스템 환경을 손실 없이 새로운 환경이나 플랫폼으로 안전하게 옮기는 과정</p>
</blockquote>
<p>보통 마이그레이션은 정의에서 알 수 있듯이, 하위 계층의 구현사항이 바뀌게 될때 예를들어 Mysql 에서 Postgre 로의 이동이 일어날때 마이그레이션 한다고 말할 수 있을것이다.</p>
<p>하지만 이번 포스트에서 다루어 볼 것은 이러한 의미의 마이그레이션은 아니고, 개발과정에서의 DB변경사항을 본래 DB의 데이터들은 유지하면서도 변경사항을 적용시키는 것을 말한다.  </p>
<p>데이터베이스를 설계하며 , 데이터베이스를 수정해야하는 경우가 많이 생긴다.</p>
<p>마이그레이션은 민감하게 설정해줘야한다. </p>
<p>이미 운영 DB에 많은 데이터들이 들어있을 것이며, 관계형 데이터베이스에서는 복잡하게 데이터들간의 관계가 형성되어있어 더더욱 민감하게 그리고 안전하게 마이그레이션을 진행해야한다.</p>
<h1 id="왜-자꾸-마이그레이션이-엉키지-😩">왜 자꾸 마이그레이션이 엉키지?? 😩</h1>
<p>Springboot 프로젝트를 절반정도 진행되고 있는 와중에, </p>
<p>생각보다 초기 DB로부터 수정사항들이 많이 생겨났고, 마이그레이션 파일이 29개나 생겨버렸다..
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/67474361-7c5e-416f-b11c-6583a1c5d7dd/image.png" alt="">
<del>이 수많은 마이그레이션 파일을 보면 초기에 데이터베이스 설계를 잘못했나 ? 라는 생각이 저절도 들게된다 😅</del> </p>
<p>사실 초반에는 많은 문제가 없었다. 당연히 수정사항이 적고, 운영 DB에 테스트 데이터들도 적으니 문제가 없었으나, </p>
<p>여러가지 원인들로 인해 Flyway Migration validation 이 계속 오류가 터졌다.</p>
<p>운영 DB에 마이그레이션 history, 즉 여태까지 마이그레이션 진행이 되었던 이력을 보고, 변경사항이 있는 경우 마이그레이션을 진행하게 되는데, 마이그레이션이 실패하게되면 Springboot 서버가 run 하지 않는다..</p>
<p>그래서 마이그레이션이 실패하게 되면 ec2에서 서버가 무한히 재시작하는 상황이 발생된다.
(당연하게도 swagger도 열리지 않아 프론트엔드에서 연동도 못하는 불상사가 일어난다🥹.)</p>
<p>이제까지 일어났던 마이그레이션 오류의 원인들을 나열해보자면 ,,</p>
<h2 id="마이그레이션-버전-문제">마이그레이션 버전 문제</h2>
<p>다른 프로젝트, 그리고 다른 개발자들도 가장 흔하게 겪어볼 수 있는 오류이지 않을까 싶다 </p>
<p>이 버전 문제의 원인은 !! 이미 운영 DB에 더 높은 , 더 최신 버전의 마이그레이션이 먼저 반영이 되어있고, 더 낮은 버전, 더 옛날 버전의 마이그레이션이 반영이 안되어있는 경우이다.</p>
<p>보통, 마이그레이션 파일의 네이밍으로 버전을 구분하는데, </p>
<pre><code class="language-sql">V1__init.sql

V2__create_user.sql

V3__add_index.sql

V4__add_display.sql</code></pre>
<p>이 방식처럼 직관적으로 네이밍하는 방법이 있다.
이는 여러명의 개발자가 동시에 많은 마이그레이션을 하게되면 당연히 동일한 버전으로 마이그레이션을 올려,충돌도 많이 나는 문제가 있다.</p>
<pre><code class="language-sql">V202607010001__init.sql
V202607010002__user.sql
V202607150001__display.sql</code></pre>
<p>날짜와 마이그레이션 내용을 합해 네이밍하는 방식이 있다. 
이는 위의 방식보다 당연히 충돌이 안난다는 장점이 있으나, 이 날짜 순서대로 마이그레이션을 진행해야하는 제약이 생긴다.</p>
<blockquote>
<p>즉, 26일에 생성된 PR의 마이그레이션이 코드리뷰가 늦어져, merge가 늦어진 이후, 28일에 생성된 PR이 먼저 merge되면 26일에 생성된 PR이 옛날 버전이 되어서 Flyway Migration 오류가 터진다!!</p>
</blockquote>
<h2 id="운영-db와의-차이">운영 DB와의 차이</h2>
<p>Flyway는 <strong>“모든 환경이 동일한 Migration을 동일한 순서로 실행했다”</strong>는 것을 전제로 동작한다.</p>
<p>하지만 운영 DB가 이 전제를 벗어나면 Schema Drift(스키마 불일치) 가 발생하고, Migration 실행 중 오류가 발생할 수 있다.</p>
<ol>
<li>Hibernate가 운영 DB를 자동으로 변경한 경우</li>
</ol>
<p>운영 환경에서 ddl-auto=update 또는 create가 설정되어 있으면 Hibernate가 엔티티를 기준으로 테이블을 직접 수정한다.</p>
<p><strong>이 과정에서 FK, Index 등의 이름이 Hibernate 규칙으로 자동 생성될 수 있으며, 이후 Flyway가 예상하는 이름과 달라질 수 있다.</strong></p>
<pre><code class="language-sql">Flyway 예상 FK
FK_ARCHIVEARTIST_CREATOR
실제 운영 DB FK
FK8dfk29a8sd...

결과적으로

ALTER TABLE ArchiveArtist
DROP FOREIGN KEY FK_ARCHIVEARTIST_CREATOR;</code></pre>
<p>와 같은 Migration이 실패하게 된다.</p>
<p>운영 환경에서는 반드시</p>
<pre><code class="language-java">spring.jpa.hibernate.ddl-auto=validate</code></pre>
<p>이 원인때문에 운영 DB와 마이그레이션 파일의 FK네이밍이 맞지 않아서 DROP FK 명령어가 실행되지 않았던 것이다.</p>
<hr>
<ol start="2">
<li><strong>오래된 Build 또는 Docker Image가 배포된 경우</strong></li>
</ol>
<p>Git에는 최신 Migration이 존재하지만</p>
<ul>
<li>오래된 JAR</li>
<li>Docker Cache</li>
<li>Clean Build 누락</li>
</ul>
<p>등으로 인해 이전 Migration이 포함된 이미지가 운영에 배포될 수 있다.</p>
<p>이를 방지하기 위해</p>
<pre><code class="language-java">./gradlew clean build</code></pre>
<p>를 수행하고</p>
<p>배포되는 JAR 안의 Migration 파일도 검증하는 것이 좋다.</p>
<p>이 경우가 가장 디버깅이 어려운 경우이다. 나는 분명히 수정해서 이미지 빌드 이후 푸시했는데, 왜 오류가 수정이 안될까라는 생각이 들때가 있는데 이 때 이전 이미지가 배포되지는 않았는지 이전 배포의 이미지를 사용하고 있지는 않은지 , clean build 를 안하고 있는것은 아닌지 확인해야할 것이다.</p>
<hr>
<ol start="3">
<li>운영 데이터 상태가 다른 경우</li>
</ol>
<p>이 경우에는 운영 DB에 예를 들어 UNIQUE CONSTARINT 를 추가하려고 하는 상황속에, 이미 운영 DB에 중복된 데이터가 많은 경우, 마이그레이션이 실패한다.</p>
<p>그 이외에도 NOT NULL 칼럼을 추가할때에도 모든 존재하는 데이터에 NOT NULL이기 때문에 값들을 추가해줘야하는 것이다.
즉, 다시말해서 NOT NULL 컬럼을 기존 테이블에 추가하는 경우, 이미 존재하는 모든 Row는 해당 컬럼의 값을 반드시 가져야 한다. 따라서 기존 데이터에 대한 값 채우기(Backfill) 과정 없이 NOT NULL 컬럼을 추가하면 Migration이 실패할 수 있다.</p>
<blockquote>
<p>일반적으로 NULL 허용 컬럼 추가 → 기존 데이터 Backfill → NOT NULL 변경의 순서로 Migration을 수행하여 운영 환경에서의 Migration 실패를 방지</p>
</blockquote>
<h1 id="해결책">해결책</h1>
<p>기존 CD 파이프라인에서는 새로운 Docker 이미지를 배포한 뒤, 애플리케이션이 시작되면서 운영 DB에 Flyway Migration을 바로 수행하는 구조였다. 이 경우 운영 DB의 실제 스키마와 Migration이 기대하는 스키마가 조금이라도 다르면 Migration이 실패하고, flyway_schema_history에 success = 0이 기록되면서 애플리케이션이 정상적으로 기동하지 못하는 문제가 발생할 수 있다.</p>
<p>이를 해결하기 위해 운영 DB에 Migration을 실행하기 전에 운영 DB와 동일한 스키마를 가진 임시 MySQL 환경에서 Migration을 먼저 검증하는 단계를 CD 파이프라인에 추가하였다.</p>
<p>새로운 배포 흐름은 다음과 같다.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/3622fdbb-50c6-4538-a704-a16e344b595e/image.png" alt=""></p>
<pre><code class="language-java">Build
        ↓
Migration Precheck
(운영 DB Schema 복제 + Flyway Migrate 테스트)
        ↓
Production Migration
        ↓
Application Deploy</code></pre>
<ol>
<li>Migration Precheck 단계 추가</li>
</ol>
<p>운영 DB의 Schema와 flyway_schema_history만 복제하여 임시 MySQL을 구성한 뒤, 해당 환경에서 실제 Flyway migrate를 수행한다.</p>
<p>이를 통해 다음과 같은 문제를 운영 배포 전에 사전에 발견할 수 있다.</p>
<ul>
<li>Foreign Key 이름 불일치</li>
<li>존재하지 않는 Column 또는 Index 삭제</li>
<li>이미 존재하는 Constraint 추가</li>
<li>NOT NULL 변경 실패</li>
<li>UNIQUE Constraint 충돌</li>
<li>MySQL 버전 또는 문법 차이</li>
</ul>
<ol start="2">
<li>Migration 성공 시에만 운영 DB 변경</li>
</ol>
<p>Precheck가 성공한 경우에만 운영 DB에서 Migration을 수행하도록 변경하였다.</p>
<p>만약 사전 검증에서 Migration이 실패하면 운영 DB에는 어떠한 변경도 발생하지 않으며, 이후 Application Deploy도 진행되지 않는다.</p>
<ol start="3">
<li>애플리케이션 배포와 DB Migration 분리</li>
</ol>
<p>기존에는 Spring Boot가 시작되면서 Flyway Migration도 함께 수행하였다.</p>
<p>개선 이후에는 Migration을 CD 파이프라인의 독립된 단계로 분리하여, Migration이 성공한 이후에만 새로운 애플리케이션을 배포하도록 변경하였다.</p>
<ol start="4">
<li>Migration 파일 보호</li>
</ol>
<p>추가적으로 CD에서 다음 사항을 검증하도록 구성하였다.</p>
<ul>
<li>이미 적용된 Migration 파일 수정 또는 삭제 금지</li>
<li>Migration Version 중복 검사</li>
<li>Migration 파일명 규칙 검사</li>
<li>위험한 DDL(DROP COLUMN, DROP FOREIGN KEY 등) 감지</li>
</ul>
<p>이를 통해 운영 환경과 Git Repository의 Migration 이력이 항상 동일하게 유지되도록 하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Fastapi와 Lamda 서버리스 컴퓨팅의 단점에 대하여]]></title>
            <link>https://velog.io/@lim_dohyeon/Fastapi%EC%99%80-Lamda-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EC%BB%B4%ED%93%A8%ED%8C%85%EC%9D%98-%EB%8B%A8%EC%A0%90%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@lim_dohyeon/Fastapi%EC%99%80-Lamda-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4-%EC%BB%B4%ED%93%A8%ED%8C%85%EC%9D%98-%EB%8B%A8%EC%A0%90%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Wed, 15 Jul 2026 05:01:28 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/c8b320b5-2148-403f-a3fe-225ffd03571f/image.png" alt=""></p>
<h1 id="개요">개요</h1>
<p>Eum (이음) 개발 과정에 있어서 데모데이 이후, develop 하는 과정에 있어서 골칫거리가 하나 있었다.</p>
<p>그것은 바로 서버비용...! </p>
<p>언제나 그렇듯 서버비용은 대학생 개발자에게 가장 큰 적이다.</p>
<p>AWS 비용이 한달에 34만원 가량이나 나왔던 그 상황을 보고 우리는 당황하지 않을 수 없었다.</p>
<p>당연히 우리는 AI 관련 기능들을 모두 도맡아서 구현하신 동료를 믿었으나, 데모데이 당시에 어떤 분께서 서비비용을 듣고, </p>
<blockquote>
<p><em>&quot;비정상적으로 너무 서버비용이 많이 나왔다. 한번 확인해볼 필요가 있다.&quot;</em></p>
</blockquote>
<p>라는 말을 해주셨고, 우리는 서버리스, AWS 의 Lamda를 이용한 음성분석 방식을 버리고, AI 음성분석을 위한 서버를 하나 더 띄우기로 하였다.</p>
<p>아래에는 람다 함수를 적용한 음성분석 서버 아키텍처이다. 
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/c4e4e590-e3fa-42ae-86db-616451220f1b/image.png" alt=""></p>
<h2 id="알게-된-사실-😅">알게 된 사실 ..😅</h2>
<p>최근에 학교에서 진행하는 온라인 교육으로 AWS CCP 자격증 공부를 하는 도중에,</p>
<p>람다 서비스에 대하여 알게되었는데 왜 서비비용이 많이 나왔는지 깨달았다 ..</p>
<p>람다 서비스는 서버리스로, 독립적인 서비스를 띄우는것이 아니라, 특정 상황, 특정 이벤트가 발생될때마다 실행되는 것이 람다 서비스이다.</p>
<p>즉 가장 람다 서비스가 적합한 구조는 요청이 적고, 트래픽이 몰리지 않는, 그리고 비동기 처리가 필요한 경우에 가장 적합한 것이다.</p>
<p>트래픽이 몰릴 경우 아래와 같은 문제가 발생하는데 ..</p>
<h3 id="warm-start-vs-cold-start">Warm start Vs Cold start</h3>
<pre><code>10:00 요청 → 환경 A 생성 → Whisper 로딩
10:01 요청 → 환경 A 재사용
10:30 환경 A 제거
11:00 요청 → 환경 B 생성 → Whisper 다시 로딩</code></pre><p>이렇게 람다 서비스에서 모델을 재사용하는 경우를 warm start라고 하며</p>
<p>환경 B처럼 새로운 환경을 구축해서 다시 모델을 로딩하는 경우를 Cold Start 라고 한다.</p>
<p>문제는 트래픽이 몰릴 경우 , 모든 요청에 대해서 모델을 로딩해야하는 경우가 생긴다는 것.</p>
<p>동시에 모든 요청에 cold start를 해버리면 서버비용도 많이 나오고 자원을 비효율적으로 사용중이라는것이다.</p>
<hr>
<p>그래서 이 람다 서비스는 24시간 띄워놓게 될 경우, 많은 요청이 몰릴 경우 매우매우 비효율 적이다. </p>
<p>효율이 사실 문제가 아니다. 돈이 엄청나게 많이 나가는 것이다.</p>
<p>이 구조로 설계한 동료분은 아마도 음성분석이 가끔 일어나는 이벤트정도라고 생각했던 것 같다 .. </p>
<p>하지만 이음 서비스는 음성분석이 메인 , 가장 주요한 기능이고, 프론트엔드와 백엔드 연동시에도 굉장히 많이 음성분석 테스트를 진행했던 터라 .. </p>
<p>사실상 24시간 람다 서비스가 구동되고 있었으며, 그래서 서버 비용이 많이 나왔던 것 같다</p>
<p><del>교훈 : 동료를 너무 믿지말자😩</del> </p>
<h1 id="fastapi-의-목적">Fastapi 의 목적</h1>
<p>Fastapi란 ? 파이썬 언어로 RestFul API 서버를 구축할 수 있는 프레임 워크를 말한다.</p>
<p>사실 파이썬이라는 언어는 굉장히 무거운 언어이다. 파이썬은 파이썬 인터프리터로 실행되는 언어이기에 다른 자바, c언어 , TS와 같은 언어에 비해 웹개발에서 선호되는 프로그래밍언어는 아니었다.</p>
<p>하지만 모든 서비스에서 AI가 빠지지 않는 지금, 파이썬의 강점이 더 부각되기 시작하였고, 이를 웹서비스에 적용시키기 위해 Fastapi의 필요성이 증가하고 있다는 것이다.</p>
<p>이음 서비스에서 Fastapi의 목적은 다음과 같다.</p>
<blockquote>
<p><strong>외부 API인 OpenAI와의 파이프라인 구축 및 벡터 코사인 유사도 계산을 통한 추천 시스템 구축</strong></p>
</blockquote>
<p>이제 백엔드에는 두개의 서버가 존재하는 것이다.</p>
<p>Nestjs 백엔드 서버 , Fastapi AI 파이프라인 구현한 서버.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/0b76d450-4dab-4f65-9e07-6e1d76d104f5/image.png" alt=""></p>
<pre><code>위의 그림 처럼 두개의 서버가 동일한 ECS안에서 로컬 통신으로 Nestjs 가 Fastapi를 호출하는 구조이다.</code></pre><p>동일한 DB를 공유하기 때문에, 책임분리를 위해서 Fastapi 서버는 DB에 데이터를 저장하거나 수정할 수 있는 권한을 주지 않았다.</p>
<p><strong>Fastapi는 Read only만 할 수 있게 제약을 걸어두어 , Nestjs 서버에서만 데이터 생성, 수정, 삭제를 진행 할 수 있게 하였다.</strong></p>
<h1 id="fastapi의-강점">Fastapi의 강점</h1>
<h2 id="📑-1-api-계약을-코드와-문서로-동시에-관리할-수-있다">📑 1. API 계약을 코드와 문서로 동시에 관리할 수 있다</h2>
<p>FastAPI의 가장 큰 장점 중 하나는 <strong>API 계약(Contract)</strong> 을 코드와 문서에서 동시에 관리할 수 있다는 점이다.</p>
<p>요청(Request)과 응답(Response)을 <strong>Pydantic Model</strong>로 정의하면,</p>
<ul>
<li>✅ Request Validation</li>
<li>✅ Response Validation</li>
<li>✅ 타입 힌트</li>
<li>✅ Swagger 문서</li>
<li>✅ 예제(Examples)</li>
</ul>
<p>까지 자동으로 생성된다.</p>
<p>예를 들어 다음과 같이 Pydantic 모델만 정의하면 된다.</p>
<pre><code class="language-python">class AnalyzeVoiceProfileRequest(BaseModel):
    voiceUrl: str</code></pre>
<p>그리고 API에서는</p>
<pre><code class="language-python">@app.post(
    &quot;/voice/analyze&quot;,
    response_model=AnalyzeVoiceProfileResponse,
)</code></pre>
<p>처럼 선언하기만 하면 Swagger 문서가 자동으로 생성된다.</p>
<h3 id="🎯-장점">🎯 장점</h3>
<ul>
<li>프론트엔드와 API 계약이 명확해진다.</li>
<li>NestJS 백엔드에서도 어떤 요청을 보내야 하는지 쉽게 확인할 수 있다.</li>
<li>응답 형태가 변경되면 문서도 함께 변경된다.</li>
<li>문서와 실제 코드가 달라지는 문제를 줄일 수 있다.</li>
</ul>
<p>즉, <strong>API 명세가 별도의 문서가 아니라 코드 자체가 되는 구조</strong>이다.</p>
<hr>
<h2 id="⚡-2-비동기-처리에-최적화되어-있다">⚡ 2. 비동기 처리에 최적화되어 있다</h2>
<p>AI 서버는 CPU 연산보다 <strong>외부 API를 기다리는 시간(I/O)</strong> 이 훨씬 많은 경우가 많다.</p>
<p>예를 들어 하나의 요청에서</p>
<ul>
<li>STT 수행</li>
<li>LLM 호출</li>
<li>Embedding 생성</li>
<li>Keyword 추출</li>
</ul>
<p>등이 함께 수행될 수 있다.</p>
<p>FastAPI는 <code>async/await</code>를 기본적으로 지원하기 때문에 이러한 작업을 효율적으로 처리할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-python">embedding_task = asyncio.create_task(create_embedding())
keyword_task = asyncio.create_task(extract_keywords())

await asyncio.gather(
    embedding_task,
    keyword_task,
)</code></pre>
<p>처럼 두 작업을 동시에 실행할 수 있다.</p>
<h3 id="❌-순차-실행">❌ 순차 실행</h3>
<pre><code class="language-text">Embedding
    ↓
Keyword
    ↓
완료</code></pre>
<h3 id="✅-병렬-실행">✅ 병렬 실행</h3>
<pre><code class="language-text">Embedding ─────┐
               ├── 완료
Keyword ───────┘</code></pre>
<p>이를 통해 전체 응답 시간을 크게 줄일 수 있다.</p>
<hr>
<h2 id="🤖-3-ai-기능을-메인-백엔드와-자연스럽게-분리할-수-있다">🤖 3. AI 기능을 메인 백엔드와 자연스럽게 분리할 수 있다</h2>
<p>프로젝트에서는 역할을 다음과 같이 분리하였다.</p>
<pre><code class="language-text">NestJS
├── 회원 관리
├── 인증
├── 비즈니스 로직
├── 채팅
└── API Gateway

            │

            ▼

FastAPI
├── STT
├── LLM
├── 임베딩 생성
├── 추천 시스템
└── 벡터 연산</code></pre>
<p>즉,</p>
<p>NestJS는 <strong>서비스 로직</strong>을 담당하고,</p>
<p>FastAPI는 <strong>AI 연산 전용 서버</strong> 역할을 수행하는 구조이다.</p>
<h3 id="🎯-장점-1">🎯 장점</h3>
<ul>
<li>AI 모델 교체가 쉽다.</li>
<li>Python 생태계를 그대로 사용할 수 있다.</li>
<li>AI 서버만 독립적으로 배포할 수 있다.</li>
<li>트래픽 증가 시 AI 서버만 Scale-Out 할 수 있다.</li>
</ul>
<p>마이크로서비스 구조를 구성하기에도 매우 적합한 방식이다.</p>
<hr>
<h2 id="🗄️-4-의존성-주입dependency-injection으로-db-관리가-깔끔하다">🗄️ 4. 의존성 주입(Dependency Injection)으로 DB 관리가 깔끔하다</h2>
<p>FastAPI는 <code>Depends()</code>를 통해 필요한 객체를 자동으로 주입받을 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-python">db: AsyncSession = Depends(get_db)</code></pre>
<p>처럼 작성하면 된다.</p>
<p>실제 DB 연결 생성 및 종료는</p>
<pre><code class="language-python">yield session</code></pre>
<p>으로 관리된다.</p>
<p>즉,</p>
<pre><code class="language-text">요청
    ↓
DB Session 생성
    ↓
API 실행
    ↓
자동 Session 종료</code></pre>
<p>개발자는 비즈니스 로직에만 집중하면 된다.</p>
<h3 id="🎯-장점-2">🎯 장점</h3>
<ul>
<li>DB 연결 누수를 방지할 수 있다.</li>
<li>코드가 단순해진다.</li>
<li>테스트하기 쉬워진다.</li>
<li>Session 생성 로직이 한 곳에서 관리된다.</li>
</ul>
<hr>
<h2 id="🧠-5-python-ai-생태계를-그대로-활용할-수-있다">🧠 5. Python AI 생태계를 그대로 활용할 수 있다</h2>
<p>FastAPI의 가장 큰 강점은 <strong>Python AI 생태계를 그대로 사용할 수 있다는 점</strong>이다.</p>
<p>프로젝트에서는</p>
<ul>
<li>OpenAI SDK</li>
<li>NumPy</li>
<li>SQLAlchemy</li>
<li>pgvector</li>
<li>Async PostgreSQL</li>
</ul>
<p>등을 함께 사용하고 있다.</p>
<p>예를 들어 추천 시스템에서는</p>
<pre><code class="language-python">cosine_similarity(...)</code></pre>
<p>를 이용하여 사용자 간 유사도를 계산하고,</p>
<p>PostgreSQL에서는</p>
<pre><code class="language-sql">embedding &lt;=&gt; queryVector</code></pre>
<p>연산을 이용하여 벡터 검색을 수행한다.</p>
<p>Python에서는 이러한 라이브러리를 매우 자연스럽게 사용할 수 있다.</p>
<h3 id="함께-사용할-수-있는-대표적인-라이브러리">함께 사용할 수 있는 대표적인 라이브러리</h3>
<ul>
<li>🤖 OpenAI SDK</li>
<li>🧠 Transformers</li>
<li>🎙️ Whisper</li>
<li>🚀 Faster-Whisper</li>
<li>📈 NumPy</li>
<li>🐼 Pandas</li>
<li>🔍 pgvector</li>
<li>🔥 PyTorch</li>
<li>📊 Scikit-learn</li>
</ul>
<p>AI 프로젝트에서는 Python 생태계 자체가 하나의 큰 장점이 된다.</p>
<hr>
<h1 id="📌-정리">📌 정리</h1>
<p>FastAPI는 단순히 <strong>&quot;빠른 웹 프레임워크&quot;</strong> 가 아니다.</p>
<p>AI 서비스를 개발하는 입장에서는 다음과 같은 강점을 제공한다.</p>
<table>
<thead>
<tr>
<th>✅ 특징</th>
<th>장점</th>
</tr>
</thead>
<tbody><tr>
<td>📑 API 계약 관리</td>
<td>코드와 Swagger 문서를 동시에 관리할 수 있다.</td>
</tr>
<tr>
<td>⚡ 비동기 처리</td>
<td>외부 AI 호출을 병렬 처리하여 응답 시간을 줄일 수 있다.</td>
</tr>
<tr>
<td>🤖 AI 서버 분리</td>
<td>NestJS와 역할을 분리하여 마이크로서비스 구조를 만들기 쉽다.</td>
</tr>
<tr>
<td>🗄️ 의존성 주입</td>
<td>DB 세션을 안전하고 깔끔하게 관리할 수 있다.</td>
</tr>
<tr>
<td>📦 공통 응답 구조</td>
<td>성공·실패 응답을 일관성 있게 관리할 수 있다.</td>
</tr>
<tr>
<td>🧠 Python 생태계</td>
<td>OpenAI, Whisper, NumPy, pgvector 등 AI 라이브러리를 그대로 활용할 수 있다.</td>
</tr>
</tbody></table>
<hr>
<h1 id="🎯-마무리">🎯 마무리</h1>
<p>바이브 벡터 연산에 대하여 MySql 서버는 지원을 하지 않아, 벡터 자료형을 지원하는 Postgre DB로의 마이그레이션까지 진행하였다.</p>
<p>본래의 MySql DB를 사용할때에는 vibevector , 벡터 자료형을 지원해주지 않아, vibevector를 json형태로 직렬화하여 저장하는 형태였다.</p>
<p>즉 유사도 측정을 위해 벡터를 계산하기 위해서는 그 모든 벡터들을 불러와서 json 형태를 다시 벡터 형식으로 직렬화하고 계산하고 다시 json 형태로 저장해야하는 병목이 존재하였는데,</p>
<p>Postegre DB로의 마이그레이션으로 이 병목을 해결함과 동시에, Fastapi 접목을 통해서 비동기 처리 및 파이썬 생태계를 사용한 추천 시스템 구축을 할 수 있었다.</p>
<p>이제 정말 출시가 얼마 남지 않았는데, 서비스가 커지는 경우에는 STT 모델을 외부api (OpenAPI)로 끌어와서 사용하는것이 아니라, STT 모델을 Fastapi에서 직접 구축하여 외부 의존성을 줄이고 직접 구축하는 방향으로 발전시켜보아야겠다 !</p>
<p>지금은 MVP단계에서 많은 사용자들의 호응과 반응을 얼른 보고싶다는 마음 뿐이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[중복 Race 처리]]></title>
            <link>https://velog.io/@lim_dohyeon/%EC%A4%91%EB%B3%B5-Race-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@lim_dohyeon/%EC%A4%91%EB%B3%B5-Race-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 09 Jul 2026 08:51:39 GMT</pubDate>
            <description><![CDATA[<h1 id="race-condition-">Race Condition ?</h1>
<p>운영체제에서 race condition이라는 상황에 대해서 들어보았다.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/8eda5548-ba19-4043-87b3-247a9152062e/image.png" alt=""></p>
<p>race condition이란 </p>
<blockquote>
<p>둘 이상의 프로세스(또는 스레드)가 동시에 공유 자원(Shared Resource)에 접근하여 데이터를 수정할 때, 실행 순서(interleaving)에 따라 결과가 달라지는 현상</p>
</blockquote>
<p>본래 서버에 어떤 요청이 랜덤하게 들어와도 그 결과는 같아야하는데 , 어떤 요청의 순서에 따라서 그 결과값이 달라진다면 , 그것은 서버의 Integrity , 완전함을 해치는 요소라서 클라이언트에게 모두 동일한 상태의 서버를 제공해 줄 수가 없는 것입니다.</p>
<h1 id="중복-race-">중복 Race ?</h1>
<p>Race Condition 중에서 발생할 수 있는 흔한 경우 중 하나가 바로 중복 Race이다.</p>
<p>동일한 생성 요청이 동시에 혹은 굉장히 짧은 시간내에 두개 이상의 요청이 들어올때, 중복 생성되는 경우를 중복 Race라고 합니다.</p>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/e127b452-9ad7-443e-806a-401d64b7e684/image.png" alt=""></p>
<h1 id="🔨-해결책">🔨 해결책</h1>
<h2 id="db-unique-index-추가">DB Unique Index 추가</h2>
<p>가장 근본적인 해결책입니다.</p>
<p>DB에서 근본적으로 동일한 값을 넣지 못하도록 제약을 걸어두는 방식입니다.</p>
<pre><code class="language-typescript">model User {

  id    Int    @id @default(autoincrement())

  email String @unique

}</code></pre>
<p>Unique Constraint, Unique Index 설정을 통해 DB 상에서 중복 생성을 막는 방식입니다.</p>
<h2 id="prisma-interactive-transaction-방식">Prisma Interactive Transaction 방식</h2>
<pre><code class="language-typescript">try {
  return await prisma.$transaction(async (tx) =&gt; {
    const exists = await tx.user.findUnique({ where: { email } });

    if (exists) {
      throw new ConflictException(&#39;이미 존재하는 이메일입니다.&#39;);
    }

    return tx.user.create({
      data: { email },
    });
  });
} catch (e) {
  if (e.code === &#39;P2002&#39;) {
    throw new ConflictException(&#39;이미 존재하는 이메일입니다.&#39;);
  }

  throw e;
}</code></pre>
<p>Prisma Interactive Transaction 방식을 사용해서, </p>
<p>async function을 넘기면, 그 함수 안에서 tx로 실행한 모든 Prisma 쿼리가 하나의 트랜잭션에 포함되는 방식입니다.</p>
<p>이 방식을 사용하게 되면 이미 존재하는 데이터의 경우 에러를 던지며, Prisma가 자동으로 rollback하는 기능이 추가되어, 중복 처리를 할 수 있다는 장점이 있습니다.</p>
<p>하나의 쿼리문이므로 결국 INSERT는 실행되지 않습니다.</p>
<h2 id="분산-lock--redis-lock-추가">분산 Lock , Redis Lock 추가</h2>
<p>동시에 같은 요청 , 생성 요청 등 중복 Race가 발생하게 된다면, Redis가 먼저 그 요청을 확인 , 자원에 대한 Lock을 Redis가 구현하게 되는 방식입니다.</p>
<p>Redis는 메모리에 존재하기 때문에 DB보다 더 사용성이 높고 , 더 빠르게 접근 가능하기 때문에 더 반응성이 좋게 반응 할 수 있다.</p>
<pre><code class="language-typescript">const lockKey = `lock:user:${email}`;
const lockValue = crypto.randomUUID();

const acquired = await redis.set(lockKey, lockValue, &#39;NX&#39;, &#39;EX&#39;, 5); 
// Lock을 생성 및 자원 점유 ! 

if (!acquired) {
  throw new ConflictException(&#39;이미 처리 중인 요청입니다.&#39;);
  //redis에서 이미 처리중인 요청인지 확인
}

try {
  // 중복되면 안 되는 작업
} finally {
  // 내가 잡은 락일 때만 삭제
}</code></pre>
<p>하지만 결국 중요한것은 Redis Lock 은 보조적인 수단이며 , </p>
<p>근본적인 해결책은 DB상에서의 Unique Index, Constraint 를 걸어놔야지만 된다는것.</p>
<p>Redis Lock이 근본적인 해결책은 아니라는것을 명심해야한다!</p>
<p>가장 근본적이며 많이 채택하는 구조는</p>
<pre><code>Client
   │
   ▼
Redis Lock 획득
   │
   ▼
Prisma Transaction 시작
   │
   ├── SELECT
   ├── INSERT
   ├── UPDATE
   └── ...
   │
Commit / Rollback
   │
   ▼
Redis Unlock</code></pre><h1 id="마무리">마무리</h1>
<p>언제나 이 Transaction 관리가 백엔드 개발자에게 가장 중요한 덕목인것같다.</p>
<p>서버를 안정화 시키며 무수한 요청에 따라서 서버가 바뀌는것이 아니라, 서버는 항상 일정한 상태를 유지하는 것 </p>
<p>그것에 대한 고민을 계속해서 해야하는것이 잘하는 개발자의 덕목중에 하나인것같다.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/6866a173-0be9-4287-8597-30a1575fa2b4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CloudFront , CDN에 관하여]]></title>
            <link>https://velog.io/@lim_dohyeon/s3-presigned-url-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@lim_dohyeon/s3-presigned-url-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Tue, 07 Jul 2026 15:54:03 GMT</pubDate>
            <description><![CDATA[<h1 id="너무-느리다">너무 느리다.</h1>
<p>진짜 개느리다. </p>
<p>몇일 후 출시 예정인데, 사용자가 화병나서 사용 안할것같은 느낌이다.</p>
<p>어느정도냐면 
<img src = https://velog.velcdn.com/images/lim_dohyeon/post/e905765f-d13b-4e37-8582-bb38bf6f82e7/image.png width=30% height=30%></p>
<p>이 화면에서 개 산책 동호회, 공개 동호회 테스트 이미지를 불러오는데 0.5초~1초 정도 걸린다.
출시..할 수 있을까?(😅😅😅😅)</p>
<h1 id="원인분석">원인분석</h1>
<p>지금 현재의 구조는 </p>
<p>DB 에 presinged url 을 저장후 , Get api가 호출 되는 경우마다, 저장된 presigend url을 던져주는 구조.</p>
<p>하지만 !! 이 presigned url은 말그대로 Presigned , 영속한 값이 아니다.
알고보니, 전 팀원께서 이 url의 유효기간을 7일로 설정해두었던것.</p>
<pre><code class="language-json">ExpiredAt = 7days .. </code></pre>
<p>그래서, 7일 뒤에 만료 된 Url로 접근하는 경우가 생겼고, </p>
<p>이에 대한 해결책으로 DB에 s3 presigned url을 저장하는것이 아니라, 영속한 값인 s3key를 저장하게 되었다. </p>
<p>URL 형태가 아니라 </p>
<pre><code>s3://eum-voice-staging/voice/club/8/seed-club-8.m4a</code></pre><p>이렇게 s3key형태로 저장후, Get api가 호출될때마다 Global interceptor가 해당 s3key값으로 URL을 만들어 반환하는 구조로 구현이 되어있다.</p>
<pre><code class="language-typescript">return next.handle().pipe(
      mergeMap(async (data) =&gt; {
        const payload = data === undefined ? ({} as unknown as T) : data;
        const transformedPayload =
          await this.s3ObjectUrlService.transformClientUrlFields(payload);

        return {
          resultType: &#39;SUCCESS&#39; as const,
          success: { data: transformedPayload },
          error: null,
          meta: {
            timestamp: new Date().toISOString(),
            path: req?.originalUrl ?? req?.url ?? &#39;&#39;,
          },
        };
      }),
    );
</code></pre>
<p>위의 코드에서처럼 transformPayload를 통해서 url을 매번 Global intercetptor로 생성해준다.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/810051f4-976a-4dbd-b089-3191c0843f3e/image.png" alt=""></p>
<h1 id="해결-">해결 ?</h1>
<p>하지만 이 구조가 절대 해결책이 아니었다는 사실 ㅋㅋ .. 
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/55744c4c-cbe5-4743-868d-eb9cb5ff60fe/image.png" alt="">
<del><em>(이것때문에 DB Reset, 데이터 전부 밀었는데 헛수고였다🥵🥵)</em></del></p>
<p>오히려 더 비효율적이게 되었다. </p>
<p>그 이유는 Get api를 호출할때마다 다른 url이 발급되어서 , 프론트엔드에서의 캐시 효율이 매우 떨어지게된것이다.</p>
<p>프론트엔드에서 가지고있던 Url과 값이 달라 항상 miss되어, 매번 새로운 url로 이미지를 다운로드하는 비효율적인 상황인것이다.</p>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/f8cd1693-5380-40e2-bfe5-f8bb95413688/image.png" alt=""></p>
<h1 id="cloudfront란-무엇일까">CloudFront란 무엇일까?</h1>
<p>AWS 인프라를 구성하다 보면 S3, EC2, ALB와 함께 자주 등장하는 서비스가 있습니다. 바로 <strong>CloudFront</strong>입니다.</p>
<p>CloudFront는 AWS에서 제공하는 <strong>CDN(Content Delivery Network)</strong> 서비스입니다. 쉽게 말하면, 사용자가 서버나 S3에 직접 접근하지 않고, 전 세계 여러 지역에 분산된 캐시 서버를 통해 더 빠르게 콘텐츠를 받아볼 수 있도록 해주는 서비스입니다.</p>
<p>예를 들어 한국에 있는 사용자가 미국 리전에 있는 S3 이미지에 접근한다고 가정해보겠습니다. 매번 미국 리전까지 요청을 보내면 네트워크 거리가 멀기 때문에 응답 속도가 느려질 수 있습니다. 하지만 CloudFront를 사용하면, 사용자의 위치와 가까운 엣지 로케이션에서 캐싱된 파일을 전달받을 수 있습니다. 결과적으로 이미지, 영상, 정적 파일, API 응답 등을 더 빠르고 안정적으로 제공할 수 있습니다.</p>
<h2 id="cloudfront가-필요한-이유">CloudFront가 필요한 이유</h2>
<p>일반적으로 클라이언트가 S3나 서버에 직접 접근하는 구조는 단순합니다.</p>
<pre><code class="language-text">Client → S3 또는 Server</code></pre>
<p>하지만 이런 구조에는 몇 가지 한계가 있습니다.</p>
<p>첫째, 사용자의 위치에 따라 응답 속도가 달라질 수 있습니다. 서버가 특정 리전에만 존재한다면, 멀리 떨어진 사용자는 상대적으로 느린 응답을 경험하게 됩니다.</p>
<p>둘째, 모든 요청이 원본 서버로 직접 전달되기 때문에 트래픽이 많아질수록 서버 부담이 커집니다. 이미지, CSS, JavaScript, 음성 파일, 영상 파일처럼 자주 요청되는 정적 리소스까지 매번 원본 서버에서 처리하면 비효율적입니다.</p>
<p>셋째, S3 버킷이나 백엔드 서버를 외부에 직접 노출해야 하는 경우가 생깁니다. 보안적으로도 더 안전한 구조를 만들기 위해서는 중간에 CloudFront를 두고, 사용자는 CloudFront를 통해서만 접근하게 만드는 방식이 더 적절할 수 있습니다.</p>
<p>CloudFront를 적용하면 구조는 다음과 같이 바뀝니다.</p>
<pre><code class="language-text">Client → CloudFront → S3 또는 Server</code></pre>
<p>사용자는 CloudFront 도메인으로 요청을 보내고, CloudFront는 필요한 경우 원본 서버에서 데이터를 가져옵니다. 이후 같은 파일이 다시 요청되면 원본 서버까지 가지 않고 CloudFront 캐시에서 바로 응답할 수 있습니다.</p>
<h2 id="cloudfront의-핵심-개념">CloudFront의 핵심 개념</h2>
<p>CloudFront를 이해하기 위해서는 몇 가지 개념을 알아야 합니다.</p>
<h3 id="1-origin">1. Origin</h3>
<p>Origin은 CloudFront가 실제 데이터를 가져오는 원본 저장소입니다.</p>
<p>대표적인 Origin은 다음과 같습니다.</p>
<pre><code class="language-text">- S3 버킷
- EC2 서버
- Application Load Balancer
- API 서버
- 외부 HTTP 서버</code></pre>
<p>예를 들어 이미지 파일을 S3에 저장해두고 CloudFront를 연결한다면, 이때 S3 버킷이 Origin이 됩니다.</p>
<h3 id="2-edge-location">2. Edge Location</h3>
<p>Edge Location은 CloudFront가 콘텐츠를 캐싱하고 사용자에게 전달하는 지점입니다.</p>
<p>CloudFront는 전 세계 여러 지역에 엣지 로케이션을 가지고 있습니다. 사용자가 요청을 보내면, CloudFront는 사용자와 가까운 엣지 로케이션에서 콘텐츠를 제공하려고 합니다.</p>
<p>이 덕분에 사용자는 원본 서버의 위치와 상관없이 더 빠르게 콘텐츠를 받을 수 있습니다.</p>
<h3 id="3-cache">3. Cache</h3>
<p>CloudFront의 가장 중요한 기능 중 하나는 캐싱입니다.
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/c47399dc-9370-49bb-a821-2c5d7f304891/image.png" alt=""></p>
<p>처음 사용자가 특정 이미지를 요청하면 CloudFront는 Origin에서 해당 이미지를 가져옵니다. 그리고 그 이미지를 엣지 로케이션에 저장합니다. 이후 다른 사용자가 같은 이미지를 요청하면 CloudFront는 Origin까지 다시 가지 않고 캐싱된 이미지를 바로 반환합니다.</p>
<p>즉, 다음과 같은 효과를 얻을 수 있습니다.</p>
<pre><code class="language-text">- 응답 속도 향상
- 원본 서버 부하 감소
- 데이터 전송 비용 최적화
- 안정적인 콘텐츠 제공</code></pre>
<h3 id="4-distribution">4. Distribution</h3>
<p>Distribution은 CloudFront 설정 단위입니다.</p>
<p>어떤 Origin을 사용할지, 어떤 도메인을 연결할지, HTTPS 인증서는 무엇을 사용할지, 캐시 정책은 어떻게 설정할지 등을 하나의 Distribution에서 관리합니다.</p>
<p>CloudFront를 사용한다는 것은 보통 CloudFront Distribution을 생성하고, 여기에 S3나 서버를 Origin으로 연결하는 것을 의미합니다.</p>
<h2 id="cloudfront와-s3를-함께-사용하는-이유">CloudFront와 S3를 함께 사용하는 이유</h2>
<p>CloudFront는 S3와 함께 자주 사용됩니다.</p>
<p>S3는 이미지, 동영상, 음성 파일, 문서 같은 정적 파일을 저장하기 좋은 서비스입니다. 하지만 S3 URL을 클라이언트에게 직접 노출하면 몇 가지 문제가 생길 수 있습니다.</p>
<p>예를 들어 S3 객체가 public으로 열려 있다면 누구나 직접 접근할 수 있습니다. 반대로 private으로 설정하면 접근 제어를 따로 처리해야 합니다. 또한 S3 리전과 사용자의 위치가 멀 경우 응답 속도가 느려질 수 있습니다.</p>
<p>CloudFront를 S3 앞단에 두면 다음과 같은 구조를 만들 수 있습니다.</p>
<pre><code class="language-text">Client → CloudFront → S3</code></pre>
<p>이 구조에서는 사용자가 S3에 직접 접근하지 않고 CloudFront를 통해 파일에 접근합니다. 그리고 CloudFront가 S3의 파일을 캐싱해두기 때문에, 반복 요청에 대해 더 빠른 응답이 가능합니다.</p>
<p>또한 S3 버킷은 private으로 유지하고, CloudFront만 S3에 접근할 수 있도록 설정할 수도 있습니다. 이렇게 하면 보안적으로도 더 좋은 구조가 됩니다.</p>
<h2 id="🧹-정리">🧹 정리</h2>
<p>CloudFront는 AWS에서 제공하는 CDN 서비스입니다. 사용자가 원본 서버나 S3에 직접 접근하지 않고, 전 세계 엣지 로케이션을 통해 빠르게 콘텐츠를 받을 수 있도록 도와줍니다.</p>
<p>그래서 CDN 서비스 곧바로 도입하자!! 고 생각하였지만 ,, 출시가 바로 코앞에 닥쳐있는 현 상황과 현재 저희가 가지고있는 자원들을 생각해서 조금 더 고민해보기로 하였습니다 .. ! </p>
<p>그래도 이런 문제들을 해결하기 위한 서비스들이 이미 존재하는것을 보고 </p>
<p>역시 현대시대 , 대 AI시대에는 내가 겪은 문제들은 모두 이미 다른 사람들이 겪어 보았고, 이에 대한 해결책들도 이미 많이 제시되어있다는 것을 깨달았다 .. 
<img src="https://velog.velcdn.com/images/lim_dohyeon/post/1ff395a5-94ab-426d-be6e-9d80c0589e5b/image.png" alt=""></p>
<p><del>나도 언젠가는 이런 문제들을 해결하는 엄청난 서비스를 개발하는 사람이 되기를..</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DDD. 도메인 주도 설계에 대하여]]></title>
            <link>https://velog.io/@lim_dohyeon/DDD.-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EB%8F%84-%EC%84%A4%EA%B3%84%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@lim_dohyeon/DDD.-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A3%BC%EB%8F%84-%EC%84%A4%EA%B3%84%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Mon, 06 Jul 2026 17:27:38 GMT</pubDate>
            <description><![CDATA[<h1 id="🧩-ddd-설계-정리-데이터-중심에서-행위-중심으로">🧩 DDD 설계 정리: 데이터 중심에서 행위 중심으로</h1>
<blockquote>
<p>DDD, 즉 Domain-Driven Design은 단순히 폴더 구조를 나누는 설계 방식이 아니다.<br>핵심은 <strong>데이터가 아니라 도메인의 행위와 규칙을 중심으로 소프트웨어를 설계하는 것</strong>이다.</p>
</blockquote>
<hr>
<h2 id="개요">개요</h2>
<p>전통적인 MVC 구조의 아키텍처가 너무 진부하다는 생각이 들었다.
가장 구현의 복잡성은 떨어지는 구조이지만, 다른 좋은 구조는 없을까 고민하던차, DDD구조에 관한 책을 추천받아 읽게 되었고, 객체지향적 사고와, 확장성이 좋은 사고방식이라는 생각이 들어, 도전하고싶은 생각이 들었다.</p>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/6b3fea5a-b035-465b-951b-784ecc4fbe2b/image.png" alt="">
<em>(대충 진부함을 느끼는 릅신 짤)</em></p>
<p>하지만 사실은 머릿속에 아무것도 들어있지 않은 감자라는것 .. 🤯🤯🤯🤯
그래도, 똑같은 구조를 통해 또 백엔드 프로젝트를 진행하는것은 의미없다는 생각이 들었다.
미지의 모르는 것들을 탐구하고, 성장해 나가고자 새로운 것들에 도전하고자한다.</p>
<hr>
<h2 id="1-ddd-설계가-일반적인-crud-설계와-다른-점">1. DDD 설계가 일반적인 CRUD 설계와 다른 점</h2>
<p>일반적인 CRUD 중심 설계에서는 주로 다음과 같은 사고방식을 가진다.</p>
<pre><code class="language-text">데이터를 저장한다.
데이터를 수정한다.
필드 값을 바꾼다.

setName()
setStatus()
setCount()</code></pre>
<p>즉, 관심사가 대부분 <strong>데이터와 필드 변경</strong>에 맞춰져 있다.</p>
<p>반면 DDD 관점에서는 다음과 같이 생각한다.</p>
<pre><code class="language-text">회원이 동호회에 가입한다.
클럽장이 모집을 마감한다.
주문이 취소된다.
게시글이 숨김 처리된다.
초대 링크가 비활성화된다.</code></pre>
<blockquote>
<p>DDD에서는 단순히 <code>status</code> 값을 변경하는 것이 아니라,<br><strong>도메인에서 실제로 일어나는 행동</strong>을 코드로 표현하려고 한다.</p>
</blockquote>
<hr>
<h3 id="ddd의-행위-중심-설계">DDD의 행위 중심 설계</h3>
<pre><code class="language-text">이 도메인은 어떤 행동을 하고,
그 행동에는 어떤 규칙이 있는가?</code></pre>
<p>예를 들어 클럽 도메인이라면 다음과 같이 질문한다.</p>
<pre><code class="language-text">클럽은 생성될 수 있다.
클럽 소개글은 변경될 수 있다.
클럽장은 모집을 마감할 수 있다.
클럽은 삭제될 수 있다.
삭제된 클럽은 다시 삭제할 수 없다.</code></pre>
<p>그리고 이러한 규칙을 도메인 객체 안에 넣는다.</p>
<pre><code class="language-java">public void delete() {
    if (this.deleted) {
        throw new IllegalStateException(&quot;이미 삭제된 클럽입니다.&quot;);
    }

    this.deleted = true;
}</code></pre>
<p>즉, DDD에서는 <code>setDeleted(true)</code>가 아니라 <code>delete()</code>라는 <strong>의미 있는 행위</strong>를 만든다.</p>
<hr>
<h2 id="3-ddd-계층-구조">3. DDD 계층 구조</h2>
<p>DDD에서 일반적으로 사용하는 계층은 다음과 같다.</p>
<pre><code class="language-text">presentation
application
domain
infrastructure</code></pre>
<p>각 계층의 역할은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>계층</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Presentation</td>
<td>Controller, Request, Response, Validation</td>
</tr>
<tr>
<td>Application</td>
<td>UseCase, Application Service, Command, Result, Transaction</td>
</tr>
<tr>
<td>Domain</td>
<td>Entity, Value Object, Aggregate, Domain Service, Repository Interface</td>
</tr>
<tr>
<td>Infrastructure</td>
<td>JPA Entity, Spring Data Repository, 외부 API Client, 파일 저장소, 메시지 큐</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-전체-의존성-흐름">4. 전체 의존성 흐름</h2>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/bb156bcf-c9dc-4a81-8f88-8f9d9ab64ad1/image.png" alt=""></p>
<p>핵심은 다음과 같다.</p>
<p><strong>presentation</strong> 에서는 우리가 흔히 아는 컨트롤러</p>
<p><strong>application/service</strong> 레이어에서는 전통적인 계층 구조와 다르게, 컨트롤러와 도메인을 이어주는 통로정도의 굉장히 가벼운 책임을 가지는 레이어이다.</p>
<p><strong>Domain</strong> 도메인 주도 설계의 가장 중요한 레이어로써 단순히 DB와 매핑되는 Entity를 의미하는것이 아니라 그 데이터와 관련된 기능들도 구현이 되어있는 , 비즈니스 로직이 포함되어있는 레이어이다.</p>
<p><strong>Repository</strong> 도메인 레이어의 repository interface를 구현한 구현체이다. 외부 db와의 연결을 담당하는 책임을 가지는 레이어이다.</p>
<hr>
<h1 id="🔁-dip-의존성-역전-원칙">🔁 DIP: 의존성 역전 원칙</h1>
<h2 id="1-dip란">1. DIP란?</h2>
<p>DIP는 Dependency Inversion Principle의 약자다.</p>
<p>일반적으로 고수준 모듈이 저수준 모듈에 직접 의존하면 변경에 취약해진다.</p>
<p>일반적인 경우에, Application Service가 JPA나 DB 기술에 직접 영향을 받는다.</p>
<p>DDD에서는 이를 다음과 같이 뒤집는다.</p>
<pre><code class="language-text">Application Service
    ↓
Repository Interface
    ↑
Infrastructure Repository Implementation</code></pre>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/61d252fd-f062-43ec-bbdf-e6b440cc3cac/image.png" alt="">
위의 그림을 보게 되면, 도메인 영역에 존재하는 RuleDiscounter 라는 인터페이스를 Infrastructure영역에서 구현 (implement)하고있는것을 확인할 수 있다.
즉, 하위 영역인 Infrastructure 가 구현되어있지 않아도, 혹은 하위 레이어의 기술스택이나 구현방식이 바뀌어도, 상위 영역인 도메인에서의 변경사항이 매우 적다는 장점을 가진다.</p>
<ul>
<li>테스트가 용이하다. </li>
<li>기술 스택 변경에 유연하다. </li>
<li>저수준 모듈에 직접 의존하지 않아, 도메인 레이어의 응집도가 증가한다.</li>
</ul>
<hr>
<h2 id="2-dip-핵심-문장">2. DIP 핵심 문장</h2>
<blockquote>
<p>하위 기능을 추상화한 인터페이스는 고수준 모듈에 위치한다.</p>
</blockquote>
<hr>
<h2 id="3-ddd에서-dip를-적용하기-좋은-지점">3. DDD에서 DIP를 적용하기 좋은 지점</h2>
<p>DDD에서는 특히 <strong>외부 기술과 만나는 지점</strong>에 DIP를 적용하는 것이 효과적이다.</p>
<p>대표적인 예시는 다음과 같다.</p>
<pre><code class="language-text">DB Repository
외부 API Client
...</code></pre>
<p>이런 요소들은 기술 변경 가능성이 높다.<br>따라서 Domain 또는 Application 영역에 인터페이스를 두고,<br>Infrastructure 영역에서 구현체를 제공하는 방식이 좋다.</p>
<hr>
<h1 id="🧱-도메인-영역의-주요-구성-요소">🧱 도메인 영역의 주요 구성 요소</h1>
<p>DDD의 Domain Layer에는 다음과 같은 구성 요소들이 있다.</p>
<ul>
<li>Entity</li>
<li>Value Object</li>
<li>Aggregate</li>
<li>Repository</li>
<li>Domain Service</li>
</ul>
<hr>
<h2 id="1-entity">1. Entity</h2>
<p>Entity는 <strong>식별자 ID를 가지는 도메인 객체</strong>다.</p>
<p>중요한 점은 Entity가 단순히 DB 테이블과 매핑되는 객체가 아니라는 것이다.<br>DDD에서 Entity는 도메인 기능과 규칙을 함께 가진다.</p>
<p>예를 들어 <code>Club</code>은 다음과 같은 도메인 행위를 가질 수 있다.</p>
<pre><code class="language-java">public class Club {

    private final ClubId id;
    private String name;
    private String introText;
    private boolean deleted;

    public void changeIntroText(String introText) {
        this.introText = introText;
    }

    public void delete() {
        if (this.deleted) {
            throw new IllegalStateException(&quot;이미 삭제된 클럽입니다.&quot;);
        }

        this.deleted = true;
    }
}</code></pre>
<p>Entity의 핵심은 다음과 같다.</p>
<blockquote>
<p>식별자를 가진다.
상태가 변할 수 있다.
도메인 행위를 가진다.
도메인 규칙을 스스로 지킨다.</p>
</blockquote>
<hr>
<h2 id="2-value-object">2. Value Object</h2>
<p>Value Object는 식별자를 가지지 않는 객체다.</p>
<p>예를 들어 다음과 같은 객체들이 Value Object가 될 수 있다.</p>
<pre><code class="language-text">Money
Email
Address
...</code></pre>
<p>Value Object의 핵심 특징은 다음과 같다.</p>
<pre><code class="language-text">ID가 없다.
값이 같으면 같은 객체로 본다.
불변 객체로 만드는 것이 좋다.
상태 변경 메서드를 제공하지 않는 것이 좋다.</code></pre>
<p>예를 들어 <code>ClubId</code>를 Value Object로 만들 수 있다.</p>
<pre><code class="language-java">public class ClubId {

    private final Long value;

    public ClubId(Long value) {
        if (value == null || value &lt;= 0) {
            throw new IllegalArgumentException(&quot;클럽 ID는 양수여야 합니다.&quot;);
        }

        this.value = value;
    }

    public Long value() {
        return value;
    }
}</code></pre>
<blockquote>
<p>Value object 는 불변객체이다.
식별자를 가지지않는다.
set 메소드를 구현하지 않는다.</p>
</blockquote>
<hr>
<h2 id="3-aggregate">3. Aggregate</h2>
<p>Aggregate는 연관된 Entity와 Value Object를 하나로 묶은 단위다.</p>
<p>예를 들어 <code>Club</code> Aggregate는 다음과 같은 객체들을 포함할 수 있다.</p>
<pre><code class="language-text">Club
 ├── ClubImage
 ├── ClubKeyword
 └── ClubMember</code></pre>
<p>Aggregate에서 가장 중요한 개념은 <strong>Aggregate Root</strong>다.</p>
<p>외부에서는 내부 객체를 직접 수정하지 않고,<br>반드시 Aggregate Root를 통해서만 변경해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/21f2af98-898c-44ee-ac9f-1f1b717d8a02/image.png" alt=""></p>
<p>예를 들어 클럽 대표 이미지를 변경한다고 하자.</p>
<p>나쁜 방식은 다음과 같다.</p>
<pre><code class="language-java">clubImage.markThumbnail();</code></pre>
<p>좋은 방식은 다음과 같다.</p>
<pre><code class="language-java">club.changeThumbnail(imageId);</code></pre>
<p>외부에서 <code>ClubImage</code>를 직접 수정하는 것이 아니라,<br><code>Club</code>이라는 Aggregate Root가 규칙을 통제해야 한다.</p>
<hr>
<h2 id="4-aggregate-root의-역할">4. Aggregate Root의 역할</h2>
<p>Aggregate Root는 내부 객체들이 항상 정상 상태를 유지하도록 관리한다.</p>
<p>예를 들어 클럽 대표 이미지는 하나만 존재해야 한다는 규칙이 있다고 하자.</p>
<pre><code class="language-java">public void changeThumbnail(Long imageId) {
    boolean exists = images.stream()
            .anyMatch(image -&gt; image.id().equals(imageId));

    if (!exists) {
        throw new IllegalArgumentException(&quot;클럽 이미지를 찾을 수 없습니다.&quot;);
    }

    images.forEach(ClubImage::unmarkThumbnail);

    images.stream()
            .filter(image -&gt; image.id().equals(imageId))
            .findFirst()
            .ifPresent(ClubImage::markThumbnail);
}</code></pre>
<p>이 규칙을 <code>ClubImage</code> 각각에 흩뿌리는 것이 아니라,<br><code>Club</code> Aggregate Root가 통제하는 것이 DDD다운 설계다.</p>
<hr>
<h1 id="🧭-application-service-영역">🧭 Application Service 영역</h1>
<p>Application Service는 Presentation 영역과 Domain 영역을 연결하는 계층이다.<br>일종의 Facade 역할을 한다.</p>
<p>Application Service의 일반적인 흐름은 다음과 같다.</p>
<pre><code class="language-text">1. Repository에서 Aggregate를 조회한다.
2. Aggregate의 도메인 기능을 실행한다.
3. 변경된 Aggregate를 저장한다.
4. 결과를 반환한다.</code></pre>
<hr>
<h2 id="3-application-service에서-하지-말아야-할-것">3. Application Service에서 하지 말아야 할 것</h2>
<blockquote>
<p>Application Service는 도메인 로직을 직접 구현하면 안 된다.</p>
</blockquote>
<p>나쁜 예시는 다음과 같다.</p>
<pre><code class="language-java">if (club.isDeleted()) {
    throw new IllegalStateException(&quot;이미 삭제된 클럽입니다.&quot;);
}

club.setDeleted(true);</code></pre>
<p>좋은 예시는 다음과 같다.</p>
<pre><code class="language-java">club.delete();</code></pre>
<p>도메인 규칙은 Domain 영역에 있어야 한다.<br>Application Service는 그 도메인 기능을 호출하는 역할만 한다.</p>
<hr>
<h2 id="4-application-service와-controller-분리">4. Application Service와 Controller 분리</h2>
<blockquote>
<p>Application Service는 Presentation 영역에 의존하면 안 된다.</p>
</blockquote>
<p>따라서 다음과 같은 객체를 Application Service에 직접 넘기는 것은 좋지 않다.</p>
<pre><code class="language-text">HttpServletRequest
HttpSession
HttpServletResponse</code></pre>
<p>나쁜 예시는 다음과 같다.</p>
<pre><code class="language-java">public void createClub(HttpServletRequest request) {
    Long memberId = Long.valueOf(request.getHeader(&quot;X-MEMBER-ID&quot;));
}</code></pre>
<p>좋은 방식은 Controller에서 필요한 값을 추출한 뒤 Command 객체로 전달하는 것이다.</p>
<pre><code class="language-java">public void createClub(CreateClubCommand command) {
    ...
}</code></pre>
<hr>
<h1 id="🎮-presentation-영역">🎮 Presentation 영역</h1>
<p>Presentation 영역은 흔히 말하는 Controller 계층이다.</p>
<p>주요 책임은 다음과 같다.</p>
<pre><code class="language-text">HTTP 요청을 받는다.
Request DTO를 검증한다.
Application Service를 호출한다.
Response DTO로 변환한다.
HTTP 응답을 반환한다.</code></pre>
<p>예시는 다음과 같다.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/v1/clubs&quot;)
public class ClubController {

    private final CreateClubService createClubService;

    @PostMapping
    public ResponseEntity&lt;CreateClubResponse&gt; createClub(
            @Valid @RequestBody CreateClubRequest request,
            Authentication authentication
    ) {
        Long memberId = Long.valueOf(authentication.getName());

        CreateClubResult result = createClubService.createClub(
                new CreateClubCommand(
                        request.name(),
                        request.intro(),
                        memberId
                )
        );

        return ResponseEntity.ok(
                new CreateClubResponse(result.clubId(), result.name())
        );
    }
}</code></pre>
<p>Controller는 도메인 규칙을 판단하지 않는다.<br>Controller는 요청과 응답의 변환에 집중한다.</p>
<hr>
<h1 id="🗃️-infrastructure-영역">🗃️ Infrastructure 영역</h1>
<p>Infrastructure 영역은 기술 세부사항을 담당한다.</p>
<p>예를 들면 다음과 같다.</p>
<pre><code class="language-text">JPA Entity
Spring Data JPA Repository
QueryDSL
Redis
S3
...</code></pre>
<p>DDD 관점에서 Infrastructure는 Domain과 Application을 지원하는 구현 영역이다.</p>
<p>즉, Infrastructure가 중심이 아니라<br>Domain을 위해 존재하는 세부 구현체다.</p>
<hr>
<h1 id="✅-마무리">✅ 마무리</h1>
<p><img src="https://velog.velcdn.com/images/lim_dohyeon/post/95897e33-e55f-4221-b4ec-c6870ff94fd3/image.png" alt="">
무엇이던간에 첫번째 도전은 어렵다.</p>
<p>본격적인 코드 작성에 앞서서 어떤 구조인지 딥다이브해서 알아보았는데 역시나 쉽지는 않을것같다.</p>
<p>가장 어려울것같은 부분은 </p>
<ul>
<li>도메인 추출</li>
<li>바운디드 컨텍스트에 대한 고민</li>
<li>애그리거트 조회 성능 문제 해결</li>
<li>DIP 적용
...</li>
</ul>
<p>아마 한두가지가 아닐것같지만, 시작이 반이라는 말이 있듯이, 벌써 반이나 왔다는 생각을 가지고 나아가고자 한다 ! </p>
<p>👏화이팅 나 자신👏</p>
]]></description>
        </item>
    </channel>
</rss>