<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>젠슈의 블로그</title>
    <link>https://juanxiu.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 17 Jul 2026 10:01:34 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>juanxiu</managingEditor>
    <image>
      <title>젠슈의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8013630/attach/bbc7252f9bd04e5eaf493b9db143a0a0</url>
      <link>https://juanxiu.tistory.com</link>
    </image>
    <item>
      <title>[캡스톤 디자인 프로젝트] 카드 혜택 서비스</title>
      <link>https://juanxiu.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요? 저는 캡스톤 디자인 프로젝트에서 백엔드 개발을 담당했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 기능을 어떻게 구현했는지 회고하려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 혜택 조회 기능에서 비즈니스 로직은 다음과 같습니다. 사용자의 안드로이드에서 결제 앱 푸시알림으로 소비 내역(결제 금액, 매장명) 을 API 호출을 통해 데이터베이스에 저장합니다. 그리고 KakaoMap API 를 호출해 사용자가 등록한 카드의 혜택 정보에서 해당 매장의 카테고리에서 사용할 수 있는 한도 금액을 차감합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 발생 가능한 문제가 있습니다. 만약 결제 앱 푸시알림이 거의 동시에 중복으로 들어온다면, 더 먼저 들어온 소비 내역을 한도 금액에서 차감하기 이전에 그 다음 소비 내역이 한도 금액을 조회합니다. 그리고 아직 한도를 넘지 않았다고 판단하고, 사용자는 해당 카드가 혜택을 사용할 수 있는 카드로 조회될 수 있습니다. 그러나 그 이전 결제에서 이미 해당 카드는 헤택 적용 한도를 넘어서 더 이상 혜택이 적용되지 않은 카드일 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 된다면, 사용자는 우리 서비스에서 혜택 적용이 가능한 카드로 조회를 하였는데 막상 매장에 가서 결제를 하면 한도를 초과해 혜택 적용이 안되는 상황을 마주할 수 있습니다. 결국 우리 서비스의 신뢰도가 떨어집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락이 필요한 이유를 구체적으로 정리하면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소비내역 중복 저장 방지 동일한 푸시 알림이 동시에 여러 번 수신되는 경우를 생각해보겠습니다. 락이 없다면 동일한 소비내역이 두 번 저장되고, 실적이 두 번 누적되며, 혜택도 두 번 적용될 수 있습니다. 이는 사용자에게 부정확한 정보를 제공하고, 비즈니스 로직에 심각한 오류를 발생시킬 수 있습니다. 분산락을 적용하면 하나의 요청만 처리되고, 나머지는 락 획득에 실패하여 처리되지 않습니다. 이를 통해 중복 처리를 근본적으로 방지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실적 업데이트 Lost Update 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 사용자의 동일한 카드에 대한 여러 소비내역이 동시에 처리되는 경우를 살펴보겠습니다. 락이 없는 상황에서는 다음과 같은 문제가 발생할 수 있습니다. 트랜잭션 A가 현재 실적 100원을 조회하고 50원을 더해 150원으로 계산한 후 저장하려고 합니다. 동시에 트랜잭션 B도 현재 실적 100원을 조회하고 30원을 더해 130원으로 계산한 후 저장합니다. 결과적으로 최종 실적은 130원으로 저장되어 50원 소비내역이 반영되지 않는 Lost Update 문제가 발생합니다. 분산락을 적용하면 트랜잭션 A가 락을 획득하여 처리 완료한 후, 트랜잭션 B가 처리되어 최종 실적이 180원으로 정확히 반영됩니다. 순차 처리를 통해 데이터 일관성을 보장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혜택 한도 초과 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 사용자의 동일한 카드에 대한 여러 소비내역이 동시에 혜택을 적용하는 경우를 고려해보겠습니다. 락이 없다면 트랜잭션 A가 현재 한도 사용량 0원을 조회하고 10,000원을 적용하려고 합니다. 동시에 트랜잭션 B도 현재 한도 사용량 0원을 조회하고 15,000원을 적용하려고 합니다. 각각의 트랜잭션은 한도 검사를 통과하여 총 25,000원이 적용되지만, 실제 한도는 20,000원이므로 한도를 초과하게 됩니다. 분산락을 적용하면 트랜잭션 A가 락을 획득하여 10,000원을 적용한 후, 트랜잭션 B가 한도 검사를 수행할 때 이미 사용된 10,000원을 확인하여 15,000원 적용 시 한도 초과로 처리됩니다. 순차 처리를 통해 한도 검사를 정확히 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 데이터 정합성을 관리하는 게 중요했고, 저는 Spring JPA 의 비관적(perssimistic) Lock 을 사용하여 동시성을 제어했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 요청 처리 흐름입니다. 소비내역 저장 이후 락을 획득하고, 실적 업데이트와 혜택 적용 이후에 락을 해제합니다. 이후 혜택 카드를 조회하면 최신 소비내역이 반영된 상태로 조회가 가능해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2078&quot; data-origin-height=&quot;1664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N0bFK/dJMcadAn992/mD9f1KpUlo0NFRKjx9ZE60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N0bFK/dJMcadAn992/mD9f1KpUlo0NFRKjx9ZE60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N0bFK/dJMcadAn992/mD9f1KpUlo0NFRKjx9ZE60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN0bFK%2FdJMcadAn992%2FmD9f1KpUlo0NFRKjx9ZE60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2078&quot; height=&quot;1664&quot; data-origin-width=&quot;2078&quot; data-origin-height=&quot;1664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>juanxiu</author>
      <guid isPermaLink="true">https://juanxiu.tistory.com/6</guid>
      <comments>https://juanxiu.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 24 Nov 2025 21:51:43 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI] 주식 회고 피드백 생성 AI 개발</title>
      <link>https://juanxiu.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요? 꽤 오랜만에 글을 쓰네요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 디프만 17기 서버 개발자로 활동했습니다. 지난 활동을 회고해보려고 해요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 주식 회고 서비스를 만들었는데, 사용자의 회고글에 대해 피드백을 생성해주는 AI 가 필요했어요. 이 글에서는 피드백 생성 AI 를 어떻게 구현했는지 설명하려고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;770&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Qj3L/dJMcabJnyLB/ydBryLDDR8U3zuqvDnCPyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Qj3L/dJMcabJnyLB/ydBryLDDR8U3zuqvDnCPyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Qj3L/dJMcabJnyLB/ydBryLDDR8U3zuqvDnCPyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Qj3L%2FdJMcabJnyLB%2FydBryLDDR8U3zuqvDnCPyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;770&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;770&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 피드백 생성 AI 는 멀티 에이전트 구조로 개발했어요. 기존에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;single agent 구조로, AI API 1번 호출하여 유저 인풋을 전달하는 방식이었습니다. 단점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;프롬프트를 길게 줄 수 없다는 점이었어요. 토큰 처리량에 한계가 있고, 프롬프트가 길어질수록 답변 퀄리티가 낮아지기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lHMdI/dJMcabJnyJu/fifN0tIQSMkw3yiLPivTc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lHMdI/dJMcabJnyJu/fifN0tIQSMkw3yiLPivTc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lHMdI/dJMcabJnyJu/fifN0tIQSMkw3yiLPivTc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlHMdI%2FdJMcabJnyJu%2FfifN0tIQSMkw3yiLPivTc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;804&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 위와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;멀티 에이전트 구조로 전환했습니다. 이것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;여러 개의 LLM 에이전트가 역할을 나눠서 문제를 해결하도록 한 구조예요.&lt;span&gt;&amp;nbsp;&lt;/span&gt;에이전트들이 체이닝 형식으로 응답을 생성하는데, LLM 호출을 단계적으로 연결하여 하나의 응답 파이프라인을 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 인풋 레이어, 오케스트레이션 레이어, 최종 판단 레이어를 분리하였고, 오케스트레이션 레이어에서는 세 개의 에이전트가 앞선 에이전트의 응답을 인풋으로 받게 됩니다. 더욱 체계적인 답변을 구조화할 수 있죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점은 에이전트 수마다 openai API 호출을 하게 하게 되므로, 속도가 느려지고 비용이 늘어나요. 근본적으로 해결은 불가함. 에이전트 여러 개를 두면 당연히 지연시간이 늘어나기 때문입니다. 답변이 완성도가 높아지는 만큼 속도와 비용 문제가 발생하는 일종의 트레이드 오프인 거죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 속도 개선을 위해서 비동기 스레드를 호출했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 병렬로 리서치 진행
CompletableFuture&amp;lt;MarketData&amp;gt; marketDataFuture = collectMarketDataAsync(symbol, date, retrievedContext);
CompletableFuture&amp;lt;TechnicalAnalysis&amp;gt; technicalAnalysisFuture = analyzeTechnicalAsync(symbol);
CompletableFuture&amp;lt;FundamentalAnalysis&amp;gt; fundamentalAnalysisFuture = analyzeFundamentalAsync(symbol);

// 모든 분석 완료 대기(비동기 메서드 여러 개이므로)
CompletableFuture.allOf(marketDataFuture, technicalAnalysisFuture, fundamentalAnalysisFuture).join();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 `CompletableFuture` 클래스를 사용하면 비동기 호출이 가능해집니다. 하나의 스레드가 끝날 때까지 대기하지 않고 그 다음 스레드가 시작되기 때문에 전체 스레드가 완료 되기까지 속도가 빨라지죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 속도 이슈를 크게 개선하기는 어려우니, UX 를 개선하는 방향으로 가는 게 더 나을 것 같습니다. 두 가지 방법이 떠오르는데요, 첫 번째는 프론트를 개선하는 거예요. 피드백 로딩 화면 대신 프로세스 바를 두어서 사용자에게 생성 현황을 공유하면 덜 답답하겠죠. 두 번째는 서버 팀원이 말해준 건데 메시지큐를 두어서 생성되고 있는 피드백을 큐에 넣고, 사용자는 피드백 생성 직후 홈화면으로 다이렉트되도록 해요. 그리고 생성이 완료되면 큐에서 꺼내서 푸시알림 전송하는 거죠. 그럼 사용자는 로딩 화면에서 멈춰서 기다리지 않고, 나중에 알림만 확인하면 되는 것이죠.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 피드백 AI 의 응답을 고도화하기 위한 여러 방법을 생각해보았어요. 먼저 가장 쉽게 할 수 있는 방법인 프롬프트 엔지니어링이었어요. 시스템 프롬프트와 유저 프롬프트를 구분했습니다. 시스템 프롬프트에는 지침(instruction)이 들어가요. 에이전트의 역할(투자 분석 컨설턴트)을 지정해주었습니다. 이때 추가로 원샷 프롬프팅 기법도 사용했는데, LLM 에게 예시를 1개 보여주어서 응답 포맷에 맞추어 응답을 생성하도록 제어했습니다. 이건 출력 패턴을 명확하게 하는 데 도움이 되었어요. JSON 구조로 출력되어야 DTO 객체에 넣어서 다음 에이전트에게 인풋으로 전달할 수 있었거든요! 그리고 유저 프롬프트는 사용자 데이터 DB 에서 조회하여 인풋으로 들어가도록 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;런칭데이 때, 다른 디퍼들과 외부인들이 우리 서비스를 테스트해보았습니다. 그때 받은 피드백 중 인상깊었던 것이 현재 주식 데이터 기반으로 피드백을 만들어주면 좋겠다는 것이었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 생각했던 방법은 RAG 기반으로 답변을 생성하는 것이었습니다. RAG 란, LLM 이 답변을 생성할 때 외부 지식을 함께 가져와 활용하는 기술입니다. 이것은 chat gpt 와 notebooklm 을 비교해서 설명하면 이해하기 쉬운데요. 우리가 gpt 에게 프롬프트를 보내면, gpt는 인터넷에 있는 모든 정보를 가져와 그것을 기반으로 답변합니다. notebooklm은 써보신 분들은 아시겠지만, 사용자가 소스를 업로드할 수 있어요. 예를 들면, 교수님의 강의 자료를 소스로 업로드하면, 이 자료 내에서만 검색하여 그것을 기반으로 답변합니다. 훨씬 정확도가 높아지죠. 시험기간에 유용하게 썼습니다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든, RAG 는 notebooklm 과 유사합니다. 내가 구축한 지식 베이스 내에서 검색하여 답변을 제공합니다. 그렇다면 저는 이것을 어떻게 구현했을까요?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Spring batch 를 사용해 한국투자증권 API 를 호출했습니다. 매일 정해진 시각에 스케쥴링하여 시장, 종목코드, 현재 시점 기준 전날 날짜, 기간으로 주식 데이터를 조회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 해당 데이터를 chromaDB 에 문서 형태로 저장합니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String documentText = String.format(
    &quot;%s (%s)의 %s 주가는 시가 %.2f, 고가 %.2f, 저가 %.2f, 종가 %.2f를 기록했으며, 거래량은 %d주였습니다.&quot;,
    stock.getCompanyName(), stock.getCode(), dailyData.getDate(),
    dailyData.getOpen(), dailyData.getHigh(), dailyData.getLow(),
    dailyData.getClose(), dailyData.getVolume());
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명을 덧붙이면, chromaDB 는 일종의 데이터베이스입니다. 정확히 말하면 vectorDB 로, 텍스트, 이미지 같은 데이터를 숫자 백터로 변환해 저장하고, 유사한 백터를 빠르게 찾아주는 데이터베이스입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763717285718&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d -p 8000:8000 --name chromadb \
  -v /path/on/host/chromadb-data:/data \
  ghcr.io/chroma-core/chroma:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chromaDB 는 애플리케이션 실행 이전에 docker 로 띄워주고, 볼륨 마운트 경로를 로컬 경로로 지정해두면 로컬에서 데이터 저장이 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음에는 ChromaDB 에 저장된 문서를 기반으로 검색하도록 하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 1. Retrieval (검색): 검색 쿼리 문자열로, 현재 매매와 관련된 유사 정보를 ChromaDB에서 검색
String queryText = String.format(
    &quot;%s 종목의 %s 날짜 근처의 시장 상황 및 주가 흐름&quot;,
    symbol,
    date.toString());

// 유사도가 높은 상위 5개의  문서 찾는다. 
SearchRequest searchRequest = SearchRequest.builder()
    .query(queryText)
    .topK(5)
    .build();

List&amp;lt;Document&amp;gt; documents = vectorStore.similaritySearch(searchRequest);

// 2. Augmented Prompt (프롬프트 강화)
String retrievedContext = documents.stream()
    .map(Document::getText)
    .collect(Collectors.joining(&quot;\n&quot;));

// 3. Generation (생성): LLM 호출
LlmResponse llmResponse = analyzeWithMultiAgent(symbol, price, volume, orderType, date, principleCheckAndImage,
    retrievedContext);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석에 써있다시피, 정해진 질의를 chromaDB 에서 검색해서 유사도가 높은 상위 5개의 문서를 찾습니다. 해당 문서들을 프롬프트에 넣어서 LLM 을 호출하는 것이죠! 프롬프트가 더 정교해집니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccAQ6S/dJMcaacDeaA/ylLxHQxxVHbEUOFwTfdiLk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccAQ6S/dJMcaacDeaA/ylLxHQxxVHbEUOFwTfdiLk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccAQ6S/dJMcaacDeaA/ylLxHQxxVHbEUOFwTfdiLk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccAQ6S%2FdJMcaacDeaA%2FylLxHQxxVHbEUOFwTfdiLk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;400&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 흐름을 알 수 있는 아키텍처입니다.&amp;nbsp; 출처는 &lt;a title=&quot;이곳&quot; href=&quot;https://www.geeksforgeeks.org/nlp/introduction-to-chromadb/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이곳&lt;/a&gt; 입니다. 애플리케이션과 chromaDB의 상호작용을 아주 잘 나타낸 그림이라고 생각해요. 주식 데이터(쿼리)가 문서 형태로 chromaDB 에 임베딩되어 저장됩니다. 임베딩이란 텍스트, 이미지 같은 데이터를 숫자 벡터로 변환하는 과정입니다. 숫자 벡터로 변환하면 AI 가 그 의미를 더 잘 이해할 수 있지요. 의미가 유사한 데이터는 벡터 공간에서 가까운 곳에 놓이게 됩니다. 임베딩된 데이터는 LLM 호출 시에 프롬프트에 들어갑니다. 지식 베이스 기반으로 답변할 수 있게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring 에서 chromaDB 를 사용하기 위해 추상화가 이미 너무 잘되어있습니다. 전 공식 문서를 보고 구현하는 걸 좋아하는데, 아래 문서가 잘 정리되어있으니 이것을 보고 쉽게 spring 에서 chromaDB 를 구현할 수 있을 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/vectordbs/chroma.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-ai/reference/api/vectordbs/chroma.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763716772606&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Chroma :: Spring AI Reference&quot; data-og-description=&quot;If you prefer to configure the Chroma Vector Store manually, you can do so by creating a ChromaVectorStore bean in your Spring Boot application. Add these dependencies to your project: * Chroma VectorStore. org.springframework.ai spring-ai-chroma-store Ope&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-ai/reference/api/vectordbs/chroma.html&quot; data-og-url=&quot;https://docs.spring.io/spring-ai/reference/api/vectordbs/chroma.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-ai/reference/api/vectordbs/chroma.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-ai/reference/api/vectordbs/chroma.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Chroma :: Spring AI Reference&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;If you prefer to configure the Chroma Vector Store manually, you can do so by creating a ChromaVectorStore bean in your Spring Boot application. Add these dependencies to your project: * Chroma VectorStore. org.springframework.ai spring-ai-chroma-store Ope&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 궁금하신 분들이 있을까봐 트러블 슈팅 과정도 적습니다. chromaDB 의 내부 동작에 관한 내용인데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;처음에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;chromaDB 를 도커로 띄우지 않고 터미널에서 실행했다가 컬렉션 조회가 반복적으로 실패하길래 API 경로가 잘못되었는지 확인하다가 내부 동작까지 뜯어보게 되었습니다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 저는 .yml에서 chromaDB 컬렉션 생성을 자동으로 설정하였으나, 제대로 동작하지 않는 것 같아 VectorStore 를 수동으로 설정하기 위해 VectorStoreConfig 를 만들었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서를 보면, ChromaDB authorization configurations 를 생성하기 위해 아래와 같은 샘플 코드를 제공합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763717375464&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public VectorStore chromaVectorStore(EmbeddingModel embeddingModel, ChromaApi chromaApi) {
 return ChromaVectorStore.builder(chromaApi, embeddingModel)
    .tenantName(&quot;your-tenant-name&quot;) // default: SpringAiTenant
    .databaseName(&quot;your-database-name&quot;) // default: SpringAiDatabase
    .collectionName(&quot;TestCollection&quot;)
    .initializeSchema(true)
    .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring ai 에서 임베딩 클라이언트 구현체(EmbeddingModel) 를 제공하는데, 이것을 인자로 받아 ChromaVectorStore 객체를 만듭니다. 임베딩을 알아서 해주는 것이죠!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChromaVectorStore 클래스 들어가면, 아래와 같이 구현이 되어있어요.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763717433350&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected ChromaVectorStore(Builder builder) {
		super(builder);

		this.chromaApi = builder.chromaApi;
		this.tenantName = builder.tenantName;
		this.databaseName = builder.databaseName;
		this.collectionName = builder.collectionName;
		this.initializeSchema = builder.initializeSchema;
		this.filterExpressionConverter = builder.filterExpressionConverter;
		this.objectMapper = JsonMapper.builder().addModules(JacksonUtils.instantiateAvailableModules()).build();

		if (builder.initializeImmediately) {
			try {
			// afterPropertiesSet() 메서드를 호출함 
				afterPropertiesSet();
			}
			catch (Exception e) {
				throw new IllegalStateException(&quot;Failed to initialize ChromaVectorStore&quot;, e);
			}
		}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;afterPropertiesSet() 메서드에서는 chromaApi.getCollection() 를 호출하는데, 이곳의 URI 로 chromaDB 의 컬렉션을 조회합니다. 정리하면, ChromaVectorStore를 생성하려면 chromadb 의 컬렉션 조회가 먼저 이루어지는데 조회 URI가 chromadb 의 API 명세가 다르면 조회가 실패합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763717472948&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
	public void afterPropertiesSet() throws Exception {
		if (!this.initialized) { 
		// chromaApi.getCollection() 를 호출하는 것을 확인할 수 있음.
			var collection = this.chromaApi.getCollection(this.tenantName, this.databaseName, this.collectionName);
			if (collection == null) {
				if (this.initializeSchema) {
					var tenant = this.chromaApi.getTenant(this.tenantName);
					if (tenant == null) {
						this.chromaApi.createTenant(this.tenantName);
					}

					var database = this.chromaApi.getDatabase(this.tenantName, this.databaseName);
					if (database == null) {
						this.chromaApi.createDatabase(this.tenantName, this.databaseName);
					}

					collection = this.chromaApi.createCollection(this.tenantName, this.databaseName,
							new ChromaApi.CreateCollectionRequest(this.collectionName));
				}
				else {
					throw new RuntimeException(&quot;Collection &quot; + this.collectionName
							+ &quot; doesn't exist and won't be created as the initializeSchema is set to false.&quot;);
				}
			}
			if (collection != null) {
				this.collectionId = collection.id();
			}
			this.initialized = true;
		}
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chromadb 도커로 띄운 이후에 &lt;a href=&quot;http://localhost:8000/docs&quot;&gt;localhost:8000/docs&lt;/a&gt; 들어가면 swagger 에서 API 명세 확인이 가능합니다.&amp;nbsp;chromadb 의 API 명세와 spring ai 버전에서 ChromaApi 클래스의 메서드 시그니처에서 호출하는 URI 가 동일한지 확인해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763717551286&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 컬렉션 조회 메서드 
@Nullable
	public Collection getCollection(String tenantName, String databaseName, String collectionName) {

		try {
			return this.restClient.get()
			// 여기 URI 가 chromadb 의 swagger 명세와 동일한 확인하라. 
				.uri(&quot;/api/v2/tenants/{tenant_name}/databases/{database_name}/collections/{collection_name}&quot;,
						tenantName, databaseName, collectionName)
				.headers(this::httpHeaders)
				.retrieve()
				.toEntity(Collection.class)
				.getBody();
		}
		catch (HttpServerErrorException | HttpClientErrorException e) {
			String msg = this.getErrorMessage(e);
			if (String.format(&quot;Collection [%s] does not exists&quot;, collectionName).equals(msg)) {
				return null;
			}
			throw new RuntimeException(msg, e);
		}
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 최신 버전으로 둘 다 설치했다면 이슈는 없을 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 이슈는 chromaDB 버전 업데이트 이후 spring AI 버전에서 제공하는 URI 경로가 서로 일치하지 않아서 실행 불가하다는 이슈입니다. 버전 이슈는 빈번하게 발생하니, 이런 이슈가 흥미롭네요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-ai/issues/2648&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-ai/issues/2648&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763717749557&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Chroma vectorstore not working after updating chroma db to version 1.0.0 &amp;middot; Issue #2648 &amp;middot; spring-projects/spring-ai&quot; data-og-description=&quot;i use translate Spring ai version is using set('springAiVersion', &amp;quot;1.0.0-M6&amp;quot;) When connecting to chroma db of version 1.0.0, an exception occurs: Caused by: org.springframework.web.client.HttpClien...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/spring-projects/spring-ai/issues/2648&quot; data-og-url=&quot;https://github.com/spring-projects/spring-ai/issues/2648&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b8Th2E/hyZOi7icfc/kjWF71gMNpX8DWik9zb4M0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cAGka7/hyZNBTB9wU/x8GPD5HW2AoKbKjAhkaKPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-ai/issues/2648&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/spring-projects/spring-ai/issues/2648&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b8Th2E/hyZOi7icfc/kjWF71gMNpX8DWik9zb4M0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cAGka7/hyZNBTB9wU/x8GPD5HW2AoKbKjAhkaKPk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Chroma vectorstore not working after updating chroma db to version 1.0.0 &amp;middot; Issue #2648 &amp;middot; spring-projects/spring-ai&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;i use translate Spring ai version is using set('springAiVersion', &quot;1.0.0-M6&quot;) When connecting to chroma db of version 1.0.0, an exception occurs: Caused by: org.springframework.web.client.HttpClien...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 마치겠습니다. Spring AI 아주 재밌었습니다.&amp;nbsp;&lt;/p&gt;</description>
      <author>juanxiu</author>
      <guid isPermaLink="true">https://juanxiu.tistory.com/5</guid>
      <comments>https://juanxiu.tistory.com/5#entry5comment</comments>
      <pubDate>Fri, 21 Nov 2025 18:38:22 +0900</pubDate>
    </item>
    <item>
      <title>[Cloud Club] 7기 활동 후기</title>
      <link>https://juanxiu.tistory.com/4</link>
      <description>&lt;h3&gt;Cloud Club 이 어떤 동아리일까? 공식 홈페이지 소개를 보면,&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Cloud Club은 클라우드의, 클라우드에 의한, 클라우드를 위한 IT 연합 동아리로 클라우드에 관심 있는 사람들이 모여 특정 벤더에 종속되지 않고(vendor-neutral) 폭 넓은 클라우드 인프라를 경험할 수 있는 IT 대외 동아리입니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;7기 활동&lt;/h3&gt;
&lt;p&gt;2025.03~07 약 4개월 동안 Cloud Club 7기 멤버로 활동했다. 스터디, 알클 신잡, 버디, 클클콘 등 다양한 활동이 있었다. &lt;/p&gt;
&lt;h4&gt;스터디&lt;/h4&gt;
&lt;p&gt;먼저 스터디는 2회차로 각각 약 8주씩 진행되는데, 다양한 주제의 스터디가 개설되었다. 홈서버 구축하기, Observability, 가상 면접 사례로 배우는 대규모 시스템 설계 기초 읽기, CKA 자격증 취득, Apache Kafka, 구글 SRE 파헤치기, 컴퓨터 밑바닥의 비밀 읽기, go lang team project, 오픈 소스 기여하기, bootable container로 OS 빌드 등 주제가 있었고, 원하는 스터디에 개수 제한 없이 참여하면 된다. &lt;/p&gt;
&lt;h4&gt;알클 신잡&lt;/h4&gt;
&lt;p&gt;그리고 알클 신잡은 기수 관계없이 모든 OB 들이 참여 가능한 행사로, 현직자 멘토님들에게 직무 소개를 듣고 자유롭게 커피챗을 할 수 있었다. 다양한 직무의 OB 들이 오셔서 해당 직무에 대한 궁금증 해결에 큰 도움이 되었다. &lt;/p&gt;
&lt;h4&gt;버디&lt;/h4&gt;
&lt;p&gt;버디 활동은 멤버들끼리 친해지기 위한 네트워킹 활동이다. 다도, 데브 컨퍼런스, 카공, 야구, 커피 버디 등 원하는 버디 활동에 자유롭게 참여할 수 있다. 멤버 인원이 많기 때문에 모르는 사람드로 있는데, 버디 활동 하면 친해지기 좋다. &lt;/p&gt;
&lt;h4&gt;클클콘&lt;/h4&gt;
&lt;p&gt;클클콘은 클라우드 클럽 컨퍼런스로, 기수 관계없이 연사자 신청을 할 수 있고 클럽 멤버뿐 아니라 외부인도 초청하여 함께 참여하는 행사였다. 재미있는 발표들이 준비되었는데, Proxmox : 홈서버 구축, 한 입 해보실래요?, BMaaS (BareMetal as a Service), 물리 서버를 CRUD, Terraform + GitOps Bridge로 누구나 딸깍 인프라 구축기, Istio와 함께 서비스 메쉬 환경 모니터링하기 주제가 있었다.&lt;br&gt;&lt;/br&gt;&lt;/p&gt;
&lt;p&gt;클클콘은 유튜브에 영상이 업로드 되는데, 내가 발표했던 주제인 &lt;a href=&quot;https://youtu.be/zU28N6YxuZY?si=b-wURD6qXMJ5Ekym&quot;&gt;Istio 와 함께 서비스 메쉬 모니터링 하기&lt;/a&gt; 영상 링크를 첨부한다 ㅎㅎ &lt;/p&gt;
&lt;h3&gt;마지막으로 7기 활동을 마무리 하면서 . .&lt;/h3&gt;
&lt;p&gt;너무너무 즐겁게 활동했다. 스터디에서 배운 점도 많고, 좋은 사람들 많이 알게 되었다. 클라우드 네이티브 기술을 공유할 커뮤니티를 찾고 있다면 클클로 오시길 :) &lt;/p&gt;</description>
      <author>juanxiu</author>
      <guid isPermaLink="true">https://juanxiu.tistory.com/4</guid>
      <comments>https://juanxiu.tistory.com/4#entry4comment</comments>
      <pubDate>Sat, 5 Jul 2025 12:56:47 +0900</pubDate>
    </item>
    <item>
      <title>AWS Solution Architect-Associate 자격증 리뷰</title>
      <link>https://juanxiu.tistory.com/3</link>
      <description>&lt;p&gt;AWS Solution Architect-Associate(SAA) 는 AWS 클라우드 환경에서 확장 가능하고, 안정적이며, 비용 효율적인 아키텍처를 설계할 수 있는 능력을 검증하는 자격증이다. 
2024년도 9월에 취득한 자격증인데, 늦게나마 후기를 써보려고 한다. &lt;/p&gt;

&lt;div data-iframe-width=&quot;150&quot; data-iframe-height=&quot;270&quot; data-share-badge-id=&quot;2ebd45ed-654f-498f-9435-aade9be9ee3d&quot; data-share-badge-host=&quot;https://www.credly.com&quot;&gt;&lt;/div&gt;&lt;script type=&quot;text/javascript&quot; async src=&quot;//cdn.credly.com/assets/utilities/embed.js&quot;&gt;&lt;/script&gt;


&lt;p&gt;자격증 취득을 위해 AWS 서비스 전반에 대해 공부해야 하는데, 많은 사람들이 말하는 것처럼 덤프를 최대한 돌리고 가는 게 좋다. 덤프는 구매하기엔 가격이 나가니 examtopics 에 있는 문제들을 크롤링하여 만드는 것도 방법이다. &lt;/p&gt;
&lt;br&gt;
&lt;p&gt;시험 응시 방법은 오프라인과 온라인 선택인데, 나는 추석 연휴에 시험을 보고 싶어서 온라인을 선택했다. 하지만 온라인 응시의 경우 여권이 꼭 필요하고 신분 인증 절차가 꽤나 복잡하니 오프라인 응시를 추천한다. 오프라인 응시는 여러 곳에 시험장이 있는데, 시험장 운영 시간만 맞춘다면 날짜 제약도 크게 없었던 것으로 기억한다.&lt;/p&gt;

&lt;br&gt;
&lt;p&gt;여담으로 곧 C04로 바뀐다는 말이 있으니 examtopics 에 있는 현재 문항들을 활용하고 싶다면 빠르게 응시하는 것을 추천한다. &lt;/p&gt;</description>
      <author>juanxiu</author>
      <guid isPermaLink="true">https://juanxiu.tistory.com/3</guid>
      <comments>https://juanxiu.tistory.com/3#entry3comment</comments>
      <pubDate>Wed, 2 Jul 2025 11:37:31 +0900</pubDate>
    </item>
    <item>
      <title>카프카 핵심가이드 리뷰</title>
      <link>https://juanxiu.tistory.com/2</link>
      <description>&lt;h3&gt;Consumer&lt;/h3&gt;
&lt;h4&gt;Consumer group&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;브로커는 성능을 위해 하나의 토픽을 여러 파티션으로 병렬 구성하여 처리합니다.&lt;/li&gt;
&lt;li&gt;하지만 둘 이상의 파티션을 하나의 컨슈머로만 처리한다면 성능 상의 문제가 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;그래서 카프카 컨슈머는 하나 이상의 컨슈머가 컨슈머 그룹(Consumer Group)을 구성하여 하나의 토픽을 구독할 수 있습니다.&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7yHTd/btsO1fUhyAU/C4FKoby5DqmO5FJs7LIcn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7yHTd/btsO1fUhyAU/C4FKoby5DqmO5FJs7LIcn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7yHTd/btsO1fUhyAU/C4FKoby5DqmO5FJs7LIcn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7yHTd%2FbtsO1fUhyAU%2FC4FKoby5DqmO5FJs7LIcn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;710&quot; height=&quot;826&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;br&gt;출처: &lt;a href=&quot;https://www.oreilly.com/library/view/kafka-the-definitive/9781491936153/ch04.html#T1_four_partitions&quot;&gt;https://www.oreilly.com/library/view/kafka-the-definitive/9781491936153/ch04.html#T1_four_partitions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;kafka 내부 mechanism&lt;/h3&gt;
&lt;h4&gt;클러스터 멤버쉽&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;각 브로커는 브로커 설정 파일에 정의되었거나 자동으로 생성된 고유한 식별자를 가진다.&lt;/li&gt;
&lt;li&gt;브로커 프로세스는 시작될 때 주키퍼에 Ephemeral 노드의 형태로 ID를 등록하고, 카프카 브로커들과 몇몇 생태계 툴들은 해당 ID가 저장된 /brokers/ids를 구독하여 이벤트를 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;컨트롤러&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;일반적인 카프카 브로커의 기능 + 파티션 리더를 선출하는 역할&lt;/li&gt;
&lt;li&gt;클러스터에서 가장 먼저 시작되는 브로커는 주키퍼의 /controller 에 Ephemeral 노드를 생성함으로써 컨트롤러가 된다.&lt;/li&gt;
&lt;li&gt;다른 브로커는 &amp;#39;노드가 이미 존재함&amp;#39; 예외를 받으면 컨트롤러 노드가 이미 있다는 걸 알아차리게 된다.&lt;/li&gt;
&lt;li&gt;브로커들은 주키퍼의 컨트롤러 노드에 변동이 생겼을 때 알림을 받기 위해 &amp;#39;와치&amp;#39;를 설정하고, 클러스터 안에 한 번에 한 개의 컨트롤러만 있도록 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Ephemeral 노드가 삭제될 경우&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;와치를 통해 이를 알게 되고, 주키퍼에 컨트롤러 노드를 생성하려고 시도한다.&lt;/li&gt;
&lt;li&gt;가장 먼저 새로운 노드를 생성한 브로커가 다음 컨트롤러가 되고, 다른 브로커는 새 컨트롤러 노드에 대한 와치를 다시 생성한다.&lt;/li&gt;
&lt;li&gt;컨트롤러 좀비&lt;ul&gt;
&lt;li&gt;새로운 컨트롤러가 선출될 때마다 &amp;#39;증가된 에포크 값&amp;#39;을 전달받는다.&lt;/li&gt;
&lt;li&gt;이것은 컨트롤러 브로커가 오랫동안 가비지 수집 때문에 멈춘 사이 주키퍼 사이의 연결이 끊어질 수 있기 때문에 중요하다.&lt;/li&gt;
&lt;li&gt;컨트롤러가 전송하는 메시지에 컨트롤러 에포크를 포함하면 브로커는 예전 컨트롤러가 보내온 메시지를 무시할 수 있다.&lt;/li&gt;
&lt;li&gt;브로커가 컨트롤러가 되면 클러스터 메타데이터 관리와 리더 선출을 시작하기 전에 먼저 주키퍼로부터 최신 레플리카 상태 맵을 읽어온다.&lt;/li&gt;
&lt;li&gt;브로커가 클러스터를 나가면, 컨트롤러는 해당 브로커가 리더를 맡고 있던 모든 파티션에 대해 새로운 브로커를 할당해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;KRaft 모드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;주키퍼 기반 컨트롤러로부터 탈피해서, 래프트 기반 컨트롤러로 옮겨간다.&lt;/li&gt;
&lt;li&gt;왜 카프카 커뮤니티는 컨트롤러를 교체하기로 결정했을까?&lt;ul&gt;
&lt;li&gt;컨트롤러가 주키퍼에 메타데이터를 쓰는 작업은 동기적으로 이루어지지만, 브로커 메시지를 보내는 작업은 비동기적으로 이루어진다. 그렇기 때문에 브로커, 컨트롤러, 주키퍼 간에 메타 데이터 불일치가 발생할 수도 있다.&lt;/li&gt;
&lt;li&gt;컨트롤러가 재시작될 때마다 주키퍼로부터 모든 브로커와 파티션에 대한 메타데이터를 읽어오고, 그것들을 모든 브로커로 전송해야 하는데 브로커와 파티션 수가 증가함에 따라 컨트롤러 재시작은 느려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;멱등적(idempotent) 프로듀서&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;멱등적이다 == 동일한 작업 실행해도 한 번만 실행한 것과 결과가 같다. (프로듀서 재시도로 인한 중복 방지)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;작동 원리&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;멱등적 프로듀서 기능을 켜면 모든 메시지는 고유한 프로듀서 ID(PID)와 sequence number를 가지게 된다.&lt;/li&gt;
&lt;li&gt;각 메시지의 고유한 식별자: 대상 토픽 및 파티션 + PID + Sequence number&lt;/li&gt;
&lt;li&gt;각 브로커는 해당 브로커에 할당한 모든 파티션들에 쓰여진 마지막 5개 메시지들을 추적하기 위해 이 고유 식별자를 사용&lt;/li&gt;
&lt;li&gt;max.in.flights.requests.per.connection(def=5,max=5: 파티션별로 추적되어야 하는 시퀀스 넘버의 수를 제한하고 싶다면 해당 설정을 5 이하로 잡으면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;작동 실패했을 경우&lt;/h4&gt;
&lt;h5&gt;1. 프로듀서 재시작&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;프로듀서에 장애가 발생할 경우, 보통 새 프로듀서를 생성해서 장애가 난 프로듀서를 대체한다.&lt;/li&gt;
&lt;li&gt;멱등적 프로듀서 기능이 켜있는 경우, 프로듀서는 프로듀서가 시작될 때 초기화 과정에서 카프카 브로커로부터 프로듀서 ID를 생성받는다.&lt;/li&gt;
&lt;li&gt;기존 프로듀서가 작동을 멈췄다가 새 프로듀서가 투입된 이후 작동을 재개해도, 서로 다른 PID를 가졌기 때문에 기존 프로듀서는 좀비로 취급되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2. 브로커 장애&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;브로커에 장애가 발생할 경우, 컨트롤러는 장애가 난 브로커가 리더를 맡고 있는 파티션에 대해 새 리더를 선출하게 된다.&lt;/li&gt;
&lt;li&gt;리더는 새 메시지가 쓰여질 때마다 인-메모리 프로듀서 상태에 저장된 최근 5개의 시퀀스 넘버를 업데이트한다.&lt;/li&gt;
&lt;li&gt;팔로워 레플리카는 리더로부터 새로운 메시지를 복제할 때마다 자체적인 인-메모리 버퍼를 업데이트한다.&lt;/li&gt;
&lt;li&gt;즉, 팔로워가 리더가 되어도 이 버퍼를 활용해 중복처리를 진행한다.&lt;/li&gt;
&lt;li&gt;만약 예전 리더가 복구된다면, 재시작 후에는 인-메모리 프로듀서 상태는 더 이상 메모리에 저장되어 있지 않다.&lt;/li&gt;
&lt;li&gt;그래서 복구 과정에 도움이 되도록 브로커는 종료되거나 새 세그먼트가 생성될 때마다 프로듀서 상태에 대한 스냅샷을 파일형태로 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Exactly Once Semantics (EOS) 구현하기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;중복 없이 정확히 한 번만 처리를 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Transactional Producer 설정&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;producer = Producer({
    &amp;#39;bootstrap.servers&amp;#39;: brokers,
    &amp;#39;transactional.id&amp;#39;: &amp;#39;eos-transactions.py&amp;#39;
})
producer.init_transactions()
producer.begin_transaction()&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;transactional.id: 프로듀서를 트랜잭션 모드로 동작&lt;/li&gt;
&lt;li&gt;읽고 쓰는 모든 행동을 하나의 묶음으로 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;메시지 읽고 변환 (Consume and Transform)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;msg = consumer.poll(timeout=1.0)
processed_key, processed_value = process_input(msg)
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;consumer.poll()로 메시지 읽어오기&lt;/li&gt;
&lt;li&gt;process_input(msg)는 읽은 메시지의 key와 value를 Base64로 인코딩해서 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;메시지 쓰기&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;producer.produce(output_topic, processed_value, processed_key,
                 on_delivery=delivery_report)&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;변환한 메시지를 새로운 토픽(output_topic)에 보냄.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Consumer offset과 함께 커밋&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;매 100개의 메시지를 처리할 때마다 트랜잭션을 커밋&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if msg_cnt % 100 == 0:
  producer.send_offsets_to_transaction(
      consumer.position(consumer.assignment()),
      consumer.consumer_group_metadata())
  producer.commit_transaction()
  producer.begin_transaction()&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;send_offsets_to_transaction: 현재 읽은 위치(offset)를 트랜잭션에 저장&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;commit_transaction: 읽은 메시지들과 쓴 메시지들, offset을 한꺼번에 커밋.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;begin_transaction: 새 트랜잭션 시작.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>juanxiu</author>
      <guid isPermaLink="true">https://juanxiu.tistory.com/2</guid>
      <comments>https://juanxiu.tistory.com/2#entry2comment</comments>
      <pubDate>Wed, 2 Jul 2025 11:13:32 +0900</pubDate>
    </item>
    <item>
      <title>[이화여대 졸업 프로젝트] 최대 혜택을 가지는 카드 추천 서비스</title>
      <link>https://juanxiu.tistory.com/1</link>
      <description>&lt;p&gt;이화여대 졸업 프로젝트 주제는 최대 혜택을 가지는 카드 추천 서비스이다. 아직 초기 MVP 기술 검증 단계라서 내용이 미흡하다. &lt;/p&gt;
&lt;h2&gt;주요 기능&lt;/h2&gt;
&lt;h3&gt;1. 매장 추천&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;가까운 거리일 때, 이전에 방문했던 매장일 때 가산점&lt;/li&gt;
&lt;li&gt;지난 달에 해당 업종에 쓴 평균 금액을 기준으로 예상 할인 금액 보여줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;카드 혜택 계산&lt;/h2&gt;
&lt;h3&gt;남은 한도 금액 계산&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;먼저 우대 조건에 해당하는지 확인 후&lt;/li&gt;
&lt;li&gt;소비 내역을 보고 동일한 혜택을 받았다면 그만큼의 할인 금액을 한도에서 빼기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;카드 혜택 프롬프트 엔지니어링&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;카드 혜택 프롬프트 엔지니어링 결과, 팀원분이 제공해주신 소비 내역은 우대 조건에 모두 만족하지 못해서 혜택 적용이 안된다는 결과가 나옴. 그래서 우선 우대 조건을 만족한다는 가정 하에 할인 한도 계산을 진행함.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
{  
    &amp;quot;업종&amp;quot;: &amp;quot;병원·약국·동물병원&amp;quot;,  
    &amp;quot;혜택유형&amp;quot;: &amp;quot;할인&amp;quot;,  
    &amp;quot;비율&amp;quot;: 5,  
    &amp;quot;최소이용금액&amp;quot;: null,  
    &amp;quot;브랜드&amp;quot;: \[\],  
    &amp;quot;할인 한도&amp;quot;: 3000,  
    &amp;quot;우대 조건&amp;quot;: &amp;quot;전월 실적 50만원 이상&amp;quot;  
  },  
  {  
    &amp;quot;업종&amp;quot;: &amp;quot;주유&amp;quot;,  
    &amp;quot;혜택유형&amp;quot;: &amp;quot;할인&amp;quot;,  
    &amp;quot;비율&amp;quot;: 5,  
    &amp;quot;최소이용금액&amp;quot;: null,  
    &amp;quot;브랜드&amp;quot;: \[\],  
    &amp;quot;할인 한도&amp;quot;: 3000,  
    &amp;quot;우대 조건&amp;quot;: &amp;quot;전월 실적 50만원 이상&amp;quot;  
  },  
  {
&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;카드별 혜택 적용 여부 LLM&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;카카오 api 로 카테고리 정렬&lt;/li&gt;
&lt;li&gt;한 달의 소비 내역에 대해, 각 카드별 혜택이 적용될 수 있는지를 판단&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;==== 8번째 거래: 우미마루 / 금액: 3000원 ====
적용여부: False
적용여부: False
적용여부: False
적용여부: False
적용여부: False
적용여부: False
적용여부: False
적용여부: True
적용카드: 디지로카 Las Vegas
할인금액: 60원
적용여부: False
적용여부: True
적용카드: 디지로카 London
할인금액: 21원
적용여부: False
적용여부: True
적용카드: 디지로카 London
할인금액: 30원
적용여부: True
적용카드: 디지로카 London
할인금액: 30원&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;할인 한도&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;위에서 각 거래(소비내역)에 대해 카드별 혜택 적용 여부 체크&lt;/li&gt;
&lt;li&gt;한도 초과 여부 판단 → 할인 적용 → 한도 갱신&lt;/li&gt;
&lt;li&gt;결과 예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;==== 2번째 거래: 코레일유통주식회사(서울역) / 금액: 4000원 ===
⛔ LOCA LIKIT 1.2 - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT 1.2 - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT 1.2 - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA LIKIT - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA for Edu - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA for Edu - 혜택 한도 소진 (0원 / 0원)
❌ LOCA for Health 혜택 미적용
❌ LOCA for Health 혜택 미적용
✅ LOCA for Health 혜택 적용! 할인: 400원 / 남은 한도: 4600원
❌ 롤라카드 혜택 미적용
❌ 롤라카드 혜택 미적용
⛔ 카카오뱅크 롯데 터치카드 - 혜택 한도 소진 (0원 / 0원)
⛔ 카카오뱅크 롯데 터치카드 - 혜택 한도 소진 (0원 / 0원)
⛔ 카카오뱅크 롯데 터치카드 - 혜택 한도 소진 (0원 / 0원)
⛔ 카카오뱅크 롯데 터치카드 - 혜택 한도 소진 (0원 / 0원)
⛔ LOCA 태그그립 카드 - 혜택 한도 소진 (0원 / 0원)
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>juanxiu</author>
      <guid isPermaLink="true">https://juanxiu.tistory.com/1</guid>
      <comments>https://juanxiu.tistory.com/1#entry1comment</comments>
      <pubDate>Tue, 27 May 2025 10:07:13 +0900</pubDate>
    </item>
  </channel>
</rss>