<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>greenlemon_t.log</title>
        <link>https://velog.io/</link>
        <description>:)</description>
        <lastBuildDate>Tue, 09 Sep 2025 04:35:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>greenlemon_t.log</title>
            <url>https://velog.velcdn.com/images/greenlemon_t/profile/d06b6fb4-e2f8-4790-a8fd-56e2981a8fdc/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. greenlemon_t.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/greenlemon_t" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[하나은행 체험형 인턴 ICT 합격후기및 수료후기]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%95%98%EB%82%98%EC%9D%80%ED%96%89-%EC%B2%B4%ED%97%98%ED%98%95-%EC%9D%B8%ED%84%B4ICT-%ED%95%A9%EA%B2%A9%ED%9B%84%EA%B8%B0%EB%B0%8F-%EC%88%98%EB%A3%8C%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@greenlemon_t/%ED%95%98%EB%82%98%EC%9D%80%ED%96%89-%EC%B2%B4%ED%97%98%ED%98%95-%EC%9D%B8%ED%84%B4ICT-%ED%95%A9%EA%B2%A9%ED%9B%84%EA%B8%B0%EB%B0%8F-%EC%88%98%EB%A3%8C%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 09 Sep 2025 04:35:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/e334cf25-fc45-4ea7-abc2-8a3f1070ecd8/image.png" alt="">
자소서만 마저쓰고 적어야지..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[투자자산운용사 비전공자 2주컷후기]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%88%AC%EC%9E%90%EC%9E%90%EC%82%B0%EC%9A%B4%EC%9A%A9%EC%82%AC-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-2%EC%A3%BC%EC%BB%B7%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@greenlemon_t/%ED%88%AC%EC%9E%90%EC%9E%90%EC%82%B0%EC%9A%B4%EC%9A%A9%EC%82%AC-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-2%EC%A3%BC%EC%BB%B7%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 09 Sep 2025 04:29:56 GMT</pubDate>
            <description><![CDATA[<p>합격한지 한달이 지났지만 뒤늦은 후기~(토마토패스 수강후 적는 후기)</p>
<ol>
<li>취득동기</li>
</ol>
<p>금융권 IT직무 취업을 목표로 하고있는데 
배경지식 학습 겸 면접때 써먹을수있지않을까해서 따게 되었습니다.</p>
<p>= 결론적으로는 전 완전 쌩 노베이스 준비생이었습니다</p>
<ol start="2">
<li>공부기간 및 방법</li>
</ol>
<p>정확히 2주 했습니다.</p>
<p>처음부터 2주를 잡은건 아니었어요.. 하도 비전공자는 한달도 힘들다? 라고 하셔서 한달전에 시작하려고 했지만 갑자기 하나은행 인턴 서류가 붙어서 코테와 면접준비를 하느라 2주를 소비해서 실질적으로 2주동안 공부를 진행하였습니다.
-&gt; 결론적으로 인턴이랑 투운사 둘다 합격해서 넘나 행복햇다 ㅎ.ㅎ
인턴 후기는 나중에 쓰겠습니다~</p>
<p>처음 개념책 진도를 나갈때 와 이거 절대 2주컷 못한다고 생각하고 다음시험 노려야겠다고 생각했는데 일단 최선은 다해보자 생각하고 출제동형 정리 pdf를 진짜 열심히 봤습니다</p>
<p>2주컷 공부방법 차례대로 설명드릴게요</p>
<p>​</p>
<p>1) 처음 6일동안은 실전마무리특강 강의를 완강했습니다.</p>
<p>2배속으로 들어서 하루에 강의 8개??정도 들었던거같아요</p>
<p>백수 학생신분이라 하루에 8시간?정도 투자했던거같아요</p>
<p>첨에 들으면 단어 하나하나가 무슨소리인지 당최 이해가 안되서 단어나 간략한 흐름은 gpt로 공부했습니다</p>
<p>gpt공부법 최고에요. 이해안되는거 쉬운말로 잘 풀어서 설명해줍니다</p>
<p>​</p>
<p>저는 부동산, 윤리법 부분은 강의 패스했습니다. 너무 연관관계가 없이 생판 암기라서 이건 그냥 찍어야겠다고 생각하고 패스했습니다 시간 없으시면 패스하는거 추천!</p>
<p>​아 거시경제? 이해너무 안되길래 이해할시간도 없고..
이해안하고 그냥 표 그대로 외었어요 수치 상승 하강 그냥 이해없이 외어도 시험치는데는 지장 없어요~
이해안되면 그냥 암기법? 알려주시는거 문장 한문장만 외어도되요</p>
<p>과목 순서 다들 물어보시는데 1-&gt;2-&gt;3 순서로 했는데 어떤순서로 하나 크게 상관은 없는거 같아요</p>
<p>​</p>
<p>2) 다음 3일동안은 실전마무리특강 교안 pdf 외었습니다</p>
<p>전 아예 쌩노베라 문제풀면서 공부? 방법을 쓰기에 너무 맨땅에 헤딩느낌이라</p>
<p>일단 PDF 눈에 익혔습니다</p>
<p>교안 구성이 기출선지들 나왔던 내용들로 구성한거라서 솔직히 실마특 교안만 외어도 안정권 합격할거같긴해요</p>
<p>​</p>
<p>3) 남은 5일동안 출제동형 하루에 기출 하나씩 풀었습니다</p>
<p>전 한문제 풀고 답지보고 비교하고 각 선지에 있는 내용 실마특 강의교안 pdf찾아보는 형식으로 공부했습니다</p>
<p>그래서 기출 하나 푸는데 8시간 걸렸어요 ㅋㅋㅋ</p>
<p>대신 교안을 봤던걸 또보고 반복해서 더 잘 외어졌던거같아요</p>
<p>단기 벼락치기라면 기출 풀고 매기는 방식보다 그냥 한문제 풀고 답지보고 풀이보고 모든 선지 분석하는 방법이 좋을것같아요 전 항상 그렇게 합니다 투운사가 시간이 부족한 시험도 아니라서 굳이 한세트 전체 다 풀 필요는 없는거같아요</p>
<p>​</p>
<p>하루 목표가 오늘 이 회차에 나온 선지에 대한 정보들은 다외운다 잡고 가세요</p>
<p>기출 많이 푼다고 좋은건 아니에요 전 5개인가?6개 풀엇어요 (출제동형)</p>
<p>(패스코드는 새책입니다 하나도 안풀엇어요 ㅋㅋ..)
안풀어도 합격가능~
​</p>
<ol start="3">
<li>시험장후기</li>
</ol>
<p>문제 풀면서 솔직히 엥 쉽다고 생각했습니다</p>
<p>이미 교안이나 기출에서 나왔던 선지들이 나오고 틀린게 확실하게 있기때문에 진짜 그냥 실마특 교안만 외어도 될정도..</p>
<p>계산문제는 버리면 안됩니다</p>
<p>다맞추진 못해도 쉬운건 맞추자~ 마인드로 가야해요</p>
<p>저도 공식은 80프로정도 외운거같아요</p>
<p>계산 어려운거 안나옵니다 계산기 한 두번은 썻나? 그것도 루트계산용도로??</p>
<p>아무튼 맨 마지막 페이지의 계산 문제들 빼고는 다 기출or 교안에서 나온거고 42회기준? 쉬웠던거같네요</p>
<p>1트라 다른회차랑 비교는 못해보겠습니다</p>
<p>​</p>
<p>결론은 비전공자 2주컷으로 86점 통과했습니다</p>
<p>시험 치고 나와서 70점은 넘겠다? 라고 생각하고 정답 비교는 안해봤지만 사실 쫄렸습니다</p>
<p>너무 단기간에 준비했는지라... 이렇게 쉬울리가 없다면서...</p>
<p>생각보다 여유로운 점수로 통과했네요</p>
<p>​</p>
<p>막 비전공자 2주컷 이런거 다 헛소문이다 미화된거다 라는 말 너무 많이 들어서 걱정했는데</p>
<p>미화가 아니라 진짜 되는거였네요</p>
<p>매일 8시간 한다 기준으로 2주컷입니다</p>
<p>여러분들도 할수있습니다 !!!</p>
<p>70점만 넘으면 되는 시험이니까 더더욱 할수있을거같아요</p>
<p>다들 화이팅입니다!</p>
<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/58e62f17-65cb-4236-b43d-82ebd800bec7/image.png" alt="">
<img src="https://velog.velcdn.com/images/greenlemon_t/post/d9011a2c-8757-4b47-b02e-5cbd9200081c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Redis Pub/Sub과 SSE를 활용한 체결 데이터 실시간 전송]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Redis-PubSub%EA%B3%BC-SSE%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B2%B4%EA%B2%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A0%84%EC%86%A1</link>
            <guid>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Redis-PubSub%EA%B3%BC-SSE%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B2%B4%EA%B2%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A0%84%EC%86%A1</guid>
            <pubDate>Sun, 16 Mar 2025 19:32:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/624c5dea-695b-4ec8-97c4-cdf971e6bfb9/image.png" alt=""></p>
<h2 id="1-문제-상황">1. 문제 상황</h2>
<p>초기 트레이딩 시스템에서는 우리 시스템에서 발생한 체결 데이터만 체결창에 표시하도록 설계되었다. 그러나 유저 수가 적어 체결창의 데이터가 부족해 보였고, 보다 정확한 서비스를 위해 <strong>외부 체결 데이터</strong>를 반영하기로 결정했다. 하지만 기존 체결 테이블은 매수·매도 정보를 함께 저장하는 구조라 <strong>짝이 없는 체결 데이터</strong>를 처리하는 데 한계가 있었다.</p>
<p>또한, <strong>실제 체결 데이터와 자체 체결 데이터를 어떻게 합쳐서 체결창에 표시할 것인지</strong> 고민이 필요했다. 이를 위해 Redis를 활용하여 <strong>실제 체결 데이터를 저장하고, SSE(Server-Sent Events)를 통해 실시간으로 클라이언트에게 전송하는 방식</strong>을 도입했다.</p>
<h2 id="2-해결-방법">2. 해결 방법</h2>
<h3 id="1-redis에-체결-데이터-저장-및-관리">1) Redis에 체결 데이터 저장 및 관리</h3>
<ul>
<li>실제 체결 데이터를 <strong>20개씩 Redis에 저장</strong>하여 유지.</li>
<li>해외 주식 체결이 발생할 때마다 Redis의 체결가 및 체결량 등의 데이터를 업데이트.</li>
<li>체결창에서는 <strong>외부 데이터(20개) + 우리 시스템 체결 데이터(a개)를 함께 표시</strong>하여 실시간으로 반영.</li>
</ul>
<h3 id="2-redis-pubsub을-활용한-이벤트-기반-처리">2) Redis Pub/Sub을 활용한 이벤트 기반 처리</h3>
<ul>
<li><strong>Redis Pub/Sub</strong>을 이용해 체결 데이터가 들어오면 해당 이벤트를 <code>publish</code>.</li>
<li>Redis에 새로운 체결 데이터가 들어올 때마다 <code>trade_updates</code> 채널로 메시지 전송.</li>
<li>SSE 서비스는 Redis 메시지를 <code>subscribe</code>하여 실시간으로 클라이언트에게 전송.</li>
</ul>
<h3 id="3-sseserver-sent-events를-활용한-실시간-체결-데이터-전송">3) SSE(Server-Sent Events)를 활용한 실시간 체결 데이터 전송</h3>
<ul>
<li><code>StockSseService</code>에서 <strong>SSE를 이용해 클라이언트와 연결</strong>을 유지하고, 실시간 체결 데이터를 전송.</li>
<li>이를 위해 <strong>두 개의 이벤트(Buy/Sell 체결)를 하나의 SSE URL에서 처리</strong>하도록 구현.</li>
</ul>
<h2 id="3-구현-코드">3. 구현 코드</h2>
<h3 id="1-체결-데이터-redis에-저장-및-pubsub-이벤트-발생">1) 체결 데이터 Redis에 저장 및 Pub/Sub 이벤트 발생</h3>
<pre><code class="language-java">public void saveSellTradeExecutionToRedis(SellTrade trade) {
    String redisKey = &quot;stock:&quot; + trade.getTradeTicker() + &quot;:purchase_inner&quot;;
    Map&lt;String, Object&gt; tradeData = new HashMap&lt;&gt;();
    tradeData.put(&quot;trade_ticker&quot;, trade.getTradeTicker());
    tradeData.put(&quot;current_price&quot;, trade.getTradePrice());
    tradeData.put(&quot;volume&quot;, trade.getTradeQuantity());
    tradeData.put(&quot;time&quot;, trade.getTradeDate().format(DateTimeFormatter.ofPattern(&quot;HHmmss&quot;)));
    tradeData.put(&quot;trade_type&quot;, &quot;SELL&quot;);

    try {
        String tradeJson = objectMapper.writeValueAsString(tradeData);
        redisTemplate.opsForList().leftPush(redisKey, tradeJson);
        redisTemplate.convertAndSend(&quot;trade_updates&quot;, tradeJson);
    } catch (JsonProcessingException e) {
        System.err.println(&quot;Redis 저장 오류: &quot; + e.getMessage());
    }
}</code></pre>
<h3 id="2-redis-pubsub을-활용한-메시지-수신-및-sse-전송">2) Redis Pub/Sub을 활용한 메시지 수신 및 SSE 전송</h3>
<pre><code class="language-java">@Override
public void onMessage(Message message, byte[] pattern) {
    try {
        String tradeJson = new String(message.getBody());
        JsonNode jsonNode = objectMapper.readTree(tradeJson);

        String stockTicker = jsonNode.get(&quot;trade_ticker&quot;).asText();
        double currentPrice = jsonNode.get(&quot;current_price&quot;).asDouble();
        long volume = jsonNode.get(&quot;volume&quot;).asLong();
        String time = jsonNode.get(&quot;time&quot;).asText();
        String orderType = jsonNode.get(&quot;trade_type&quot;).asText();

        String tradeUpdate = objectMapper.writeValueAsString(
                new TradeUpdate(stockTicker, currentPrice, volume, time, orderType)
        );
        sendTradeUpdate(stockTicker, tradeUpdate);
    } catch (Exception e) {
        e.printStackTrace();
    }
}</code></pre>
<h3 id="3-sse를-활용한-클라이언트-실시간-업데이트">3) SSE를 활용한 클라이언트 실시간 업데이트</h3>
<pre><code class="language-java">public void sendTradeUpdate(String stockTicker, String tradeJson) {
    List&lt;SseEmitter&gt; emitters = emittersByStock.getOrDefault(stockTicker, new ArrayList&lt;&gt;());
    for (SseEmitter emitter : emitters) {
        try {
            emitter.send(SseEmitter.event()
                    .name(&quot;tradeUpdate&quot;)
                    .data(tradeJson));
        } catch (IOException e) {
            emitter.complete();
            emitters.remove(emitter);
        }
    }
}</code></pre>
<h2 id="4-트러블슈팅-결과">4. 트러블슈팅 결과</h2>
<ul>
<li><strong>체결창에 실시간 데이터 반영</strong><ul>
<li>외부 체결 데이터(20개)와 자체 체결 데이터를 하나의 화면에서 함께 표시 가능.</li>
<li>데이터가 들어올 때마다 Redis Pub/Sub을 통해 <strong>자동으로 SSE 업데이트 전송</strong>.</li>
</ul>
</li>
<li><strong>이벤트 기반 아키텍처 구현</strong><ul>
<li>Redis의 <code>trade_updates</code> 채널을 활용해 <strong>체결 데이터가 실시간으로 전파</strong>됨.</li>
<li>SSE를 이용해 <strong>별도의 폴링 없이 효율적인 실시간 업데이트 가능</strong>.</li>
</ul>
</li>
<li><strong>Kafka보다 가벼운 Redis Pub/Sub 활용</strong><ul>
<li>Kafka 대신 Redis Pub/Sub을 활용하여 <strong>지연시간이 짧고 빠른 데이터 전송 가능</strong>.</li>
</ul>
</li>
</ul>
<h2 id="5-결론-및-개선점">5. 결론 및 개선점</h2>
<ul>
<li>기존 체결 데이터 구조에서는 외부 체결 데이터를 함께 반영하는 것이 어려웠으나, Redis와 SSE를 활용해 <strong>체결창에 실시간 반영</strong>할 수 있도록 개선함.</li>
<li>Redis Pub/Sub과 SSE를 함께 활용하면 <strong>트레이딩 시스템에서 실시간 체결 데이터를 효과적으로 전달</strong>할 수 있음.</li>
<li>향후 체결 데이터 저장 방식 최적화 및 Kafka와의 연계를 고려하면 더욱 확장성 있는 시스템을 구축할 수 있을 것.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/26a10def-f0a1-4abd-8643-6cfecb055436/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] 개발후반부에 테이블 정규화요???? 말도안돼]]></title>
            <link>https://velog.io/@greenlemon_t/%E3%85%87-wn4c7anh</link>
            <guid>https://velog.io/@greenlemon_t/%E3%85%87-wn4c7anh</guid>
            <pubDate>Sun, 09 Mar 2025 14:22:33 GMT</pubDate>
            <description><![CDATA[<p>트레이딩 시스템에서 체결 처리는 매수자와 매도자를 매칭하는 방식으로 동작하지만,
외부 체결 데이터를 반영하는 과정에서 기존 설계가 확장성을 가지지 못하는 문제가 발생했다.
특히 짝이 없는 체결 데이터 처리와 Kafka 메시지 직렬화 이슈로 인해 설계를 변경해야 했다.</p>
<hr>
<h3 id="1-초기-설계-및-문제-발생">1. 초기 설계 및 문제 발생</h3>
<p>초기 트레이딩 시스템은 1:1 매칭 기반의 체결 테이블을 사용했다.
즉, 매수 주문과 매도 주문이 짝을 이루어야만 체결 데이터가 저장되는 방식이었다.</p>
<ul>
<li>기존 설계 방식
TradeMatchingEvent 엔티티를 활용하여 매수와 매도를 한 번에 매칭
매수자와 매도자가 매칭되지 않으면 체결이 발생하지 않음
TradeMatchingEvent를 기반으로 체결 테이블을 관리</li>
<li>발생한 문제
체결창 데이터 부족 문제</li>
</ul>
<p>사용자가 많지 않은 경우, 실시간 체결창에 체결 데이터가 거의 표시되지 않음.
성능 최적화를 위해 외부 체결 데이터를 반영하려 했지만, 기존 방식에서는 매칭되지 않은 데이터가 반영되지 않는 문제 발생.
외부 체결 데이터 반영 어려움</p>
<p>외부 데이터는 매수 또는 매도만 포함될 수 있는데, 기존 설계에서는 짝이 없는 데이터는 체결로 처리되지 않음.
매수 또는 매도 데이터만 들어와도 체결 테이블에 저장될 수 있어야 했음.</p>
<h3 id="2-kafka-메시지-직렬화-문제">2. Kafka 메시지 직렬화 문제</h3>
<ul>
<li>Kafka 메시지에서 null 값 처리 이슈 발생</li>
</ul>
<p>기존 TradeMatchingEvent는 매수와 매도를 한 번에 처리하는 구조였음.
그러나 외부 체결 데이터(매수만 있는 경우, 매도만 있는 경우) 를 반영하려면 매칭되지 않은 값이 null로 들어올 가능성이 있었음.
Kafka는 기본적으로 null 값을 직렬화할 수 없음 → 메시지 전송 실패</p>
<pre><code>@Data
@AllArgsConstructor
@NoArgsConstructor
public class TradeMatchingEvent {
    private Long tradeId;       // 체결 고유 번호
    private Long buyOfferNumber;    // 매수 주문 ID
    private UUID sellOfferNumber;   // 매도 주문 ID
    private Long buyerUserId;       // 매수자 ID
    private Long sellerUserId;      // 매도자 ID
    private String stockTicker; // 주식 티커
    private Long tradeQuantity; // 체결 수량
    private Long tradePrice;    // 체결 가격
    private LocalDateTime tradeTimestamp; // 체결 시각
    private Long buyerOrderQuentity; //매도자가 주문넣엇던 수향
    private Long sellerOrderQuentity; //매수자가 주문넣엇던 수량
    private Float exchangeRate=0.0f;//환율
}
</code></pre><p> 기존 구조에서는 buyOrderId 또는 sellOrderId가 null이 될 수 있음
 Kafka 직렬화 문제로 null 값이 포함되면 메시지 전송 불가</p>
<ul>
<li><p>해결책:
TradeMatchingEvent를 매수 체결과 매도 체결로 분리하여 메시지를 전송
BuyTradeEvent, SellTradeEvent 두 개의 Kafka 메시지 구조로 변경</p>
<h3 id="3-체결-테이블-구조-변경-db-정규화">3. 체결 테이블 구조 변경 (DB 정규화)</h3>
<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/4ec761ff-0986-4de5-98d9-aa18ee1d68f5/image.png" alt=""></p>
</li>
</ul>
<h3 id="4-kafka-메시지-구조-변경">4. Kafka 메시지 구조 변경</h3>
<pre><code>@Data
@AllArgsConstructor
@NoArgsConstructor
public class BuyTradeMatchEvent {
    private Long tradeId;       // 체결 고유 번호
    private Long buyOfferNumber;    // 매수 주문 ID
    private Long buyerUserId;       // 매수자 ID
    private String stockTicker; // 주식 티커
    private Long tradeQuantity; // 체결 수량
    private Long unfilledQuantity; //미체결 수량
    private Long tradePrice;    // 체결 가격
    private LocalDateTime tradeTimestamp; // 체결 시각
    private Long buyerOrderQuantity; // 매수 주문 수량
    private Float exchangeRate; // 환율
    private Long buyerOfferPrice; // 매수 주문 가격
}</code></pre><pre><code>@Data
@AllArgsConstructor
@NoArgsConstructor
public class SellTradeMatchEvent {
    private Long tradeId;       // 체결 고유 번호
    private Long sellOfferNumber;   // 매도 주문 ID
    private Long sellerUserId;      // 매도자 ID
    private String stockTicker; // 주식 티커
    private Long tradeQuantity; // 체결 수량
    private Long unfilledQuantity; //미체결 수량
    private Long tradePrice;    // 체결 가격
    private LocalDateTime tradeTimestamp; // 체결 시각
    private Long sellerOrderQuantity; // 매도 주문 수량
    private Float exchangeRate; // 환율
    private Long sellerOfferPrice; // 매도 주문 가격
}
</code></pre><p> null 값 없이 매수 또는 매도 단독으로 전송 가능
 체결되지 않은 주문도 관리할 수 있도록 확장성 증가</p>
<hr>
<h3 id="5-모듈-수정-및-코드-변경-영향">5. 모듈 수정 및 코드 변경 영향</h3>
<p>설계 변경으로 인해 트레이딩 시스템 내 5개 이상의 모듈에 영향을 줌</p>
<ul>
<li>주요 수정 사항
체결 데이터를 DB에 저장하는 서비스 로직 변경
Kafka 메시지 구조 변경 (TradeMatchingEvent → BuyTradeEvent, SellTradeEvent)
API 응답 구조 변경 (체결 데이터 조회 방식 수정)
체결 테이블을 참조하는 기존 코드 전체 수정</li>
</ul>
<h3 id="6-결론-및-개선점">6. 결론 및 개선점</h3>
<ul>
<li>트러블슈팅 과정에서 배운 점
초기에 확장성을 고려하여 체결 테이블을 정규화했더라면 수정 없이 쉽게 확장할 수 있었음
실시간 체결 시스템에서는 매칭 기반 체결뿐만 아니라, 개별 체결(매수 또는 매도 단독 체결)도 처리할 수 있도록 설계해야 함
Kafka 메시지 설계 시 null 값이 발생할 가능성을 고려해야 하며, 메시지 구조를 분리하는 것이 안정적</li>
<li><blockquote>
<p>다음엔 KAFKA event를 인터페이스로 객체지향적으로 만들어야겠다</p>
</blockquote>
</li>
</ul>
<p>-&gt; 추후에 테이블 하나로 통합해서 enum으로 구분함 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] 해외주식 정산 D+2 반영을 위한 자정 배치 처리]]></title>
            <link>https://velog.io/@greenlemon_t/%E3%85%87</link>
            <guid>https://velog.io/@greenlemon_t/%E3%85%87</guid>
            <pubDate>Thu, 06 Mar 2025 15:07:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/0581f25c-cf03-4240-be1a-e947b080e963/image.png" alt=""></p>
<p>은행이나 금융 시스템에서는 사용자의 모든 거래 내역을 즉시 데이터베이스(DB)에 반영할 수 없다.
거래가 발생할 때마다 DB를 업데이트하면 부하가 커지고 성능이 저하될 위험이 있기 때문이다.
그래서 실제 금융 시스템에서는 Redis 같은 캐싱 시스템을 활용하여 임시 저장한 후, 자정에 배치(batch) 처리를 통해 DB에 반영하는 방식이 일반적이다.</p>
<hr>
<h3 id="1-왜-배치-처리가-필요한가">1. 왜 배치 처리가 필요한가?</h3>
<p>해외주식 거래는 국내 주식과 달리 D+2 결제 원칙을 따른다.
즉, 매수 또는 매도 주문이 체결되더라도 2일 후(D+2)에 예수금이 최종 반영된다.</p>
<ul>
<li>해외주식 예수금 처리 방식
매수 시: 주문 즉시 예수금 차감 → D+2일에 최종 출금 반영
매도 시: 주문 즉시 증거금 증가 → D+2일에 매도 대금 입금 반영</li>
</ul>
<h3 id="즉시-db를-업데이트하면-안-되는-이유">즉시 DB를 업데이트하면 안 되는 이유</h3>
<ul>
<li>실시간으로 업데이트하면 트랜잭션이 많아져 DB 부하 증가</li>
<li>해외 거래소에서 실제 정산이 완료되지 않았는데, 사용자 잔고를 업데이트하면 오류 발생 가능</li>
<li>대형 금융 서비스에서는 배치 처리로 성능 최적화</li>
<li><blockquote>
<p>따라서 Redis에 저장해두고, 자정에 배치 프로세스를 실행하여 최종적으로 반영하는 방식을 구현했다.</p>
</blockquote>
</li>
</ul>
<h3 id="2-배치-처리-시스템-설계">2. 배치 처리 시스템 설계</h3>
<p>배치 처리 시스템은 매일 밤 12시(00:00) 에 실행되어 D+2일이 된 데이터만 반영한다.</p>
<p>&lt;배치 처리 흐름&gt;</p>
<ul>
<li>해외주식 거래 체결 후 배치 예수금 데이터를 Redis에 저장 (user:{userId}:batch_balance:{날짜})</li>
<li>매일 자정에 배치 스케줄러 실행 (@Scheduled(cron = &quot;0 0 0 * * ?&quot;))</li>
<li>오늘 날짜(D+2)의 예수금 데이터를 조회하여 DB 업데이트</li>
<li>업데이트된 데이터를 다시 Redis에 저장하여 최신 상태 유지</li>
</ul>
<hr>
<h3 id="3-배치-처리-코드-분석">3. 배치 처리 코드 분석</h3>
<p>00:00 자정 배치 실행 (applyPendingUpdates())</p>
<pre><code>@Scheduled(cron = &quot;0 0 0 * * ?&quot;) // 매일 00:00 실행
public void applyPendingUpdates() {
    String today = LocalDate.now().format(DATE_FORMATTER);
    Set&lt;String&gt; keys = redisTemplate.keys(&quot;user:*:batch_balance:&quot; + today);

    if (keys == null || keys.isEmpty()) {
        log.info(&quot;[배치 처리] 오늘({}) 적용할 예수금 데이터 없음.&quot;, today);
        return;
    }

    for (String key : keys) {
        String userId = key.split(&quot;:&quot;)[1];  // Redis 키에서 userId 추출
        String balanceStr = redisTemplate.opsForValue().get(key);
        String newBalance = balanceStr;

        accountRepository.updateAccountWithholding(Long.parseLong(userId), Long.parseLong(newBalance));
        log.info(&quot;사용자 {} 예수금 DB 업데이트 (D+2 반영): {}&quot;, userId, newBalance);

        // Redis 사용자 잔고 업데이트
        String userBalanceKey = &quot;user:&quot; + userId + &quot;:balance&quot;;
        redisTemplate.opsForValue().set(userBalanceKey, newBalance, EXPIRATION_DAYS, TimeUnit.DAYS);
        log.info(&quot;Redis 사용자 {} 실제 예수금 업데이트: {}&quot;, userId, newBalance);

        log.info(&quot;[배치 적용 완료] 사용자ID: {}, 적용 예수금: {}&quot;, userId, newBalance);
    }
}
</code></pre><ul>
<li>매일 00:00 (@Scheduled(cron = &quot;0 0 0 * * ?&quot;)) 실행</li>
<li>오늘 날짜(D+2)에 해당하는 배치 예수금 데이터를 Redis에서 가져옴</li>
<li>DB(accountRepository.updateAccountWithholding())에 업데이트</li>
<li>업데이트된 값을 다시 Redis에 저장하여 캐싱 데이터 유지</li>
</ul>
<h3 id="4-redis에-배치-예수금-저장-방식">4. Redis에 배치 예수금 저장 방식</h3>
<p>체결 후 배치 예수금을 저장할 때, Redis에 D+2 날짜를 포함한 키로 저장!</p>
<ul>
<li>배치 예수금 저장 방식 (user:{userId}:batch_balance:{날짜})<pre><code>user:123:batch_balance:2025-03-19 -&gt; &quot;5000000&quot;
user:456:batch_balance:2025-03-19 -&gt; &quot;2500000&quot;
</code></pre></li>
</ul>
<pre><code>user:123:batch_balance:2025-03-19 → D+2일(2025-03-19)에 500만 원 반영 예정
✔ user:456:batch_balance:2025-03-19 → D+2일(2025-03-19)에 250만 원 반영 예정

- 장점:

D+2 날짜가 포함된 키로 저장하여 배치 실행 시 필요한 데이터만 조회 가능
캐싱을 활용하여 빠른 조회 가능
실제 DB 업데이트가 필요한 시점(D+2)에만 반영하여 부하 감소

### 트러블슈팅: 배치 예수금 반영 시 발생한 문제 및 해결
- 문제 1: 배치 실행 후에도 Redis 값이 즉시 반영되지 않음
이슈:
-&gt; 배치가 실행된 후 DB에는 정상 반영되었지만, Redis의 사용자 잔고(user:{userId}:balance)가 갱신되지 않아 사용자가 조회할 때 오래된 값이 보임

- 해결 방법:
배치 실행 후 DB 업데이트와 동시에 Redis에도 최신 데이터를 반영</code></pre><p>String userBalanceKey = &quot;user:&quot; + userId + &quot;:balance&quot;;
redisTemplate.opsForValue().set(userBalanceKey, newBalance, EXPIRATION_DAYS, TimeUnit.DAYS);</p>
<p>```
-&gt; 배치 실행 후 DB &amp; Redis 동시 업데이트하여 사용자 조회 시 최신 예수금 표시</p>
<h3 id="결론-자정-배치-처리의-중요성">결론: 자정 배치 처리의 중요성</h3>
<p> 금융 시스템에서 예수금을 실시간으로 DB에 반영하는 것은 비효율적이며, 부하를 줄이기 위해 배치 프로세스가 필수적이다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA프로젝트] kafka 토픽 설계: 비즈니스 기반 vs 이벤트 기반]]></title>
            <link>https://velog.io/@greenlemon_t/2</link>
            <guid>https://velog.io/@greenlemon_t/2</guid>
            <pubDate>Wed, 05 Mar 2025 07:52:44 GMT</pubDate>
            <description><![CDATA[<p>Kafka는 데이터 스트리밍을 처리하는 강력한 메시징 시스템으로, 토픽을 어떻게 설계하느냐에 따라 시스템의 확장성과 유지보수성이 크게 달라진다.
Kafka 토픽 설계 방식에는 크게 비즈니스 기반(Business-Oriented) 과 이벤트 기반(Event-Oriented) 접근 방식이 있다.</p>
<h3 id="1-비즈니스-기반-토픽-설계">1. 비즈니스 기반 토픽 설계</h3>
<p>비즈니스 기반 설계는 도메인 또는 서비스의 역할을 기준으로 토픽을 정의하는 방식이다.
토픽이 특정 비즈니스 개념과 1:1로 매칭되므로, 서비스별 데이터 흐름을 명확하게 정리할 수 있다.</p>
<p>EX) 
order-topic
execution-topic
settlement-topic
matching-topic</p>
<ul>
<li>특징
✔ 직관적 구조: 토픽 이름이 도메인 개념과 직결되어 있어 이해하기 쉽다.
✔ 팀 단위 관리 용이: 마이크로서비스 아키텍처에서 서비스별로 토픽을 관리하기 좋다.
✖ 확장성 부족: 새로운 이벤트가 추가될 때 기존 토픽을 변경해야 할 수도 있다.
✖ 재사용 어려움: 이벤트 유형별로 다룰 때 비효율적일 수 있다.</li>
</ul>
<h3 id="2-이벤트-기반-토픽-설계">2. 이벤트 기반 토픽 설계</h3>
<p>이벤트 기반 설계는 발생하는 이벤트를 중심으로 토픽을 정의하는 방식이다.
이벤트 소스(Event Source) 또는 특정한 액션(Action)을 기준으로 토픽을 분류한다.</p>
<p>EX) 
user.created
order.placed
payment.processed
inventory.updated</p>
<ul>
<li>특징
✔ 확장성 뛰어남: 새로운 이벤트 유형을 쉽게 추가할 수 있다.
✔ 재사용성 높음: 여러 서비스에서 동일한 이벤트를 소비할 수 있어 데이터 흐름이 유연하다.
✖ 이해도 요구됨: 이벤트 흐름을 정확히 파악해야 한다.
✖ 권한 관리 복잡: 여러 서비스가 같은 이벤트를 사용할 경우, 컨슈머 관리가 어려울 수 있다.</li>
</ul>
<h3 id="트레이딩-시스템의-kafka-topic구조-설계">트레이딩 시스템의 Kafka topic구조 설계</h3>
<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/9cfe0308-08ff-47c0-b832-48c1ec88401d/image.png" alt=""></p>
<h2 id="비즈니스-기반--일부-이벤트-기반-하이브리드-설계">비즈니스 기반 + 일부 이벤트 기반 (하이브리드 설계)</h2>
<h3 id="kafka-토픽-설계-비즈니스-기반--일부-이벤트-기반-하이브리드-설계"><strong>Kafka 토픽 설계: 비즈니스 기반 + 일부 이벤트 기반 (하이브리드 설계)</strong></h3>
<p>MSA 기반 트레이딩 시스템에서는 기본적으로 <strong>비즈니스 기반 토픽 설계</strong>를 따르면서도, 특정 상황에서 <strong>이벤트 기반 방식</strong>을 도입한 <strong>하이브리드 토픽 설계</strong>를 적용했다.</p>
<hr>
<h2 id="1-기본적인-비즈니스-기반-설계"><strong>1. 기본적인 비즈니스 기반 설계</strong></h2>
<ul>
<li><strong>서비스 단위로 토픽을 분리</strong>하여 <code>order-topic</code>, <code>matching-topic</code>, <code>execution-topic</code>, <code>settlement-topic</code>, <code>notice-topic</code>을 운영함.</li>
<li>각 토픽에서 주문 생성, 매칭, 체결, 정산 등 <strong>각 모듈이 자체적으로 이벤트를 관리</strong>.</li>
<li>이를 통해 <strong>서비스별 로직을 독립적으로 유지하면서도, 필요 시 특정 서비스만 스케일링할 수 있도록 설계</strong>.</li>
</ul>
<hr>
<h3 id="2-이벤트-기반-요소를-추가한-이유"><strong>2. 이벤트 기반 요소를 추가한 이유</strong></h3>
<h3 id="1-실패-처리의-효율성-→-failure-topic-사용"><strong>(1) 실패 처리의 효율성 → Failure Topic 사용</strong></h3>
<p>** 이유: 서비스 간 결합도를 낮추고, 롤백 및 재처리를 용이하게 하기 위함.**</p>
<ul>
<li><code>execution-failure-topic</code>, <code>settlement-failure-topic</code>을 별도로 분리하여 <strong>체결 및 정산 실패 시 해당 이벤트만 별도로 관리</strong>하도록 함.</li>
<li>만약 하나의 <code>execution-topic</code>에서 실패 이벤트까지 처리하려 하면, 실패 이벤트가 기존 정상 체결 이벤트 흐름과 섞여 복잡성이 증가할 수 있음.</li>
<li>따라서 <strong>실패 시 별도의 토픽으로 분리하여 비동기적으로 재처리 가능하도록 설계</strong>.</li>
<li>또한, <code>DLT(Dead Letter Topic)</code>도 각 모듈별로 나누어 <strong>서비스별 장애를 독립적으로 관리</strong>하도록 함.</li>
</ul>
<h3 id="2-알림의-성격에-따라-분리-→-filling-notice-vs-general-notice"><strong>(2) 알림의 성격에 따라 분리 → Filling Notice vs. General Notice</strong></h3>
<p>** 이유: 알림 유형별로 중요도가 다르고, 목적이 다르기 때문.**</p>
<ul>
<li><strong><code>filling-notice-topic</code> (체결 알림):</strong> 유저에게 <strong>거래 체결 내역을 즉시 전달</strong>해야 하는 실시간성 요구.</li>
<li><strong><code>notice-topic</code> (일반 공시 알림):</strong> 금융 공시 등 상대적으로 중요도가 낮고, 대량으로 전송될 수 있는 알림.</li>
<li>만약 두 개의 알림을 하나의 <code>notice-topic</code>에서 처리한다면, 공시 알림이 많아질 경우 <strong>체결 알림 전송이 지연될 가능성이 있음</strong>.</li>
<li>따라서 <strong>체결 알림을 우선적으로 보장</strong>하기 위해 두 개의 알림을 <strong>서로 다른 토픽으로 분리</strong>함.</li>
</ul>
<hr>
<h3 id="3-하이브리드-설계를-선택한-근거"><strong>3. 하이브리드 설계를 선택한 근거</strong></h3>
<ol>
<li><strong>트레이딩 시스템 특성상 서비스 단위 토픽 구조(비즈니스 기반)가 적합</strong><ul>
<li>주문, 매칭, 체결, 정산이 각각 독립적으로 동작하며, 서비스별 확장성을 고려해야 하기 때문.</li>
<li>모든 이벤트를 작은 단위로 분리하면 오히려 관리 복잡성이 증가할 가능성이 큼.</li>
</ul>
</li>
<li><strong>실패 이벤트 처리는 이벤트 기반 설계가 더 적합</strong><ul>
<li>체결 및 정산 실패 시, <strong>재처리 및 장애 관리가 중요하므로</strong> 별도 토픽(<code>failure-topic</code>)을 통해 장애를 격리하고 처리.</li>
</ul>
</li>
<li><strong>알림 서비스는 이벤트 특성을 반영할 필요가 있음</strong><ul>
<li>체결 알림과 공시 알림을 분리함으로써 <strong>우선순위를 고려한 효율적 전송</strong>이 가능하도록 함.</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] 미체결이 문제로다]]></title>
            <link>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%AF%B8%EC%B2%B4%EA%B2%B0%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4-412ez7f2</link>
            <guid>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%AF%B8%EC%B2%B4%EA%B2%B0%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4-412ez7f2</guid>
            <pubDate>Thu, 27 Feb 2025 13:20:48 GMT</pubDate>
            <description><![CDATA[<p>ORDER 테이블은 주문모듈, 매칭은 매칭 모듈에서 미체결을 어떻게 DB에 저장하고 사용자에게 알려줄까?
주말 업로드 예정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] 공통 예외 핸들러가 다른 모듈에서 적용되지 않는 문제 해결]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EA%B3%B5%ED%86%B5-%EC%98%88%EC%99%B8-%ED%95%B8%EB%93%A4%EB%9F%AC%EA%B0%80-%EB%8B%A4%EB%A5%B8-%EB%AA%A8%EB%93%88%EC%97%90%EC%84%9C-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EA%B3%B5%ED%86%B5-%EC%98%88%EC%99%B8-%ED%95%B8%EB%93%A4%EB%9F%AC%EA%B0%80-%EB%8B%A4%EB%A5%B8-%EB%AA%A8%EB%93%88%EC%97%90%EC%84%9C-%EC%A0%81%EC%9A%A9%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Thu, 27 Feb 2025 13:04:13 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">&lt;문제 상황&gt;</h2>
<p>Postman에서 API를 테스트하는 중 예외가 발생하면 403 Forbidden 등의 상태 코드만 반환되고, 실제 에러 메시지가 응답 본문에 포함되지 않는 문제가 발생함.</p>
<p>프로젝트 내에서 공통 모듈(Common)에 GlobalExceptionHandler를 정의했지만, 다른 모듈에서 예외 핸들러가 동작하지 않음.</p>
<hr>
<h2 id="원인-분석">&lt;원인 분석&gt;</h2>
<ul>
<li><code>@RestControllerAdvice</code>는 Spring이 관리하는 Bean이지만, 현재 모듈에서는 Common 모듈 내부에서만 인식됨.</li>
<li>Spring Boot는 기본적으로 자신이 속한 패키지 및 하위 패키지만 자동 스캔하기 때문에,
다른 모듈에서 <code>GlobalExceptionHandler</code>를 인식하지 못함.</li>
<li>즉, 예외 핸들러를 모든 모듈에서 적용하려면 Common 모듈을 스캔하도록 설정해야 함.</li>
</ul>
<hr>
<h2 id="해결-방법">&lt;해결 방법&gt;</h2>
<h3 id="componentscan을-사용하여-공통-모듈-스캔-"><strong><code>@ComponentScan</code>을 사용하여 공통 모듈 스캔 !</strong></h3>
<p>공통 모듈(Common)에 있는 <code>GlobalExceptionHandler</code>가 다른 모듈에서도 자동으로 스캔되도록 설정.</p>
<p>해당 모듈의 <code>@SpringBootApplication</code>이 선언된 클래스에서 <code>@ComponentScan</code> 추가</p>
<pre><code class="language-java">@EnableDiscoveryClient
@SpringBootApplication
@ComponentScan(basePackages = {&quot;finpago.common&quot;, &quot;finpago.userservice&quot;})
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }

}</code></pre>
<p><code>@ComponentScan(basePackages = {&quot;finpago.common&quot;, &quot;finpago.settlementservice&quot;})</code>를 추가하여 공통 모듈의 핸들러를 현재 모듈에서도 인식하도록 설정.</p>
<hr>
<h3 id="테스트">테스트</h3>
<p><code>GlobalExceptionHandler</code>가 정상적으로 동작하는지 확인해보자</p>
<pre><code class="language-java">@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 필수 입력값 누락 예외 처리
    @ExceptionHandler(IllegalArgumentException.class)
    protected ResponseEntity&lt;CommonResponse&gt; handleIllegalArgumentException(IllegalArgumentException exception) {
        log.error(&quot;필수 입력값이 누락되었습니다.&quot;);

        ErrorCode errorCode = ErrorCode.LOGIN_MISSING_FIELDS;

        ErrorResponse error = ErrorResponse.builder()
                .status(errorCode.getStatus().value())
                .message(errorCode.getMessage())
                .code(errorCode.getCode())
                .build();

        CommonResponse response = CommonResponse.builder()
                .success(false)
                .error(error)
                .build();

        return ResponseEntity.status(errorCode.getStatus()).body(response);
    }

    // JWT 토큰 만료 예외 처리
    @ExceptionHandler(ExpiredJwtException.class)
    protected ResponseEntity&lt;CommonResponse&gt; handleExpiredJwtException(ExpiredJwtException exception) {
        log.error(&quot;JWT 토큰 만료: {}&quot;, exception.getMessage());

        ErrorCode errorCode = ErrorCode.TOKEN_EXPIRED;

        ErrorResponse error = ErrorResponse.builder()
                .status(errorCode.getStatus().value())
                .message(errorCode.getMessage())
                .code(errorCode.getCode())
                .build();

        CommonResponse response = CommonResponse.builder()
                .success(false)
                .error(error)
                .build();

        return ResponseEntity.status(errorCode.getStatus()).body(response);
    }
}</code></pre>
<hr>
<h3 id="3-postman을-이용한-테스트"><strong>3) Postman을 이용한 테스트</strong></h3>
<p>ex)</p>
<pre><code>{
  &quot;username&quot;: &quot;&quot;,
  &quot;password&quot;: &quot;password123&quot;
}</code></pre><p>필수 입력값 누락 시, 응답 예시:</p>
<pre><code>{
  &quot;success&quot;: false,
  &quot;error&quot;: {
    &quot;status&quot;: 400,
    &quot;message&quot;: &quot;필수 입력값이 누락되었습니다.&quot;,
    &quot;code&quot;: &quot;LOGIN_MISSING_FIELDS&quot;
  }
}</code></pre><p>JWT 토큰 만료 시, 응답 예시:</p>
<pre><code>{
  &quot;success&quot;: false,
  &quot;error&quot;: {
    &quot;status&quot;: 401,
    &quot;message&quot;: &quot;JWT 토큰이 만료되었습니다.&quot;,
    &quot;code&quot;: &quot;TOKEN_EXPIRED&quot;
  }
}</code></pre><hr>
<h2 id="결론">결론</h2>
<p><code>@ComponentScan</code>을 이용해 Common 모듈의 핸들러를 다른 모듈에서도 인식하도록 설정한다</p>
<p>Componentscan의 역할 추가</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Kafka Consumer 역직렬화 오류 및 공통 모듈 활용 ]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-Consumer-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98-%EB%B0%8F-%EA%B3%B5%ED%86%B5-%EB%AA%A8%EB%93%88-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-Consumer-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98-%EB%B0%8F-%EA%B3%B5%ED%86%B5-%EB%AA%A8%EB%93%88-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Thu, 27 Feb 2025 13:00:11 GMT</pubDate>
            <description><![CDATA[<h2 id="kafka-consumer-역직렬화-오류-및-공통-모듈-활용-트러블슈팅">Kafka Consumer 역직렬화 오류 및 공통 모듈 활용 트러블슈팅</h2>
<h2 id="문제-상황">&lt;문제 상황&gt;</h2>
<p>Kafka 메시지를 Consumer에서 처리하는 과정에서 아래와 같은 <code>ClassNotFoundException</code>이 발생했다.</p>
<pre><code>Caused by: java.lang.ClassNotFoundException: finpago.matchingservice.matching.messaging.events.TradeMatchingEvent</code></pre><p>이는 Execution Service에서 Kafka 메시지를 역직렬화할 때 <code>TradeMatchingEvent</code> 클래스를 찾을 수 없어 발생하는 문제다.</p>
<hr>
<h2 id="문제-원인">&lt;문제 원인&gt;</h2>
<p>원래 각 모듈마다  <code>TradeMatchingEvent</code>을 넣어서 사용햇는데 (객체 내용으로 판별하는줄)
그 파일 경로로 판단하는거여서 경로가 달라서 역직렬화 오류가 생기는거엿다</p>
<p>현재 Matching Service가 <code>TradeMatchingEvent</code>를 Kafka에 전송하고 있지만, Execution Service에서는 해당 클래스를 찾을 수 없다. </p>
<p>이는 <code>TradeMatchingEvent</code>가 Matching Service의 패키지 경로에 존재하기 때문이며, Execution Service에서는 이를 로드할 수 없는 상태이다.</p>
<h3 id="오류-메시지">오류 메시지</h3>
<pre><code>Caused by: java.lang.ClassNotFoundException: finpago.matchingservice.matching.messaging.events.TradeMatchingEvent</code></pre><p>Execution Service가 Matching Service에서 전송한 <code>TradeMatchingEvent</code>를 역직렬화하려고 하지만, 해당 클래스가 존재하지 않아 역직렬화에 실패한다.</p>
<hr>
<h2 id="해결-방법">&lt;해결 방법&gt;</h2>
<h3 id="1-matching-모듈과-execution-모듈이-동일한-tradematchingevent-클래스를-사용하도록-설정">1. Matching 모듈과 Execution 모듈이 동일한 <code>TradeMatchingEvent</code> 클래스를 사용하도록 설정</h3>
<p>현재 Matching 모듈과 Execution 모듈이 서로 다른 패키지에서 <code>TradeMatchingEvent</code>를 정의하고 있기 때문에, Kafka 메시지를 처리할 때 클래스가 일치하지 않아 역직렬화 오류가 발생한다.</p>
<p>해결책: 공통 모듈에  <code>TradeMatchingEvent</code>와 <code>OrderCreateReqEvent</code>을 생성하여 공유</p>
<ol>
<li><p><code>TradeMatchingEvent</code>, <code>OrderCreateReqEvent</code> 클래스를 <code>common</code> 모듈로 이동한다.</p>
<ul>
<li>경로: <code>finpago.common.messaging.events.TradeMatchingEvent</code></li>
</ul>
</li>
<li><p>MatchingService 및 ExecutionService가 공통 모듈을 의존하도록 설정</p>
<ul>
<li><p>각 서비스의 <code>build.gradle</code>에 다음과 같이 추가한다.</p>
<pre><code>dependencies {
  implementation project(&#39;:common&#39;)
}</code></pre></li>
</ul>
</li>
<li><p>Kafka Consumer 및 Producer 설정에서 올바른 패키지 경로를 지정</p>
<ul>
<li><p>ExecutionService의 Kafka Consumer 설정 수정</p>
<pre><code class="language-java">@Bean
public ConsumerFactory&lt;String, TradeMatchingEvent&gt; tradeConsumerFactory() {
  Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
  props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
  props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
  props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
  props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);

  // 공통 모듈의 패키지로 변경
  props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, &quot;finpago.common.messaging.events.TradeMatchingEvent&quot;);
  props.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;finpago.common.messaging.events&quot;);

  return new DefaultKafkaConsumerFactory&lt;&gt;(props);
}</code></pre>
</li>
<li><p><strong>MatchingService의 Kafka Producer 설정 수정</strong></p>
<pre><code>@Bean
public ProducerFactory&lt;String, TradeMatchingEvent&gt; tradeProducerFactory() {
  Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
  props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
  props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
  props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);

  // 헤더 정보를 추가하여 직렬화 문제 방지
  props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, true);

  return new DefaultKafkaProducerFactory&lt;&gt;(props);
}</code></pre></li>
</ul>
</li>
</ol>
<h3 id="2-matching-모듈과-execution-모듈의-trusted_packages-설정">2. Matching 모듈과 Execution 모듈의 <code>TRUSTED_PACKAGES</code> 설정</h3>
<p>공통 모듈을 사용하더라도 <code>Kafka Consumer</code>가 특정 패키지만 신뢰하도록 설정하면 역직렬화 오류가 발생할 수 있다. </p>
<p>이를 방지하기 위해 <code>TRUSTED_PACKAGES</code> 설정을 수정해야 한다.</p>
<pre><code class="language-java">@Bean
public ConsumerFactory&lt;String, TradeMatchingEvent&gt; tradeConsumerFactory() {
    Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
    props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);

    // Matching 모듈에서 보낸 메시지를 신뢰할 수 있도록 설정
    props.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;finpago.common.messaging.events&quot;);

    return new DefaultKafkaConsumerFactory&lt;&gt;(props);
}</code></pre>
<h2 id="결론">결론</h2>
<p>Kafka 메시지 역직렬화 오류(<code>ClassNotFoundException</code>)는 MatchingService와 ExecutionService가 서로 다른 패키지에 동일한 클래스를 정의할 때 발생할 수 있다. </p>
<p>이를 해결하려면 공통 모듈(<code>common</code>)을 만들어 모든 서비스에서 동일한 이벤트 클래스를 사용하도록 구성해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Kafka Consumer 역직렬화 오류 및 공통 모듈 활용 ]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-Consumer-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98-%EB%B0%8F-%EA%B3%B5%ED%86%B5-%EB%AA%A8%EB%93%88-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-Consumer-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98-%EB%B0%8F-%EA%B3%B5%ED%86%B5-%EB%AA%A8%EB%93%88-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Thu, 27 Feb 2025 13:00:04 GMT</pubDate>
            <description><![CDATA[<h2 id="kafka-메시지-역직렬화-및-retry-설정-트러블슈팅">Kafka 메시지 역직렬화 및 Retry 설정 트러블슈팅</h2>
<h2 id="문제-상황">&lt;문제 상황&gt;</h2>
<p>Kafka 메시지를 Consumer에서 처리하는 과정에서 아래와 같은 <code>ClassNotFoundException</code>이 발생했다.</p>
<pre><code>Caused by: java.lang.ClassNotFoundException: finpago.matchingservice.matching.messaging.events.TradeMatchingEvent</code></pre><p>이는 Execution Service에서 Kafka 메시지를 역직렬화할 때 <code>TradeMatchingEvent</code> 클래스를 찾을 수 없어 발생하는 문제다.</p>
<hr>
<h2 id="문제-원인">&lt;문제 원인&gt;</h2>
<p>원래 각 모듈마다  <code>TradeMatchingEvent</code>을 넣어서 사용햇는데 (객체 내용으로 판별하는줄)
그 파일 경로로 판단하는거여서 경로가 달라서 역직렬화 오류가 생기는거엿다</p>
<p>현재 Matching Service가 <code>TradeMatchingEvent</code>를 Kafka에 전송하고 있지만, Execution Service에서는 해당 클래스를 찾을 수 없다. </p>
<p>이는 <code>TradeMatchingEvent</code>가 Matching Service의 패키지 경로에 존재하기 때문이며, Execution Service에서는 이를 로드할 수 없는 상태이다.</p>
<h3 id="오류-메시지">오류 메시지</h3>
<pre><code>Caused by: java.lang.ClassNotFoundException: finpago.matchingservice.matching.messaging.events.TradeMatchingEvent</code></pre><p>Execution Service가 Matching Service에서 전송한 <code>TradeMatchingEvent</code>를 역직렬화하려고 하지만, 해당 클래스가 존재하지 않아 역직렬화에 실패한다.</p>
<hr>
<h2 id="해결-방법">&lt;해결 방법&gt;</h2>
<h3 id="1-matching-모듈과-execution-모듈이-동일한-tradematchingevent-클래스를-사용하도록-설정">1. Matching 모듈과 Execution 모듈이 동일한 <code>TradeMatchingEvent</code> 클래스를 사용하도록 설정</h3>
<p>현재 Matching 모듈과 Execution 모듈이 서로 다른 패키지에서 <code>TradeMatchingEvent</code>를 정의하고 있기 때문에, Kafka 메시지를 처리할 때 클래스가 일치하지 않아 역직렬화 오류가 발생한다.</p>
<p>해결책: 공통 모듈에  <code>TradeMatchingEvent</code>와 <code>OrderCreateReqEvent</code>을 생성하여 공유</p>
<ol>
<li><p><code>TradeMatchingEvent</code>, <code>OrderCreateReqEvent</code> 클래스를 <code>common</code> 모듈로 이동한다.</p>
<ul>
<li>경로: <code>finpago.common.messaging.events.TradeMatchingEvent</code></li>
</ul>
</li>
<li><p>MatchingService 및 ExecutionService가 공통 모듈을 의존하도록 설정</p>
<ul>
<li><p>각 서비스의 <code>build.gradle</code>에 다음과 같이 추가한다.</p>
<pre><code>dependencies {
  implementation project(&#39;:common&#39;)
}</code></pre></li>
</ul>
</li>
<li><p>Kafka Consumer 및 Producer 설정에서 올바른 패키지 경로를 지정</p>
<ul>
<li><p>ExecutionService의 Kafka Consumer 설정 수정</p>
<pre><code class="language-java">@Bean
public ConsumerFactory&lt;String, TradeMatchingEvent&gt; tradeConsumerFactory() {
  Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
  props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
  props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
  props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
  props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);

  // 공통 모듈의 패키지로 변경
  props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, &quot;finpago.common.messaging.events.TradeMatchingEvent&quot;);
  props.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;finpago.common.messaging.events&quot;);

  return new DefaultKafkaConsumerFactory&lt;&gt;(props);
}</code></pre>
</li>
<li><p><strong>MatchingService의 Kafka Producer 설정 수정</strong></p>
<pre><code>@Bean
public ProducerFactory&lt;String, TradeMatchingEvent&gt; tradeProducerFactory() {
  Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
  props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
  props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
  props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);

  // 헤더 정보를 추가하여 직렬화 문제 방지
  props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, true);

  return new DefaultKafkaProducerFactory&lt;&gt;(props);
}</code></pre></li>
</ul>
</li>
</ol>
<h3 id="2-matching-모듈과-execution-모듈의-trusted_packages-설정">2. Matching 모듈과 Execution 모듈의 <code>TRUSTED_PACKAGES</code> 설정</h3>
<p>공통 모듈을 사용하더라도 <code>Kafka Consumer</code>가 특정 패키지만 신뢰하도록 설정하면 역직렬화 오류가 발생할 수 있다. </p>
<p>이를 방지하기 위해 <code>TRUSTED_PACKAGES</code> 설정을 수정해야 한다.</p>
<pre><code class="language-java">@Bean
public ConsumerFactory&lt;String, TradeMatchingEvent&gt; tradeConsumerFactory() {
    Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
    props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);

    // Matching 모듈에서 보낸 메시지를 신뢰할 수 있도록 설정
    props.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;finpago.common.messaging.events&quot;);

    return new DefaultKafkaConsumerFactory&lt;&gt;(props);
}</code></pre>
<h2 id="결론">결론</h2>
<p>Kafka 메시지 역직렬화 오류(<code>ClassNotFoundException</code>)는 MatchingService와 ExecutionService가 서로 다른 패키지에 동일한 클래스를 정의할 때 발생할 수 있다. </p>
<p>이를 해결하려면 공통 모듈(<code>common</code>)을 만들어 모든 서비스에서 동일한 이벤트 클래스를 사용하도록 구성해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Kafka Retry 설정 ]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-Retry-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-Retry-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 27 Feb 2025 12:58:45 GMT</pubDate>
            <description><![CDATA[<h2 id="kafka-메시지-역직렬화-및-retry-설정-트러블슈팅">Kafka 메시지 역직렬화 및 Retry 설정 트러블슈팅</h2>
<h2 id="문제-상황">&lt;문제 상황&gt;</h2>
<p>Kafka 메시지를 Consumer에서 처리하는 과정에서 아래와 같은 <code>Bean</code> 관련 오류가 발생했다.</p>
<pre><code>A component required a bean named &#39;kafkaRetryListenerContainerFactory&#39; that could not be found.</code></pre><p>이는 <code>KafkaRetryConfig</code>에서 <code>kafkaRetryListenerContainerFactory</code>라는 Bean이 존재하지 않아서 발생하는 문제다.</p>
<hr>
<h2 id="문제-원인">&lt;문제 원인&gt;</h2>
<p>현재 <code>KafkaRetryConfig</code>에서 <code>kafkaRetryTradeListenerContainerFactory</code>와 <code>kafkaRetryOrderListenerContainerFactory</code> 두 개의 Bean을 선언했지만, <code>KafkaListener</code>가 기본적으로 참조하는 <code>kafkaRetryListenerContainerFactory</code>라는 이름의 Bean이 존재하지 않아서 오류가 발생했다.</p>
<hr>
<h2 id="해결-방법">&lt;해결 방법&gt;</h2>
<h3 id="1-kafkaretrytradelistenercontainerfactory와-kafkaretryorderlistenercontainerfactory를-분리한-이유">1. <code>kafkaRetryTradeListenerContainerFactory</code>와 <code>kafkaRetryOrderListenerContainerFactory</code>를 분리한 이유</h3>
<p>각 메시지 타입(<code>TradeMatchingEvent</code>, <code>OrderCreateReqEvent</code>)이 다르기 때문에 별도의 <code>ConcurrentKafkaListenerContainerFactory</code>를 생성하여 사용해야 한다.</p>
<ul>
<li><strong><code>kafkaRetryTradeListenerContainerFactory</code></strong><ul>
<li><code>TradeMatchingEvent</code> 메시지를 처리하는 Consumer의 리스너 팩토리</li>
<li>메시지 역직렬화 후 <code>TradeMatchingEvent</code> 타입으로 변환하여 사용</li>
<li>실패 시 3번 재시도 후 <code>DLT(Dead Letter Topic)</code>으로 이동</li>
</ul>
</li>
<li><strong><code>kafkaRetryOrderListenerContainerFactory</code></strong><ul>
<li><code>OrderCreateReqEvent</code> 메시지를 처리하는 Consumer의 리스너 팩토리</li>
<li>메시지 역직렬화 후 <code>OrderCreateReqEvent</code> 타입으로 변환하여 사용</li>
<li>실패 시 3번 재시도 후 <code>DLT</code>로 이동</li>
</ul>
</li>
</ul>
<p>각 이벤트마다 처리 방식이 다를 수 있으므로 개별적으로 <code>ConsumerFactory</code>를 설정하는 것이 유리하다.</p>
<h3 id="2-kafkaretrylistenercontainerfactory-추가해야-하는-이유">2. <code>kafkaRetryListenerContainerFactory</code> 추가해야 하는 이유</h3>
<p><code>KafkaListener</code>가 기본적으로 <code>kafkaRetryListenerContainerFactory</code>라는 이름의 Bean을 찾기 때문에 이 Bean이 등록되지 않으면 <code>ApplicationContext</code>가 시작되지 않는다.</p>
<ul>
<li><strong>추가된 <code>kafkaRetryListenerContainerFactory</code>의 역할</strong><ul>
<li>모든 Kafka 메시지 유형을 처리할 수 있도록 <code>Object</code> 타입의 Consumer 설정</li>
<li><code>DeadLetterPublishingRecoverer</code>를 통해 실패한 메시지를 DLT로 전송</li>
<li><code>DefaultErrorHandler</code>를 적용하여 3번 재시도 후 DLT로 이동</li>
</ul>
</li>
</ul>
<h3 id="수정된-kafkaretryconfigjava">수정된 KafkaRetryConfig.java</h3>
<pre><code class="language-java">@Bean(name = &quot;kafkaRetryListenerContainerFactory&quot;)
public ConcurrentKafkaListenerContainerFactory&lt;String, Object&gt; kafkaRetryListenerContainerFactory(
        ConsumerFactory&lt;String, Object&gt; consumerFactory,
        KafkaTemplate&lt;String, Object&gt; kafkaTemplate) {

    ConcurrentKafkaListenerContainerFactory&lt;String, Object&gt; factory = new ConcurrentKafkaListenerContainerFactory&lt;&gt;();
    factory.setConsumerFactory(consumerFactory);

    // DLT 설정
    DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(
            kafkaTemplate,
            (ConsumerRecord&lt;?, ?&gt; record, Exception e) -&gt;
                    new TopicPartition(DLT_TOPIC, record.partition())
    );

    // 3번 재시도 후 DLT로 이동
    DefaultErrorHandler errorHandler = new DefaultErrorHandler(recoverer, new FixedBackOff(RETRY_INTERVAL, RETRY_COUNT));

    factory.setCommonErrorHandler(errorHandler);
    return factory;
}</code></pre>
<hr>
<h2 id="결론">결론</h2>
<p>Kafka 메시지 처리를 위한 Retry 설정에서 <code>kafkaRetryTradeListenerContainerFactory</code>와 <code>kafkaRetryOrderListenerContainerFactory</code>를 각각 분리한 이유는 개별적인 메시지 유형에 맞게 처리하기 위해서이다.</p>
<p>하지만, KafkaListener가 기본적으로 찾는 <code>kafkaRetryListenerContainerFactory</code>가 존재하지 않으면 애플리케이션이 실행되지 않으므로, 이를 추가하여 모든 메시지를 처리할 수 있도록 설정해야 한다. 이렇게 함으로써 Kafka 메시지의 안정적인 소비와 오류 처리를 보장할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Kafka 메시지 역직렬화 오류]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@greenlemon_t/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Kafka-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Thu, 27 Feb 2025 12:56:42 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">&lt;문제 상황&gt;</h2>
<p>Kafka 메시지를 Consumer에서 처리하는 과정에서 아래와 같은 <code>SerializationException</code>이 발생했다.</p>
<pre><code>Caused by: java.lang.IllegalStateException: No type information in headers and no default type provided</code></pre><p>이는 Kafka 메시지의 헤더에 타입 정보(<code>spring.json.type</code>)가 없고, 기본적으로 역직렬화할 타입이 설정되지 않아 발생하는 문제다.</p>
<hr>
<h2 id="문제-원인">&lt;문제 원인&gt;</h2>
<p>Spring Kafka에서 <code>JsonDeserializer</code>를 사용할 경우 기본적으로 Kafka 메시지에 타입 정보를 포함한 헤더가 필요하다. 하지만 현재 <code>Producer</code>가 메시지를 보낼 때 이 정보를 포함하지 않아 <code>Consumer</code>가 어떤 타입으로 역직렬화해야 할지 알 수 없는 상태이다.</p>
<hr>
<h2 id="해결-방법">&lt;해결 방법&gt;</h2>
<h3 id="1-kafka-producer에서-타입-정보를-포함하도록-설정">1. Kafka Producer에서 타입 정보를 포함하도록 설정</h3>
<p>KafkaTemplate이 메시지를 직렬화할 때 타입 정보를 헤더에 추가하도록 설정해야 한다.</p>
<p> <strong>KafkaConfig.java</strong> </p>
<pre><code class="language-java">@Bean
public ProducerFactory&lt;String, OrderCreateReqEvent&gt; producerFactory() {
    Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
    props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, true); // 타입 정보를 헤더에 포함

    return new DefaultKafkaProducerFactory&lt;&gt;(props);
}</code></pre>
<p>위 설정에서 <code>JsonSerializer.ADD_TYPE_INFO_HEADERS</code> 값을 <code>true</code>로 설정하면 Kafka 메시지의 헤더에 타입 정보가 포함된다. </p>
<p>이를 설정하면 <code>Consumer</code>가 메시지를 받을 때 자동으로 적절한 타입으로 역직렬화할 수 있다.</p>
<h3 id="2-kafka-consumer에서-기본-역직렬화-타입을-설정">2. Kafka Consumer에서 기본 역직렬화 타입을 설정</h3>
<p>Kafka 메시지에 헤더 없이 오는 경우에도 처리할 수 있도록 <code>Consumer</code>에서 기본 타입을 설정해야 한다.</p>
<p><strong>KafkaConfig.java</strong> </p>
<pre><code class="language-java">@Bean
public ConsumerFactory&lt;String, OrderCreateReqEvent&gt; consumerFactory() {
    Map&lt;String, Object&gt; props = new HashMap&lt;&gt;();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
    props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
    props.put(JsonDeserializer.TRUSTED_PACKAGES, &quot;*&quot;); // 모든 패키지를 신뢰하도록 설정
    props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, &quot;finpago.matchingservice.matching.messaging.events.OrderCreateReqEvent&quot;); // 기본 타입 설정

    return new DefaultKafkaConsumerFactory&lt;&gt;(props, new StringDeserializer(), new JsonDeserializer&lt;&gt;(OrderCreateReqEvent.class));
}</code></pre>
<p>위 설정에서 <code>JsonDeserializer.VALUE_DEFAULT_TYPE</code> 값을 <code>OrderCreateReqEvent</code> 클래스로 지정하면, Kafka 메시지에 타입 정보가 없더라도 기본 타입으로 역직렬화할 수 있다.</p>
<hr>
<h2 id="결론">결론</h2>
<p>Kafka 메시지 역직렬화 오류는 <code>JsonDeserializer</code>가 메시지의 타입을 알지 못할 때 발생한다. 이를 해결하려면 다음과 같이 설정하면 된다.</p>
<ul>
<li><strong>Producer에서 <code>JsonSerializer.ADD_TYPE_INFO_HEADERS = true</code>로 설정하여 타입 정보를 포함하도록 변경</strong></li>
<li><strong>Consumer에서 <code>JsonDeserializer.VALUE_DEFAULT_TYPE</code>을 설정하여 기본 타입을 제공</strong></li>
</ul>
<p>이러한 설정을 적용하면 Kafka 메시지를 안정적으로 처리할 수 있고, 타입 정보가 없는 메시지로 인해 발생하는 역직렬화 오류를 방지할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] 예수금, 주문가능금액의 분리 이유]]></title>
            <link>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%98%88%EC%88%98%EA%B8%88-%EC%A3%BC%EB%AC%B8%EA%B0%80%EB%8A%A5%EA%B8%88%EC%95%A1%EC%9D%98-%EB%B6%84%EB%A6%AC-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%98%88%EC%88%98%EA%B8%88-%EC%A3%BC%EB%AC%B8%EA%B0%80%EB%8A%A5%EA%B8%88%EC%95%A1%EC%9D%98-%EB%B6%84%EB%A6%AC-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Thu, 27 Feb 2025 12:50:42 GMT</pubDate>
            <description><![CDATA[<h2 id="해외주식-d2-정산-방식">해외주식 D+2 정산 방식</h2>
<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/8b56ff72-939e-4897-9d96-b3d4a9ffb981/image.png" alt="">
<img src="https://velog.velcdn.com/images/greenlemon_t/post/2d970951-7fdc-4588-a0f1-62e99d1852d1/image.png" alt=""></p>
<p>해외 주식 거래에서는 D+2 결제 방식이 적용된다. 즉, 매수 또는 매도가 이루어져도 실제 금액은 거래일(D)로부터 2일 후(D+2)에 정산 및 입출금 가능하다.
이 과정에서 예수금(증거금)과 출금 가능 금액을 어떻게 관리할 것인지가 중요하다.
특히, 매수와 매도의 예수금 반영 방식이 다르기 때문에, 출금 가능 금액을 즉시 반영하면 안 되는 경우를 잘 처리해야 한다.</p>
<hr>
<h3 id="1-해외-주식-거래의-d2-결제-방식">1. 해외 주식 거래의 D+2 결제 방식</h3>
<ul>
<li><p>매수 시 (D+2 결제 적용)
주문 즉시 예수금이 차감되지만, 실제 출금은 D+2일에 이루어진다.
출금 가능 금액은 바로 감소하며, 2일 후 정산이 완료되면 최종 반영된다.</p>
</li>
<li><p>매도 시 (D+2 결제 적용)
매도 즉시 증거금이 증가하지만, 매도 대금은 D+2일에 입금된다.
따라서 매도 시점에서 출금 가능 금액을 즉시 증가시키면 안 된다.
정산이 완료된 후(D+2)에 출금 가능 금액을 증가시켜야 한다.</p>
</li>
</ul>
<p>-&gt; 매수는 즉시 출금 가능 금액 감소
-&gt; 매도는 즉시 출금 가능 금액 증가 X → D+2 정산 후 증가</p>
<h3 id="2-트레이딩-시스템에서-예수금-관리">2. 트레이딩 시스템에서 예수금 관리</h3>
<h3 id="주문-모듈에서-예수금-반영-validateanddeductavailablebalance">주문 모듈에서 예수금 반영 (validateAndDeductAvailableBalance)</h3>
<ul>
<li>매수 주문 시 사용 가능 예수금(availableBalance) 를 확인 후 차감</li>
<li>예수금 부족 시 InsufficientBalanceException 발생</li>
<li>차감된 금액은 updateAvailableBalance 메서드를 통해 반영</li>
</ul>
<p>private void validateAndDeductAvailableBalance(Long userId, OrderCreateReqDto orderCreateReqDto) {
    Long availableBalance = getCachedAvailableBalance(userId);
    Long requiredAmount = orderCreateReqDto.getOfferPrice() * orderCreateReqDto.getOfferQuantity();</p>
<pre><code>if (availableBalance &lt; requiredAmount) {
    throw new InsufficientBalanceException(&quot;예수금이 부족합니다&quot;);
}

updateAvailableBalance(userId, -requiredAmount);</code></pre><p>}</p>
<ul>
<li>주문 시 처리 방식
매수 주문 → 사용 가능 예수금 감소 (출금 가능 금액도 즉시 감소)
매도 주문 → 사용 가능 예수금 증가 (출금 가능 금액은 증가 X)</li>
</ul>
<h3 id="정산-모듈에서-출금-가능-금액-반영-processbuysettlement">정산 모듈에서 출금 가능 금액 반영 (processBuySettlement)</h3>
<p>processBuySettlement 메서드는 정산이 완료된 매수 주문을 처리한다.
매수 체결 후 D+2일에 출금 가능 금액을 증가시키도록 처리해야 한다.</p>
<pre><code>    @Transactional
    public void processBuySettlement(BuyTradeMatchEvent event) {
    validateBuyerBalance(event);  
    Float exchangeRate = getExchangeRate(event.getStockTicker());  
    event.setExchangeRate(exchangeRate);  

    updateBatchBalance(event.getBuyerUserId());  
    updateBalance(event.getBuyerUserId());  

    updateHoldings(event.getBuyerUserId(), event.getStockTicker(), event.getTradeQuantity());  
    updateStockForFxTracking(event.getBuyerUserId(), event.getStockTicker(), event.getTradeQuantity(), exchangeRate);  

    settlementProducer.sendBuySettlementSuccess(event);  
    log.info(&quot;매수 정산 완료: {}&quot;, event);  
}</code></pre><ul>
<li>정산 시 처리 방식
매수 주문 → 정산 완료 시 출금 가능 금액 증가
매도 주문 → 정산 완료 시 출금 가능 금액 증가 (즉시 반영 X)</li>
</ul>
<h3 id="3-해결해야-할-문제-매도-시-출금-가능-금액-반영-타이밍">3. 해결해야 할 문제: 매도 시 출금 가능 금액 반영 타이밍</h3>
<p>매수 시에는 출금 가능 금액이 즉시 차감되지만,
매도 시 출금 가능 금액을 즉시 증가시키면 안 된다.</p>
<ul>
<li><p>해결 방법:
매수는 주문 시 즉시 출금 가능 금액을 차감
매도는 정산 완료 후(D+2)에 출금 가능 금액을 증가시키도록 처리</p>
</li>
<li><p>적용 방안:
매도 주문 시 예수금 증가, 출금 가능 금액 증가 X
D+2일 정산이 완료되면 출금 가능 금액 증가</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] 부분체결이 문제로다]]></title>
            <link>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B6%80%EB%B6%84%EC%B2%B4%EA%B2%B0%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4</link>
            <guid>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%B6%80%EB%B6%84%EC%B2%B4%EA%B2%B0%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4</guid>
            <pubDate>Thu, 27 Feb 2025 12:47:31 GMT</pubDate>
            <description><![CDATA[<h3 id="kafka-기반-트레이딩-시스템---체결-및-부분-체결-처리">Kafka 기반 트레이딩 시스템 - 체결 및 부분 체결 처리</h3>
<p>트레이딩 시스템에서 주문 매칭(Matching) 은 체결(Execution) 단계 이전에 수행되는 핵심 프로세스다.
매수와 매도 주문이 체결되려면 가격과 수량이 맞아야 하고,
완전히 체결되지 않으면 부분 체결(pending) 또는 미체결(Unmatched) 상태로 유지되어야 한다.</p>
<hr>
<h3 id="1-매칭-서비스의-핵심-역할">1. 매칭 서비스의 핵심 역할</h3>
<p>매칭 서비스는 주문을 접수하고, 체결 가능한 주문을 찾아 처리하는 역할을 한다.</p>
<ul>
<li>핵심 기능
주문을 우선순위 큐에 저장하여 시간순 정렬
Redis에서 최신 20개의 체결 데이터를 가져와 최신 체결가 반영
주문과 체결 데이터를 비교하여 완전 체결, 부분 체결, 미체결 상태 관리
체결된 주문을 Kafka를 통해 Execution 모듈로 전송
10초 내에 체결되지 않으면 Order 모듈로 전송하여 미체결 처리</li>
</ul>
<h3 id="2-주문-접수-및-매칭-시작">2. 주문 접수 및 매칭 시작</h3>
<pre><code>public void processOrder(OrderCreateReqEvent order) {
    log.info(&quot;주문 접수: {}&quot;, order);

    orders.offer(order); // 주문을 큐에 추가
    processMatching(); // 매칭 프로세스 실행
}
</code></pre><p>1) 사용자의 매수 또는 매도 주문을 orders 큐에 저장
2) 저장된 주문을 처리하기 위해 processMatching() 실행</p>
<h3 id="3-주문-매칭-로직-processmatching">3. 주문 매칭 로직 (processMatching)</h3>
<pre><code>@Transactional
protected void processMatching() {
    log.info(&quot;매칭 시작&quot;);
    long startTime = System.currentTimeMillis();

    while (!orders.isEmpty()) {
        if (System.currentTimeMillis() - startTime &gt; MAX_WAIT_TIME) {
            log.warn(&quot;5분 초과 - 미체결 주문을 Order 모듈로 전송&quot;);
            moveUnmatchedOrdersToQueue();
            sendUnmatchedOrdersToOrderService();
            return;
        }

        OrderCreateReqEvent order = orders.poll(); // 주문을 하나 가져옴
        List&lt;Map&lt;String, Object&gt;&gt; recentTrades = getRecentTradesFromRedis(order.getStockTicker());

        if (!recentTrades.isEmpty()) {
            // 가격 기준 정렬
            List&lt;Map&lt;String, Object&gt;&gt; sortedTrades = recentTrades.stream()
                    .sorted(Comparator.comparing(trade -&gt; (long) trade.get(&quot;current_price&quot;)))
                    .toList();

            long maxTradePrice = (long) sortedTrades.get(sortedTrades.size() - 1).get(&quot;current_price&quot;);
            long minTradePrice = (long) sortedTrades.get(0).get(&quot;current_price&quot;);

            if (order.getOfferType() == OrderType.BUY) {
                handleBuyOrder(order, sortedTrades, minTradePrice);
            } else {
                handleSellOrder(order, sortedTrades, maxTradePrice);
            }
        }

        if (System.currentTimeMillis() - startTime &gt; MAX_WAIT_TIME) {
            log.warn(&quot;5분 초과 - 미체결 주문을 Order 모듈로 전송&quot;);
            moveUnmatchedOrdersToQueue();
            sendUnmatchedOrdersToOrderService();
        }
    }
}
</code></pre><p> 매칭 프로세스를 실행하면 주문을 하나씩 꺼내서 처리
 Redis에서 최신 체결 데이터를 가져와 최근 가격을 확인
 매수 주문이면 최저가(minTradePrice), 매도 주문이면 최고가(maxTradePrice)를 기준으로 매칭
 10초 내에 매칭되지 않으면 미체결 주문으로 Order 모듈에 전달</p>
<h3 id="4-매수-주문-처리-handlebuyorder">4. 매수 주문 처리 (handleBuyOrder)</h3>
<pre><code>private void handleBuyOrder(OrderCreateReqEvent order, List&lt;Map&lt;String, Object&gt;&gt; sortedTrades, long minTradePrice) {
    long currentPrice = minTradePrice;

    if (order.getOfferPrice() &gt; minTradePrice) {
        handleTradeExecution(order, order.getOfferQuantity(), 0L, currentPrice, true);
        long refundAmount = (order.getOfferPrice() - currentPrice) * order.getOfferQuantity();
        updateAvailableBalance(order.getUserId(), refundAmount);
    } else {
        int matchedIndex = linearSearch(sortedTrades, order.getOfferPrice());

        if (matchedIndex != -1) {
            Map&lt;String, Object&gt; matchedTrade = sortedTrades.get(matchedIndex);
            long tradePrice = Math.round(((Number) matchedTrade.get(&quot;current_price&quot;)).doubleValue());
            long tradeVolume = ((Number) matchedTrade.get(&quot;volume&quot;)).longValue();

            long orderQuantity = order.getOfferQuantity();
            long matchedQuantity = Math.min(orderQuantity, tradeVolume);
            long unfilledQuantity = orderQuantity - matchedQuantity;

            handleTradeExecution(order, matchedQuantity, unfilledQuantity, tradePrice, true);
            order.setOfferQuantity(unfilledQuantity);

            if (unfilledQuantity &gt; 0) {
                orders.offer(order);
            }
        } else {
            handleTradeExecution(order, 0, order.getOfferQuantity(), order.getOfferPrice(), true);
        }
    }
}
</code></pre><p> 매수 주문이 들어오면 최저 체결가(minTradePrice)와 비교
 주문 가격이 체결 가능 가격보다 높으면 바로 체결 후 남은 금액을 환불
 체결이 가능한 주문이 있으면 매칭 후 일부 체결(Partial Fill) 가능
 체결되지 않은 주문은 다시 큐에 넣어 미체결 상태 유지</p>
<h3 id="5-체결-완료-및-부분-체결-처리-handletradeexecution">5. 체결 완료 및 부분 체결 처리 (handleTradeExecution)</h3>
<pre><code>@Transactional
protected void handleTradeExecution(OrderCreateReqEvent order, long matchedQuantity, long unfilledQuantity, long matchedPrice, boolean isBuy) {
    if (isBuy) {
        BuyTradeMatchEvent event = new BuyTradeMatchEvent(
                generateRandomId(),
                order.getOfferNumber(),
                order.getUserId(),
                order.getStockTicker(),
                matchedQuantity,
                unfilledQuantity,
                matchedPrice,
                LocalDateTime.now(),
                order.getOfferQuantity(),
                getExchangeRateFromRedis(order.getStockTicker()),
                order.getOfferPrice()
        );
        sendBuyTradeToExecution(event);
    } else {
        SellTradeMatchEvent event = new SellTradeMatchEvent(
                generateRandomId(),
                order.getOfferNumber(),
                order.getUserId(),
                order.getStockTicker(),
                matchedQuantity,
                unfilledQuantity,
                matchedPrice,
                LocalDateTime.now(),
                order.getOfferQuantity(),
                getExchangeRateFromRedis(order.getStockTicker()),
                order.getOfferPrice()
        );
        sendSellTradeToExecution(event);
    }
}
</code></pre><p>매수/매도 주문이 체결되면 BuyTradeMatchEvent,SellTradeMatchEvent 생성
 체결 수량과 체결되지 않은 수량을 포함하여 Kafka로 Execution 모듈에 전송
 부분 체결이 발생하면 남은 주문 수량을 유지하여 다시 매칭 가능</p>
<h3 id="6-미체결-주문-처리-sendunmatchedorderstoorderservice">6. 미체결 주문 처리 (sendUnmatchedOrdersToOrderService)</h3>
<pre><code>private void sendUnmatchedOrdersToOrderService() {
    while (!unmatchedOrders.isEmpty()) {
        OrderCreateReqEvent unmatchedOrder = unmatchedOrders.poll();
        matchingProducer.sendUnmatchedOrderToOrderService(unmatchedOrder);
        log.info(&quot;미체결 주문 Order 모듈 전송: {}&quot;, unmatchedOrder);
    }
}
</code></pre><p>10초 내에 체결되지 않은 주문은 Order 모듈로 전송하여 미체결 주문으로 처리
사용자가 미체결 주문을 확인하고 취소할 수 있도록 관리</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] 금융시스템은 롤백 로직 98198723개 있을거같다]]></title>
            <link>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%88%EC%9C%B5%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%80-%EB%A1%A4%EB%B0%B1-%EB%A1%9C%EC%A7%81-98198723%EA%B0%9C-%EC%9E%88%EC%9D%84%EA%B1%B0%EA%B0%99%EB%8B%A4</link>
            <guid>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%88%EC%9C%B5%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%80-%EB%A1%A4%EB%B0%B1-%EB%A1%9C%EC%A7%81-98198723%EA%B0%9C-%EC%9E%88%EC%9D%84%EA%B1%B0%EA%B0%99%EB%8B%A4</guid>
            <pubDate>Thu, 27 Feb 2025 12:41:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/b316659f-3250-4480-969f-d6a37ce683e1/image.png" alt=""></p>
<h2 id="kafka-기반-트레이딩-시스템에서-3단계-롤백-처리-설계-및-트러블슈팅">Kafka 기반 트레이딩 시스템에서 3단계 롤백 처리 설계 및 트러블슈팅</h2>
<p>트레이딩 시스템을 설계할 때, 예외 처리를 언제, 어디서 수행할 것인지가 중요하다.
특히 주문(Order), 체결(Execution), 정산(Settlement) 단계에서 예수금 부족이나 보유 주식 부족과 같은 문제가 발생할 수 있으며, 이를 효과적으로 처리하기 위해 3단계 롤백 전략을 적용했다.</p>
<h3 id="1-3단계-롤백-처리-설계-이유">1. 3단계 롤백 처리 설계 이유</h3>
<p>예수금 부족, 보유 주식 부족 같은 예외 처리를 왜 3단계에서 했을까?
트레이딩 시스템에서 롤백을 수행하는 위치는 주문 → 체결 → 정산으로 진행되며, 각 단계에서 문제가 발생하면 다음 단계로 넘어가지 않도록 방지해야 한다.</p>
<h3 id="①-주문order-단계">① 주문(Order) 단계:</h3>
<ul>
<li><p>발생할 수 있는 문제:
사용자의 예수금 부족
매도 주문 시 보유 주식 부족</p>
</li>
<li><p>왜 필요한가?
불필요한 체결 및 정산 단계로 넘어가는 것을 방지</p>
</li>
</ul>
<h3 id="②-체결execution-단계-실시간-검증-및-일부-체결">② 체결(Execution) 단계: 실시간 검증 및 일부 체결</h3>
<ul>
<li><p>발생할 수 있는 문제:
주문량보다 낮은 수량만 체결되는 경우 (부분 체결)
매칭 중 예수금 부족 / 보유 주식 부족
kafka event 처리 중 오류 발생</p>
</li>
<li><p>처리 방식:
matching-topic에서 주문을 소비하여 체결 시도</p>
</li>
<li><p>일부 체결이 가능한 경우:
주문 상태를 PENDING으로 변경
체결된 부분만 execution-topic으로 전송</p>
</li>
<li><p>체결이 불가능한 경우:
체결 실패 주문을 DLT(Dead Letter Topic)로 전송
매칭 모듈에서 재처리</p>
</li>
<li><p>왜 필요한가?
일부 체결이 가능한 경우 체결을 완료하고 나머지 주문을 다시 매칭
체결 실패 시 롤백 처리하여 불완전한 주문이 진행되지 않도록 함</p>
</li>
</ul>
<h3 id="③-정산settlement-단계-최종-검증-및-롤백">③ 정산(Settlement) 단계: 최종 검증 및 롤백</h3>
<ul>
<li><p>발생할 수 있는 문제:
체결된 주문을 정산할 때 최종적으로 예수금이 부족한 경우
kafka event 처리 중 오류 발생</p>
</li>
<li><p>처리 방식:
execution-topic에서 체결된 주문을 소비하여 정산 처리
정산 서비스에서 예수금 부족을 다시 검증
정산 실패 시 FAILED 상태로 변경하고 settlement-topic으로 전송
실패한 주문은 다시 체결 모듈로 전송하여 재시도</p>
</li>
<li><p>왜 필요한가?
정산 단계에서 최종적으로 문제가 발생하면, 체결 데이터를 롤백해야 함
만약 체결까지 완료되었는데 정산이 실패하면, 체결된 주문을 되돌리는 작업이 필요</p>
</li>
</ul>
<h3 id="2-kafka-기반-주문-롤백-처리-흐름">2. Kafka 기반 주문 롤백 처리 흐름</h3>
<p>주문 실패 시 롤백 플로우</p>
<p>1️⃣ 주문 생성 (order-topic)
주문 서비스에서 order-topic으로 주문 전송</p>
<p>2️⃣ 체결 요청 (matching-topic)
매칭 서비스에서 matching-topic으로 주문을 전송하여 체결 시도
일부 체결 가능하면 PENDING 상태로 전환 후 처리
체결 실패 주문은 DLT로 보내고 다시 매칭 시도</p>
<p>3️⃣ 체결 완료 (execution-topic)
체결된 주문을 execution-topic으로 전송하여 정산 서비스로 전달
체결 성공 시 정산 서비스에서 settlement-topic으로 전송</p>
<p>4️⃣ 정산 (settlement-topic)
정산 서비스에서 체결된 주문을 정산
정산 중 예수금 부족 등의 문제 발생 시 주문을 롤백
체결 서비스로 다시 전송하여 FAILED 처리</p>
<p>5️⃣ DLT(Dead Letter Topic) 활용
체결 또는 정산 실패한 주문은 DLT로 저장하여 재처리 가능
DLT에서 주문을 조회하여 매칭 서비스에서 재시도</p>
<h3 id="마무리">마무리</h3>
<p>금융 시스템을 개발하면서 가장 크게 느낀 점은 롤백 로직이 정말 많고, 예외 처리를 철저하게 해야 한다는 것이다.
특히 트레이딩 시스템에서는 예수금 부족, 보유 주식 부족, 체결 실패, 정산 오류 등 다양한 예외 상황이 발생할 수 있기 때문에 각 단계별로 철저한 검증과 롤백이 필수적이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] Kafka message흐름(롤백 한스푼, DLT한스푼 첨가..)]]></title>
            <link>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Kafka-message%ED%9D%90%EB%A6%84%EB%A1%A4%EB%B0%B1-%ED%95%9C%EC%8A%A4%ED%91%BC-DLT%ED%95%9C%EC%8A%A4%ED%91%BC-%EC%B2%A8%EA%B0%80</link>
            <guid>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Kafka-message%ED%9D%90%EB%A6%84%EB%A1%A4%EB%B0%B1-%ED%95%9C%EC%8A%A4%ED%91%BC-DLT%ED%95%9C%EC%8A%A4%ED%91%BC-%EC%B2%A8%EA%B0%80</guid>
            <pubDate>Thu, 27 Feb 2025 12:38:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/76289174-be5c-4dc1-ba1b-7523ba16f542/image.png" alt=""></p>
<h3 id="kafka를-사용한-msa-트레이딩-시스템-메시지-흐름">Kafka를 사용한 MSA 트레이딩 시스템 메시지 흐름</h3>
<p>트레이딩 시스템을 설계하면서 가장 고민했던 부분은 각 모듈 간의 데이터 흐름이었다. 주문, 매칭, 체결, 정산 등 여러 단계로 나뉜 프로세스를 효율적으로 연결하고, 메시지를 안전하게 전달할 방법이 필요했다. 초기에는 RabbitMQ를 고려했지만, Kafka를 선택한 이유는 다음과 같다.</p>
<h3 id="🧐-왜-rabbitmq-대신-kafka인가">🧐 왜 RabbitMQ 대신 Kafka인가?</h3>
<ol>
<li>여러 모듈에서 동일한 메시지를 소비(consume)할 필요가 있음</li>
</ol>
<p>예를 들어, 체결 내역 데이터는 유저 모듈에서도 필요하고 알림 모듈에서도 필요하다. 
RabbitMQ는 기본적으로 한 개의 컨슈머 그룹만 메시지를 소비할 수 있지만, Kafka는 여러 개의 컨슈머 그룹에서 동일한 메시지를 구독할 수 있다.</p>
<ol start="2">
<li>대량의 트랜잭션을 빠르게 처리해야 함</li>
</ol>
<p>트레이딩 시스템에서는 주문과 체결이 초당 수천 건 이상 발생할 수 있다. Kafka는 고속 스트리밍 데이터 처리에 최적화되어 있으며, 배치 처리와 로그 기반 아키텍처 덕분에 대량 데이터 전송이 유리하다.</p>
<ol start="3">
<li>메시지의 재처리 가능</li>
</ol>
<p>Kafka는 메시지를 브로커에 저장하여 필요할 때 다시 소비할 수 있다. 
반면, RabbitMQ는 메시지가 한 번 소비되면 삭제되므로 메시지 유실 가능성이 더 크다.</p>
<h3 id="4-신한투자증권이-kafka를-사용한다고">4. 신한투자증권이 Kafka를 사용한다고?</h3>
<p>신한투자증권이 Kafka를 적극 활용하고 있다는 이야기를 듣고, 실무에서도 이 기술을 다룰 기회가 많을 거라고 생각했다!</p>
<h3 id="kafka를-활용한-모듈-간-메시지-흐름">Kafka를 활용한 모듈 간 메시지 흐름</h3>
<p>1️⃣ 유저 모듈 (User Module)</p>
<ul>
<li><p>Topic: notice-topic</p>
</li>
<li><p>역할: 사용자의 주문 체결 및 계좌 변동 사항을 알림으로 저장</p>
</li>
<li><p>Message Flow:</p>
<p>정산이 완료되면 알림 메시지가 notice-topic으로 발행됨</p>
<p>유저 모듈이 notice-topic을 구독하고 알림 데이터를 저장함</p>
</li>
</ul>
<p>2️⃣ 주문 모듈 (Order Module)</p>
<ul>
<li><p>Topic: order-topic, unmatched-orders-topic, order-dlt-topic, unmatched-orders-dlt-topic</p>
</li>
<li><p>역할: 사용자의 주문을 저장하고, 매칭되지 않은 주문을 재처리</p>
</li>
<li><p>Message Flow:</p>
<p>주문이 생성되면 order-topic으로 메시지 발행</p>
<p>매칭되지 않은 주문은 unmatched-orders-topic으로 보내짐</p>
<p>DLT(Dead Letter Topic)를 활용하여 실패한 주문을 복구</p>
</li>
</ul>
<p>3️⃣ 매칭 모듈 (Matching Module)</p>
<ul>
<li><p>Topic: order-topic, trade-matching-topic, matching-dlt-topic, failed-execution-topic</p>
</li>
<li><p>역할: 매수와 매도 주문을 매칭하여 체결 가능 여부 판단</p>
</li>
<li><p>Message Flow:</p>
<p>order-topic에서 주문을 받아 매칭 로직 실행</p>
<p>매칭된 주문은 trade-matching-topic으로 발행</p>
<p>체결 실패 주문은 failed-execution-topic을 통해 다시 매칭 시도</p>
<p>매칭 실패 시 matching-dlt-topic으로 메시지 저장</p>
</li>
</ul>
<p>4️⃣ 체결 모듈 (Execution Module)</p>
<ul>
<li><p>Topic: trade-matching-topic, trade-execution-topic, execution-dlt-topic, failed-execution-topic</p>
</li>
<li><p>역할: 매칭된 주문을 체결하고, 체결 실패 시 매칭 모듈로 롤백</p>
</li>
<li><p>Message Flow:</p>
<p>trade-matching-topic에서 매칭된 주문을 받아 체결 수행</p>
<p>체결 성공 시 trade-execution-topic으로 메시지 전송하여 정산 모듈에서 처리</p>
<p>체결 실패 시 failed-execution-topic을 통해 매칭 모듈로 재전송</p>
<p>처리 실패 주문은 execution-dlt-topic에 저장</p>
</li>
</ul>
<p>5️⃣ 정산 모듈 (Settlement Module)</p>
<ul>
<li><p>Topic: trade-execution-topic, settlement-topic, settlement-dlt-topic, settlement-failure-topic</p>
</li>
<li><p>역할: 체결된 주문의 금액 정산 및 자산 이동</p>
</li>
<li><p>Message Flow:</p>
<p>trade-execution-topic에서 체결된 주문을 받아 정산 진행</p>
<p>정산 성공 시 settlement-topic으로 전송</p>
<p>정산 실패 시 settlement-failure-topic을 통해 체결 모듈로 롤백 요청</p>
<p>최종 실패한 경우 settlement-dlt-topic에 저장</p>
</li>
</ul>
<p>6️⃣ 알림 모듈 (Notification Module)</p>
<ul>
<li><p>Topic: settlement-topic, notice-topic</p>
</li>
<li><p>역할: 체결 완료 시 사용자에게 알림 발송, 공시 발행시 알림발송</p>
</li>
<li><p>Message Flow:</p>
<p>settlement-topic을 구독하여 정산 완료 시 알림 생성</p>
<h2 id="notice-topic으로-메시지-발행하여-유저-모듈에서-알림-저장">notice-topic으로 메시지 발행하여 유저 모듈에서 알림 저장</h2>
</li>
</ul>
<h3 id="❗-dltdead-letter-topic란">❗ DLT(Dead Letter Topic)란?</h3>
<p>Kafka를 사용하면서 가장 신경 써야 할 부분 중 하나가 메시지 유실 방지다. 만약 특정 메시지가 지속적으로 처리되지 못하면 어떻게 해야 할까? <strong>DLT(Dead Letter Topic)</strong>로 처리하자!
rabbitMQ의 DLQ와 비슷하다고 생각하면 된다.</p>
<h3 id="dlt의-동작-방식">DLT의 동작 방식</h3>
<p>모듈에서 메시지 처리 실패 → 재시도</p>
<p>여러 번 재시도 후에도 실패 → DLT(Dead Letter Topic)로 이동</p>
<p>DLT에서 메시지 저장 → 운영자가 확인 후 수동 처리 or 자동 재처리</p>
<p>예를 들어, 정산 모듈에서 정산이 실패하면 settlement-dlt-topic으로 메시지가 이동한다. 그 후 순차적으로 앞단계에서 롤백 로직이 일어난다.</p>
<p>Kafka는 기본적으로 메시지를 유지하면서도 여러 컨슈머 그룹이 사용할 수 있도록 설계되었기 때문에, DLT를 활용하면 메시지 유실을 방지할수있다 !</p>
<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/bc55c2b0-2c4c-45e4-898b-f2f3c21d18c2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] 트레이딩 시스템을 4개로 나눴다고요? 그래서 모듈 수가 총 11개라고요???]]></title>
            <link>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%94%A9-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-4%EA%B0%9C%EB%A1%9C-%EB%82%98%EB%88%B4%EB%8B%A4%EA%B3%A0%EC%9A%94</link>
            <guid>https://velog.io/@greenlemon_t/MSA-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%94%A9-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-4%EA%B0%9C%EB%A1%9C-%EB%82%98%EB%88%B4%EB%8B%A4%EA%B3%A0%EC%9A%94</guid>
            <pubDate>Thu, 27 Feb 2025 12:37:29 GMT</pubDate>
            <description><![CDATA[<h3 id="트레이딩-시스템을-4개의-모듈로-분리한-이유">트레이딩 시스템을 4개의 모듈로 분리한 이유</h3>
<p>트레이딩 시스템을 개발하면서 가장 먼저 고민했던 건 &quot;어떻게 하면 확장성과 유지보수를 고려한 구조를 만들 수 있을까?&quot;였다. 처음에는 단일 서비스(monolithic)로 구성하는 것이 더 쉽고 빠를 것 같았지만, 트레이딩 시스템 특성상 높은 트래픽과 빠른 데이터 처리가 필수적이었다. 그래서 마이크로서비스 아키텍처(MSA)로 설계하면서 주문, 매칭, 체결, 정산의 4개 모듈로 분리했다.</p>
<h3 id="1-주문-모듈">1. 주문 모듈</h3>
<p>사용자가 주문을 넣으면 가장 먼저 주문 모듈에서 예수금과 보유 주식을 조회해 주문이 가능한지 판별한다. 만약 조건을 충족하면 주문 테이블에 CREATED 상태로 저장한 후, 매수/매도 주문을 Kafka 메시지를 통해 매칭 모듈로 전달한다. 또한, 주문 내역을 저장하여 이후의 트랜잭션을 추적할 수 있도록 한다.</p>
<h3 id="2-매칭-모듈">2. 매칭 모듈</h3>
<p>매칭 모듈에서는 매수와 매도 주문을 비교하여 매칭되는지를 확인한다. 여기서 부분 체결이 발생할 수도 있고, 터무니없이 높은 가격으로 매도를 하거나 낮은 가격으로 매수를 했을 경우 미체결 상태로 남아 있을 수도 있다. 이러한 주문을 처리하는 로직이 핵심인 모듈이다. 매칭이 성공하면 체결 모듈로 Kafka 메시지를 보낸다.</p>
<h3 id="3-체결-모듈">3. 체결 모듈</h3>
<p>매칭이 완료된 주문이 체결 모듈로 전달되면, 다시 한 번 예수금과 보유 주식을 검증한 후 체결 테이블에 저장한다. 이 단계에서 주문이 실제로 체결되었다는 의미를 갖게 된다. 체결된 데이터는 이후 정산 모듈로 Kafka를 통해 전송된다. 사용자가 체결내역을 조회할경우 이 모듈에서 DB접근을 한다.</p>
<h3 id="4-정산-모듈">4. 정산 모듈</h3>
<p>체결된 주문을 기반으로 매수자와 매도자의 돈을 주고받고, 주식을 실질적으로 이동시키는 역할을 한다. 매수자의 계좌에서 돈이 빠져나가고, 매도자의 계좌에 입금되며, 동시에 주식 보유량이 변동된다. 이 과정이 완료되어야 최종적으로 주문이 정상적으로 체결되었다고 볼 수 있다.</p>
<h3 id="msa에서-kafka를-도입한-이유">MSA에서 Kafka를 도입한 이유</h3>
<p>트레이딩 시스템을 구성하면서 모듈 간의 통신 방식도 중요한 요소였다. HTTP 요청을 사용해 동기적으로 처리하는 방법도 있지만, 트레이딩 시스템에서는 빠른 속도와 높은 안정성이 필요했다. 이를 위해 비동기 메시지 큐인 Apache Kafka를 도입했다.</p>
<ol>
<li>높은 처리량과 실시간 데이터 스트리밍</li>
</ol>
<p>Kafka는 대량의 트랜잭션을 빠르게 처리할 수 있고, 주문, 매칭, 체결, 정산 과정에서 발생하는 데이터를 실시간으로 전달하는 데 적합하다. RabbitMQ와 같은 메시지 브로커도 있지만, Kafka는 순차적인 데이터 처리, 높은 처리량, 메시지 재처리 기능에서 강점을 가진다.</p>
<ol start="2">
<li>이벤트 유실 방지를 위한 DLT(Dead Letter Topic)</li>
</ol>
<p>트레이딩 시스템에서 메시지 유실은 치명적이다. 이를 방지하기 위해 DLT(Dead Letter Topic)을 구성하여 실패한 메시지를 별도로 저장하고, 이후 재처리할 수 있도록 했다. 이를 통해 데이터의 신뢰성을 확보할 수 있었다.</p>
<ol start="3">
<li>주문 및 체결 알림 시스템 구축</li>
</ol>
<p>주문이 접수되거나 체결이 완료되었을 때 실시간으로 사용자에게 알림을 보내야 한다. Kafka의 Pub-Sub 구조를 활용해 사용자의 계좌 변동 사항이나 체결 내역을 실시간으로 전송할 수 있도록 구현했다.</p>
<h3 id="전체-시스템-구성">전체 시스템 구성</h3>
<p>처음에는 트레이딩 시스템을 4개의 모듈로만 구성했지만, 개발을 진행하면서 공통 기능과 기타 서비스까지 고려해야 했다. 최종적으로 다음과 같이 11개의 모듈로 구성했다.</p>
<p>트레이딩 관련 모듈: 주문, 매칭, 체결, 정산 / 알림</p>
<p>기타 모듈: 유저 모듈, 게이트웨이(Gateway), 서비스 디스커버리(Eureka 서버), 공통 모듈, 공시 모듈, 공공 데이터 수집 서버</p>
<hr>
<p>모듈을 분리하면서 시스템이 더 복잡해졌지만, 그만큼 확장성과 안정성이 보장되었으며, 덕분에 Kafka도 깊이 있게 활용해볼 수 있는 좋은 기회가 되었다. Kafka를 직접 적용하면서 메시지 처리 방식에 대한 이해도 높아졌고, 이를 통해 실시간 데이터 처리의 중요성을 다시금 깨달을 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA 프로젝트] - Finpago 기획안]]></title>
            <link>https://velog.io/@greenlemon_t/%ED%94%84%EB%94%94%EC%95%84-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Finpago-%EA%B8%B0%ED%9A%8D%EC%95%88</link>
            <guid>https://velog.io/@greenlemon_t/%ED%94%84%EB%94%94%EC%95%84-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Finpago-%EA%B8%B0%ED%9A%8D%EC%95%88</guid>
            <pubDate>Wed, 26 Feb 2025 14:08:13 GMT</pubDate>
            <description><![CDATA[<h2 id="finpago-실시간-해외-공시-번역·요약-및-트레이딩-지원-플랫폼">Finpago: 실시간 해외 공시 번역·요약 및 트레이딩 지원 플랫폼</h2>
<h2 id="finance--papago">(Finance + Papago)</h2>
<h3 id="프로젝트-개요">프로젝트 개요</h3>
<ol>
<li>프로젝트 주제</li>
</ol>
<p>실시간 해외 공시 번역·요약 및 트레이딩 지원 플랫폼</p>
<ol start="2">
<li>프로젝트 설명</li>
</ol>
<p>Finpago는 SEC(미국 증권거래위원회) 전자 공시 정보를 실시간으로 번역·요약하고, 투자자가 효율적으로 매매 전략을 수립할 수 있도록 지원하는 트레이딩 플랫폼입니다. SEC 공시 데이터를 빠르게 이해하고, 이를 기반으로 트레이딩 결정을 내릴 수 있도록 돕는 것이 주요 목표입니다.</p>
<ol start="3">
<li>주요 기능</li>
</ol>
<h3 id="실시간-해외-공시-번역-및-요약">실시간 해외 공시 번역 및 요약</h3>
<p>1) 실시간 공시 정보 조회</p>
<ul>
<li><p>최신 해외 공시 정보를 실시간 제공</p>
</li>
<li><p>관심 종목 공시 필터링 기능 지원</p>
</li>
<li><p>SEC 전자공시 원본 및 번역본 제공</p>
</li>
</ul>
<p>2) AI 기반 공시 번역 및 요약</p>
<ul>
<li><p>DeepL 번역 AI를 활용한 SEC 공시 전문 번역 제공</p>
</li>
<li><p>주요 공시 유형 (Schedule 13D &amp; 13G, Form 4, Form S-1, 10-K, 10-Q, 8-K 등) 지원</p>
</li>
<li><p>공시 유형별 투자 전략 요약본 생성</p>
</li>
<li><p>ChatGPT 기반 공시 분석 및 핵심 데이터 요약</p>
</li>
<li><p>투자 실전 활용법 및 매매 전략 가이드 생성</p>
</li>
</ul>
<p>3) 공시 관련 트레이딩 지원</p>
<ul>
<li><p>특정 종목의 공시와 트레이딩을 한 화면에서 제공</p>
</li>
<li><p>사용자의 보유 종목 및 관심 종목의 새로운 공시 알림 기능</p>
</li>
</ul>
<h3 id="종목-검색-및-정보-제공">종목 검색 및 정보 제공</h3>
<p>1) 종목 검색 및 필터링</p>
<ul>
<li><p>검색창에서 종목 검색 및 연관 리스트 제공</p>
</li>
<li><p>관심 종목 등록 및 관리 기능</p>
</li>
</ul>
<p>2) 해외 종목 순위 조회</p>
<ul>
<li><p>시장별 해외 주식 순위 제공 (상승률 기준, 거래량 기준 등)</p>
</li>
<li><p>특정 종목 클릭 시 상세 종목 페이지로 이동</p>
</li>
</ul>
<p>3) 환율 및 경제 뉴스 제공</p>
<ul>
<li><p>해외 경제 뉴스 실시간 제공</p>
</li>
<li><p>환율 정보 및 변동성 분석 기능</p>
</li>
</ul>
<h3 id="트레이딩-기능">트레이딩 기능</h3>
<p>1) 주문 및 체결 기능</p>
<p>매수/매도 주문 창 및 체결 창 제공</p>
<p>주문 정정 및 취소 기능 지원</p>
<p>체결 내역 및 체결가 조회</p>
<p>2) 계좌 관리 및 손익 분석</p>
<p>보유 종목 리스트 및 손익 현황 제공</p>
<p>환차손익, 계좌 손익률 조회 가능</p>
<p>3) 공시 기반 매매 연동</p>
<p>AI 공시 요약 결과를 기반으로 매수/매도 결정을 도와주는 기능 제공</p>
<p>내가 맡은 역할: 트레이딩 시스템 개발</p>
<hr>
<p>&lt;백엔드 기술 스택&gt;</p>
<p>Spring Boot: REST API 개발</p>
<p>Kafka: 주문 데이터 스트리밍 및 체결 처리</p>
<p>Redis: 트랜잭션 처리 및 캐싱</p>
<p>MySQL: 계좌 및 주문 데이터 저장</p>
<p>JWT 인증 및 보안: Gateway에서 JWT 인증 처리</p>
<h2 id="eureka--gateway-msa-기반-서비스-디스커버리-및-라우팅">Eureka &amp; Gateway: MSA 기반 서비스 디스커버리 및 라우팅</h2>
<p>MSA 환경에서 트레이딩 시스템과 공시 데이터 처리 모듈 간의 효율적인 통신을 위해 Kafka를 활용한 비동기 메시징을 적용할 예정입니다. 또한, 낙관적 락(@Version)과 Redisson 분산락을 활용하여 데이터 정합성을 유지할 계획입니다.</p>
<p>&lt;앞으로의 계획&gt;</p>
<p>트레이딩 기능 개발을 완료한 후, 공시 데이터 처리 및 AI 기반 요약 기능을 도와줄 예정입니다. SEC 공시 데이터를 어떻게 효율적으로 분류하고 투자자에게 실질적인 정보를 제공할지 고민하며, AI 모델을 활용한 요약 및 분석 시스템을 개선할 계획입니다.</p>
<p>앞으로도 프로젝트 진행 과정을 꾸준히 기록하며, 주요 기술 적용 및 문제 해결 경험을 공유할 예정입니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Redis] 분산락 갱신 유실 문제]]></title>
            <link>https://velog.io/@greenlemon_t/Redis-%EB%B6%84%EC%82%B0%EB%9D%BD-%EA%B0%B1%EC%8B%A0-%EC%9C%A0%EC%8B%A4-%EB%AC%B8%EC%A0%9C-5hawm2jq</link>
            <guid>https://velog.io/@greenlemon_t/Redis-%EB%B6%84%EC%82%B0%EB%9D%BD-%EA%B0%B1%EC%8B%A0-%EC%9C%A0%EC%8B%A4-%EB%AC%B8%EC%A0%9C-5hawm2jq</guid>
            <pubDate>Tue, 11 Feb 2025 04:26:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/greenlemon_t/post/fdd0fc89-d02e-4d4b-a53a-0e8fda433692/image.png" alt=""></p>
<p><a href="https://www.youtube.com/watch?v=UOWy6zdsD-c&amp;t=424s">https://www.youtube.com/watch?v=UOWy6zdsD-c&amp;t=424s</a>
토스증권의 영상을 참고하였습니다
<img src="https://velog.velcdn.com/images/greenlemon_t/post/697d9f09-07a8-47d9-a9b1-9177df47b797/image.png" alt=""></p>
<h3 id="🔹-분산락-타임아웃이-발생하는-이유">🔹 <strong>분산락 타임아웃이 발생하는 이유</strong></h3>
<ul>
<li>분산락을 무제한으로 유지하면 데드락(Deadlock) 현상이 발생할 수 있음.</li>
<li>따라서 분산락에는 타임아웃이 설정되어 일정 시간이 지나면 자동 해제됨.</li>
<li>하지만 트랜잭션이 끝나기도 전에 분산락이 해제되면, 다른 요청이 해당 자원을 점유하면서 갱신 유실(Update Loss) 문제가 발생할 수 있음.</li>
<li>jpa에서는 쿼리쓰기 지연으로 인해 자주 발생할수있음</li>
</ul>
<h3 id="🔹-갱신-유실이-발생하는-시나리오"><strong>🔹 갱신 유실이 발생하는 시나리오</strong></h3>
<ul>
<li><strong>1</strong>: 분산락을 해제하기 전에 트랜잭션이 커밋됨 (정상적인 경우)</li>
<li><strong>2</strong>: 분산락을 해제한 후에 트랜잭션이 커밋됨 (경합 발생) ⚠️→ 트랜잭션이 끝나기도 전에 다른 요청이 같은 데이터를 변경할 수 있음.</li>
<li><strong>3</strong>: 여러 트랜잭션이 동시에 같은 데이터를 변경하면서 충돌 발생→ 한 트랜잭션의 변경 내용이 다른 트랜잭션에 의해 덮어씌워질 수 있음.</li>
</ul>
<h3 id="🔹-낙관적-락-vs-비관적-락-비교"><strong>🔹 낙관적 락 vs 비관적 락 비교</strong></h3>
<table>
<thead>
<tr>
<th><strong>구분</strong></th>
<th><strong>낙관적 락 (Optimistic Lock)</strong></th>
<th><strong>비관적 락 (Pessimistic Lock)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>경합 처리 방식</strong></td>
<td>트랜잭션 충돌 시 예외 발생 후 재시도</td>
<td>트랜잭션이 완료될 때까지 락을 유지</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>충돌이 적을 때 성능 좋음</td>
<td>트랜잭션이 길어질수록 성능 저하</td>
</tr>
<tr>
<td><strong>예제</strong></td>
<td><code>@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)</code></td>
<td><code>@Lock(LockModeType.PESSIMISTIC_WRITE)</code></td>
</tr>
</tbody></table>
<h3 id="낙관적-락-사용">낙관적 락 사용</h3>
<h3 id="locklockmodetypeoptimistic_force_increment-사용"><strong><code>@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)</code> 사용</strong></h3>
<pre><code class="language-java">@Repository
public interface StockRepository extends JpaRepository&lt;Stock, UUID&gt; {

    @Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) // 낙관적 락 사용
    @Query(&quot;SELECT s FROM Stock s WHERE s.product.productId = :productId&quot;)
    Optional&lt;Stock&gt; findByProduct_ProductIdWithLock(UUID productId);
}</code></pre>
<ul>
<li>JPA의 <code>@Lock(OPTIMISTIC_FORCE_INCREMENT)</code>을 사용하여 충돌 감지</li>
<li>데이터를 조회할 때 버전(version) 필드를 자동 증가시켜 동시성 문제 해결</li>
<li>버전 불일치 발생 시 <code>OptimisticLockException</code> 예외 발생 → 이를 감지하여 자동 재시도 수행</li>
</ul>
<h3 id="retryable을-이용한-재시도-로직">@Retryable을 이용한 재시도 로직</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class StockRetryService {

    private final StockRepository stockRepository;

    // 예약 구매: 재고 1 감소 메서드
    @Retryable(
            retryFor = {StaleObjectStateException.class, OptimisticLockException.class, ObjectOptimisticLockingFailureException.class},
            maxAttempts = 500, // 최대 500번 재시도
            backoff = @Backoff(100) // 100ms 대기 후 재시도
    )
    @Transactional
    public void decreaseStockQuantityWithRetry(UUID productId) {
        Stock stock = stockRepository.findByProduct_ProductIdWithLock(productId)
                .orElseThrow(() -&gt; new CustomException(CommerceErrorCode.STOCK_DATA_NOT_FOUND_FOR_PRODUCT));
        stock.decreaseStock();  // 수량 1 감소
        stockRepository.save(stock);
    }
}</code></pre>
<h3 id=""></h3>
<p><strong><code>@Retryable</code>을 사용하는 이유</strong></p>
<ul>
<li>JPA의 낙관적 락 충돌 예외(<code>OptimisticLockException</code>)가 발생하면 자동으로 재시도</li>
<li>500번까지 최대 재시도하며, 충돌이 해결될 때까지 100ms 간격으로 재시도</li>
<li>트랜잭션 단위로 충돌 감지 후 롤백 후 다시 시도하여 경합이 발생해도 최종적으로 한 요청만 성공하도록 보장</li>
</ul>
<hr>
<h3 id="🔹version을-사용할-때-jpa의-동작-방식">🔹<code>@Version</code>을 사용할 때 JPA의 동작 방식</h3>
<p> <code>@Version</code>이 있는 엔티티(<code>Stock</code>)에서 업데이트가 발생하면, JPA는 자동으로 <code>WHERE version = ?</code> 조건을 추가함</p>
<pre><code class="language-java">@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query(&quot;SELECT s FROM Stock s WHERE s.product.productId = :productId&quot;)
Optional&lt;Stock&gt; findByProduct_ProductIdWithLock(UUID productId);</code></pre>
<ul>
<li><strong>JPA가 자동으로 <code>WHERE version = ?</code>을 추가한 <code>UPDATE</code> 쿼리를 실행</strong>한다.</li>
<li><code>@Version</code> 필드가 존재하기 때문에 <code>UPDATE</code> 쿼리를 실행할 때 <code>version</code>을 체크하고, 충돌이 발생하면 <code>OptimisticLockException</code>을 발생시킨다.</li>
</ul>
<hr>
<h2 id="📌-낙관적-락version-분산-락redisson">📌 낙관적 락(@Version)+ 분산 락(Redisson)</h2>
<pre><code class="language-java">RLock lock = redissonClient.getLock(&quot;stock:&quot; + productId);
try {
    if (!lock.tryLock(10, 2, TimeUnit.SECONDS)) {  
        throw new CustomException(CommerceErrorCode.LOCK_ACQUISITION_FAILED);
    }

    // 낙관적 락과 함께 사용 (버전 증가)
    Stock stock = stockRepository.findByProduct_ProductIdWithLock(productId)
            .orElseThrow(() -&gt; new CustomException(CommerceErrorCode.STOCK_DATA_NOT_FOUND_FOR_PRODUCT));

    stock.decreaseStock();  // 재고 감소
    stockRepository.save(stock);  // 업데이트 시 WHERE version = ? 자동 추가

} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
</code></pre>
<ul>
<li><strong>분산 락(Redisson)</strong> 을 먼저 획득하여 <strong>멀티 서버에서 하나의 요청만 처리 가능</strong></li>
<li><strong>낙관적 락(@Version)</strong> 을 적용하여, <strong>단일 서버 내에서 동시 접근 충돌 방지</strong></li>
</ul>
<p>→ 즉, <strong>멀티 서버 환경 + JPA 동시성 제어</strong> 를 동시에 해결하는 방식!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] Eureka + gateway]]></title>
            <link>https://velog.io/@greenlemon_t/Eureka-gateway</link>
            <guid>https://velog.io/@greenlemon_t/Eureka-gateway</guid>
            <pubDate>Fri, 07 Feb 2025 01:44:41 GMT</pubDate>
            <description><![CDATA[<h2 id="eureka--gateway">Eureka + gateway</h2>
<h2 id="eureka-discovery-service란">Eureka Discovery Service란?</h2>
<p>Netflix OSS의 서비스 디스커버리(Server Discovery) 기능을 제공하는 microservice registry
<img src="https://velog.velcdn.com/images/greenlemon_t/post/00d171e7-a31e-4c24-8a38-bef6bdb561c6/image.png" alt="">
내가 아는 그 OTT Netflix? 
ㅇㅇ 맞음 그 넷플릭스랍니다</p>
<p><strong>&lt;역할&gt;</strong></p>
<ul>
<li><strong>서비스 레지스트리 (Service Registry)</strong><ul>
<li>MSA(Microservice Architecture)에서 서비스들이 동적으로 등록되고 검색될 수 있도록 함.</li>
</ul>
</li>
<li><strong>클라이언트 서비스 디스커버리 (Service Discovery)</strong><ul>
<li>서비스가 실행되면 <strong>자동으로 Eureka Server에 등록</strong>되고, 다른 서비스가 이를 조회하여 통신 가능.</li>
</ul>
</li>
<li><strong>로드 밸런싱 지원</strong><ul>
<li>클라이언트가 서비스 위치를 직접 지정하는 대신, <strong>Eureka에서 자동으로 최적의 인스턴스를 선택하여 요청을 보냄.</strong></li>
</ul>
</li>
</ul>
<p>→ <strong>즉, Eureka를 사용하면 마이크로서비스 간의 통신에서 정적인 IP 주소가 필요 없으며, 자동으로 서비스 위치를 찾아서 요청할 수 있음</strong></p>
<hr>
<h2 id="1-eureka-server-eureka-server-모듈">1. Eureka Server (<code>eureka-server</code> 모듈)**</h2>
<h2 id="eureka-server">Eureka-server</h2>
<h3 id="eurekaserverapplication-java">EurekaServerApplication .java</h3>
<pre><code class="language-java">@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}</code></pre>
<ul>
<li><code>@EnableEurekaServer</code> → <strong>Eureka 서버로 동작</strong>하도록 설정.</li>
</ul>
<h3 id="applicationyml">application.yml</h3>
<pre><code class="language-yaml">server:
  port: 19090

eureka:
  client:
    register-with-eureka: false # Eureka 서버 자체는 다른 Eureka 서버에 등록되지 않음
    fetch-registry: false # 다른 서비스 목록을 가져오지 않음 (스스로 관리)
  instance:
    hostname: localhost</code></pre>
<ul>
<li>Eureka 서버는 자신을 다른 Eureka 서버에 등록하지 않음 (<code>register-with-eureka: false</code>).</li>
<li>자신이 서비스 레지스트리 역할을 하기 때문에 다른 서비스 목록을 가져오지 않음 (<code>fetch-registry: false</code>).</li>
</ul>
<p>→  <strong>즉, 이 모듈은 &quot;Service Registry&quot; 역할을 하며, 다른 서비스들이 이 Eureka 서버에 등록하여 서로의 위치를 찾을 수 있음</strong></p>
<h2 id="gateway-server">gateway-server</h2>
<h3 id="applicationyml-1">application.yml</h3>
<pre><code class="language-yaml">
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/
    register-with-eureka: true
    fetch-registry: true

server:
  port: 8000

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://USER-SERVICE  # Eureka에서 서비스 검색 후 요청 전달
          predicates:
            - Path=/api/v1/users/**
          filters:
            - name: CircuitBreaker
              args:
                name: userServiceBreaker
                fallbackUri: forward:/fallback/users

            - name: JwtAuthenticationFilter
</code></pre>
<ul>
<li>Eureka Server와 연결하여 서비스 디스커버리 활성화.</li>
<li>Gateway도 Eureka에 등록되며, 다른 서비스 정보를 가져와 동적으로 라우팅 가능.</li>
</ul>
<p><strong>라우팅(routes) 설정</strong></p>
<ul>
<li><code>id: user-service</code> → <code>USER-SERVICE</code>로 요청을 보냄.</li>
<li><code>uri: lb://USER-SERVICE</code> → Eureka에서 <code>USER-SERVICE</code>의 위치를 찾아서 요청 전달.</li>
<li><code>predicates: Path=/api/v1/users/**</code> → <code>/api/v1/users/**</code> 경로의 요청만 처리.</li>
</ul>
<p><strong>→ 즉, 클라이언트가 <code>/api/v1/users/</code> 경로로 요청하면, Gateway는 이를</strong> <code>lb://user-service</code> → <strong>Eureka에서 <code>user-service</code>의 위치를 자동 검색 후 요청 전달</strong>.</p>
<h3 id="gatewayapplicationjava">GatewayApplication.java</h3>
<pre><code class="language-java">@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

}</code></pre>
<p><code>@EnableDiscoveryClient</code>를 사용하면 해당 서비스가 <strong>자동으로 Eureka에 등록됨</strong>.</p>
<p>→ <strong>Spring Cloud에서 service discovery를 활성화</strong>하는 역할.</p>
<hr>
<p>💡 <code>@EnableDiscoveryClient</code> 없어도 근데 등록됨 ㅋ</p>
<p>Spring Boot 2.3 이상에서는 <strong>Spring Cloud Netflix Eureka 클라이언트(<code>spring-cloud-starter-netflix-eureka-client</code>)가 의존성으로 추가되어 있으면</strong>
 <code>@EnableDiscoveryClient</code>를 명시적으로 사용하지 않아도 자동으로 Eureka에 등록된다 !!</p>
<h3 id="의존성-추가">의존성 추가</h3>
<pre><code class="language-bash">dependencies {
    implementation &#39;org.springframework.cloud:spring-cloud-starter-netflix-eureka-client&#39;
}</code></pre>
<h3 id="applicationyml-2"><strong>application.yml</strong></h3>
<pre><code class="language-yaml">eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/
    register-with-eureka: true # 서비스 등록 활성화
    fetch-registry: true # 다른 서비스 목록 가져오기
</code></pre>
<ul>
<li><code>register-with-eureka: true</code> 설정이 있으면 <strong>자동으로 Eureka에 등록</strong>됨.</li>
</ul>
<p>💡 서비스 등록 여부 확인
Eureka 대시보드 접속 (<a href="http://localhost:19090/">http://localhost:19090/</a>)
→ user-service, auth-service, gateway 등의 서비스가 정상적으로 등록되었는지 확인</p>
]]></description>
        </item>
    </channel>
</rss>