<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>하마의 개발자 성장일기</title>
    <link>https://stark77.tistory.com/</link>
    <description>하마의 IT 자기개발 이모저모, 백엔드 개발자로 거듭나기</description>
    <language>ko</language>
    <pubDate>Fri, 31 Jul 2026 22:16:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>stark77</managingEditor>
    <image>
      <title>하마의 개발자 성장일기</title>
      <url>https://tistory1.daumcdn.net/tistory/8137173/attach/9a94a53b271f4ef1a7b944f1395dd778</url>
      <link>https://stark77.tistory.com</link>
    </image>
    <item>
      <title>[실시간 대용량 서비스] 모니터링의 다음 단계: 로그 기반 관측 설계</title>
      <link>https://stark77.tistory.com/115</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus와 Grafana를 통해 요청 수, 에러율, 응답 시간, JVM 상태 등 주요 지표를 실시간으로 확인할 수 있게 되었다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 API에서 에러를 발생 시켰는가?&lt;/li&gt;
&lt;li&gt;특정 사용자의 요청 흐름은 문제가 없는가?&lt;/li&gt;
&lt;li&gt;처리 시간이 비정상적으로 길었던 요청은 무엇이 있는가?&lt;/li&gt;
&lt;li&gt;동일한 예외가 언제부터 반복되고 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metrics는 무엇이 비정상적인가를 알려주지만, 왜 그런일이 발생했는가 까지의 설명은 해주지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지점에서 로그 기반 관측 환경 즉, ELK의 도입 필요성을 느끼게 되었다.&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;b&gt;✓ Metrics와 Logs의 역할 구분&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Observability는 일반적으로 다음 세 가지 요소로 구성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Metrics : 수치 기반 상태 관측&lt;/li&gt;
&lt;li&gt;Logs : 사건 기반 기록&lt;/li&gt;
&lt;li&gt;Tracing : 요청 흐름 추적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Metrics 기반 모니터링을 구축함으로서 시스템의 상태를 수치로 파악하고, 이상 징후를 조기에 감지하는데 초점을 두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 챙겨보아야 할 점으로 정리했던 내용처럼 Logs는 다음과 같은 목적을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 요청의 상세 정보 확인&lt;/li&gt;
&lt;li&gt;예외 스택 트레이스 분석&lt;/li&gt;
&lt;li&gt;사용자 단위 추적&lt;/li&gt;
&lt;li&gt;endpoint 기반 검색 및 필터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Metrics는 이상 감지에 Logs는 원인 분석에 특화되어 있다.&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;b&gt;  이번 단계의 설계 목표&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELK 도입은 단순히 로그를 한 곳에 저장하는 것이 목적이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 조건을 만족하는 환경을 구성하는 것이 목표인데 그 내용은 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kibana에서 요청 로그를 시간 기준으로 조회할 수 있을 것&lt;/li&gt;
&lt;li&gt;endpoint / userId / latency 기준 필터가 가능할 것&lt;/li&gt;
&lt;li&gt;특정 예외 유형 검색이 가능할 것&lt;/li&gt;
&lt;li&gt;시간대별 에러 발생 추이를 시각화할 수 있을 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 텍스트가 아닌 검색 가능한 데이터 구조로 전환하는 것이 핵심이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  ELK를 선택한 이유&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 기반 관측을 구현하기 위한 대표적인 스택은 ELK이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfGfJ0/dJMcabXyS3a/A8KUTmja6d1NklSsjy6LIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfGfJ0/dJMcabXyS3a/A8KUTmja6d1NklSsjy6LIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfGfJ0/dJMcabXyS3a/A8KUTmja6d1NklSsjy6LIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfGfJ0%2FdJMcabXyS3a%2FA8KUTmja6d1NklSsjy6LIk%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;600&quot; height=&quot;344&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ELK는 ElasticSearch, Logstash, Kibana 를 정리한 스택인데&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ElasticSearch : 로그 저장 및 검색 엔진&lt;/li&gt;
&lt;li&gt;Logstash : 로그 가공 및 변환&lt;/li&gt;
&lt;li&gt;Kibana : 로그 시각화 및 검색 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 ELK 구조는 Logstash를 통해 로그를 파싱하고, 필드를 가공한 뒤 Elasticsearch로 전달하는 형태가 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 Logstash를 포함하지 않는 구조를 우선적으로 고려했다.&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;b&gt;Logstash를 제외하고 Filebeat를 선택한 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;로그를 애플리케이션 레벨에서 구조화하는 방식을 채택할 계획이기 때문이다.&lt;br /&gt;endpoint / userId / latency 등 주요 필드를 포함한 JSON 형태의 로그를 출력하면, 별도의 복잡한 파싱 없이도 Elasticsearch에서 필드 기반 검색이 가능해진다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;단일 EC2 환경에서 운영되고 있는 프로젝트 특성상 불필요한 리소스 사용을 최소화하기 위함이다.&lt;br /&gt;Logstash는 강력한 변환 기능을 제공하지만 이번 단계에서는 단순 수집 및 검색을 목적으로 진행하기에 Filebeat 기반 구조가 적절하다고 판단했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 구조를 설계하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1771947642518&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Spring Boot (구조화된 로그 출력 예정)
        &amp;darr;
Docker stdout
        &amp;darr;
Filebeat
        &amp;darr;
Elasticsearch
        &amp;darr;
Kibana&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;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 로그를 JSON 구조로 변경&lt;/li&gt;
&lt;li&gt;Filebeat를 통해 Elasticsearch로 적재하며&lt;/li&gt;
&lt;li&gt;Kibana에서 로그를 검색 및 시각화하는 과정을 정리하고자 한다.&lt;/li&gt;
&lt;/ul&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;b&gt;이전 포스팅&lt;/b&gt;&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stark77.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://stark77.tistory.com/113&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stark77.tistory.com/114&quot;&gt;https://stark77.tistory.com/114&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1771933708935&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[실시간 대용량 서비스] 모니터링 시스템 구축은 왜 필요한가 ?&quot; data-og-description=&quot;프로젝트의 구현과정을 어느정도 경험하면서 모니터링 시스템은 왜 필요한지에 대한 궁금증이 생겼다. 모니터링 시스템은 종류도 많고 담당하는 역할도 정말 다양한데 그 중에서도 나는 이번 &quot; data-og-host=&quot;stark77.tistory.com&quot; data-og-source-url=&quot;https://stark77.tistory.com/113&quot; data-og-url=&quot;https://stark77.tistory.com/113&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dnzhA0/dJMb83SfN0G/I8IEmTKto6Sbbl6mSAFOu0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A9sKN/dJMb87f3o6n/KziKMPfSZA7C548xpddKY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/buPQrd/dJMb83kpUo9/aYubBJMfUkkDlNk7tjQBl0/img.png?width=1196&amp;amp;height=1101&amp;amp;face=0_0_1196_1101&quot;&gt;&lt;a href=&quot;https://stark77.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stark77.tistory.com/113&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dnzhA0/dJMb83SfN0G/I8IEmTKto6Sbbl6mSAFOu0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A9sKN/dJMb87f3o6n/KziKMPfSZA7C548xpddKY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/buPQrd/dJMb83kpUo9/aYubBJMfUkkDlNk7tjQBl0/img.png?width=1196&amp;amp;height=1101&amp;amp;face=0_0_1196_1101');&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;stark77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1771933711763&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[실시간 대용량 서비스] 모니터링 시스템 구축은 왜 필요한가 ?&quot; data-og-description=&quot;프로젝트의 구현과정을 어느정도 경험하면서 모니터링 시스템은 왜 필요한지에 대한 궁금증이 생겼다. 모니터링 시스템은 종류도 많고 담당하는 역할도 정말 다양한데 그 중에서도 나는 이번 &quot; data-og-host=&quot;stark77.tistory.com&quot; data-og-source-url=&quot;https://stark77.tistory.com/113&quot; data-og-url=&quot;https://stark77.tistory.com/113&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dnzhA0/dJMb83SfN0G/I8IEmTKto6Sbbl6mSAFOu0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A9sKN/dJMb87f3o6n/KziKMPfSZA7C548xpddKY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/buPQrd/dJMb83kpUo9/aYubBJMfUkkDlNk7tjQBl0/img.png?width=1196&amp;amp;height=1101&amp;amp;face=0_0_1196_1101&quot;&gt;&lt;a href=&quot;https://stark77.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stark77.tistory.com/113&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dnzhA0/dJMb83SfN0G/I8IEmTKto6Sbbl6mSAFOu0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A9sKN/dJMb87f3o6n/KziKMPfSZA7C548xpddKY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/buPQrd/dJMb83kpUo9/aYubBJMfUkkDlNk7tjQBl0/img.png?width=1196&amp;amp;height=1101&amp;amp;face=0_0_1196_1101');&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;stark77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/115</guid>
      <comments>https://stark77.tistory.com/115#entry115comment</comments>
      <pubDate>Wed, 25 Feb 2026 09:00:23 +0900</pubDate>
    </item>
    <item>
      <title>[실시간 대용량 서비스] Prometheus 모니터링 시스템 실제 적용기</title>
      <link>https://stark77.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개념 정리와 설계 의도를 담았던 이전 &lt;a href=&quot;https://stark77.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;포스팅&lt;/a&gt;에 이어서 실제 구현을 진행하고 나서의 내용들을 정리해보고자 한다.&lt;/p&gt;
&lt;figure id=&quot;og_1771915346074&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[실시간 대용량 서비스] 모니터링 시스템 구축은 왜 필요한가 ?&quot; data-og-description=&quot;프로젝트의 구현과정을 어느정도 경험하면서 모니터링 시스템은 왜 필요한지에 대한 궁금증이 생겼다. 모니터링 시스템은 종류도 많고 담당하는 역할도 정말 다양한데 그 중에서도 나는 이번 &quot; data-og-host=&quot;stark77.tistory.com&quot; data-og-source-url=&quot;https://stark77.tistory.com/113&quot; data-og-url=&quot;https://stark77.tistory.com/113&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dnzhA0/dJMb83SfN0G/I8IEmTKto6Sbbl6mSAFOu0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A9sKN/dJMb87f3o6n/KziKMPfSZA7C548xpddKY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/buPQrd/dJMb83kpUo9/aYubBJMfUkkDlNk7tjQBl0/img.png?width=1196&amp;amp;height=1101&amp;amp;face=0_0_1196_1101&quot;&gt;&lt;a href=&quot;https://stark77.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stark77.tistory.com/113&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dnzhA0/dJMb83SfN0G/I8IEmTKto6Sbbl6mSAFOu0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/A9sKN/dJMb87f3o6n/KziKMPfSZA7C548xpddKY1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/buPQrd/dJMb83kpUo9/aYubBJMfUkkDlNk7tjQBl0/img.png?width=1196&amp;amp;height=1101&amp;amp;face=0_0_1196_1101');&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;stark77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 CI/CD 기반 배포 환경이 구성되어 있었기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링 시스템 또한 Docker 기반 구조 안에서 자연스럽게 통합하는 방향으로 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 프로젝트의 설정을 진행했는데, 가장 먼저 필요했던 건 의존성 추가를 통해 정상적으로 모니터링이 동작할 수 있도록 하는 부분이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771916183031&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'&lt;/code&gt;&lt;/pre&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;b&gt;1. Actuator 연동과 첫 번째 문제 - 401 오류&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 추가 이후 실제 서버에서의 동작을 점검하기 위해 &lt;span&gt;/actuator/prometheus&lt;/span&gt; 엔드포인트가 정상적으로 노출되는지 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus는 이 엔드포인트를 주기적으로 scrape 하여 메트릭을 수집하게 되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 설정에서는 Prometheus Target 상태가 &lt;span&gt;DOWN&lt;/span&gt;으로 표시되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 확인해보니 다음과 같은 메시지가 나타났다.&lt;/p&gt;
&lt;pre id=&quot;code_1771921041910&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server returned HTTP status 401&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;이 에러의 경우는 Spring Security의 설정으로 인해 인증이 요구됨으로서 접근이 차단되었던 부분을 고려하지 못해 발생했었다.&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;b&gt;2. 단순 해결과 두번째 고민 - 보안 이슈&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하기 위해 SecurityPathPolicy 내 permitAll() 대상으로 해당 엔드포인트를 추가하였더니&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Prometheus Target 상태가 정상적으로 UP 되고, 메트릭 수집 또한 정상적으로 동작하였다.&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;blockquote data-ke-style=&quot;style3&quot;&gt;/actuator/prometheus를 공개 API 포트(8080)에서 permitAll로 열어두는 것은 보안상 위험할 수 있다.&lt;/blockquote&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;b&gt;3. Management Port 분리 - 구조적 개선&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 선택한 방법은 Actuator를 서비스 포트와 분리하는 것이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 요청 포트 : 8080&lt;/li&gt;
&lt;li&gt;관리용 Actuator 포트 : 9091&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;management.server.port=9091 설정을 통해 Actuator는 별도의 관리 포트에서만 동작하도록 분리하였다.&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;또한 Docker Compose 설정에서 해당 포트는 expose만 사용하여 Docker 네트워크 내부에서만 접근 가능하도록 구성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1771928950919&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    expose:
      - &quot;9091&quot;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. Docker 네트워크의 이해&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana에서 Prometheus를 연동하는 과정에서도 작은 혼란이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Prometheus URL을 다음과 같이 설정하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1771930622407&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://localhost:9090&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;Grafana는 별도의 컨테이너에서 실행되고 있기 때문에 Grafana 컨테이너 내부에서의 localhostsms Prometheus가 아니라 자기 자신을 의미했다.&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_1771930696254&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://peaktime-prometheus:9090&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 네트워크 내부에서는 컨테이너 이름이 DNS처럼 동작한다는 점을 이해해야만 해결할 수 있는 문제였다.&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;b&gt;5. EC2에서의 충돌 - 운영 서버는 개발 공간이 아니다.&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 서버에서 git pull을 통해 작업을 진행함에 있어서 충돌이 발생하였다.&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;이는 운영 서버에 존재하는 docker-compose 파일과 원격 파일의 변경사항이 충돌함으로서 발생한 문제였다.&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;EC2 서버를 다시 재설정하고 띄우는 과정에서 발생한 부분으로&amp;nbsp; 한가지 원칙을 명확하게 해야함을 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&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;rarr; PR &amp;rarr; Merge &amp;rarr; 서버 pull 의 흐름으로 진행하기로 했다.&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;b&gt;6. 최종 구성한 대시보드 구성&amp;nbsp;&lt;/b&gt;&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPS (Request per Second)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 요청량 추적&lt;/li&gt;
&lt;li&gt;트래픽 급증 감지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;5xx Error Rate
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 내부 오류 비율 모니터링&lt;/li&gt;
&lt;li&gt;1% 이상 시 주의 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;평균 응답 시간
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 체감 성능 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JVM Heap / GC
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 누수 및 GC 과부하 감지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;1044&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sJzaE/dJMcagEALYp/LdyPZ98AszuAcHInVw4kT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sJzaE/dJMcagEALYp/LdyPZ98AszuAcHInVw4kT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sJzaE/dJMcagEALYp/LdyPZ98AszuAcHInVw4kT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsJzaE%2FdJMcagEALYp%2FLdyPZ98AszuAcHInVw4kT0%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;800&quot; height=&quot;654&quot; data-origin-width=&quot;1277&quot; data-origin-height=&quot;1044&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;grafana_JVM.png&quot; data-origin-width=&quot;2555&quot; data-origin-height=&quot;1285&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdlvFa/dJMcac3c2VY/BnwUUG5RIV0PLxKiNYyGWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdlvFa/dJMcac3c2VY/BnwUUG5RIV0PLxKiNYyGWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdlvFa/dJMcac3c2VY/BnwUUG5RIV0PLxKiNYyGWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdlvFa%2FdJMcac3c2VY%2FBnwUUG5RIV0PLxKiNYyGWK%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;800&quot; height=&quot;402&quot; data-filename=&quot;grafana_JVM.png&quot; data-origin-width=&quot;2555&quot; data-origin-height=&quot;1285&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;grafana_springboot_monitor.png&quot; data-origin-width=&quot;2551&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cArt41/dJMb99SYkfM/Ik8wUrhIhXIVIAFHkgRdG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cArt41/dJMb99SYkfM/Ik8wUrhIhXIVIAFHkgRdG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cArt41/dJMb99SYkfM/Ik8wUrhIhXIVIAFHkgRdG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcArt41%2FdJMb99SYkfM%2FIk8wUrhIhXIVIAFHkgRdG0%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;800&quot; height=&quot;342&quot; data-filename=&quot;grafana_springboot_monitor.png&quot; data-origin-width=&quot;2551&quot; data-origin-height=&quot;1092&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단순히 로그를 뒤져보는 방식이 아닌 시스템 상태를 수치로 바라보는 관점을 제공한다.&lt;/li&gt;
&lt;li&gt;기존에는 장애가 발생한 이후 로그를 통해 원인을 추적하는 방식이었다면, 이제는 지표를 통해 이상 징후를 먼저 감지할 수 있는 구조가 되었다.&lt;/li&gt;
&lt;li&gt;GC, Heap 사용량, 응답 시간 등의 지표를 함께 보면서&lt;br /&gt;트래픽 증가 &amp;rarr; Heap 증가 &amp;rarr; GC 증가 &amp;rarr; Latency 증가 와 같은 흐름을 관찰할 수 있게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 단순 기능 개발을 넘어서서, 애플리케이션이 런타임 환경에서 어떻게 동작하는지를 이해하는 데 도움이 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI/CD 기반으로 배포가 이루어지는 구조에서 배포 이후 성능 변화나 오류율 변화를 즉시 확인할 수 있다.&lt;br /&gt;이를 통해 배포가 시스템에 어떤 영향을 미쳤는지 확인할 수 있는 근거를 제공할 수 있었다.&lt;/li&gt;
&lt;li&gt;모니터링 관련 추가 이후에는 왜 에러가 발생했을까? 에서 질문이 출발했다면 지표가 언제부터 비정상적으로 변했는가에 대해 선제적 운영에 가까워질 수 있었다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  정리하며..&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, Metrics 기반 모니터링은 이상 징후를 감지하는데는 효과적이지만, 구체적인 요청 로그나 사용자 단위 분석까지는 제공하지 못한다.&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;따라서 다음 단계로 로그 기반 관측 환경 (ELK)을 구성하여 보다 정밀한 원인 분석이 가능한 구조로 확장해보고자 한다.&lt;/p&gt;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/114</guid>
      <comments>https://stark77.tistory.com/114#entry114comment</comments>
      <pubDate>Tue, 24 Feb 2026 20:19:48 +0900</pubDate>
    </item>
    <item>
      <title>[실시간 대용량 서비스] 모니터링 시스템 구축은 왜 필요한가 ?</title>
      <link>https://stark77.tistory.com/113</link>
      <description>&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grafana + Prometheus를 활용한 모니터링 시스템 구축을 진행하고자 한다.&amp;nbsp;&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;b&gt;  모니터링 시스템의 종류와 비교 정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했듯 모니터링 시스템의 종류는 아래와 같이 다양했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.93%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;&lt;b&gt;대표 툴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;&lt;b&gt;적합한 환경&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;Metrics 수집&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;Prometheus&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;메트릭 수집/ 저장&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;Kubernetes 표준, 강력한 쿼리(PromQL), 오픈소스&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;초기 세팅 필요&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;컨테이너 /클라우드 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;시각화&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;Grafana&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;대시보드/알림&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;다양한 데이터소스, Alert 기능&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;별도 구성 필요&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;거의 모든 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;로그 분석&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;ELK Stack&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;로그 저장/검색&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;강력한 검색, 대규모 운영 적합&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;무겁고 비용 높음&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;대규모 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;로그 분석(경량)&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;Loki&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;로그 집계&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;가볍고 Grafana와 궁합 좋음&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;ELK보다 기능 제한&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;중소규모 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;분산 추적&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;Jaeger / Grafana Tempo&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;요청 병목 추적&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;마이크로서비스에 유ㅠ용&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;초기 학습 필요&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;MSA 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 12.9415%; text-align: center;&quot;&gt;상용 APM&lt;/td&gt;
&lt;td style=&quot;width: 12.8338%; text-align: center;&quot;&gt;Datadog / New Relic&lt;/td&gt;
&lt;td style=&quot;width: 16.7829%; text-align: center;&quot;&gt;통합 모니터링&lt;/td&gt;
&lt;td style=&quot;width: 27.1319%; text-align: center;&quot;&gt;자동 계측, UI 완성도 높음&lt;/td&gt;
&lt;td style=&quot;width: 14.9225%; text-align: center;&quot;&gt;비용&lt;/td&gt;
&lt;td style=&quot;width: 16.3178%; text-align: center;&quot;&gt;기업/대규모&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;위의 내용을 풀어서 다시한번 설명하자면&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Metrics vs Logs vs Tracing&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Metrics &amp;rarr; &quot;서버 상태를 숫자로 본다&quot;&lt;/li&gt;
&lt;li&gt;Logs &amp;rarr; &quot;무슨 일이 발생했는지 본다&quot;&lt;/li&gt;
&lt;li&gt;Tracing &amp;rarr; &quot;요청 하나가 어디서 느린지 본다&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 관점에서 가장 먼저 필요한건 보통 Metrics에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유로는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장애 감지&lt;/li&gt;
&lt;li&gt;성능 저하 감지&lt;/li&gt;
&lt;li&gt;리소스 초과 감지&lt;/li&gt;
&lt;/ul&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;b&gt;  왜 Prometheus + Grafana를 선택해야 했는가&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 진행하는 프로젝트 자체가 Spring Boot 기반 프로젝트인 점,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 컨테이너 환경에 EC2 서버로 운영을 진행하고 있고 규모가 크지 않은 단계에서의 운영환경인 점을 고려하여&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비용 효율이 좋고&lt;/li&gt;
&lt;li&gt;컨테이너 친화성이 있고&amp;nbsp;&lt;/li&gt;
&lt;li&gt;확장 가능성을 갖고 있으며&amp;nbsp;&lt;/li&gt;
&lt;li&gt;참조할 만한 레퍼런스가 풍부한 툴을 사용해야 했다.&lt;/li&gt;
&lt;/ul&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;☑️ Prometheus를 선택한 이유&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot Actuator와 바로 연동 가능&lt;/li&gt;
&lt;li&gt;Kubernetes / 컨테이너 환경에서 사실상 표준으로 사용을 한다는 점&lt;/li&gt;
&lt;li&gt;Pull 기반 구조 &amp;rarr; 중앙 관리가 용이하고&lt;/li&gt;
&lt;li&gt;PromQL을 통한 세밀한 분석이 가능하고&lt;/li&gt;
&lt;li&gt;완전 오픈소스로 비용 부담이 없다는 점이었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 인프라 구조와 가장 자연스럽게 연결되는 메트릭 수집 시스템&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;☑️ Grafana를 선택한 이유&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Prometheus와 궁합이 뛰어남&lt;/li&gt;
&lt;li&gt;다양한 대시보드 템플릿 존재&lt;/li&gt;
&lt;li&gt;Alert 기능 기본 제공&lt;/li&gt;
&lt;li&gt;Loki, Tempo까지 확장 가능&lt;/li&gt;
&lt;/ul&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;상용 APM은 아무래도 프로젝트의 규모나, 비용의 부담을 고려하지 않을 수 없어서 위의 툴을 채택하게 되었다.&amp;nbsp;&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;b&gt;  현재의 아키텍쳐 구조, 그리고 모니터링 시스템 도입 이후 아키텍쳐의 모습&lt;/b&gt;&lt;/h3&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;1141&quot; data-origin-height=&quot;751&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ad1QI/dJMcafZVfM4/eHCIoRD9okHEUMNASRDz5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ad1QI/dJMcafZVfM4/eHCIoRD9okHEUMNASRDz5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ad1QI/dJMcafZVfM4/eHCIoRD9okHEUMNASRDz5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAd1QI%2FdJMcafZVfM4%2FeHCIoRD9okHEUMNASRDz5K%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;800&quot; height=&quot;527&quot; data-origin-width=&quot;1141&quot; data-origin-height=&quot;751&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 요청 &amp;rarr; 서버 &amp;rarr; DB&lt;/li&gt;
&lt;li&gt;단일 요청 흐름 중심 구조에 있었다면,&lt;/li&gt;
&lt;/ul&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;1196&quot; data-origin-height=&quot;1101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MnzOa/dJMb99ZKL7M/6mryPXvSxgDn2BXIvIKpyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MnzOa/dJMb99ZKL7M/6mryPXvSxgDn2BXIvIKpyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MnzOa/dJMb99ZKL7M/6mryPXvSxgDn2BXIvIKpyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMnzOa%2FdJMb99ZKL7M%2F6mryPXvSxgDn2BXIvIKpyK%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;800&quot; height=&quot;736&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;1101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 요청 흐름&lt;/li&gt;
&lt;li&gt;메트릭 수집 흐름&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Before : 장애 / 지연 발생&amp;nbsp; &amp;rarr; 로그 찾아보기 &amp;rarr; 재현 어려움&lt;/li&gt;
&lt;li&gt;After : 지표 이상 감지 &amp;rarr; 문제 원인의 후보를 좁힘 &amp;rarr; 대응 시간 단축&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  그렇다면 무엇을 모니터링 할 것인가?&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;1. Traffic(트래픽)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초당 요청 수(RPS)&lt;/li&gt;
&lt;li&gt;엔드포인트별 호출량&lt;/li&gt;
&lt;/ul&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;2. Errors(에러율)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4xx 에러 비율&lt;/li&gt;
&lt;li&gt;5xx 에러 비율&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5xx 에러 비율은 서버 내부 오류를 의미하기 때문에 운영에서 가장 민감하게 관찰해야 하는 지표 중 하나이다.&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. Duration(지연 시간)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 응답 시간&lt;/li&gt;
&lt;li&gt;p95 / p99 latency&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균값은 정상이어도 일부 요청이 극단적으로 느려질 수 있기 때문에 p95 지표를 기준으로 모니터링을 진행할 예정이다.&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. Saturation (자원 사용률)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 사용량&lt;/li&gt;
&lt;li&gt;JVM Heap 사용률&lt;/li&gt;
&lt;li&gt;GC pause 시간&lt;/li&gt;
&lt;li&gt;DB 커넥션 풀 사용 현황&lt;/li&gt;
&lt;/ul&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;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/113</guid>
      <comments>https://stark77.tistory.com/113#entry113comment</comments>
      <pubDate>Mon, 23 Feb 2026 16:55:31 +0900</pubDate>
    </item>
    <item>
      <title>[실시간 대용량 서비스] JWT 인증에서 블랙리스트로도 충분한데, 왜 화이트리스트를 선택했을까?</title>
      <link>https://stark77.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 JWT 인증/인가 방식에 대한 설계를 정리한 이후, 인증인가에 대한 대화를 주고받는 과정에서 다음과 같은 질문을 받았다.&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;ldquo;블랙리스트와 화이트리스트의 차이가 정확히 무엇인가요?&amp;rdquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;화이트리스트에서 하려는 제어를 블랙리스트로는 구현할 수 없나요?&amp;rdquo;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  블랙리스트란?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;블랙리스트란 다시 말하자면 &lt;b&gt;이미 문제가 된 토큰 목록&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;JWT 인증 구조에서 블랙리스트는 보통 다음을 의미한다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그아웃된 AccessToken&lt;/li&gt;
&lt;li&gt;탈취가 의심되어 강제로 만료한 토큰&lt;/li&gt;
&lt;li&gt;특정 정책에 의해 더 이상 유효하지 않은 토큰&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;정리하면, 원래는 유효했지만 지금은 사용하면 안되는 토큰을 따로 기록해두는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  블랙리스트의 인증 흐름&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 AccessToken을 들고 요청&lt;/li&gt;
&lt;li&gt;서버는 서명 + 만료 검증&lt;/li&gt;
&lt;li&gt;블랙리스트에 포함되어 있는지 추가 확인&lt;/li&gt;
&lt;li&gt;포함되어 있다면 거부&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, 블랙리스트에 없으면 &lt;b&gt;토큰은 유효하다&lt;/b&gt;고 가정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  블랙리스트의 설계적 의미&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT의 Stateless 기반 개념을 크게 벗어나지 않는다.&lt;/li&gt;
&lt;li&gt;서버는 문제가 생긴 토큰만 기억한다.&lt;/li&gt;
&lt;li&gt;정상적인 토큰 흐름에는 개입하지 않는다.&lt;br /&gt;&amp;rarr; 쉽게 생각하면 &lt;b&gt;사후대응&amp;nbsp;&lt;/b&gt;이다.&lt;br /&gt;&amp;rarr; 이러한 점을 고려해야 하는 부분이다. 그래서 아직 발견되지 않은 탈취 토큰은 어떻게 처리할 것인가?&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  화이트리스트란?&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화이트리스트란 다시 말하자면 &lt;b&gt;서버가 현재 인정하고 있는 토큰 목록&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;JWT 인증 구조에서 화이트리스트는 &lt;b&gt;서버의 명시적 승인&lt;/b&gt;이 있어야만 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 화이트리스트의 인증 흐름&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 토큰을 들고 요청&lt;/li&gt;
&lt;li&gt;서버는 서명 + 만료 검증&lt;/li&gt;
&lt;li&gt;화이트리스트에 존재하는지 확인&lt;/li&gt;
&lt;li&gt;존재할 때만 요청 허용&lt;/li&gt;
&lt;/ol&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  화이트리스트의 설계적 의미&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버는 언제든 자격을 회수할 수 있어야 하고&lt;/li&gt;
&lt;li&gt;어떤 토큰이 살아 있는지도 알고 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 관점은 보안적으로는 강력하지만, 명확한 비용을 동반한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 상태 관리 필요&lt;/li&gt;
&lt;li&gt;저장소(Redis 등) 의존&lt;/li&gt;
&lt;li&gt;완전한 Stateless는 포기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  '기본 허용' 과 '기본 차단' 관점의 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용을 정리하면 블랙리스트와 화이트리스트의 차이는 단순히 목록을 어디에 두느냐의 문제가 아니라&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;토큰을 바라보는 기본 관점의 차이&lt;/b&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;b&gt;기본 허용(Default Allow) &lt;/b&gt;구조이고, 화이트리스트는 &lt;b&gt;기본 차단(Default Deny) &lt;/b&gt;구조에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보안 사고를 어떤 시점에서 막고 싶은가의 선택의 차이&lt;/b&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;b&gt;❓ 그래서 블랙리스트로 화이트리스트에서 하려는 제어를 구현할 수 있을까 없을까?&lt;/b&gt;&lt;/h3&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;예를 들어, RefreshToken을 재발급할 때마다 이전 토큰을 전부 블랙리스트로 등록하고 현재 토큰만 살아있게 관리한다면 결과적으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;허용된 토큰만 사용 가능한 상태&lt;/i&gt;&amp;nbsp;를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;✓ 그럼에도 블랙리스트를 선택하지 않는 이유는 무엇인가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계 단계에서 고민했던 것은 가능한가가 아니라 이 처리 방식이 맞는가에 대해서 였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블랙리스트는 구조적으로 이 토큰이 과거에 문제가 있었는가? 를 묻는다면, 내가 고민했던 부분은 &lt;b&gt;이 토큰을 지금도 신뢰할 수 있는가?&lt;/b&gt; 였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  블랙리스트의 한계 (RefreshToken 관점)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RefreshToken은 다음과 같은 특성을 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수명이 길고&lt;/li&gt;
&lt;li&gt;사용 빈도는 낮지만&lt;/li&gt;
&lt;li&gt;탈취 시 피해가 크며&lt;/li&gt;
&lt;li&gt;로그아웃, 강제 만료가 반드시 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;이러한 특성은 이전 글에서도 언급했지만,이번 글에서는 &amp;lsquo;왜 블랙리스트 방식이 맞지 않았는가&amp;rsquo;라는 관점에서 다시 살펴본다.&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;b&gt;  화이트리스트가 적합했던 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화이트리스트는 위에서 작성했던 것과 같이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;토큰이 살아 있는지&lt;/li&gt;
&lt;li&gt;서버가 지금도 인정하는 상태인지&lt;/li&gt;
&lt;li&gt;언제든 자격을 회수할 수 있는지&lt;/li&gt;
&lt;/ul&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;b&gt;  도메인 성향을 고려한 최종 판단&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;이런 도메인에서는 단순히 &amp;ldquo;로그인에 성공했는가&amp;rdquo;보다 &amp;ldquo;지금 이 접근을 허용해도 되는가&amp;rdquo;를 판단할 수 있는 구조가 더 중요하다고 생각했다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계정 공유를 통한 동시 접속&lt;/li&gt;
&lt;li&gt;로그아웃 이후에도 유지되는 접근&lt;/li&gt;
&lt;li&gt;탈취된 토큰을 통한 장시간 이용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;이러한 이유로, RefreshToken은 화이트리스트 방식으로 통제하고, AccessToken은 Stateless하게 유지하는 혼합 인증 구조를 선택하게 되었다.&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;b&gt;  정리하며 - 이 질문이 내 설계에서 의미했던 것&amp;nbsp;&lt;/b&gt;&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;블랙리스트는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이미 문제가 발생한 토큰을 어떻게 차단할 것인가&quot; 에 대한 해결법이고,&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;&quot;이 토큰을 지금 이 순간에도 신뢰할 수 있는가&quot;를 묻는 방식이다.&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;RefreshToken은&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;그래서 이전 글에서 정리한 Redis 기반 화이트리스트 구조는 단순한 구현 선택이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 신뢰하는 기준을 어디에 둘 것인가에 대한 의도적인 설계 판단의 결과였다.&lt;/p&gt;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/112</guid>
      <comments>https://stark77.tistory.com/112#entry112comment</comments>
      <pubDate>Fri, 6 Feb 2026 21:00:24 +0900</pubDate>
    </item>
    <item>
      <title>[실시간 대용량 서비스] AccessToken / RefreshToken 분리 설계와 Redis 화이트리스트</title>
      <link>https://stark77.tistory.com/111</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  왜 인증 구조에 대해 고민했는가&lt;/b&gt;&lt;/h3&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;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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다중 디바이스 로그인 가능 (PC / Mobile / Tablet 등...)&amp;nbsp;&lt;/li&gt;
&lt;li&gt;관리자 / 강사 / 수강생 역할 분리&lt;/li&gt;
&lt;li&gt;향후 계정 공유 방지, 동시 로그인 제한 정책 확장 가능성&lt;/li&gt;
&lt;li&gt;API 서버의 수평 확장 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 세션 기반 인증보다는 Stateless 한 구조가 더 적합하다고 생각했다.&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;b&gt;  세션 기반 인증을 선택하지 않은 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 사용할 때 가장 익숙한 방식은 세션 기반 인증이다.&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;b&gt;❌ 세션 기반 인증의 한계점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 메모리 또는 외부 저장소(Session Storage)에 상태 의존&lt;/li&gt;
&lt;li&gt;수평 확장 시 세션 동기화 비용 발생&lt;/li&gt;
&lt;li&gt;디바이스 단위 제어가 까다로움&lt;/li&gt;
&lt;li&gt;로그아웃 / 강제 만료 정책이 세션에 강하게 결합됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ JWT 기반 인증의 장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 요청의 주 경로를 서버 상태 없이 처리 가능&lt;/li&gt;
&lt;li&gt;토큰 자체로 사용자 식별 가능&lt;/li&gt;
&lt;li&gt;API 서버 확장에 유리&lt;/li&gt;
&lt;li&gt;모바일 / 웹 클라이언트와의 궁합이 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 따라서 AccessToken은 완전히 Stateless 하게 가져가는 방향으로 결정했다.&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;b&gt;  AccessToken / RefreshToken 분리 설계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT를 사용할 때 가장 먼저 부딪히는 문제는 &lt;i&gt;&lt;b&gt;토큰 탈취&lt;/b&gt;&lt;/i&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;위에서 정리한 세션 기반 / JWT 기반 인증의 특징을 고려하고, 교육 도메인의 특성을 고려하여 아래와 같은 구조로 선택을 진행했다.&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;b&gt;AccessToken&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;짧은 만료 시간&lt;/li&gt;
&lt;li&gt;API 요청마다 사용&lt;/li&gt;
&lt;li&gt;서버에 저장하지 않음 (완전 무결성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RefreshToken&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상대적으로 긴 만료 시간&lt;/li&gt;
&lt;li&gt;AccessToken 재발급 용도&lt;/li&gt;
&lt;li&gt;서버에서 통제 가능해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 JWT 설계에서 나는 RefreshToken을 어디에서 관리할 것인가에 대한 고민이 필요했다.&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;b&gt;  RefreshToken을 Redis에 저장한 이유&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RefreshToken을 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;&lt;b&gt;&quot;Stateless가 아닌 것 아닌가?&quot;&lt;/b&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;이는 RefreshToken을 서버에서 관리하고자 하는 의도적인 선택이었다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계정 공유를 통한 다중 접속&lt;/li&gt;
&lt;li&gt;토큰 탈취 후 장시간 접근&lt;/li&gt;
&lt;li&gt;로그아웃 이후에도 계속되는 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황을 고려했을 때 RefreshToken까지 완전히 Stateless로 두는 것은 보안 및 운영 측면에서 제어하기 어려운 구조라고 생각했다.&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;따라서 인증 구조 전체가 Stateless 하지는 않지만, 상태가 필요한 지점을 의도적으로 제한한 구조라고 생각한다.&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;b&gt;☑️ Redis 화이트리스트 구조 선택&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RefreshToken 저장소로 Redis를 선택했고, 단순 저장이 아니라 화이트리스트 방식으로 관리하도록 설계했다.&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;b&gt;Redis 구조 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1770029442108&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;key: refresh:{userId}:{deviceId}
value: refreshToken
TTL: refreshToken 만료 시간&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그아웃 시 즉시 무효화&lt;/li&gt;
&lt;li&gt;디바이스별 토큰 관리&lt;/li&gt;
&lt;li&gt;특정 디바이스만 강제 로그아웃&lt;/li&gt;
&lt;li&gt;동시 로그인 제한 정책 확장 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;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;b&gt;Spring Security에서 지킨 원칙&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Stateless 우선 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; AccessToken 자체는 서버에 상태를 두지 않고 처리하되, 필요 시 통제할 수 있는 지점을 남김으로써 완전한 stateless아 보안성 사이의 균형을 유지했다.&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;b&gt;2. 통제 가능한 RefreshToken 정책&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; RefreshToken 을 서버(Redis)에 저장함으로써 로그아웃 즉시 만료 처리, 디바이스 별 토큰 관리, 세션 공유 차단 같은 보안 요구사항을 만족했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이는 모든 RefreshToken을 상태로 저장한다는 점에서 stateless 철학과는 다르지만 통제와 보안 우선의 설계 선택이었다.&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;b&gt;3. Whitelist를 통한 RefreshToken 관리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Redis에 등록된 RefreshToken만 유효하게 처리함으로써 공격자가 탈취한 토큰을 마음대로 재사용하지 못하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이를 통해, 단순 JWT 기반 인증 구조보다 보안적으로 더 강한 제어력을 갖게 됐다.&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;b&gt;마무리하며..&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 설계에서는 JWT 기반 인증을 적용하면서도 보안성과 통제력을 높이는 방향으로 선택했다.&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;✓ AccessToken은 무상태 처리로 확장성이 좋고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✓ RefreshToken은 서버에서 관리하며&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;즉, Stateless 구조의 장점을 살리면서 Stateful한 통제도 활용한 하이브리드 설계가 됐다.&lt;/p&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;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;최종 프로젝트 발제 포스팅&quot; href=&quot;https://stark77.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stark77.tistory.com/110&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1770026626587&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[실시간 대용량 서비스] 최종 프로젝트 발제(작성중)&quot; data-og-description=&quot;  프로젝트 개요본 프로젝트는 자유 주제로 진행된 최종 프로젝트로, 온라인 교육 플랫폼을 목표로 한 백엔드 서버 구현에 초점을 두었습니다. JWT 기반 인증&amp;middot;인가 구조를 중심으로 사용자, 강&quot; data-og-host=&quot;stark77.tistory.com&quot; data-og-source-url=&quot;https://stark77.tistory.com/110&quot; data-og-url=&quot;https://stark77.tistory.com/110&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dUjVsd/dJMb9b3NdkT/F3GYKyg4NWjy9gSdlxkT1k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/fkUA4/dJMb9fZqrbO/ugIXxrM8ZMk1obMyzWdZKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://stark77.tistory.com/110&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stark77.tistory.com/110&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dUjVsd/dJMb9b3NdkT/F3GYKyg4NWjy9gSdlxkT1k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/fkUA4/dJMb9fZqrbO/ugIXxrM8ZMk1obMyzWdZKk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;  프로젝트 개요본 프로젝트는 자유 주제로 진행된 최종 프로젝트로, 온라인 교육 플랫폼을 목표로 한 백엔드 서버 구현에 초점을 두었습니다. JWT 기반 인증&amp;middot;인가 구조를 중심으로 사용자, 강&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stark77.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/111</guid>
      <comments>https://stark77.tistory.com/111#entry111comment</comments>
      <pubDate>Mon, 2 Feb 2026 19:52:26 +0900</pubDate>
    </item>
    <item>
      <title>[실시간 대용량 서비스] 최종 프로젝트 발제</title>
      <link>https://stark77.tistory.com/110</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  프로젝트 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 프로젝트는 자유 주제로 진행된 최종 프로젝트로, &lt;b&gt;온라인 교육 플랫폼을 목표로 한 백엔드 서버 구현&lt;/b&gt;&lt;span&gt;에 초점을 두었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 기반 인증&amp;middot;인가 구조를 중심으로 사용자, 강의, 주문, 관리자 기능을 &lt;span&gt;&lt;b&gt;확장 가능한 아키텍처&lt;/b&gt;&lt;/span&gt;로 설계하고 구현했으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 기능 구현보다는 &lt;span&gt;&lt;b&gt;설계 의도와 구조적 일관성&lt;/b&gt;&lt;/span&gt;을 중요하게 고려했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  프로젝트 목표&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 기반 stateless 인증/인가 구조 설계 및 구현&lt;/li&gt;
&lt;li&gt;역할(Role) 기반 접근 제어 정책 명확화 (STUDENT / LECTURER / ADMIN)&lt;/li&gt;
&lt;li&gt;도메인 규칙을 코드로 표현하는 도메인 중심 설계&lt;/li&gt;
&lt;li&gt;리뷰 및 확장을 고려한 일관된 아키텍처 구조 유지&lt;/li&gt;
&lt;li&gt;기능 추가 시 기존 구졸르 해치지 않는 설계 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  아키텍처 개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 프로젝트는 4-Layer Architecture를 기반으로 구성되어 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Presentation Layer&lt;br /&gt;API 요청 / 응답 처리 및 인증 진입점&lt;/li&gt;
&lt;li&gt;Application Layer (Service)&lt;br /&gt;유스케이스 흐름 제어 및 트랜잭션 관리&lt;/li&gt;
&lt;li&gt;Domain Layer&lt;br /&gt;엔티티 및 도메인 규칙 관리&lt;/li&gt;
&lt;li&gt;Infrastructure Layer&lt;br /&gt;데이터 접근 및 외부 시스템 연동&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  인증 / 인가 설계 개요&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access Token
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 기반&lt;/li&gt;
&lt;li&gt;Stateless 구조 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Refresh Token
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 화이트리스트 방식 관리&lt;/li&gt;
&lt;li&gt;로그아웃 및 강제 만료 대응&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OAuth2 로그인&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth2 인증은 서버에서 처리&lt;/li&gt;
&lt;li&gt;로그인 성공 후 JWT 발급&lt;/li&gt;
&lt;li&gt;서버 세션은 사용하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설계 원칙&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 기반 인증 미사용 (STATELESS)&lt;/li&gt;
&lt;li&gt;인증/인가 정책은 SecurityCofnig에서 선언적으로 관리&lt;/li&gt;
&lt;li&gt;API 명세 기준으로 공개 / 보호 엔드포인트 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; &amp;nbsp; 내가 담당한 구현 범위 (MVP v1 기준)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 기반 인증/인가 구조 설계 및 구현&lt;/li&gt;
&lt;li&gt;AccessToken / RefreshToken 발급 및 검증&lt;/li&gt;
&lt;li&gt;OAuth2 로그인 이후 JWT 발급 흐름 구현&lt;/li&gt;
&lt;li&gt;SecurityConfig 기반 역할(Role)별 접근 제어&lt;/li&gt;
&lt;li&gt;관리자 API 인증/인가 정책 구성&lt;/li&gt;
&lt;li&gt;전역 예외 처리 및 인증 실패 응답 일관화&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; ️ 사용 기술 스택&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Backend&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Sproing Boot 3.5.9&lt;/li&gt;
&lt;li&gt;Spring Security&lt;/li&gt;
&lt;li&gt;JPA&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Data &amp;amp; Infra&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Redis
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RefreshToken 화이트리스트 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Docker / Docker Compose&lt;/li&gt;
&lt;/ul&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;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;  설계 &amp;amp; 고민 정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 항목들은 구현 과정에서 실제로 고민했던 주제들이며 별도 포스팅으로 정리해 연결할 예정입니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인&lt;/b&gt;&lt;b&gt;증 인가 설계&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 기반 인증 구조를 선택한 이유&lt;/li&gt;
&lt;li&gt;Stateless 인증과 서버 세션 미사용에 대한 판단&lt;/li&gt;
&lt;li&gt;RefreshToken을 Redis 화이트리스트로 관리한 이유&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OAuth2 로그인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OAuth2 로그인 이후 JWT 발급 흐름 설계&lt;/li&gt;
&lt;li&gt;OAuth2 + JWT 조합에서 서버의 책임 범위&lt;/li&gt;
&lt;li&gt;OAuth2 로그인 성공 핸들러의 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecurityConfig &amp;amp; 권한 제어&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SecurityConfig에서 인증/인가 정책을 선언적으로 관리한 이유&lt;/li&gt;
&lt;li&gt;@PreAuthorize의 역할 분리 기준&lt;/li&gt;
&lt;li&gt;역할 기반 접근 제어 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아키텍쳐 &amp;amp; 도메인 설계&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;4-Layer Architecture를 선택한 이유&lt;/li&gt;
&lt;li&gt;도메인 규칙을 어디까지 도메인에 둘 것인가에 대한 기준&lt;/li&gt;
&lt;li&gt;Service 와 Domain의 책임 분리 고민&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;⏱️ 앞으로 정리할 예정인 내용&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테스트 코드 정리&lt;/li&gt;
&lt;li&gt;관리자 기능 확장&lt;/li&gt;
&lt;li&gt;MVP v2 설정 및 설계 방향 정리&lt;/li&gt;
&lt;li&gt;프로젝트 회고 정리&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/110</guid>
      <comments>https://stark77.tistory.com/110#entry110comment</comments>
      <pubDate>Fri, 30 Jan 2026 21:01:58 +0900</pubDate>
    </item>
    <item>
      <title>[과제 회고] K사 서버 개발하기</title>
      <link>https://stark77.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Redis-Kafka 기반의 개념을 학습한 이후 실전과제를 진행하게 되었다.&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;b&gt;  과제 소개&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다수 서버 환경에서도 안정적으로 동작하는 커피숍 주문 시스템을 구현하기&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커피 주문에 필요한 메뉴를 구성하고 조회가 가능해야 합니다.&lt;/li&gt;
&lt;li&gt;커피 주문은 포인트로 가능합니다.&lt;/li&gt;
&lt;li&gt;커피 주문내역을 통해 인기 있는 메뉴를 추천합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  구현 필요 API&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커피 메뉴 목록 조회 API&lt;/li&gt;
&lt;li&gt;포인트 충전하기 API&lt;/li&gt;
&lt;li&gt;커피 주문, 결제하기 API&lt;/li&gt;
&lt;li&gt;인기 메뉴 목록 조회 API&lt;/li&gt;
&lt;/ol&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;b&gt;  설계 요약&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멱등성 키 기반 중복 요청 방지&lt;/li&gt;
&lt;li&gt;DB + 서비스 레벨 이중 방어로 동시성 대응&lt;/li&gt;
&lt;li&gt;주문 결제와 집계 로직의 명확한 책임 분리&lt;/li&gt;
&lt;li&gt;이벤트 기반 비동기 처리로 확장성 확보&lt;/li&gt;
&lt;li&gt;실시간 조회에 적합한 Redis 집계 구조 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;b&gt;  설계 및 구현 간 고민했던 부분&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;트랜잭션의 범위가 적합한가?&lt;/b&gt;&lt;/h4&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;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;b&gt;핵심 트랜잭션만 최소 단위로 유지하자!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주문 도메인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 생성&lt;/li&gt;
&lt;li&gt;주문 상태 변경&lt;br /&gt;  주문 자체의 정합성만 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포인트 도메인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;포인트 차감&lt;/li&gt;
&lt;li&gt;포인트 거래 이력 저장&lt;br /&gt;  금전 처리의 원자성만 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;집계 / 전송&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka 이벤트 발행&lt;/li&gt;
&lt;li&gt;Redis 집계&lt;br /&gt;  트랜잭션 밖에서 비동기 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 의도가 드러난 코드는 아래와 같이 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1768440772168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handle(OrderCompletedDomainEvent event) {
    // Kafka 이벤트 전송
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 방식의 의미&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 트랜잭션이 정상 커밋된 이후에만 이벤트를 전송&lt;/li&gt;
&lt;li&gt;Kafka 전송 실패가 되더라도 주문/결제는 롤백되지 않음&lt;/li&gt;
&lt;li&gt;외부 시스템 장애가 핵심 비즈니스 로직에 전달되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 주문/결제는 안정적으로 유지되고 집계 및 데이터 수집 로직은 독립적으로 확장 가능하게끔 구조를 만들었다.&lt;/p&gt;</description>
      <category>Spring/Spring 실습</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/109</guid>
      <comments>https://stark77.tistory.com/109#entry109comment</comments>
      <pubDate>Wed, 14 Jan 2026 13:38:51 +0900</pubDate>
    </item>
    <item>
      <title>[개념정리] Kafka Offset과 Commit</title>
      <link>https://stark77.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 기초 개념을 정리하며 경험했었던 Consumer Group과 Offset의 개념, 그리고 Kafka에서의 Commit이란 무엇인가에 대해 정리해보겠다.&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;b&gt;☑️ Offset과 Consumer Group이 뭐였었지?&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Offset이란?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Offset은 각 파티션에서 메시지에 붙는 번호&lt;/li&gt;
&lt;li&gt;파티션마다 0,1,2,3 과 같이 순서대로 증가&lt;/li&gt;
&lt;li&gt;Consumer는 어떤 파ㅣㅌ셔넹서 어디까지 읽었는지에 대한 기준을 Offset을 통해 기억&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Consumer Group과 Offset&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Offset은 Consumer Group 단위로 관리&lt;/li&gt;
&lt;li&gt;어디까지 읽었는지에 대해서는 Consumer Group + Partition 조합으로 구분된다.&lt;/li&gt;
&lt;li&gt;Consumer Group이 다르다면 같은 토픽을 읽더라도 어디까지 읽었는가에 대해서는 서로 독립적이라고 할 수 있다.&lt;/li&gt;
&lt;/ul&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;b&gt;✅ Commit은 무엇인가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset은 번호, Commit은 그 번호를 저장하는 행위라고 말할 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(GitHub로 작업하면서 수없이 진행했던 커밋의 의미와 크게 다르지 않다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Commit의 의미&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Commit은 이 Offset까지 읽었습니다. 라고 Kafka에 기록해 두는 것&lt;/li&gt;
&lt;li&gt;Consumer가 다시 시작되면 마지막으로 커밋한 Offset 이후부터 메시지를 계속 읽는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 Commit이 중요한가?&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Commit이 없다면 Consumer가 재시작될 때마다 처음부터 다시 읽어야 한다.&lt;/li&gt;
&lt;li&gt;Commit을 너무 빨리 해버리면 아직 처리하지 못한 메시지도 읽었다고 표시될 수 있고, 이는 데이터 유실 위험이 생기게 된다.&lt;/li&gt;
&lt;li&gt;Commit을 너무 늦게 해버리면 이미 처리한 메시지를 다시 읽어버릴 수 있고, 이는 중복 처리 위험이 생기게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 기본적으로 중복되더라도 일단 한 번 이상은 꼭 전달하자라는 철학을 갖고 있다. (&lt;i&gt;&lt;b&gt;at-least-once 전달 방식&lt;/b&gt;&lt;/i&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;b&gt;✅ Spring Kafka에서 Commit은 어떻게 동작하는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Kafka 는 @KafkaListener 뒤에서 Listener Container를 사용하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨테이너가 메시지를 가져와서 @KafkaListener 메서드에 넘겨주고, 적절한 시점에 Offset을 Commit 해주는 것&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;자동으로만 진행되었던 Commit을 수동으로 구현 및 실제로 진행해보며 어떤 흐름으로 동작하는지를 확인할 수 있었는데 그 내용은 아래와 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;수동 Commit용 Listener 설정 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767786395401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;

// ...

    // 수동 Commit(Ack)용 ConsumerGroup 생성
    @Bean
    public ConsumerFactory&amp;lt;String, String&amp;gt; manualConsumerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        // Kafka 서버 위치
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);

        // 이 Consumer가 속할 그룹 아이디
        props.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;manual-ack-group&quot;);

        // key, value를 문자열로 역직렬화
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        // 처음 시작할 때, 토픽 맨 앞(earliest)부터 읽도록 설정
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props);
    }
    
    
    
    
    // 수동 Commit(Ack)용 ListenerContainerFactory
    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt;
    manualAckKafkaListenerContainerFactory() {

        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory =
            new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();

        factory.setConsumerFactory(manualConsumerFactory());

        // AckMode를 MANUAL로 설정
        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);

        return factory;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AckMode.MANUAL 설정을 진행하여
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@KafkaListener 메서드에서 Acknowledgement 객체를 받아서&amp;nbsp;&lt;/li&gt;
&lt;li&gt;직접 ack.acknowledge()를 호출할 수 있다. 이 호출 시점에 Offset이 커밋된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;수동 Commit Listener 작성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767786478877&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ManualAckConsumerListener {

    @KafkaListener(
        topics = &quot;simple-messages&quot;,
        groupId = &quot;manual-ack-group&quot;,
        containerFactory = &quot;manualAckKafkaListenerContainerFactory&quot;
    )
    public void consume(ConsumerRecord&amp;lt;String, String&amp;gt; record,
                        Acknowledgment ack) {

        String message = record.value();
        log.info(&quot;[manual-ack] 받은 메시지: {}&quot;, message);
        
        if (message.equals(&quot;error&quot;)) {
            throw new RuntimeException(&quot;kafka 에러 발생&quot;);
        }
        
        // 처리에 성공했다고 판단되면 Offset을 Commit합니다.
        ack.acknowledge();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConsumerRecord&amp;lt;String, String&amp;gt;을 사용하면 메시지 값 뿐만 아니라 Offset, 파티션 정보를 함께 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;Acknowledgement ack 를 통해 메시지의 처리 완료 상태와 Offset을 Commit해도 좋다고 직접 알려줄 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진행해보며 ack.acknowledge()를 호출하지 않아도 커밋되는 상황이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Error가 들어오면 여러번 재시도 된 뒤 재시도 횟수를 넘기면 메시지가 들어오지 않는 상황이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 현상이 발생하는 이유로는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@KafkaListener에서 예외가 발생하면 메시지 처리 제어권은 에러 핸들러에게 넘어간다.&lt;/li&gt;
&lt;li&gt;Spring Kafka의 기본 에러 핸들러는 설정된 횟수만큼 동일한 메시지를 재시도한다.&lt;/li&gt;
&lt;li&gt;재시도 후에도 계속 실패하면, 이 메시지는 더이상 처리할 수 없다고 판단하고 그 레코드를 포기하는 과정에서 해당 Offset을 커밋할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 정상 처리 흐름에서는 ack.acknowledge() 를 호출한 시점에 커밋이 일어나고, 에러 흐름에서는 에러 핸들러가 재시도를 포기하는 순간 Offset이 커밋될 수 있다.&lt;/p&gt;</description>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/108</guid>
      <comments>https://stark77.tistory.com/108#entry108comment</comments>
      <pubDate>Thu, 8 Jan 2026 10:00:40 +0900</pubDate>
    </item>
    <item>
      <title>[개념정리] Kafka + Spring 적용방법</title>
      <link>https://stark77.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;kafka의 기본 개념을 기반으로 Spring에 적용하고 동작하는 방식을 확인하는 과정을 정리하고자 한다.&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;b&gt;✅ Kafka 기본 개념&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Producer, Consumer, Topic&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka로 메시지를 보내는 클라이언트&lt;/li&gt;
&lt;li&gt;특정 Topic에 메시지를 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Consumer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에 저장된 메시지를 읽어오는 클라이언트&lt;/li&gt;
&lt;li&gt;한 개 이상의 Topic을 구독(subscribe)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Topic
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 저장하는 논리적인 이름 공간&lt;/li&gt;
&lt;li&gt;로그처럼 메시지가 순서대로 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 프로젝트를 진행하기 전에 앞서 우리는 docker-compose 설정을 진행하고, KafkaConfig 설정을 통해 Spring과의 연동을 준비했다.&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;size18&quot;&gt;&lt;b&gt;Kafka Cluster (docker-compose.yml)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767783815264&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.8'

services:
  # --------------------------
  # Broker 1 (Node ID: 1)
  # --------------------------
  kafka-1:
    image: apache/kafka:3.7.0
    container_name: kafka-1
    ports:
      - &quot;9092:9092&quot;
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093

      # listeners
      KAFKA_LISTENERS: CONTROLLER://:9093,INTERNAL://:29092,EXTERNAL://:9092
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-1:29092,EXTERNAL://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL

      # replication configs
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0

      KAFKA_LOG_DIRS: /var/lib/kafka/data
    volumes:
      - kafka1_data:/var/lib/kafka/data
    networks:
      - kafka-net

  # --------------------------
  # Broker 2 (Node ID: 2)
  # --------------------------
  kafka-2:
    image: apache/kafka:3.7.0
    container_name: kafka-2
    ports:
      - &quot;9093:9092&quot;
    environment:
      KAFKA_NODE_ID: 2
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093

      # listeners
      KAFKA_LISTENERS: CONTROLLER://:9093,INTERNAL://:29092,EXTERNAL://:9092
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-2:29092,EXTERNAL://localhost:9093
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL

      # replication configs
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2

      KAFKA_LOG_DIRS: /var/lib/kafka/data
    volumes:
      - kafka2_data:/var/lib/kafka/data
    networks:
      - kafka-net

  # --------------------------
  # Broker 3 (Node ID: 3)
  # --------------------------
  kafka-3:
    image: apache/kafka:3.7.0
    container_name: kafka-3
    ports:
      - &quot;9094:9092&quot;
    environment:
      KAFKA_NODE_ID: 3
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093

      # listeners
      KAFKA_LISTENERS: CONTROLLER://:9093,INTERNAL://:29092,EXTERNAL://:9092
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka-3:29092,EXTERNAL://localhost:9094
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL

      # replication configs
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2

      KAFKA_LOG_DIRS: /var/lib/kafka/data
    volumes:
      - kafka3_data:/var/lib/kafka/data
    networks:
      - kafka-net

  # --------------------------
  # Kafka UI
  # --------------------------
  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name: kafka-ui
    ports:
      - &quot;8088:8080&quot;
    environment:
      KAFKA_CLUSTERS_0_NAME: local-kraft-cluster
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:29092,kafka-2:29092,kafka-3:29092
      KAFKA_CLUSTERS_0_METADATAPROVIDER: &quot;KAFKA&quot;
      DYNAMIC_CONFIG_ENABLED: true
    depends_on:
      - kafka-1
      - kafka-2
      - kafka-3
    networks:
      - kafka-net

volumes:
  kafka1_data:
  kafka2_data:
  kafka3_data:

networks:
  kafka-net:
    driver: bridge&lt;/code&gt;&lt;/pre&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;b&gt;✅ Kafka Config 설정&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1767783899348&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

@EnableKafka
@Configuration
public class KafkaConfig {

    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;)
    private String bootstrapServers;

    // 1) Producer 설정 (String, String)
    @Bean
    public ProducerFactory&amp;lt;String, String&amp;gt; stringProducerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        // Kafka 서버 위치
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);

        // key, value를 문자열로 직렬화
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(props);
    }

    // 2) Producer가 사용할 KafkaTemplate
    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; stringKafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(stringProducerFactory());
    }

    // 3) Consumer 설정 (String, String)
    @Bean
    public ConsumerFactory&amp;lt;String, String&amp;gt; stringConsumerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        // Kafka 서버 위치
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);

        // 이 Consumer가 속할 그룹 아이디
        props.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;simple-group&quot;);

        // key, value를 문자열로 역직렬화
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        // 처음 시작할 때, 토픽 맨 앞(earliest)부터 읽도록 설정
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props);
    }

    // 4) @KafkaListener가 사용할 ListenerContainerFactory
    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; stringKafkaListenerContainerFactory() {

        ConcurrentKafkaListenerContainerFactory&amp;lt;String, String&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();      
        // 3) 에서 만든 consumer 등록 !
        factory.setConsumerFactory(stringConsumerFactory());
        
        return factory;
    }
}&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Producer 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ProducerFactory&amp;lt;String, String&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer가 사용할 설정 정보를 담는다&lt;/li&gt;
&lt;li&gt;Key, Value 모두 문자로 직렬화하기 위해 StringSerializer를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;KafkaTemplate&amp;lt;String, String&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer 대신 실제로 send()를 호출하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;Kafka와 Spring boot가 통신하는 객체&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer 설정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConsumerFactory&amp;lt;String, String&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer가 어떤 방식으로 메시지를 읽을지 설정&lt;/li&gt;
&lt;li&gt;Key, Value를 문자열로 역질렬화 하기 위해 StringDesrializer를 사용&lt;/li&gt;
&lt;li&gt;Group_ID_CONFIG = &quot;simple-group&quot;&lt;br /&gt;&amp;rarr; 이 컨슈머가 속한 그룹 이름&lt;/li&gt;
&lt;li&gt;AUTO_OFFSET_RESET_CONFIG = &quot;earliest&quot;&lt;br /&gt;&amp;rarr; 처음 실행 시, 토픽에 쌓여 있던 예전 메시지까지 모두 읽음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ConCurrnetKafkaListenerContainerFactory&amp;lt;String, String&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@KafkaListener가 내부적으로 사용할 컨테이너를 생성해주는 팩토리&lt;/li&gt;
&lt;li&gt;containerFactory 이름으로 @KafkaListener에 연결해서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ 객체를 이벤트로 쓰기 : SimpleEvent 설계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순한 문자열을 Kafka에 저장하고 소비하는 방식이 아닌 객체(DTO)를 Kafka를 통해 주고받을 수 있는 과정 또한 경험해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;SimpleEvent 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767785048299&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SimpleEvent {

    // 실제 메시지 내용
    private String message;
    
    // 작업자
    private String worker;

    // 이벤트 생성 시각
    private LocalDateTime createdAt;
}&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;이러한 객체를 받아주기 위한 Producer 설정도 진행해야 하는데, 방식은 크게 다르지 않았다.&lt;/p&gt;
&lt;pre id=&quot;code_1767785082086&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// KafkaConfig.java 내부

import com.example.kafka.event.SimpleEvent;
import org.springframework.kafka.support.serializer.JsonSerializer;

// ...

    // Producer 설정 (String, SimpleEvent) - JSON 직렬화
    @Bean
    public ProducerFactory&amp;lt;String, SimpleEvent&amp;gt; eventProducerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);

        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(props);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, SimpleEvent&amp;gt; eventKafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(eventProducerFactory());
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ProducerFactory&amp;lt;String, SimpleEvent&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key는 String, value는 SimpleEvent 타입으로 설정하고&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;JsonSerializer
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SimpleEvent 객체를 JSON 문자열로 변환하여 Kafka에 전송&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;SimpleEvent용 Consumer 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767785173132&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// KafkaConfig.java 내부

import org.springframework.kafka.support.serializer.JsonDeserializer;

// ...

    // Consumer 설정 (String, SimpleEvent) - JSON 역직렬화
    @Bean
    public ConsumerFactory&amp;lt;String, SimpleEvent&amp;gt; eventConsumerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;simple-event-group&quot;);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, &quot;earliest&quot;);

        // SimpleEvent 타입을 역직렬화하기 위한 JsonDeserializer 생성
        JsonDeserializer&amp;lt;SimpleEvent&amp;gt; deserializer = new JsonDeserializer&amp;lt;&amp;gt;(SimpleEvent.class);

        // 역직렬화를 허용할 패키지 지정 (보안 및 타입 안전성)
        // !!! 개인마다 패키지 경로가 다를수있습니다. !!!
        // SimpleEvent.class가 있는 위치를 넣어주시면 됩니다.
        deserializer.addTrustedPackages(&quot;com.example.kafka.event&quot;);

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(
            props,
            new StringDeserializer(),
            deserializer
        );
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, SimpleEvent&amp;gt; eventKafkaListenerContainerFactory() {

        ConcurrentKafkaListenerContainerFactory&amp;lt;String, SimpleEvent&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();

        factory.setConsumerFactory(eventConsumerFactory());
        return factory;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JsonDeserializer&amp;lt;SimpleEvent&amp;gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에서 읽어온 JSON 데이터를 SimpleEvent 객체로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;addTrustPackages(&quot;com.example.kafka.event&quot;)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패키지에 있는 클래스는 신뢰할 수 있다고 지정해주고&lt;/li&gt;
&lt;li&gt;역직렬화 시 임의의 타입으로 변환하는 것을 방지하기 위한 보안 장치이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;eventKafkaListenerContainerFactory
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@KafkaListener( containerFactory = &quot;eventKafkaListenerContainerFactory&quot;)에서 사용&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;Config 구현 이후 Producer , Listener는 아래와 같이 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;SimpleEvent Producer&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767785367823&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SimpleEventProducer {

    private static final String TOPIC = &quot;simple-events&quot;;

    private final KafkaTemplate&amp;lt;String, SimpleEvent&amp;gt; eventKafkaTemplate;

    public void send(SimpleEvent event) {
        eventKafkaTemplate.send(TOPIC, event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KafkaTemplate&amp;lt;String, SimpleEvent&amp;gt; 타입을 주입받는다&lt;/li&gt;
&lt;li&gt;send() 메서드에 SimpleEvent 객체를 그대로 넘긴다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON 변환은 JsonSerializer가 자동으로 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;토픽 이름은 simple-events로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;SimpleEventListener&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767785444846&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class SimpleEventListener {

    @KafkaListener(
        topics = &quot;simple-events&quot;,
        groupId = &quot;simple-event-group&quot;,
        containerFactory = &quot;eventKafkaListenerContainerFactory&quot;
    )
    public void consume(SimpleEvent event) {
        log.info(&quot;받은 이벤트: {}&quot;, event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;topics = &quot;simple-evenets&quot;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SimpleEventProducer 에서 사용한 토픽과 동일한 토픽을 구독&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;groupId = &quot;simple-event-group&quot;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KafkaConfig의 eventConsumerFactory에서 사용한 그룹 아이디와 동일해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;containerFactory = &quot;eventKafkaListenerContainerFactory&quot;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KafkaConfig에 정의한 이벤트용 ListenerContainerFactory를 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메서드 파라미터 타입이 SimpleEvent이다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer는 JSON 문자열을 직접 다루지 않는다&lt;/li&gt;
&lt;li&gt;JsonDeserializer&amp;lt;SimpleEvent&amp;gt;가 JSON &amp;rarr; SimpleEvent 변환을 자동으로 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/백엔드 기초</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/107</guid>
      <comments>https://stark77.tistory.com/107#entry107comment</comments>
      <pubDate>Wed, 7 Jan 2026 20:32:53 +0900</pubDate>
    </item>
    <item>
      <title>[개념정리] Kafka 클러스터 구조 이해하기</title>
      <link>https://stark77.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 처리에 용이한 Kafka의 브로커, 소비자 그룹에 대한 이해를 기반으로 마지막으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터 구조에 대한 이해를 갖춰 나아가려고 한다.&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;b&gt;☑️ 단일 브로커 구조는 위험성과 한계가 어떻게 될까?&lt;/b&gt;&lt;/h3&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버에 문제가 생기면 Kafka 전체에도 문제가 발생한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 저장할 수 없다.&lt;/li&gt;
&lt;li&gt;메시지를 소비할 수도 없다.&lt;/li&gt;
&lt;li&gt;서비스 전체가 중단되어 심각한 장애를 초래한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;확장이 불가능하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처리량이 늘어나도 단일 서버 한 대가 모든 요청을 감당해야 한다.&lt;/li&gt;
&lt;li&gt;특정 Partition에 트래픽이 몰리면 병목이 쉽게 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 데이터가 한 서버에 쌓여 위험하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스크가 고장나면 메시지 전체가 손실될 위험이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;고가용성(High Availability)이 보장되지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일시적인 네트워크 오류, 서버 재부팅으로도 서비스가 멈추게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실 서비스에서는 여러 브로커를 함께 운영하는 &lt;b&gt;클러스터 구조&lt;/b&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;b&gt;✅ Kafka 클러스터란 무엇인가?&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터는 여러 브로커가 하나의 Kafka처럼 동작하는 구조&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBipr/dJMcahwjZAf/f4KWUVA0oaqBLyAOBMOeWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBipr/dJMcahwjZAf/f4KWUVA0oaqBLyAOBMOeWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBipr/dJMcahwjZAf/f4KWUVA0oaqBLyAOBMOeWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBipr%2FdJMcahwjZAf%2Ff4KWUVA0oaqBLyAOBMOeWK%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;600&quot; height=&quot;482&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 이처럼 여러 브로커를 하나의 클러스터로 통합하여 다음과 같은 기능을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition을 여러 브로커에 분산해 저장하여 병렬 처리 성능을 확장할 수 있음&lt;/li&gt;
&lt;li&gt;장애가 발생하면 다른 브로커가 역할을 자동으로 인계 받아 전체 시스템의 고가용성(High Availability)을 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ Partition 분산 저장 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 Topic 내부 Partition은 여러 브로커에 나누어 저장된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ltH4T/dJMcagqDP2x/87Yuz3ItwEMLoS10hDHmOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ltH4T/dJMcagqDP2x/87Yuz3ItwEMLoS10hDHmOk/img.png&quot; data-alt=&quot;단일 브로커의 형태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ltH4T/dJMcagqDP2x/87Yuz3ItwEMLoS10hDHmOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FltH4T%2FdJMcagqDP2x%2F87Yuz3ItwEMLoS10hDHmOk%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;600&quot; height=&quot;160&quot; data-origin-width=&quot;682&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;단일 브로커의 형태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFGTGx/dJMcacofiBg/Shd2aawaqni7iqFNbbRsiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFGTGx/dJMcacofiBg/Shd2aawaqni7iqFNbbRsiK/img.png&quot; data-alt=&quot;클러스터 구조의 형태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFGTGx/dJMcacofiBg/Shd2aawaqni7iqFNbbRsiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFGTGx%2FdJMcacofiBg%2FShd2aawaqni7iqFNbbRsiK%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;600&quot; height=&quot;517&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클러스터 구조의 형태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition 분산 저장 구조를 통해 3가지의 장점을 가질 수 있게 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;부하가 분산된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 서버가 모든 메시지를 처리하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;병렬 처리(Parellelism)가 자연스럽게 증가한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Partition 개수가 곧 병렬 처리 상한선이기 때문에 병렬 처리가 자연스럽게 증가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;특정 브로커 장애에도 전체 시스템이 멈추지 않는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Replica 구조 덕분에 특정 브로커 장애에도 시스템이 멈추지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ Replica(복제본) 구조&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition은 원본만 존재하는 것이 아니라, Kafka는 각 Partition의 복제본(replica)을 다른 브로커에 저장한다.&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;812&quot; data-origin-height=&quot;695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DzWLl/dJMcahiMY1b/NC5Srb4ifvAlpli3QEPw40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DzWLl/dJMcahiMY1b/NC5Srb4ifvAlpli3QEPw40/img.png&quot; data-alt=&quot;빨강 &amp;amp;rarr; Leader / 청록 &amp;amp;rarr; Follower&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DzWLl/dJMcahiMY1b/NC5Srb4ifvAlpli3QEPw40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDzWLl%2FdJMcahiMY1b%2FNC5Srb4ifvAlpli3QEPw40%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;600&quot; height=&quot;514&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;695&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;빨강 &amp;rarr; Leader / 청록 &amp;rarr; Follower&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Leader / Follower 역할&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Leader&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer와 Consumer가 실제로 통신하는 대상으로, Leader와만 통신한다&lt;/li&gt;
&lt;li&gt;모든 read/write 작업을 Leader가 처리한다.&lt;/li&gt;
&lt;li&gt;장애가 발생하면 새로운 리더가 자동으로 선출된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Follower&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Leader의 데이터를 그대로 복제&lt;/li&gt;
&lt;li&gt;Leader 장애 발생 시 Follower가 자동으로 Leader로 승격된다 (Failover)&lt;/li&gt;
&lt;li&gt;Follower에서 장애가 발생한 경우에는 장애가 복구된 이후부터 Leader의 데이터를 다시 저장하기 시작한다. (동기화)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;복제의 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Broker 1대가 죽어도 Topic이 사라지지 않는다.&lt;/li&gt;
&lt;li&gt;Producer와 Consumer는 계속 정상적으로 동작한다.&lt;/li&gt;
&lt;li&gt;데이터 안전성이 크게 증가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;✅ Controller 역할&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 클러스터에서는 누가 Leader가 될지, 어떤 브로커가 살아있는지, 파티션을 어디에 저장할지에 대한 총괄 관리자가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;컨트롤러(Controller)가 하는 일&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 브로커가 살아 있는지 확인한다.&lt;/li&gt;
&lt;li&gt;어떤 파티션의 Leader는 누구인지 관리한다.&lt;/li&gt;
&lt;li&gt;Failover 시 새로운 Leader 선출을 진행한다.&lt;/li&gt;
&lt;li&gt;Topic 정보, Partition 정보 등 Metadata를 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Kafka는 이처럼 Controller를 중앙 두뇌처럼 활용한다.&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;835&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baUMOK/dJMcabXbpYu/E9WDz5ocgwZbJ7RKSoeHAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baUMOK/dJMcabXbpYu/E9WDz5ocgwZbJ7RKSoeHAK/img.png&quot; data-alt=&quot;장애가 발생하여 새로운 Leader가 선출된 Kafka Cluster&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baUMOK/dJMcabXbpYu/E9WDz5ocgwZbJ7RKSoeHAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaUMOK%2FdJMcabXbpYu%2FE9WDz5ocgwZbJ7RKSoeHAK%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;600&quot; height=&quot;489&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;장애가 발생하여 새로운 Leader가 선출된 Kafka Cluster&lt;/figcaption&gt;
&lt;/figure&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;b&gt;✅ KRaft Controller&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 3.x 부터 Kafka는 자체적으로 메타데이터를 관리하는 구조를 도입했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식이 KRaft(Kafka Raft Metadata Mode)라고 부름&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;KRaft의 역할&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Controller의 기능을 Kafka 내부에 직접 포함한다.&lt;/li&gt;
&lt;li&gt;클러스터 메타데이터를 Metadata Log 형태로 Kafka 내부에 저장한다.&lt;/li&gt;
&lt;li&gt;Raft 합의 알고리즘 기반으로 안정적인 Leader 선출을 지원한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해 KRaft는 Kafka Cluster를 안정적으로 운영하기 위한 내부 조율 시스템이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Zookeeper 구조와의 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 2.x 버전까지는 Controller의 역할을 Kafka 가 아닌 Zookeeper가 수행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 메타데이터의 저장과 클러스터 조율 기능이 Zookeeper에 의존하는 구조였다는 것.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Zookeeper의 역할&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Broker 정보 관리&lt;/li&gt;
&lt;li&gt;Topic alc Partition 메타데이터 저장&lt;/li&gt;
&lt;li&gt;Partition Leader 선출&lt;/li&gt;
&lt;li&gt;브로커 살아있음 체크 (heartbeat)&lt;/li&gt;
&lt;li&gt;클러스터 전체 상태 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Zookeeper는 Kafka 클러스터의 뇌 역할을 수행했다.&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;하지만 Zookeeper 기반 구조의 문제는&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;운영해야 하는 시스템이 Kafka와 Zookeeper 2개로 운영 복잡도 증가&lt;/li&gt;
&lt;li&gt;Zookeeper 장애 시 Kafka도 직접적인 영향을 받음&lt;/li&gt;
&lt;li&gt;높은 동시성 환경에서는 Zookeeper가 메타데이터 업데이트 병목이 될 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제점을 해결하기 위해 위에서 서술한 KRaft가 등장하게 된 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/백엔드 기초</category>
      <author>stark77</author>
      <guid isPermaLink="true">https://stark77.tistory.com/106</guid>
      <comments>https://stark77.tistory.com/106#entry106comment</comments>
      <pubDate>Mon, 5 Jan 2026 15:26:42 +0900</pubDate>
    </item>
  </channel>
</rss>