<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>TUE</title>
    <link>https://tuesday04.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 4 Aug 2026 22:20:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>tues</managingEditor>
    <item>
      <title>Apache Kafka 공식 문서 보면서 핵심 기능 정리</title>
      <link>https://tuesday04.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/intro?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kafka.apache.org/intro?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762775996776&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;Apache Kafka&quot; data-og-description=&quot;Apache Kafka: A Distributed Streaming Platform.&quot; data-og-host=&quot;kafka.apache.org&quot; data-og-source-url=&quot;https://kafka.apache.org/intro?utm_source=chatgpt.com&quot; data-og-url=&quot;https://kafka.apache.org/intro?utm_source=chatgpt.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bYE4XG/hyZMBmg7un/KkNcVV4BzDDfmmhC0iJ5iK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/intro?utm_source=chatgpt.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kafka.apache.org/intro?utm_source=chatgpt.com&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bYE4XG/hyZMBmg7un/KkNcVV4BzDDfmmhC0iJ5iK/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Apache Kafka&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka: A Distributed Streaming Platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kafka.apache.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;Kafka는 단순한 메시지큐가 아니라, &lt;b&gt;데이터의 중추 신경계&lt;/b&gt;다.&amp;rdquo;&lt;br /&gt;실시간 데이터가 생성되고, 저장되고, 처리되는 전 과정을 하나의 시스템으로 연결하는 이벤트 스트리밍 플랫폼이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;373&quot; data-start=&quot;352&quot; data-ke-size=&quot;size26&quot;&gt;1️⃣ 이벤트 스트리밍이란?&lt;/h2&gt;
&lt;p data-end=&quot;504&quot; data-start=&quot;375&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트 스트리밍(Event Streaming)&lt;/b&gt; 은&lt;br /&gt;데이터베이스, 센서, 모바일 앱, 클라우드, 소프트웨어 등에서 발생하는 **&amp;ldquo;사건(Event)&amp;rdquo;**을&lt;br /&gt;실시간으로 &lt;b&gt;수집 &amp;rarr; 저장 &amp;rarr; 처리 &amp;rarr; 전달&lt;/b&gt; 하는 기술이다.&lt;/p&gt;
&lt;p data-end=&quot;517&quot; data-start=&quot;506&quot; data-ke-size=&quot;size16&quot;&gt;  쉽게 말해,&lt;/p&gt;
&lt;blockquote data-end=&quot;605&quot; data-start=&quot;518&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;605&quot; data-start=&quot;520&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;데이터를 한 번 저장하고 끝내는 게 아니라,&lt;br /&gt;계속 &lt;b&gt;흐르게(stream)&lt;/b&gt; 해서 필요한 곳에서 즉시 반응할 수 있게 만드는 기술&quot;이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-end=&quot;620&quot; data-start=&quot;607&quot; data-ke-size=&quot;size23&quot;&gt;  예시로 보면&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;783&quot; data-start=&quot;621&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;665&quot; data-start=&quot;621&quot;&gt;&lt;b&gt;은행 앱&lt;/b&gt; &amp;rarr; &amp;ldquo;입금/송금 이벤트&amp;rdquo;를 즉시 감지해 거래 내역에 반영&lt;/li&gt;
&lt;li data-end=&quot;702&quot; data-start=&quot;666&quot;&gt;&lt;b&gt;택배 서비스&lt;/b&gt; &amp;rarr; 차량 위치 이벤트를 실시간으로 추적&lt;/li&gt;
&lt;li data-end=&quot;743&quot; data-start=&quot;703&quot;&gt;&lt;b&gt;스마트 공장&lt;/b&gt; &amp;rarr; 센서 데이터로 기계 이상 여부를 즉시 감지&lt;/li&gt;
&lt;li data-end=&quot;783&quot; data-start=&quot;744&quot;&gt;&lt;b&gt;쇼핑몰&lt;/b&gt; &amp;rarr; 고객 클릭 이벤트를 바로 분석해 추천상품 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;788&quot; data-start=&quot;785&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;817&quot; data-start=&quot;790&quot; data-ke-size=&quot;size26&quot;&gt;⚙️ 2️⃣ Kafka의 세 가지 핵심 기능&lt;/h2&gt;
&lt;p data-end=&quot;922&quot; data-start=&quot;819&quot; data-ke-size=&quot;size16&quot;&gt;Kafka는 이벤트 스트리밍을 위한 &lt;b&gt;세 가지 본질적 기능&lt;/b&gt;을 통합해 제공한다.&lt;br /&gt;이 세 가지를 이해하면 &amp;ldquo;Kafka가 왜 단순 메시지 브로커와 다른가&amp;rdquo;를 명확히 구분할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1408&quot; data-start=&quot;924&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1408&quot; data-start=&quot;975&quot;&gt;
&lt;tr data-end=&quot;1156&quot; data-start=&quot;975&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1007&quot; data-start=&quot;975&quot;&gt;  &lt;b&gt;1. Publish / Subscribe&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;1117&quot; data-start=&quot;1007&quot; data-col-size=&quot;md&quot;&gt;데이터를 &lt;b&gt;발행(Publish)&lt;/b&gt; 하고 &lt;b&gt;구독(Subscribe)&lt;/b&gt; 하는 기능.&lt;br /&gt;Producer가 이벤트를 보내면, Consumer는 구독한 Topic에서 이를 실시간으로 읽는다.&lt;/td&gt;
&lt;td data-end=&quot;1156&quot; data-start=&quot;1117&quot; data-col-size=&quot;sm&quot;&gt;SNS: 사용자가 게시글을 올리면 팔로워가 실시간으로 보는 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1276&quot; data-start=&quot;1157&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1175&quot; data-start=&quot;1157&quot;&gt;  &lt;b&gt;2. Store&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;1254&quot; data-start=&quot;1175&quot;&gt;들어온 이벤트 스트림을 &lt;b&gt;영구적으로 저장&lt;/b&gt;.&lt;br /&gt;보존 기간(retention)을 설정할 수 있으며, 일정 기간 후 자동 삭제 가능.&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1276&quot; data-start=&quot;1254&quot;&gt;&amp;ldquo;모든 대화를 기록하는 블랙박스&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1408&quot; data-start=&quot;1277&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1297&quot; data-start=&quot;1277&quot;&gt;⚙️ &lt;b&gt;3. Process&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;md&quot; data-end=&quot;1381&quot; data-start=&quot;1297&quot;&gt;들어온 데이터를 &lt;b&gt;실시간으로 처리하거나&lt;/b&gt;, 과거 데이터를 회고적으로 재처리.&lt;br /&gt;Kafka Streams API로 집계&amp;middot;조인&amp;middot;변환 등 가능.&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1408&quot; data-start=&quot;1381&quot;&gt;&amp;ldquo;두뇌처럼 생각하고 반응하는 데이터 흐름&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1526&quot; data-start=&quot;1410&quot; data-ke-size=&quot;size16&quot;&gt;  이 세 가지 기능은 모두&lt;br /&gt;&lt;b&gt;분산형(Distributed)&lt;/b&gt;, &lt;b&gt;고확장성(Scalable)&lt;/b&gt;, &lt;b&gt;내결함성(Fault-tolerant)&lt;/b&gt;, &lt;b&gt;보안(Security)&lt;/b&gt; 구조로 설계되어 있다.&lt;/p&gt;
&lt;hr data-end=&quot;1531&quot; data-start=&quot;1528&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1576&quot; data-start=&quot;1533&quot; data-ke-size=&quot;size26&quot;&gt;  3️⃣ Kafka의 주요 요소 구조 (Core Components)&lt;/h2&gt;
&lt;div&gt;&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2128&quot; data-start=&quot;1578&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2128&quot; data-start=&quot;1618&quot;&gt;
&lt;tr data-end=&quot;1684&quot; data-start=&quot;1618&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1633&quot; data-start=&quot;1618&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1650&quot; data-start=&quot;1633&quot;&gt;데이터를 Kafka로 전송&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1684&quot; data-start=&quot;1650&quot;&gt;주문 서비스가 order.created 이벤트 발행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1752&quot; data-start=&quot;1685&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1700&quot; data-start=&quot;1685&quot;&gt;&lt;b&gt;Consumer&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1718&quot; data-start=&quot;1700&quot;&gt;Kafka에서 데이터를 읽음&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1752&quot; data-start=&quot;1718&quot;&gt;결제 서비스가 order.created 이벤트 소비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1824&quot; data-start=&quot;1753&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1765&quot; data-start=&quot;1753&quot;&gt;&lt;b&gt;Topic&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1788&quot; data-start=&quot;1765&quot;&gt;이벤트가 저장되는 공간 (폴더 개념)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1824&quot; data-start=&quot;1788&quot;&gt;&amp;ldquo;orders&amp;rdquo;, &amp;ldquo;payments&amp;rdquo;, &amp;ldquo;clicks&amp;rdquo; 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1888&quot; data-start=&quot;1825&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1841&quot; data-start=&quot;1825&quot;&gt;&lt;b&gt;Partition&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1867&quot; data-start=&quot;1841&quot;&gt;Topic을 나눈 단위 (병렬 처리 용도)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1888&quot; data-start=&quot;1867&quot;&gt;주문 ID 기준으로 파티션 분할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1952&quot; data-start=&quot;1889&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1902&quot; data-start=&quot;1889&quot;&gt;&lt;b&gt;Broker&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1922&quot; data-start=&quot;1902&quot;&gt;Kafka 서버 (실제 저장소)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1952&quot; data-start=&quot;1922&quot;&gt;Kafka 클러스터 내 3개의 Broker 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2005&quot; data-start=&quot;1953&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1967&quot; data-start=&quot;1953&quot;&gt;&lt;b&gt;Cluster&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1983&quot; data-start=&quot;1967&quot;&gt;여러 Broker의 집합&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2005&quot; data-start=&quot;1983&quot;&gt;운영환경에서는 보통 3~5개 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2065&quot; data-start=&quot;2006&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2019&quot; data-start=&quot;2006&quot;&gt;&lt;b&gt;Offset&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2036&quot; data-start=&quot;2019&quot;&gt;메시지의 위치(순서) 정보&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2065&quot; data-start=&quot;2036&quot;&gt;&amp;ldquo;Consumer가 어디까지 읽었는가&amp;rdquo;를 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2128&quot; data-start=&quot;2066&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2080&quot; data-start=&quot;2066&quot;&gt;&lt;b&gt;Replica&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2100&quot; data-start=&quot;2080&quot;&gt;데이터 복제본 (내결함성 보장)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2128&quot; data-start=&quot;2100&quot;&gt;3개 복제본 &amp;rarr; 한 서버가 죽어도 복구 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2246&quot; data-start=&quot;2130&quot; data-ke-size=&quot;size16&quot;&gt;  예를 들어 Topic: orders 에 &amp;ldquo;order.created&amp;rdquo; 이벤트가 들어오면,&lt;br /&gt;Producer는 이를 Kafka에 publish &amp;rarr; Broker에 저장 &amp;rarr; Consumer가 읽는 구조다.&lt;/p&gt;
&lt;hr data-end=&quot;2251&quot; data-start=&quot;2248&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2282&quot; data-start=&quot;2253&quot; data-ke-size=&quot;size26&quot;&gt;  4️⃣ Kafka의 다섯 가지 핵심 API&lt;/h2&gt;
&lt;p data-end=&quot;2370&quot; data-start=&quot;2284&quot; data-ke-size=&quot;size16&quot;&gt;Kafka는 이벤트의 &amp;ldquo;생명주기 전체&amp;rdquo;를 다루기 위해 5개의 API를 제공한다.&lt;br /&gt;이 API들은 각기 다른 역할을 맡아 완성된 스트리밍 시스템을 만든다.&lt;/p&gt;
&lt;div&gt;&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2883&quot; data-start=&quot;2372&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2883&quot; data-start=&quot;2429&quot;&gt;
&lt;tr data-end=&quot;2508&quot; data-start=&quot;2429&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2448&quot; data-start=&quot;2429&quot;&gt;  &lt;b&gt;Admin API&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2476&quot; data-start=&quot;2448&quot; data-col-size=&quot;sm&quot;&gt;Kafka 클러스터, 토픽, 파티션 등을 관리&lt;/td&gt;
&lt;td data-end=&quot;2508&quot; data-start=&quot;2476&quot; data-col-size=&quot;sm&quot;&gt;토픽 생성, 삭제, 파티션 확장, 브로커 상태 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2598&quot; data-start=&quot;2509&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2531&quot; data-start=&quot;2509&quot;&gt;✉️ &lt;b&gt;Producer API&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2561&quot; data-start=&quot;2531&quot; data-col-size=&quot;sm&quot;&gt;Kafka로 이벤트를 &lt;b&gt;발행(Publish)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2598&quot; data-start=&quot;2561&quot; data-col-size=&quot;sm&quot;&gt;주문 생성 시 order.created.v1 이벤트 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2690&quot; data-start=&quot;2599&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2621&quot; data-start=&quot;2599&quot;&gt;  &lt;b&gt;Consumer API&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2652&quot; data-start=&quot;2621&quot; data-col-size=&quot;sm&quot;&gt;Kafka에서 이벤트를 &lt;b&gt;구독(Consume)&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2690&quot; data-start=&quot;2652&quot; data-col-size=&quot;sm&quot;&gt;결제 서비스가 order.created.v1 이벤트를 읽음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2777&quot; data-start=&quot;2691&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2712&quot; data-start=&quot;2691&quot;&gt;  &lt;b&gt;Streams API&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2745&quot; data-start=&quot;2712&quot; data-col-size=&quot;sm&quot;&gt;Kafka 내부 스트림을 &lt;b&gt;실시간 처리/변환/집계&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2777&quot; data-start=&quot;2745&quot; data-col-size=&quot;sm&quot;&gt;주문 + 결제 데이터를 조인해 &amp;ldquo;구매 전환율&amp;rdquo; 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2883&quot; data-start=&quot;2778&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2799&quot; data-start=&quot;2778&quot;&gt;  &lt;b&gt;Connect API&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2839&quot; data-start=&quot;2799&quot; data-col-size=&quot;sm&quot;&gt;Kafka와 외부 시스템(DB, 클라우드 등)을 &lt;b&gt;자동 동기화&lt;/b&gt;&lt;/td&gt;
&lt;td data-end=&quot;2883&quot; data-start=&quot;2839&quot; data-col-size=&quot;sm&quot;&gt;PostgreSQL &amp;rarr; Kafka &amp;rarr; Elasticsearch 자동 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2913&quot; data-start=&quot;2885&quot; data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;Streams vs Connect 차이&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 73px;&quot; border=&quot;1&quot; data-end=&quot;3086&quot; data-start=&quot;2914&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;3008&quot; data-start=&quot;2960&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;2982&quot; data-start=&quot;2960&quot;&gt;Streams&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;3008&quot; data-start=&quot;2982&quot; data-col-size=&quot;sm&quot;&gt;Connect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kafka 내부에서 &amp;ldquo;데이터 처리&amp;rdquo;&lt;/td&gt;
&lt;td&gt;Kafka 외부 시스템과 &amp;ldquo;데이터 이동&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;3036&quot; data-start=&quot;3009&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;3021&quot; data-start=&quot;3009&quot;&gt;코드로 로직 작성&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;3036&quot; data-start=&quot;3021&quot; data-col-size=&quot;sm&quot;&gt;설정으로 커넥터 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot; data-end=&quot;3086&quot; data-start=&quot;3037&quot;&gt;
&lt;td style=&quot;height: 21px;&quot; data-col-size=&quot;sm&quot; data-end=&quot;3054&quot; data-start=&quot;3037&quot;&gt;실시간 분석, 집계, 조인&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot; data-end=&quot;3086&quot; data-start=&quot;3054&quot; data-col-size=&quot;sm&quot;&gt;DB 변경사항 캡처 (CDC), 클라우드 업로드 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;4106&quot; data-start=&quot;4091&quot; data-ke-size=&quot;size26&quot;&gt;  마무리: 내 생각&lt;/h2&gt;
&lt;p data-end=&quot;4128&quot; data-start=&quot;4108&quot; data-ke-size=&quot;size16&quot;&gt;Kafka를 공부하며 느낀 점은,&lt;/p&gt;
&lt;blockquote data-end=&quot;4197&quot; data-start=&quot;4129&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;4197&quot; data-start=&quot;4131&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;Kafka는 단순히 데이터를 옮기는 기술이 아니라,&lt;br /&gt;데이터가 실시간으로 &lt;b&gt;살아 움직이게 하는 인프라&lt;/b&gt;다.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;4269&quot; data-start=&quot;4199&quot; data-ke-size=&quot;size16&quot;&gt;REST API가 요청이 있을 때만 움직인다면,&lt;br /&gt;Kafka는 &amp;ldquo;사건이 발생하는 즉시 반응하는 뇌&amp;rdquo;처럼 동작한다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/114</guid>
      <comments>https://tuesday04.tistory.com/114#entry114comment</comments>
      <pubDate>Mon, 10 Nov 2025 21:04:16 +0900</pubDate>
    </item>
    <item>
      <title>  아파트 실거래가 API 연동기 - 프록시 문제 해결</title>
      <link>https://tuesday04.tistory.com/113</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://apt-trade-web.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://apt-trade-web.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759382586354&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;Vite + React + TS&quot; data-og-description=&quot;&quot; data-og-host=&quot;apt-trade-web.vercel.app&quot; data-og-source-url=&quot;https://apt-trade-web.vercel.app/&quot; data-og-url=&quot;https://apt-trade-web.vercel.app/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://apt-trade-web.vercel.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://apt-trade-web.vercel.app/&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;Vite + React + TS&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;apt-trade-web.vercel.app&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;h2 data-end=&quot;152&quot; data-start=&quot;136&quot; data-ke-size=&quot;size26&quot;&gt;1️⃣ 프록시를 쓴 이유&lt;/h2&gt;
&lt;p data-end=&quot;242&quot; data-start=&quot;153&quot; data-ke-size=&quot;size16&quot;&gt;서울시 공공데이터포털에서 제공하는 &lt;b&gt;아파트 매매 실거래가 API&lt;/b&gt;를 React 프론트엔드에서 직접 불러오려 했습니다.&lt;br /&gt;하지만 두 가지 문제가 있었습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;399&quot; data-start=&quot;244&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;322&quot; data-start=&quot;244&quot;&gt;&lt;b&gt;CORS 차단&lt;/b&gt;&lt;br /&gt;브라우저에서 바로 openapi.seoul.go.kr API를 호출하면 보안 정책 때문에 차단됨.&lt;/li&gt;
&lt;li data-end=&quot;399&quot; data-start=&quot;324&quot;&gt;&lt;b&gt;API Key 노출&lt;/b&gt;&lt;br /&gt;발급받은 인증키를 그대로 프론트 코드에 넣으면, 배포 후 누구나 볼 수 있어 보안에 취약.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;401&quot; data-ke-size=&quot;size16&quot;&gt;  그래서 &lt;b&gt;프론트 &amp;rarr; 프록시 서버(Express) &amp;rarr; 공공데이터 API&lt;/b&gt; 구조로 바꾸었습니다.&lt;br /&gt;프록시가 대신 API를 호출하고, 응답을 JSON으로 정리해 프론트로 전달하는 방식입니다.&lt;/p&gt;
&lt;hr data-end=&quot;517&quot; data-start=&quot;514&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;531&quot; data-start=&quot;519&quot; data-ke-size=&quot;size26&quot;&gt;2️⃣ 문제 원인&lt;/h2&gt;
&lt;p data-end=&quot;589&quot; data-start=&quot;532&quot; data-ke-size=&quot;size16&quot;&gt;프록시 구조를 적용하니 로컬에서는 잘 됐지만,&lt;br /&gt;Vercel에 배포했을 때는 이런 문제가 생겼습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;768&quot; data-start=&quot;591&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;639&quot; data-start=&quot;591&quot;&gt;&lt;b&gt;Timeout 발생&lt;/b&gt; : 프론트에서 API를 직접 때릴 때 응답을 못 받음&lt;/li&gt;
&lt;li data-end=&quot;705&quot; data-start=&quot;640&quot;&gt;&lt;b&gt;404 Not Found&lt;/b&gt; : Vercel 배포 후 /api/seoul/trades 경로를 찾지 못함&lt;/li&gt;
&lt;li data-end=&quot;768&quot; data-start=&quot;706&quot;&gt;&lt;b&gt;@vercel/node 타입 오류&lt;/b&gt; : 서버리스 함수에서 타입을 잘못 import해 빌드가 막힘&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;781&quot; data-start=&quot;770&quot; data-ke-size=&quot;size16&quot;&gt;원인을 정리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;930&quot; data-start=&quot;782&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;812&quot; data-start=&quot;782&quot;&gt;브라우저 직접 호출 &amp;rarr; CORS + Key 노출&lt;/li&gt;
&lt;li data-end=&quot;875&quot; data-start=&quot;813&quot;&gt;Vercel 배포 &amp;rarr; Express 서버 자체는 지원하지 않고, &lt;b&gt;서버리스 함수 단위로 배포해야 함&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;930&quot; data-start=&quot;876&quot;&gt;타입 선언 &amp;rarr; 런타임에는 필요 없는 @vercel/node를 import해서 에러 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;935&quot; data-start=&quot;932&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;949&quot; data-start=&quot;937&quot; data-ke-size=&quot;size26&quot;&gt;3️⃣ 해결 방법&lt;/h2&gt;
&lt;h3 data-end=&quot;971&quot; data-start=&quot;950&quot; data-ke-size=&quot;size23&quot;&gt;✅ 프록시 서버 (로컬 개발용)&lt;/h3&gt;
&lt;p data-end=&quot;1049&quot; data-start=&quot;972&quot; data-ke-size=&quot;size16&quot;&gt;개발 중에는 Express로 프록시 서버를 열어두고, vite.config.ts에서 /api 요청을 프록시로 넘기도록 설정했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1759382602715&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.get('/api/seoul/trades', async (req, res) =&amp;gt; {
  const { page, size, rcptYr, cggCd } = req.query;
  const url = `http://openapi.seoul.go.kr:8088/${process.env.SEOUL_SERVICE_KEY}/json/tbLnOpendataRtmsV/1/10/${rcptYr}/${cggCd}/`;
  const { data } = await axios.get(url);
  res.json(data);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1381&quot; data-start=&quot;1362&quot; data-ke-size=&quot;size23&quot;&gt;✅ 서버리스 함수 (배포용)&lt;/h3&gt;
&lt;p data-end=&quot;1467&quot; data-start=&quot;1382&quot; data-ke-size=&quot;size16&quot;&gt;Vercel은 Express 서버를 그대로 올릴 수 없으므로,&lt;br /&gt;api/seoul/trades.ts 파일을 만들어 &lt;b&gt;개별 함수&lt;/b&gt;로 배포했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1759382617096&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export default async function handler(req: any, res: any) {
  const { page = 1, size = 10, rcptYr, cggCd } = req.query;
  const start = (Number(page) - 1) * Number(size) + 1;
  const end = Number(page) * Number(size);

  const url = `http://openapi.seoul.go.kr:8088/${process.env.SEOUL_SERVICE_KEY}/json/tbLnOpendataRtmsV/${start}/${end}/${rcptYr}/${cggCd}/`;

  try {
    const { data } = await axios.get(url, { timeout: 20000 });
    res.status(200).json(data);
  } catch (e: any) {
    res.status(500).json({ message: 'Seoul API 호출 실패', detail: e.message });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2160&quot; data-start=&quot;2068&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2160&quot; data-start=&quot;2068&quot; data-ke-size=&quot;size26&quot;&gt;4️⃣ 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포 환경 차이, 서버리스 구조의 한계&lt;/b&gt;까지 이해할 수 있었습니다.&lt;br /&gt;이후에는 axios 요청에 타임아웃 로직을 추가하고,&lt;br /&gt;Vercel Edge Cache를 활용해 응답 속도와 안정성을 높였습니다.&lt;/p&gt;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/113</guid>
      <comments>https://tuesday04.tistory.com/113#entry113comment</comments>
      <pubDate>Thu, 2 Oct 2025 14:24:01 +0900</pubDate>
    </item>
    <item>
      <title>[error 해결기] React 페이지네이션 시 이전 데이터가 화면에 남는 문제</title>
      <link>https://tuesday04.tistory.com/112</link>
      <description>&lt;h2 data-end=&quot;150&quot; data-start=&quot;139&quot; data-ke-size=&quot;size26&quot;&gt;  문제 상황&lt;/h2&gt;
&lt;p data-end=&quot;273&quot; data-start=&quot;151&quot; data-ke-size=&quot;size16&quot;&gt;아파트 실거래가 API를 연동해 React로 리스트를 보여주는 프로젝트를 진행했다.&lt;br /&gt;API 응답은 정상적으로 내려오는데, 페이지 이동을 하면 &lt;b&gt;이전 페이지 데이터가 한두 개씩 화면에 남아 있는 현상&lt;/b&gt;이 발생했다.&lt;/p&gt;
&lt;p data-end=&quot;293&quot; data-start=&quot;275&quot; data-ke-size=&quot;size16&quot;&gt;API 응답 예시는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1759382288045&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;page&quot;: 2,
  &quot;totalCount&quot;: 4737,
  &quot;items&quot;: [
    {
      &quot;aptSeq&quot;: &quot;11680-11200-0619-0000-20250923&quot;,
      &quot;aptNm&quot;: &quot;강남자곡 힐스테이트&quot;,
      &quot;dealYmd&quot;: &quot;2025-09-23&quot;,
      &quot;dealAmountManwon&quot;: 128000,
      &quot;excluUseAr&quot;: 84.39,
      &quot;floor&quot;: 7
    },
    {
      &quot;aptSeq&quot;: &quot;11680-11200-0619-0000-20250923&quot;,
      &quot;aptNm&quot;: &quot;강남자곡 힐스테이트&quot;,
      &quot;dealYmd&quot;: &quot;2025-09-23&quot;,
      &quot;dealAmountManwon&quot;: 112500,
      &quot;excluUseAr&quot;: 59.93,
      &quot;floor&quot;: 7
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;689&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8yaCa/btsQ2lEPmMu/eoIOuzTg1v9kcrPVYjrzq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8yaCa/btsQ2lEPmMu/eoIOuzTg1v9kcrPVYjrzq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8yaCa/btsQ2lEPmMu/eoIOuzTg1v9kcrPVYjrzq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8yaCa%2FbtsQ2lEPmMu%2FeoIOuzTg1v9kcrPVYjrzq1%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;1252&quot; height=&quot;689&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;689&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단에 자꾸 이전에 조회한 데이터가 남아있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 aptSeq + 같은 날짜(dealYmd)로 &lt;b&gt;중복된 거래 데이터&lt;/b&gt;가 내려올 수 있다.&lt;br /&gt;그런데 React에서 &amp;lt;tr key={it.aptSeq}&amp;gt;를 쓰고 있어서 문제가 터졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;897&quot; data-start=&quot;886&quot; data-ke-size=&quot;size26&quot;&gt;  원인 분석&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1040&quot; data-start=&quot;898&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;934&quot; data-start=&quot;898&quot;&gt;React는 key 값으로 DOM 재사용 여부를 판단한다.&lt;/li&gt;
&lt;li data-end=&quot;1002&quot; data-start=&quot;935&quot;&gt;그런데 aptSeq 값이 &lt;b&gt;거래 단위에서 유일하지 않음&lt;/b&gt; &amp;rarr; 같은 단지, 같은 날짜, 다른 면적/금액이 존재.&lt;/li&gt;
&lt;li data-end=&quot;1040&quot; data-start=&quot;1003&quot;&gt;결과적으로 key 충돌이 일어나면서 이전 행이 남아 보이게 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1066&quot; data-start=&quot;1042&quot; data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;중복 key 사용&lt;/b&gt;이 원인이었다.&lt;/p&gt;
&lt;hr data-end=&quot;1071&quot; data-start=&quot;1068&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1083&quot; data-start=&quot;1073&quot; data-ke-size=&quot;size26&quot;&gt;✅ 해결 방법&lt;/h2&gt;
&lt;h3 data-end=&quot;1104&quot; data-start=&quot;1084&quot; data-ke-size=&quot;size23&quot;&gt;1) key를 고유하게 만들기&lt;/h3&gt;
&lt;p data-end=&quot;1139&quot; data-start=&quot;1105&quot; data-ke-size=&quot;size16&quot;&gt;중복될 수 없는 여러 필드를 조합해서 key를 만들어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1759382362849&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;tbody&amp;gt;
  {items.map((it) =&amp;gt; {
    const rowKey = [
      it.aptSeq,
      it.dealYmd,
      it.excluUseAr,
      it.dealAmountManwon,
      it.floor,
    ].join('|');

    return (
      &amp;lt;tr key={rowKey}&amp;gt;
        &amp;lt;td&amp;gt;{fmtDate(it.dealYmd)}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{it.aptNm}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{fmtAmt(it.dealAmountManwon)}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{fmtArea(it.excluUseAr)}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{it.floor}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{it.dealingGbn}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{[it.roadNm, it.jibun].filter(Boolean).join(' ')}&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
    );
  })}
&amp;lt;/tbody&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1726&quot; data-start=&quot;1677&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이렇게 하면 &lt;b&gt;각 거래 건별로 고유 key&lt;/b&gt;가 생성되어 React가 잘 구분한다.&lt;/p&gt;
&lt;h3 data-end=&quot;1751&quot; data-start=&quot;1728&quot; data-ke-size=&quot;size23&quot;&gt;2)&amp;nbsp; API에서 고유 ID 제공 시&lt;/h3&gt;
&lt;p data-end=&quot;1801&quot; data-start=&quot;1752&quot; data-ke-size=&quot;size16&quot;&gt;백엔드가 tradeId 같은 식별자를 내려준다면 그걸 key로 사용하는 게 최선이다.&lt;/p&gt;
&lt;p data-end=&quot;1801&quot; data-start=&quot;1752&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2007&quot; data-start=&quot;1999&quot; data-ke-size=&quot;size26&quot;&gt;  정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2141&quot; data-start=&quot;2008&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2045&quot; data-start=&quot;2008&quot;&gt;React 리스트 key는 반드시 &lt;b&gt;고유값&lt;/b&gt;이어야 한다.&lt;/li&gt;
&lt;li data-end=&quot;2087&quot; data-start=&quot;2046&quot;&gt;API에서 고유 ID가 없다면 여러 필드를 조합해 key를 만든다.&lt;/li&gt;
&lt;li data-end=&quot;2141&quot; data-start=&quot;2088&quot;&gt;이번 경우 aptSeq만 쓰면 중복이 발생했기 때문에 &lt;b&gt;복합 key&lt;/b&gt;로 해결했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2191&quot; data-start=&quot;2143&quot; data-ke-size=&quot;size16&quot;&gt;  이렇게 수정하니 페이지 이동 시 이전 데이터가 남는 현상은 깔끔하게 사라졌다.&lt;/p&gt;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/112</guid>
      <comments>https://tuesday04.tistory.com/112#entry112comment</comments>
      <pubDate>Thu, 2 Oct 2025 14:19:51 +0900</pubDate>
    </item>
    <item>
      <title>서울시 열린 데이터 광장 공공데이터 Open API 신청 &amp;amp; 활용</title>
      <link>https://tuesday04.tistory.com/111</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;공공데이터가 불이 나는 바람에 사용이 중단되었습니다ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Redis 적용을 다시 하려고 서울 공공데이터를 활용했습니다.&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를 활용합니다!&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;1. 회원가입, 로그인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://data.seoul.go.kr/&quot;&gt;https://data.seoul.go.kr/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759374576835&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;열린데이터광장 메인&quot; data-og-description=&quot;데이터분류,데이터검색,데이터활용&quot; data-og-host=&quot;data.seoul.go.kr&quot; data-og-source-url=&quot;https://data.seoul.go.kr/&quot; data-og-url=&quot;https://data.seoul.go.kr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b2Fd0h/hyZJ9oQopT/ONUYWtzKW6kLnVT0BaPnZk/img.jpg?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291,https://scrap.kakaocdn.net/dn/bkPM5x/hyZKh1t2Yz/gYEznyaKjGLqdsESRsZdmk/img.png?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291&quot;&gt;&lt;a href=&quot;https://data.seoul.go.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://data.seoul.go.kr/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b2Fd0h/hyZJ9oQopT/ONUYWtzKW6kLnVT0BaPnZk/img.jpg?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291,https://scrap.kakaocdn.net/dn/bkPM5x/hyZKh1t2Yz/gYEznyaKjGLqdsESRsZdmk/img.png?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291');&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;열린데이터광장 메인&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;데이터분류,데이터검색,데이터활용&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;data.seoul.go.kr&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 인증키 신청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://data.seoul.go.kr/dataList/OA-21275/S/1/datasetView.do&quot;&gt;https://data.seoul.go.kr/dataList/OA-21275/S/1/datasetView.do&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759374699983&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;열린데이터광장 메인&quot; data-og-description=&quot;데이터분류,데이터검색,데이터활용&quot; data-og-host=&quot;data.seoul.go.kr&quot; data-og-source-url=&quot;https://data.seoul.go.kr/dataList/OA-21275/S/1/datasetView.do&quot; data-og-url=&quot;https://data.seoul.go.kr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/eS9nK/hyZKqw9hNv/i6Bg5SmquTf6I43pDdyAl0/img.png?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291,https://scrap.kakaocdn.net/dn/b6WLHM/hyZJ1Z15It/upBe7cFlmhd2E2KkJpbFX0/img.jpg?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291&quot;&gt;&lt;a href=&quot;https://data.seoul.go.kr/dataList/OA-21275/S/1/datasetView.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://data.seoul.go.kr/dataList/OA-21275/S/1/datasetView.do&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/eS9nK/hyZKqw9hNv/i6Bg5SmquTf6I43pDdyAl0/img.png?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291,https://scrap.kakaocdn.net/dn/b6WLHM/hyZJ1Z15It/upBe7cFlmhd2E2KkJpbFX0/img.jpg?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291');&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;열린데이터광장 메인&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;데이터분류,데이터검색,데이터활용&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;data.seoul.go.kr&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;공공데이터인증키신청ㄷ.png&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IvxPs/btsQ0DMDKtJ/ekd9Dm1I1I2LOAZNJN1YjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IvxPs/btsQ0DMDKtJ/ekd9Dm1I1I2LOAZNJN1YjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IvxPs/btsQ0DMDKtJ/ekd9Dm1I1I2LOAZNJN1YjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIvxPs%2FbtsQ0DMDKtJ%2Fekd9Dm1I1I2LOAZNJN1YjK%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;1252&quot; height=&quot;613&quot; data-filename=&quot;공공데이터인증키신청ㄷ.png&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;613&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;인증키 신청을 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 가입 신청서 작성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;가입신청서.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4JL6l/btsQ04QHiFN/Btbxa3yGdVJddNVXXsLIy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4JL6l/btsQ04QHiFN/Btbxa3yGdVJddNVXXsLIy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4JL6l/btsQ04QHiFN/Btbxa3yGdVJddNVXXsLIy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4JL6l%2FbtsQ04QHiFN%2FBtbxa3yGdVJddNVXXsLIy0%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;1338&quot; height=&quot;768&quot; data-filename=&quot;가입신청서.png&quot; data-origin-width=&quot;1338&quot; data-origin-height=&quot;768&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;활용할 목적에 맞게 신청서를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용url이 없을 경우에 localhost를 적으면 됩니다.&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;4. 상단 인증키 관리 클릭&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;인증키관리.png&quot; data-origin-width=&quot;1427&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wbiks/btsQ0zpSfBK/SryfMvMxXgqvwVuNv2AtoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wbiks/btsQ0zpSfBK/SryfMvMxXgqvwVuNv2AtoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wbiks/btsQ0zpSfBK/SryfMvMxXgqvwVuNv2AtoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwbiks%2FbtsQ0zpSfBK%2FSryfMvMxXgqvwVuNv2AtoK%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;1427&quot; height=&quot;364&quot; data-filename=&quot;인증키관리.png&quot; data-origin-width=&quot;1427&quot; data-origin-height=&quot;364&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;5. 인증키 복사&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PfwPV/btsQY5Rdtli/NStMmoBsuYKGkYtCvA6Va1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PfwPV/btsQY5Rdtli/NStMmoBsuYKGkYtCvA6Va1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PfwPV/btsQY5Rdtli/NStMmoBsuYKGkYtCvA6Va1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPfwPV%2FbtsQY5Rdtli%2FNStMmoBsuYKGkYtCvA6Va1%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;1330&quot; height=&quot;181&quot; data-origin-width=&quot;1330&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증키를 복사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 데이터를 불러옵니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 호출&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 샘플 URL 구조 이해하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 호출할 API의 구조는 다음과 같습니다.&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;1253&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO7sBx/btsQ0j8HkdZ/Icr0yVArmgDK2OmZAM0s8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO7sBx/btsQ0j8HkdZ/Icr0yVArmgDK2OmZAM0s8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO7sBx/btsQ0j8HkdZ/Icr0yVArmgDK2OmZAM0s8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO7sBx%2FbtsQ0j8HkdZ%2FIcr0yVArmgDK2OmZAM0s8k%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;1253&quot; height=&quot;614&quot; data-origin-width=&quot;1253&quot; data-origin-height=&quot;614&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;(인증키)에는 발급받은 개인 키를 입력하면 됩니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1207&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uq8Xw/btsQZfZVXwq/Pd1kmufK7EiWcfKVVwfDMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uq8Xw/btsQZfZVXwq/Pd1kmufK7EiWcfKVVwfDMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uq8Xw/btsQZfZVXwq/Pd1kmufK7EiWcfKVVwfDMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuq8Xw%2FbtsQZfZVXwq%2FPd1kmufK7EiWcfKVVwfDMK%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;1207&quot; height=&quot;717&quot; data-origin-width=&quot;1207&quot; data-origin-height=&quot;717&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;출력값&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;896&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KASac/btsQ1oO3xyA/0JM4G2Nw8rLrA8UOToOsRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KASac/btsQ1oO3xyA/0JM4G2Nw8rLrA8UOToOsRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KASac/btsQ1oO3xyA/0JM4G2Nw8rLrA8UOToOsRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKASac%2FbtsQ1oO3xyA%2F0JM4G2Nw8rLrA8UOToOsRK%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;1092&quot; height=&quot;896&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;896&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;따라서 출력되는 결과값은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1759375727533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;tbLnOpendataRtmsV&amp;gt;
&amp;lt;list_total_count&amp;gt;2742500&amp;lt;/list_total_count&amp;gt;
&amp;lt;RESULT&amp;gt;
&amp;lt;CODE&amp;gt;INFO-000&amp;lt;/CODE&amp;gt;
&amp;lt;MESSAGE&amp;gt;정상 처리되었습니다&amp;lt;/MESSAGE&amp;gt;
&amp;lt;/RESULT&amp;gt;
&amp;lt;row&amp;gt;
&amp;lt;RCPT_YR&amp;gt;2025&amp;lt;/RCPT_YR&amp;gt;
&amp;lt;CGG_CD&amp;gt;11290&amp;lt;/CGG_CD&amp;gt;
&amp;lt;CGG_NM&amp;gt;성북구&amp;lt;/CGG_NM&amp;gt;
&amp;lt;STDG_CD&amp;gt;13300&amp;lt;/STDG_CD&amp;gt;
&amp;lt;STDG_NM&amp;gt;정릉동&amp;lt;/STDG_NM&amp;gt;
&amp;lt;LOTNO_SE&amp;gt;1&amp;lt;/LOTNO_SE&amp;gt;
&amp;lt;LOTNO_SE_NM&amp;gt;대지&amp;lt;/LOTNO_SE_NM&amp;gt;
&amp;lt;MNO&amp;gt;0266&amp;lt;/MNO&amp;gt;
&amp;lt;SNO&amp;gt;0157&amp;lt;/SNO&amp;gt;
&amp;lt;BLDG_NM&amp;gt;(266-157)&amp;lt;/BLDG_NM&amp;gt;
&amp;lt;CTRT_DAY&amp;gt;20250930&amp;lt;/CTRT_DAY&amp;gt;
&amp;lt;THING_AMT&amp;gt;9000&amp;lt;/THING_AMT&amp;gt;
&amp;lt;ARCH_AREA&amp;gt;31.68&amp;lt;/ARCH_AREA&amp;gt;
&amp;lt;LAND_AREA&amp;gt;18.000000&amp;lt;/LAND_AREA&amp;gt;
&amp;lt;FLR&amp;gt;1&amp;lt;/FLR&amp;gt;
&amp;lt;RGHT_SE/&amp;gt;
&amp;lt;RTRCN_DAY/&amp;gt;
&amp;lt;ARCH_YR&amp;gt;2006&amp;lt;/ARCH_YR&amp;gt;
&amp;lt;BLDG_USG&amp;gt;연립다세대&amp;lt;/BLDG_USG&amp;gt;
&amp;lt;DCLR_SE&amp;gt;중개거래&amp;lt;/DCLR_SE&amp;gt;
&amp;lt;OPBIZ_RESTAGNT_SGG_NM&amp;gt;서울 성북구&amp;lt;/OPBIZ_RESTAGNT_SGG_NM&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;row&amp;gt;
&amp;lt;RCPT_YR&amp;gt;2025&amp;lt;/RCPT_YR&amp;gt;
&amp;lt;CGG_CD&amp;gt;11215&amp;lt;/CGG_CD&amp;gt;
&amp;lt;CGG_NM&amp;gt;광진구&amp;lt;/CGG_NM&amp;gt;
&amp;lt;STDG_CD&amp;gt;10100&amp;lt;/STDG_CD&amp;gt;
&amp;lt;STDG_NM&amp;gt;중곡동&amp;lt;/STDG_NM&amp;gt;
&amp;lt;LOTNO_SE&amp;gt;1&amp;lt;/LOTNO_SE&amp;gt;
&amp;lt;LOTNO_SE_NM&amp;gt;대지&amp;lt;/LOTNO_SE_NM&amp;gt;
&amp;lt;MNO&amp;gt;0123&amp;lt;/MNO&amp;gt;
&amp;lt;SNO&amp;gt;0038&amp;lt;/SNO&amp;gt;
&amp;lt;BLDG_NM&amp;gt;나경타운&amp;lt;/BLDG_NM&amp;gt;
&amp;lt;CTRT_DAY&amp;gt;20250930&amp;lt;/CTRT_DAY&amp;gt;
&amp;lt;THING_AMT&amp;gt;39300&amp;lt;/THING_AMT&amp;gt;
&amp;lt;ARCH_AREA&amp;gt;49.59&amp;lt;/ARCH_AREA&amp;gt;
&amp;lt;LAND_AREA&amp;gt;30.000000&amp;lt;/LAND_AREA&amp;gt;
&amp;lt;FLR&amp;gt;2&amp;lt;/FLR&amp;gt;
&amp;lt;RGHT_SE/&amp;gt;
&amp;lt;RTRCN_DAY/&amp;gt;
&amp;lt;ARCH_YR&amp;gt;2015&amp;lt;/ARCH_YR&amp;gt;
&amp;lt;BLDG_USG&amp;gt;연립다세대&amp;lt;/BLDG_USG&amp;gt;
&amp;lt;DCLR_SE&amp;gt;중개거래&amp;lt;/DCLR_SE&amp;gt;
&amp;lt;OPBIZ_RESTAGNT_SGG_NM&amp;gt;서울 광진구&amp;lt;/OPBIZ_RESTAGNT_SGG_NM&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;row&amp;gt;
&amp;lt;RCPT_YR&amp;gt;2025&amp;lt;/RCPT_YR&amp;gt;
&amp;lt;CGG_CD&amp;gt;11200&amp;lt;/CGG_CD&amp;gt;
&amp;lt;CGG_NM&amp;gt;성동구&amp;lt;/CGG_NM&amp;gt;
&amp;lt;STDG_CD&amp;gt;12200&amp;lt;/STDG_CD&amp;gt;
&amp;lt;STDG_NM&amp;gt;용답동&amp;lt;/STDG_NM&amp;gt;
&amp;lt;LOTNO_SE&amp;gt;1&amp;lt;/LOTNO_SE&amp;gt;
&amp;lt;LOTNO_SE_NM&amp;gt;대지&amp;lt;/LOTNO_SE_NM&amp;gt;
&amp;lt;MNO&amp;gt;0229&amp;lt;/MNO&amp;gt;
&amp;lt;SNO&amp;gt;0013&amp;lt;/SNO&amp;gt;
&amp;lt;BLDG_NM&amp;gt;(229-13)&amp;lt;/BLDG_NM&amp;gt;
&amp;lt;CTRT_DAY&amp;gt;20250930&amp;lt;/CTRT_DAY&amp;gt;
&amp;lt;THING_AMT&amp;gt;8000&amp;lt;/THING_AMT&amp;gt;
&amp;lt;ARCH_AREA&amp;gt;29.88&amp;lt;/ARCH_AREA&amp;gt;
&amp;lt;LAND_AREA&amp;gt;41.480000&amp;lt;/LAND_AREA&amp;gt;
&amp;lt;FLR&amp;gt;8&amp;lt;/FLR&amp;gt;
&amp;lt;RGHT_SE/&amp;gt;
&amp;lt;RTRCN_DAY/&amp;gt;
&amp;lt;ARCH_YR&amp;gt;1991&amp;lt;/ARCH_YR&amp;gt;
&amp;lt;BLDG_USG&amp;gt;오피스텔&amp;lt;/BLDG_USG&amp;gt;
&amp;lt;DCLR_SE&amp;gt;직거래&amp;lt;/DCLR_SE&amp;gt;
&amp;lt;OPBIZ_RESTAGNT_SGG_NM/&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;row&amp;gt;
&amp;lt;RCPT_YR&amp;gt;2025&amp;lt;/RCPT_YR&amp;gt;
&amp;lt;CGG_CD&amp;gt;11530&amp;lt;/CGG_CD&amp;gt;
&amp;lt;CGG_NM&amp;gt;구로구&amp;lt;/CGG_NM&amp;gt;
&amp;lt;STDG_CD&amp;gt;10200&amp;lt;/STDG_CD&amp;gt;
&amp;lt;STDG_NM&amp;gt;구로동&amp;lt;/STDG_NM&amp;gt;
&amp;lt;LOTNO_SE&amp;gt;1&amp;lt;/LOTNO_SE&amp;gt;
&amp;lt;LOTNO_SE_NM&amp;gt;대지&amp;lt;/LOTNO_SE_NM&amp;gt;
&amp;lt;MNO&amp;gt;1256&amp;lt;/MNO&amp;gt;
&amp;lt;SNO&amp;gt;0000&amp;lt;/SNO&amp;gt;
&amp;lt;BLDG_NM&amp;gt;구로현대&amp;lt;/BLDG_NM&amp;gt;
&amp;lt;CTRT_DAY&amp;gt;20250930&amp;lt;/CTRT_DAY&amp;gt;
&amp;lt;THING_AMT&amp;gt;57700&amp;lt;/THING_AMT&amp;gt;
&amp;lt;ARCH_AREA&amp;gt;56.49&amp;lt;/ARCH_AREA&amp;gt;
&amp;lt;LAND_AREA&amp;gt;0.000000&amp;lt;/LAND_AREA&amp;gt;
&amp;lt;FLR&amp;gt;2&amp;lt;/FLR&amp;gt;
&amp;lt;RGHT_SE/&amp;gt;
&amp;lt;RTRCN_DAY/&amp;gt;
&amp;lt;ARCH_YR&amp;gt;1992&amp;lt;/ARCH_YR&amp;gt;
&amp;lt;BLDG_USG&amp;gt;아파트&amp;lt;/BLDG_USG&amp;gt;
&amp;lt;DCLR_SE&amp;gt;중개거래&amp;lt;/DCLR_SE&amp;gt;
&amp;lt;OPBIZ_RESTAGNT_SGG_NM&amp;gt;서울 구로구, 서울 영등포구&amp;lt;/OPBIZ_RESTAGNT_SGG_NM&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;row&amp;gt;
&amp;lt;RCPT_YR&amp;gt;2025&amp;lt;/RCPT_YR&amp;gt;
&amp;lt;CGG_CD&amp;gt;11650&amp;lt;/CGG_CD&amp;gt;
&amp;lt;CGG_NM&amp;gt;서초구&amp;lt;/CGG_NM&amp;gt;
&amp;lt;STDG_CD&amp;gt;10800&amp;lt;/STDG_CD&amp;gt;
&amp;lt;STDG_NM&amp;gt;서초동&amp;lt;/STDG_NM&amp;gt;
&amp;lt;LOTNO_SE&amp;gt;1&amp;lt;/LOTNO_SE&amp;gt;
&amp;lt;LOTNO_SE_NM&amp;gt;대지&amp;lt;/LOTNO_SE_NM&amp;gt;
&amp;lt;MNO&amp;gt;1434&amp;lt;/MNO&amp;gt;
&amp;lt;SNO&amp;gt;0011&amp;lt;/SNO&amp;gt;
&amp;lt;BLDG_NM&amp;gt;CM빌&amp;lt;/BLDG_NM&amp;gt;
&amp;lt;CTRT_DAY&amp;gt;20250930&amp;lt;/CTRT_DAY&amp;gt;
&amp;lt;THING_AMT&amp;gt;34200&amp;lt;/THING_AMT&amp;gt;
&amp;lt;ARCH_AREA&amp;gt;29.13&amp;lt;/ARCH_AREA&amp;gt;
&amp;lt;LAND_AREA&amp;gt;20.000000&amp;lt;/LAND_AREA&amp;gt;
&amp;lt;FLR&amp;gt;3&amp;lt;/FLR&amp;gt;
&amp;lt;RGHT_SE/&amp;gt;
&amp;lt;RTRCN_DAY/&amp;gt;
&amp;lt;ARCH_YR&amp;gt;2017&amp;lt;/ARCH_YR&amp;gt;
&amp;lt;BLDG_USG&amp;gt;연립다세대&amp;lt;/BLDG_USG&amp;gt;
&amp;lt;DCLR_SE&amp;gt;중개거래&amp;lt;/DCLR_SE&amp;gt;
&amp;lt;OPBIZ_RESTAGNT_SGG_NM&amp;gt;서울 서초구&amp;lt;/OPBIZ_RESTAGNT_SGG_NM&amp;gt;
&amp;lt;/row&amp;gt;
&amp;lt;/tbLnOpendataRtmsV&amp;gt;&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;2. Postman으로 호출하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Method: GET&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- URL: 위 샘플대로 입력합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1759375844975&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://openapi.seoul.go.kr:8088/(인증키)/xml/tbLnOpendataRtmsV/1/5/2025/11110&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1759375902432&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://openapi.seoul.go.kr:8088/(인증키)/xml/tbLnOpendataRtmsV/1/5/2024/11500/강서구/10300/1/대지/0151/0039/극동빌라/20240903/연립다세대&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;응답 확인 가능&lt;/p&gt;
&lt;pre id=&quot;code_1759375941541&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;tbLnOpendataRtmsV&amp;gt;
    &amp;lt;list_total_count&amp;gt;1&amp;lt;/list_total_count&amp;gt;
    &amp;lt;RESULT&amp;gt;
        &amp;lt;CODE&amp;gt;INFO-000&amp;lt;/CODE&amp;gt;
        &amp;lt;MESSAGE&amp;gt;정상 처리되었습니다&amp;lt;/MESSAGE&amp;gt;
    &amp;lt;/RESULT&amp;gt;
    &amp;lt;row&amp;gt;
        &amp;lt;RCPT_YR&amp;gt;2024&amp;lt;/RCPT_YR&amp;gt;
        &amp;lt;CGG_CD&amp;gt;11500&amp;lt;/CGG_CD&amp;gt;
        &amp;lt;CGG_NM&amp;gt;강서구&amp;lt;/CGG_NM&amp;gt;
        &amp;lt;STDG_CD&amp;gt;10300&amp;lt;/STDG_CD&amp;gt;
        &amp;lt;STDG_NM&amp;gt;화곡동&amp;lt;/STDG_NM&amp;gt;
        &amp;lt;LOTNO_SE&amp;gt;1&amp;lt;/LOTNO_SE&amp;gt;
        &amp;lt;LOTNO_SE_NM&amp;gt;대지&amp;lt;/LOTNO_SE_NM&amp;gt;
        &amp;lt;MNO&amp;gt;0151&amp;lt;/MNO&amp;gt;
        &amp;lt;SNO&amp;gt;0039&amp;lt;/SNO&amp;gt;
        &amp;lt;BLDG_NM&amp;gt;극동빌라(151-39)&amp;lt;/BLDG_NM&amp;gt;
        &amp;lt;CTRT_DAY&amp;gt;20240903&amp;lt;/CTRT_DAY&amp;gt;
        &amp;lt;THING_AMT&amp;gt;16300&amp;lt;/THING_AMT&amp;gt;
        &amp;lt;ARCH_AREA&amp;gt;57.09&amp;lt;/ARCH_AREA&amp;gt;
        &amp;lt;LAND_AREA&amp;gt;31.000000&amp;lt;/LAND_AREA&amp;gt;
        &amp;lt;FLR&amp;gt;1&amp;lt;/FLR&amp;gt;
        &amp;lt;RGHT_SE/&amp;gt;
        &amp;lt;RTRCN_DAY/&amp;gt;
        &amp;lt;ARCH_YR&amp;gt;1998&amp;lt;/ARCH_YR&amp;gt;
        &amp;lt;BLDG_USG&amp;gt;연립다세대&amp;lt;/BLDG_USG&amp;gt;
        &amp;lt;DCLR_SE&amp;gt;직거래&amp;lt;/DCLR_SE&amp;gt;
        &amp;lt;OPBIZ_RESTAGNT_SGG_NM/&amp;gt;
    &amp;lt;/row&amp;gt;
&amp;lt;/tbLnOpendataRtmsV&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;827&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D2rhe/btsQZP7Q79w/Mco19CkJhze0fFzSkjfV40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D2rhe/btsQZP7Q79w/Mco19CkJhze0fFzSkjfV40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D2rhe/btsQZP7Q79w/Mco19CkJhze0fFzSkjfV40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD2rhe%2FbtsQZP7Q79w%2FMco19CkJhze0fFzSkjfV40%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;1407&quot; height=&quot;827&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;827&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/111</guid>
      <comments>https://tuesday04.tistory.com/111#entry111comment</comments>
      <pubDate>Thu, 2 Oct 2025 12:35:29 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot + Redis로 아파트 실거래 조회 API 만들기</title>
      <link>https://tuesday04.tistory.com/110</link>
      <description>&lt;p data-end=&quot;228&quot; data-start=&quot;125&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;228&quot; data-start=&quot;125&quot; data-ke-size=&quot;size16&quot;&gt;최근에 &lt;b&gt;공공데이터포털 아파트 실거래 조회 API&lt;/b&gt;를 이용해서 프론트엔드에서 거래 내역을 보여주는 프로젝트를 진행했다.&lt;br /&gt;하지만 바로 부딪힌 문제는 &lt;b&gt;속도와 중복 호출&lt;/b&gt;이었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;635&quot; data-start=&quot;230&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;375&quot; data-start=&quot;230&quot;&gt;&lt;b&gt;응답 속도&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;375&quot; data-start=&quot;248&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;297&quot; data-start=&quot;248&quot;&gt;정부 API 특성상 XML로 데이터를 내려주고, 이를 JSON으로 변환해야 했다.&lt;/li&gt;
&lt;li data-end=&quot;375&quot; data-start=&quot;301&quot;&gt;네트워크도 느리고 응답 처리 과정까지 거치니 &lt;b&gt;사용자가 같은 조건으로 여러 번 조회할 때마다 매번 느린 응답을 받아야 했다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;488&quot; data-start=&quot;377&quot;&gt;&lt;b&gt;중복 호출&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;488&quot; data-start=&quot;395&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;488&quot; data-start=&quot;395&quot;&gt;유저가 같은 조건(예: &quot;강남구 2025년 9월&quot;)으로 여러 번 검색할 경우,&lt;br /&gt;매번 정부 서버를 다시 치게 되어 &lt;b&gt;불필요한 트래픽&lt;/b&gt;이 발생했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;635&quot; data-start=&quot;490&quot;&gt;&lt;b&gt;최근 검색 내역 없음&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;635&quot; data-start=&quot;514&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;571&quot; data-start=&quot;514&quot;&gt;&quot;내가 조금 전에 검색했던 조건&quot;을 다시 보려면,&lt;br /&gt;유저가 직접 다시 입력해야 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;791&quot; data-start=&quot;730&quot; data-ke-size=&quot;size16&quot;&gt;  이 세 가지 문제를 해결하기 위해 &lt;b&gt;Spring Boot + Redis 캐시&lt;/b&gt;를 도입하기로 했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;932&quot; data-start=&quot;792&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;857&quot; data-start=&quot;792&quot;&gt;&lt;b&gt;캐시(Cache)&lt;/b&gt;: 같은 조건으로 조회하면 일정 시간 동안은 Redis에서 바로 꺼내오기 &amp;rarr; 빠른 응답&lt;/li&gt;
&lt;li data-end=&quot;932&quot; data-start=&quot;858&quot;&gt;&lt;b&gt;최근 검색(Recent List)&lt;/b&gt;: 사용자가 조회한 조건을 Redis 리스트에 저장해 두고, 최근 검색 내역을 보여주기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전체적인 구조&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1759312031929&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Frontend (Vite/React)]
         │
         ▼
[Spring Boot (8081)]
  - Redis Cache (@Cacheable)
  - Redis List (최근 검색)
         │
         ▼
[Node Proxy (8080, Express)]
  - 공공데이터포털 API 호출
  - XML &amp;rarr; JSON 변환
         │
         ▼
[Government Open API]
         │
         ▼
[Redis (6379)]
  - 캐시 저장소
  - 최근 검색 내역 기록&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1 data-end=&quot;492&quot; data-start=&quot;482&quot;&gt;동작 원리&lt;/h1&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;799&quot; data-start=&quot;494&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;523&quot; data-start=&quot;494&quot;&gt;&lt;b&gt;프론트&lt;/b&gt;에서 /api/trades 호출&lt;/li&gt;
&lt;li data-end=&quot;609&quot; data-start=&quot;524&quot;&gt;&lt;b&gt;Spring Boot&lt;/b&gt;가 먼저 Redis 캐시 확인
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;609&quot; data-start=&quot;564&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;582&quot; data-start=&quot;564&quot;&gt;캐시 HIT &amp;rarr; 바로 응답&lt;/li&gt;
&lt;li data-end=&quot;609&quot; data-start=&quot;586&quot;&gt;캐시 MISS &amp;rarr; Node 프록시 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;652&quot; data-start=&quot;610&quot;&gt;&lt;b&gt;Node 프록시&lt;/b&gt;가 정부 API 호출 후 JSON 응답으로 정규화&lt;/li&gt;
&lt;li data-end=&quot;749&quot; data-start=&quot;653&quot;&gt;Spring Boot가 받은 결과를 Redis에 저장
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;749&quot; data-start=&quot;691&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;705&quot; data-start=&quot;691&quot;&gt;캐시: TTL 2분&lt;/li&gt;
&lt;li data-end=&quot;749&quot; data-start=&quot;709&quot;&gt;최근 검색: Redis List로 기록 (최대 20개, 7일 TTL)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;799&quot; data-start=&quot;750&quot;&gt;프론트에서 /api/recent 호출 시 Redis에 쌓인 최근 검색 내역 응답&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 아키텍처 다이어그램&lt;/b&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1759312576020&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;┌───────────────────────────────┐
│         Frontend (5173)       │
│   React / Vite / TS           │
└───────────────▲───────────────┘
                │ fetch /api/*
                │
┌───────────────┴───────────────┐
│     Spring Boot (8081)        │
│  - @Cacheable (Redis Cache)   │
│  - 최근검색 (Redis List)       │
└───────────────▲───────────────┘
                │ REST call
┌───────────────┴───────────────┐
│   Node Proxy (8080, Express)  │
│   - 공공데이터포털 API 호출    │
│   - XML &amp;rarr; JSON 변환           │
└───────────────▲───────────────┘
                │
┌───────────────┴───────────────┐
│ Government Open API (RTMS)    │
└───────────────────────────────┘

┌───────────────────────────────┐
│          Redis (6379)          │
│  - Cache (2분 TTL)             │
│  - 최근검색 리스트(20개, 7일)   │
└───────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨트롤러&lt;/h2&gt;
&lt;pre id=&quot;code_1759312562257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.redis_demo.api;

import com.example.redis_demo.dto.TradeListResp;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping(&quot;/api&quot;)
@RequiredArgsConstructor
public class DealController {

    private final DealService service;

    @GetMapping(&quot;/trades&quot;)
    public TradeListResp trades(
            @RequestParam String sgg,
            @RequestParam String ym,
            @RequestParam(defaultValue = &quot;1&quot;) int page,
            @RequestParam(defaultValue = &quot;10&quot;) int size,
            @RequestHeader(value = &quot;X-User-Id&quot;, required = false) String userId
    ) {
        return service.getDeals(sgg, ym, page, size, userId);
    }

    @GetMapping(&quot;/recent&quot;)
    public List&amp;lt;String&amp;gt; recent(
            @RequestParam(defaultValue = &quot;10&quot;) int limit,
            @RequestHeader(value = &quot;X-User-Id&quot;, required = false) String userId
    ) {
        return service.getRecent(userId, limit);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/110</guid>
      <comments>https://tuesday04.tistory.com/110#entry110comment</comments>
      <pubDate>Wed, 1 Oct 2025 18:56:44 +0900</pubDate>
    </item>
    <item>
      <title>국토교통부 아파트 실거래가 API 활용기</title>
      <link>https://tuesday04.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.data.go.kr/data/15126468/openapi.do#/API%20%EB%AA%A9%EB%A1%9D/getRTMSDataSvcAptTradeDev&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.data.go.kr/data/15126468/openapi.do#/API%20%EB%AA%A9%EB%A1%9D/getRTMSDataSvcAptTradeDev&lt;/a&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;h2 data-end=&quot;366&quot; data-start=&quot;351&quot; data-ke-size=&quot;size26&quot;&gt;API 기본 정보&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;796&quot; data-start=&quot;367&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;453&quot; data-start=&quot;367&quot;&gt;&lt;b&gt;Endpoint&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758792971779&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;https://apis.data.go.kr/1613000/RTMSDataSvcAptTradeDev&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;796&quot; data-start=&quot;367&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;473&quot; data-start=&quot;454&quot;&gt;&lt;b&gt;Method&lt;/b&gt;: GET&lt;/li&gt;
&lt;li data-end=&quot;523&quot; data-start=&quot;474&quot;&gt;&lt;b&gt;주요 Path&lt;/b&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1758792998010&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/getRTMSDataSvcAptTradeDev&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;796&quot; data-start=&quot;367&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;796&quot; data-start=&quot;524&quot;&gt;&lt;b&gt;필수 파라미터&lt;/b&gt;:&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;Name설명예시
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;796&quot; data-start=&quot;541&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;796&quot; data-start=&quot;587&quot;&gt;
&lt;tr data-end=&quot;643&quot; data-start=&quot;587&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;599&quot; data-start=&quot;587&quot;&gt;LAWD_CD&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;625&quot; data-start=&quot;599&quot;&gt;지역코드 (법정동 10자리 중 앞 5자리)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;643&quot; data-start=&quot;625&quot;&gt;41117 (수원 영통구)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;690&quot; data-start=&quot;646&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;659&quot; data-start=&quot;646&quot;&gt;DEAL_YMD&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;680&quot; data-start=&quot;659&quot;&gt;계약년월 (6자리, YYYYMM)&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;690&quot; data-start=&quot;680&quot;&gt;202509&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;732&quot; data-start=&quot;693&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;708&quot; data-start=&quot;693&quot;&gt;serviceKey&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;722&quot; data-start=&quot;708&quot;&gt;공공데이터포털 인증키&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;732&quot; data-start=&quot;722&quot;&gt;발급받은 키&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;760&quot; data-start=&quot;735&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;746&quot; data-start=&quot;735&quot;&gt;pageNo&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;755&quot; data-start=&quot;746&quot;&gt;페이지 번호&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;760&quot; data-start=&quot;755&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;796&quot; data-start=&quot;763&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;777&quot; data-start=&quot;763&quot;&gt;numOfRows&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;790&quot; data-start=&quot;777&quot;&gt;페이지당 데이터 수&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;796&quot; data-start=&quot;790&quot;&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;801&quot; data-start=&quot;798&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;822&quot; data-start=&quot;803&quot; data-ke-size=&quot;size26&quot;&gt;실제 Request 예시&lt;/h2&gt;
&lt;pre id=&quot;code_1758793012349&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET https://apis.data.go.kr/1613000/RTMSDataSvcAptTradeDev/getRTMSDataSvcAptTradeDev
  ?serviceKey=발급받은키
  &amp;amp;LAWD_CD=41117
  &amp;amp;DEAL_YMD=202509
  &amp;amp;pageNo=1
  &amp;amp;numOfRows=10&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-end=&quot;1033&quot; data-start=&quot;1010&quot; data-ke-size=&quot;size26&quot;&gt;Response 분석 (XML)&lt;/h2&gt;
&lt;p data-end=&quot;1055&quot; data-start=&quot;1034&quot; data-ke-size=&quot;size16&quot;&gt;응답은 기본적으로 &lt;b&gt;XML&lt;/b&gt; 형태:&lt;/p&gt;
&lt;pre id=&quot;code_1758793028813&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;response&amp;gt;
  &amp;lt;header&amp;gt;
    &amp;lt;resultCode&amp;gt;000&amp;lt;/resultCode&amp;gt;
    &amp;lt;resultMsg&amp;gt;OK&amp;lt;/resultMsg&amp;gt;
  &amp;lt;/header&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;items&amp;gt;
      &amp;lt;item&amp;gt;
        &amp;lt;aptNm&amp;gt;신나무실신안&amp;lt;/aptNm&amp;gt;
        &amp;lt;dealAmount&amp;gt;36,000&amp;lt;/dealAmount&amp;gt;
        &amp;lt;dealYear&amp;gt;2025&amp;lt;/dealYear&amp;gt;
        &amp;lt;dealMonth&amp;gt;9&amp;lt;/dealMonth&amp;gt;
        &amp;lt;dealDay&amp;gt;24&amp;lt;/dealDay&amp;gt;
        &amp;lt;excluUseAr&amp;gt;59.965&amp;lt;/excluUseAr&amp;gt;
        &amp;lt;floor&amp;gt;3&amp;lt;/floor&amp;gt;
        &amp;lt;buildYear&amp;gt;1997&amp;lt;/buildYear&amp;gt;
        &amp;lt;umdNm&amp;gt;영통동&amp;lt;/umdNm&amp;gt;
      &amp;lt;/item&amp;gt;
      ...
    &amp;lt;/items&amp;gt;
    &amp;lt;totalCount&amp;gt;325&amp;lt;/totalCount&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/response&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-end=&quot;1604&quot; data-start=&quot;1592&quot; data-ke-size=&quot;size26&quot;&gt;데이터 해석&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1802&quot; data-start=&quot;1605&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1630&quot; data-start=&quot;1605&quot;&gt;aptNm: 단지명 (신나무실신안)&lt;/li&gt;
&lt;li data-end=&quot;1673&quot; data-start=&quot;1631&quot;&gt;dealAmount: 거래금액 (36,000 &amp;rarr; 3억 6천만 원)&lt;/li&gt;
&lt;li data-end=&quot;1718&quot; data-start=&quot;1674&quot;&gt;dealYear, dealMonth, dealDay: 계약일자&lt;/li&gt;
&lt;li data-end=&quot;1745&quot; data-start=&quot;1719&quot;&gt;excluUseAr: 전용면적(m&amp;sup2;)&lt;/li&gt;
&lt;li data-end=&quot;1760&quot; data-start=&quot;1746&quot;&gt;floor: 층&lt;/li&gt;
&lt;li data-end=&quot;1782&quot; data-start=&quot;1761&quot;&gt;buildYear: 건축년도&lt;/li&gt;
&lt;li data-end=&quot;1802&quot; data-start=&quot;1783&quot;&gt;umdNm: 행정동 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;1878&quot; data-start=&quot;1863&quot; data-ke-size=&quot;size26&quot;&gt;내가 겪은 깨달음&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2124&quot; data-start=&quot;1879&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1951&quot; data-start=&quot;1879&quot;&gt;문서만 보면 쉬워 보이지만, 실제 호출 시 &lt;b&gt;serviceKey 인코딩 문제&lt;/b&gt;나 &lt;b&gt;XML 파싱 문제&lt;/b&gt;가 숨어있었다.&lt;/li&gt;
&lt;li data-end=&quot;2010&quot; data-start=&quot;1952&quot;&gt;특히 응답이 XML인데, 브라우저나 axios에서 바로 JSON으로 착각할 때 파싱 에러가 났다.&lt;/li&gt;
&lt;li data-end=&quot;2124&quot; data-start=&quot;2011&quot;&gt;정리하자면:
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2124&quot; data-start=&quot;2022&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2054&quot; data-start=&quot;2022&quot;&gt;공공데이터 API는 &lt;b&gt;요청 파라미터가 정확해야 함&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2086&quot; data-start=&quot;2057&quot;&gt;응답 형식(XML/JSON)을 반드시 확인해야 함&lt;/li&gt;
&lt;li data-end=&quot;2124&quot; data-start=&quot;2089&quot;&gt;serviceKey는 이중 인코딩 문제를 주의해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/109</guid>
      <comments>https://tuesday04.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 25 Sep 2025 18:38:57 +0900</pubDate>
    </item>
    <item>
      <title>  CORS? 403? API 호출 대참사 해결기</title>
      <link>https://tuesday04.tistory.com/108</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;blockquote data-end=&quot;235&quot; data-start=&quot;203&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;235&quot; data-start=&quot;205&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;로컬에서는 되는데 EC2에서는 왜 안 되지?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;240&quot; data-start=&quot;237&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;253&quot; data-start=&quot;242&quot; data-ke-size=&quot;size26&quot;&gt;  사건 개요&lt;/h2&gt;
&lt;p data-end=&quot;342&quot; data-start=&quot;255&quot; data-ke-size=&quot;size16&quot;&gt;클라우드에 React + Spring Boot 서비스를 배포하고 나서,&lt;br /&gt;&lt;b&gt;프론트에서 API를 호출하자마자 403 Forbidden 오류&lt;/b&gt;가 발생했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;425&quot; data-start=&quot;344&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;362&quot; data-start=&quot;344&quot;&gt;EC2 보안 그룹? 열려 있음&lt;/li&gt;
&lt;li data-end=&quot;384&quot; data-start=&quot;363&quot;&gt;백엔드 포트? 정상적으로 열려 있음&lt;/li&gt;
&lt;li data-end=&quot;425&quot; data-start=&quot;385&quot;&gt;API 주소? 맞게 입력함&lt;br /&gt;&lt;b&gt;그런데도 계속 에러가 난다...&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;430&quot; data-start=&quot;427&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;452&quot; data-start=&quot;432&quot; data-ke-size=&quot;size26&quot;&gt;❓ 왜 이런 문제가 발생했을까?&lt;/h2&gt;
&lt;h3 data-end=&quot;467&quot; data-start=&quot;454&quot; data-ke-size=&quot;size23&quot;&gt;  CORS란?&lt;/h3&gt;
&lt;blockquote data-end=&quot;573&quot; data-start=&quot;469&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;573&quot; data-start=&quot;471&quot; data-ke-size=&quot;size16&quot;&gt;CORS(Cross-Origin Resource Sharing)는 &lt;b&gt;브라우저 보안 정책&lt;/b&gt; 중 하나로,&lt;br /&gt;&lt;b&gt;출처(origin)가 다른 도메인 간 요청을 제한&lt;/b&gt;하는 기능이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;703&quot; data-start=&quot;575&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;627&quot; data-start=&quot;575&quot;&gt;로컬에서는 localhost:3000 &amp;rarr; localhost:8080으로 요청하니 괜찮음&lt;/li&gt;
&lt;li data-end=&quot;703&quot; data-start=&quot;628&quot;&gt;EC2에 배포 후에는 브라우저: http://&amp;lt;IP&amp;gt; &amp;rarr; API: http://&amp;lt;IP&amp;gt;:8081로 &lt;b&gt;다른 포트 간 통신&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;746&quot; data-start=&quot;705&quot; data-ke-size=&quot;size16&quot;&gt;  브라우저는 이를 &lt;b&gt;보안 위반&lt;/b&gt;으로 판단 &amp;rarr; 요청 자체를 막아버린다&lt;/p&gt;
&lt;hr data-end=&quot;751&quot; data-start=&quot;748&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;767&quot; data-start=&quot;753&quot; data-ke-size=&quot;size26&quot;&gt;  에러 상황 정리&lt;/h2&gt;
&lt;pre id=&quot;code_1757580997890&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 프론트에서 fetch 요청
fetch('http://&amp;lt;EC2-IP&amp;gt;:8081/api/rewards/summary?userId=u-123')
  .then(res =&amp;gt; res.json())
  .then(data =&amp;gt; console.log(data));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❌ 콘솔 에러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757581017677&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Access to fetch at 'http://&amp;lt;EC2-IP&amp;gt;:8081/...' from origin 'http://&amp;lt;EC2-IP&amp;gt;' 
has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1204&quot; data-start=&quot;1179&quot; data-ke-size=&quot;size26&quot;&gt;  해결 방법 (Spring Boot)&lt;/h2&gt;
&lt;h3 data-end=&quot;1243&quot; data-start=&quot;1206&quot; data-ke-size=&quot;size23&quot;&gt;✅ WebMvcConfigurer를 통한 CORS 설정 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1757581031231&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;) // 전체 API 경로 허용
                .allowedOrigins(&quot;*&quot;) // 모든 출처 허용 (*은 개발 환경에서만!)
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;) // 허용 메서드
                .allowedHeaders(&quot;*&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;1743&quot; data-start=&quot;1633&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1643&quot; data-start=&quot;1635&quot; data-ke-size=&quot;size16&quot;&gt;  주의:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1743&quot; data-start=&quot;1646&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1683&quot; data-start=&quot;1646&quot;&gt;allowedOrigins(&quot;*&quot;)는 개발환경에서만 사용&lt;/li&gt;
&lt;li data-end=&quot;1743&quot; data-start=&quot;1686&quot;&gt;운영 환경에서는 &lt;a href=&quot;http://yourdomain.com&quot;&gt;http://yourdomain.com&lt;/a&gt;과 같이 &lt;b&gt;정확한 도메인 지정&lt;/b&gt;이 필수&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;1748&quot; data-start=&quot;1745&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1770&quot; data-start=&quot;1750&quot; data-ke-size=&quot;size26&quot;&gt;  디버깅 중 알게 된 사실들&lt;/h2&gt;
&lt;h3 data-end=&quot;1791&quot; data-start=&quot;1772&quot; data-ke-size=&quot;size23&quot;&gt;  프론트 요청 흐름 정리&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1912&quot; data-start=&quot;1793&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1832&quot; data-start=&quot;1793&quot;&gt;&lt;b&gt;브라우저가 Preflight 요청(OPTIONS) 전송&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1871&quot; data-start=&quot;1833&quot;&gt;백엔드가 CORS 설정이 없으면 &amp;rarr; 403 Forbidden&lt;/li&gt;
&lt;li data-end=&quot;1912&quot; data-start=&quot;1872&quot;&gt;프론트는 본 요청(GET, POST)을 보내지 못하고 실패함&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-end=&quot;1917&quot; data-start=&quot;1914&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1932&quot; data-start=&quot;1919&quot; data-ke-size=&quot;size26&quot;&gt;✅ 해결 이후 결과&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2016&quot; data-start=&quot;1934&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1953&quot; data-start=&quot;1934&quot;&gt;API 요청 정상 응답  &lt;/li&gt;
&lt;li data-end=&quot;1984&quot; data-start=&quot;1954&quot;&gt;실제로 브라우저 콘솔에서 CORS 관련 에러 사라짐&lt;/li&gt;
&lt;li data-end=&quot;2016&quot; data-start=&quot;1985&quot;&gt;프론트에서 리워드 정보 조회 및 리더보드 정상 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2021&quot; data-start=&quot;2018&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2033&quot; data-start=&quot;2023&quot; data-ke-size=&quot;size26&quot;&gt;  배운 점&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2207&quot; data-start=&quot;2035&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2207&quot; data-start=&quot;2071&quot;&gt;
&lt;tr data-end=&quot;2117&quot; data-start=&quot;2071&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2078&quot; data-start=&quot;2071&quot;&gt;CORS&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2117&quot; data-start=&quot;2078&quot;&gt;단순 서버 문제 아님. &lt;b&gt;브라우저 보안 정책에 의한 차단&lt;/b&gt;임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2162&quot; data-start=&quot;2118&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2130&quot; data-start=&quot;2118&quot;&gt;Spring 설정&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2162&quot; data-start=&quot;2130&quot;&gt;WebMvcConfigurer를 통해 제어 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2207&quot; data-start=&quot;2163&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2174&quot; data-start=&quot;2163&quot;&gt;로컬 vs 운영&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2207&quot; data-start=&quot;2174&quot;&gt;로컬에서는 안 생기던 문제가 배포 후 발생할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;blockquote data-end=&quot;2298&quot; data-start=&quot;2209&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;2298&quot; data-start=&quot;2211&quot; data-ke-size=&quot;size16&quot;&gt;  결국 &lt;b&gt;로컬 개발 환경과 실제 브라우저 환경은 다르다&lt;/b&gt;는 걸 절실히 느꼈다.&lt;br /&gt;배포 이후 브라우저에서 실제 요청 흐름을 추적하는 게 중요하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/108</guid>
      <comments>https://tuesday04.tistory.com/108#entry108comment</comments>
      <pubDate>Thu, 11 Sep 2025 17:58:23 +0900</pubDate>
    </item>
    <item>
      <title>AWS EC2 + Docker로 React &amp;amp; Spring Boot 실전 배포기</title>
      <link>https://tuesday04.tistory.com/107</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;ldquo;진짜 유저가 접근 가능한 배포, 나도 해봤다&amp;rdquo;&lt;/p&gt;
&lt;blockquote data-end=&quot;542&quot; data-start=&quot;308&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;542&quot; data-start=&quot;310&quot; data-ke-size=&quot;size16&quot;&gt;개발자 포트폴리오에서 코드 구현만큼 중요한 건, &lt;b&gt;서비스를 실제 운영 환경에 띄울 수 있는 배포 경험&lt;/b&gt;이다.&lt;br /&gt;그래서 이번에는 &amp;ldquo;&lt;b&gt;로컬에서만 되는 게 아니라, 진짜 유저가 접근 가능한 서비스&lt;/b&gt;&amp;rdquo;를 만드는 걸 목표로 EC2에 프론트와 백엔드를 배포했다.&lt;br /&gt;&lt;b&gt;보안 설정, 포트 바인딩, CORS 이슈 대응&lt;/b&gt; 등도 직접 다루며 실제 운영에서 고려해야 할 요소들을 경험할 수 있었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-end=&quot;295&quot; data-start=&quot;263&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;295&quot; data-start=&quot;263&quot; data-ke-size=&quot;size26&quot;&gt;✨ 프로젝트 개요: Share &amp;amp; Reward 시스템&lt;/h2&gt;
&lt;p data-end=&quot;416&quot; data-start=&quot;297&quot; data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 상품 링크를 공유하고, 친구가 클릭하면 포인트를 받는 &lt;b&gt;리워드 시스템&lt;/b&gt;을 구현한 것이다.&lt;br /&gt;실제 서비스를 운영하는 것처럼 &lt;b&gt;EC2 서버에 배포&lt;/b&gt;하고, 브라우저에서 접근 가능한 형태로 구성해봤다.&lt;/p&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;603&quot; data-start=&quot;418&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;603&quot; data-start=&quot;446&quot;&gt;
&lt;tr data-end=&quot;472&quot; data-start=&quot;446&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;454&quot; data-start=&quot;446&quot;&gt;프로젝트명&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;472&quot; data-start=&quot;454&quot;&gt;Share &amp;amp; Reward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;517&quot; data-start=&quot;473&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;481&quot; data-start=&quot;473&quot;&gt;기술 스택&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;517&quot; data-start=&quot;481&quot;&gt;React (Vite), Spring Boot, Kafka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;560&quot; data-start=&quot;518&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;526&quot; data-start=&quot;518&quot;&gt;배포 대상&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;560&quot; data-start=&quot;526&quot;&gt;프론트 (80 포트), 백엔드 API (8081 포트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;603&quot; data-start=&quot;561&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;569&quot; data-start=&quot;561&quot;&gt;구현 기능&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;603&quot; data-start=&quot;569&quot;&gt;공유 링크 생성, 클릭 추적, 리워드 지급 이벤트 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-end=&quot;627&quot; data-start=&quot;610&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;627&quot; data-start=&quot;610&quot; data-ke-size=&quot;size26&quot;&gt;  기술 스택 선택 이유&lt;/h2&gt;
&lt;h3 data-end=&quot;650&quot; data-start=&quot;629&quot; data-ke-size=&quot;size23&quot;&gt;▶ 왜 AWS EC2를 썼을까?&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;항목이유
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;826&quot; data-start=&quot;652&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;826&quot; data-start=&quot;680&quot;&gt;
&lt;tr data-end=&quot;730&quot; data-start=&quot;680&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;695&quot; data-start=&quot;680&quot;&gt;✅ &lt;b&gt;저렴한 비용&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;730&quot; data-start=&quot;695&quot;&gt;프리 티어 (t2.micro)로 테스트용 서버 운영 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;771&quot; data-start=&quot;731&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;749&quot; data-start=&quot;731&quot;&gt;✅ &lt;b&gt;퍼블릭 IP 제공&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;771&quot; data-start=&quot;749&quot;&gt;도메인 없이도 외부에서 접근 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;826&quot; data-start=&quot;772&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;789&quot; data-start=&quot;772&quot;&gt;✅ &lt;b&gt;직접 제어 가능&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;826&quot; data-start=&quot;789&quot;&gt;인바운드 포트, 디렉토리 구조, Docker 등 자유도 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;871&quot; data-start=&quot;828&quot; data-ke-size=&quot;size16&quot;&gt;➡️ 초보 개발자도 실제 클라우드 환경에서 배포 경험을 쌓기에 아주 적합하다.&lt;/p&gt;
&lt;hr data-end=&quot;876&quot; data-start=&quot;873&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;900&quot; data-start=&quot;878&quot; data-ke-size=&quot;size23&quot;&gt;▶ 왜 Docker로 배포했을까?&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;항목이유
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;1068&quot; data-start=&quot;902&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;1068&quot; data-start=&quot;930&quot;&gt;
&lt;tr data-end=&quot;973&quot; data-start=&quot;930&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;945&quot; data-start=&quot;930&quot;&gt;✅ &lt;b&gt;환경 일관성&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;973&quot; data-start=&quot;945&quot;&gt;로컬에서 만든 이미지를 그대로 서버에서 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1012&quot; data-start=&quot;974&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;990&quot; data-start=&quot;974&quot;&gt;✅ &lt;b&gt;프로세스 격리&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1012&quot; data-start=&quot;990&quot;&gt;프론트/백엔드 컨테이너 분리 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1068&quot; data-start=&quot;1013&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1029&quot; data-start=&quot;1013&quot;&gt;✅ &lt;b&gt;간편한 재배포&lt;/b&gt;&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1068&quot; data-start=&quot;1029&quot;&gt;docker pull + docker run으로 배포 끝&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1111&quot; data-start=&quot;1070&quot; data-ke-size=&quot;size16&quot;&gt;➡️ CI/CD가 없어도 운영 가능한 실전형 수동 배포를 경험할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;✈️ EC2 인스턴스 준비하기&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. EC2 생성&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Ubuntu 20.04 이상 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;인스턴스 유형: &lt;/span&gt;&lt;span&gt;t2.micro&lt;/span&gt;&lt;span&gt; (프리티어)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;보안 그룹 설정:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;22 (SSH): 본인 IP만 허용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;80 (HTTP): 전체 허용&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;8081 (API 테스트용): 전체 허용 또는 특정 IP&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2. EC2 접속 (Windows 기준)&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757579158996&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh -i &quot;C:/Users/yourname/your-key.pem&quot; ubuntu@&amp;lt;퍼블릭-IP&amp;gt;&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;h2 data-ke-size=&quot;size26&quot;&gt;  Docker로 서비스 이미지 빌드 &amp;amp; 푸시&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. Docker 설치 (EC2 내부)&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757579183079&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt update
sudo apt install docker.io -y
sudo usermod -aG docker ubuntu&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후 exit &amp;rarr; 다시 접속해야 docker 권한 적용됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker Hub 로그인&lt;/h2&gt;
&lt;pre id=&quot;code_1757580692414&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker login&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;⚖️ 이미지 빌드 &amp;amp; 푸시&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백엔드 빌드 &amp;amp; 푸시&lt;/h3&gt;
&lt;pre id=&quot;code_1757579448771&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 루트에 Dockerfile 있어야 함
cd 개발 폴더명
./gradlew build -x test

docker build -t &amp;lt;계정명&amp;gt;/share-back:latest .
docker push &amp;lt;계정명&amp;gt;/share-back:latest&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3. 프론트 빌드 &amp;amp; 푸시&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757579497155&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd 폴더명
npm run build

docker build -t &amp;lt;계정명&amp;gt;/share-front:latest .
docker push &amp;lt;계정명&amp;gt;/share-front:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1928&quot; data-start=&quot;1903&quot; data-ke-size=&quot;size26&quot;&gt;☁️ EC2에서 Docker 이미지 실행&lt;/h2&gt;
&lt;h3 data-end=&quot;1944&quot; data-start=&quot;1930&quot; data-ke-size=&quot;size23&quot;&gt;백엔드 (8081)&lt;/h3&gt;
&lt;pre id=&quot;code_1757579545963&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker pull &amp;lt;계정명&amp;gt;/share-back:latest
docker run -d --name share-back -p 8081:8081 &amp;lt;계정명&amp;gt;/share-back:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;프론트 실행 (nginx 내장)&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757579608067&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker pull &amp;lt;계정명&amp;gt;/share-front:latest
docker run -d --name share-front -p 80:80 &amp;lt;계정명&amp;gt;/share-front: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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2224&quot; data-start=&quot;2200&quot; data-ke-size=&quot;size26&quot;&gt;  문제 해결: CORS 403 오류&lt;/h2&gt;
&lt;h3 data-end=&quot;2235&quot; data-start=&quot;2226&quot; data-ke-size=&quot;size23&quot;&gt;  문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2280&quot; data-start=&quot;2236&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2280&quot; data-start=&quot;2236&quot;&gt;프론트에서 EC2의 백엔드 API 호출 시 403 Forbidden 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2291&quot; data-start=&quot;2282&quot; data-ke-size=&quot;size23&quot;&gt;  원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2335&quot; data-start=&quot;2292&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2335&quot; data-start=&quot;2292&quot;&gt;Spring Boot에서 별도의 CORS 설정이 없으면 브라우저 요청 차단&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2348&quot; data-start=&quot;2337&quot; data-ke-size=&quot;size23&quot;&gt;✅ 해결 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1757579635492&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOrigins(&quot;*&quot;)
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;DELETE&quot;)
                .allowedHeaders(&quot;*&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-pm-slice=&quot;1 1 [&amp;quot;blockquote&amp;quot;,{&amp;quot;start&amp;quot;:2459,&amp;quot;end&amp;quot;:2511}]&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 [&amp;quot;blockquote&amp;quot;,{&amp;quot;start&amp;quot;:2459,&amp;quot;end&amp;quot;:2511}]&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;배포용 환경에서는 &lt;/span&gt;&lt;span&gt;allowedOrigins(&quot;*&quot;)&lt;/span&gt;&lt;span&gt; 대신 도메인 또는 IP 제한 필요&lt;/span&gt;&lt;/p&gt;
&lt;p data-pm-slice=&quot;1 1 [&amp;quot;blockquote&amp;quot;,{&amp;quot;start&amp;quot;:2459,&amp;quot;end&amp;quot;:2511}]&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2727&quot; data-start=&quot;2710&quot; data-ke-size=&quot;size26&quot;&gt;  테스트 및 결과 확인&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2835&quot; data-start=&quot;2729&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2801&quot; data-start=&quot;2729&quot;&gt;백엔드 API 확인&lt;br /&gt;http://&amp;lt;EC2-IP&amp;gt;:8081/api/rewards/summary?userId=u-123&lt;/li&gt;
&lt;li data-end=&quot;2835&quot; data-start=&quot;2803&quot;&gt;프론트 웹 확인&lt;br /&gt;http://&amp;lt;EC2-IP&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h2 data-end=&quot;2852&quot; data-start=&quot;2842&quot; data-ke-size=&quot;size26&quot;&gt;  배운 점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3064&quot; data-start=&quot;2854&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2922&quot; data-start=&quot;2854&quot;&gt;배포는 &quot;서버에 띄우는 것&quot; 이상으로, &lt;b&gt;네트워크, 보안, 설정, 디버깅 등 실무 감각&lt;/b&gt;이 필요한 작업이다.&lt;/li&gt;
&lt;li data-end=&quot;2992&quot; data-start=&quot;2923&quot;&gt;직접 퍼블릭 클라우드에 Docker 컨테이너를 띄우며, &lt;b&gt;운영 환경과 개발 환경의 차이&lt;/b&gt;를 몸으로 체감할 수 있었다.&lt;/li&gt;
&lt;li data-end=&quot;3064&quot; data-start=&quot;2993&quot;&gt;&lt;b&gt;&amp;ldquo;왜 이 기술을 선택했는가?&amp;rdquo; &amp;ldquo;무엇이 문제였고 어떻게 해결했는가?&amp;rdquo;&lt;/b&gt; 를 고민하는 과정에서 개발자로서 한층 성장했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  아키텍처 구성도&lt;/h2&gt;
&lt;pre id=&quot;code_1757580849934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; [  사용자]
     │
     ▼
 [  브라우저]
     │
     ▼
[  EC2: 프론트 (80포트, Vite + Nginx)]
     │
     ▼
[  EC2: 백엔드 API (8081포트, Spring Boot)]
     │
     ▼
[  Kafka: 공유/보상 이벤트 전송 및 처리]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AWS</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/107</guid>
      <comments>https://tuesday04.tistory.com/107#entry107comment</comments>
      <pubDate>Thu, 11 Sep 2025 17:37:12 +0900</pubDate>
    </item>
    <item>
      <title>  Spring Boot Kafka 개발 중 &amp;ldquo;No qualifying bean&amp;rdquo; 에러 해결기</title>
      <link>https://tuesday04.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 이벤트 발행/소비를 구현하면서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ShareController 에서 ShareEventProducer 를 주입받아 /shares 요청을 처리하도록 만들었다.&lt;br /&gt;하지만 실행 직후 아래와 같은 에러로 애플리케이션이 뜨지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;348&quot; data-start=&quot;338&quot; data-ke-size=&quot;size16&quot;&gt;로그에 찍힌 에러:&lt;/p&gt;
&lt;pre id=&quot;code_1757224150104&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Parameter 0 of constructor in com.kafka.demo.ShareController 
required a bean of type 'com.kafka.demo.ShareEventProducer' that could not be found.&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;h4 data-end=&quot;2310&quot; data-start=&quot;2300&quot; data-ke-size=&quot;size20&quot;&gt;원인&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2601&quot; data-start=&quot;2311&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2378&quot; data-start=&quot;2311&quot;&gt;ShareController 는 com.kafka.demo.ShareEventProducer 타입을 의존.&lt;/li&gt;
&lt;li data-end=&quot;2441&quot; data-start=&quot;2379&quot;&gt;실제 클래스는 com.kafka.demo.producer.ShareEventProducer 에 존재.&lt;/li&gt;
&lt;li data-end=&quot;2484&quot; data-start=&quot;2442&quot;&gt;&lt;b&gt;패키지 불일치로 인해 스프링이 해당 빈을 찾을 수 없었던 것&lt;/b&gt;.&lt;/li&gt;
&lt;li data-end=&quot;2561&quot; data-start=&quot;2485&quot;&gt;스프링의 컴포넌트 스캔은 @SpringBootApplication 이 선언된 패키지의 &lt;b&gt;하위 패키지&lt;/b&gt;만 대상으로 동작한다.&lt;/li&gt;
&lt;li data-end=&quot;2601&quot; data-start=&quot;2562&quot;&gt;따라서 import 경로가 정확히 맞지 않으면 빈 주입에 실패한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;2615&quot; data-start=&quot;2603&quot; data-ke-size=&quot;size20&quot;&gt;해결방법&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2917&quot; data-start=&quot;2616&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2726&quot; data-start=&quot;2616&quot;&gt;ShareController 에서 올바른 import 적용:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1757224361068&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class ShareEventProducer { ... }&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;2. ShareEventProducer 클래스에 @Service 추가:&lt;/p&gt;
&lt;pre id=&quot;code_1757224335342&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.kafka.demo.producer.ShareEventProducer;&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;3. 메인 클래스(DemoApplication)는 com.kafka.demo 최상위에 위치해 서브 패키지를 스캔하도록 유지.&lt;/p&gt;
&lt;p data-end=&quot;3003&quot; data-start=&quot;2919&quot; data-ke-size=&quot;size16&quot;&gt;빌드 후 다시 실행하니 정상적으로 서버가 기동되었고, /shares 요청이 Kafka share.created.v1 토픽으로 메시지를 발행했다.&lt;/p&gt;
&lt;h4 data-end=&quot;3020&quot; data-start=&quot;3005&quot; data-ke-size=&quot;size20&quot;&gt;원칙 &amp;amp; 교훈&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3236&quot; data-start=&quot;3021&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3095&quot; data-start=&quot;3021&quot;&gt;&lt;b&gt;FQCN(Fully Qualified Class Name)&lt;/b&gt; 은 패키지 + 클래스명 조합으로 스프링 빈을 찾는 기준이다.&lt;/li&gt;
&lt;li data-end=&quot;3158&quot; data-start=&quot;3096&quot;&gt;컨트롤러 &amp;rarr; 서비스 &amp;rarr; 리포지토리 구조를 만들 때는 &lt;b&gt;패키지/임포트 경로를 일관되게 관리&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li data-end=&quot;3236&quot; data-start=&quot;3159&quot;&gt;스프링의 컴포넌트 스캔은 @SpringBootApplication 의 패키지를 기준으로 하위 패키지까지 탐색한다는 점을 잊지 말자.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757224450793&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;Classpath Scanning and Managed Components :: Spring Framework&quot; data-og-description=&quot;By default, classes annotated with @Component, @Repository, @Service, @Controller, @Configuration, or a custom annotation that itself is annotated with @Component are the only detected candidate components. However, you can modify and extend this behavior &quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html&quot; data-og-url=&quot;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.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;Classpath Scanning and Managed Components :: Spring Framework&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;By default, classes annotated with @Component, @Repository, @Service, @Controller, @Configuration, or a custom annotation that itself is annotated with @Component are the only detected candidate components. However, you can modify and extend this behavior&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;</description>
      <category>개발로 사고하기</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/106</guid>
      <comments>https://tuesday04.tistory.com/106#entry106comment</comments>
      <pubDate>Sun, 7 Sep 2025 14:54:39 +0900</pubDate>
    </item>
    <item>
      <title>카프카(Kafka) 로컬 환경 구축과 기본 개념 정리</title>
      <link>https://tuesday04.tistory.com/105</link>
      <description>&lt;h2 data-end=&quot;827&quot; data-start=&quot;807&quot; data-ke-size=&quot;size26&quot;&gt;1. Kafka 기본 구성 요소&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8QCe3/btsQlXNbxIA/f5tQyBY6MKOZXM5XFnuTl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8QCe3/btsQlXNbxIA/f5tQyBY6MKOZXM5XFnuTl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8QCe3/btsQlXNbxIA/f5tQyBY6MKOZXM5XFnuTl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8QCe3%2FbtsQlXNbxIA%2Ff5tQyBY6MKOZXM5XFnuTl0%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;481&quot; height=&quot;720&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-end=&quot;844&quot; data-start=&quot;829&quot; data-ke-size=&quot;size23&quot;&gt;  Producer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;925&quot; data-start=&quot;845&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;881&quot; data-start=&quot;845&quot;&gt;메시지를 생성해서 Kafka 브로커(클러스터)에 전송하는 주체&lt;/li&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;882&quot;&gt;예: 회원가입 이벤트 발생 시 user.signed-up 메시지를 발행&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;940&quot; data-start=&quot;927&quot; data-ke-size=&quot;size23&quot;&gt;  Broker&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1045&quot; data-start=&quot;941&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;954&quot; data-start=&quot;941&quot;&gt;Kafka 서버 자체&lt;/li&gt;
&lt;li data-end=&quot;1013&quot; data-start=&quot;955&quot;&gt;Producer가 보낸 메시지를 받아서 &lt;b&gt;토픽(Topic)&lt;/b&gt; 에 저장하고 Consumer에게 전달&lt;/li&gt;
&lt;li data-end=&quot;1045&quot; data-start=&quot;1014&quot;&gt;여러 개의 Broker가 모여 &lt;b&gt;클러스터&lt;/b&gt;를 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1059&quot; data-start=&quot;1047&quot; data-ke-size=&quot;size23&quot;&gt;  Topic&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1181&quot; data-start=&quot;1060&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1080&quot; data-start=&quot;1060&quot;&gt;메시지가 저장되는 &lt;b&gt;카테고리&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1135&quot; data-start=&quot;1081&quot;&gt;예: demo.topic, user.signed-up, order.completed&lt;/li&gt;
&lt;li data-end=&quot;1181&quot; data-start=&quot;1136&quot;&gt;토픽은 여러 개의 &lt;b&gt;파티션(Partition)&lt;/b&gt; 으로 나눠져서 분산 저장됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1208&quot; data-start=&quot;1183&quot; data-ke-size=&quot;size23&quot;&gt;  Partition &amp;amp; Offset&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1289&quot; data-start=&quot;1209&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1254&quot; data-start=&quot;1209&quot;&gt;&lt;b&gt;Partition&lt;/b&gt;: 토픽을 여러 조각으로 나눈 단위 &amp;rarr; 병렬 처리 가능&lt;/li&gt;
&lt;li data-end=&quot;1289&quot; data-start=&quot;1255&quot;&gt;&lt;b&gt;Offset&lt;/b&gt;: 각 파티션에서 메시지가 기록되는 순번&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1306&quot; data-start=&quot;1291&quot; data-ke-size=&quot;size23&quot;&gt;  Consumer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1385&quot; data-start=&quot;1307&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1337&quot; data-start=&quot;1307&quot;&gt;Kafka 브로커에서 메시지를 가져와 처리하는 주체&lt;/li&gt;
&lt;li data-end=&quot;1385&quot; data-start=&quot;1338&quot;&gt;예: 이메일 서비스가 user.signed-up 메시지를 읽고 환영 메일 발송&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1408&quot; data-start=&quot;1387&quot; data-ke-size=&quot;size23&quot;&gt;  Consumer Group&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1487&quot; data-start=&quot;1409&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1443&quot; data-start=&quot;1409&quot;&gt;여러 Consumer가 협력해서 토픽을 분산 처리하는 구조&lt;/li&gt;
&lt;li data-end=&quot;1487&quot; data-start=&quot;1444&quot;&gt;같은 그룹 안에서는 한 파티션을 동시에 두 Consumer가 처리하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(출처: Apache Kafka 공식 문서)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/#introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kafka.apache.org/documentation/#introduction&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757076606943&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;Apache Kafka&quot; data-og-description=&quot;Apache Kafka: A Distributed Streaming Platform.&quot; data-og-host=&quot;kafka.apache.org&quot; data-og-source-url=&quot;https://kafka.apache.org/documentation/#introduction&quot; data-og-url=&quot;https://kafka.apache.org/documentation/#introduction&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UEufS/hyZGlDTsCa/t9g1g291GhOdzJsuzaXrw1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/#introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kafka.apache.org/documentation/#introduction&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UEufS/hyZGlDTsCa/t9g1g291GhOdzJsuzaXrw1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&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;Apache Kafka&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka: A Distributed Streaming Platform.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;kafka.apache.org&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1618&quot; data-start=&quot;1615&quot; data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1767&quot; data-start=&quot;1619&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1649&quot; data-start=&quot;1619&quot;&gt;Producer가 메시지를 특정 Topic에 전송&lt;/li&gt;
&lt;li data-end=&quot;1686&quot; data-start=&quot;1650&quot;&gt;Broker는 메시지를 받아서 Partition 단위로 저장&lt;/li&gt;
&lt;li data-end=&quot;1726&quot; data-start=&quot;1687&quot;&gt;Consumer Group이 Topic을 구독하고 메시지를 가져옴&lt;/li&gt;
&lt;li data-end=&quot;1767&quot; data-start=&quot;1727&quot;&gt;Consumer는 메시지를 처리 (예: DB 저장, 알림 발송 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-end=&quot;1805&quot; data-start=&quot;1774&quot; data-ke-size=&quot;size26&quot;&gt;2. 로컬 환경 구성 (Docker Compose)&lt;/h2&gt;
&lt;h3 data-end=&quot;1832&quot; data-start=&quot;1807&quot; data-ke-size=&quot;size23&quot;&gt;  docker-compose.yml&lt;/h3&gt;
&lt;pre id=&quot;code_1757076626397&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  kafka:
    image: bitnami/kafka:latest
    ports:
      - &quot;29092:29092&quot;
    environment:
      - KAFKA_CFG_NODE_ID=0
      - KAFKA_CFG_PROCESS_ROLES=broker,controller
      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:29092,CONTROLLER://:9093
      - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093
      - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true
      - KAFKA_ENABLE_KRAFT=yes

  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    ports:
      - &quot;8080:8080&quot;
    environment:
      - KAFKA_CLUSTERS_0_NAME=local
      - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
    depends_on:
      - kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;2750&quot; data-start=&quot;2727&quot; data-ke-size=&quot;size26&quot;&gt;3. 토픽 생성 &amp;amp; 메시지 전송 실습&lt;/h2&gt;
&lt;p data-end=&quot;2764&quot; data-start=&quot;2752&quot; data-ke-size=&quot;size16&quot;&gt;1) 토픽 생성&lt;/p&gt;
&lt;p data-end=&quot;2817&quot; data-start=&quot;2765&quot; data-ke-size=&quot;size16&quot;&gt;Kafka UI &amp;rarr; Topics &amp;rarr; Create Topic &amp;rarr; demo.topic 생성&lt;/p&gt;
&lt;p data-end=&quot;2817&quot; data-start=&quot;2765&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2844&quot; data-start=&quot;2819&quot; data-ke-size=&quot;size16&quot;&gt;2) 메시지 보내기 (Producer)&lt;/p&gt;
&lt;pre id=&quot;code_1757076645036&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose exec -it kafka /opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka:9092 --topic demo.topic&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1757076654032&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;hello
kafka
event-driven&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;3) 메시지 받기 (Consumer)&lt;/p&gt;
&lt;pre id=&quot;code_1757076674013&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker compose exec -it kafka /opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic demo.topic --from-beginning&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3263&quot; data-start=&quot;3248&quot; data-ke-size=&quot;size26&quot;&gt;4. 정리 및 느낀 점&lt;/h2&gt;
&lt;p data-end=&quot;3274&quot; data-start=&quot;3264&quot; data-ke-size=&quot;size16&quot;&gt;이번 실습을 통해:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3383&quot; data-start=&quot;3275&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3339&quot; data-start=&quot;3275&quot;&gt;&lt;b&gt;Kafka의 기본 원리와 요소&lt;/b&gt; (Producer, Broker, Topic, Consumer)를 이해하고&lt;/li&gt;
&lt;li data-end=&quot;3383&quot; data-start=&quot;3340&quot;&gt;&lt;b&gt;로컬 환경에서 메시지를 발행하고 소비&lt;/b&gt;하는 과정을 직접 경험했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3458&quot; data-start=&quot;3385&quot; data-ke-size=&quot;size16&quot;&gt;이 단계만으로도 &amp;ldquo;내 서비스가 이벤트를 Kafka로 흘려보내고, 다른 서비스가 이를 받아 처리한다&amp;rdquo;는 그림이 머리에 그려집니다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <author>tues</author>
      <guid isPermaLink="true">https://tuesday04.tistory.com/105</guid>
      <comments>https://tuesday04.tistory.com/105#entry105comment</comments>
      <pubDate>Fri, 5 Sep 2025 21:57:20 +0900</pubDate>
    </item>
  </channel>
</rss>