<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>1im_chaereong.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 01 Feb 2026 10:59:58 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>1im_chaereong.log</title>
            <url>https://velog.velcdn.com/images/1im_chaereong/profile/c0b910fb-4722-45bd-afea-0ef1f525c4c8/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 1im_chaereong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/1im_chaereong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[SCRAM Authentication]]></title>
            <link>https://velog.io/@1im_chaereong/SCRAM-Authentication</link>
            <guid>https://velog.io/@1im_chaereong/SCRAM-Authentication</guid>
            <pubDate>Sun, 01 Feb 2026 10:59:58 GMT</pubDate>
            <description><![CDATA[<h3 id="kafka에서-자주-쓰는-scram-인증이란">Kafka에서 자주 쓰는 SCRAM 인증이란?</h3>
<p>Kafka 보안 설정을 보다 보면 가장 자주 마주치는 인증 방식이 있다.<br>바로 <strong>SASL/SCRAM</strong> 이다.</p>
<p>처음 보면 용어도 어렵고,<br>“비밀번호를 안 보낸다는데 그럼 어떻게 인증을 하지?”라는 의문이 든다.</p>
<p>이 글에서는</p>
<ol>
<li>Kafka에서 SCRAM이 무엇인지 간단히 소개하고  </li>
<li>실제 인증 흐름이 어떻게 동작하는지  </li>
<li>왜 Kafka 같은 서버 간 통신에서 SCRAM이 많이 쓰이는지  </li>
</ol>
<p>를 <strong>흐름 중심으로</strong> 정리해본다.</p>
<h3 id="saslscram-이란">SASL/SCRAM 이란?</h3>
<p><strong>SASL (Simple Authentication and Security Layer)</strong></p>
<ul>
<li>인증을 위한 <strong>프레임워크</strong></li>
<li>실제 인증 방식은 플러그인처럼 붙는다</li>
</ul>
<p><strong>SCRAM (Salted Challenge Response Authentication Mechanism)</strong></p>
<ul>
<li><strong>비밀번호 기반 인증 메커니즘</strong></li>
<li>핵심 특징<ul>
<li>비밀번호를 네트워크로 보내지 않음</li>
<li>Salt + 반복 해시</li>
<li>Challenge–Response 방식</li>
</ul>
</li>
</ul>
<p>즉,</p>
<blockquote>
<p><strong>SASL = 인증 틀</strong><br><strong>SCRAM = 그 안에서 동작하는 실제 인증 방식</strong></p>
</blockquote>
<p>Kafka에서는 보통 다음을 사용한다.</p>
<ul>
<li><code>SCRAM-SHA-256</code></li>
<li><code>SCRAM-SHA-512</code></li>
</ul>
<h3 id="2-kafka에서-scram이-자주-쓰이는-이유">2. Kafka에서 SCRAM이 자주 쓰이는 이유</h3>
<p>:contentReference[oaicite:0]{index=0} 는<br>사람이 로그인하는 시스템이 아니다.</p>
<p>Kafka 환경을 보면 보통 이런 구조다.</p>
<pre><code>
Producer  →  Broker  ←  Consumer
↑           ↑
Connect     Stream App
</code></pre><p>특징은 다음과 같다.</p>
<ul>
<li>수많은 <strong>프로세스</strong>가 자동으로 접속</li>
<li>로그인 UI 없음</li>
<li>항상 켜져 있음</li>
<li>내부 통신 비중이 큼</li>
</ul>
<p>Kafka가 인증 방식에 요구하는 조건은 대략 이렇다.</p>
<ul>
<li>자동 인증 가능</li>
<li>비밀번호 노출 위험 최소화</li>
<li>외부 인증 서버 없이 동작</li>
<li>성능 예측 가능</li>
</ul>
<p>이 조건에 가장 잘 맞는 방식이 <strong>SCRAM</strong>이다.</p>
<h3 id="3-scram에서-말하는-client--server">3. SCRAM에서 말하는 Client / Server</h3>
<p>SCRAM에서의 Client / Server는 <strong>역할 개념</strong>이다.</p>
<ul>
<li><strong>Client</strong><ul>
<li>인증을 요청하는 쪽</li>
<li>비밀번호를 알고 있음</li>
</ul>
</li>
<li><strong>Server</strong><ul>
<li>인증을 검증하는 쪽</li>
<li>비밀번호 원문은 없음</li>
</ul>
</li>
</ul>
<p>Kafka 기준으로 보면</p>
<ul>
<li>Client → Kafka Producer / Consumer</li>
<li>Server → Kafka Broker</li>
</ul>
<p>이 정도가 되겠다.</p>
<h3 id="4-scram-인증-흐름-핵심">4. SCRAM 인증 흐름 (핵심)</h3>
<p>아래는 <strong>Kafka Client가 Broker에 처음 연결할 때</strong> 일어나는 인증 흐름이다.</p>
<p><strong>0단계: 사전 상태</strong></p>
<h4 id="서버kafka-broker">서버(Kafka Broker)</h4>
<p>사용자 생성 시점에 이미 다음 정보를 저장하고 있다.</p>
<ul>
<li>salt</li>
<li>iterationCount</li>
<li>StoredKey</li>
<li>ServerKey</li>
</ul>
<p>비밀번호 원문은 저장하지 않는다.</p>
<h4 id="클라이언트kafka-client">클라이언트(Kafka Client)</h4>
<ul>
<li>설정 파일에 <code>username / password</code>를 가지고 있음</li>
</ul>
<p><strong>1단계: 인증 시작</strong></p>
<p><strong>Client → Server</strong></p>
<pre><code>
username
clientNonce
</code></pre><ul>
<li>비밀번호는 보내지 않는다</li>
<li>“이 사용자로 인증하고 싶다”는 신호</li>
</ul>
<p><strong>2단계: 서버의 Challenge</strong></p>
<p><strong>Server → Client</strong></p>
<pre><code>
salt
iterationCount
serverNonce (clientNonce 포함)
</code></pre><p>의미는 이렇다.</p>
<blockquote>
<p>“이 salt와 반복 횟수로 계산해서<br>네가 진짜인지 증명해봐”</p>
</blockquote>
<p><strong>3단계: 클라이언트 내부 계산</strong></p>
<p>이 단계는 <strong>네트워크로 전송되지 않는다</strong>.</p>
<p>클라이언트는 본인이 가진 비밀번호로 다음을 수행한다.</p>
<ol>
<li>password + salt</li>
<li>iterationCount 만큼 반복 해시</li>
<li>ClientKey 생성</li>
<li>StoredKey 생성</li>
<li>인증용 메시지(AuthMessage) 구성</li>
<li>ClientProof 생성</li>
</ol>
<p><strong>비밀번호를 알고 있는 쪽만 만들 수 있는 값</strong>이 생성된다.</p>
<p><strong>4단계: 증명 제출</strong></p>
<p><strong>Client → Server</strong></p>
<pre><code>
ClientProof
</code></pre><p>이 값은</p>
<ul>
<li>비밀번호 자체가 아니고</li>
<li>이번 인증 세션에서만 유효한 증명값이다</li>
</ul>
<p><strong>5단계: 서버 검증 (핵심 로직)</strong></p>
<p>서버는 클라이언트가 보낸 <code>ClientProof</code>를 그대로 믿지 않는다.<br>이미 서버는 <strong>검증에 필요한 기준값</strong>을 모두 가지고 있기 때문이다.</p>
<p>서버가 인증 시점에 가지고 있는 정보는 다음 세 가지다.</p>
<ul>
<li><code>StoredKey</code>  <ul>
<li>사용자 생성 시점에 DB에 저장된 값  </li>
<li>비밀번호로부터 파생된 <strong>검증 기준값</strong></li>
</ul>
</li>
<li><code>AuthMessage</code>  <ul>
<li>클라이언트와 서버가 동일하게 알고 있는 인증용 메시지</li>
</ul>
</li>
<li><code>ClientProof</code>  <ul>
<li>방금 클라이언트가 제출한 증명값</li>
</ul>
</li>
</ul>
<p>서버는 이 값들을 이용해 다음 과정을 수행한다.</p>
<h4 id="1-clientkey-복원">1) ClientKey 복원</h4>
<p>SCRAM에서 클라이언트는 <code>ClientKey</code> 자체를 보내지 않는다.<br>대신 <code>ClientProof</code>라는 값을 보낸다.</p>
<p>서버는 다음 연산을 통해 <strong>ClientKey를 역으로 복원</strong>한다.</p>
<p>ClientKey = ClientProof XOR hash(StoredKey + AuthMessage)</p>
<p>이 연산이 가능한 이유는 다음과 같다.</p>
<ul>
<li>서버는 <code>StoredKey</code>를 이미 DB에 저장하고 있고</li>
<li><code>AuthMessage</code> 역시 서버와 클라이언트가 동일하게 알고 있기 때문이다</li>
</ul>
<p>즉, <strong>비밀번호를 몰라도 ClientKey를 복원할 수 있는 조건</strong>이 이미 갖춰져 있다.</p>
<h4 id="2-storedkey-검증">2) StoredKey 검증</h4>
<p>서버는 복원한 <code>ClientKey</code>를 다시 해시한다.</p>
<p>hash(ClientKey)</p>
<p>그리고 그 결과를 DB에 저장된 <code>StoredKey</code>와 비교한다.</p>
<p>hash(ClientKey) == StoredKey ?</p>
<h4 id="3-검증-결과-판단">3) 검증 결과 판단</h4>
<ul>
<li>같으면<br>→ 이 <code>ClientProof</code>는 <strong>올바른 비밀번호로부터 생성되었다</strong><br>→ 즉, <strong>비밀번호를 알고 있는 클라이언트다</strong></li>
<li>다르면<br>→ 잘못된 비밀번호이거나 위조된 요청이다<br>→ 인증 실패</li>
</ul>
<p>이 과정에서 서버는 <strong>비밀번호 원문을 한 번도 사용하지 않는다</strong>.  
오직 이미 저장된 검증용 값(<code>StoredKey</code>)과<br>클라이언트가 제출한 증명값(<code>ClientProof</code>)만을 이용해 판단한다.</p>
<h4 id="핵심-정리">핵심 정리</h4>
<ul>
<li>클라이언트는 비밀번호를 <strong>제출하지 않는다</strong></li>
<li>서버는 비밀번호를 <strong>알 필요가 없다</strong></li>
<li>서버는  <blockquote>
<p>“이 증명값이 내가 알고 있는 StoredKey로부터 나올 수 있는 값인가?”<br>만 확인한다</p>
</blockquote>
</li>
</ul>
<p><strong>6단계: 서버도 자기 자신을 증명</strong></p>
<p><strong>Server → Client</strong></p>
<pre><code>
ServerSignature
</code></pre><p>클라이언트는 이를 검증해</p>
<ul>
<li>가짜 서버(MITM)가 아님을 확인한다</li>
</ul>
<p><strong>7단계: 인증 완료</strong></p>
<ul>
<li>클라이언트 ↔ 서버 상호 신뢰 성립</li>
<li>Kafka 연결 유지</li>
</ul>
<h3 id="5-이-흐름의-핵심-포인트">5. 이 흐름의 핵심 포인트</h3>
<ul>
<li>비밀번호는 <strong>단 한 번도 네트워크로 전송되지 않는다</strong></li>
<li>인증은<pre><code></code></pre></li>
</ul>
<p>요청 → 도전 → 증명 → 검증</p>
<p>```
구조다</p>
<ul>
<li>ClientProof는 재사용할 수 없다 (nonce 포함)</li>
<li>서버 DB가 털려도 비밀번호는 알 수 없다</li>
</ul>
<h3 id="6-왜-실사용자-로그인에는-잘-안-쓰일까">6. 왜 실사용자 로그인에는 잘 안 쓰일까?</h3>
<p>SCRAM은 다음 전제를 가진다.</p>
<blockquote>
<p>“클라이언트가 비밀번호를 직접 알고 있다”</p>
</blockquote>
<p>이 전제는</p>
<ul>
<li>브라우저 로그인</li>
<li>모바일 앱 로그인</li>
<li>SSO</li>
<li>토큰 기반 인증</li>
</ul>
<p>과는 잘 맞지 않는다.</p>
<p>SCRAM은</p>
<ul>
<li>세션 관리 불가능</li>
<li>토큰 없음</li>
<li>권한 전달 불가능</li>
</ul>
<p>그래서 <strong>사람 로그인</strong>에는 OAuth/OIDC,<br><strong>서버 간 인증</strong>에는 SCRAM이 주로 쓰인다.</p>
<h3 id="7-한-줄-요약">7. 한 줄 요약</h3>
<blockquote>
<p><strong>Kafka에서 SCRAM은<br>사람이 로그인하는 인증이 아니라<br>프로세스가 프로세스를 신뢰하기 위한 인증 방식이다.</strong></p>
</blockquote>
<p>Kafka, DB, 내부 메시징 시스템에서<br>SCRAM이 오래 살아남는 이유도 여기에 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Windowing & Aggregation 완전정복]]></title>
            <link>https://velog.io/@1im_chaereong/Windowing-Aggregation-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5</link>
            <guid>https://velog.io/@1im_chaereong/Windowing-Aggregation-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5</guid>
            <pubDate>Sun, 07 Dec 2025 14:24:57 GMT</pubDate>
            <description><![CDATA[<p>실시간 스트림 분석의 진정한 핵심은 바로 <strong>Windowing + Aggregation</strong>이다.
Stream Processing의 난이도는 대부분 “시간 기반 계산”에서 나온다.
예를 들어 아래 같은 정보는 모두 윈도우 계산 없이는 불가능하다.</p>
<ul>
<li>최근 1분간 오류율</li>
<li>최근 10초 동안 평균 응답 시간</li>
<li>최근 5분간 로그인 실패 증가 추세</li>
<li>IP별 30초 로그인 실패 횟수</li>
<li>P95, P99 반응 시간 지표</li>
</ul>
<p>이 글에서는 Stream Processing의 꽃이라 불리는 <strong>Tumbling / Sliding / Session Window</strong>, 그리고 Event time, Watermark, Out-of-order 처리까지 실무에서 반드시 알아야 하는 개념만 집중적으로 정리한다.</p>
<h1 id="1-window-종류-tumbling--sliding--session">1. Window 종류: Tumbling / Sliding / Session</h1>
<p>스트림 윈도우는 “시간을 잘라서 처리하는 기법”이다.
Batch는 모아서 처리하지만, Stream은 들어오자마자 처리하므로 기간을 정의해야 계산이 가능하다.
스트림 엔진은 아래 세가지를 기본적으로 제공한다.</p>
<h2 id="1-tumbling-window">1) Tumbling Window</h2>
<ul>
<li>겹치지 않는 고정 크기(window-size)의 윈도우</li>
<li>예: 1분 단위 정확한 집계</li>
<li>00:00<del>00:01 / 00:01</del>00:02 이런 식으로 딱딱 끊김</li>
</ul>
<p><strong>내부 동작</strong></p>
<ol>
<li>이벤트는 event-time 또는 processing-time 기반으로 특정 구간에 매핑됨</li>
<li>윈도우 끝나는 시점에 집계 결과를 emit</li>
<li>Late Event는 allowed lateness 정책에 따라 처리되거나 버려짐</li>
</ol>
<p><strong>장점</strong></p>
<ul>
<li>구현 간단</li>
<li>정확한 시간 단위 집계(1분 단위 오류율 등)에 적합</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>구간이 고정이라 실시간 감지에는 반응성이 떨어짐</li>
</ul>
<p><strong>사용 예</strong></p>
<ul>
<li>1분 오류율 보고</li>
<li>10초 평균 응답 시간 계산</li>
<li>TTL 처리 용이</li>
</ul>
<p><strong>실제 예시</strong>
00:00~00:10 동안 1400개의 요청 → throughput = 140 req/sec</p>
<h2 id="2-sliding-window">2) Sliding Window</h2>
<ul>
<li>일정 간격(step)으로 움직이는 윈도우</li>
<li>겹치는 부분 존재</li>
<li>예: window-size=1분, slide=5초</li>
</ul>
<p>즉, <strong>매 5초마다 최근 1분 데이터를 계산</strong>하는 구조
<strong>왜 중요한가?</strong>
장애 감지는 대부분 “Sliding Window 기반”이다.</p>
<p><strong>내부 동작</strong></p>
<p>예: window=60초, slide=5초</p>
<ul>
<li><p>00:00~01:00</p>
</li>
<li><p>00:05~01:05</p>
</li>
<li><p>00:10~01:10</p>
<p>  … 이런 식으로 계속 마지막 1분 데이터를 재계산한다.</p>
</li>
</ul>
<p>Stream Processor는 각 윈도우마다</p>
<ul>
<li>부분 집계(state)를 유지하고</li>
<li>slide가 발생할 때마다 결과물만 계산해 emit한다.</li>
</ul>
<p>예시
30초 응답 오류율이 10% 이상 증가하면 알림
→ 이건 Tumbling Window로는 실시간성이 부족하다.</p>
<p><strong>사용 예</strong></p>
<ul>
<li>실시간 장애 감지</li>
<li>실시간 지연율 증가 탐지</li>
<li>5초 단위 rolling average</li>
</ul>
<h2 id="3-session-window">3) Session Window</h2>
<ul>
<li>Tumbling/Sliding은 시간중심, Session Window는 사용자 행동 중심</li>
<li>사용자/세션의 비활동 시간(gap)을 기준으로 window가 결정됨</li>
<li>예: 30초 동안 새로운 이벤트가 없으면 session 종료</li>
</ul>
<p><strong>내부 동작</strong></p>
<ol>
<li>동일 Key(userId)의 이벤트들이 들어오면 세션을 열고 이벤트를 추가</li>
<li>이벤트가 일정 시간(gap) 동안 없으면 윈도우 종료</li>
<li>세션 별로 전체 행동 집계(클릭 수, 체류 시간, 평균 등)를 출력</li>
<li>Late Event는 해당 세션이 이미 닫혔는지에 따라 처리 여부 결정</li>
</ol>
<p><strong>사용 예</strong></p>
<ul>
<li>사용자 활동 패턴 분석</li>
<li>로그인 후 행동 분석</li>
<li>장바구니 세션 분석</li>
</ul>
<p>예시
사용자가 10초, 5초 간격으로 이벤트를 발생시키다가 마지막 이벤트 이후 40초 동안 아무 입력이 없으면 그 40초 구간을 포함하는 세션 윈도우가 끝남</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/a568d152-902c-4345-a688-6dcd8999bbfd/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Tumbling Window는 5분 단위로 구간이 딱딱 끊어져 서로 겹치지 않는 방식이다.</li>
<li>Sliding Window는 크기는 10분이지만 5분마다 이동하며 겹치는 구간을 계속 계산한다.</li>
<li>그래서 Sliding은 더 촘촘하게 “최근 데이터”를 분석할 수 있다.</li>
<li>Session Window는 시간 기반이 아니라 이벤트 사이의 간격(gap 5분)으로 윈도우가 자동으로 닫힌다.</li>
<li>즉, Tumbling은 고정·비중첩, Sliding은 고정·중첩, Session은 동적 윈도우 구조를 시각적으로 잘 보여주는 그림이다.</li>
</ul>
<h1 id="2-event-time-vs-processing-time">2. Event Time vs Processing Time</h1>
<p>Stream Processing 시스템(Flink, Kafka Streams, Spark Streaming)은 “시간”을 기준으로 집계, 윈도우, 트리거, watermark 등을 처리한다.
Stream Processing에서 <strong>시간이 무엇인지</strong>를 명확히 정의해야 한다.
그 모델이 바로
Event Time (이벤트 발생 시간)
Processing Time (처리 시간)
이다.</p>
<h2 id="processing-time">Processing Time</h2>
<p>스트림 엔진이 이벤트를 읽는 순간의 시간</p>
<ul>
<li>Consumer가 “지금 시계” 기준으로 처리하는 시간</li>
<li>시스템 처리 속도에 따라 변동 가능</li>
<li>지연, 네트워크 장애에 취약</li>
</ul>
<p>예시
12:00에 처리하고 싶었는데 네트워크 지연 때문에 12:04에 도착
→ Processing Time 12:04로 집계됨
→ 오류율 왜곡 가능</p>
<h2 id="event-time">Event Time</h2>
<p>데이터 내부에 기록된 timestamp를 기준으로 스트림을 처리하는 시간 모델</p>
<ul>
<li>이벤트가 “실제로 발생한 시간(timestamp)”</li>
<li>로그 자체에 들어 있는 timestamp 기준</li>
<li>스트림 분석에서 거의 표준 방식</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>지연, 재전송, 네트워크 문제와 무관하게 정확한 집계 가능</li>
</ul>
<p>왜 Event Time이 중요한가?
실시간 장애 분석에서 정확한 타이밍은 필수다.</p>
<p>예시
실제 오류는 12:00<del>12:01에 폭증했는데 로그는 12:03</del>12:04에 들어왔다고 가정하자.
Processing Time 기준 → 12:03에 장애로 판단 (틀림)
Event Time 기준 → 12:00에 장애로 판단 (정확)</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/3f7595cb-cb83-4fd1-8802-8fce92ec5a5a/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>위쪽 축은 이벤트가 <em>실제로 발생한 시간(Event Time)</em> 을 나타낸다.</li>
<li>아래쪽 축은 시스템이 이벤트를 <em>처리한 시간(Processing Time)</em> 을 나타낸다.</li>
<li>Post 2처럼 늦게 도착한 이벤트는 Event Time 순서와 다르게 처리된다.</li>
<li>이런 Out-of-order 상황 때문에 Processing Time만 쓰면 분석 결과가 왜곡된다.</li>
<li>Stream Processing에서 Event Time + Watermark가 필요한 이유를 잘 설명하는 그림이다.</li>
</ul>
<h1 id="3-watermark-개념-out-of-order-해결-핵심">3. Watermark 개념 (Out-of-order 해결 핵심)</h1>
<p>Event Time을 쓰려면 반드시 Watermark를 이해해야 한다.
Event Time 기반 스트리밍을 쓰려면 현실에서 항상 “늦게오는 데이터(Out of order)” 문제와 싸워야한다.
Watermark는 이를 해결하는 핵심 기법이다.</p>
<h2 id="out-of-order-문제란">Out-of-order 문제란?</h2>
<p>네트워크/시스템 지연 때문에 <strong>이벤트가 늦게 도착하거나 순서가 뒤죽박죽으로 들어오는 현상</strong></p>
<p>예시
이 순서로 발생했지만 t=1 → t=3 → t=2 순으로 Kafka에 들어올 수도 있다.</p>
<h2 id="watermark란">Watermark란?</h2>
<p>“이 시점 시간 이전의 이벤트는 더 이상 도착하지 않을 것”이라는 기준선
즉 스트림 시스템이 내부적으로 유지하는 하나의 시간 기준선이다.</p>
<p>Watermark가 12:00:30이라면
→ 12:00:30 이전의 이벤트는 늦게 와도 버리거나 처리 방식을 바꾼다.</p>
<p>Watermark는 다음 문제 해결을 위한 장치다.</p>
<ul>
<li>Out-of-order 이벤트 처리</li>
<li>윈도우 종료 시점 결정</li>
<li>집계 완료 여부 판단</li>
</ul>
<h3 id="그럼-watermark는-왜-필요한가-">그럼 Watermark는 왜 필요한가 ?</h3>
<p>Event Time 윈도우는 “발생 시간 기준”으로 계산된다.
그러나 이벤트가 늦게 들어오면 계산을 언제 끝내야 할지 모르기 때문에 <strong>watermark가 없는 Event Time 윈도우는 영원히 닫히지 않는다.</strong></p>
<h2 id="watermark-설정-전략">Watermark 설정 전략</h2>
<p>보통</p>
<pre><code>watermark = max_event_time - allowed_late</code></pre><p>allowed_late를 크게 잡으면</p>
<p>→ 정확도 ↑
→ 지연도 ↑</p>
<p>allowed_late를 작게 잡으면</p>
<p>→ 빠른 처리
→ 늦게 온 이벤트 처리 불가(정확도 ↓)</p>
<p>실시간 알림 시스템에서는 보통 <strong>1~5초</strong> BI/배치 성격 스트리밍에서는 <strong>10~60초</strong>까지도 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/c0fe6810-895b-4b84-bafb-faf5c9441652/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ol>
<li>위쪽은 Processing Time(실제로 이벤트가 도착한 시간 흐름)이다.</li>
<li>가운데는 각 이벤트의 Event Time(실제로 발생한 시각)이다.</li>
<li>처음 세 개 이벤트는 모두 Watermark보다 과거 시간이라 Watermark가 변하지 않는다.</li>
<li>Watermark는 “이 시점 이전 이벤트는 더 이상 도착하지 않을 것”이라는 기준선이다.</li>
<li>최신 Event Time(오늘 5:10 AM)이 들어오면 Watermark가 그 시점으로 이동한다.</li>
<li>Watermark가 이동하면 이후 도착하는 오래된 이벤트는 늦게 온(out-of-order) 이벤트로 처리된다.</li>
</ol>
<h1 id="4-rolling-aggregation-계속-굴러가는-집계">4. Rolling Aggregation (계속 굴러가는 집계)</h1>
<p>Sliding Window와 함께 가장 많이 사용되는 방식이다.
Rolling Aggregation은 &quot;이전 값이 다음 계산에 계속 누적되는 형태&quot;다.
즉, 일정 시간 범위를 계속 유지하되, 새로운 이벤트가 들어올 때마다 집계 결과를 즉시 갱신하는 방식</p>
<p>예시
최근 1분 평균 값이 매 5초마다 다시 계산됨
즉, “움직이는 평균(moving average)”과 동일하다.</p>
<h3 id="대표-계산들">대표 계산들</h3>
<ul>
<li>rolling average</li>
<li>rolling count</li>
<li>rolling sum</li>
<li>rolling max/min</li>
<li>최근 1분 오류율 (매 3초 계산)</li>
<li>최근 30초 평균 지연 시간</li>
</ul>
<p>이 방식은 실시간 장애 감지에서 거의 필수다.</p>
<h1 id="5-p95-p99-계산-방법">5. P95, P99 계산 방법</h1>
<p>실시간 성능 모니터링에서 가장 중요한 지표가 바로 P95, P99 지연 시간이다.</p>
<h2 id="percentile-계산-방식">Percentile 계산 방식</h2>
<p>P95 = 전체 요청 중 95%가 이 값 이하에 처리됨
P99 = 전체 요청 중 99%가 이 값 이하에 처리됨</p>
<p>예시</p>
<ul>
<li>윈도우 데이터 100개 → P95 = 95번째 윈도우 데이터</li>
<li>윈도우 데이터 100개 → P99 = 99번째 윈도우 데이터</li>
</ul>
<p>즉,
P95 = “상위 5% 느린 요청을 제외한 응답시간”
P99 = “가장 느린 상위 1% 응답시간”</p>
<p>이 지표는 평균보다 훨씬 정확하게 병목을 보여준다.</p>
<h2 id="스트림-환경에서-percentile-계산-방법">스트림 환경에서 Percentile 계산 방법</h2>
<p>전체 데이터를 메모리에 쌓을 수 없으므로 다음 기법을 사용한다.</p>
<ol>
<li><strong>윈도우 내부에서 응답 시간 정렬 후 Percentile 계산</strong></li>
</ol>
<ul>
<li>작은 윈도우(5초, 10초 등)에 적합</li>
</ul>
<ol>
<li><strong>t-digest 알고리즘 사용</strong></li>
</ol>
<ul>
<li>대량 데이터에서 효율적</li>
<li>Spark/Flink 대부분 지원</li>
<li>Kafka Streams는 custom 구현 가능</li>
</ul>
<ol>
<li><strong>Histogram 기반 버킷팅</strong></li>
</ol>
<ul>
<li>응답 시간 범위를 구간(0<del>50ms, 50</del>100ms...)으로 나누고</li>
<li>각 버킷 count를 기반으로 percentile 계산</li>
</ul>
<p>이 방식으로 P95/P99를 실시간 업데이트할 수 있다.</p>
<h1 id="6-에러율-지연율-계산-논리">6. 에러율, 지연율 계산 논리</h1>
<p>장애 감지는 대부분 “최근 일정 구간에서의 비율 변화”로 판단한다.</p>
<h2 id="1-오류율error-rate">1) 오류율(Error Rate)</h2>
<p>최근 30초 Sliding Window 기준</p>
<pre><code>error_rate = error_count / total_count</code></pre><p>실제로는 더 정교하게 계산한다.</p>
<p>예시
최근 30초 오류율이</p>
<ul>
<li>이전 5분 평균 대비 3배 증가 → 알림</li>
<li>절대 기준 5% 이상이면 → 알림</li>
</ul>
<p>이런 규칙 기반 경보(rule-based alert)가 가장 널리 쓰인다.</p>
<h2 id="2-지연율latency-rate">2) 지연율(Latency Rate)</h2>
<p>지연율 = 특정 기준 응답 시간 이상 비율</p>
<p>예: 500ms 이상 비율</p>
<pre><code>latency_rate = slow_count(&gt; 500ms) / total_count</code></pre><p>또는</p>
<ul>
<li><p>P95 &gt; 400ms</p>
</li>
<li><p>P99 &gt; 700ms</p>
<p>  둘 중 하나라도 초과하면 장애 경보</p>
</li>
</ul>
<h1 id="7-ttl-상태-저장state-store">7. TTL, 상태 저장(State Store)</h1>
<p>윈도우 연산에는 상태(state)가 필요하다.</p>
<p>왜 ?</p>
<ul>
<li>스트림은 계속 흐르는 데이터다..</li>
<li>Kafka 같은 스트림은 무한대 데이터가 계속 들어온다.</li>
<li>윈도우 연산인 이런 무한 스트림에서 일부 구간만 잘라서 계산하는 연산인데..</li>
<li>윈도우에 속하는 데이터를 저장해두는 공간(상태)가 필요하다</li>
</ul>
<p>Kafka Streams는 RocksDB 기반 State Store를 사용한다.</p>
<h2 id="state-store가-필요한-이유">State Store가 필요한 이유</h2>
<ul>
<li>윈도우별 카운트 저장</li>
<li>이전 집계 값 유지</li>
<li>세션 윈도우 상태 유지</li>
<li>P95/P99 계산 시 히스토그램 유지</li>
</ul>
<p>Without State Store → 실시간 Aggregation은 불가능하다.</p>
<h2 id="ttltime-to-live">TTL(Time-To-Live)</h2>
<p>State Store에는 TTL이 필요하다.</p>
<p>왜?</p>
<ul>
<li>윈도우가 끝난 뒤 데이터를 무한정 보유하면 메모리 폭발</li>
<li>오래된 상태는 필요 없음</li>
</ul>
<p>예시
1분 윈도우면 TTL = 1분 + allowed-late(5초) 정도가 적당하다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/151599fb-1ecf-4676-abf1-830074418fe2/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Kafka Streams 앱들은 Kafka Topic에서 데이터를 소비하며 각자 로컬 State Store를 유지한다.</li>
<li>State Store는 RocksDB 대신 Cassandra/Scylla 같은 외부 저장소로 확장 가능하다.</li>
<li>HTTP 클라이언트는 특정 Kafka Streams 인스턴스로 직접 RPC를 보내지 않고,</li>
<li>각 Streams 앱이 로컬 또는 외부 State Store에 직접 질의해 결과를 반환한다.</li>
<li>인스턴스 간 RPC를 피함으로써 분산 시스템의 복잡성을 줄인다.</li>
<li>이 구조 덕분에 Streams 애플리케이션이 스케일아웃되면서도 상태 일관성을 유지하고 빠르게 응답할 수 있다.</li>
</ul>
<h1 id="8-out-of-order-문제-해결">8. Out-of-order 문제 해결</h1>
<p>실시간 로그는 절대 순서대로 들어오지 않는다.
네트워크 지연, retry, 로그 수집기 문제 등 다양한 이유가 있다.
이를 해결하려면 3가지가 필요하다.</p>
<h2 id="1-event-time-기반-처리">1) Event Time 기반 처리</h2>
<p>순서가 맞지 않아도 “실제 발생 시간(timestamp)”으로 정렬 가능</p>
<h2 id="2-watermark-사용">2) Watermark 사용</h2>
<p>“이 시점 이전은 더 이상 신규 이벤트 없다”고 선언하여 윈도우 종료 시점을 명확히 한다.</p>
<h2 id="3-late-event-처리-정책">3) Late event 처리 정책</h2>
<p>늦게 도착한 이벤트를 어떻게 처리할지 결정해야 한다.</p>
<p>정책 예시</p>
<ul>
<li>drop: 버리고 집계에 포함하지 않음</li>
<li>update: 이미 끝난 윈도우 값을 수정</li>
<li>side-output: 별도 토픽으로 늦게 도착한 데이터 전송</li>
<li>log only: 장애 원인 추적용 로그만 남김</li>
</ul>
<p>실무에서는 정확성이 중요한 BI 쪽은 update 사용 / 실시간 알림 시스템은 drop 많이 사용한다.</p>
<h1 id="마무리">마무리</h1>
<p>Windowing + Aggregation은 실시간 스트림 분석의 완성이다.
장애 감지, 성능 모니터링, 지연/오류율 분석, 보안 탐지 등 대부분의 실시간 시스템이 Sliding Window와 Aggregation 기반으로 동작한다.</p>
<p>이번 편에서 다룬 주요 개념</p>
<ul>
<li>Tumbling / Sliding / Session Window</li>
<li>Event Time과 Processing Time</li>
<li>Watermark와 Late Event 처리</li>
<li>Rolling Aggregation</li>
<li>P95/P99 계산 방식</li>
<li>오류율/지연율 계산</li>
<li>State Store &amp; TTL</li>
<li>Out-of-order 처리 전략</li>
</ul>
<p>이 내용을 이해하면 Kafka Streams나 Flink에서 <strong>실제 장애 감지 로직을 구현할 수 있는 수준</strong> 정도가 될거라고 생각한다..</p>
<h3 id="참고-문헌">참고 문헌</h3>
<p><a href="https://www.databricks.com/blog/2021/10/12/native-support-of-session-window-in-spark-structured-streaming.html">https://www.databricks.com/blog/2021/10/12/native-support-of-session-window-in-spark-structured-streaming.html</a></p>
<p><a href="https://otee.dev/2021/10/19/event-and-processing-time-semantics.html">https://otee.dev/2021/10/19/event-and-processing-time-semantics.html</a></p>
<p><a href="https://www.gcpstudyhub.com/pages/blog/dataflow-watermarks-and-triggers">https://www.gcpstudyhub.com/pages/blog/dataflow-watermarks-and-triggers</a></p>
<p><a href="https://thriving.dev/blog/interactive-queries-with-kafka-streams-cassandra-state-store">https://thriving.dev/blog/interactive-queries-with-kafka-streams-cassandra-state-store</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka 기반 Stream Processing 핵심 개념]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-%EA%B8%B0%EB%B0%98-Stream-Processing-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-%EA%B8%B0%EB%B0%98-Stream-Processing-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Sun, 07 Dec 2025 11:16:55 GMT</pubDate>
            <description><![CDATA[<p>Kafka는 단순 메시지 큐가 아니라 “분산 스트리밍 플랫폼”이다.
Stream Processing을 구현할 때 Kafka를 중심으로 설계하는 이유는 내부 구조가 실시간 처리에 최적화되어 있기 때문이다.
이번 편에서는 Kafka 기반 스트림 처리의 근간이 되는 Topic, Partition, Group, Exactly-once 등 필수 개념을 <strong>조금 더 깊이</strong> 설명한다.</p>
<p>이러한 대부분은 내 벨로그의 Kafka를 공부하면서 다뤘던 내용들이다.
Stream Processing을 더 깊게 학습하기위해 이번 편에서 복습하는 느낌으로 해보려한다.</p>
<h1 id="1-kafka-topic-모델">1. Kafka Topic 모델</h1>
<p>Kafka Topic은 단순 메시지 큐가 아니라 <strong>append-only 로그의 논리적 묶음</strong>이다.
중요한 특징은 다음과 같다.</p>
<h3 id="1-메시지가-삭제되지-않고-보존된다">1) 메시지가 삭제되지 않고 보존된다</h3>
<p>일반 MQ는 Consumer가 읽으면 메시지가 큐에서 사라지지만, Kafka는 <strong>읽어도 사라지지 않는다.</strong>
Consumer는 단지 “offset 커서만 이동할 뿐”이다.
즉, Kafka는 <strong>읽기/쓰기 모두 시간에 독립적인 구조</strong>다.</p>
<h3 id="2-여러-consumer-그룹이-독립적으로-읽을-수-있다">2) 여러 Consumer 그룹이 독립적으로 읽을 수 있다</h3>
<p>토픽 하나를 서로 다른 애플리케이션이 자유롭게 읽어도 된다.</p>
<p>예시</p>
<ul>
<li>A팀: 실시간 모니터링 시스템</li>
<li>B팀: 배치 분석</li>
<li>C팀: 보안 이상징후 탐지 시스템</li>
</ul>
<p>Kafka는 동일 데이터를 여러 팀이 공유하도록 설계되어 있다.</p>
<h3 id="3-partition-단위-스케일링">3) Partition 단위 스케일링</h3>
<p>Topic 자체는 단순한 논리 개념이며, 실제 스케일링의 기준은 <strong>Partition</strong>이다.</p>
<h1 id="2-partition--offset">2. Partition / Offset</h1>
<p>Partition은 Kafka 성능의 핵심이므로 조금 더 자세하게 설명한다.</p>
<h3 id="1-partition은-순서를-보장하는-세그먼트">1) Partition은 &quot;순서를 보장하는 세그먼트&quot;</h3>
<p>Partition 안에서는 절대 순서가 뒤섞이지 않는다.
이는 &quot;정확한 스트림 처리&quot;에 필수적이다.</p>
<p>예시
사용자 클릭 이벤트가 click_1 → click_2 → click_3 순서로 Partition에 들어가면 Consumer는 반드시 같은 순서로 읽는다.</p>
<h3 id="2-partition이-여러-개면-순서는-보장되지-않는다">2) Partition이 여러 개면 순서는 보장되지 않는다</h3>
<p>Partition이 3개면 Kafka는 Key 해시 기반으로 메시지를 분산한다.
따라서 <strong>전체 Topic 레벨 순서는 없다.</strong>
→ 그래서 Keying 전략이 중요해진다.</p>
<h3 id="3-offset은-읽기-위치">3) Offset은 “읽기 위치”</h3>
<p>Kafka 메시지는 삭제되거나 이동되지 않는다.
그저 offset이 증가할 뿐이며, Consumer가 “어디까지 읽었는지” 기록할 뿐이다.
offset=100에서 장애가 나면 재시작 후 offset=101부터 다시 읽으면 된다.
이 단순하고 강력한 구조가 Kafka 안정성의 기반이다.</p>
<h1 id="3-consumer-group-구조">3. Consumer Group 구조</h1>
<p>Consumer Group은 Kafka를 스트리밍 플랫폼으로 만드는 핵심 메커니즘이다.</p>
<h3 id="1-병렬성을-자동으로-확보한다">1) 병렬성을 자동으로 확보한다</h3>
<p>Partition 개수가 10개라면 최대 10개의 Consumer가 병렬로 처리할 수 있다.
Kafka가 자동으로 partition → consumer 매핑을 설정한다.</p>
<h3 id="2-consumer가-죽어도-서비스는-중단되지-않는다">2) Consumer가 죽어도 서비스는 중단되지 않는다</h3>
<p>어떤 Consumer가 장애가 나면 남아있는 Consumer가 해당 Partition을 자동으로 가져간다.
즉, <strong>Failover가 자동화되어 있다.</strong></p>
<h3 id="3-consumer-group별로-offset을-독립적으로-유지한다">3) Consumer Group별로 offset을 독립적으로 유지한다</h3>
<p>Group A가 offset=500까지 읽었어도 Group B는 offset=0부터 새로 읽어도 상관없다.
이 구조 덕분에 하나의 Kafka Topic이 여러 시스템에서 재사용된다.</p>
<h1 id="4-producer--consumer-동작-구조">4. Producer / Consumer 동작 구조</h1>
<h2 id="producer">Producer</h2>
<p>Producer는 Kafka로 메시지를 전송하기 전 다음 단계를 거친다.</p>
<h3 id="1-recordaccumulator에-적재">1) RecordAccumulator에 적재</h3>
<p>메시지는 메모리 버퍼에 모였다가 batch로 전송된다.
Batch가 커질수록 전송 속도(throughput)는 기하급수적으로 증가한다.</p>
<h3 id="2-partitioner로-partition-결정">2) Partitioner로 Partition 결정</h3>
<p>Key를 기반으로 Hash를 계산해 Partition을 선택한다.
Key가 없으면 round-robin 방식으로 분배된다.</p>
<h3 id="3-acks-옵션으로-내구성-제어">3) acks 옵션으로 내구성 제어</h3>
<ul>
<li>acks=0 → 가장 빠르지만 데이터 손실 가능</li>
<li>acks=1 → Leader만 쓰면 OK</li>
<li>acks=all → ISR 모두 쓰기 완료해야 성공(안정성 최고)</li>
</ul>
<p>Producer는 “속도 vs 안정성&quot;을 설정하는 구조다.</p>
<h2 id="consumer">Consumer</h2>
<p>Consumer는 다음과 같은 방식으로 동작한다.</p>
<h3 id="1-poll로-일정량을-가져온다">1) poll()로 일정량을 가져온다</h3>
<p>단일 메시지가 아니라 batch로 가져와 처리 속도를 올린다.</p>
<h3 id="2-비즈니스-로직-처리">2) 비즈니스 로직 처리</h3>
<p>지연이 발생하면 heartbeat 지연 → rebalance 발생
따라서 처리 시간을 잘 관리해야 한다.</p>
<h3 id="3-offset-commit">3) offset commit</h3>
<ul>
<li>auto-commit 사용 시 일정 간격으로 자동 commit</li>
<li>manual-commit 사용 시 처리 후 개발자가 명시적으로 commit</li>
</ul>
<p>offset commit 전략에 따라 중복 처리 / 손실 처리 여부가 결정된다.</p>
<h1 id="5-at-most-once--at-least-once--exactly-once">5. At-most-once / At-least-once / Exactly-once</h1>
<p>Kafka 스트림 처리에서 가장 중요한 개념 중 하나다.</p>
<h3 id="at-most-once">At-most-once</h3>
<ul>
<li><p>commit → 처리 순으로 동작</p>
</li>
<li><p>중복 없음</p>
</li>
<li><p>대신 메시지 손실 가능</p>
</li>
<li><p>로그 중요도가 낮거나 실시간성이 최우선일 때 사용</p>
<p>  (예: 단순 metrics, tracking)</p>
</li>
</ul>
<h3 id="at-least-once">At-least-once</h3>
<ul>
<li>처리 → commit 순으로 동작</li>
<li>메시지 손실 없음</li>
<li>대신 중복 가능</li>
<li>가장 일반적이고 안전한 방식</li>
</ul>
<h3 id="exactly-once">Exactly-once</h3>
<p>Kafka Streams에서만 완벽 지원한다.
프로듀서/컨슈머 상태 저장소를 하나의 트랜잭션처럼 묶어 “중복도 없고, 손실도 없는” 가장 안정적인 처리를 지원한다.</p>
<p>예시</p>
<ul>
<li>금융 처리</li>
<li>재고 수량 계산</li>
<li>여러 데이터 소스를 join하는 스트림 처리</li>
</ul>
<h1 id="6-backpressure">6. Backpressure</h1>
<p>Backpressure는 스트림 시스템에서 흔히 발생하는 핵심 문제다.
Backpressure란?
하위 처리 단계가 상위 단계 속도를 감당하지 못해 시스템이 밀리는 현상이다.
즉, 소비자가 처리하는 속도보다 생산자가 훨씬 빠를 때 발생하는 “압력 역전 현상”이다.</p>
<h3 id="발생-원인">발생 원인</h3>
<ol>
<li>Producer가 너무 빠르게 보냄</li>
<li>Consumer 응답이 느림 (DB 호출 등)</li>
<li>특정 Key로 메시지 쏠림<ol>
<li>Kafka Partition은 Key에 의해 결정되므로 특정 Key로 이벤트가 몰릴수있다.</li>
</ol>
</li>
<li>Partition 수가 충분하지 않음</li>
<li>Consumer가 poll을 자주 못함 (GC, 스레드 블로킹)</li>
</ol>
<h3 id="backpressure-결과">Backpressure 결과</h3>
<ul>
<li>Kafka Lag 급증<ul>
<li>Partition의 끝 offset - Consumer가 읽은 offset</li>
<li>즉 Lag가 커진다 ? → 처리 효율이 낮다..</li>
</ul>
</li>
<li>처리 지연 증가</li>
<li>알림 시스템 폭주</li>
<li>Rebalance 연속 발생</li>
</ul>
<p>Backpressure는 “시스템이 위험 신호를 보내는 상태”이며 이를 감지하기 위해 Kafka 모니터링에서 Lag은 절대적 지표이다.</p>
<h1 id="7-rebalancing">7. Rebalancing</h1>
<p>Rebalance는 Kafka Consumer Group의 재조정 작업이다.</p>
<h3 id="언제-발생하는가">언제 발생하는가?</h3>
<ol>
<li>Consumer 추가</li>
<li>Consumer 제거</li>
<li>Consumer poll 지연 → heartbeat timeout<ol>
<li>Coordinator는 Consumer가 죽었다고 판단..</li>
<li>남은 Consumer에게 Partition을 재할당</li>
<li>Rebalance 발생</li>
</ol>
</li>
<li>Topic Partition 개수 변경</li>
</ol>
<h3 id="rebalance의-문제점">Rebalance의 문제점</h3>
<p>Rebalance가 일어나는 동안에는 <strong>모든 Consumer가 작업을 중단하고 협의를 진행한다.</strong>
이는 Stream Processing에서 치명적인 지연을 만든다.
그래서 아래 설정을 매우 중요하게 다룬다.</p>
<ul>
<li>heartbeat.interval.ms<ul>
<li>Consumer가 Coordinator에게 “나 살아있어 !”라고 신호를 보내는 주기</li>
</ul>
</li>
<li>session.timeout.ms<ul>
<li>Coordinator가 Consumer를 “죽었다”고 판단하는 시간</li>
</ul>
</li>
<li>max.poll.interval.ms<ul>
<li>poll() 사이 최대 허용 시간</li>
<li>이 값을 초과하면 Consumer를 죽은것으로 판단</li>
</ul>
</li>
</ul>
<p>애플리케이션이 오래 처리하면 → poll 지연 → heartbeat 지연 → Rebalance 발생 → 전체 서비스가 멈춘 것처럼 느려진다.
즉, Rebalance는 “필요한 기능이지만 최대한 적게 일어나야 하는 기능”이다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/13e4c148-62f4-4776-a7b1-5acdb7e4b5b6/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>여러 Consumer들은 Coordinator에게 주기적으로 Heartbeat를 보내며 “나는 살아있다”고 알린다.</li>
<li>Coordinator는 각 Consumer의 subscription 정보를 기반으로 Partition을 배분한다.</li>
<li>Consumer 요청(Request)에는 group_id, generation_id, member_id가 포함된다.</li>
<li>Coordinator 응답(Response)의 error_code는 Rebalance가 진행 중인지 여부를 나타낸다.</li>
<li>Consumer 1이 Leader 역할을 하여 최종 Partition assignment를 그룹에 전달한다.</li>
</ul>
<h1 id="8-keying-전략-path--service--node-기반">8. Keying 전략 (Path / Service / Node 기반)</h1>
<p>Partitioning은 Key를 기준으로 이루어진다.
따라서 어떤 Key를 넣느냐에 따라 스트림 처리 품질이 완전히 달라진다.
Kafka에서 Partition을 결정하는 요소 = Key이다.</p>
<p>즉,</p>
<pre><code class="language-jsx">Partition = hash(key) % partition_count</code></pre>
<h3 id="key-전략이-중요한-이유">Key 전략이 중요한 이유</h3>
<ol>
<li>Partition 분산 여부가 결정된다.<ol>
<li>Key가 치우치면 하나의 Partition에만 과부화 → BackPressure 발생</li>
<li>Lag 증가 → 전체 스트림 지연..</li>
</ol>
</li>
<li>Aggregation 정확도가 Key에 의해 결정된다. <ol>
<li>Kafka Strams/Flink의 Window 집계는 Key 단위로 수행된다.</li>
<li>Key가 곧 집계 단위가 된다.</li>
</ol>
</li>
<li>Hot Key 문제가 생길수있다.<ol>
<li>특정 Key에 트래픽이 몰리면 해당 Partition만 장애 수준의 Backlog가 생긴다.</li>
</ol>
</li>
</ol>
<h3 id="1-path-기반">1) Path 기반</h3>
<p>REST API 경로에 기반해 Key를 설정하는 방법</p>
<p>예시</p>
<ul>
<li>/order/** → order</li>
<li>/user/** → user</li>
</ul>
<p>장점</p>
<ul>
<li>API 단위로 트래픽을 묶기 좋다.<ul>
<li>특정 API에서 오류율이 폭증하는지 바로 알 수 있다.</li>
</ul>
</li>
<li>Monitoring 지표가 직관적</li>
</ul>
<h3 id="2-service-기반">2) Service 기반</h3>
<p>마이크로서비스 구조에서 추천되는 방식
즉, Key가 서비스 이름이 되는경우</p>
<p>예시</p>
<ul>
<li>payment-service</li>
<li>auth-service</li>
<li>inventory-service</li>
</ul>
<p>서비스 단위 KPI 집계에 효과적이다.</p>
<h3 id="3-node-기반">3) Node 기반</h3>
<p>노드 ID, IP, Pod ID 등을 key로 사용</p>
<p>장점</p>
<ul>
<li><p>특정 서버의 이상징후를 즉시 감지</p>
</li>
<li><p>장애 서버 탐지 가능</p>
<p>  단점:</p>
</li>
<li><p>특정 서버에 트래픽 쏠리면 Hot Partition 발생 위험</p>
</li>
</ul>
<h3 id="keying-전략의-핵심-기준">Keying 전략의 핵심 기준</h3>
<ul>
<li>파티션 간 트래픽 분산<ul>
<li>Key가 너무 적다 ? → 특정 Partition만 과부화</li>
<li>Key가 너무 많다 ? → 집계의 의미가 없어진다.</li>
</ul>
</li>
<li>비즈니스 단위 집계 가능</li>
<li>Hotspot 방지 (Hot Key / Hot Partiotion)<ul>
<li>방지 전략<ul>
<li>Key randomizer</li>
<li>composite Key</li>
<li>round-robin Partitioner</li>
<li>consistent hashing 개선</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="9-kafka-streams-vs-consumer-기반-처리">9. Kafka Streams vs Consumer 기반 처리</h1>
<h2 id="일반-consumer">일반 Consumer</h2>
<ul>
<li>단순히 메시지를 읽고 처리</li>
<li>join/aggregation/window 등을 직접 구현해야 함</li>
<li>중복/재처리/오프셋 관리 복잡</li>
<li>상태 저장 불가</li>
<li>병렬 처리도 직접 구현</li>
</ul>
<p>간단한 이벤트 처리에는 충분하지만 “실시간 분석 시스템”에는 부적합하다.</p>
<h2 id="kafka-streams">Kafka Streams</h2>
<ul>
<li>Topology 기반으로 Stream → Processor → Sink 연결</li>
<li>State Store로 상태 저장</li>
<li>Changlog Topic으로 Store 내역을 백업</li>
<li>Event-time 기반 window 연산</li>
<li>Exactly-once 구현</li>
<li>Failover 자동</li>
<li>Repartition 자동</li>
</ul>
<p>Kafka Streams는 “실시간 데이터 처리 엔진”이며 일반 Consumer는 “단순 메시지 소비자”다.
<strong>둘은 목적 자체가 다르다.</strong></p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/fdca3982-c5bc-4c7e-a419-c4bd2e67f89b/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>왼쪽은 Kafka Streams가 Topic A/B를 직접 읽어 내부 Topology에서 처리하고 다시 Kafka에 쓰는 구조다.</li>
<li>Streams 내부에서 join, aggregation, windowing 같은 고급 처리를 자동으로 수행한다.</li>
<li>오른쪽은 Consumer가 Topic A/B를 읽어 직접 처리한 뒤 필요하면 다시 Producer가 Topic으로 기록한다.</li>
<li>즉, Consumer 기반은 모든 처리 로직과 상태 관리, 재처리, 병렬화를 개발자가 직접 구현해야 한다.</li>
<li>반면 Kafka Streams는 “실시간 처리 엔진”이기 때문에 상태 저장, window, 재처리, failover 등을 자동 처리한다.</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>이번 편에서는 Kafka 기반 스트림 처리에서 반드시 알아야 하는 핵심 개념들을 좀 더 깊게 다뤘다.</p>
<ul>
<li>Topic이 로그 기반이라는 점</li>
<li>Partition/Offset의 구조적 의미</li>
<li>Consumer Group의 자동 Failover</li>
<li>Exactly-once의 본질</li>
<li>Backpressure가 발생하는 이유</li>
<li>Rebalance가 왜 위험한지</li>
<li>Keying 전략이 얼마나 중요한지</li>
<li>Kafka Streams의 강점</li>
</ul>
<p>이 개념들만 정확히 이해해도 Kafka 기반 실시간 분석 아키텍처를 어느정도 이해했다고 볼수있을거같다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://seonkyukim.github.io/kafka-rebalancing/">https://seonkyukim.github.io/kafka-rebalancing/</a></p>
<p><a href="https://stackoverflow.com/questions/44014975/kafka-consumer-api-vs-streams-api">https://stackoverflow.com/questions/44014975/kafka-consumer-api-vs-streams-api</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Stream Processing 개념과 구조]]></title>
            <link>https://velog.io/@1im_chaereong/Stream-Processing-%EA%B0%9C%EB%85%90%EA%B3%BC-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@1im_chaereong/Stream-Processing-%EA%B0%9C%EB%85%90%EA%B3%BC-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Tue, 02 Dec 2025 13:48:08 GMT</pubDate>
            <description><![CDATA[<p>데이터가 폭발적으로 증가하면서, “나중에 분석하는 Batch 중심 구조”만으로는 서비스 운영과 장애 대응이 불가능해졌다.
결제 실패율이 갑자기 오르는지, 로그인 오류가 특정 지역에서 집중되는지, 서버 응답시간이 급격히 증가하는지와 같은 문제는 <strong>발생하고 10분 뒤에 알면 이미 늦었다.</strong>
이 문제를 해결하기 위해 등장한 개념이 바로 <strong>Stream Processing</strong>이다.
Kafka, Flink, Spark Streaming 같은 기술들이 “들어오자마자 바로 처리하는 구조”를 가능하게 만들었다.</p>
<p>이 글에서는 Stream Processing이 무엇인지, Batch와 어떤 점이 다른지, 왜 지금의 서비스 환경에 필수적인지 개념적으로 정리한다.</p>
<h1 id="1-batch-processing-vs-stream-processing">1. Batch Processing vs Stream Processing</h1>
<p>두 방식의 차이를 명확히 이해해야 Stream Processing의 필요성이 보인다.</p>
<h2 id="batch-processing">Batch Processing</h2>
<ul>
<li>일정 시간 동안 데이터를 모은 뒤 한 번에 많은 양을 처리하는 방식</li>
<li>Hadoop MapReduce, Spark Core가 대표적</li>
</ul>
<p>장점</p>
<ul>
<li>대규모 데이터 처리에 효율적</li>
<li>정확성을 확보하기 좋음</li>
<li>비용이 적게 듦(EC2 spot 사용, 야간 처리 등)</li>
</ul>
<p>단점</p>
<ul>
<li><strong>지연 시간(latency)이 크다</strong></li>
<li>실시간 장애 감지 불가</li>
<li>데이터가 쌓여야 의미 있는 결과가 나옴</li>
</ul>
<h2 id="stream-processing">Stream Processing</h2>
<ul>
<li>데이터가 들어오는 즉시 처리</li>
<li>이벤트 중심(event-driven) 아키텍처</li>
<li>Kafka Streams, Flink, Spark Structured Streaming 등</li>
</ul>
<p>장점</p>
<ul>
<li>실시간 분석</li>
<li>빠른 의사 결정</li>
<li>운영 모니터링에 최적</li>
<li>이벤트 단위 처리 가능</li>
</ul>
<p>단점</p>
<ul>
<li>아키텍처 복잡도 증가</li>
<li>상태 관리(State store)가 어려움</li>
<li>정확도 보장 로직 필요(Exactly-once)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/2806d2f0-4173-45ba-921d-fd5b4f3777d5/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>좌측 Real-Time은 데이터가 들어오자마자 Analytics Engine이 즉시 처리하고 Action으로 이어진다.</li>
<li>Database도 실시간으로 업데이트되며 초 단위 의사결정이 가능하다.</li>
<li>우측 Batch는 데이터가 먼저 DB에 쌓인 뒤 일정 주기마다 Analytics Engine이 묶음으로 처리한다.</li>
<li>분석 결과는 실시간이 아니라 사후적으로 제공된다.</li>
<li>즉, 실시간은 즉각 반응 중심, 배치는 쌓아두고 요약 분석 중심 구조이다.</li>
</ul>
<h1 id="2-real-time-vs-near-real-time">2. Real-time vs Near Real-time</h1>
<p>많은 사람들이 “실시간 = 0초”라고 오해한다.
하지만 기술적으로 완전한 Real-time은 거의 없다.</p>
<h2 id="real-time">Real-time</h2>
<ul>
<li>1~100ms 수준</li>
<li>보통 금융 결제, IoT 장비 제어 등 초저지연 시스템</li>
</ul>
<h2 id="near-real-time">Near Real-time</h2>
<ul>
<li>0.5초~수 초 단위 지연</li>
<li>Kafka 기반 스트리밍 시스템이 여기에 속함</li>
</ul>
<p>현실적으로 Web 서비스, 로그 분석, 장애 감지 같은 분야에서는 <strong>Near Real-time이면 충분히 실시간으로 인식된다.</strong></p>
<p>예)
APM 모니터링에서 2초 동안 오류율이 폭증 → 즉시 Slack 알림 전송
이 정도 속도면 운영 측면에서 실시간 대응이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/df5a28f9-b794-4d12-a931-c3dc148889af/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Streaming은 이벤트가 들어오는 즉시 하나씩 연속적으로 전달된다.</li>
<li>Near Real-time은 작은 묶음(batch micro)이 생긴 뒤 짧은 지연 후 전달된다.</li>
<li>Batch는 데이터를 오래 모았다가 한 번에 큰 덩어리로 전달하는 방식이다.</li>
</ul>
<h1 id="3-kafka-consumer-vs-stream-processor">3. Kafka Consumer vs Stream Processor</h1>
<p>Kafka Consumer를 스트림 엔진이라고 생각하면 안 된다.</p>
<h2 id="일반-kafka-consumer">일반 Kafka Consumer</h2>
<p>동작 방식</p>
<ol>
<li>메시지 poll</li>
<li>비즈니스 로직 처리</li>
<li>offset commit</li>
</ol>
<p>문제점</p>
<ul>
<li>상태(State)를 유지하기 어려움</li>
<li>윈도우 계산 불가(“5분 동안 오류 수” 같은 집계)</li>
<li>메시지 재처리(reprocessing) 구조 약함</li>
<li>Exactly-once 보장 어렵다</li>
<li>고도화된 join/aggregation 불가</li>
</ul>
<p>즉, 단순 소비자로는 <strong>실시간 데이터 처리 시스템을 형성하기 어렵다.</strong></p>
<h2 id="stream-processor-kafka-streams-flink-spark-streaming">Stream Processor (Kafka Streams, Flink, Spark Streaming)</h2>
<ul>
<li>State store 내장</li>
<li>Windowed aggregation</li>
<li>Event-time 기반 처리</li>
<li>Exactly-once 제공</li>
<li>Repartition / Shuffle / Join 지원</li>
<li>장애 발생 시 자동 재처리 가능</li>
</ul>
<p>즉, Stream Processor는 단순 Consumer가 아니라 <strong>실시간 분산 처리 엔진</strong>이다.</p>
<h1 id="4-event-time-vs-processing-time">4. Event Time vs Processing Time</h1>
<p>스트림 처리에서 가장 중요한 개념이다.</p>
<h2 id="processing-time">Processing Time</h2>
<p>이벤트를 “받은 시각”을 기준으로 처리</p>
<ul>
<li>Kafka로 도착한 시간</li>
<li>Consumer가 읽은 시간</li>
<li>시스템 time 사용</li>
</ul>
<p>문제점
하지만 ? 현실 세계에서 이벤트는 절대 순서대로 오지않는다.. 그렇다면 ?
네트워크 지연, 시스템 장애가 발생하면 이벤트 도착 순서가 뒤틀려 잘못된 집계를 하게 된다.</p>
<h2 id="event-time">Event Time</h2>
<p>데이터가 “실제로 발생한 시각”을 기준으로 처리</p>
<ul>
<li>로그에 기록된 timestamp</li>
<li>기기에서 발생한 실제 시간</li>
</ul>
<p>예시</p>
<p>12:00에 발생한 오류 로그가 네트워크 지연으로 12:03에 Kafka로 들어옴
Processing Time → 12:03에 오류 증가로 판단해서 잘못 분석됨
Event Time → 12:00의 오류로 정확히 반영됨
고도화된 스트림 시스템은 대부분 Event Time 기반이다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/a42a09b0-4ab0-4326-b458-b0e44e2a2a50/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>위쪽 파란 점들은 이벤트가 실제로 발생한 시간(Event Time)을 의미한다.</li>
<li>아래쪽 보라색 점들은 이벤트가 시스템에 도착해 처리된 시간(Processing Time)이다.</li>
<li>Post 2처럼 늦게 도착하는 데이터는 Event Time과 Processing Time이 크게 어긋난다.</li>
<li>Processing Time만 쓰면 늦게 도착한 이벤트가 잘못된 구간에 들어가 집계가 뒤틀린다.</li>
<li>Event Time을 사용해야 실제 발생 순서대로 정확한 스트림 분석을 할 수 있다.</li>
</ul>
<h1 id="5-메시지-단위-처리-vs-윈도우window-처리">5. 메시지 단위 처리 vs 윈도우(Window) 처리</h1>
<p>실시간 처리라고 해서 항상 이벤트 하나하나만 처리하는 것은 아니다.</p>
<h2 id="메시지-단위-처리">메시지 단위 처리</h2>
<ul>
<li>단순 이벤트 처리</li>
<li>결제 이벤트 1건</li>
<li>로그인 성공/실패 1건</li>
<li>Audit 로그 1건</li>
</ul>
<p>이 방식은 직접 처리하는데 윈도우가 필요 없다.</p>
<h2 id="윈도우-기반-처리">윈도우 기반 처리</h2>
<p>대부분의 실시간 분석은 <strong>집계(aggregation)</strong> 가 필요하다.</p>
<p>예를 들어 다음과 같은 정보는 윈도우 기반으로만 얻을 수 있다.</p>
<ul>
<li>최근 1분간 오류율</li>
<li>최근 5분 동안 평균 응답 시간</li>
<li>최근 10초 동안 특정 URI에서 실패율 급증</li>
<li>IP별 30초 동안 로그인 실패 횟수(보안 탐지)</li>
</ul>
<p>Stream Processing 엔진의 핵심 역할이 바로</p>
<p><strong>시간 단위로 데이터를 모아 의미 있는 지표로 만드는 것</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/388fb937-a071-4640-988d-244efbeded8f/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Tumbling Window는 구간이 겹치지 않고 고정된 크기만큼 “뚝뚝” 잘라서 집계한다.</li>
<li>Sliding Window는 구간이 서로 겹치며 더 짧은 간격으로 계속 이동하면서 집계를 반복한다.</li>
<li>즉, Tumbling은 비연속적 집계, Sliding은 더 촘촘한 연속 집계 방식이다.</li>
</ul>
<h1 id="6-stream-processing이-왜-필요한가-핵심-설득-포인트">6. Stream Processing이 왜 필요한가? (핵심 설득 포인트)</h1>
<h2 id="장애-감지-속도">장애 감지 속도</h2>
<p>APM이나 운영 모니터링에서 “5~10분 뒤 대시보드에 올라오는 정보”는 사실상 무의미하다.</p>
<p>예:</p>
<ul>
<li>로그인 오류가 3분 동안 폭증 → 이미 콜센터 난리</li>
<li>결제 실패율 증가 → 이미 매출 손실 발생</li>
<li>502가 특정 API에서 급증 → 고객 불편 이미 발생</li>
</ul>
<p>Stream Processing은 이런 문제를 <strong>들어오자마자 즉시 감지한다.</strong></p>
<h2 id="보안-이상징후-탐지">보안 이상징후 탐지</h2>
<ul>
<li>특정 IP에서 10초 동안 로그인 실패 20회</li>
<li>의심스러운 OTP 요청 빈도</li>
<li>세션 탈취 패턴</li>
</ul>
<p>이런 이벤트는 Batch로 처리하면 보안 사고가 이미 발생한 후다.</p>
<h2 id="실시간-개인화">실시간 개인화</h2>
<ul>
<li>사용자가 앱을 사용하는 흐름을 따라가며 실시간 추천</li>
<li>최근 본 상품 기반으로 즉시 추천 API 제공</li>
<li>실시간 클릭스트림 분석</li>
</ul>
<h2 id="서비스-운영-자동화">서비스 운영 자동화</h2>
<p>Stream Processing은</p>
<p>“문제 감지 → 자동 조치” 흐름을 만든다.</p>
<p>예시</p>
<p>로그 분석 → 오류율 급증 → 자동 슬랙 알림</p>
<p>CPU 85% 초과 10초 지속 → 자동 스케일 아웃</p>
<p>특정 지역 장애 감지 → 트래픽 우회</p>
<h2 id="비용-절감">비용 절감</h2>
<p>Batch 기반 Spark 클러스터처럼 거대한 연산 자원을 상시 띄워둘 필요가 없다.</p>
<p>Kafka Streams/Flink는 필요할 때만 scale-out</p>
<h1 id="7-로그-→-kafka-→-stream-processor-전체-데이터-흐름">7. 로그 → Kafka → Stream Processor (전체 데이터 흐름)</h1>
<p>이 흐름을 이해하면 Stream Processing의 구조가 확실히 잡힌다.</p>
<ol>
<li>웹서버/애플리케이션 로그 생성</li>
<li>로그 수집 에이전트(Filebeat, FluentD 등)가 Kafka로 전송</li>
<li>Kafka는 로그를 분산 파티션에 저장</li>
<li>Stream Processor는 Kafka의 topic을 실시간으로 구독</li>
<li>이벤트를 윈도우 단위로 집계</li>
<li>오류율 상승, 지연 시간 증가 등 이상징후 탐지</li>
<li>알림 시스템(Slack, PagerDuty 등)으로 통보</li>
<li>관리자 또는 자동화된 조치가 대응</li>
</ol>
<p>이 전체가 1~2초 안에 끝난다.</p>
<h1 id="8-마무리">8. 마무리</h1>
<p>1편에서는 Stream Processing의 철학과 구조를 다뤘다.</p>
<p>왜 필요한지, 어떤 문제를 해결하는지,</p>
<p>Kafka Consumer와 어떤 점이 다른지 전체 그림을 잡는 데 초점을 맞췄다.</p>
<h3 id="참고-문헌">참고 문헌</h3>
<p><a href="https://estuary.dev/blog/batch-processing-vs-stream-processing/">https://estuary.dev/blog/batch-processing-vs-stream-processing/</a></p>
<p><a href="https://dataengineeringcentral.substack.com/p/batch-vs-near-realtime-vs-streaming">https://dataengineeringcentral.substack.com/p/batch-vs-near-realtime-vs-streaming</a></p>
<p><a href="https://otee.dev/2021/10/19/event-and-processing-time-semantics.html">https://otee.dev/2021/10/19/event-and-processing-time-semantics.html</a></p>
<p><a href="https://sunrise-min.tistory.com/entry/Tumbling-window%EC%99%80-Sliding-window">https://sunrise-min.tistory.com/entry/Tumbling-window%EC%99%80-Sliding-window</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka Retention Policy – 로그 보존, 삭제, Compaction 완전정복]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-Retention-Policy-%EB%A1%9C%EA%B7%B8-%EB%B3%B4%EC%A1%B4-%EC%82%AD%EC%A0%9C-Compaction-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-Retention-Policy-%EB%A1%9C%EA%B7%B8-%EB%B3%B4%EC%A1%B4-%EC%82%AD%EC%A0%9C-Compaction-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5</guid>
            <pubDate>Sun, 30 Nov 2025 14:20:11 GMT</pubDate>
            <description><![CDATA[<p>Kafka는 “메시지를 큐처럼 바로 삭제하는 시스템”이 아니다.
Kafka는 <strong>로그 저장 시스템(Log Store)</strong>이며, 메시지를 보존(retain)하는 전략은 데이터 구조와 비용, 성능에 직결된다.</p>
<p>Retention은 크게 두 가지로 나뉜다.</p>
<ul>
<li><strong>삭제 기반 Retention (Time/Size Retention)</strong></li>
<li><strong>Compaction 기반 Retention (Log Compaction)</strong></li>
</ul>
<p>이 두 방식의 차이를 정확히 이해하면 토픽 설계, 로그 보존 전략, Kafka Streams(KTable/ChangeLog) 구조가 명확해진다.</p>
<h1 id="1-kafka-retention의-기본-개념">1. Kafka Retention의 기본 개념</h1>
<p>Kafka의 메시지는 Consumer가 읽었다고 해서 삭제되지 않는다.
메시지는 <strong>Retention 정책</strong>에 의해 삭제되거나 정리된다.</p>
<p>Retention의 목적은 다음과 같다.</p>
<ul>
<li>디스크 무한 증가 방지</li>
<li>오래된 이벤트 제거</li>
<li>ChangeLog(상태 변경 로그) 유지</li>
<li>장애 복구를 위해 일정 기간 데이터 유지</li>
<li>분산 시스템에서 재처리 가능하게 함</li>
</ul>
<p>Retention은 Kafka 운영에서 가장 중요한 정책 중 하나이다.</p>
<h1 id="2-retention-시간time-기반-삭제">2. Retention 시간(Time) 기반 삭제</h1>
<p>가장 기본적인 정책이며, 다음 옵션으로 설정한다.</p>
<pre><code>retention.ms</code></pre><p>예시
7일 보관
604,800,000ms → 7일</p>
<p>동작 방식</p>
<ul>
<li>Segment 단위로 삭제</li>
<li>Segment의 가장 오래된 메시지 timestamp가 retention.ms를 초과하면 삭제</li>
<li>메시지를 개별 삭제하는 것이 아니라, <strong>파일 단위로 삭제</strong></li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>이해하기 쉽고 단순</li>
<li>로그 저장 비용 제어 가능</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>일정 기간이 지나면 데이터 완전히 제거</li>
<li>장애 시 과거 데이터 재처리 불가능할 수 있음</li>
</ul>
<h1 id="kafka-retention-policy--로그-보존-삭제-compaction-완전정복">Kafka Retention Policy – 로그 보존, 삭제, Compaction 완전정복</h1>
<p>Kafka는 “메시지를 큐처럼 바로 삭제하는 시스템”이 아니다.
Kafka는 <strong>로그 저장 시스템(Log Store)</strong>이며, 메시지를 보존(retain)하는 전략은 데이터 구조와 비용, 성능에 직결된다.
Retention은 크게 두 가지로 나뉜다.</p>
<ul>
<li><strong>삭제 기반 Retention (Time/Size Retention)</strong></li>
<li><strong>Compaction 기반 Retention (Log Compaction)</strong></li>
</ul>
<p>이 두 방식의 차이를 정확히 이해하면 토픽 설계, 로그 보존 전략, Kafka Streams(KTable/ChangeLog) 구조가 명확해진다.</p>
<h1 id="1-kafka-retention의-기본-개념-1">1. Kafka Retention의 기본 개념</h1>
<p>Kafka의 메시지는 Consumer가 읽었다고 해서 삭제되지 않는다.
메시지는 <strong>Retention 정책</strong>에 의해 삭제되거나 정리된다.</p>
<p>Retention의 목적은 다음과 같다.</p>
<ul>
<li>디스크 무한 증가 방지</li>
<li>오래된 이벤트 제거</li>
<li>ChangeLog(상태 변경 로그) 유지</li>
<li>장애 복구를 위해 일정 기간 데이터 유지</li>
<li>분산 시스템에서 재처리 가능하게 함</li>
</ul>
<p>Retention은 Kafka 운영에서 가장 중요한 정책 중 하나이다.</p>
<h1 id="2-retention-시간time-기반-삭제-1">2. Retention 시간(Time) 기반 삭제</h1>
<p>가장 기본적인 정책이며, 다음 옵션으로 설정한다.</p>
<pre><code>retention.ms</code></pre><p>예시</p>
<p>7일 보관</p>
<p>604,800,000ms → 7일</p>
<p>동작 방식</p>
<ul>
<li>Segment 단위로 삭제</li>
<li>Segment의 가장 오래된 메시지 timestamp가 retention.ms를 초과하면 삭제</li>
<li>메시지를 개별 삭제하는 것이 아니라, <strong>파일 단위로 삭제</strong></li>
</ul>
<h3 id="장점-1">장점</h3>
<ul>
<li>이해하기 쉽고 단순</li>
<li>로그 저장 비용 제어 가능</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>일정 기간이 지나면 데이터 완전히 제거</li>
<li>장애 시 과거 데이터 재처리 불가능할 수 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/d563e48a-59d6-4961-8314-1793dfbf6266/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Kafka는 오래된 메시지를 하나씩 지우지 않고 <strong>Segment 파일 단위</strong>로 삭제한다.</li>
<li><code>retention.ms</code> 기간(예: 7일)을 기준으로, 그 기간을 벗어난 Segment는 <strong>통째로 삭제</strong>된다.</li>
<li>그림에서 보라색(Deleted)은 retention 초과로 삭제된 Segment를, 파란색(Retained)은 보존되는 Segment를 의미한다.</li>
<li>최신 데이터가 쌓이는 구간은 <strong>active log segment</strong>로 유지된다.</li>
<li>결국 retention.ms는 “보존 기간 안의 Segment만 남기고 나머지는 제거”하는 정책임을 시각적으로 보여준다.</li>
</ul>
<h1 id="3-retention-용량size-기반-삭제">3. Retention 용량(Size) 기반 삭제</h1>
<pre><code>retention.bytes</code></pre><p>토픽 전체 용량이 지정한 크기를 초과하면 오래된 Segment부터 삭제된다.</p>
<p>예시</p>
<p>retention.bytes=100GB</p>
<p>→ Topic 전체가 100GB를 넘지 않도록 자동 관리</p>
<h3 id="장점-2">장점</h3>
<ul>
<li>디스크 용량 보호 확실</li>
<li>무한 데이터 스트림 처리에도 안정적</li>
</ul>
<h3 id="단점-2">단점</h3>
<ul>
<li>오래된 데이터가 얼마나 빨리 없어질지 예측 어려움</li>
</ul>
<p>대규모 실시간 로그 플랫폼(ELK ingest pipeline)은 보통 <strong>size 기반 retention</strong>을 많이 사용한다.</p>
<h1 id="4-log-compaction--최신-상태만-유지하는-방식">4. Log Compaction – 최신 상태만 유지하는 방식</h1>
<p>Retention Delete 정책과 완전히 다른 구조다.
Compaction은 <strong>Key 기반으로 최신 값만 남기는 방식</strong>이다.</p>
<p>예시</p>
<p>orderId=123 이 여러 번 업데이트된 경우
→ Compaction 후에는 가장 최신의 메시지 한 개만 남음
Compaction은 다음 옵션으로 활성화한다.</p>
<pre><code>cleanup.policy=compact
cleanup.policy=compact,delete #이것도 가능 !</code></pre><p>Compaction은 “삭제”가 아니라 “정리(clean)” 이다.</p>
<h3 id="compaction이-일어나는-과정">Compaction이 일어나는 과정</h3>
<ul>
<li>Log Cleaner가 백그라운드 스레드로 동작</li>
<li>동일 Key의 오래된 메시지들을 무시</li>
<li>최신 Key-Value만 유지</li>
<li>Tombstone(삭제 표시) 메시지가 있으면 나중에 제거</li>
</ul>
<p>Compaction은 Kafka가 “Key-Value 저장소처럼 동작”하도록 만들어준다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/b16e79e6-0616-4453-bbad-a29b90715e77/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Compaction 전(Log Before Compaction)에는 동일 Key(K3, K5 등)의 여러 버전이 존재한다.</li>
<li>Compaction은 각 Key의 <strong>가장 최신(offset이 가장 큰)</strong> 메시지를 선택한다.</li>
<li>오래된 메시지(K3의 V2 이전 값 등)는 무시된다.</li>
<li>Compaction 후(Log After Compaction)에는 Key당 최신 레코드만 남는다.</li>
<li>Kafka가 Key-Value 저장소처럼 “최신 상태”만 유지하도록 하는 구조를 시각적으로 나타낸 그림이다.</li>
</ul>
<h1 id="5-delete-vs-compact--근본적인-차이">5. Delete vs Compact – 근본적인 차이</h1>
<h3 id="delete-정책">Delete 정책</h3>
<ul>
<li>시간/용량 기반</li>
<li>Segment 단위 삭제</li>
<li>과거 로그가 사라짐</li>
<li>일반적인 이벤트 스트림에 적합 (로그/트랜잭션 추적)</li>
</ul>
<h3 id="compaction-정책">Compaction 정책</h3>
<ul>
<li>Key 최신값만 유지</li>
<li>ChangeLog/상태 기반 스트림에 적합</li>
<li>데이터가 영구적으로 “정상화(normalize)” 됨</li>
<li>오래된 key는 tombstone 메시지로 제거 가능</li>
</ul>
<h3 id="정리-표">정리 표</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Delete</th>
<th>Compaction</th>
</tr>
</thead>
<tbody><tr>
<td>삭제 기준</td>
<td>시간/용량</td>
<td>Key 최신값 기준</td>
</tr>
<tr>
<td>삭제 단위</td>
<td>Segment 파일</td>
<td>메시지 단위</td>
</tr>
<tr>
<td>사용 목적</td>
<td>로그 스트림</td>
<td>상태 저장</td>
</tr>
<tr>
<td>저장 비용</td>
<td>적음</td>
<td>다소 증가</td>
</tr>
<tr>
<td>주 사용처</td>
<td>이벤트 로그</td>
<td>Kafka Streams, KTable</td>
</tr>
</tbody></table>
<h1 id="6-log-cleaner-구조-compaction-엔진">6. Log Cleaner 구조 (Compaction 엔진)</h1>
<p>Compaction은 Log Cleaner라는 별도 쓰레드가 담당한다.
Log Cleaner의 목표는 딱 하나다.</p>
<p>“각 Key에 대해 가장 최신 메시지만 남기고 오래된 메시지는 로그에서 제거하자.”</p>
<h3 id="log-cleaner의-동작">Log Cleaner의 동작</h3>
<ol>
<li>Dirty Segment 스캔</li>
<li>Key별 최신 offset 찾기</li>
<li>새로운 Segment로 복사 (compact)</li>
<li>불필요한 데이터 제거</li>
<li>cleanup 후 새로운 파일로 교체</li>
</ol>
<p>Log Cleaner는 CPU와 Disk IO를 소모하므로 대규모 compact topic에서는 브로커 리소스 사용량이 증가할 수 있다.</p>
<h3 id="성능-팁">성능 팁</h3>
<ul>
<li>heap 크기 안정적으로 유지</li>
<li>cleaner.threads 수 조절</li>
<li>고성능 디스크 권장</li>
</ul>
<h1 id="7-ktable-changelog와-compaction의-관계">7. KTable, ChangeLog와 Compaction의 관계</h1>
<p>Kafka Streams에서 KTable은 “Key-Value 상태 저장소”이다.
KTable의 모든 상태 변경은 Compaction이 적용되는 ChangeLog Topic에 저장된다.</p>
<h3 id="예시">예시</h3>
<pre><code>userId=10  name=kim
userId=10  name=park   (업데이트)
userId=10  name=lee    (업데이트)</code></pre><p>Compaction 후</p>
<pre><code>userId=10  name=lee</code></pre><p>즉, KTable은 사실상 Compacted Topic을 기반으로 동작하며, Kafka는 이를 통해 지속적이고 정확한 상태 저장 구조를 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/e1dfbbc2-bae4-4e32-bf92-e7a834cc07d6/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>KTable 입력 토픽의 각 파티션(P1~P4)은 각각 다른 Stream Task로 매핑된다.</li>
<li>각 Stream Task는 해당 파티션의 데이터를 처리하고 자체 State Store(로컬 저장소)에 상태를 유지한다.</li>
<li>그림의 2GB, 3GB, 5GB 등은 파티션별 로컬 상태 크기를 의미한다.</li>
<li>App Instance가 여러 개 존재하면 파티션은 인스턴스 간에 분산된다.</li>
<li>이 구조 덕분에 KTable은 분산 Key-Value 저장소처럼 동작하며, 각 Task가 ChangeLog Topic을 기반으로 상태를 유지한다.</li>
</ul>
<h1 id="8-토픽-설계-시-retention-전략">8. 토픽 설계 시 Retention 전략</h1>
<p>Retention은 “데이터 처리 철학”과 직결된다.</p>
<h3 id="1-단순-이벤트-스트림-로그-분석">1) 단순 이벤트 스트림 (로그, 분석)</h3>
<ul>
<li>cleanup.policy=delete</li>
<li>retention.ms = 요구 보존기간</li>
<li>retention.bytes = 한계 디스크</li>
</ul>
<h3 id="2-재처리가-필요한-분석머신러닝">2) 재처리가 필요한 분석/머신러닝</h3>
<ul>
<li>retention.ms 매우 길게 설정 (예: 14~30일 이상)</li>
<li>또는 S3 Connect sink로 영구 보관</li>
</ul>
<h3 id="3-상태-저장-ktable-db-cdc-customer-state">3) 상태 저장 (KTable, DB CDC, Customer State)</h3>
<ul>
<li>cleanup.policy=compact</li>
<li>segment.bytes는 작게(빠른 compaction)</li>
<li>min.cleanable.dirty.ratio 설정 중요</li>
</ul>
<h3 id="4-결제주문금융-이벤트감사로그">4) 결제/주문/금융 이벤트(감사로그)</h3>
<ul>
<li>삭제 정책 매우 조심</li>
<li>반드시 acks=all + replication &gt;= 3</li>
<li>retention.ms 길게 유지하거나 external storage backup 필수</li>
</ul>
<h3 id="5-elk-연동-로그-수집">5) ELK 연동 로그 수집</h3>
<ul>
<li>retention.bytes 중심으로 관리</li>
<li>3~7일 rolling이 일반적</li>
</ul>
<h1 id="9-정리">9. 정리</h1>
<p>Kafka의 Retention은 단순한 삭제 정책이 아니라
<strong>데이터 철학, 비용, 시스템 신뢰성, 재처리 전략을 결정하는 핵심 구조</strong>이다.</p>
<p>핵심 요약</p>
<ul>
<li>Delete retention: Segment 단위 삭제 (시간/용량 기반)</li>
<li>Compaction: Key 최신값만 유지하는 log cleaner 구조</li>
<li>Delete/Compact는 목적이 완전히 다름</li>
<li>KTable/ChangeLog는 compaction 기반</li>
<li>토픽 설계 시 retention 전략을 반드시 사용 목적에 맞게 설정해야 함</li>
</ul>
<p>Retention 정책을 이해하면 Kafka의 저장 구조와 데이터 흐름을 훨씬 사용자 중심적으로 설계할 수 있다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://developer.confluent.io/courses/architecture/compaction/">https://developer.confluent.io/courses/architecture/compaction/</a>
<a href="https://cloudoses.com/log-compaction-cleanup-policy/">https://cloudoses.com/log-compaction-cleanup-policy/</a>
<a href="https://www.confluent.io/blog/kafka-streams-tables-part-3-event-processing-fundamentals/">https://www.confluent.io/blog/kafka-streams-tables-part-3-event-processing-fundamentals/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka Producer 동작 원리 – ACKS, Retries, Idempotence, Batch, Compression]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-Producer-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-ACKS-Retries-Idempotence-Batch-Compression</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-Producer-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-ACKS-Retries-Idempotence-Batch-Compression</guid>
            <pubDate>Sun, 30 Nov 2025 13:57:38 GMT</pubDate>
            <description><![CDATA[<p>Kafka Producer는 단순히 메시지를 전송하는 컴포넌트가 아니다.
내부적으로는 <strong>대규모 고속 전송을 위한 버퍼링·배치·압축·재전송·멱등성·순서 관리</strong>까지 수행하는 매우 정교한 구조이다.
Producer를 제대로 이해하면 다음 문제들을 스스로 분석할 수 있다.</p>
<ul>
<li>왜 특정 상황에서 메시지가 중복 전송되는가</li>
<li>어떤 설정이 처리량을 극적으로 높이는가</li>
<li>acks=all이 왜 중요하며 언제 실패하는가</li>
<li>재전송이 무한 루프로 빠지는 이유</li>
<li>멱등성(idempotence)이 없을 때 어떤 장애가 발생하는가</li>
<li>파티션별 순서가 언제 보장되는가</li>
</ul>
<p>이번 편에서는 Kafka Producer의 핵심 구조를 깊게 설명한다.</p>
<h2 id="1-producer-내부-구조-recordaccumulator">1. Producer 내부 구조: RecordAccumulator</h2>
<p>Producer는 메시지를 전송할 때 바로 네트워크로 보내지 않는다.
먼저 <strong>RecordAccumulator</strong>라는 메모리 버퍼에 저장한다.</p>
<p>RecordAccumulator 동작 방식</p>
<ol>
<li>Producer가 send() 호출</li>
<li>메시지가 Partition별로 Accumulator에 저장</li>
<li>Accumulator는 메시지를 batch 단위로 묶음</li>
<li>Sender thread가 별도로 batch를 Broker에 전송</li>
</ol>
<h3 id="왜-이런-구조인가">왜 이런 구조인가?</h3>
<ul>
<li>메시지 1건씩 전송하면 네트워크 비용이 매우 큼</li>
<li>Accumulator는 여러 이벤트를 묶어서 “한 번에” 전송함 → 처리량 증가</li>
<li>Partition별로 독립된 큐를 가져 병렬 처리 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/d4e78e5e-147d-46a8-9179-cd3e021136ad/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Producer는 ProducerRecord를 생성하고 Serializer가 이를 byte 배열로 변환한다.</li>
<li>Partitioner가 메시지가 들어갈 Topic-Partition을 결정한다.</li>
<li>RecordAccumulator는 Partition별로 Batch를 생성해 메시지를 메모리 버퍼에 쌓는다.</li>
<li>Sender 스레드가 준비된 Batch를 모아 Kafka 클러스터의 해당 Partition Leader로 전송한다.</li>
<li>이 구조 덕분에 Producer는 네트워크 I/O에 묶이지 않고 높은 처리량을 얻는다.</li>
</ul>
<h2 id="2-batch--compression-producer-성능의-핵심">2. Batch + Compression: Producer 성능의 핵심</h2>
<p>Producer는 메시지를 batch로 묶어서 단 한 번의 네트워크 호출로 전송한다.</p>
<h3 id="배치batch의-이점">배치(Batch)의 이점</h3>
<ul>
<li>네트워크 요청 수 감소 → TPS 증가</li>
<li>압축 효율 증가</li>
<li>Broker에 전달되는 레코드 덩어리가 커서 스루풋 향상</li>
</ul>
<p>Producer는 아래 두 설정에 영향을 받는다.</p>
<ul>
<li>batch.size</li>
<li>linger.ms</li>
</ul>
<h3 id="batchsize">batch.size</h3>
<ul>
<li>배치 하나의 최대 크기 (기본 16KB~32KB)</li>
<li>batch가 이 크기까지 차면 즉시 전송</li>
</ul>
<h3 id="lingerms">linger.ms</h3>
<ul>
<li>배치를 기다리는 시간</li>
<li>메시지가 적게 들어오면 linger.ms가 찰 때까지 기다렸다가 전송</li>
</ul>
<ul>
<li>high throughput → batch.size↑ + linger.ms↑</li>
<li>low latency → linger.ms=0</li>
</ul>
<h3 id="압축compression">압축(Compression)</h3>
<p>Producer는 메시지를 Broker에 보내기 전에 압축할 수 있다.</p>
<ul>
<li>gzip</li>
<li>snappy</li>
<li>lz4</li>
<li>zstd (추천, 최신/고효율)</li>
</ul>
<p>압축은 Producer와 Broker 사이에서만 발생하고, Follower Replica는 이미 압축된 그대로 받아 복제한다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/35996fbe-cdab-4965-b0da-2c30d97ee794/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Producer는 메시지를 직렬화·파티셔닝한 뒤 Accumulator의 Topic별 버퍼에 저장한다.</li>
<li>Accumulator는 메시지를 batch.size 기준으로 Batch 1, Batch 2처럼 묶어서 관리한다.</li>
<li>linger.ms 동안 메시지를 모았다가 Batch가 준비되면 Sender가 Broker로 전송한다.</li>
<li>이 구조로 Producer는 네트워크 호출을 최소화하고 높은 처리량을 확보한다.</li>
</ul>
<h2 id="3-acks0--1--all-차이">3. ACKS=0 / 1 / ALL 차이</h2>
<p>Producer가 send() 후 언제 “성공”으로 간주할지는 <strong>acks 설정</strong>으로 결정한다.</p>
<h3 id="acks0">acks=0</h3>
<ul>
<li>Producer는 전송 후 응답을 기다리지 않음</li>
<li>속도 가장 빠름</li>
<li>유실 위험 매우 큼</li>
<li>모니터링 로그/metric 등 유실 허용 가능한 환경에서만 사용</li>
</ul>
<h3 id="acks1">acks=1</h3>
<ul>
<li>Leader가 메시지를 파일에 append한 것만 확인</li>
<li>ISR 복제는 확인하지 않음</li>
<li>Leader 장애 시 데이터 유실 가능</li>
</ul>
<h3 id="acksall---1">acks=all (= -1)</h3>
<ul>
<li>Leader + 모든 ISR Replica에 복제 완료된 후 성공 반환</li>
<li>가장 안전한 설정</li>
<li>금융/주문/결제 시스템에서는 필수</li>
</ul>
<p>요약하면..</p>
<ul>
<li>성능 1순위 → acks=0</li>
<li>성능/안정성 적당히 → acks=1</li>
<li>안전성 절대우선 → acks=all</li>
</ul>
<h2 id="4-retries와-재전송">4. Retries와 재전송</h2>
<p>Kafka Producer는 메시지 전송 실패 시 자동으로 재시도한다.
하지만 재시도에는 치명적인 문제가 숨어 있다.</p>
<h3 id="retries-기본-개념">Retries 기본 개념</h3>
<p>다음 상황에서 Retry가 발생한다.</p>
<ul>
<li>네트워크 타임아웃</li>
<li>Leader not available</li>
<li>Follower lag 초과로 ISR 축소</li>
<li>UnknownTopicOrPartition</li>
<li>Request too large</li>
</ul>
<h3 id="retry-문제가-왜-위험한가">Retry 문제가 왜 위험한가?</h3>
<p>retry는 동일 메시지를 다시 보내므로 중복 메시지 발생 가능
특히 acks=1 + retry 조합은 <strong>대표적인 중복 전송 패턴</strong>이다.</p>
<p>예시</p>
<ol>
<li>Producer가 메시지를 Leader에 전송</li>
<li>Leader는 기록했지만 Producer에게 응답을 보내는 중 네트워크 단절</li>
<li>Producer는 “실패”로 판단하고 다시 send()</li>
<li>같은 메시지가 두 번 저장됨</li>
</ol>
<p>이런 중복을 막기 위한 기능이 <strong>Idempotent Producer</strong> 이다.</p>
<h2 id="5-idempotent-producer-중복-없는-전송">5. Idempotent Producer (중복 없는 전송)</h2>
<p>Kafka 0.11부터 <strong>idempotent producer</strong> 기능을 제공한다.
Producer가 중복 전송하더라도 Broker가 동일 메시지를 중복 저장하지 않는다.</p>
<p>활성화 방법</p>
<pre><code>enable.idempotence=true</code></pre><p>Idempotent Producer 동작 원리</p>
<ul>
<li>Producer는 PID(Producer ID)와 sequence number를 부여</li>
<li>Broker는 Partition별로 &quot;마지막 sequence number&quot;를 기억</li>
<li>같은 PID + 같은 sequence number 메시지는 <strong>중복으로 판단하고 무시</strong></li>
</ul>
<p>즉, retry가 몇 번 일어나도 <strong>논리적으로 한 번만 전송된 것처럼 처리</strong>된다.</p>
<h3 id="장점">장점</h3>
<ul>
<li>메시지 중복 방지</li>
<li>retry가 많은 환경에서도 데이터 무결성 유지</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>하나의 Producer가 여러 Transactional 작업을 동시에 하기에는 제한적</li>
<li>Partition 단위 멱등성만 제공됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/49a91441-4d16-4604-9ec8-8cb78476657f/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ol>
<li>Producer는 메시지를 전송하고 Broker는 정상적으로 커밋한 후 ACK을 보낸다.</li>
<li>하지만 네트워크 장애로 ACK이 Producer에게 도착하지 않으면 Producer는 실패로 판단한다.</li>
<li>Producer는 같은 메시지를 retry 전송하지만 Idempotent Producer는 PID+Sequence 번호를 확인한다.</li>
<li>Broker는 이미 처리한 메시지임을 인지하고 중복을 무시한다.</li>
<li>결과적으로 재전송이 발생해도 메시지는 단 한 번만 커밋된다.</li>
</ol>
<h2 id="6-message-ordering-보존-조건순서-보장-조건">6. Message Ordering 보존 조건(순서 보장 조건)</h2>
<p>Kafka는 “Partition 단위”로만 순서를 보장한다.
하지만 Producer가 몇 가지 조건을 위반하면 순서가 깨질 수 있다.</p>
<h3 id="producer-순서가-깨지는-조건">Producer 순서가 깨지는 조건</h3>
<ol>
<li><p>같은 Partition에 대해 <strong>동시에 여러 배치(batch)</strong>가 전송될 때</p>
<pre><code class="language-jsx"> Batch 1: [msg 1, msg 2, msg 3]
 Batch 2: [msg 4, msg 5]</code></pre>
<p> 위 상황처럼 하나의 Partition에 여러 배치가 있다고하자.
 그랬을때 네트워크 경로가 다르거나, retry가 발생하거나, 배치1이 압축되어 더 오래걸리거나 .. 여러 상황이 있을수있겠지만 배치2가 먼저 도착해버린다면 ?
 그럼 순서가 뒤집히게된다. (msg 4 → msg1)</p>
</li>
<li><p>retries &gt; 0 + acks=0 조합</p>
</li>
<li><p>max.in.flight.requests.per.connection &gt; 1 (기본은 5)</p>
<p> 이 설정은 “동시에 몇 개의 요청을 브로커로 날릴 수 있는가”다.
 즉 5라면 Producer는 한 커넥션에서 동시에 5개의 배치를 전송할수있다.
 근데 여기서 문제는 ?
 배치1이 전송됨, 배치 2도 전송, 배치1이 retry 발생.. 늦게 도착
 그 사이에 배치2가 commit 돼버림. 이렇게 되면 순서가 뒤바뀐다.</p>
</li>
<li><p>Producer가 여러 스레드에서 같은 Producer instance를 공유할 때</p>
</li>
</ol>
<h3 id="순서-보장을-원한다면">순서 보장을 원한다면?</h3>
<p>아래 설정이 필수</p>
<pre><code>enable.idempotence=true
max.in.flight.requests.per.connection=1
acks=all</code></pre><p>이 조합이면, retry가 발생해도 메시지 순서가 보장된다.</p>
<h2 id="7-backpressure와-producer-처리량-최적화">7. Backpressure와 Producer 처리량 최적화</h2>
<p>Kafka Producer는 메시지를 계속 빠르게 보내고싶어한다.
그래서 Producer는 Broker가 처리 속도가 느릴 때 자동으로 backpressure를 만든다.</p>
<p>대표적인 backpressure 조건</p>
<ul>
<li>RecordAccumulator이 꽉 참 (buffer.memory 초과)</li>
<li>batch가 너무 많이 밀림</li>
<li>Broker 응답 지연</li>
<li>네트워크 지연 증가</li>
</ul>
<p>이 과정에서 Producer는 다음 동작을 한다.</p>
<ul>
<li>새로운 send() 요청 block</li>
<li>enqueue 안 됨</li>
<li>timeout 발생</li>
<li>retries 증가</li>
</ul>
<h3 id="producer-성능-튜닝-핵심">Producer 성능 튜닝 핵심</h3>
<ol>
<li><p>batch.size ↑</p>
<p> 더 적은 네트워크 호출 = 높은 처리량</p>
</li>
<li><p>linger.ms ↑</p>
<p> 배치를 얼마나 모을 것인가 ? 
 linger.ms를 키우면 배치가 충분히 커진 뒤 전송 = 성능 상승</p>
</li>
<li><p>compression.type=zstd</p>
</li>
<li><p>buffer.memory ↑</p>
</li>
<li><p>max.in.flight.requests.per.connection 조정</p>
<p> 값이 크면 → 높은 처리량, 값이 작으면 → 순서 보존</p>
</li>
<li><p>acks=all + idempotence 안정화</p>
</li>
</ol>
<p>올바른 설정 조합만 맞춰줘도 Producer 처리량은 2배~10배까지 상승할 수 있다.</p>
<h2 id="8-정리">8. 정리</h2>
<p>Kafka Producer는 단순 HTTP client와 비교할 수 없을 만큼 복잡하고 정교하다.</p>
<p>핵심 개념을 요약하면 다음과 같다.</p>
<ul>
<li>RecordAccumulator는 Producer 성능의 핵심 버퍼</li>
<li>batch + linger.ms 조합은 Throughput에 절대적</li>
<li>acks=all은 내구성과 안정성을 위한 기본값</li>
<li>retry는 중복을 일으키므로 idempotence 필수</li>
<li>ordering 보장은 설정 조합이 맞아야 유지됨</li>
<li>backpressure는 Producer-Broker 부하 균형을 맞추는 핵심 메커니즘</li>
</ul>
<p>Producer 구조를 확실히 이해하면 Kafka 메시지 전송 중 발생하는 대부분의 문제(중복, 지연, 순서깨짐, ACK 실패)를 스스로 분석하고 해결할 수 있다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://zzzzseong.tistory.com/106">https://zzzzseong.tistory.com/106</a></p>
<p><a href="https://magpienote.tistory.com/251">https://magpienote.tistory.com/251</a></p>
<p><a href="https://www.geeksforgeeks.org/apache-kafka/apache-kafka-idempotent-producer/">https://www.geeksforgeeks.org/apache-kafka/apache-kafka-idempotent-producer/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka Replication 구조 – Leader, Follower, ISR, HW, LEO 완전정복]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-Replication-%EA%B5%AC%EC%A1%B0-Leader-Follower-ISR-HW-LEO-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-Replication-%EA%B5%AC%EC%A1%B0-Leader-Follower-ISR-HW-LEO-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5</guid>
            <pubDate>Tue, 25 Nov 2025 14:37:43 GMT</pubDate>
            <description><![CDATA[<p>Kafka가 높은 가용성과 데이터 일관성을 유지할 수 있는 이유는
<strong>Replication(복제) 구조가 매우 강력하게 설계되어 있기 때문</strong>이다.
Replication Factor, ISR, LEO/HW, ACKS, commit 조건 등을 정확히 이해하면</p>
<p>다음과 같은 상황을 스스로 분석할 수 있다.</p>
<ul>
<li>데이터가 언제 “정말로 안전하게 저장되었는지”</li>
<li>브로커 장애 시 어떤 Replica가 Leader가 되는지</li>
<li>왜 메시지가 중복/유실되는지</li>
<li>ack=all이 왜 중요한지</li>
<li>복제 지연(replica lag)이 왜 문제가 되는지</li>
</ul>
<p>이번 편에서는 Kafka 복제 구조의 핵심 개념을 아주 상세하게 설명한다.</p>
<h2 id="1-replication-factor">1. Replication Factor</h2>
<p>Replication Factor는 하나의 Partition이 몇 개의 복제본(replica)을 가지는지 의미한다.</p>
<p>예: Replication Factor = 3
→ Leader 1개 + Follower 2개</p>
<p>복제본이 많을수록 장애에 강해지지만, 그만큼 저장 비용·네트워크 비용이 증가한다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/1d5fbbf8-3d7d-4cf9-9893-60ae60e1abcb/image.png" alt=""></p>
<p>그림으로 다시보자.
1편에서도 등장했던 그림이다.</p>
<ol>
<li><strong>Producer는 Leader(브로커101)에만 데이터를 기록</strong>하고, Follower들은 이를 그대로 복제한다.</li>
<li><strong>Consumer는 기본적으로 Leader에서 읽지만</strong>, 필요하면 Follower들도 동일한 데이터를 가진다.</li>
<li><strong>Replication Factor=3</strong>이므로 동일한 파티션 데이터가 브로커 3대에 저장되어 장애에도 안전하다.</li>
</ol>
<h2 id="2-leaderfollower-구조">2. Leader–Follower 구조</h2>
<p>각 Partition은 여러 replica를 가지지만,
그중 <strong>Leader</strong>만 Producer/Consumer의 읽기·쓰기 요청을 처리한다.</p>
<h3 id="leader">Leader</h3>
<ul>
<li>유일하게 기록(write)을 처리</li>
<li>Consumer가 읽는 대상</li>
</ul>
<h3 id="follower">Follower</h3>
<ul>
<li>Leader의 로그를 그대로 복제</li>
<li>Leader가 새 메시지를 append하면 Follower는 pull 방식으로 따라감</li>
<li>Lag가 너무 크면 ISR에서 제외됨</li>
</ul>
<p>Kafka의 복제는 “push가 아닌 pull 구조”이기 때문에
Follower가 빠지거나 느려져도 Leader가 성능 저하 없이 움직일 수 있다.</p>
<h2 id="3-isrin-sync-replica의-조건">3. ISR(In-Sync Replica)의 조건</h2>
<p>ISR은 “Leader와 거의 동일한 데이터를 가진 Replica들의 집합”이다.</p>
<p>ISR에 포함되기 위한 조건(핵심):</p>
<ol>
<li>Leader와 LEO 차이가 <code>replica.lag.time.max.ms</code>를 넘지 않을 것</li>
<li>Follower가 장애 상태가 아닐 것</li>
<li>Follower의 네트워크 지연이 너무 크지 않을 것</li>
</ol>
<p>ISR에 포함된 Replica만 데이터 일관성이 있다고 본다.</p>
<h3 id="isr의-특징">ISR의 특징</h3>
<ul>
<li>Leader는 메시지를 append한 뒤, ISR 내 replica들이 따라올 때까지 기다린다.(ACK=all일 경우)</li>
<li>ISR이 1개(=Leader 혼자)만 남아도 Kafka는 쓰기 작업을 진행할 수 있다.</li>
<li>ISR 밖 Replica는 “데이터가 뒤쳐진 상태”로 간주되며, Leader 장애 시 새로운 Leader로 승격될 수 없다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/e1623293-ff11-43af-8655-ca34c8852f70/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li><strong>각 Partition마다 Leader는 하나, Follower는 여러 개이며 이 전체가 ISR(In-Sync Replica) 그룹을 이룬다.</strong></li>
<li><strong>ISR 안의 Leader만이 Producer/Consumer 요청을 처리하고, Follower들은 Leader의 로그를 뒤에서 따라간다.</strong></li>
<li><strong>Broker들이 여러 대일 때 Partition의 Leader가 분산되어 클러스터 전체에 부하가 균형 있게 퍼진다.</strong></li>
</ul>
<h2 id="4-leolog-end-offset-vs-hwhigh-watermark">4. LEO(Log End Offset) vs HW(High Watermark)</h2>
<p>Replication 구조를 이해하려면 <strong>LEO</strong>와 <strong>HW</strong>의 차이를 반드시 이해해야 한다.</p>
<h2 id="leolog-end-offset">LEO(Log End Offset)</h2>
<p>Replica가 가진 “마지막 메시지의 Offset + 1”
→ Replica는 Leader와 Follower 전체를 의미한다.</p>
<p>쉽게 말해</p>
<ul>
<li>LEO = “내 로그는 여기까지 도착했다.”</li>
</ul>
<p>예시:</p>
<ul>
<li><p>Leader가 offset 0, 1, 2까지 메시지를 append했다면</p>
<p>  → Leader의 LEO = 3</p>
</li>
<li><p>Follower는 replication이 밀려서 offset 0, 1까지만 복제했다면</p>
<p>  → Follower의 LEO = 2</p>
</li>
</ul>
<p>Leader와 Follower 각각 LEO를 가진다.</p>
<h2 id="hwhigh-watermark">HW(High Watermark)</h2>
<p>클라이언트가 <strong>읽을 수 있는 최대 Offset</strong>
즉, “모든 ISR replica에 복제 완료된 가장 마지막 위치”
Leader는 HW까지만 Consumer에게 데이터를 노출한다.</p>
<h1 id="정리하면">정리하면</h1>
<ul>
<li>LEO = 각 Replica의 로그 끝</li>
<li>HW = 모든 ISR에서 공통으로 가진 위치</li>
<li>Consumer는 “HW 이하” 데이터만 읽을 수 있다</li>
</ul>
<p>즉, 메시지가 LEO에 append되었다고 해서 바로 Consumer가 읽을 수 있는 것은 아니다.</p>
<p><strong>ISR 전체가 복제 완료되어야 HW가 증가한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/6ecc5204-33a9-41c8-8813-66e82bc90ed7/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ol>
<li><strong>Leader와 In-Sync Follower(ISR)는 offset 0~4까지 동일하게 복제되어 있고, Leader는 5까지 도달해 LEO가 더 크다.</strong></li>
<li><strong>ISR 모두가 공통으로 가진 마지막 offset(=4)이 High Watermark(HW)로 설정된다.</strong></li>
<li><strong>Stuck Follower는 ISR에서 제외되며, Consumer는 HW(=4) 이하의 메시지만 읽을 수 있다.</strong></li>
</ol>
<h2 id="5-메시지는-언제-커밋되는가">5. 메시지는 언제 “커밋”되는가?</h2>
<p>Kafka에서 말하는 &quot;commit&quot;은 다음 상태를 의미한다.
메시지가 ISR 모든 Replica에 복제되면 → HW 증가 → Consumer가 읽을 수 있게 됨
이 상태가 <strong>Kafka의 commit</strong>이다.
commit = HW가 그 메시지를 포함하도록 이동한 시점 Producer가 Leader에 append한 시점이 아니라,
ISR의 모든 replica가 해당 메시지를 가져가서 LEO를 끌어올린 순간 commit된다.
Producer가 메시지를 보낸 시점 &lt; HW 포함된 시점</p>
<p>이 둘은 완전히 다른 사건이다.</p>
<h2 id="6-producer-acksall과-isr의-관계">6. Producer ACKS=all과 ISR의 관계</h2>
<p>Producer는 메시지를 전송할 때 다음과 같은 ACK 모드를 선택할 수 있다.</p>
<ul>
<li>acks=0</li>
<li>acks=1</li>
<li>acks=all (or -1)</li>
</ul>
<p>각 설정은 Replication과 밀접하게 연결된다.</p>
<h3 id="acks0">acks=0</h3>
<ul>
<li>Leader에 기록되었는지 확인하지 않음</li>
<li>최저 지연 / 최대 유실 위험</li>
</ul>
<h3 id="acks1">acks=1</h3>
<ul>
<li>Leader에 기록되었음을 확인</li>
<li>Follower 복제는 무시</li>
<li>Leader 장애 시 데이터 유실 발생 가능</li>
</ul>
<h3 id="acksall-or--1">acks=all (or -1)</h3>
<ul>
<li>Leader와 ISR 모든 replica에 복제되어야 성공 응답</li>
<li>Follower가 살아 있어야 함</li>
<li>높은 내구성(Durability)</li>
<li>유실 방지 → 실무에서 가장 권장</li>
</ul>
<p>즉, <strong>Replication 구조를 제대로 활용하려면 “acks=all”은 사실상 필수</strong>이다.
특히 금융/결제/주문 시스템은 반드시 acks=all 사용</p>
<h2 id="7-replication이-깨질-때-발생하는-문제들">7. Replication이 깨질 때 발생하는 문제들</h2>
<p>Replication 문제는 대부분 Follower lag이나 브로커 장애에서 시작된다.
아래는 실제 운영 환경에서 자주 발생하는 문제들이다.</p>
<h2 id="a-follower-lag-증가-→-isr에서-제외">A. Follower lag 증가 → ISR에서 제외</h2>
<p>Follower가 Leader 속도를 따라오지 못하면 ISR에서 빠진다.</p>
<p>결과:</p>
<ul>
<li>acks=all 모드에서는 Producer write 실패</li>
<li>리더 혼자 ISR이 되어 durability 약화</li>
</ul>
<h2 id="b-leader-장애-→-새로운-leader-선출">B. Leader 장애 → 새로운 Leader 선출</h2>
<p>새 Leader는 <strong>ISR에 포함된 Replica 중 하나</strong>에서만 선출된다.</p>
<p>ISR 밖에 있던 Replica는 데이터가 뒤쳐져 있으므로 선출 불가</p>
<p>문제는?</p>
<ul>
<li>ISR이 Leader 혼자만 있던 상황이면 → Leader 장애 시 Partition이 잠시 동안 unavailable</li>
<li>Replication Factor 1이면 → 아예 데이터 유실</li>
</ul>
<h2 id="c-unclean-leader-election">C. Unclean Leader Election</h2>
<p>설정을 잘못하면 Kafka는 ISR이 아닌 Replica를 Leader로 승격할 수 있다.</p>
<p>un clean leader election = true 일 때</p>
<ul>
<li>뒤쳐진 Replica가 Leader가 됨</li>
<li>데이터 역순서, rollback, 복구 불가능한 유실 발생</li>
</ul>
<p>그래서 실무에서는 반드시 다음처럼 설정한다.</p>
<pre><code>unclean.leader.election.enable=false</code></pre><h2 id="d-network-partition으로-인한-split-brain">D. Network partition으로 인한 split-brain</h2>
<p>Leader가 잘 살아 있지만 Follower들이 네트워크 단절로 ISR에서 빠지는 현상
잘못된 설정 시 데이터 충돌이나 재조정 지연 발생</p>
<h2 id="e-replica-재동기화-비용-급증">E. Replica 재동기화 비용 급증</h2>
<p>Follower가 매우 뒤쳐져 있을 경우
Leader와 오랜 시간 동안 대량 데이터 동기화를 해야 함 → 성능 저하 &amp; Rebalance 지연</p>
<h2 id="8-정리">8. 정리</h2>
<p>Kafka의 Replication 구조는 단순한 백업 기능이 아니다.
다음의 중요한 역할을 모두 수행한다.</p>
<ul>
<li>Leader–Follower 구조로 고가용성 확보</li>
<li>ISR로 Replica 정합성 유지</li>
<li>HW와 LEO로 데이터의 “안전한 상태” 구분</li>
<li>Producer ACK 설정에 따라 durability가 크게 좌우</li>
<li>Follower lag, ISR 축소, Leader 장애는 운영 핵심 이벤트</li>
<li>unclean leader election 설정은 반드시 OFF</li>
</ul>
<p>Replication 구조를 제대로 이해하면
Kafka에서 발생하는 유실·중복·지연·Failover 문제의 90% 이상을 설명할 수 있다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://velog.io/@ddongh1122/Kafka-%EC%9D%B4%ED%95%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-3">https://velog.io/@ddongh1122/Kafka-%EC%9D%B4%ED%95%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-3</a></p>
<p><a href="https://xonmin.tistory.com/80">https://xonmin.tistory.com/80</a></p>
<p><a href="https://colevelup.tistory.com/19">https://colevelup.tistory.com/19</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka Consumer Group 구조 – Rebalance, Partition Ownership, Lag]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-Consumer-Group-%EA%B5%AC%EC%A1%B0-Rebalance-Partition-Ownership-Lag</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-Consumer-Group-%EA%B5%AC%EC%A1%B0-Rebalance-Partition-Ownership-Lag</guid>
            <pubDate>Mon, 24 Nov 2025 14:40:09 GMT</pubDate>
            <description><![CDATA[<p>Kafka에서 Consumer Group은 단순히 “여러 Consumer가 메시지를 나눠 읽게 하는 기능”이 아니다.
Consumer Group 구조는 <strong>고가용성(HA), 병렬 처리, Failover, 부하 분산, Offset 관리, Rebalance 안정성</strong>까지 포함하는 Kafka 고유의 핵심 메커니즘이다.
Consumer Group을 제대로 이해하면 Kafka 메시지가 왜 특정 Consumer로만 배정되는지, Failover 시 어떤 편향이 생기는지, Lag이 왜 생기는지 등 대부분의 문제를 스스로 분석할 수 있게 된다.</p>
<h2 id="1-consumer-group이-필요한-이유">1. Consumer Group이 필요한 이유</h2>
<p>Kafka는 병렬 처리와 확장성 때문에 Partition 구조를 사용한다.
하지만 Partition을 여러 Consumer가 동시에 읽으면 순서가 깨지고 중복 처리 문제가 발생할 수 있다.
이를 해결하는 구조가 바로 <strong>Consumer Group</strong>이다.</p>
<h3 id="consumer-group-목적">Consumer Group 목적</h3>
<ol>
<li>각 Partition을 한 시점에 <em>오직 한 Consumer만</em> 읽도록 보장</li>
<li>Consumer 수를 늘려 병렬 처리량 획득</li>
<li>Consumer 장애 시 자동 Failover</li>
<li>Offset을 Group 단위로 관리하여 상태 일관성 유지</li>
</ol>
<p>예: Partition 6개, Consumer Group 1개 (3개 Consumer)
→ Consumer당 2개 Partition씩 배정되어 병렬 처리 가능</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/84df9e1a-9061-4439-aeee-c80a09a48686/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Topic의 Partition들은 Consumer Group A와 B에 각각 독립적으로 할당되며, 서로의 처리에 영향을 주지 않는다.</li>
<li>각 Consumer Group 내부에서는 Partition이 겹치지 않게 분배되어 Group 내에서 <strong>한 Partition은 반드시 하나의 Consumer만 읽는다</strong>.</li>
<li>Consumer Group을 늘리면 병렬 처리량이 증가하고, 각 Group은 자체 offset을 관리해 완전히 독립된 처리 흐름을 가진다.</li>
</ul>
<h2 id="2-partition-assignment-방식">2. Partition Assignment 방식</h2>
<p>Kafka는 Rebalance 시 Partition을 Consumer들에게 어떻게 배정할지 다양한 전략을 제공한다.
가장 많이 사용되는 세 가지는 다음과 같다.</p>
<h3 id="a-range-assignment">A. Range Assignment</h3>
<p>가장 기본적인 방식
Partition ID를 기준으로 Range 단위로 나눈다.
예: Partition 6개, Consumer 3명</p>
<ul>
<li>C1: 0,1</li>
<li>C2: 2,3</li>
<li>C3: 4,5</li>
</ul>
<p>특징</p>
<ul>
<li>단순하고 빠름</li>
<li>Partition Key가 특정 범위에 모이면 편향(Skew)이 발생할 수 있음<ul>
<li>여기서 말하는 편향이란 ?<ul>
<li>일부 Partition에만 데이터가 몰려서 특정 Consumer만 과부하가 걸리는 상황</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="b-round-robin-assignment">B. Round Robin Assignment</h3>
<p>Partition을 Consumer에게 순환 방식으로 배정한다.
예: Partition 6개, Consumer 3명</p>
<ul>
<li>C1: 0,3</li>
<li>C2: 1,4</li>
<li>C3: 2,5</li>
</ul>
<p>특징</p>
<ul>
<li>가장 균등 분배</li>
<li>Consumer와 Partition 수가 클 때 유리</li>
</ul>
<h3 id="c-sticky-assignment-kafka-최신-기본-방식">C. Sticky Assignment (Kafka 최신 기본 방식)</h3>
<p>Kafka 2.4 이후 기본 정책
Rebalance가 발생하더라도 “이전 배치를 최대한 유지”하려고 한다.</p>
<p>특징</p>
<ul>
<li>Rebalance 비용 최소화</li>
<li>Partition 재배치 최소</li>
<li>안정성 및 성능 개선</li>
</ul>
<h2 id="3-rebalance의-전체-과정">3. Rebalance의 전체 과정</h2>
<p>Rebalance는 Kafka Consumer Group에서 <strong>가장 중요한 동작</strong>이다.
여기서 Rebalance란 Kafka Consumer Group 안에서 Partition을 어떤 Consumer가 가져갈지 재조명 하는 과정이다.
Consumer Group 내 Consumer 수가 바뀌거나, Consumer가 느려지거나, Heartbeat가 끊기면 Rebalance가 발생한다.
Rebalance는 다음 단계로 진행된다.</p>
<h3 id="1-join-group">1) Join Group</h3>
<p>모든 Consumer는 GroupCoordinator에게 “나는 이 그룹에 참여할게” 요청을 보낸다.
이 단계에서는 Consumer 모두가 일시적으로 메시지 소비(poll loop)를 멈춘다.</p>
<h3 id="2-leader-consumer-선출">2) Leader Consumer 선출</h3>
<p>GroupCoordinator는 참여한 Consumer 중 한 명을 Leader로 지정한다.
Leader는 Partition–Consumer 매핑을 담당한다. (Partition Allocator)</p>
<h3 id="3-partition-assignment">3) Partition Assignment</h3>
<p>Leader는 할당 정책(Range, RoundRobin, Sticky 등)을 기준으로
“어떤 Consumer가 어떤 Partition을 가져갈지” 계산한다.</p>
<ul>
<li>Range 방식</li>
<li>RoundRobin 방식</li>
<li>Sticky 방식</li>
</ul>
<h3 id="4-sync-group">4) Sync Group</h3>
<p>Leader는 계산된 assignment를 GroupCoordinator에게 보내고, Coordinator는 이를 모든 Consumer에게 전파한다.</p>
<h3 id="5-각-consumer는-assigned-partition을-poll하여-소비-시작">5) 각 Consumer는 Assigned Partition을 poll()하여 소비 시작</h3>
<p>모든 Consumer가 새로운 Partition 세트를 받아 poll loop 재개</p>
<h3 id="rebalance가-일어나면-어떤-일이-발생하는가">Rebalance가 일어나면 어떤 일이 발생하는가?</h3>
<ul>
<li>모든 Consumer가 잠깐 멈춤</li>
<li>Offset commit이 이루어지지 않았다면 중복 처리 가능</li>
<li>Partition 소유권이 바뀌므로 상태(세션 캐시 등) 초기화 필요</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/7871e99a-3f74-494b-917e-231356479d60/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ol>
<li>Consumer가 리밸런스를 시작하면 기존 Partition을 반납(onRevoked)하고, JoinGroup → Assign → SyncGroup 순서로 새로운 Partition 할당을 받는다.</li>
<li>이 과정 동안 Heartbeat 간격, 네트워크 왕복 시간, Rebalance Listener 콜백(onRevoked/onAssigned)이 단계별로 발생한다.</li>
<li>전체 Rebalance Latency(파란색)는 Consumer가 메시지를 완전히 멈춘 채 Partition 재할당을 기다리는 시간 구간을 의미한다.</li>
</ol>
<hr>
<h2 id="4-consumer-failover">4. Consumer Failover</h2>
<p>Consumer 하나가 죽거나 느려지면 Kafka는 자동으로 Failover를 수행한다.</p>
<p>동작:</p>
<ol>
<li>특정 Consumer가 Heartbeat를 일정 시간 동안 보내지 않음</li>
<li>GroupCoordinator는 해당 Consumer를 그룹에서 제외</li>
<li>Rebalance 시작</li>
<li>해당 Consumer가 담당하던 Partition을 다른 Consumer에게 재배정</li>
<li>정상 소비 재개</li>
</ol>
<p>Kafka는 Consumer 장애를 별도의 알림 없이 자동으로 처리한다.
이 때문에 Kafka는 HA 분산 메시징 시스템으로 높은 신뢰성을 가진다.</p>
<h2 id="5-consumer-lag-의미-및-계산">5. Consumer Lag 의미 및 계산</h2>
<h3 id="consumer-lag이란">Consumer Lag이란?</h3>
<p>어떤 Partition에서 현재 Producer가 보내서 Kafka 서버가 기록한 “최신 Offset”과 Consumer가 “마지막으로 committed한 Offset”의 차이를 뜻한다.
즉,” 읽어야할 메시지가 얼마나 밀려있는가” 를 뜻한다.</p>
<pre><code>Lag = LatestOffset - CommittedOffset</code></pre><p>Lag가 높다는 것은?</p>
<ul>
<li>Consumer가 정상 속도로 데이터를 읽지 못하고 있다</li>
<li>처리량 병목 현상 발생</li>
<li>Scaling 또는 Consumer 튜닝 필요</li>
</ul>
<h3 id="lag가-발생하는-주요-원인">Lag가 발생하는 주요 원인</h3>
<ul>
<li>Consumer 처리 속도가 Producer보다 느림</li>
<li>Rebalance 직후 아직 데이터 읽기 시작 못함</li>
<li>Consumer 장애/지연</li>
<li>네트워크 부하</li>
</ul>
<h3 id="lag-모니터링은-kafka-운영의-필수-요소">Lag 모니터링은 Kafka 운영의 필수 요소</h3>
<p>대부분의 Kafka Dashboard(Grafana, Lenses, Conduktor 등)에서 Consumer Lag은 핵심 지표 중 하나다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/dfcd7c77-1b3b-4f89-b04e-90e4073f34b2/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Producer는 시간이 지날수록 더 높은 Offset(134 → 144 → 154)을 가진 메시지를 계속 생성한다.</li>
<li>Consumer는 뒤처진 Offset에서 읽기 때문에 Producer와 Consumer 사이에 Lag이 발생한다.</li>
<li>Consumer가 읽은 최신 Offset과 Producer가 생성한 최신 Offset의 차이가 바로 <strong>Consumer Lag</strong>이다.</li>
</ul>
<h2 id="6-consumer-확장과-한계">6. Consumer 확장과 한계</h2>
<p>Kafka에서 Consumer를 늘린다고 무조건 처리량이 세 배, 네 배 증가하지는 않는다.
Consumer 확장성에는 명확한 제약이 있다.</p>
<h3 id="제약-1-partition-수-≥-consumer-수">제약 1: Partition 수 ≥ Consumer 수</h3>
<p>Consumer 하나는 한 순간에 <strong>하나의 Partition만</strong> 담당할 수 있다.
따라서 Partition이 10개면 Consumer도 최대 10개까지만 병렬 처리 가능하다.</p>
<h3 id="제약-2-partition보다-consumer-수가-많으면">제약 2: Partition보다 Consumer 수가 많으면?</h3>
<ul>
<li>초과 Consumer들은 Idle 상태가 되며, Poll을 하지 않는다.</li>
<li>즉, Consumer는 Partition 수 만큼만 유효하게 동작한다.</li>
</ul>
<h3 id="제약-3-너무-많은-consumer는-rebalance-비용-증가">제약 3: 너무 많은 Consumer는 Rebalance 비용 증가</h3>
<p>Consumer가 늘어나면 Rebalance 시간이 비례해 늘어난다.
대규모 환경에서는 Rebalance가 장애처럼 느껴질 수 있다.</p>
<h3 id="실무-팁">실무 팁</h3>
<p>대부분 회사에서는 Partition 수를 다음 기준으로 정한다:</p>
<ul>
<li>Consumer 한 대 처리량 * Consumer 수 = 목표 처리량</li>
<li>Partition 수는 Consumer 수보다 약간 많게</li>
</ul>
<h2 id="7-groupcoordinator--heartbeat-구조">7. GroupCoordinator / Heartbeat 구조</h2>
<p>Consumer Group에는 “그룹을 관리하는 중앙 관리자”가 필요하다.
Kafka는 이 역할을 <strong>GroupCoordinator</strong>에게 맡긴다.</p>
<h3 id="groupcoordinator-역할">GroupCoordinator 역할</h3>
<ul>
<li>Consumer Group의 메타데이터 관리<ul>
<li>어떤 Consumer가 어떤 Group에 참여 중인지</li>
<li>어떤 Consumer가 살아 있는지/죽었는지</li>
<li>Partition–Consumer 매핑 정보 유지</li>
</ul>
</li>
<li>Consumer Join/Leave 이벤트 처리<ul>
<li>새로운 Consumer 등록</li>
<li>Group의 ‘leader consumer’를 다시 선출할지 여부 판단</li>
<li>필요한 경우 Rebalance 트리거</li>
</ul>
</li>
<li>Partition assignment 전달</li>
<li>commit한 Offset 저장 관리(__consumer_offsets)</li>
<li>Heartbeat 감시하여 Failover 처리<ul>
<li>모든 Consumer는 Coordinator에게 정기적으로 heartbeat를 보낸다.</li>
</ul>
</li>
</ul>
<h3 id="groupcoordinator는-어디에-존재하는가-">GroupCoordinator는 어디에 존재하는가 ?</h3>
<p>Kafka Broker 중 하나가 자동으로 특정 Consumer Group의 Coordinator 역할을 맡는다.</p>
<ul>
<li>Consumer Group ID → 내부 알고리즘으로 hash</li>
<li>hash 값을 특정 Broker에 매핑</li>
<li>그 Broker가 해당 Group의 Coordinator가 됨</li>
</ul>
<h3 id="heartbeat는-왜-중요한가">Heartbeat는 왜 중요한가?</h3>
<p>Consumer는 poll()과 별개로 Heartbeat를 정기적으로 보내 “나 아직 살아있다”를 Coordinator에게 알린다.</p>
<p>Heartbeat가 끊기면?</p>
<ul>
<li>Coordinator는 해당 Consumer를 죽은 것으로 판단</li>
<li>즉시 Rebalance 발생</li>
<li>Partition은 다른 Consumer에게 재배정</li>
</ul>
<p>Heartbeat가 느리면 Rebalance가 과도하게 발생할 수 있으므로 Consumer 애플리케이션에서 poll/처리 시간을 적절히 조절해야 한다.</p>
<h2 id="8-정리">8. 정리</h2>
<p>Consumer Group은 Kafka의 핵심이자 가장 섬세한 구성 요소다.
그 역할은 단순히 “여러 Consumer가 메시지를 나눠 읽는 것”에 그치지 않는다.</p>
<p>핵심 요약</p>
<ul>
<li>Consumer Group은 Partition–Consumer 1:1 매핑을 보장</li>
<li>Assignment 방식은 Range / Round Robin / Sticky</li>
<li>Rebalance는 Join → Leader 선출 → Assignment → Sync 흐름</li>
<li>Failover는 Heartbeat 기반으로 자동 처리</li>
<li>Lag는 Consumer 성능 병목을 나타내는 핵심 지표</li>
<li>Consumer 수는 Partition 수 이상의 스케일 아웃 불가</li>
<li>GroupCoordinator가 Commit, Rebalance, Heartbeat를 관리</li>
</ul>
<p>이 구조를 명확히 이해하면 Kafka의 메시지 처리 속도, 중복, 유실, Failover, Lag 원인을 대부분 설명할 수 있다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://ibm-cloud-architecture.github.io/refarch-eda/technology/kafka-consumers/">https://ibm-cloud-architecture.github.io/refarch-eda/technology/kafka-consumers/</a></p>
<p><a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-429%3A+Kafka+Consumer+Incremental+Rebalance+Protocol">https://cwiki.apache.org/confluence/display/KAFKA/KIP-429%3A+Kafka+Consumer+Incremental+Rebalance+Protocol</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka Offset & Commit – 메시지 중복·유실을 결정하는 핵심]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-Offset-Commit-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%A4%91%EB%B3%B5%EC%9C%A0%EC%8B%A4%EC%9D%84-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%ED%95%B5%EC%8B%AC</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-Offset-Commit-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%A4%91%EB%B3%B5%EC%9C%A0%EC%8B%A4%EC%9D%84-%EA%B2%B0%EC%A0%95%ED%95%98%EB%8A%94-%ED%95%B5%EC%8B%AC</guid>
            <pubDate>Mon, 24 Nov 2025 11:01:46 GMT</pubDate>
            <description><![CDATA[<p>Kafka의 메시지 처리에서 가장 중요한 요소는 “Offset을 어떻게 관리하느냐”다.
Offset은 단순한 숫자 같지만, 실제로는 <strong>중복 처리, 유실 방지, 재처리, 장애 복구, Rebalance 안정성</strong>을 좌우하는 핵심 개념이다.
Consumer의 모든 동작은 Offset을 기준으로 이루어진다.</p>
<p>이 글에서는 Offset의 의미부터 Commit 방식, Auto/Manual commit 차이, Commit 지연 시 실제 발생하는 문제, Rebalance와의 관계, 그리고 At-most-once / At-least-once / Exactly-once 동작 방식까지 체계적으로 정리한다.</p>
<h2 id="1-offset이란-무엇인가">1. Offset이란 무엇인가?</h2>
<p>Offset은 <strong>Partition 내 메시지의 고유 번호</strong>이다.
메시지가 Partition에 append될 때마다 Offset은 0부터 시작해 1씩 증가한다.</p>
<ul>
<li>Partition 0: 0, 1, 2, 3 …</li>
<li>Partition 1: 0, 1, 2, 3 …</li>
<li>Partition마다 독립적인 Offset 시퀀스가 존재</li>
</ul>
<p>Kafka는 메시지를 삭제하더라도 Offset 번호를 재사용하지 않는다.
즉, Offset은 <strong>파일의 라인 번호 같은 개념</strong>이다.</p>
<h3 id="왜-중요한가">왜 중요한가?</h3>
<p>Consumer는 Offset을 기준으로 “어디까지 읽었는지”를 판단한다.
즉, Offset 관리는 곧 <strong>메시지 처리 상태 관리</strong>다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/80e14448-1ab2-403f-a9a8-e42de7475632/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Producer는 Partition의 맨 끝에 새로운 메시지를 append하며, 메시지는 0부터 연속된 offset 번호를 갖는다.</li>
<li>Consumer A와 Consumer B는 같은 Partition을 읽지만, 각자 다른 위치(offset)를 기준으로 읽고 있다.</li>
<li>Kafka는 Consumer가 읽은 offset을 기반으로 “어디까지 처리했는지”를 관리하게 되고, 이는 메시지 처리 상태 추적의 핵심이 된다.</li>
</ul>
<h2 id="2-consumer는-offset을-어떻게-추적하는가">2. Consumer는 Offset을 어떻게 추적하는가?</h2>
<p>Consumer는 두 가지 상태를 관리한다.</p>
<h3 id="1-현재-처리한-offset-processed-offset">(1) 현재 처리한 Offset (processed offset)</h3>
<p>Consumer가 실제로 데이터를 읽고 처리 완료한 위치</p>
<h3 id="2-commit된-offset-committed-offset">(2) Commit된 Offset (committed offset)</h3>
<p>Kafka에 “여기까지 읽었음”이라고 공식적으로 저장한 위치
Kafka는 이 정보를 <strong>__consumer_offsets</strong>라는 내부 토픽에 저장한다.</p>
<p>즉, 처리한 offset과 Commit된 offset은 같을 수도 있고 다를 수도 있다.</p>
<blockquote>
<p>Consumer가 재시작하면 “Commit된 Offset”부터 다시 읽는다.</p>
</blockquote>
<p>어떻게 이런 상황들이 발생할까 ?</p>
<ul>
<li>Consumer는 메시지를 처리(process)한 시점과 Kafka에 commit 기록하는 시점이 서로 다르다.</li>
<li>처리 도중 장애가 나면 processed offset은 증가했지만 commit은 못 해서 둘이 달라진다.</li>
<li>반대로 auto commit이 먼저 일어나면 처리 전에 commit이 저장되어 메시지 유실이 발생할 수도 있다.</li>
<li>따라서 Consumer 재시작 시에는 항상 <strong>committed offset 기준</strong>으로 다시 읽기 때문에 두 값의 차이가 발생한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/b13cb7ed-55c9-4ccc-b81d-1eda56924d3b/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ol>
<li>Consumer는 offset 2까지만 commit했지만, poll()로 3~11까지 읽어 처리 중이다.</li>
<li>commit되지 않은 3~10 구간은 리밸런스나 재시작 시 다시 읽혀 <strong>중복 처리</strong>가 발생할 수 있다.</li>
<li>오른쪽의 offset 10은 현재 처리 중인 이벤트이며, commit 지점과 processed 지점의 차이가 중복 발생 영역임을 보여준다.</li>
</ol>
<h2 id="3-commit-구조--auto-commit-vs-manual-commit">3. Commit 구조 – Auto Commit vs Manual Commit</h2>
<p>Commit은 “어디까지 처리했는지 Kafka에게 저장하는 일”이다.
Commit 방식은 크게 두 가지다.</p>
<h3 id="a-auto-commit">A. Auto Commit</h3>
<p>기본 설정: enable.auto.commit=true
Kafka Consumer 라이브러리가 일정 주기마다 자동 커밋한다.</p>
<p>동작 방식:</p>
<ul>
<li>poll()으로 데이터를 읽는다</li>
<li>처리 여부와 상관없이 주기마다 offset commit</li>
</ul>
<p>장점:</p>
<ul>
<li>코드가 매우 단순</li>
<li>빠르게 개발할 때 편함</li>
</ul>
<p>단점:</p>
<ul>
<li>메시지가 실제로 처리되지 않았는데도 commit될 위험</li>
<li>장애 시 메시지가 유실될 가능성</li>
<li>실무에서 거의 사용하지 않음</li>
</ul>
<p>Auto commit은 편하지만 <strong>정확한 처리</strong>가 필요한 시스템에서는 위험하다.</p>
<h3 id="b-manual-commit">B. Manual Commit</h3>
<p>enable.auto.commit=false
개발자가 직접 commit 시점을 제어한다.</p>
<p>종류는 두 가지:</p>
<ol>
<li><strong>Sync commit (commitSync)</strong></li>
<li><strong>Async commit (commitAsync)</strong></li>
</ol>
<h3 id="commitsync">commitSync</h3>
<p>Kafka에 commit 요청을 보낸 뒤, commit이 성공할 때까지 기다린다.</p>
<p>장점:</p>
<ul>
<li>확실함 (실패 시 재시도 가능)</li>
</ul>
<p>단점:</p>
<ul>
<li>속도가 느릴 수 있음</li>
<li>대량 스트림 처리에서는 성능 저하 가능</li>
</ul>
<h3 id="commitasync">commitAsync</h3>
<p>Kafka에게 commit 요청만 보내고 결과를 기다리지 않는다.</p>
<p>장점:</p>
<ul>
<li>빠름</li>
<li>Throughput이 중요할 때 적합</li>
</ul>
<p>단점:</p>
<ul>
<li>commit이 실제로 실패했을 때 복구가 어려움</li>
<li>순서 보장이 어렵기도 함</li>
</ul>
<p>실무에서는 다음과 같이 섞어서 사용한다.</p>
<ul>
<li>일반 메시지 처리: commitAsync</li>
<li>종료 직전 / Rebalance 직전: commitSync<ul>
<li>종료직전과 Rebalance 직전을 어떻게알까 ?<ul>
<li><strong>애플리케이션 종료 타이밍</strong>은 JVM의 Shutdown Hook(try-with-resources, SIGTERM 등)을 등록하여 감지하고 종료 직전 commitSync()를 호출</li>
<li><strong>리밸런스 직전 타이밍</strong>은 Kafka가 제공하는 ConsumerRebalanceListener의 onPartitionsRevoked() 콜백에서 감지됨</li>
<li>즉, 종료는 OS/JVM 신호로, 리밸런스는 Kafka 내부 프로토콜 이벤트로 각각 자동 감지됨</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="rebalance란-무엇인가">Rebalance란 무엇인가?</h3>
<p>→ Rebalance는 Consumer Group 내부에서 Partition 할당이 변경되는 작업을 뜻한다.</p>
<p>그럼 언제 Rebalance가 발생할까?</p>
<ul>
<li>Consumer Group에 새로운 Consumer가 추가됨 → 파티션 재분배</li>
<li>Consumer 하나가 죽음 → 남은 Consumer에게 파티션 재배분</li>
<li>Consumer가 너무 오래 poll() 안 함 → 그룹에서 제외됨</li>
<li>구독한 Topic의 Partition 개수가 증가함</li>
</ul>
<h2 id="4-offset-commit-실패·지연-시-실제로-발생하는-문제">4. Offset commit 실패·지연 시 실제로 발생하는 문제</h2>
<p>Offset commit이 중요하다는 건 이 문제를 보면 바로 이해된다.</p>
<h3 id="a-commit이-너무-빠르면-→-메시지-유실">A. Commit이 너무 빠르면 → 메시지 유실</h3>
<p>예:</p>
<ol>
<li>offset=10까지 commit</li>
<li>메시지 11~20 읽음</li>
<li>아직 처리 중인데 장애 발생</li>
</ol>
<p>Consumer 재시작하면 offset=10 다음인 11부터 읽어야 한다.
하지만 11~20은 이미 처리 중이었지만 commit되지 않았기 때문에 <strong>다시 읽게 된다.</strong>
반대로, commit을 처리가 끝나기 전 미리 해버리면?</p>
<ol>
<li>offset=20까지 commit</li>
<li>21~30 읽는 중 장애 발생</li>
<li>재시작 시 offset=21부터 읽음</li>
</ol>
<p>하지만 21~30 처리 도중 실패했는데 commit은 이미 20까지 되어 있으므로
그 전에 메시지를 잃을 위험이 있다.</p>
<h3 id="b-commit이-너무-느리면-→-메시지-중복-처리">B. Commit이 너무 느리면 → 메시지 중복 처리</h3>
<p>Commit이 지연되는 경우:</p>
<ol>
<li>offset=20까지 commit</li>
<li>21~30 읽고 처리 완료</li>
<li>commitSync 지연 발생</li>
<li>장애 발생</li>
</ol>
<p>다시 시작하면 offset=20 다음인 21부터 다시 읽는다 → <strong>중복 처리</strong>
Kafka에서는 특별한 작업 없이도 중복 처리가 쉽게 발생한다.
그래서 idempotent(멱등성) 구현이 매우 중요하다.</p>
<h2 id="5-rebalance와-offset의-관계">5. Rebalance와 Offset의 관계</h2>
<p>Consumer Group은 동적으로 consumer 개수가 변화할 수 있고, Partition은 Rebalance 과정에서 재할당된다.</p>
<p>Rebalance가 발생하면</p>
<ol>
<li>Consumer는 poll 종료</li>
<li>모든 Consumer가 현재 처리한 메시지를 정리</li>
<li>각 Consumer는 마지막 상태를 commit</li>
<li>GroupCoordinator가 파티션 재할당</li>
</ol>
<p>즉, Rebalance의 안정성은 <strong>commit 시점</strong>에 달려 있다.
Rebalance 전에 commit하지 않은 메시지는 다른 Consumer에게 넘어가면서 <strong>중복 처리될 가능성</strong>이 있다.
이 때문에 실무에서는 Rebalance Listener를 활용해 Rebalance 시작 시 commitSync를 수행하는 패턴을 많이 사용한다.</p>
<h2 id="6-at-most-once--at-least-once--exactly-once-메시지-처리-모델">6. At-most-once / At-least-once / Exactly-once 메시지 처리 모델</h2>
<p>Kafka는 Offset commit 방식에 따라 메시지 처리 모델이 달라진다.</p>
<h3 id="a-at-most-once-최대-한-번-처리-→-유실-가능성-있음">A. At-most-once (최대 한 번 처리 → 유실 가능성 있음)</h3>
<p>동작:</p>
<ol>
<li>메시지 읽기 전 commit</li>
<li>처리</li>
<li>실패해도 이미 commit 되어 있음</li>
</ol>
<p>특징:</p>
<ul>
<li>빠르지만 신뢰성이 낮음</li>
<li>유실 가능성 존재</li>
<li>실무에서 잘 사용하지 않음</li>
</ul>
<h3 id="b-at-least-once-최소-한-번-처리-→-중복-가능성">B. At-least-once (최소 한 번 처리 → 중복 가능성)</h3>
<p>동작:</p>
<ol>
<li>메시지 처리</li>
<li>처리 후 commit</li>
</ol>
<p>장점:</p>
<ul>
<li>메시지 유실 없음</li>
</ul>
<p>단점:</p>
<ul>
<li>중복 처리 발생 가능</li>
<li>대부분 Kafka Consumer 기본 모델</li>
</ul>
<p>오늘날 기업들의 90% 이상 Kafka 아키텍처가 이 모델을 사용한다.</p>
<h3 id="c-exactly-once-정확히-한-번-처리">C. Exactly-once (정확히 한 번 처리)</h3>
<p>Kafka Streams나 Transactional Producer에서 제공하는 모델이다.</p>
<p>Producer + Consumer 조합에서 Exactly-once 구현하려면:</p>
<ul>
<li>Transactional Producer</li>
<li>idempotent 메시지 처리</li>
<li>atomic commit</li>
</ul>
<p>이 구조는 까다롭고 운영 난이도도 있다.
그래서 “일반 Consumer → DB insert” 같은 시나리오에서는
대부분 At-least-once로 처리하고
비즈니스 레이어에서 중복 제거(idempotence) 처리를 한다.</p>
<h2 id="7-정리">7. 정리</h2>
<p>Offset은 단순한 번호가 아니라 Kafka 메시지 처리의 기준점이다.</p>
<p>Offset을 commit하는 방식에 따라 Kafka는 다음처럼 동작한다.</p>
<ul>
<li>Auto commit → 편하지만 메시지 유실 위험</li>
<li>Manual commit → 안정적이지만 개발 비용 증가</li>
<li>commitSync / commitAsync 각각 장단점 존재</li>
<li>commit 타이밍 잘못 잡으면 중복 또는 유실 발생</li>
<li>Rebalance 안정성은 commit 시점에 좌우</li>
<li>Kafka의 기본 모델은 &quot;At-least-once&quot;</li>
<li>Exactly-once는 Streams나 Transactional Producer에서만</li>
</ul>
<p>Offset을 정확히 이해하면 “Kafka 메시지가 왜 중복되었는지, 왜 유실되었는지”를 99% 원인 분석할 수 있게 된다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://sjh9708.tistory.com/269">https://sjh9708.tistory.com/269</a></p>
<p><a href="https://shubhamagtech.home.blog/2019/07/31/kafka-offset-management/">https://shubhamagtech.home.blog/2019/07/31/kafka-offset-management/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka Partition 완전정복 – 병렬 처리, Key Routing, 순서 보장 단위]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-Partition-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC-Key-Routing-%EC%88%9C%EC%84%9C-%EB%B3%B4%EC%9E%A5-%EB%8B%A8%EC%9C%84</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-Partition-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC-Key-Routing-%EC%88%9C%EC%84%9C-%EB%B3%B4%EC%9E%A5-%EB%8B%A8%EC%9C%84</guid>
            <pubDate>Sun, 23 Nov 2025 13:47:09 GMT</pubDate>
            <description><![CDATA[<p>카프카의 성능, 확장성, 메시지 처리 구조를 이해하려면 <strong>Partition</strong>이라는 개념을 완전히 이해해야 한다.
Partition은 단순히 Topic을 쪼개는 구조가 아니라, 카프카가 <strong>대규모 스트림을 병렬로 처리하고 순서를 유지하며 빠르게 동작</strong>하는 이유를 모두 담고 있다.
이 글에서는 Partition의 역할부터 내부 구조, 메시지 라우팅 방식, 순서 보장 규칙, Sticky Partitioner, Partition 수를 결정하는 실무 기준까지 자세하게 정리한다.</p>
<h2 id="1-partition은-왜-필요한가-가장-근본적인-이유">1. Partition은 왜 필요한가? (가장 근본적인 이유)</h2>
<p>Partition은 다음 세 가지 목적을 동시에 충족시키기 위해 존재한다.</p>
<h3 id="1-병렬-처리-parallelism">1) 병렬 처리 (Parallelism)</h3>
<p>Topic을 여러 Partition으로 나누면, 각 Partition을 서로 다른 Consumer가 동시에 읽을 수 있다.</p>
<ul>
<li>파티션 1개 → Consumer 1개만 처리 가능</li>
<li>파티션 6개 → Consumer 6개까지 병렬 처리 가능</li>
</ul>
<p>즉, Partition 수가 Kafka 성능의 상한선을 결정한다.</p>
<h3 id="2-분산-저장">2) 분산 저장</h3>
<p>Partition은 하나의 Broker에 몰리지 않고 <strong>여러 Broker에 균등하게 나누어 저장된다.</strong>
즉, Topic 전체 데이터가 1개의 서버에 집중되는 것이 아니라, Partition 단위로 물리적으로 분산된다.
이 덕분에 Topic이 매우 큰 규모로 성장하더라도 저장과 처리 부담이 여러 Broker로 자동 분산되어, 개별 Broker에 과도한 부하가 몰리는 일을 방지할 수 있다.</p>
<h3 id="3-확장성">3) 확장성</h3>
<p>처리량이 부족하면?</p>
<ul>
<li><p>Consumer 개수 추가</p>
</li>
<li><p>Partition 개수 증가</p>
<p>  = 처리량 바로 상승</p>
</li>
</ul>
<p>Partition은 카프카의 스케일아웃 구조를 뒷받침하는 핵심이다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/d50bad69-cbeb-418e-bb9d-134ec37b764c/image.png" alt=""></p>
<p>그림으로 다시보자.
Producer가 보낸 메시지는 Kafka Broker 내부에서 Topic별로 구분되고, 각 Topic은 여러 Partition으로 나누어 저장된다.
Partition들은 병렬 처리를 위해 Consumer Group 내의 여러 Consumer에게 나누어 전달된다.
즉, 이 구조는 Kafka가 <strong>데이터를 Topic으로 분리하고, Partition으로 분산해 저장하며, Consumer Group으로 병렬 소비</strong>하는 전체 아키텍처를 설명한다.</p>
<h2 id="2-partition-내부-구조">2. Partition 내부 구조</h2>
<p>Partition은 내부적으로 append-only 로그이며, Segment 파일로 구성된다.
이 구조는 1편에서 설명했지만, Partition 관점에서 조금 더 상세히 보면 다음과 같다.</p>
<h3 id="partition-내부-구성">Partition 내부 구성</h3>
<ul>
<li>메시지는 오프셋 순서대로 파일 끝에 추가된다</li>
<li>Partition은 여러 Segment로 나뉘어 저장된다</li>
<li>각 Segment는 로그 파일(.log) + 인덱스(.index, .timeindex)로 구성된다</li>
</ul>
<h3 id="중요한-점">중요한 점</h3>
<p>Partition은 서로 독립적이며,
하나의 Partition에 저장된 메시지는 다른 Partition과 독립된 순서/타임라인을 가진다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/94923adc-4351-43f5-9940-d39cdd57212a/image.png" alt=""></p>
<p>1편에서도 등장했던 그림인데 이만한 그림이 없다. 다시보자.
Kafka의 Partition 내부에서 <strong>timeindex → index → log</strong> 순서로 timestamp 기반 메시지 검색이 이루어지는 구조를 보여준다.
timeindex는 timestamp로 offset을 찾고, index는 offset으로 log 파일의 byte 위치를 찾으며, log 파일에서 실제 메시지를 읽어오는 전체 탐색 과정을 시각적으로 나타낸 그림이다.</p>
<h2 id="3-메시지-순서-보장-범위는-partition-단위다">3. 메시지 순서 보장 범위는 “Partition 단위”다</h2>
<p>Kafka의 가장 중요한 규칙 중 하나는 <strong>순서 보장은 Topic 전체가 아니라 Partition 단위</strong>라는 점이다.</p>
<h3 id="왜-topic-전체-순서를-보장하지-않나">왜 Topic 전체 순서를 보장하지 않나?</h3>
<p>Topic 전체의 순서를 보장하려면 다음이 필요하다.</p>
<ul>
<li>메시지 한 줄로만 처리</li>
<li>Consumer 1개만 읽을 수 있음</li>
<li>병렬 처리 불가</li>
</ul>
<p>이러면 Kafka의 장점이 완전히 사라진다.
그래서 카프카는 <strong>Partition 내부에서만 순서 보장</strong>이라는 현실적인 구조를 선택했다.</p>
<h3 id="순서-보장을-원하는-경우">순서 보장을 원하는 경우</h3>
<p>Key를 지정해서 특정 Partition으로만 메시지가 가도록 해야 한다.</p>
<p>예시)</p>
<ul>
<li>같은 User ID 이벤트가 항상 동일 Partition으로 들어가야 함</li>
<li>같은 Order ID 이벤트는 한 Partition으로 몰아야 함</li>
</ul>
<p>이게 바로 <strong>Key Based Routing</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/a9b8cba5-38a6-4841-97fe-b6745eac8850/image.png" alt=""></p>
<p>그림으로 다시보자.</p>
<ul>
<li>Producer가 Topic으로 메시지를 보내면 Kafka는 이를 여러 Partition으로 나누어 Broker 1, Broker 2에 분산 저장한다.</li>
<li>각 Partition 내부에서는 메시지가 0 → 1 → 2 → … 순서대로 append 되며, Partition 간에는 순서가 독립적이다.</li>
<li>Consumer는 Broker들에 분산 저장된 Partition들을 병렬로 읽으며 높은 처리량을 유지한다.</li>
</ul>
<h2 id="4-key-기반-partitioner-동작-방식">4. Key 기반 Partitioner 동작 방식</h2>
<p>Producer는 메시지를 보낼 때 Partition을 직접 지정하지 않으면 카프카 내부 <strong>Partitioner</strong>가 Partition을 결정한다.</p>
<h3 id="key가-있을-때">Key가 있을 때</h3>
<pre><code class="language-jsx">String topic = &quot;user-login-events&quot;;
String key = &quot;user_1001&quot;;    // userId
String value = &quot;{ \&quot;event\&quot;: \&quot;LOGIN_SUCCESS\&quot;, \&quot;ip\&quot;: \&quot;10.10.0.2\&quot; }&quot;;

ProducerRecord&lt;String, String&gt; record =
        new ProducerRecord&lt;&gt;(topic, key, value);

producer.send(record);</code></pre>
<p>위처럼 key(userId)를 지정하면 ..
기본 Partitioner는 다음 알고리즘 방식으로 partition을 정한다.</p>
<pre><code>hash(key) % 파티션 개수</code></pre><p>즉, 같은 Key는 항상 같은 Partition으로 간다.
→ 동일한 key에 대해서는 같은 hash 결과를 도출하기 때문에 항상 같은 Partition으로 가는것.
이 덕분에 Key 단위 순서 보장이 가능해진다.</p>
<p>예시)</p>
<ul>
<li>user-1001 → Partition 0</li>
<li>user-1002 → Partition 1</li>
<li>user-1003 → Partition 0 (hash값이 같으면)</li>
</ul>
<h3 id="key가-필요할-때">Key가 필요할 때</h3>
<ul>
<li>유저 단위 액션 처리</li>
<li>주문 단위 이벤트 처리</li>
<li>특정 그룹의 트랜잭션 순서 유지</li>
</ul>
<p>이런 케이스에서는 반드시 Key를 설정해야 한다.</p>
<h3 id="but-key가-없다면-">But, Key가 없다면 ?</h3>
<pre><code class="language-jsx">P0 → P1 → P2 → P0 → P1 → ...</code></pre>
<p>이렇게 돌아가면서 메시지를 분배한다.
즉 Round-robin 방식으로 Partition을 골라넣는다.
다음 내용에서 더 자세하게 알아보자.</p>
<p>Kafka를 적용할 업무에 대한 순서의 중요도를 잘 판단하여 Key의 유무를 선택하면 되겠다.</p>
<h2 id="5-sticky-partitioner-kafka-24-이후-기본-partitioner">5. Sticky Partitioner: Kafka 2.4 이후 기본 Partitioner</h2>
<p>Kafka의 Producer는 여러 메시지를 <strong>Batch 단위</strong>로 묶어서 보내면 성능이 크게 향상된다.
하지만 기존 Round-Robin 방식은 이 batch 효율을 깨뜨리는 문제가 있었다.
Sticky Partitioner는 이 문제를 해결하기 위해 나온 설계다.</p>
<h3 id="기존-round-robin-문제점">기존 Round-Robin 문제점</h3>
<p>Kafka 2.4 이전에는 Key가 없을 때 partition을 이렇게 정했다:</p>
<pre><code>P0 → P1 → P2 → P0 → P1 → P2 …</code></pre><p>즉, 매 메시지마다 다음 partition으로 넘어가는 방식</p>
<h3 id="문제는">문제는?</h3>
<p>Producer는 내부적으로 아래 구조를 가진다.</p>
<ul>
<li>partition별로 <strong>batch buffer</strong>가 따로 존재한다</li>
<li>같은 partition에 메시지가 많이 들어와야 batch 크기가 커진다</li>
</ul>
<p>Round-Robin은 메시지를 <strong>골고루 흩뿌리기 때문에</strong></p>
<ul>
<li>모든 partition의 batch가 1~2개씩밖에 쌓이지 않음</li>
<li>batch 사이즈가 작아서 네트워크 호출이 증가함</li>
<li>Producer 성능 저하</li>
</ul>
<p>즉, 너무 <strong>균등하게</strong> 분산하는 바람에 오히려 성능이 떨어졌음.</p>
<h3 id="sticky-partitioner의-핵심-아이디어"><strong>Sticky Partitioner의 핵심 아이디어</strong></h3>
<p><strong>“일단 한 Partition에 몰아서 보내자”</strong></p>
<p>Sticky Partitioner는 이렇게 동작한다.</p>
<ol>
<li><strong>현재 선택된 Partition이 하나 있다</strong></li>
<li>Key가 없는 모든 메시지가 <strong>당분간 그 Partition에 계속 append</strong>됨</li>
<li>batch가 꽉 차거나 timeout이 지나면</li>
<li><strong>그제서야 다른 Partition으로 스위칭</strong></li>
</ol>
<pre><code>[단계 1] P0에 계속 보내기 → batch 크기 크게
→ Flush / 전송 후

[단계 2] P1에 계속 보내기 → batch 크기 크게
→ Flush 후

[단계 3] P2에 계속 보내기 → …</code></pre><p>이 구조라서 <strong>균등 분산 + 높은 batch 효율</strong>을 모두 얻는다.</p>
<h3 id="sticky-partitioner의-장점"><strong>Sticky Partitioner의 장점</strong></h3>
<ol>
<li><p><strong>batch 크기 증가 → Producer TPS 상승</strong></p>
<ul>
<li>같은 partition에 몰아서 보내므로 큰 batch가 만들어진다.</li>
<li>큰 batch = 네트워크 호출 횟수 감소 = TPS 증가</li>
</ul>
</li>
<li><p><strong>Broker/Network 효율 향상</strong></p>
<p> 큰 batch는 압축 효율도 좋아서</p>
<ul>
<li>throughput 증가</li>
<li>네트워크 사용량 감소</li>
</ul>
</li>
<li><p><strong>partition 편중 문제 없음</strong></p>
<p> 처음 들으면 &quot;한 partition에 몰아서 보내면 한쪽이 터지는거 아니야?&quot;라고 걱정하지만 Kafka는 다음을 기준으로 <strong>자연스럽게 partition을 변경</strong>한다.</p>
<ul>
<li><p>batch.max.size</p>
</li>
<li><p>linger.ms</p>
</li>
<li><p>buffer pool pressure</p>
<p>그래서 장기적으로는 Partition 분산이 이루어진다.</p>
</li>
</ul>
</li>
</ol>
<h2 id="6-partition-수-설계-기준-실무-관점">6. Partition 수 설계 기준 (실무 관점)</h2>
<p>실무에서 Partition 개수를 어떻게 정하는가?
아래는 실제 Kafka 운영팀에서 사용하는 기준들이다.</p>
<h3 id="1-필요한-처리량-기준">1) 필요한 처리량 기준</h3>
<p>Partition 하나는 대략 다음 정도 처리한다:</p>
<ul>
<li>SSD 기준: 초당 5~50MB</li>
<li>HDD 환경: 초당 2~10MB</li>
</ul>
<p>필요한 Topic 처리량 / Partition 성능 = Partition 개수</p>
<p>예를들어, 로그성 데이터가 초당 300MB 들어오는 Topic이 있다면</p>
<pre><code>필요 처리량 300MB/s
SSD 파티션 1개 성능 약 30MB/s 라고 하면
300 / 30 = 10개의 파티션 필요</code></pre><p>즉, 이 Topic은 최소 10 partition이 필요하다는 계산이 나온다.</p>
<h3 id="2-consumer-확장성">2) Consumer 확장성</h3>
<p>Consumer Group 확장 한계는 <strong>Partition 수</strong>이다.</p>
<p>예를들어, 
Partition이 12개 → Consumer 최대 12개까지 병렬 처리 가능
Consumer를 20대까지 늘리고 싶다면 Partition을 20개 이상 만들어야 한다.</p>
<h3 id="3-replication-비용-고려">3) Replication 비용 고려</h3>
<p>Replication Factor가 3이고 Partition이 30개라면?
브로커 전체에 총 90개의 파티션 replica가 저장됨</p>
<p>Partition 수가 많아질수록</p>
<ul>
<li>복제 트래픽 증가</li>
<li>재배치(rebalance) 비용 증가</li>
<li>Broker 메모리 관리 비용 증가</li>
</ul>
<p>적당한 선에서 잡아야 한다.</p>
<h3 id="4-운영-난이도">4) 운영 난이도</h3>
<p>Partition이 너무 많으면 다음이 문제가 된다.</p>
<ul>
<li>Rebalance 오래 걸림</li>
<li>Controller 부하 증가</li>
<li>Cluster 재시작 시 재할당 시간 증가</li>
<li>Monitoring key 수 증가</li>
</ul>
<p>진짜 대규모(high throughput) 서비스가 아니라면 무작정 Partition을 늘리는 건 오히려 해가 된다.</p>
<h3 id="5-일반적인-권장치">5) 일반적인 권장치</h3>
<p>Kafka 운영 경험이 많은 기업들은 다음과 같은 기준을 사용한다.</p>
<table>
<thead>
<tr>
<th>서비스 규모</th>
<th>Partition 개수</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>일반적인 업무 서비스</strong></td>
<td>6–12개</td>
<td>웹 로그, 인증 이벤트 등 평범한 트래픽</td>
</tr>
<tr>
<td><strong>고부하 이벤트 스트림</strong></td>
<td>24–48개</td>
<td>주문, 결제, 센서 스트림 등 고처리량</td>
</tr>
<tr>
<td><strong>초대규모 트래픽</strong></td>
<td>100개 이상</td>
<td>전문 Kafka 운영팀 + 자동화 툴 필수</td>
</tr>
<tr>
<td><strong>대기업 데이터 플랫폼</strong></td>
<td>300~1,000개</td>
<td>(하지만 운영 난이도 매우 높음)</td>
</tr>
</tbody></table>
<p>대부분의 서비스는 사실 6~24개만 돼도 충분하다.</p>
<h2 id="7-정리">7. 정리</h2>
<p>Partition은 Kafka의 성능과 확장성을 만들어내는 가장 중요한 구조다.</p>
<p>핵심 포인트는 다음과 같다.</p>
<ul>
<li>Partition은 병렬 처리의 기본 단위다.</li>
<li>순서 보장은 Partition 내부에서만 이루어진다.</li>
<li>Key 기반 routing은 순서를 유지하는 핵심이다.</li>
<li>Kafka 2.4 이후에는 Sticky Partitioner가 기본</li>
<li>Partition 개수는 처리량·복제 비용·운영 난이도를 종합해서 결정해야 한다.</li>
</ul>
<p>Partition 구조를 확실히 이해하면 Offset, Consumer Group, ACK, Replication 같은 다음 개념들이 훨씬 명확해진다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://medium.com/@mkumar9009/kafka-partition-strategies-trade-offs-monitoring-in-real-world-b2d7ca5ce180">https://medium.com/@mkumar9009/kafka-partition-strategies-trade-offs-monitoring-in-real-world-b2d7ca5ce180</a></p>
<p><a href="https://magpienote.tistory.com/212">https://magpienote.tistory.com/212</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka 기본 구조 – Topic, Partition, Broker, Log Segments]]></title>
            <link>https://velog.io/@1im_chaereong/Kafka-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0-Topic-Partition-Broker-Log-Segments</link>
            <guid>https://velog.io/@1im_chaereong/Kafka-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0-Topic-Partition-Broker-Log-Segments</guid>
            <pubDate>Sun, 23 Nov 2025 12:52:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/fb05cfab-5b89-4e20-bf4f-cb413f569b18/image.png" alt=""></p>
<p>위 그림은 Kafka의 가장 단순하고 핵심적인 구조를 보여준다.
Producer들이 데이터를 보내면, Kafka Cluster 내부의 여러 Broker가 이를 받아 저장하고, Consumer들은 저장된 데이터를 가져가는 형태다.
카프카를 깊게 이해하는 첫 단계는 이 단순한 그림 뒤에서 <strong>메시지가 실제로 어떻게 저장되고 관리되는지</strong>를 정확히 파악하는 것이다. 카프카는 메시지를 일시적으로 전달하는 단순 큐가 아니다.
내부적으로는 <strong>분산 로그 저장 시스템</strong>이며, 메시지를 디스크에 효율적으로 저장하고 복제하는 구조를 기반으로 높은 처리량, 안정성, 확장성을 제공한다.</p>
<p>이 글에서는 Topic, Partition, Broker 같은 기본 요소부터 Partition 내부의 Segment·Index 파일 구조, 메시지가 저장되는 흐름, Leader–Follower 복제 구조까지 핵심 동작 원리를 체계적으로 정리한다.</p>
<h2 id="1-topic-partition-broker의-기본-개념">1. Topic, Partition, Broker의 기본 개념</h2>
<p>Kafka에서 데이터는 Topic 단위로 관리되며, Topic은 단지 이름만 존재하는 <strong>논리적 구분</strong>이다.
실제 메시지가 저장되는 물리적 단위는 Partition이다.</p>
<h3 id="topic">Topic</h3>
<ul>
<li>메시지 스트림을 구분하는 이름</li>
<li>예: user-login, order-complete, payment-event</li>
</ul>
<h3 id="partition">Partition</h3>
<ul>
<li>Topic을 나누어 저장하는 최소 물리 단위</li>
<li>각 Partition은 독립적인 로그 파일</li>
<li>순서가 보장되는 범위는 <strong>Partition 내부</strong>까지만<ul>
<li>Why?<ul>
<li><strong>Partition은 append-only 로그이기 때문</strong><ul>
<li>Kafka는 Partition을 뒤에만 붙이는(append) 단일 로그 파일로 관리한다.</li>
<li>append-only 구조는 자연스럽게 순서를 형성하므로, Partition 내부 순서는 보장된다.</li>
</ul>
</li>
<li><strong>단일 Writer 모델이기 때문</strong><ul>
<li>하나의 Partition에는 단 하나의 직렬화된 쓰기 스트림만 존재한다.</li>
<li>동시에 여러 Writer가 들어오지 않으므로 순서가 섞일 일이 없다.</li>
</ul>
</li>
<li><strong>전역 순서를 유지하려면 병목이 생기기 때문</strong><ul>
<li>Topic 전체에서 순서를 강제하면 모든 메시지가 단일 큐를 거쳐야 하고,
  이는 처리량을 극단적으로 제한한다. → 수평 확장 불가</li>
</ul>
</li>
<li><strong>분산 환경에서 전역 순서는 사실상 불가능하기 때문</strong><ul>
<li>여러 Broker, 여러 Partition이 존재하는 구조에서 전체 순서를 맞추려면
  글로벌 락 또는 글로벌 리더만 존재해야 한다 → 내결함성과 가용성 붕괴</li>
</ul>
</li>
<li><strong>Kafka가 선택한 확장성 철학 때문</strong><ul>
<li>Kafka는 서버를 늘리면 처리량이 선형 증가하도록 설계되었다.</li>
<li>Partition을 늘려 병렬 소비가 가능해야 하므로 전체 순서는 포기하고 파티션 단위만 보장</li>
</ul>
</li>
<li><strong>비즈니스적으로 필요한 순서는 대부분 key 단위이기 때문</strong><ul>
<li>userId/계좌번호/주문번호 같은 동일 key만 순서가 중요하다.</li>
<li>Kafka는 이 key를 같은 Partition으로 라우팅함으로써 필요한 부분만 정확히 순서를 유지한다.</li>
</ul>
</li>
<li><strong>Partition이 확장성과 순서를 동시에 만족하는 최소 단위이기 때문</strong><ul>
<li>Partition = 여러 개로 쪼개면 병렬 처리 → 확장성</li>
<li>Partition 내부는 순수 로그 → 순서 보장</li>
<li>두 요구사항을 동시에 충족할 수 있는 유일한 단위</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="broker">Broker</h3>
<ul>
<li>카프카 서버 한 대를 의미</li>
<li>실제 Partition 파일을 디스크에 저장하고 관리</li>
</ul>
<p>Topic은 Partition을 여러 개 가질 수 있고, Partition은 Broker 여러 대에 분산 저장된다.
이 덕분에 카프카는 <strong>메시지 처리량을 물리적으로 확장할 수 있는 구조</strong>를 갖는다.
<img src="https://velog.velcdn.com/images/1im_chaereong/post/ef872877-0e31-4958-a1fe-25ffd47f5780/image.png" alt=""></p>
<p>그림으로 다시보자.
브로커는 하나의 Kafka 서버를 의미하며, 그 안에는 Topic이라는 논리적 이름 공간이 구성된다.
Topic은 실제 데이터를 저장하는 단위가 아니며, 내부적으로 여러 개의 Partition으로 나뉘어 있다.
이 Partition이 바로 메시지가 순차적으로 기록되는 <strong>물리적 저장 단위</strong>이며, 각 파티션은 브로커의 디스크에 실제 파일 형태로 저장되고 관리된다.</p>
<h2 id="2-partition의-동작-방식--append-only-log">2. Partition의 동작 방식 – Append-Only Log</h2>
<p>Partition은 내부적으로 append-only 구조의 로그다.
즉, 새로운 메시지가 오면 기존 데이터를 수정하거나 끼워넣지 않고 <strong>무조건 파일 끝에 추가</strong>된다.</p>
<p>이 방식은 다음과 같은 장점을 제공한다.</p>
<ul>
<li>디스크 순차 쓰기 기반 → 고속 처리 가능</li>
<li>파일 변경(중간 수정)이 없으므로 락 경합 최소화</li>
<li>구조가 단순해 장애 복구가 쉬움</li>
</ul>
<p>카프카의 성능은 결국 <strong>처음 설계된 저장 방식</strong>에서 나온다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/6de5da9a-9129-4f4a-9fee-0512c66a7e5b/image.png" alt=""></p>
<p>그림으로 다시 보자.
<strong>Producer가 보낸 메시지가 Kafka Broker 내부의 Partition에 offset 순서대로 차곡차곡 append되는 과정</strong>을 보여준다.
새로운 메시지는 항상 Partition의 <strong>가장 끝(offset 4)</strong> 에 추가되며, Consumer는 이 로그 스트림을 <strong>offset 순서대로 읽어 간다.</strong> 즉, Kafka Partition이 <strong>append-only 로그로 동작하며 순서를 강하게 보장하는 구조</strong>임을 설명한다.</p>
<h2 id="3-partition-내부는-segment-파일로-구성된다">3. Partition 내부는 Segment 파일로 구성된다</h2>
<p>Partition을 하나의 큰 파일로 운영하면 관리가 어렵다.
그래서 카프카는 Partition을 여러 Segment 파일로 나누어 저장한다.</p>
<h3 id="하나의-segment-구성-요소">하나의 Segment 구성 요소</h3>
<ul>
<li>.log 파일 : 실제 메시지 기록<ul>
<li>메시지 key / value (JSON, Avro 등)</li>
<li>timestamp (이벤트 시간)</li>
<li>메시지 크기</li>
<li>CRC (무결성 체크)</li>
<li>기타 header 필드</li>
</ul>
</li>
<li>.index 파일 : 메시지 offset → 파일 위치 매핑</li>
<li>.timeindex 파일 : timestamp → offset 매핑</li>
</ul>
<p>예를 들어 segment size가 1GB라면,
Partition이 커질 때마다 다음과 같은 파일이 생성된다:</p>
<pre><code>00000000000000000000.log
00000000000000000000.index
00000000000000000000.timeindex

00000000000001000000.log
00000000000001000000.index
...</code></pre><p>Segment를 나누는 이유는 크게 두 가지다:</p>
<ol>
<li>대용량 파일이 너무 커지는 것 방지</li>
<li>오래된 Segment를 삭제(또는 compact)하기 쉬움</li>
</ol>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/edf6ad17-787d-451b-90f2-1ae668a61772/image.png" alt=""></p>
<p>그림으로 다시보자.
Kafka는 <code>.timeindex → .index → .log</code> 순서로 매핑된 구조를 이용해 <strong>timestamp 기반 탐색을 내부적으로 수행한다.</strong>
즉, 특정 시각 이후의 메시지를 찾을 때는 timeindex에서 offset을 찾고, index에서 해당 offset의 byte 위치를 확인한 뒤, log 파일의 해당 위치로 바로 jump하는 방식이다.
하지만 일반적인 메시지 소비는 timestamp가 아니라 <strong>offset을 기준으로</strong> 이루어진다.
예를 들어 offset=100까지 읽었다면, 파일 포인터는 이미 그 위치에 있으며,
그 다음 메시지는 자연스럽게 바로 뒤에 있는 offset=101을 순차적으로 읽는 구조다.</p>
<h2 id="4-메시지가-broker에-저장되는-실제-흐름">4. 메시지가 Broker에 저장되는 실제 흐름</h2>
<p>Producer가 메시지를 전송하면 Broker는 다음 순서로 저장한다.</p>
<h3 id="1단계-tcp로-메시지-수신">1단계: TCP로 메시지 수신</h3>
<p>Producer는 Leader broker로 요청을 보낸다.</p>
<h3 id="2단계-메시지를-메모리페이지-캐시에-적재">2단계: 메시지를 메모리(페이지 캐시)에 적재</h3>
<p>Broker는 메시지를 직접 파일에 바로 쓰지 않고 OS 파일 시스템 캐시를 활용한다.
이 덕분에 디스크 I/O를 최소화하면서도 높은 처리량을 유지할 수 있다.</p>
<h3 id="3단계-partition-로그-파일-끝에-append">3단계: Partition 로그 파일 끝에 append</h3>
<p>Append-only 구조이므로 파일 끝에 바로 추가하면 된다.</p>
<h3 id="4단계-os가-flush-정책에-따라-디스크에-기록">4단계: OS가 flush 정책에 따라 디스크에 기록</h3>
<p>Broker 자체가 디스크를 직접 “sync write” 하는 것이 아니다.
대부분 OS의 write-back 캐시 메커니즘을 활용한다.</p>
<h3 id="5단계-follower가-leader의-데이터를-복제">5단계: Follower가 Leader의 데이터를 복제</h3>
<p>Follower는 다음과 같은 흐름으로 Leader의 메시지를 동기화한다:</p>
<ul>
<li>Leader 오프셋 확인 → 미싱 데이터 요청 → append → 증분 반복
Leader와 Follower가 완전히 동기화된 경우를 ISR(In-Sync Replica)라고 부른다.</li>
</ul>
<p>이 구조는 카프카가 높은 성능과 안정성을 동시에 유지하는 핵심이다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/52b62aaa-215c-40b3-9606-79d318189370/image.png" alt=""></p>
<p>그림으로 다시보자.
<strong>Replication Factor = 3</strong>
환경에서 하나의 파티션이 3개의 브로커(Leader 1개, Follower 2개)에 복제되는 구조를 보여준다.
Producer는 오직 Leader 브로커에만 메시지를 쓰고, Followers는 Leader에서 새로운 데이터를 읽어와 동일한 파티션 내용을 복제한다.
Consumer는 Leader(또는 설정에 따라 Follower)에서 메시지를 읽어가며, 세 브로커는 항상 동일한 offset의 동일한 데이터를 유지한다.</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/0cb164bd-6414-453d-9f5e-94cd2427b8b0/image.png" alt=""></p>
<p>그림으로 다시보자.
사용자가 파일을 읽으려고 하면 커널이 먼저 <strong>PageCache</strong>에 해당 데이터가 있는지 확인하는 구조를 보여준다.
PageCache에 없으면(Miss) 커널은 디스크에서 데이터를 읽어 PageCache에 저장하고, 그 데이터를 사용자에게 반환한다.
즉, 이 그림은 <strong>디스크 읽기 요청이 PageCache를 통해 최적화되는 OS의 기본 동작 방식</strong>을 설명한다.</p>
<h2 id="5-leaderfollower-구조기초">5. Leader–Follower 구조(기초)</h2>
<p>Partition은 여러 개의 복제본(replica)을 가질 수 있는데, 그중 하나는 Leader이고 나머지는 Follower다.</p>
<h3 id="leader">Leader</h3>
<ul>
<li>Producer와 Consumer가 접근하는 엔드포인트</li>
<li>읽기·쓰기를 직접 담당</li>
</ul>
<h3 id="follower">Follower</h3>
<ul>
<li>Leader의 로그를 그대로 복제</li>
<li>Leader 장애 시 Leader로 승격 가능</li>
</ul>
<p>Kafka의 replication은 push 방식이 아니라 <strong>pull</strong> 방식이다.
즉, Follower가 Leader에게 접근해 필요한 메시지를 직접 가져가는 구조다.
이 방식은 장애 상황에서도 Replica 간 비동기 동작을 유지하며 확장성을 가지게 한다.</p>
<h2 id="6-kafka가-빠른-이유--디스크-순차-쓰기-기반-구조">6. Kafka가 빠른 이유 – 디스크 순차 쓰기 기반 구조</h2>
<p>카프카의 성능 구조는 단순히 클러스터 구조 때문이 아니라
<strong>저장 방식 자체가 고성능을 만들어내는 구조</strong>이기 때문이다.</p>
<h3 id="카프카가-빠른-이유-요약">카프카가 빠른 이유 요약</h3>
<ol>
<li>순차 쓰기 기반 → 디스크에서 가장 빠른 작업</li>
<li>페이지 캐시를 적극 활용 → OS 레벨 캐시로 처리량 극대화</li>
<li>append-only 구조 → 락 경합이 거의 없음</li>
<li>Segment 파일 단위 관리 → 삭제, compaction, 검색 효율적</li>
<li>Producer → Leader로 단일 접근 → 구조 단순화</li>
</ol>
<p>이 모든 요소가 결합되어 <strong>디스크 기반 로그 시스템</strong>임에도
메모리 기반 메시지 시스템 수준의 높은 처리량을 낼 수 있다.</p>
<h2 id="7-요약">7. 요약</h2>
<p>Kafka 기본 구조의 핵심은 다음과 같다:</p>
<ul>
<li>Topic은 논리적 개념이고, 실제 저장은 Partition이 담당한다.</li>
<li>Partition은 append-only 로그이며 내부적으로 Segment 파일로 나뉜다.</li>
<li>Broker는 Partition을 디스크에 저장하고 관리한다.</li>
<li>Leader–Follower 구조를 통해 장애 복구와 고가용성을 제공한다.</li>
<li>카프카 성능의 핵심 비밀은 <strong>순차 쓰기 기반의 로그 구조</strong>이다.</li>
</ul>
<p>이 저장 구조를 이해하면 다음 단계인 Offset, Consumer Group, Replication, ACK, Retention 등의 개념이 자연스럽게 연결된다.</p>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://www.cloudkarafka.com/">https://www.cloudkarafka.com/</a></p>
<p><a href="https://www.linkedin.com/pulse/topic-partition-offset-broker-apache-kafka-tu%E1%BA%A5n-d%C6%B0%C6%A1ng-d4nrc/">https://www.linkedin.com/pulse/topic-partition-offset-broker-apache-kafka-tu%E1%BA%A5n-d%C6%B0%C6%A1ng-d4nrc/</a></p>
<p><a href="https://strimzi.io/blog/2021/12/17/kafka-segment-retention/">https://strimzi.io/blog/2021/12/17/kafka-segment-retention/</a></p>
<p><a href="https://velog.io/@ddongh1122/Kafka-%EC%9D%B4%ED%95%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-3">https://velog.io/@ddongh1122/Kafka-%EC%9D%B4%ED%95%B4-%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90-3</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동기, 비동기처리]]></title>
            <link>https://velog.io/@1im_chaereong/%EB%8F%99%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@1im_chaereong/%EB%8F%99%EA%B8%B0-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Sun, 02 Feb 2025 14:44:31 GMT</pubDate>
            <description><![CDATA[<h3 id="동기synchronous">동기(Synchronous)</h3>
<p>동기는 사전적으로 &#39;동시에 일어난다&#39;는 의미를 갖고 있습니다.</p>
<p>프로그래밍에서 동기는 작업이 순차적으로 진행되는 것을 의미합니다. 즉, 한 작업이 시작되면 해당 작업이 완료될 때까지 다른 작업이 기다려야 합니다. 동기 방식은 호출한 함수 또는 작업이 반환될 때까지 대기하는 동안 실행 흐름이 차단되는 특징이 있습니다.</p>
<p>동기 방식은 일반적으로 간단하고 직관적인 코드를 작성하기 쉽습니다. 하지만 여러 작업이 동시에 실행되어야 하는 경우, 각 작업의 완료를 기다리는 동안 시간이 소요되어 전체 프로세스의 성능이 저하될 수 있습니다. 또한 한 작업이 지연되면 다른 작업들도 모두 지연되는 문제가 발생할 수 있습니다.</p>
<h3 id="비동기asynchronous">비동기(Asynchronous)</h3>
<p>비동기는 사전적으로 &#39;동시에 일어나지 않는다&#39;는 의미를 갖고 있습니다.</p>
<p>프로그래밍에서 비동기는 작업이 독립적으로 실행되며, 작업의 완료 여부를 기다리지 않고 다른 작업을 실행할 수 있는 방식을 의미합니다. 즉, 비동기 방식은 작업이 시작되면 해당 작업이 완료될 때까지 기다리지 않고 다음 코드를 실행할 수 있습니다.</p>
<p>비동기 방식은 주로 I/O 작업이나 네트워크 요청과 같이 시간이 오래 걸리는 작업에 유용합니다. 이러한 작업을 비동기적으로 처리하면, 프로그램은 작업이 완료되기를 기다리는 동안 다른 작업을 처리할 수 있으므로 전체적인 성능이 향상됩니다. 비동기 방식은 콜백(callback), 프라미스(Promise), async/await 등의 메커니즘을 통해 구현될 수 있습니다.</p>
<h3 id="동기-vs-비동기-활용-사례">동기 vs. 비동기: 활용 사례</h3>
<p>동기 방식은 주로 간단하고 직관적인 코드 작성이 요구되는 경우에 사용됩니다. 예를 들어, 순차적으로 실행되어야 하는 작업이나 작업 간의 의존성이 높은 경우 동기 방식이 적합합니다. 또한 동기 방식은 특정 작업이 반드시 완료된 후에 다음 작업을 수행해야 하는 경우에 유용합니다.</p>
<p>비동기 방식은 여러 작업을 동시에 처리해야 하는 경우나 시간이 오래 걸리는 작업을 다른 작업과 병렬로 처리해야 하는 경우에 적합합니다. 예를 들어, 사용자 인터페이스 응답성을 향상시키기 위해 네트워크 요청이나 파일 다운로드를 비동기적으로 처리할 수 있습니다.</p>
<p>동기와 비동기는 프로그래밍에서 중요한 개념으로, 작업의 실행 방식과 완료 여부를 다룹니다. 동기 방식은 작업을 순차적으로 처리하며, 다음 작업을 실행하기 위해 이전 작업의 완료를 기다립니다. 반면에 비동기 방식은 작업을 독립적으로 실행하며, 다른 작업을 실행하면서 작업의 완료를 기다리지 않습니다.</p>
<p>동기와 비동기의 선택은 프로그램의 요구사항과 성능에 따라 달라집니다. 동기 방식은 간단하고 직관적인 코드 작성이 필요한 경우 적합하며, 비동기 방식은 병렬 처리와 응답성 향상이 필요한 경우에 유용합니다. 프로그래밍에서 동기와 비동기의 적절한 활용은 효율적이고 반응성이 뛰어난 프로그램을 개발하는 핵심 요소입니다.</p>
<h3 id="카페를-예시로-든-동기-비동기-처리">카페를 예시로 든 동기, 비동기 처리</h3>
<p>동기와 비동기 처리를 카페를 예시로 들어 설명해보겠습니다.</p>
<ul>
<li><strong>동기 처리</strong>: 카페에서 손님이 커피를 주문하고, 바리스타가 커피를 만들 때까지 손님이 기다린다고 가정해 봅시다. 이 경우, 커피가 완성되기 전까지 손님은 아무것도 할 수 없습니다. 이것이 동기 방식입니다. 하나의 작업이 끝날 때까지 다음 작업을 시작하지 못하는 상황입니다.</li>
<li><strong>비동기 처리</strong>: 반면, 비동기 방식에서는 손님이 커피를 주문한 후 바리스타가 커피를 만드는 동안 손님은 자리에 앉아 다른 일을 할 수 있습니다. 커피가 완성되면 바리스타가 손님에게 커피가 준비되었다고 알리거나, 벨을 울립니다. 이렇게 작업이 완료될 때까지 기다리지 않고 다른 작업을 할 수 있는 것이 비동기 방식입니다.</li>
</ul>
<h3 id="간단한-자바-코드와-출력을-통한-동기-비동기">간단한 자바 코드와 출력을 통한 동기, 비동기</h3>
<h3 id="동기-코드-예제">동기 코드 예제</h3>
<p>아래는 동기 방식으로 작성된 간단한 자바 코드입니다. 이 코드는 두 작업이 순차적으로 실행되는 모습을 보여줍니다.</p>
<pre><code class="language-java">public class SynchronousExample {
    public static void main(String[] args) {
        System.out.println(&quot;첫 번째 작업 시작&quot;);
        performTask();
        System.out.println(&quot;두 번째 작업 시작&quot;);
    }

    public static void performTask() {
        try {
            Thread.sleep(2000); // 2초 동안 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(&quot;작업 완료&quot;);
    }
}</code></pre>
<p><strong>출력</strong>:</p>
<pre><code>첫 번째 작업 시작
작업 완료
두 번째 작업 시작</code></pre><p>위 코드에서는 <code>performTask()</code> 메서드가 완료될 때까지 다음 작업이 시작되지 않습니다. 즉, 첫 번째 작업이 끝날 때까지 두 번째 작업은 대기합니다.</p>
<h3 id="비동기-코드-예제">비동기 코드 예제</h3>
<p>아래는 비동기 방식으로 작성된 자바 코드 예제입니다. 이 코드는 두 작업이 동시에 실행되는 모습을 보여줍니다.</p>
<pre><code class="language-java">import java.util.concurrent.CompletableFuture;

public class AsynchronousExample {
    public static void main(String[] args) {
        System.out.println(&quot;첫 번째 작업 시작&quot;);
        CompletableFuture.runAsync(() -&gt; performTask());
        System.out.println(&quot;두 번째 작업 시작&quot;);
    }

    public static void performTask() {
        try {
            Thread.sleep(2000); // 2초 동안 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(&quot;작업 완료&quot;);
    }
}</code></pre>
<p><strong>출력</strong>:</p>
<pre><code>첫 번째 작업 시작
두 번째 작업 시작
작업 완료</code></pre><p>위 코드에서는 <code>performTask()</code> 메서드가 비동기적으로 실행되므로, 첫 번째 작업이 시작된 후 바로 두 번째 작업이 시작됩니다. <code>performTask()</code>는 별도의 스레드에서 실행되기 때문에 두 번째 작업이 이를 기다리지 않고 바로 실행됩니다.</p>
<hr>
<p>이렇게 동기와 비동기의 개념, 카페를 예시로 든 설명, 그리고 간단한 자바 코드 예제를 통해 동기와 비동기의 차이점을 명확하게 이해할 수 있습니다. 각각의 상황에 맞게 동기와 비동기 방식을 적절히 활용하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT와 PKI 기반 SSO: 토큰의 종류와 SSO 플로우에서의 발급 및 갱신]]></title>
            <link>https://velog.io/@1im_chaereong/JWT%EC%99%80-PKI-%EA%B8%B0%EB%B0%98-SSO-%ED%86%A0%ED%81%B0%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-SSO-%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%9C%EA%B8%89-%EB%B0%8F-%EA%B0%B1%EC%8B%A0-ingycnrd</link>
            <guid>https://velog.io/@1im_chaereong/JWT%EC%99%80-PKI-%EA%B8%B0%EB%B0%98-SSO-%ED%86%A0%ED%81%B0%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-SSO-%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%9C%EA%B8%89-%EB%B0%8F-%EA%B0%B1%EC%8B%A0-ingycnrd</guid>
            <pubDate>Sun, 02 Feb 2025 14:43:55 GMT</pubDate>
            <description><![CDATA[<p>저번 포스팅에서는 SSO를 조금이나마 더 이해하기위해 JWT와 PKI를 공부하고 각 요소의 역할을 알아보았다 ! 이번 포스팅에서는 SSO에서의 토큰 생성 및 검증 동작 흐름과 이를 더 보안하는 방법을 알아보려한다 !</p>
<h2 id="jwt-토큰의-종류와-목적">JWT 토큰의 종류와 목적</h2>
<p>SSO 시스템에서 사용되는 <strong>JWT (JSON Web Token)</strong>은 다양한 목적을 위해 여러 종류의 토큰을 사용한다. 각각의 토큰은 특정한 역할을 담당하며, 이를 통해 사용자의 인증과 권한을 관리한다.</p>
<h3 id="access-토큰">Access 토큰</h3>
<ul>
<li><strong>목적</strong>: 특정 리소스에 대한 접근 권한을 부여하기 위해 사용</li>
<li><strong>유효 기간</strong>: 일반적으로 짧으며 (5분~15분), 보안성을 높이기 위해 빠르게 만료되도록 설정</li>
<li><strong>사용 위치</strong>: 각 <strong>SP (Service Provider)</strong> 서버에서 사용자 권한을 확인할 때 사용</li>
</ul>
<h3 id="id-토큰">ID 토큰</h3>
<ul>
<li><strong>목적</strong>: 사용자 인증 정보를 담고 있으며, SP에서 사용자의 인증 상태를 확인하기 위해 사용</li>
<li><strong>유효 기간</strong>: Access 토큰보다 길지만, Refresh 토큰보다는 짧음</li>
<li><strong>사용 위치</strong>: 사용자 정보를 필요로 하는 SP 애플리케이션에서 활용</li>
</ul>
<h3 id="refresh-토큰">Refresh 토큰</h3>
<ul>
<li><strong>목적</strong>: Access 토큰이 만료되었을 때 새로운 Access 토큰을 발급받기 위해 사용</li>
<li><strong>유효 기간</strong>: 긴 유효 기간을 가지며 (일반적으로 몇 주에서 몇 달), 클라이언트가 장기간 인증된 상태를 유지할 수 있도록 도움</li>
<li><strong>사용 위치</strong>: 클라이언트가 <strong>IDP (Identity Provider)</strong>로부터 새로운 Access 토큰을 발급받기 위해 사용</li>
</ul>
<h2 id="jwt-토큰-생성-및-검증-과정">JWT 토큰 생성 및 검증 과정</h2>
<p>JWT 토큰은 사용자의 인증 상태를 나타내며, 이를 통해 SP는 사용자의 신원을 확인하고 권한을 부여한다. 이 과정에서 IDP는 토큰을 생성하고, SP는 이를 검증하는 역할을 한다.</p>
<h3 id="토큰-생성-과정">토큰 생성 과정</h3>
<ol>
<li><strong>사용자 인증 요청</strong>: 사용자가 IDP 서버에 로그인 요청</li>
<li><strong>사용자 정보 검증</strong>: IDP 서버는 사용자 ID 및 비밀번호를 검증하여 사용자를 확인</li>
<li><strong>JWT 생성</strong>: 사용자가 인증되면 IDP 서버는 Access 토큰, ID 토큰, Refresh 토큰을 생성. 이때 각 토큰은 IDP의 <strong>비밀 키</strong>를 사용해 서명</li>
</ol>
<h3 id="토큰-검증-과정">토큰 검증 과정</h3>
<ol>
<li><strong>클라이언트의 요청</strong>: 사용자는 SP에 자원 요청을 보냄. 이때 <strong>Access 토큰</strong>과 <strong>ID 토큰</strong>이 함께 전송</li>
<li><strong>JWT 서명 검증</strong>: SP는 JWT의 헤더에 포함된 <strong>kid</strong> 값을 사용해 IDP의 <strong>JWKS 엔드포인트</strong>에서 적절한 공개 키를 가져옴. 가져온 공개 키를 사용해 서명을 검증</li>
<li><strong>페이로드 확인</strong>: 서명이 유효하다면 JWT의 페이로드를 확인하여 만료 기간(<code>exp</code>)이 유효한지, 그리고 필요한 클레임들이 포함되어 있는지를 검증</li>
</ol>
<p>이제 실제로는 어떤식으로 동작이 흘러가는지 알아보려한다 !!!</p>
<p>밑의 이미지는 내가 그려본 Token기반 SSO의 동작 흐름이다. 아주 간략하게 그려놓은 플로우라 그냥 스쳐지나가듯이 봐도될거같다..!</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/94a47cbf-987e-4817-b1e0-011642ae11ed/image.png" alt=""></p>
<h2 id="sso-플로우에서의-토큰-발급과-갱신">SSO 플로우에서의 토큰 발급과 갱신</h2>
<p>SSO 시스템에서는 토큰을 사용하여 사용자 인증 상태를 관리하고, 필요할 때마다 토큰을 갱신하는 과정을 거친다. 이를 통해 사용자는 여러 서비스에 한 번의 로그인으로 접근할 수 있다.</p>
<h3 id="로그인-및-토큰-발급">로그인 및 토큰 발급</h3>
<ol>
<li><strong>로그인 시도</strong>: 사용자가 SP1에 처음 접근하면, SP1은 인증이 필요하다고 판단하고 IDP로 리다이렉트</li>
<li><strong>사용자 인증</strong>: 사용자가 IDP에서 자격 증명을 제출하고 인증되면, IDP는 <strong>Access 토큰</strong>, <strong>ID 토큰</strong>, <strong>Refresh 토큰</strong>을 발급한다.</li>
<li><strong>토큰 저장</strong>: 브라우저는 <strong>Access 토큰</strong>과 <strong>ID 토큰</strong>을 <strong>HttpOnly</strong> 및 <strong>Secure</strong> 쿠키에 저장한다. <strong>Refresh 토큰</strong>도 HttpOnly 쿠키에 저장된다.</li>
</ol>
<h3 id="왜-리다이렉트를-하는걸까--sp1이-직접-idp에-요청하면-안될까-">왜 리다이렉트를 하는걸까 ? SP1이 직접 IDP에 요청하면 안될까 ?</h3>
<ul>
<li>리다이렉트를 통해 브라우저가 IDP에 접근하면, IDP는 브라우저의 <strong>세션 및 쿠키</strong>를 활용해 이미 로그인되어 있는지 확인할 수 있게된다.</li>
<li>만약 사용자가 이미 IDP에 로그인한 상태라면, 추가적인 로그인 없이 자동으로 인증 정보를 SP1으로 전달할 수 있는데 이것이 SSO의 핵심이라고 할수있다.</li>
<li>리다이렉트를 통해 IDP는 사용자 인증 후 <strong>SAML, OAuth, 또는 OIDC</strong> 같은 표준 프로토콜을 사용해 SP1으로 인증 결과를 안전하게 전달한다.</li>
<li>이 과정에서 <strong>JWT, SAML Assertion</strong> 같은 토큰이 포함된 응답을 SP1에 전달하며, 이를 통해 SP1은 IDP가 사용자 인증을 완료했음을 신뢰할 수 있다.</li>
</ul>
<h3 id="자원-접근">자원 접근</h3>
<ol>
<li><strong>SP1에 접근</strong>: 사용자가 SP1에 접근하면, 클라이언트는 Access 토큰과 ID 토큰을 전송한다.</li>
<li><strong>토큰 검증</strong>: SP1은 토큰의 서명을 검증하고, 페이로드 정보를 확인하여 사용자의 접근을 허용한다.</li>
</ol>
<h3 id="access-토큰-만료-및-갱신">Access 토큰 만료 및 갱신</h3>
<ol>
<li><strong>토큰 만료</strong>: Access 토큰이 만료되면, 사용자는 새로운 토큰이 필요하게 된다.</li>
<li><strong>Refresh 토큰 사용</strong>: 클라이언트는 <strong>Refresh 토큰</strong>을 사용해 IDP에 새로운 Access 토큰과 ID 토큰을 요청한다.</li>
<li><strong>갱신된 토큰 발급</strong>: IDP는 Refresh 토큰을 검증한 후, 새로운 Access 토큰과 ID 토큰을 발급하여 클라이언트에 전달한다.</li>
</ol>
<h3 id="로그아웃-처리">로그아웃 처리</h3>
<ul>
<li>사용자가 <strong>로그아웃</strong>을 요청하면, IDP는 서버 측에서 세션 정보를 삭제하고 클라이언트에 저장된 모든 토큰(Access, ID, Refresh)을 폐기한다.</li>
</ul>
<h2 id="보안-고려-사항-및-베스트-프랙티스">보안 고려 사항 및 베스트 프랙티스</h2>
<h3 id="토큰의-보안-저장">토큰의 보안 저장</h3>
<ul>
<li><strong>HttpOnly 및 Secure 쿠키 사용</strong>: 클라이언트에서 토큰이 탈취되는 것을 방지하기 위해 HttpOnly 쿠키에 저장하고, HTTPS 통신에서만 전송되도록 설정한다.</li>
</ul>
<h3 id="짧은-access-토큰-유효-기간">짧은 Access 토큰 유효 기간</h3>
<ul>
<li><strong>짧은 Access 토큰 유효 기간</strong>을 설정하여 토큰이 탈취되더라도 악용될 위험을 최소화한다.</li>
</ul>
<h3 id="refresh-토큰-보안">Refresh 토큰 보안</h3>
<ul>
<li><strong>장기적 사용</strong>을 위해 Refresh 토큰의 보안이 중요하다. Refresh 토큰이 탈취되지 않도록 HttpOnly 설정을 사용하며, Refresh 토큰의 사용 횟수를 모니터링하여 비정상적인 사용을 감지한다.</li>
</ul>
<h3 id="서명-알고리즘-선택">서명 알고리즘 선택</h3>
<ul>
<li>RSA와 같은 <strong>비대칭 서명 알고리즘</strong>을 사용하는 것이 HMAC보다 보안성이 높다. IDP와 SP 간의 관계에서 비대칭 서명을 사용하면, SP는 공개 키만으로 JWT를 검증할 수 있어 보안성이 향상된다.</li>
</ul>
<h3 id="추가적인-보안-강화-방법">추가적인 보안 강화 방법</h3>
<ul>
<li><strong>PKCE (Proof Key for Code Exchange)</strong>: 특히 공용 클라이언트(예: 모바일 앱)에서는 PKCE를 사용하여 인증 코드 교환 시 중간자 공격을 방지한다.</li>
<li><strong>IP 허용 목록 설정</strong>: 민감한 리소스에 접근할 수 있는 서버의 IP를 허용 목록으로 관리하여, 허가된 IP에서만 접근할 수 있도록 제한한다.</li>
<li><strong>로그 모니터링 및 알림</strong>: 비정상적인 로그인 시도나 토큰 재발급 요청을 모니터링하고, 의심스러운 활동에 대한 알림을 설정하여 빠르게 대응할 수 있도록 한다.</li>
<li><strong>CORS 정책 설정</strong>: CORS(Cross-Origin Resource Sharing) 정책을 올바르게 설정하여, 신뢰할 수 있는 도메인에서만 리소스에 접근하도록 제한한다.</li>
<li><strong>강력한 암호 정책</strong>: 사용자 계정의 보안을 위해 IDP에서 강력한 암호 정책을 설정하고, 주기적인 암호 변경을 요구한다.</li>
</ul>
<p>아직 SSO에 대해 이해하기는 한참 멀었지만 이렇게 조금이나마 공부를 하고서 SSO에 대한 서버를 구현해보려한다. IDP와 SP1, SP2 서버를 구현 할것인데, SP2는 다른 루트 도메인을 사용하는 서버로 구분 지어서 구현할것이다. 위에 언급했듯이 CORS 정책을 설정해 구현해보려한다.</p>
<p>그리고 JWT 토큰 구현을 외부 라이브러리 없이 직접 구현해보려한다. 그렇게 하면 토큰이 만들어지고 서명하고 검증하는 부분을 더 깊게 알수있지않을까 한다. 깃허브에 올려볼 생각입니다 !!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[KeyPair와 KeyFactory: RSA를 활용한 공개키 기반 인증 구현기]]></title>
            <link>https://velog.io/@1im_chaereong/KeyPair%EC%99%80-KeyFactory-RSA%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EA%B3%B5%EA%B0%9C%ED%82%A4-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%EA%B8%B0</link>
            <guid>https://velog.io/@1im_chaereong/KeyPair%EC%99%80-KeyFactory-RSA%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EA%B3%B5%EA%B0%9C%ED%82%A4-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%EA%B8%B0</guid>
            <pubDate>Sun, 02 Feb 2025 14:42:04 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 <strong>KeyPair</strong>와 <strong>KeyFactory</strong>에 대해 설명하고, 실제 프로젝트에서 어떻게 활용했는지 공유해 보려고 합니다 ! 특히, <strong>JWT 인증</strong>을 구현하는 과정에서 이 두 가지 클래스가 어떤 역할을 했고, 이를 통해 인증 흐름을 어떻게 설계했는지 제가 구현한것을 기반으로 구체적으로 다룰 예정입니다 !</p>
<h3 id="rsa와-공개키-암호화-개념">RSA와 공개키 암호화 개념</h3>
<p><strong>RSA</strong>는 대표적인 공개키 암호화 알고리즘으로, <strong>비대칭 키</strong>를 사용하는 암호화 기법입니다. 여기서 비대칭 키란, <strong>개인키(Private Key)</strong>와 <strong>공개키(Public Key)</strong>를 한 쌍으로 생성하여 사용하는 방식을 의미하는데 RSA의 가장 큰 특징은 개인키와 공개키를 서로 다르게 사용한다는 점입니다. 개인키로 암호화한 데이터를 공개키로 복호화할 수 있고, 반대로 공개키로 암호화한 데이터는 개인키로만 복호화할 수 있다. 이를 활용해 암호화, 디지털 서명, 인증 등의 다양한 기능을 구현할 수 있다 ! </p>
<p>여기서 왜 개인키와 공개키가 필요한지 의문이 들수있다 ! 이 의문은 제가 이전에 포스팅한 PKI에 대해서 읽어보시면 조금이나마 도움이 될 것 같습니다 !!</p>
<p><a href="https://velog.io/@1im_chaereong/posts?tag=PKI">https://velog.io/@1im_chaereong/posts?tag=PKI</a></p>
<h3 id="keypair와-keyfactory의-개념">KeyPair와 KeyFactory의 개념</h3>
<p>본격적인 구현에 앞서, <strong>KeyPair</strong>와 <strong>KeyFactory</strong>에 대해 알아보겠습니다.</p>
<h3 id="keypair">KeyPair</h3>
<p><strong>KeyPair</strong>는 <strong>공개키</strong>와 <strong>개인키</strong>를 쌍으로 생성하는 클래스다. 이를 통해 RSA 기반의 비대칭 키를 쉽게 생성할 수 있다. <strong>KeyPairGenerator</strong> 클래스를 사용하여 키 쌍을 생성하고, 여기서 나온 <strong>KeyPair</strong> 객체를 통해 <strong>공개키</strong>와 <strong>개인키</strong>를 얻을 수 있다. 즉, <strong>KeyPair</strong>는 RSA 알고리즘을 활용해 우리에게 필요한 암호화 키들을 제공하는 역할을 한다.</p>
<pre><code class="language-java">KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(&quot;RSA&quot;);
keyPairGenerator.initialize(2048); // 키 길이 2048비트 설정
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();</code></pre>
<p>이렇게 생성된 <strong>개인키</strong>와 <strong>공개키</strong>는 각각 데이터 서명 및 검증에 사용된다.</p>
<h3 id="keyfactory">KeyFactory</h3>
<p><strong>KeyFactory</strong>는 <strong>키의 형식을 변환</strong>해주는 클래스다. 일반적으로 <strong>키를 저장</strong>하거나 <strong>전달</strong>할 때는 문자열 형태로 인코딩하여 사용하게 된다. <strong>KeyFactory</strong>는 인코딩된 키를 다시 <strong>키 객체(Key Object)</strong>로 변환하는 데 사용된다. 즉, <strong>외부에서 제공된 키</strong>를 실제 <strong>암호화 작업</strong>에 사용할 수 있도록 변환하는 것이 KeyFactory의 주된 역할이다.</p>
<p>아래와 같이 <strong>Base64</strong>로 인코딩된 키 문자열을 <strong>KeyFactory</strong>를 통해 <strong>PublicKey</strong>나 <strong>PrivateKey</strong> 객체로 변환할 수 있다.</p>
<pre><code class="language-java">byte[] keyBytes = Base64.getDecoder().decode(publicKeyString);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(&quot;RSA&quot;);
PublicKey publicKey = keyFactory.generatePublic(keySpec);</code></pre>
<p>이 과정에서 <strong>X509EncodedKeySpec</strong>은 공개키의 인코딩 형식을 나타내며, 이를 통해 문자열 형태의 공개키를 실제 <strong>PublicKey</strong> 객체로 변환하게 된다.</p>
<h3 id="구현한-인증-구조-설명">구현한 인증 구조 설명</h3>
<p>이번에는 <strong>JWT 인증</strong>을 사용해 SSO(Single Sign-On) 구조를 구현하였다. IDP(Identity Provider) 서버와 SP(Service Provider) 서버를 구성하고, IDP 서버에서 <strong>JWT</strong>를 발급하고 SP 서버에서 이를 검증하는 방식으로 인증을 구현했다. 이 과정에서 <strong>KeyPair</strong>와 <strong>KeyFactory</strong>를 활용하여 <strong>공개키 기반 서명 검증</strong>을 구현하였다.</p>
<h3 id="idp-서버-keypair를-통한-키-생성과-서명">IDP 서버: KeyPair를 통한 키 생성과 서명</h3>
<p>먼저 <strong>IDP 서버</strong>에서 <strong>KeyPair</strong>를 사용해 <strong>공개키</strong>와 <strong>개인키</strong>를 생성하였다. 생성된 키들은 각각 다음과 같은 역할을 한다:</p>
<ul>
<li><strong>개인키</strong>: JWT를 서명하는 데 사용된다. 이를 통해 IDP 서버가 발급한 토큰임을 증명할 수 있다.</li>
<li><strong>공개키</strong>: SP 서버에 제공되어 서명을 검증하는 데 사용된다.</li>
</ul>
<p>IDP 서버의 <strong>KeyPairProvider</strong> 클래스는 다음과 같이 <strong>공개키</strong>와 <strong>개인키</strong>를 생성하고 관리한다.</p>
<pre><code class="language-java">@Component
public class KeyPairProvider {
    private final PrivateKey privateKey;
    private final PublicKey publicKey;

    public KeyPairProvider() {
        KeyPair keyPair = generateKeyPair();
        this.privateKey = keyPair.getPrivate();
        this.publicKey = keyPair.getPublic();
    }

    private KeyPair generateKeyPair() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(&quot;RSA&quot;);
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception e) {
            throw new RuntimeException(&quot;Error generating KeyPair&quot;, e);
        }
    }

    public String getEncodedPublicKey() {
        return Base64.getEncoder().encodeToString(publicKey.getEncoded());
    }
}</code></pre>
<p>이렇게 생성된 <strong>공개키</strong>는 엔드포인트(<code>/api/publicKey</code>)를 통해 <strong>SP 서버</strong>에 제공된다.</p>
<h3 id="sp-서버-keyfactory를-통한-서명-검증">SP 서버: KeyFactory를 통한 서명 검증</h3>
<p><strong>SP 서버</strong>는 IDP 서버에서 제공한 JWT의 서명을 검증하여 유효성을 판단한다. 이때, <strong>KeyFactory</strong>를 사용해 <strong>공개키 문자열</strong>을 <strong>PublicKey 객체</strong>로 변환한 후 서명 검증에 활용한다.</p>
<p>아래는 <strong>SP 서버</strong>의 필터에서 JWT의 서명을 검증하는 과정이다.</p>
<pre><code class="language-java">private boolean verifySignature(String data, String signature) {
    try {
        byte[] keyBytes = Base64.getDecoder().decode(publicKeyValue);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(&quot;RSA&quot;);
        PublicKey publicKey = keyFactory.generatePublic(keySpec);

        Signature sig = Signature.getInstance(&quot;SHA256withRSA&quot;);
        sig.initVerify(publicKey);
        sig.update(data.getBytes(StandardCharsets.UTF_8));

        return sig.verify(Base64.getUrlDecoder().decode(signature));
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}</code></pre>
<ul>
<li><strong>IDP 서버</strong>에서 받은 <strong>Base64 인코딩된 공개키</strong>를 디코딩하고, 이를 <strong>KeyFactory</strong>와 <strong>X509EncodedKeySpec</strong>을 사용해 <strong>PublicKey 객체</strong>로 변환한다.</li>
<li>변환된 <strong>PublicKey</strong> 객체를 통해 JWT의 서명이 올바른지 검증한다.</li>
</ul>
<h3 id="keypair와-keyfactory의-역할-요약">KeyPair와 KeyFactory의 역할 요약</h3>
<ul>
<li><strong>KeyPair</strong>는 <strong>공개키와 개인키를 생성</strong>하는 역할을 한다. 이 프로젝트에서는 IDP 서버에서 JWT 서명에 사용할 <strong>개인키</strong>와 SP 서버에서 서명 검증에 사용할 <strong>공개키</strong>를 생성했다.</li>
<li><strong>KeyFactory</strong>는 외부에서 제공된 인코딩된 키를 실제 암호화에 사용할 수 있는 <strong>키 객체로 변환</strong>하는 역할을 한다. SP 서버에서는 IDP 서버에서 받은 <strong>Base64 인코딩된 공개키</strong>를 <strong>KeyFactory</strong>를 통해 <strong>PublicKey 객체</strong>로 변환하고, 이를 서명 검증에 활용하였다.</li>
</ul>
<p>이번 프로젝트에서는 RSA 기반의 <strong>공개키 암호화</strong>와 <strong>디지털 서명</strong>을 활용해 JWT 인증을 구현하였다. <strong>KeyPair</strong>를 통해 <strong>공개키와 개인키</strong>를 생성하고, <strong>KeyFactory</strong>를 사용해 외부에서 제공된 공개키를 검증에 사용할 수 있는 <strong>PublicKey 객체</strong>로 변환하였다. 이 과정을 통해 IDP 서버가 발급한 JWT의 <strong>무결성</strong>을 SP 서버에서 확인할 수 있었다.</p>
<p>이를 통해 <strong>안전한 인증 시스템</strong>을 구현할 수 있었고, 각 키의 역할과 그 활용법을 명확히 이해하게 되었다. 공개키와 개인키의 올바른 사용은 보안에서 매우 중요하며, 이를 적절히 활용함으로써 신뢰할 수 있는 인증 흐름을 구축할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT와 PKI 기반 SSO: 토큰의 종류와 SSO 플로우에서의 발급 및 갱신]]></title>
            <link>https://velog.io/@1im_chaereong/JWT%EC%99%80-PKI-%EA%B8%B0%EB%B0%98-SSO-%ED%86%A0%ED%81%B0%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-SSO-%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%9C%EA%B8%89-%EB%B0%8F-%EA%B0%B1%EC%8B%A0</link>
            <guid>https://velog.io/@1im_chaereong/JWT%EC%99%80-PKI-%EA%B8%B0%EB%B0%98-SSO-%ED%86%A0%ED%81%B0%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-SSO-%ED%94%8C%EB%A1%9C%EC%9A%B0%EC%97%90%EC%84%9C%EC%9D%98-%EB%B0%9C%EA%B8%89-%EB%B0%8F-%EA%B0%B1%EC%8B%A0</guid>
            <pubDate>Wed, 20 Nov 2024 13:30:23 GMT</pubDate>
            <description><![CDATA[<p>저번 포스팅에서는 SSO를 조금이나마 더 이해하기위해 JWT와 PKI를 공부하고 각 요소의 역할을 알아보았다 ! 이번 포스팅에서는 SSO에서의 토큰 생성 및 검증 동작 흐름과 이를 더 보안하는 방법을 알아보려한다 !</p>
<h2 id="jwt-토큰의-종류와-목적">JWT 토큰의 종류와 목적</h2>
<p>SSO 시스템에서 사용되는 <strong>JWT (JSON Web Token)</strong>은 다양한 목적을 위해 여러 종류의 토큰을 사용한다. 각각의 토큰은 특정한 역할을 담당하며, 이를 통해 사용자의 인증과 권한을 관리한다.</p>
<h3 id="access-토큰">Access 토큰</h3>
<ul>
<li><strong>목적</strong>: 특정 리소스에 대한 접근 권한을 부여하기 위해 사용</li>
<li><strong>유효 기간</strong>: 일반적으로 짧으며 (5분~15분), 보안성을 높이기 위해 빠르게 만료되도록 설정</li>
<li><strong>사용 위치</strong>: 각 <strong>SP (Service Provider)</strong> 서버에서 사용자 권한을 확인할 때 사용</li>
</ul>
<h3 id="id-토큰">ID 토큰</h3>
<ul>
<li><strong>목적</strong>: 사용자 인증 정보를 담고 있으며, SP에서 사용자의 인증 상태를 확인하기 위해 사용</li>
<li><strong>유효 기간</strong>: Access 토큰보다 길지만, Refresh 토큰보다는 짧음</li>
<li><strong>사용 위치</strong>: 사용자 정보를 필요로 하는 SP 애플리케이션에서 활용</li>
</ul>
<h3 id="refresh-토큰">Refresh 토큰</h3>
<ul>
<li><strong>목적</strong>: Access 토큰이 만료되었을 때 새로운 Access 토큰을 발급받기 위해 사용</li>
<li><strong>유효 기간</strong>: 긴 유효 기간을 가지며 (일반적으로 몇 주에서 몇 달), 클라이언트가 장기간 인증된 상태를 유지할 수 있도록 도움</li>
<li><strong>사용 위치</strong>: 클라이언트가 <strong>IDP (Identity Provider)</strong>로부터 새로운 Access 토큰을 발급받기 위해 사용</li>
</ul>
<h2 id="jwt-토큰-생성-및-검증-과정">JWT 토큰 생성 및 검증 과정</h2>
<p>JWT 토큰은 사용자의 인증 상태를 나타내며, 이를 통해 SP는 사용자의 신원을 확인하고 권한을 부여한다. 이 과정에서 IDP는 토큰을 생성하고, SP는 이를 검증하는 역할을 한다.</p>
<h3 id="토큰-생성-과정">토큰 생성 과정</h3>
<ol>
<li><strong>사용자 인증 요청</strong>: 사용자가 IDP 서버에 로그인 요청</li>
<li><strong>사용자 정보 검증</strong>: IDP 서버는 사용자 ID 및 비밀번호를 검증하여 사용자를 확인</li>
<li><strong>JWT 생성</strong>: 사용자가 인증되면 IDP 서버는 Access 토큰, ID 토큰, Refresh 토큰을 생성. 이때 각 토큰은 IDP의 <strong>비밀 키</strong>를 사용해 서명</li>
</ol>
<h3 id="토큰-검증-과정">토큰 검증 과정</h3>
<ol>
<li><strong>클라이언트의 요청</strong>: 사용자는 SP에 자원 요청을 보냄. 이때 <strong>Access 토큰</strong>과 <strong>ID 토큰</strong>이 함께 전송</li>
<li><strong>JWT 서명 검증</strong>: SP는 JWT의 헤더에 포함된 <strong>kid</strong> 값을 사용해 IDP의 <strong>JWKS 엔드포인트</strong>에서 적절한 공개 키를 가져옴. 가져온 공개 키를 사용해 서명을 검증</li>
<li><strong>페이로드 확인</strong>: 서명이 유효하다면 JWT의 페이로드를 확인하여 만료 기간(<code>exp</code>)이 유효한지, 그리고 필요한 클레임들이 포함되어 있는지를 검증</li>
</ol>
<p>이제 실제로는 어떤식으로 동작이 흘러가는지 알아보려한다 !!!</p>
<p>밑의 이미지는 내가 그려본 Token기반 SSO의 동작 흐름이다. 아주 간략하게 그려놓은 플로우라 그냥 스쳐지나가듯이 봐도될거같다..!</p>
<p><img src="https://velog.velcdn.com/images/1im_chaereong/post/d9c24f87-a414-4343-8feb-c71e7021290f/image.png" alt=""></p>
<h2 id="sso-플로우에서의-토큰-발급과-갱신">SSO 플로우에서의 토큰 발급과 갱신</h2>
<p>SSO 시스템에서는 토큰을 사용하여 사용자 인증 상태를 관리하고, 필요할 때마다 토큰을 갱신하는 과정을 거친다. 이를 통해 사용자는 여러 서비스에 한 번의 로그인으로 접근할 수 있다.</p>
<h3 id="로그인-및-토큰-발급">로그인 및 토큰 발급</h3>
<ol>
<li><strong>로그인 시도</strong>: 사용자가 SP1에 처음 접근하면, SP1은 인증이 필요하다고 판단하고 IDP로 리다이렉트</li>
<li><strong>사용자 인증</strong>: 사용자가 IDP에서 자격 증명을 제출하고 인증되면, IDP는 <strong>Access 토큰</strong>, <strong>ID 토큰</strong>, <strong>Refresh 토큰</strong>을 발급한다.</li>
<li><strong>토큰 저장</strong>: 브라우저는 <strong>Access 토큰</strong>과 <strong>ID 토큰</strong>을 <strong>HttpOnly</strong> 및 <strong>Secure</strong> 쿠키에 저장한다. <strong>Refresh 토큰</strong>도 HttpOnly 쿠키에 저장된다.</li>
</ol>
<h3 id="왜-리다이렉트를-하는걸까--sp1이-직접-idp에-요청하면-안될까-">왜 리다이렉트를 하는걸까 ? SP1이 직접 IDP에 요청하면 안될까 ?</h3>
<ul>
<li>리다이렉트를 통해 브라우저가 IDP에 접근하면, IDP는 브라우저의 <strong>세션 및 쿠키</strong>를 활용해 이미 로그인되어 있는지 확인할 수 있게된다.</li>
<li>만약 사용자가 이미 IDP에 로그인한 상태라면, 추가적인 로그인 없이 자동으로 인증 정보를 SP1으로 전달할 수 있는데 이것이 SSO의 핵심이라고 할수있다.</li>
<li>리다이렉트를 통해 IDP는 사용자 인증 후 <strong>SAML, OAuth, 또는 OIDC</strong> 같은 표준 프로토콜을 사용해 SP1으로 인증 결과를 안전하게 전달한다.</li>
<li>이 과정에서 <strong>JWT, SAML Assertion</strong> 같은 토큰이 포함된 응답을 SP1에 전달하며, 이를 통해 SP1은 IDP가 사용자 인증을 완료했음을 신뢰할 수 있다.</li>
</ul>
<h3 id="자원-접근">자원 접근</h3>
<ol>
<li><strong>SP1에 접근</strong>: 사용자가 SP1에 접근하면, 클라이언트는 Access 토큰과 ID 토큰을 전송한다.</li>
<li><strong>토큰 검증</strong>: SP1은 토큰의 서명을 검증하고, 페이로드 정보를 확인하여 사용자의 접근을 허용한다.</li>
</ol>
<h3 id="access-토큰-만료-및-갱신">Access 토큰 만료 및 갱신</h3>
<ol>
<li><strong>토큰 만료</strong>: Access 토큰이 만료되면, 사용자는 새로운 토큰이 필요하게 된다.</li>
<li><strong>Refresh 토큰 사용</strong>: 클라이언트는 <strong>Refresh 토큰</strong>을 사용해 IDP에 새로운 Access 토큰과 ID 토큰을 요청한다.</li>
<li><strong>갱신된 토큰 발급</strong>: IDP는 Refresh 토큰을 검증한 후, 새로운 Access 토큰과 ID 토큰을 발급하여 클라이언트에 전달한다.</li>
</ol>
<h3 id="로그아웃-처리">로그아웃 처리</h3>
<ul>
<li>사용자가 <strong>로그아웃</strong>을 요청하면, IDP는 서버 측에서 세션 정보를 삭제하고 클라이언트에 저장된 모든 토큰(Access, ID, Refresh)을 폐기한다.</li>
</ul>
<h2 id="보안-고려-사항-및-베스트-프랙티스">보안 고려 사항 및 베스트 프랙티스</h2>
<h3 id="토큰의-보안-저장">토큰의 보안 저장</h3>
<ul>
<li><strong>HttpOnly 및 Secure 쿠키 사용</strong>: 클라이언트에서 토큰이 탈취되는 것을 방지하기 위해 HttpOnly 쿠키에 저장하고, HTTPS 통신에서만 전송되도록 설정한다.</li>
</ul>
<h3 id="짧은-access-토큰-유효-기간">짧은 Access 토큰 유효 기간</h3>
<ul>
<li><strong>짧은 Access 토큰 유효 기간</strong>을 설정하여 토큰이 탈취되더라도 악용될 위험을 최소화한다.</li>
</ul>
<h3 id="refresh-토큰-보안">Refresh 토큰 보안</h3>
<ul>
<li><strong>장기적 사용</strong>을 위해 Refresh 토큰의 보안이 중요하다. Refresh 토큰이 탈취되지 않도록 HttpOnly 설정을 사용하며, Refresh 토큰의 사용 횟수를 모니터링하여 비정상적인 사용을 감지한다.</li>
</ul>
<h3 id="서명-알고리즘-선택">서명 알고리즘 선택</h3>
<ul>
<li>RSA와 같은 <strong>비대칭 서명 알고리즘</strong>을 사용하는 것이 HMAC보다 보안성이 높다. IDP와 SP 간의 관계에서 비대칭 서명을 사용하면, SP는 공개 키만으로 JWT를 검증할 수 있어 보안성이 향상된다.</li>
</ul>
<h3 id="45-추가적인-보안-강화-방법">4.5 추가적인 보안 강화 방법</h3>
<ul>
<li><strong>PKCE (Proof Key for Code Exchange)</strong>: 특히 공용 클라이언트(예: 모바일 앱)에서는 PKCE를 사용하여 인증 코드 교환 시 중간자 공격을 방지한다.</li>
<li><strong>IP 허용 목록 설정</strong>: 민감한 리소스에 접근할 수 있는 서버의 IP를 허용 목록으로 관리하여, 허가된 IP에서만 접근할 수 있도록 제한한다.</li>
<li><strong>로그 모니터링 및 알림</strong>: 비정상적인 로그인 시도나 토큰 재발급 요청을 모니터링하고, 의심스러운 활동에 대한 알림을 설정하여 빠르게 대응할 수 있도록 한다.</li>
<li><strong>CORS 정책 설정</strong>: CORS(Cross-Origin Resource Sharing) 정책을 올바르게 설정하여, 신뢰할 수 있는 도메인에서만 리소스에 접근하도록 제한한다.</li>
<li><strong>강력한 암호 정책</strong>: 사용자 계정의 보안을 위해 IDP에서 강력한 암호 정책을 설정하고, 주기적인 암호 변경을 요구한다.</li>
</ul>
<p>아직 SSO에 대해 이해하기는 한참 멀었지만 이렇게 조금이나마 공부를 하고서 SSO에 대한 서버를 구현해보려한다. IDP와 SP1, SP2 서버를 구현 할것인데, SP2는 다른 루트 도메인을 사용하는 서버로 구분 지어서 구현할것이다. 위에 언급했듯이 CORS 정책을 설정해 구현해보려한다.</p>
<p>그리고 JWT 토큰 구현을 외부 라이브러리 없이 직접 구현해보려한다. 그렇게 하면 토큰이 만들어지고 서명하고 검증하는 부분을 더 깊게 알수있지않을까 한다. 깃허브에 올려볼 생각입니다 !!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT 토큰과 PKI 비대칭 키를 통한 SSO: 개요 및 기본 원리]]></title>
            <link>https://velog.io/@1im_chaereong/JWT-%ED%86%A0%ED%81%B0%EA%B3%BC-PKI-%EB%B9%84%EB%8C%80%EC%B9%AD-%ED%82%A4%EB%A5%BC-%ED%86%B5%ED%95%9C-SSO-%EA%B0%9C%EC%9A%94-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@1im_chaereong/JWT-%ED%86%A0%ED%81%B0%EA%B3%BC-PKI-%EB%B9%84%EB%8C%80%EC%B9%AD-%ED%82%A4%EB%A5%BC-%ED%86%B5%ED%95%9C-SSO-%EA%B0%9C%EC%9A%94-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Tue, 19 Nov 2024 14:12:08 GMT</pubDate>
            <description><![CDATA[<p>OAuth 2.0을 공부하다보니 현대의 웹 애플리케이션 환경에서 다양한 서비스 간의 사용자 인증을 통합하고 사용자 경험을 단순화하기 위한 방식으로 <strong>Single Sign-On (SSO)</strong>이 많이 사용된다라는것을 알게되었다. SSO는 여러 서비스 제공자(Service Provider, SP)에 대해 한 번의 인증으로 접근할 수 있도록 하는 기술이다. SSO의 구현에서 중요한 역할을 하는 것은 <strong>JWT (JSON Web Token)</strong>와 <strong>PKI (Public Key Infrastructure)</strong>라고 생각한다. JWT와 PKI를 사용하여 SSO 시스템을 구성하는 방법을 설명하고, 각 요소의 역할과 동작 방식을 알아보려한다 !!</p>
<h2 id="jwt-json-web-token-이해">JWT (JSON Web Token) 이해</h2>
<ul>
<li><strong>JWT (JSON Web Token)</strong>는 클레임 기반의 보안 정보를 안전하게 전송하기 위한 <strong>JSON 객체</strong>를 사용하는 인코딩된 문자열이다. JWT는 보통 다음 세 가지 파트로 이루어져있다.</li>
</ul>
<h3 id="jwt-구조">JWT 구조</h3>
<ol>
<li><strong>Header (헤더)</strong>: JWT의 타입과 서명 알고리즘 정보를 포함</li>
<li><strong>Payload (페이로드)</strong>: 사용자 정보(클레임)가 들어 있으며, 이 정보는 SP에서 사용자 접근을 결정하는 데 활용</li>
<li><strong>Signature (서명)</strong>: 헤더와 페이로드를 특정 비밀 키로 해시한 값</li>
</ol>
<p>각 파트는 <code>Base64Url</code> 인코딩 방식으로 인코딩되며, <code>.</code> 문자로 구분된다.</p>
<pre><code>&lt;Header&gt;.&lt;Payload&gt;.&lt;Signature&gt;</code></pre><h3 id="jwt의-장점">JWT의 장점</h3>
<ul>
<li><strong>간단한 구조</strong>로 클라이언트-서버 간 데이터 전송에 사용하기 쉽다.</li>
<li><strong>서명을 통해 무결성</strong>을 보장하며, 서명을 검증하여 토큰이 변조되지 않았음을 확인할 수 있다.</li>
<li><strong>확장성</strong>이 좋아서 다양한 시스템과의 통합이 용이하다.</li>
</ul>
<h3 id="jwt-클레임">JWT 클레임</h3>
<p>JWT의 클레임에는 다음과 같은 정보가 포함될 수 있다.</p>
<ul>
<li><strong>등록된 클레임 (Registered Claims)</strong>: <code>iss</code> (발급자), <code>sub</code> (주체), <code>exp</code> (만료 시간) 등 표준화된 클레임이다.</li>
<li><strong>공개 클레임 (Public Claims)</strong>: <code>userId</code>, <code>role</code>과 같이 애플리케이션에서 사용하는 사용자 정의 클레임이다.</li>
<li><strong>비공개 클레임 (Private Claims)</strong>: SP와 IDP 간의 특별한 계약에 따라 사용되는 클레임이다.</li>
</ul>
<h2 id="pki와-비대칭-키-서명">PKI와 비대칭 키 서명</h2>
<p><strong>PKI</strong>는 공개 키 암호 방식을 활용해 데이터의 무결성과 인증을 보장하는 시스템이다. 이걸 SSO에 적용시켜본다면 ?! SSO에서 PKI는 IDP가 JWT를 서명하고, SP가 이를 검증하는 역할을 한다.</p>
<h3 id="비대칭-키-개념">비대칭 키 개념</h3>
<ul>
<li><strong>공개 키 (Public Key)</strong>: 누구에게나 공개할 수 있는 키로, 주로 서명 검증에 사용</li>
<li><strong>개인 키 (Private Key)</strong>: 오직 소유자만이 가지고 있는 비밀 키로, 주로 서명 생성에 사용</li>
</ul>
<p><strong>IDP</strong>는 개인 키로 JWT에 서명하며, <strong>SP</strong>는 공개 키를 사용하여 JWT가 변조되지 않았음을 검증한다.</p>
<p>토큰 발급자만이 가질수있는 개인키로 서명함으로써 신뢰관계를 형성 할 수 있는 것이다 !</p>
<h3 id="jwks-json-web-key-set">JWKS (JSON Web Key Set)</h3>
<ul>
<li><strong>JWKS</strong>는 공개 키를 JSON 형태로 제공하는 일종의 키 저장소</li>
<li>SP는 IDP의 JWKS 엔드포인트에서 공개 키를 가져와 JWT 서명을 검증</li>
<li>JWKS는 여러 키를 포함할 수 있으며, 각 키는 <strong>kid (Key ID)</strong>를 통해 식별</li>
</ul>
<p>자 이제 SSO를 좀 더 이해하기위한 기본 개념을 다루었으니 이어서 SSO의 기본을 다뤄보겠다 !!</p>
<h2 id="sso-single-sign-on의-이해">SSO (Single Sign-On)의 이해</h2>
<ul>
<li><strong>Single Sign-On (SSO)</strong>는 사용자가 한 번의 로그인으로 여러 독립된 애플리케이션 또는 시스템에 접근할 수 있도록 해주는 인증 방식이다. SSO는 사용자의 편의성을 극대화하고, 각 서비스에서 별도의 로그인 절차를 거치는 불편함을 없애준다.</li>
</ul>
<h3 id="sso의-필요성">SSO의 필요성</h3>
<ul>
<li><strong>사용자 경험 향상</strong>: 한 번의 로그인으로 여러 서비스에 접근 가능하게 하여 사용자가 반복적인 로그인을 할 필요가 없도록 한다.</li>
<li><strong>중앙 집중식 관리</strong>: IDP를 통해 인증 정보가 중앙에서 관리되므로 보안성과 효율성이 향상된다.<ul>
<li>애초에 인증하는 길이 하나인 셈이니 길이 여러개일때보다 위험이 적을것이다 !</li>
</ul>
</li>
</ul>
<h3 id="sso의-구성-요소">SSO의 구성 요소</h3>
<ul>
<li><strong>IDP (Identity Provider)</strong>: 사용자 인증을 담당하며, Access, ID, Refresh 토큰을 발급한다.</li>
<li><strong>SP (Service Provider)</strong>: IDP가 발급한 토큰을 검증하고 사용자가 접근하려는 서비스에 대해 권한을 부여한다.</li>
</ul>
<h2 id="jwt-기반-sso-아키텍처-설계">JWT 기반 SSO 아키텍처 설계</h2>
<p>SSO 시스템은 크게 <strong>IDP</strong>와 <strong>SP</strong>로 나뉘며, 이들이 협력하여 사용자 인증 및 권한 부여를 수행한다. 이때 <strong>JWT</strong>는 사용자에 대한 인증 정보를 안전하게 SP로 전달하는 매개체 역할을 한다.</p>
<h3 id="주요-아키텍처">주요 아키텍처</h3>
<ol>
<li><strong>IDP</strong>: 사용자를 인증하고 JWT를 발급한다. 각 JWT에는 서명이 포함되어 있어 무결성을 보장한다.</li>
<li><strong>SP</strong>: 클라이언트로부터 JWT를 수신한 후, 이를 검증하여 사용자의 인증 여부와 접근 권한을 확인한다.</li>
<li><strong>JWT와 JWKS</strong>: SP는 JWT에 포함된 <strong>kid</strong> 값을 사용해 IDP의 <strong>JWKS 엔드포인트</strong>에서 적절한 공개 키를 가져와 서명을 검증한다.</li>
</ol>
<h2 id="idp와-sp의-역할">IDP와 SP의 역할</h2>
<h3 id="idp-identity-provider의-역할">IDP (Identity Provider)의 역할</h3>
<ul>
<li>사용자 로그인 페이지 제공</li>
<li>사용자 자격 증명 확인</li>
<li>성공적인 인증 후 Access 토큰, ID 토큰, Refresh 토큰을 생성</li>
<li>JWKS 엔드포인트를 통해 공개 키를 제공하여 SP들이 서명을 검증할 수 있도록 함</li>
</ul>
<h3 id="sp-service-provider의-역할">SP (Service Provider)의 역할</h3>
<ul>
<li>IDP로부터 발급받은 JWT를 검증하여 사용자가 인증되었음을 확인</li>
<li>Access 토큰을 통해 사용자가 요청한 리소스에 대한 권한을 결정</li>
<li>JWKS를 사용하여 서명 검증을 통해 JWT의 무결성을 확인</li>
</ul>
<p>이와 같은 내용으로 JWT와 PKI를 사용한 SSO 시스템의 개요와 기본 원리를 설명하였다. 다음 포스팅에서는 JWT 토큰의 종류와 목적, SSO 플로우에서의 토큰 발급 및 갱신 과정, IDP와 SP 간의 통신 및 인증 과정 등을 더 깊이 있게 다룰 예정이다 ! ! !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTPS 요청 처리부터 WAS 응답 반환까지의 전체 흐름]]></title>
            <link>https://velog.io/@1im_chaereong/HTTPS-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC%EB%B6%80%ED%84%B0-WAS-%EC%9D%91%EB%8B%B5-%EB%B0%98%ED%99%98%EA%B9%8C%EC%A7%80%EC%9D%98-%EC%A0%84%EC%B2%B4-%ED%9D%90%EB%A6%84</link>
            <guid>https://velog.io/@1im_chaereong/HTTPS-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC%EB%B6%80%ED%84%B0-WAS-%EC%9D%91%EB%8B%B5-%EB%B0%98%ED%99%98%EA%B9%8C%EC%A7%80%EC%9D%98-%EC%A0%84%EC%B2%B4-%ED%9D%90%EB%A6%84</guid>
            <pubDate>Sun, 17 Nov 2024 14:15:03 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅까지 이어서 PKI와 SSL/TLS에 대해서 공부를 했으니 ! 이제 이것들이 어떻게 적용되어 흘러가는지, Spring의 환경에서는 어떤 흐름으로 동작하는지 정리해보았다 !!</p>
<h3 id="1-클라이언트-https-요청-및-ssltls-설정">1. 클라이언트 HTTPS 요청 및 SSL/TLS 설정</h3>
<ol>
<li><strong>클라이언트가 HTTPS URL을 통해 서버로 요청</strong><ul>
<li>사용자가 웹 브라우저를 통해 <strong>https://</strong>로 시작하는 URL에 접근한다.</li>
<li>브라우저는 서버와 <strong>암호화된 연결</strong>을 위해 <strong>HTTPS</strong> 프로토콜을 사용한다.</li>
</ul>
</li>
<li><strong>Client Hello 메시지 전송</strong><ul>
<li>브라우저는 <strong>SSL/TLS 핸드셰이크</strong>를 시작한다.</li>
<li><strong>Client Hello</strong> 메시지를 서버에 보내며, 이 메시지에는 클라이언트가 지원하는 <strong>SSL/TLS 버전</strong>, <strong>암호화 알고리즘(Cipher Suites)</strong> 목록, 그리고 <strong>클라이언트 난수(Random Number)</strong> 등이 포함되어 있다.</li>
</ul>
</li>
<li><strong>Server Hello와 인증서 전달</strong><ul>
<li>서버는 클라이언트의 <strong>Client Hello</strong>를 수신하고, 서버에서 사용할 <strong>SSL/TLS 버전</strong>과 <strong>암호화 알고리즘</strong>을 선택한다.</li>
<li><strong>Server Hello</strong> 메시지와 함께 <strong>SSL 인증서</strong>를 클라이언트에게 보낸다. 이 인증서에는 서버의 <strong>공개키</strong>와 <strong>CA의 서명</strong>이 포함되어 있다.</li>
</ul>
</li>
<li><strong>인증서 검증</strong><ul>
<li>클라이언트는 받은 인증서를 검증한다.</li>
<li>클라이언트는 <strong>CA의 공개키</strong>로 인증서의 <strong>서명</strong>을 검증하고, <strong>서버의 공개키</strong>가 신뢰할 수 있는지 판단한다.</li>
</ul>
</li>
<li><strong>프리마스터 시크릿 전송 및 대칭키 생성</strong><ul>
<li>클라이언트는 <strong>프리마스터 시크릿(Premaster Secret)</strong>을 생성하고, 서버의 <strong>공개키</strong>로 암호화하여 서버에 전송한다.</li>
<li>서버는 자신의 <strong>개인키</strong>로 <strong>프리마스터 시크릿</strong>을 복호화한다.</li>
<li>서버와 클라이언트는 서로 주고받은 <strong>난수</strong>와 <strong>프리마스터 시크릿</strong>을 조합해 <strong>대칭키(세션 키)</strong>를 생성한다.</li>
</ul>
</li>
<li><strong>암호화된 데이터 통신 준비 완료</strong><ul>
<li>이제 클라이언트와 서버는 <strong>대칭키</strong>를 사용하여 이후의 모든 데이터를 <strong>암호화</strong>하고 <strong>복호화</strong>하면서 통신을 진행한다.</li>
</ul>
</li>
</ol>
<h3 id="2-https-요청의-was-수신">2. HTTPS 요청의 WAS 수신</h3>
<ol>
<li><strong>로드 밸런서(선택적 단계)</strong><ul>
<li>요청이 서버에 도달하기 전에, 큰 웹사이트나 서비스의 경우 <strong>로드 밸런서</strong>를 통해 여러 대의 서버 중 하나로 요청이 전달된다.</li>
<li>로드 밸런서는 서버 부하를 분산시키기 위해 클라이언트의 요청을 가장 적합한 서버로 전달한다.</li>
</ul>
</li>
<li><strong>웹 서버 수신 (Nginx/Apache 등)</strong><ul>
<li>서버에 도달한 요청은 <strong>웹 서버(예: Nginx, Apache)</strong>가 먼저 수신한다.</li>
<li>웹 서버는 정적 콘텐츠(HTML, 이미지 등)를 처리하거나, <strong>동적 요청</strong>을 처리하기 위해 WAS에 요청을 넘긴다.</li>
</ul>
</li>
<li><strong>WAS(Web Application Server)로 전달</strong><ul>
<li><strong>동적 요청</strong>은 <strong>WAS(예: Tomcat, JBoss)</strong>로 전달된다.</li>
<li>WAS는 요청을 분석하고, 해당 요청에 대해 어떤 <strong>서블릿</strong>이나 <strong>엔드포인트</strong>가 요청을 처리해야 하는지 결정한다.</li>
</ul>
</li>
</ol>
<h3 id="3-쓰레드-풀thread-pool에서-쓰레드-할당">3. 쓰레드 풀(Thread Pool)에서 쓰레드 할당</h3>
<ol>
<li><strong>쓰레드 풀에서 쓰레드 확보</strong><ul>
<li>WAS는 <strong>쓰레드 풀(Thread Pool)</strong>에서 사용 가능한 <strong>쓰레드</strong>를 확보한다.</li>
<li>쓰레드 풀은 서버의 성능을 최적화하기 위해 일정 수의 쓰레드를 미리 생성하고 관리한다.</li>
</ul>
</li>
<li><strong>쓰레드 할당 및 요청 처리 준비</strong><ul>
<li>요청이 쓰레드에 할당되면, 요청 처리를 위해 할당된 쓰레드는 <strong>서블릿 컨테이너(Servlet Container)</strong>에 요청을 전달한다.</li>
</ul>
</li>
</ol>
<h3 id="4-서블릿-컨테이너-및-필터filter-처리">4. 서블릿 컨테이너 및 필터(Filter) 처리</h3>
<ol>
<li><strong>서블릿 매핑 확인</strong><ul>
<li>서블릿 컨테이너는 요청된 URL을 기준으로 <strong>서블릿 매핑</strong>을 확인하여, 해당 요청을 처리할 서블릿을 찾는다.</li>
</ul>
</li>
<li><strong>필터 적용</strong><ul>
<li>요청에 대해 <strong>필터(Filter)</strong>가 존재하는 경우, 이를 먼저 적용한다.</li>
<li>필터는 <strong>인증/인가</strong>, <strong>로깅</strong>, <strong>압축</strong> 등 요청 전후 처리에 사용된다.</li>
</ul>
</li>
<li><strong>서블릿 호출</strong><ul>
<li>필터 처리가 완료되면, 요청은 해당 서블릿으로 전달되어 <strong>비즈니스 로직</strong> 처리가 시작된다.</li>
</ul>
</li>
</ol>
<h3 id="5-비즈니스-로직-처리-및-데이터베이스-접근">5. 비즈니스 로직 처리 및 데이터베이스 접근</h3>
<ol>
<li><strong>비즈니스 로직 처리</strong><ul>
<li>서블릿에서는 요청에 따른 <strong>비즈니스 로직</strong>을 처리한다. 예를 들어, 사용자가 상품을 검색하거나 주문을 요청하는 등의 작업이 여기에 해당된다.</li>
</ul>
</li>
<li><strong>서비스 계층 호출</strong><ul>
<li>서블릿은 보통 <strong>서비스 계층(Service Layer)</strong>을 호출하여 구체적인 비즈니스 로직을 처리한다.</li>
<li>서비스 계층은 필요시 <strong>DAO(Data Access Object)</strong>를 호출하여 데이터베이스에 접근한다.</li>
</ul>
</li>
<li><strong>데이터베이스 접근</strong><ul>
<li><strong>DAO</strong>는 <strong>JDBC</strong>나 <strong>JPA</strong> 등을 사용해 데이터베이스에 접근하여 필요한 데이터를 조회하거나 업데이트한다.</li>
</ul>
</li>
</ol>
<h3 id="6-응답-생성-및-반환">6. 응답 생성 및 반환</h3>
<ol>
<li><strong>응답 데이터 준비</strong><ul>
<li>서비스 계층에서 얻은 데이터를 바탕으로 응답을 준비한다.</li>
<li>데이터는 <strong>JSP</strong>를 통해 HTML 형식으로 변환되거나, <strong>JSON</strong> 형식으로 만들어진다.</li>
</ul>
</li>
<li><strong>HTTP 응답 객체에 담기</strong><ul>
<li>생성된 응답 데이터를 <strong>HTTP 응답 객체(HttpServletResponse)</strong>에 담는다.</li>
<li>응답에는 <strong>상태 코드(예: 200 OK)</strong>, <strong>헤더 정보</strong>, <strong>본문 데이터</strong>가 포함된다.</li>
</ul>
</li>
<li><strong>필터를 통한 후처리</strong><ul>
<li>응답이 생성된 후에는, 다시 <strong>필터(Filter)</strong>를 통해 후처리가 이루어질 수 있다.</li>
<li>예를 들어, 응답 압축이나 헤더 추가와 같은 작업이 이루어진다.</li>
</ul>
</li>
</ol>
<h3 id="7-쓰레드-반환-및-커넥션-종료">7. 쓰레드 반환 및 커넥션 종료</h3>
<ol>
<li><strong>쓰레드 반환</strong><ul>
<li>응답 처리가 완료되면, 사용한 <strong>쓰레드</strong>는 <strong>쓰레드 풀</strong>로 반환되어 다른 요청을 처리할 수 있도록 대기 상태로 돌아간다.</li>
</ul>
</li>
<li><strong>커넥션 유지/종료</strong><ul>
<li>클라이언트와 서버 간의 <strong>커넥션</strong>은 <strong>keep-alive</strong> 설정에 따라 유지되거나 종료된다.</li>
<li>유지된 커넥션은 다음 요청에 재사용될 수 있다.</li>
</ul>
</li>
<li><strong>자원 정리</strong><ul>
<li>사용한 자원(예: 데이터베이스 연결 등)은 <strong>close()</strong> 메서드를 통해 해제하여 <strong>자원 누수</strong>를 방지한다.</li>
</ul>
</li>
</ol>
<h3 id="8-세션-및-쿠키-처리">8. 세션 및 쿠키 처리</h3>
<ol>
<li><strong>세션 식별 및 유지</strong><ul>
<li>서버는 클라이언트의 요청에 포함된 <strong>쿠키</strong>에서 <strong>세션 ID</strong>를 확인하여 기존 세션을 유지할지, 새로운 세션을 생성할지 결정한다.</li>
</ul>
</li>
<li><strong>세션 데이터 활용</strong><ul>
<li>기존 세션이 존재하는 경우, 해당 세션에 저장된 데이터를 활용하여 요청을 처리한다.</li>
</ul>
</li>
<li><strong>세션 쿠키 설정</strong><ul>
<li>세션 정보가 갱신되면, 서버는 <strong>Set-Cookie</strong> 헤더를 통해 새로운 <strong>세션 ID</strong> 쿠키를 브라우저에 전달할 수 있다.</li>
</ul>
</li>
</ol>
<p>이렇게 HTTPS 요청부터 WAS를 거쳐 응답이 클라이언트에게 돌아가기까지의 흐름을 세부적으로 나누어 설명해 보았다. 전체 과정을 통해 암호화된 연결 설정, 웹 서버와 애플리케이션 서버의 역할, 쓰레드 처리, 비즈니스 로직 처리, 그리고 응답 생성과 반환의 과정을 조금이나마 더 이해할수있지않을가 생각한다 !</p>
<p>그래도 계속 공부해야 할 것 같다 !!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTPS: SSL/TLS 핸드셰이크와 세션 관리 이해하기]]></title>
            <link>https://velog.io/@1im_chaereong/HTTPS-SSLTLS-%ED%95%B8%EB%93%9C%EC%85%B0%EC%9D%B4%ED%81%AC%EC%99%80-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%AC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@1im_chaereong/HTTPS-SSLTLS-%ED%95%B8%EB%93%9C%EC%85%B0%EC%9D%B4%ED%81%AC%EC%99%80-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%AC-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 17 Nov 2024 14:12:23 GMT</pubDate>
            <description><![CDATA[<p>PKI를 통해 전자서명과 전자봉투를 생성하고 생성된 전자봉투를 더 안정적으로 전송하기위해서 사용하는 SSL/TLS를 좀 더 깊게 공부하려한다 ! 웹에서 HTTPS를 사용한다는 것은 단순히 주소창에 <code>https://</code>를 붙이는 것을 넘어, <strong>보안</strong>을 보장하기 위한 SSL/TLS 프로토콜을 활용한 암호화 과정을 의미한다. 이 포스팅에서는 <strong>HTTP에서 HTTPS로의 변환 과정</strong>과 <strong>SSL/TLS 핸드셰이크 과정</strong>을 포함해 HTTPS가 어떻게 데이터를 보호하는지 알아보겠다 !</p>
<h2 id="1-https란">1. HTTPS란?</h2>
<p>HTTPS는 HTTP에 SSL/TLS 보안 계층을 추가하여 <strong>암호화된 안전한 통신</strong>을 가능하게 하는 프로토콜이다. HTTPS는 주로 웹사이트에서 개인정보 보호를 위해 사용된다. 예를 들어, HTTPS는 데이터 전송 중 <strong>도청, 변조</strong>를 방지하여 사용자 정보가 안전하게 보호된다.</p>
<h3 id="http-vs-https">HTTP vs HTTPS</h3>
<ul>
<li><strong>HTTP</strong>는 데이터가 평문으로 전송되기 때문에 도청 위험이 있다.</li>
<li><strong>HTTPS</strong>는 SSL/TLS 암호화를 사용해 데이터 전송을 보호하며, 이를 위해 <strong>핸드셰이크 과정</strong>을 통해 클라이언트와 서버 간 <strong>암호화 키를 교환</strong>한다.</li>
</ul>
<h2 id="2-https-전환의-핵심-ssltls-핸드셰이크">2. HTTPS 전환의 핵심: SSL/TLS 핸드셰이크</h2>
<p>HTTPS 통신에서 <strong>핸드셰이크(Handshake)</strong> 과정이 초기 연결의 핵심이다. 핸드셰이크 과정은 클라이언트와 서버가 <strong>암호화 방식과 키를 협상하고 안전한 통신을 설정</strong>하기 위한 단계이다. 각 단계에서 클라이언트와 서버는 특정 데이터를 교환하고, <strong>대칭 키</strong>를 생성하여 이후 통신에서 사용할 준비를 한다.</p>
<h3 id="핸드셰이크-과정-요약">핸드셰이크 과정 요약</h3>
<p>핸드셰이크는 크게 다음 단계로 나뉜다:</p>
<ol>
<li><strong>ClientHello</strong>: 클라이언트가 지원하는 암호화 방식, TLS 버전, 클라이언트 난수 전송</li>
<li><strong>ServerHello</strong>: 서버가 선택한 암호화 방식, 서버 난수 전송</li>
<li><strong>서버 인증서 전송</strong>: 서버는 자신의 공개 키를 포함한 인증서를 클라이언트에 전송</li>
<li><strong>프리마스터 시크릿 전송</strong>: 클라이언트가 생성한 프리마스터 시크릿을 서버 공개 키로 암호화해 전송</li>
<li><strong>대칭 키 생성</strong>: 클라이언트와 서버는 클라이언트 난수, 서버 난수, 프리마스터 시크릿을 조합해 대칭 키 생성</li>
<li><strong>암호화 통신 시작</strong>: 이후 데이터는 이 대칭 키를 사용해 암호화하여 통신</li>
</ol>
<h2 id="3-단계별-https-핸드셰이크-과정-자세히-살펴보기">3. 단계별 HTTPS 핸드셰이크 과정 자세히 살펴보기</h2>
<p>밑의 그림은 SSL/TLS의 전체적인 동작흐름과 CA가 인증서를 서명하는 과정까지를 내가 직접 그려보았다 ! 참고 해서 밑의 정리된 글을 읽으면 더 이해가 조금이라도 잘 되지않을까.. 싶다 ..
<img src="https://velog.velcdn.com/images/1im_chaereong/post/6b495dd0-6d3f-41e1-b8a7-fd0020d84015/image.png" alt=""></p>
<h3 id="31-clienthello-메시지-전송">3.1. ClientHello 메시지 전송</h3>
<ol>
<li>클라이언트는 서버에 <strong>ClientHello</strong> 메시지를 전송한다.</li>
<li>이 메시지에는 클라이언트가 지원하는 <strong>암호화 방식 목록</strong>, <strong>TLS 버전</strong>, 그리고 <strong>클라이언트 난수</strong>가 포함된다.</li>
</ol>
<h3 id="32-serverhello-메시지-전송">3.2. ServerHello 메시지 전송</h3>
<ol>
<li>서버는 클라이언트의 요청을 수신한 후 <strong>클라이언트의 암호화 방식 목록 중 하나를 선택</strong>하여 암호화 방식을 결정한다.</li>
<li><strong>서버 난수</strong>를 생성하여 <strong>ServerHello</strong> 메시지로 클라이언트에게 응답한다.</li>
</ol>
<h3 id="33-서버의-인증서-전송">3.3. 서버의 인증서 전송</h3>
<ol>
<li>서버는 <strong>인증서(Certificate)</strong>를 클라이언트에게 전송한다.</li>
<li>인증서에는 서버의 <strong>공개 키</strong>와 서버의 신원 정보를 포함하고 있으며, 인증 기관(CA)이 서명하여 신뢰성을 보장한다.</li>
</ol>
<h3 id="34-클라이언트의-프리마스터-시크릿-전송">3.4. 클라이언트의 프리마스터 시크릿 전송</h3>
<ol>
<li>클라이언트는 서버의 공개 키를 사용해 <strong>프리마스터 시크릿(Premaster Secret)</strong>을 생성한 후, 이를 서버의 공개키로 암호화하여 서버로 전송한다.</li>
<li>서버는 자신의 <strong>개인 키</strong>로 이 프리마스터 시크릿을 복호화하여 원래 값을 얻는다.</li>
</ol>
<h3 id="35-대칭-키-생성">3.5. 대칭 키 생성</h3>
<ol>
<li><strong>클라이언트 난수</strong>, <strong>서버 난수</strong>, <strong>프리마스터 시크릿</strong>을 조합하여 클라이언트와 서버 모두 같은 <strong>대칭 키(세션 키)</strong>를 생성한다.</li>
<li>이 대칭 키는 이후의 통신 데이터를 암호화하는 데 사용된다.</li>
</ol>
<h3 id="36-암호화된-데이터-전송">3.6. 암호화된 데이터 전송</h3>
<p>핸드셰이크가 완료되면 클라이언트와 서버는 이제 <strong>대칭 키로 데이터를 암호화</strong>하여 안전하게 통신을 시작할 수 있다.</p>
<h2 id="4-세션-재사용으로-핸드셰이크-간소화하기">4. 세션 재사용으로 핸드셰이크 간소화하기</h2>
<p>핸드셰이크 과정은 많은 연산을 필요로 하므로, <strong>세션 ID</strong> 또는 <strong>세션 티켓</strong>을 통해 세션 재사용을 지원하여 이후 요청에서 핸드셰이크 과정을 생략할 수 있다.</p>
<h3 id="세션-id-방식">세션 ID 방식</h3>
<p>세션 ID는 서버와 클라이언트 간의 연결을 유지하고, 동일한 세션을 재사용할 수 있게 하기 위해 사용되는 고유한 식별자이다. 세션 ID 방식은 다음과 같은 방식으로 작동한다.</p>
<ol>
<li><strong>세션 생성</strong>: 클라이언트가 처음 서버에 연결할 때, 서버는 <strong>세션 ID</strong>를 생성하여 클라이언트에게 제공한다. 이 세션 ID는 쿠키에 저장되어 클라이언트의 브라우저에 남아 있게 된다 !</li>
<li><strong>세션 정보 저장</strong>: 서버는 생성한 <strong>대칭키</strong>와 관련된 암호화 매개변수 등의 세션 정보를 <strong>서버 내부의 데이터베이스</strong> 또는 <strong>메모리</strong>에 저장한다. 이때, 세션 정보에는 이 연결에 사용된 대칭키가 포함되며, 이 데이터를 참조할 수 있는 위치(주소)가 세션 ID에 쌓이게 된다.</li>
<li><strong>세션 재사용 요청</strong>: 클라이언트가 다시 서버에 연결할 때, 쿠키에 저장된 세션 ID를 서버에 전송한다. 서버는 이 <strong>세션 ID를 사용해 데이터베이스나 메모리에서 해당 세션 정보</strong>를 조회한다.</li>
<li><strong>대칭키 복원</strong>: 세션 ID에 쌓여있는 메모리나, 데이터베이스 주소를 통해 서버는 기존 세션에 사용되었던 <strong>대칭키와 관련된 정보</strong>를 복원한다. 이로 인해 서버와 클라이언트 간의 핸드셰이크 과정을 생략하고, 이전 세션에서 사용된 대칭키를 그대로 재사용할 수 있다.</li>
<li><strong>빠른 연결 설정</strong>: 이렇게 복원된 대칭키를 이용해 서버와 클라이언트는 <strong>빠르게 데이터를 암호화하여 통신</strong>할 수 있다. 즉, 모든 복잡한 핸드셰이크 과정을 다시 수행하지 않고도 암호화된 상태로 데이터 전송이 가능해지는것이다 ! ! !</li>
</ol>
<p>세션 ID 방식의 핵심은 <strong>세션 ID가 단순히 세션 정보를 저장하고 있는 위치를 가리키는 역할</strong>을 한다는 점이다. 실제 대칭키와 기타 세션 관련 정보는 서버 쪽에 저장되며, 세션 ID는 이를 참조할 수 있는 인덱스 역할을 한다 ! !</p>
<h3 id="세션-티켓-방식">세션 티켓 방식</h3>
<p>세션 티켓 방식은 세션 정보를 서버에 저장하지 않고, 클라이언트 측에 전달하여 다음 연결 시 재사용하는 방식이다. 이는 세션 ID 방식과 다르게 서버의 메모리를 절약하고, 서버에 대한 부담을 줄이는 장점이 있다. 세션 티켓 방식은 다음과 같은 방식으로 작동한다.</p>
<ol>
<li><strong>세션 티켓 생성</strong>: 처음 서버와 클라이언트가 연결을 맺을 때, 서버는 연결에 사용된 <strong>대칭키</strong>와 암호화 매개변수 등의 정보를 담은 <strong>세션 티켓</strong>을 생성한다. 이 세션 티켓은 서버가 보유한 <strong>대칭키</strong>를 사용하여 암호화되기 때문에, 클라이언트는 이 티켓의 내용을 알 수 없다.</li>
<li><strong>세션 티켓 전달</strong>: 서버는 이 암호화된 <strong>세션 티켓</strong>을 클라이언트에게 전달한다. 클라이언트는 이 세션 티켓을 <strong>쿠키</strong>나 <strong>로컬 스토리지</strong>에 저장하여 이후 서버와의 연결에 사용할 수 있게 한다.</li>
<li><strong>세션 티켓 재사용</strong>: 클라이언트가 서버와 다시 연결을 맺을 때, 이전에 받은 <strong>세션 티켓을 서버에 다시 전송한</strong>다. 서버는 <strong>자신의 대칭키</strong>를 사용해 이 세션 티켓을 복호화하고, 이전에 사용된 <strong>대칭키와 암호화 매개변수</strong>를 복원한다.</li>
<li><strong>빠른 연결 설정</strong>: 이렇게 복원된 대칭키를 이용해 서버와 클라이언트는 <strong>핸드셰이크 과정 없이 빠르게 연결</strong>을 설정하고, 대칭키로 데이터를 암호화하여 통신할 수 있다.</li>
</ol>
<p>세션 티켓 방식의 핵심은 <strong>세션 정보를 클라이언트 측에 저장</strong>하여 서버의 메모리 부담을 줄이는 것이다. 서버는 단지 세션 티켓을 복호화하여 정보를 복원하는 역할을 하며, 이를 통해 세션을 재사용할 수 있게 된다 !</p>
<h3 id="세션-id--세션-티켓">세션 ID / 세션 티켓</h3>
<ul>
<li><strong>세션 ID 방식</strong>은 <strong>세션 정보가 서버에 저장</strong>되고, 클라이언트는 세션을 식별하기 위한 <strong>세션 ID</strong>만을 보유한다. 서버는 해당 세션 정보를 기반으로 대칭키를 복원하여 재사용한다.</li>
<li><strong>세션 티켓 방식</strong>은 <strong>세션 정보가 클라이언트 측에 저장된</strong>다. 서버는 클라이언트가 제공한 <strong>세션 티켓을 복호화</strong>하여 필요한 정보를 복원하고 세션을 재사용한다.</li>
</ul>
<p>하지만 ?! 주로 세션 아이디만을 사용하는거같긴하다 !</p>
<p>HTTPS는 SSL/TLS 프로토콜을 사용하여 HTTP 요청을 암호화된 상태로 전송함으로써 웹의 보안을 강화한다. HTTPS 전환 과정은 핸드셰이크를 통해 이루어지며, 세션 재사용 기능을 통해 성능을 최적화한다. 공개 키와 개인 키 쌍은 보안을 강화하는 핵심이며, HTTPS는 이 과정을 통해 클라이언트와 서버 간 안전한 데이터 전송을 보장할수있다 !!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PKI 기반 인증서 발급, 갱신, 폐지 절차의 이해]]></title>
            <link>https://velog.io/@1im_chaereong/PKI-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89-%EA%B0%B1%EC%8B%A0-%ED%8F%90%EC%A7%80-%EC%A0%88%EC%B0%A8%EC%9D%98-%EC%9D%B4%ED%95%B4</link>
            <guid>https://velog.io/@1im_chaereong/PKI-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%B0%9C%EA%B8%89-%EA%B0%B1%EC%8B%A0-%ED%8F%90%EC%A7%80-%EC%A0%88%EC%B0%A8%EC%9D%98-%EC%9D%B4%ED%95%B4</guid>
            <pubDate>Sun, 17 Nov 2024 14:10:38 GMT</pubDate>
            <description><![CDATA[<p>이번 포스팅에서는 <strong>PKI(공개키 기반 인프라)</strong>를 활용한 <strong>공개키 배포 방식</strong>, <strong>암복호화 과정</strong>, 그리고 <strong>인증서 발급 절차</strong>와 <strong>인증서 갱신 및 폐지</strong>에 대해 설명하려한다. PKI는 데이터의 기밀성, 무결성, 신원 인증, 부인 방지 등의 요소를 보장하기 위해 필수적인 보안 인프라이다. 아래에서 각 요소가 어떻게 작동하고, 이들이 서로 연결되어 데이터 보안을 강화하는지 공부해보려한다 !!</p>
<h3 id="1-pki-기반-공개키-배포">1. PKI 기반 공개키 배포</h3>
<p><strong>PKI 기반 공개키 배포</strong>는 공개키의 신뢰성을 보장하기 위해 <strong>CA(인증기관)</strong>에서 발급한 <strong>인증서</strong>를 사용하는 방식이다. 이 방식에서 CA는 사용자의 공개키와 신원 정보를 포함한 인증서를 발급하여, 해당 공개키가 신뢰할 수 있는 사용자에게 속함을 증명한다.</p>
<h3 id="디렉토리를-통한-공개키-관리-방식">디렉토리를 통한 공개키 관리 방식</h3>
<ol>
<li><strong>공개 디렉토리 사용</strong>: <strong>공개 디렉토리</strong>에 사용자의 인증서를 저장하여 누구나 필요할 때 이를 조회할 수 있도록 한다.</li>
<li><strong>공개키 요청</strong>: 송신자가 수신자의 <strong>인증서</strong>를 요청하고, 이 인증서에 포함된 <strong>공개키</strong>를 사용해 데이터를 암호화한다.</li>
<li><strong>인증서 검증</strong>: 인증서를 받은 송신자는 해당 인증서가 신뢰할 수 있는 CA에 의해 발급된 것인지를 확인하여 <strong>공개키의 신뢰성</strong>을 확보한다.</li>
</ol>
<h3 id="2-pki-기반-암복호화">2. PKI 기반 암복호화</h3>
<p><strong>암호화</strong>와 <strong>복호화</strong>는 데이터의 기밀성을 보장하기 위한 중요한 과정이다. PKI는 <strong>대칭키 암호화</strong>와 <strong>비대칭키 암호화</strong>를 모두 사용하여 효율성과 안전성을 동시에 확보한다.</p>
<ul>
<li><strong>대칭키 암호화</strong>: 송신자는 데이터를 <strong>대칭키(비밀키)</strong>로 암호화한다. 대칭키 암호화는 속도가 빠르기 때문에 대량의 데이터 암호화에 적합하다.</li>
<li><strong>대칭키의 비대칭 암호화</strong>: 대칭키 자체는 송신자의 <strong>개인키</strong>와 수신자의 <strong>공개키</strong>를 사용한 <strong>비대칭키 암호화</strong> 방식으로 보호한다. 이를 통해 대칭키가 전송 중에 노출되지 않도록 한다.</li>
<li><strong>복호화 과정</strong>: 수신자는 <strong>자신의 개인키</strong>를 사용해 대칭키를 복호화한 후, 이 대칭키로 암호화된 데이터를 복호화하여 원본 데이터를 획득한다.</li>
</ul>
<h3 id="3-사용자-ra-ca를-포함한-인증서-발급-절차">3. 사용자, RA, CA를 포함한 인증서 발급 절차</h3>
<p>PKI 시스템에서 <strong>인증서 발급 절차</strong>는 <strong>사용자</strong>, <strong>RA(등록기관)</strong>, <strong>CA(인증기관)</strong> 간의 협력으로 이루어진다. 각 기관은 다음과 같은 역할을 수행한다:</p>
<ol>
<li><strong>사용자</strong><ul>
<li><strong>키 쌍 생성</strong>: 사용자는 <strong>개인키</strong>와 <strong>공개키</strong>로 이루어진 <strong>키 쌍</strong>을 생성한다.</li>
<li><strong>인증서 발급 요청</strong>: 사용자는 RA를 통해 <strong>인증서 발급 요청 메시지</strong>를 생성하고, 이 메시지에 자신의 <strong>공개키</strong>와 <strong>신원 정보</strong>를 포함시킨다.</li>
</ul>
</li>
<li><strong>RA(등록기관)</strong><ul>
<li><strong>사용자 정보 확인</strong>: RA는 사용자의 신원을 확인하고 사용자가 신뢰할 수 있는지 검증한다.</li>
<li><strong>요청 전송</strong>: RA는 사용자의 인증서 발급 요청을 <strong>CA</strong>로 전달한다.</li>
</ul>
</li>
<li><strong>CA(인증기관)</strong><ul>
<li><strong>인증서 발급</strong>: CA는 사용자가 제공한 정보를 바탕으로 <strong>인증서</strong>를 발급한다. 이 인증서에는 사용자의 공개키와 신원 정보가 포함되며, CA의 서명을 통해 신뢰성을 부여한다.<ul>
<li><strong>CA의 서명</strong>: CA는 서버에서 받은 인증서에 자신의 <strong>개인키</strong>로 서명을 한다. 이를 통해 해당 인증서가 신뢰할 수 있는 CA에 의해 발급되었음을 보장한다.</li>
</ul>
</li>
<li><strong>인증서 게시</strong>: 발급된 인증서는 <strong>공개 디렉토리</strong>에 게시되어 다른 사용자가 접근할 수 있도록 한다.<ul>
<li><strong>클라이언트의 검증</strong>: 클라이언트는 CA가 서명한 인증서를 받아서 <strong>CA의 공개키</strong>로 서명을 검증한다. 이를 통해 인증서의 신뢰성을 확인하고, 서버의 <strong>공개키</strong>를 신뢰할 수 있게 된다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="4-인증서-갱신">4. 인증서 갱신</h3>
<p><strong>인증서 갱신</strong>은 인증서의 유효 기간이 만료되기 전에 수행되어야 한다. 갱신 과정은 기존 인증서와 동일한 정보를 포함하되, 새로운 유효 기간을 설정한다. 갱신 과정에서는 다음과 같은 단계가 수행된다.</p>
<ol>
<li><strong>갱신 요청</strong>: 사용자는 기존 인증서의 만료 기간이 다가오면 <strong>CA</strong>에 갱신을 요청한다.</li>
<li><strong>RA 확인</strong>: RA는 사용자의 신원을 다시 한번 확인하고 갱신 요청을 승인한다.</li>
<li><strong>CA 발급</strong>: CA는 새로운 인증서를 발급하여 <strong>디렉토리</strong>에 게시한다.</li>
<li><strong>기존 인증서 폐지</strong>: 기존의 만료된 인증서는 <strong>폐지 리스트</strong>에 등록되어 더 이상 사용할 수 없게 된다.</li>
</ol>
<h3 id="5-인증서-폐지">5. 인증서 폐지</h3>
<p><strong>인증서 폐지</strong>는 인증서가 더 이상 유효하지 않음을 선언하는 과정이다. 인증서가 폐지되는 이유는 여러 가지가 있으며, 그중에는 <strong>비밀키 유출</strong>, <strong>사용자의 신원 변경</strong>, <strong>인증서 만료</strong> 등이 있다. 폐지된 인증서는 <strong>CRL</strong> 또는 <strong>OCSP</strong>를 통해 관리된다.</p>
<ul>
<li><strong>CRL(Certificate Revocation List)</strong>: <strong>폐기된 인증서 목록</strong>을 관리하여 특정 인증서가 유효하지 않음을 알리는 방법이다. 사용자는 CRL을 조회하여 인증서의 유효성을 확인할 수 있다.</li>
<li><strong>OCSP(Online Certificate Status Protocol)</strong>: <strong>실시간으로 인증서의 상태</strong>를 확인할 수 있는 프로토콜로, 사용자가 요청한 인증서가 유효한지, 폐지되었는지 즉시 확인할 수 있도록 한다.</li>
</ul>
<h3 id="6-crl과-ocsp를-통한-인증서-유효성-검증">6. CRL과 OCSP를 통한 인증서 유효성 검증</h3>
<ul>
<li><strong>CRL</strong> 방식: 주기적으로 갱신되는 폐기된 인증서 목록을 통해 인증서의 상태를 확인한다. CRL은 대규모 환경에서 효율적으로 사용될 수 있지만, 목록이 커질수록 관리와 다운로드가 어려워지는 단점이 있다.</li>
<li><strong>OCSP</strong> 방식: 실시간으로 인증서의 상태를 확인할 수 있어, 최신 정보에 대한 신뢰성을 제공한다. OCSP는 <strong>RFC2560</strong> 표준에 맞춰 운영되며, 인증서의 상태가 변할 때마다 빠르게 반영된다.</li>
</ul>
<h3 id="7-pki-기반-암복호화와-인증서-발급의-종합적인-이해">7. PKI 기반 암복호화와 인증서 발급의 종합적인 이해</h3>
<ul>
<li><strong>PKI 기반 공개키 배포</strong>는 CA가 발급한 인증서를 통해 사용자가 공개키의 신뢰성을 보장받을 수 있도록 한다. 이러한 인증서에는 공개키와 함께 사용자의 신원 정보가 포함되어 있어, 이를 통해 공개키가 올바른 사용자에게 속하는지를 보장한다.</li>
<li><strong>PKI 기반 암복호화</strong>는 대칭키와 비대칭키 암호화를 혼합하여 사용한다. 대칭키 암호화를 통해 데이터의 암호화를 빠르게 수행하고, 대칭키 자체를 비대칭 암호화를 통해 보호하여 보안성을 높인다.</li>
<li><strong>사용자, RA, CA 간의 인증서 발급 과정</strong>에서는 각 역할이 명확하게 정의되어 있어, 사용자의 신원 확인 및 인증서 발급의 신뢰성을 보장한다.</li>
</ul>
<p>이번 포스팅에서는 <strong>PKI 기반 공개키 배포</strong>, <strong>암복호화</strong>, <strong>인증서 발급 및 갱신 절차</strong>에 대해 다루었다. <strong>CA</strong>와 <strong>RA</strong>의 역할을 이해하고, <strong>CRL</strong>과 <strong>OCSP</strong>를 통해 인증서의 유효성을 검증하는 방법을 통해 PKI 환경에서 데이터의 <strong>기밀성</strong>, <strong>무결성</strong>, <strong>신원 인증</strong>, <strong>부인 방지</strong>를 보장할 수 있다. 이를 통해 신뢰할 수 있는 안전한 통신 환경을 구축하는 것이 PKI의 핵심 목표인것이다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전자 서명과 전자 봉투 그리고 SSL/TLS의 보안 원리]]></title>
            <link>https://velog.io/@1im_chaereong/%EC%A0%84%EC%9E%90-%EC%84%9C%EB%AA%85%EA%B3%BC-%EC%A0%84%EC%9E%90-%EB%B4%89%ED%88%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SSLTLS%EC%9D%98-%EB%B3%B4%EC%95%88-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@1im_chaereong/%EC%A0%84%EC%9E%90-%EC%84%9C%EB%AA%85%EA%B3%BC-%EC%A0%84%EC%9E%90-%EB%B4%89%ED%88%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-SSLTLS%EC%9D%98-%EB%B3%B4%EC%95%88-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Sun, 17 Nov 2024 14:10:04 GMT</pubDate>
            <description><![CDATA[<p>이어서 이번 포스팅은 현대 인터넷 보안의 근간을 이루고 있는 <strong>비대칭형 암호 시스템</strong>과 <strong>전자 서명</strong>, 그리고 이를 사용하는 <strong>PKI(공개키 기반 인프라)</strong>와 안전하게 데이터를 전송하는 방식인 SSL/TLS에 대해 아주 간단히 다뤄보겠다. <strong>전자 서명</strong>과 <strong>전자 봉투</strong>, 그리고 <strong>SSL/TLS</strong>의 역할을 단계별로 설명하며, 어떻게 각각의 요소가 데이터의 <strong>기밀성</strong>, <strong>무결성</strong>, <strong>신원 인증</strong>, <strong>부인 방지</strong>를 보장하는지 공부해보려한다 !!</p>
<h3 id="1-비대칭형-암호-시스템이란">1. 비대칭형 암호 시스템이란?</h3>
<p><strong>비대칭형 암호 시스템</strong>은 두 개의 키, 즉 <strong>공개키(Public Key)</strong>와 <strong>개인키(Private Key)</strong>를 사용하는 암호화 방식이다. <strong>공개키</strong>는 누구나 볼 수 있게 공개되어 있으며, <strong>개인키</strong>는 오직 소유자만이 보유하여 비밀로 유지한다. 비대칭형 암호화의 핵심은 <strong>공개키로 암호화된 데이터는 개인키로만 복호화할 수 있으며</strong>, 그 반대도 가능하다 !</p>
<p>이러한 특성을 활용하여 <strong>전자 서명</strong>과 <strong>기밀성</strong>을 동시에 보장하는 다양한 보안 기술이 사용된다. 특히 <strong>PKI(Public Key Infrastructure)</strong>는 공개키와 개인키를 이용하여 신뢰할 수 있는 인증과 안전한 데이터 전송을 보장하는 인프라이다.</p>
<h3 id="2-전자-서명이란">2. 전자 서명이란?</h3>
<ul>
<li><strong>전자 서명(Digital Signature)</strong>은 데이터를 송신자가 작성했음을 증명하고, 데이터가 전송 중에 변조되지 않았음을 보장하기 위한 기술이다. <strong>송신자의 신원 인증</strong>, <strong>문서의 무결성</strong>, 그리고 <strong>부인 방지</strong>를 보장한다.</li>
</ul>
<h3 id="전자-서명의-생성-과정">전자 서명의 생성 과정</h3>
<p>이전 포스팅에서 전자 서명의 생성 과정을 다뤘기에 간단하게 언급하겠다 !</p>
<ol>
<li><strong>원본 문서 준비</strong>: 송신자가 전송할 <strong>원본 문서</strong>를 준비한다.</li>
<li><strong>해시 함수 적용</strong>: 문서에 대해 <strong>해시 함수</strong>(예: SHA-256)를 적용하여 <strong>고정된 길이의 해시값</strong>을 생성한다. 이 해시값은 문서의 고유한 요약본으로, 문서가 조금이라도 변경되면 다른 해시값이 생성된다.</li>
<li><strong>개인키로 암호화 (전자 서명 생성)</strong>: 송신자는 <strong>자신의 개인키</strong>를 사용해 해시값을 암호화하여 <strong>전자 서명</strong>을 생성한다. 이 전자 서명은 해당 문서가 송신자에 의해 작성되었으며 변조되지 않았음을 증명한다.</li>
<li><strong>서명된 전자문서 생성</strong>: <strong>원본 문서</strong>와 <strong>전자 서명</strong>을 결합하여 <strong>서명된 전자문서</strong>를 생성한다. 이 문서는 수신자에게 전송된다.</li>
</ol>
<h3 id="3-전자-봉투란">3. 전자 봉투란?</h3>
<ul>
<li><strong>전자 봉투</strong>는 <strong>대칭키 암호화</strong>와 <strong>비대칭키 암호화</strong>를 결합하여 데이터를 <strong>기밀하게 보호</strong>하는 방식이다. 전자 서명이 데이터의 무결성과 신원을 보장하는 기술이라면, 전자 봉투는 전송 중 데이터의 <strong>기밀성</strong>을 보장하는 기술이다.</li>
</ul>
<h3 id="전자-봉투의-생성-과정">전자 봉투의 생성 과정</h3>
<ol>
<li><strong>대칭키 생성</strong>: 송신자는 데이터를 암호화할 임시 <strong>대칭키(비밀키)</strong>를 생성한다. 대칭키는 동일한 키로 암호화와 복호화를 수행하며, 대량의 데이터를 빠르고 효율적으로 처리할 수 있다.</li>
<li><strong>서명된 전자문서 대칭키 암호화</strong>: 생성된 <strong>대칭키</strong>를 사용해 <strong>서명된 전자문서</strong>를 암호화한다. 이 암호화된 문서를 <strong>암호화된 전자문서</strong>라고 한다.<ul>
<li><strong>대칭키를 사용하는 이유</strong>는 대칭키 암호화가 비대칭키 암호화보다 훨씬 <strong>빠르고 효율적</strong>이기 때문이다. 대칭키는 데이터의 암호화와 복호화에 동일한 키를 사용하기 때문에 대량의 데이터를 처리할 때 속도가 빠르다.</li>
</ul>
</li>
<li><strong>대칭키 비대칭 암호화</strong>: 송신자는 <strong>수신자의 공개키</strong>를 사용해 대칭키를 암호화하여 <strong>암호화된 대칭키</strong>를 생성한다. 이를 통해 대칭키는 안전하게 전송될 수 있으며, 수신자만이 <strong>개인키</strong>로 복호화할 수 있다.<ul>
<li><strong>대칭키를 비대칭키로 암호화하는 이유</strong>는 <strong>대칭키의 기밀성</strong>을 보장하기 위해서이다. 수신자의 공개키로 암호화된 대칭키는 오직 수신자의 개인키로만 복호화할 수 있으므로, 대칭키가 전송 중에 탈취되더라도 안전하게 보호된다.</li>
</ul>
</li>
<li><strong>전자 봉투 생성 및 전송</strong>: 송신자는 <strong>암호화된 서명된 전자문서</strong>와 <strong>암호화된 대칭키</strong>를 결합하여 <strong>전자 봉투</strong>를 생성하고, 이를 수신자에게 전송한다.</li>
</ol>
<p>그래서 ! 주로 전자봉투를 안전하게 전송하는 방식이 ?! SSL/TLS이다 ! 이번 포스팅에서 간단하게 알아보고 다음 포스팅에서 자세하게 다뤄보려한다 !</p>
<h3 id="4-ssltls를-통한-안전한-데이터-전송">4. SSL/TLS를 통한 안전한 데이터 전송</h3>
<ul>
<li><strong>SSL(보안 소켓 계층) / TLS(전송 계층 보안)</strong>는 인터넷을 통한 안전한 데이터 전송을 보장하는 <strong>보안 프로토콜</strong>이다. <strong>전자 봉투</strong>와 <strong>SSL/TLS</strong>를 결합하여 데이터의 <strong>기밀성</strong>, <strong>무결성</strong>을 보장한다.</li>
</ul>
<h3 id="ssltls를-통한-전송-과정">SSL/TLS를 통한 전송 과정</h3>
<p><strong>SSL/TLS</strong>는 <strong>대칭키 암호화</strong>와 <strong>비대칭키 암호화</strong>를 결합하여 안전한 데이터 전송을 보장한다. 이 과정에서 <strong>대칭키와 비대칭키</strong>가 어떻게 조합되는지 자세히 설명하겠다.</p>
<ol>
<li><strong>SSL/TLS 세션 시작</strong>: <strong>클라이언트(송신자)</strong>와 <strong>서버(수신자)</strong>는 SSL/TLS 연결을 설정하기 위해 <strong>핸드셰이크 과정</strong>을 시작한다. 이 과정에서 서버는 <strong>인증서</strong>를 통해 자신의 신원을 증명하고, 클라이언트는 서버의 신뢰성을 검증한다.</li>
<li><strong>대칭키(세션키) 교환</strong>: 핸드셰이크 과정에서 <strong>비대칭키 암호화</strong>를 사용해 <strong>대칭키(세션키)</strong>를 안전하게 교환한다. 이 세션키는 이후의 데이터 암호화에 사용된다. 비대칭키를 사용해 대칭키를 교환하는 이유는 대칭키를 안전하게 전달하기 위해서이다. 비대칭키 암호화는 공개키와 개인키의 쌍을 이용해 <strong>기밀성</strong>을 보장할 수 있기 때문에, 세션키를 안전하게 전송하는 데 적합하다.</li>
<li><strong>대칭키를 사용한 데이터 암호화</strong>: 세션이 설정된 후에는 <strong>대칭키</strong>를 사용해 데이터 전송을 수행한다. 대칭키 암호화는 <strong>빠르고 효율적</strong>이기 때문에, 이후의 모든 데이터 전송이 대칭키로 암호화되어 이루어진다. 이렇게 하면 대량의 데이터를 효율적으로 보호할 수 있다.</li>
<li><strong>전자 봉투 전송</strong>: 송신자는 생성한 <strong>전자 봉투</strong>를 SSL/TLS를 통해 수신자에게 전송한다. <strong>SSL/TLS</strong>는 이 데이터를 <strong>대칭키 암호화</strong>로 보호하며, 전송 채널의 기밀성과 무결성을 보장한다.</li>
</ol>
<p>이와 같이 <strong>SSL/TLS</strong>는 <strong>비대칭키 암호화</strong>를 사용해 <strong>세션키(대칭키)</strong>를 안전하게 교환하고, 이후의 데이터 전송을 <strong>대칭키 암호화</strong>로 처리하여 <strong>보안성과 효율성</strong>을 동시에 확보한다. 이를 통해 SSL/TLS는 <strong>대칭키와 비대칭키의 결합</strong>을 통해 데이터의 <strong>기밀성</strong>과 <strong>무결성</strong>을 보장한다.</p>
<h3 id="5-수신자의-복호화-및-검증-과정">5. 수신자의 복호화 및 검증 과정</h3>
<ol>
<li><strong>대칭키 복호화</strong>: 수신자는 자신의 <strong>개인키</strong>를 사용해 <strong>암호화된 대칭키</strong>를 복호화한다. 이를 통해 송신자가 사용했던 <strong>대칭키</strong>를 복원한다.</li>
<li><strong>서명된 전자문서 복호화</strong>: 복원한 <strong>대칭키</strong>를 사용해 <strong>암호화된 서명된 전자문서</strong>를 복호화하여 원본 <strong>서명된 전자문서</strong>를 얻는다.</li>
<li><strong>전자 서명 검증</strong>: 서명된 전자문서에서 <strong>전자 서명</strong>을 추출하고, <strong>송신자의 공개키</strong>로 복호화하여 서명 시 생성된 <strong>해시값 B</strong>를 얻는다. 이후 <strong>원본 문서</strong>에 해시 함수를 적용하여 <strong>해시값 A</strong>를 계산하고, <strong>해시값 A</strong>와 <strong>B</strong>를 비교하여 문서가 변조되지 않았음을 확인한다.</li>
</ol>
<p>이렇게 해서 <strong>대칭키와 비대칭키</strong>를 결합하여 데이터의 <strong>기밀성</strong>, <strong>무결성</strong>, <strong>신원 인증</strong>, <strong>부인 방지</strong>를 모두 충족하는 안전한 데이터 전송 방법이 구현된다. <strong>전자 서명</strong>은 데이터의 신뢰성을, <strong>전자 봉투</strong>는 기밀성을, <strong>SSL/TLS</strong>는 안전한 전송을 보장함으로써 인터넷 보안의 주요 요소들이 서로 협력하고 상호작용하며 데이터를 안전하게 보호한다 !</p>
]]></description>
        </item>
    </channel>
</rss>