<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Technical Architect</title>
    <link>https://technical-architect.tistory.com/</link>
    <description>복잡한 아키텍처 설계와 최적화를 고민하며, 테크니컬 아키텍트로서 치열하게 부딪히고 검증한 모든 실무 경험을 공유합니다.</description>
    <language>ko</language>
    <pubDate>Sat, 11 Jul 2026 20:43:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dongka</managingEditor>
    <item>
      <title>Java - 02. types</title>
      <link>https://technical-architect.tistory.com/entry/Java-02-types</link>
      <description>&lt;h1&gt;&lt;code&gt;==&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt;이기도 하고 &lt;code&gt;false&lt;/code&gt;이기도 한 이유 — Java 타입 시스템의 두 세계&lt;/h1&gt;
&lt;p&gt;다음 코드를 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java 25
Integer a = 127;
Integer b = 127;
System.out.println(a == b);   // (1)

Integer c = 200;
Integer d = 200;
System.out.println(c == d);   // (2)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(1)은 &lt;code&gt;true&lt;/code&gt;, (2)는 &lt;code&gt;false&lt;/code&gt;다. 같은 &lt;code&gt;==&lt;/code&gt; 연산자인데, 값이 127일 때와 200일 때 결과가 다르다. 이게 말이 되는가?&lt;/p&gt;
&lt;p&gt;이 미스터리를 풀려면 한 가지를 알아야 한다 — Java에는 &lt;strong&gt;데이터를 저장하는 두 가지 방식&lt;/strong&gt;이 있다는 것. 이 두 방식의 차이가 &lt;code&gt;==&lt;/code&gt;의 동작을 결정하고, &lt;code&gt;null&lt;/code&gt;의 의미를 결정하고, 성능과 메모리 사용량까지 결정한다. 이 글은 이 두 세계를 하나씩 풀어가며, 처음의 미스터리를 해결한다.&lt;/p&gt;
&lt;h2&gt;두 가지 저장 방식 — &amp;quot;값&amp;quot;과 &amp;quot;주소&amp;quot;&lt;/h2&gt;
&lt;p&gt;Java의 모든 변수는 &lt;strong&gt;값을 직접 들고 있거나&lt;/strong&gt;, &lt;strong&gt;값이 있는 곳의 주소를 들고 있다&lt;/strong&gt;. 이게 전부다. 이 두 가지를 Java는 각각 &lt;strong&gt;primitive 타입&lt;/strong&gt;과 &lt;strong&gt;reference 타입&lt;/strong&gt;이라 부른다.&lt;/p&gt;
&lt;p&gt;쉽게 비유하자면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;primitive&lt;/strong&gt;는 &lt;strong&gt;지갑에 현금을 넣는 것&lt;/strong&gt;이다. &lt;code&gt;int x = 5000&lt;/code&gt;이면 지갑에 5000원이 직접 들어있다. 돈을 확인하려면 지갑을 열면 된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reference&lt;/strong&gt;는 &lt;strong&gt;지갑에 체크카드를 넣는 것&lt;/strong&gt;이다. &lt;code&gt;String name = &amp;quot;Alice&amp;quot;&lt;/code&gt;는 지갑에 카드가 있고, 카드는 계좌(객체)를 가리킨다. 잔액을 확인하려면 카드를 읽어서 계좌에 가봐야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 차이가 왜 중요한가? &lt;code&gt;==&lt;/code&gt;로 두 변수를 비교할 때:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;현금(primitive)끼리 비교하면 → &lt;strong&gt;금액이 같은지&lt;/strong&gt; 본다. 5000원과 5000원은 같다.&lt;/li&gt;
&lt;li&gt;카드(reference)끼리 비교하면 → &lt;strong&gt;같은 계좌를 가리키는지&lt;/strong&gt; 본다. 다른 두 카드가 같은 잔액을 가져도, &lt;strong&gt;다른 카드면 &lt;code&gt;false&lt;/code&gt;다&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이것이 &lt;code&gt;==&lt;/code&gt;가 혼란스러운 이유다. primitive에서는 &amp;quot;값이 같은가?&amp;quot;를 묻지만, reference에서는 &amp;quot;같은 객체인가?&amp;quot;를 묻는다.&lt;/p&gt;
&lt;h2&gt;primitive — 현금처럼 값 자체를 저장하는 8가지&lt;/h2&gt;
&lt;p&gt;Java는 8개의 primitive 타입을 정의한다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.1&quot;&gt;(JLS §4.1)&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;타입&lt;/th&gt;
&lt;th&gt;크기&lt;/th&gt;
&lt;th&gt;범위&lt;/th&gt;
&lt;th&gt;언제 쓰나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32-bit&lt;/td&gt;
&lt;td&gt;약 ±21억&lt;/td&gt;
&lt;td&gt;정수의 &lt;strong&gt;기본&lt;/strong&gt;. 웬만하면 이걸 쓴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64-bit&lt;/td&gt;
&lt;td&gt;약 ±920경&lt;/td&gt;
&lt;td&gt;큰 숫자. &lt;code&gt;L&lt;/code&gt; 붙임: &lt;code&gt;10000000000L&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64-bit&lt;/td&gt;
&lt;td&gt;소수&lt;/td&gt;
&lt;td&gt;실수의 &lt;strong&gt;기본&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;조건. 크기 명세 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;char&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16-bit&lt;/td&gt;
&lt;td&gt;0~65535&lt;/td&gt;
&lt;td&gt;글자 하나. &lt;strong&gt;부호 없는&lt;/strong&gt; 정수이기도 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;byte&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8-bit&lt;/td&gt;
&lt;td&gt;-128~127&lt;/td&gt;
&lt;td&gt;이진 데이터 (파일, 네트워크)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;short&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16-bit&lt;/td&gt;
&lt;td&gt;±3만&lt;/td&gt;
&lt;td&gt;거의 안 씀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32-bit&lt;/td&gt;
&lt;td&gt;소수&lt;/td&gt;
&lt;td&gt;거의 안 씀. &lt;code&gt;double&lt;/code&gt; 쓰자&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;primitive 변수는 메모리에 &lt;strong&gt;값 자체&lt;/strong&gt;를 저장한다. &lt;code&gt;int x = 42&lt;/code&gt;라고 하면, 그 메모리 칸에는 42라는 숫자가 직접 들어간다. &amp;quot;42를 가리키는 주소&amp;quot;가 아니라 42 자체다.&lt;/p&gt;
&lt;h3&gt;정수에 끝이 있다 — 오버플로우&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt;의 최대값은 2,147,483,647(약 21억)이다. 이걸 넘으면 어떻게 될까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int max = 2147483647;
System.out.println(max + 1);   // -2147483648&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;예외가 안 난다.&lt;/strong&gt; 조용히 음수가 된다. 자동차 주행거리계가 999,999에서 0으로 돌아가는 것과 같다 — 한 바퀴 돌아서 최소값으로 간다.&lt;/p&gt;
&lt;p&gt;이게 왜 위험한가? 은행에서 잔액이 21억에서 1원이 입금됐는데 -21억이 되는 상황을 상상해 보자. 금액 계산에는 &lt;code&gt;Math.addExact()&lt;/code&gt;를 쓰면 — 초과 시 &lt;code&gt;ArithmeticException&lt;/code&gt;을 던져서 즉시 알려준다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;0.1 + 0.2&lt;/code&gt;는 &lt;code&gt;0.3&lt;/code&gt;이 아니다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;System.out.println(0.1 + 0.2 == 0.3);   // false
System.out.println(0.1 + 0.2);         // 0.30000000000000004&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이건 Java의 버그가 아니다. 컴퓨터가 0.1을 이진수로 표현할 때 정확히 맞아떨어지지 않기 때문이다. 10진수로 1/3을 정확히 표현할 수 없는 것과 같은 이치다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;돈 계산에는 &lt;code&gt;double&lt;/code&gt;을 쓰지 마라.&lt;/strong&gt; &lt;code&gt;BigDecimal&lt;/code&gt;을 쓴다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java 25
import java.math.BigDecimal;

BigDecimal a = new BigDecimal(&amp;quot;0.1&amp;quot;);   // String으로!
BigDecimal b = new BigDecimal(&amp;quot;0.2&amp;quot;);
System.out.println(a.add(b));           // 0.3 (정확)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;new BigDecimal(&amp;quot;0.1&amp;quot;)&lt;/code&gt;에서 문자열을 쓰는 이유: &lt;code&gt;new BigDecimal(0.1)&lt;/code&gt;은 이미 부정확한 &lt;code&gt;double&lt;/code&gt; 값을 받아들이기 때문이다. &lt;code&gt;0.1&lt;/code&gt;이라고 써도 &lt;code&gt;0.1000000000000000055...&lt;/code&gt;로 해석된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;reference — 카드처럼 주소를 저장하는 세계&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt;, &lt;code&gt;ArrayList&lt;/code&gt;, 우리가 만든 클래스 — 이들은 모두 reference 타입이다. 변수가 &lt;strong&gt;객체 자체가 아니라 객체의 주소&lt;/strong&gt;를 들고 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    subgraph stack[&amp;quot;변수 공간 (Stack)&amp;quot;]
        X[&amp;quot;int x = 42&amp;lt;br/&amp;gt;(값이 직접 있음)&amp;quot;]
        S[&amp;quot;String s&amp;lt;br/&amp;gt;(주소만 있음)&amp;quot;]
    end
    subgraph heap[&amp;quot;객체 공간 (Heap)&amp;quot;]
        O[&amp;quot;String 객체&amp;lt;br/&amp;gt;값: &amp;amp;quot;Hello&amp;amp;quot;&amp;quot;]
    end
    S --&amp;gt;|&amp;quot;이쪽에 있음&amp;quot;| O&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;int x = 42&lt;/code&gt;는 x 칸에 42가 있다. &lt;code&gt;String s = &amp;quot;Hello&amp;quot;&lt;/code&gt;는 s 칸에 &amp;quot;Hello&amp;quot;가 있는 게 아니라 — &lt;strong&gt;&amp;quot;Hello 객체는 heap의 이 주소에 있어&amp;quot;라는 안내판&lt;/strong&gt;이 있다. 실제 &amp;quot;Hello&amp;quot;는 heap이라는 별도 영역에 있다.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;==&lt;/code&gt;가 다시 문제된다&lt;/h3&gt;
&lt;p&gt;이제 &lt;code&gt;==&lt;/code&gt;가 왜 위험한지 보인다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String s1 = new String(&amp;quot;hello&amp;quot;);
String s2 = new String(&amp;quot;hello&amp;quot;);
System.out.println(s1 == s2);        // false — 다른 객체(다른 주소)
System.out.println(s1.equals(s2));   // true — 내용은 같음&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;s1&lt;/code&gt;과 &lt;code&gt;s2&lt;/code&gt;는 둘 다 &amp;quot;hello&amp;quot;라는 같은 내용을 가진 객체를 가리킨다. 하지만 &lt;strong&gt;서로 다른 객체&lt;/strong&gt;다. &lt;code&gt;==&lt;/code&gt;는 주소를 비교하므로 &lt;code&gt;false&lt;/code&gt;. &lt;code&gt;.equals()&lt;/code&gt;는 내용을 비교하므로 &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;규칙이 하나 있다: reference 타입을 비교할 때는 항상 &lt;code&gt;.equals()&lt;/code&gt;를 쓴다.&lt;/strong&gt; &lt;code&gt;==&lt;/code&gt;는 primitive에만.&lt;/p&gt;
&lt;h2&gt;미스터리 해결 — Integer 캐싱&lt;/h2&gt;
&lt;p&gt;이제 처음의 퍼즐로 돌아가자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Integer a = 127;   // Integer는 reference 타입 (wrapper)
Integer b = 127;
System.out.println(a == b);   // true (?!)

Integer c = 200;
Integer d = 200;
System.out.println(c == d);   // false&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Integer&lt;/code&gt;는 &lt;code&gt;int&lt;/code&gt;의 wrapper 클래스다. &lt;code&gt;int&lt;/code&gt;는 primitive(값)이고, &lt;code&gt;Integer&lt;/code&gt;는 reference(객체)다. Java 5부터 &lt;code&gt;Integer a = 127&lt;/code&gt;이라고 쓰면 컴파일러가 자동으로 &lt;code&gt;Integer.valueOf(127)&lt;/code&gt;로 변환한다(오토박싱).&lt;/p&gt;
&lt;p&gt;이 &lt;code&gt;valueOf&lt;/code&gt; 메서드가 바로 범인이다. &lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html#valueOf(int)&quot;&gt;(Integer.valueOf API)&lt;/a&gt; 내부 코드를 보면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Integer.valueOf의 내부 동작 (단순화)
private static final Integer[] cache = new Integer[256];

public static Integer valueOf(int i) {
    if (i &amp;gt;= -128 &amp;amp;&amp;amp; i &amp;lt;= 127) {
        return cache[i + 128];   // 미리 만들어둔 객체 재사용
    }
    return new Integer(i);        // 범위 밖 → 새 객체 매번 생성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, &lt;strong&gt;-128에서 127 사이의 숫자는 미리 객체를 만들어두고 재사용&lt;/strong&gt;한다. 127을 요청하면 항상 &lt;strong&gt;같은 객체&lt;/strong&gt;를 돌려준다. 그래서 &lt;code&gt;a == b&lt;/code&gt;가 &lt;code&gt;true&lt;/code&gt; — 둘이 같은 객체를 가리키니까.&lt;/p&gt;
&lt;p&gt;하지만 200은 캐시 범위 밖이다. &lt;code&gt;valueOf(200)&lt;/code&gt;을 부를 때마다 &lt;strong&gt;새로운 객체&lt;/strong&gt;를 만든다. &lt;code&gt;c&lt;/code&gt;와 &lt;code&gt;d&lt;/code&gt;는 다른 객체 → &lt;code&gt;==&lt;/code&gt;가 &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;비유&lt;/strong&gt;: 편의점에서 &amp;quot;물&amp;quot;을 사면, 진열대에 미리 채워둔 물(캐시)을 준다 — 두 사람이 물을 사면 같은 진열대에서 줘서 &amp;quot;같은 물&amp;quot;이다(127). 하지만 &amp;quot;한정판 음료&amp;quot;(200)는 주문할 때마다 새로 만들어 주니까, 두 개는 &amp;quot;다른 병&amp;quot;이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;왜 하필 -128~127인가? 이 범위의 숫자가 프로그래밍에서 가장 빈번하게 쓰이기 때문이다 (루프 인덱스, 작은 카운터 등). 자주 쓰는 객체를 미리 만들어두어 성능을 높이려는 설계다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html#jls-5.1.7&quot;&gt;(JLS §5.1.7)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;실전에서 왜 문제인가&lt;/h3&gt;
&lt;p&gt;이 캐싱은 &lt;strong&gt;테스트에서는 통과하고 프로덕션에서 터진다&lt;/strong&gt;. 테스트할 때는 ID가 1, 2, 3 같은 작은 값이니까 &lt;code&gt;==&lt;/code&gt;가 우연히 &lt;code&gt;true&lt;/code&gt;. 프로덕션에서 ID가 9,999가 되면 갑자기 &lt;code&gt;false&lt;/code&gt;. &amp;quot;테스트에서 됐는데요?&amp;quot;라는 말이 나오는 순간이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결책은 단순하다&lt;/strong&gt;: wrapper 타입(&lt;code&gt;Integer&lt;/code&gt;, &lt;code&gt;Long&lt;/code&gt;, &lt;code&gt;Double&lt;/code&gt;)을 비교할 때는 &lt;strong&gt;항상 &lt;code&gt;.equals()&lt;/code&gt;&lt;/strong&gt;. 규칙은 하나다.&lt;/p&gt;
&lt;h3&gt;실전 시나리오 — ID 비교 버그&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 실제 발생할 수 있는 버그 패턴
class UserService {
    private Map&amp;lt;Long, User&amp;gt; cache = new HashMap&amp;lt;&amp;gt;();

    public boolean isCached(Long userId) {
        // 개발 중: userId가 1, 2, 3 → 캐시 범위 → == 로 우연히 동작
        // 프로덕션: userId가 98765 → 캐시 범위 밖 → == 실패
        return userId == getLastAccessedId();   // 버그!
    }

    public boolean isCachedSafe(Long userId) {
        return userId.equals(getLastAccessedId());   // 정확
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 패턴이 위험한 이유는 — 개발 환경에서 ID가 작은 값일 때는 &amp;quot;잘 되는데?&amp;quot;라고 넘어가기 때문이다. 코드 리뷰에서도 &lt;code&gt;==&lt;/code&gt;와 &lt;code&gt;.equals()&lt;/code&gt;의 차이를 놓치기 쉽다.&lt;/p&gt;
&lt;h3&gt;오토박싱의 숨은 비용&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Long sum = 0L;   // Long (wrapper) — primitive long이 아님!
for (int i = 0; i &amp;lt; 1000000; i++) {
    sum += i;    // 매번: 객체 열기(언박싱) → 더하기 → 새 객체 만들기(박싱)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Long&lt;/code&gt;은 &lt;code&gt;long&lt;/code&gt;이 아니다. &lt;code&gt;Long&lt;/code&gt;은 객체이므로, &lt;code&gt;sum += i&lt;/code&gt;를 할 때마다 (1) 객체에서 &lt;code&gt;long&lt;/code&gt; 값 꺼내기, (2) 더하기, (3) 결과로 &lt;strong&gt;새 &lt;code&gt;Long&lt;/code&gt; 객체 생성&lt;/strong&gt;이 일어난다. 100만 번의 루프에서 100만 개의 쓰레기 객체가 만들어진다 — GC가 청소하느라 CPU를 낭비한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결&lt;/strong&gt;: &lt;code&gt;long sum = 0L;&lt;/code&gt; (primitive). 단 한 글자 차이지만 성능은 수십 배 다르다.&lt;/p&gt;
&lt;h2&gt;형변환 — 큰 그릇에서 작은 그릇으로&lt;/h2&gt;
&lt;p&gt;Java에서 타입을 바꾸는 것을 형변환(casting)이라 한다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html&quot;&gt;(JLS §5)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;작은 것 → 큰 것 (widening)&lt;/strong&gt;: 데이터 손실이 없으니 컴파일러가 알아서 해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;int i = 42;
long l = i;       // OK: int(32비트)를 long(64비트)에 넣기 — 남는 공간이 충분&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;큰 것 → 작은 것 (narrowing)&lt;/strong&gt;: 데이터가 잘릴 수 있으니 &amp;quot;정말 할 거야?&amp;quot;라고 명시해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;double d = 3.99;
int i = (int) d;   // (int) 캐스트: 소수점 버림 → i = 3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;비유하자면: 1L 주전자의 물을 2L 주전자에 붓는 건 문제없다(widening). 2L 주전자의 물을 1L 주전자에 붓는 건 넘친다(narrowing) — &amp;quot;알고 있다&amp;quot;고 &lt;code&gt;(int)&lt;/code&gt;로 명시해야 컴파일러가 허락한다.&lt;/p&gt;
&lt;h3&gt;참조 타입의 캐스트 — 안전하게&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Object obj = &amp;quot;Hello&amp;quot;;
// Integer num = (Integer) obj;   // ClassCastException! &amp;quot;Hello&amp;quot;는 Integer가 아니다

// Java 16+ 안전한 방법
if (obj instanceof String s) {   // 타입 확인 + 변수 바인딩 동시에
    System.out.println(s.length());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://openjdk.org/jeps/394&quot;&gt;(JEP-394)&lt;/a&gt; &lt;code&gt;instanceof String s&lt;/code&gt;는 &amp;quot;obj가 String이면 s에 넣어라&amp;quot; — 확인과 캐스트를 한 번에 한다.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;var&lt;/code&gt; — 타입을 생략하면 컴파일러가 추론한다&lt;/h2&gt;
&lt;p&gt;Java 10부터 &lt;code&gt;var&lt;/code&gt; 키워드로 타입을 생략할 수 있다. &lt;a href=&quot;https://openjdk.org/jeps/286&quot;&gt;(JEP-286)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;var name = &amp;quot;Alice&amp;quot;;           // String으로 추론
var count = 42;               // int로 추론
var users = List.of(&amp;quot;A&amp;quot;,&amp;quot;B&amp;quot;); // List&amp;lt;String&amp;gt;으로 추론&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;오해하지 마라&lt;/strong&gt;: &lt;code&gt;var&lt;/code&gt;은 JavaScript의 &lt;code&gt;var&lt;/code&gt;과 다르다. &lt;strong&gt;동적 타입이 아니다.&lt;/strong&gt; 컴파일 타임에 타입이 고정된다. &lt;code&gt;var name = &amp;quot;Alice&amp;quot;&lt;/code&gt;는 &lt;code&gt;String name = &amp;quot;Alice&amp;quot;&lt;/code&gt;와 &lt;strong&gt;바이트코드가 동일&lt;/strong&gt;하다. &lt;code&gt;var&lt;/code&gt;은 단지 타입을 적기 귀찮을 때 쓰는 단축키일 뿐이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;주의&lt;/strong&gt;: 타입이 명확하지 않으면 &lt;code&gt;var&lt;/code&gt;이 독이 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;var result = process(data);   // result의 타입이 뭔지 코드만 보고 모름
var x = 0;                    // int인데, long이 의도였다면?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;초기화식의 타입이 한눈에 보이면 &lt;code&gt;var&lt;/code&gt;을 쓰고, 메서드 반환값처럼 &amp;quot;뭔지 알아내야 하는&amp;quot; 경우는 명시적 타입을 쓴다.&lt;/p&gt;
&lt;h2&gt;초기화되지 않은 변수 — Java의 안전망&lt;/h2&gt;
&lt;p&gt;Java는 &amp;quot;초기화 안 된 변수&amp;quot;를 사용하지 못하게 막는다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void method() {
    int x;
    // System.out.println(x);  // 컴파일 에러: x를 초기화 안 했음
    x = 42;
    System.out.println(x);      // OK
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C/C++에서는 초기화 안 된 변수에 &amp;quot;쓰레기값&amp;quot;이 들어있어서 버그의 온상이었다. Java는 컴파일 타임에 이것을 막는다 — 안전망이다.&lt;/p&gt;
&lt;p&gt;다만 필드(클래스의 멤버 변수)는 자동으로 기본값(&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;)으로 초기화된다. 지역 변수는 자동 초기화가 안 되므로 명시적으로 초기화해야 한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;분류&lt;/th&gt;
&lt;th&gt;기본값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;숫자 (&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;long&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt; 등)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;char&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;#39;\u0000&amp;#39;&lt;/code&gt; (null 문자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reference (&lt;code&gt;String&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt; 등)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;reference 타입의 기본값이 &lt;code&gt;null&lt;/code&gt;이라는 것이 함정이다. 필드로 &lt;code&gt;private List&amp;lt;Order&amp;gt; orders;&lt;/code&gt;를 선언하고 초기화하지 않으면, &lt;code&gt;orders&lt;/code&gt;는 &lt;code&gt;null&lt;/code&gt;이다. 이 상태에서 &lt;code&gt;orders.add(...)&lt;/code&gt;를 부르면 &lt;code&gt;NullPointerException&lt;/code&gt;. &amp;quot;선언만 하고 안 넣었으면 빈 리스트여야 하는 거 아닌가?&amp;quot;라고 생각할 수 있지만 — Java는 &lt;code&gt;null&lt;/code&gt;과 빈 컬렉션을 구분한다. &lt;code&gt;private List&amp;lt;Order&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;로 명시적 초기화가 필요하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Java에는 두 세계가 있다.&lt;/strong&gt; primitive는 값 자체를, reference는 객체의 주소를 저장한다. &lt;code&gt;==&lt;/code&gt;는 primitive에서는 &amp;quot;값이 같나?&amp;quot;를, reference에서는 &amp;quot;같은 객체인가?&amp;quot;를 묻는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;wrapper 비교에는 항상 &lt;code&gt;.equals()&lt;/code&gt;를 쓴다.&lt;/strong&gt; &lt;code&gt;Integer ==&lt;/code&gt;가 127에서는 &lt;code&gt;true&lt;/code&gt;, 200에서는 &lt;code&gt;false&lt;/code&gt;인 이유는 캐싱 때문이다. 이 버그는 작은 숫자로 테스트하면 잡히지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정수에는 끝이 있다.&lt;/strong&gt; 21억을 넘으면 조용히 음수가 된다. 금액 계산에는 &lt;code&gt;Math.addExact()&lt;/code&gt;로 방어한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;0.1 + 0.2 != 0.3&lt;/code&gt;이다.&lt;/strong&gt; 돈 계산에는 &lt;code&gt;BigDecimal(String)&lt;/code&gt;을 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;var&lt;/code&gt;은 동적 타입이 아니다.&lt;/strong&gt; 컴파일 타임에 타입이 고정되는 단축키다. 타입이 명확할 때만 쓴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Double a = 0.0 / 0.0;&lt;/code&gt;의 값은 무엇인가? &lt;code&gt;a == a&lt;/code&gt;의 결과는? (&lt;code&gt;NaN&lt;/code&gt;은 자기 자신과도 같지 않다.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char c = 65; System.out.println(c);&lt;/code&gt;의 출력은? (힌트: &lt;code&gt;char&lt;/code&gt;는 부호 없는 정수이기도 하다.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Long sum = 0L&lt;/code&gt; 루프와 &lt;code&gt;long sum = 0L&lt;/code&gt; 루프의 실행 시간을 비교해 보자. 얼마나 차이가 나는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;var list = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;에서 &lt;code&gt;list&lt;/code&gt;의 원소 타입은 무엇으로 추론되는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-XX:AutoBoxCacheMax=1000&lt;/code&gt;으로 캐싱 범위를 늘리면 &lt;code&gt;Integer a = 500; Integer b = 500; a == b&lt;/code&gt;의 결과는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html&quot;&gt;JLS §4 - Types, Values, and Variables&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html&quot;&gt;JLS §5 - Conversions and Contexts&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html#valueOf(int)&quot;&gt;Integer.valueOf API (Java 25)&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/286&quot;&gt;JEP-286: Local-Variable Type Inference&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/394&quot;&gt;JEP-394: Pattern Matching for instanceof&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Develop Artifacts/Java</category>
      <category>autoboxing</category>
      <category>Java</category>
      <category>primitive</category>
      <category>reference</category>
      <category>types</category>
      <category>var</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/26</guid>
      <comments>https://technical-architect.tistory.com/entry/Java-02-types#entry26comment</comments>
      <pubDate>Fri, 10 Jul 2026 17:39:07 +0900</pubDate>
    </item>
    <item>
      <title>Java - 01. intro</title>
      <link>https://technical-architect.tistory.com/entry/Java-01-intro</link>
      <description>&lt;h1&gt;&lt;code&gt;java Hello.java&lt;/code&gt;가 숨기고 있는 것 — JVM, 바이트코드, 그리고 &amp;quot;한 번 쓰면 어디서나&amp;quot;의 진짜 의미&lt;/h1&gt;
&lt;p&gt;Java 11부터 &lt;code&gt;java Hello.java&lt;/code&gt; 한 줄로 프로그램을 바로 실행할 수 있게 됐다. &lt;a href=&quot;https://openjdk.org/jeps/330&quot;&gt;(JEP-330)&lt;/a&gt; 파일을 저장하고 &lt;code&gt;java&lt;/code&gt; 명령 하나면 끝난다. 이 명령이 내부적으로 무엇을 하는지 모른 채 써도 프로그램은 잘 돌아간다.&lt;/p&gt;
&lt;p&gt;문제는 여기서 시작된다. &amp;quot;Java는 컴파일 언어인가 인터프리터 언어인가?&amp;quot;라는 질문에 답하지 못하는 것은, 단계를 건너뛰었기 때문이다. &lt;code&gt;javac&lt;/code&gt;가 &lt;code&gt;.java&lt;/code&gt; 파일을 무엇으로 바꾸는지, JVM이 그 결과물을 어떻게 실행하는지, &amp;quot;Write Once Run Anywhere&amp;quot;가 마케팅 구호가 아니라 &lt;strong&gt;정밀한 기술적 설계&lt;/strong&gt;임을 이해하지 못하면 — GC 튜닝, 클래스 로딩 에러, 버전 호환성 문제가 발생했을 때 원인을 추적할 수 없다.&lt;/p&gt;
&lt;p&gt;이 글은 &lt;code&gt;java Hello.java&lt;/code&gt;가 숨기는 두 단계 — 소스 코드에서 바이트코드, 바이트코드에서 기계어 — 를 직접 들여다본다. JDK와 JVM이 정확히 무엇이고, Java 11에서 &amp;quot;JRE&amp;quot;를 다운로드할 수 없는 이유는 무엇이며, &lt;code&gt;.class&lt;/code&gt; 파일에 담긴 바이트코드가 어떻게 Linux, macOS, Windows에서 같은 의미로 실행되는지를 다룬다.&lt;/p&gt;
&lt;h2&gt;JDK, JRE, JVM — 세 개를 구분해야 하는 이유&lt;/h2&gt;
&lt;p&gt;Java를 처음 설치할 때 마주치는 혼란: JDK, JRE, JVM 중 무엇을 설치해야 하는가? Java 11 이전 튜토리얼은 &amp;quot;JRE만 필요하면 JRE를 설치하고, 개발하려면 JDK를 설치하라&amp;quot;고 안내한다. &lt;strong&gt;이 안내는 Java 11부터 틀렸다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    JDK[&amp;quot;JDK (Java Development Kit)&amp;lt;br/&amp;gt;javac, java, jshell, jlink 등&amp;lt;br/&amp;gt;+ JRE 포함 (Java 10까지)&amp;quot;]
    JDK --&amp;gt; JRE[&amp;quot;JRE (Java Runtime Environment)&amp;lt;br/&amp;gt;JVM + core library&amp;lt;br/&amp;gt;별도 다운로드: Java 10까지만&amp;quot;]
    JRE --&amp;gt; JVM[&amp;quot;JVM (Java Virtual Machine)&amp;lt;br/&amp;gt;바이트코드 → 기계어 실행&amp;lt;br/&amp;gt;spec, 구현은 다양&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oracle은 Java 11부터 &lt;strong&gt;JRE를 별도로 제공하지 않는다&lt;/strong&gt;. &lt;a href=&quot;https://www.oracle.com/java/technologies/javase/11-relnote-issues.html&quot;&gt;(Oracle Java 11 릴리스 노트)&lt;/a&gt; JDK가 유일한 배포판이다. &amp;quot;실행만 하려면 JRE&amp;quot;라는 구분이 사라진 것 — 더 이상 &amp;quot;개발 도구 없이 실행 환경만&amp;quot; 제공하는 패키지를 다운로드할 수 없다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;대신 Java 9에서 도입된 &lt;code&gt;jlink&lt;/code&gt;로 &lt;strong&gt;필요한 모듈만 포함한 사용자 정의 런타임 이미지&lt;/strong&gt;를 생성할 수 있다. &lt;a href=&quot;https://openjdk.org/jeps/282&quot;&gt;(JEP-282)&lt;/a&gt; JRE 대신 더 정밀한 방식 — 애플리케이션이 사용하는 모듈만 추려서 경량 런타임을 만든다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;각각이 무엇인가&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성&lt;/th&gt;
&lt;th&gt;포함하는 것&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JVM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;실행 엔진(인터프리터 + JIT 컴파일러), GC, 클래스 로더&lt;/td&gt;
&lt;td&gt;바이트코드를 읽고 기계어로 실행. 사양(spec)은 있고 구현은 다양(HotSpot, OpenJ9, GraalVM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JRE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JVM + core library(&lt;code&gt;java.base&lt;/code&gt;, &lt;code&gt;java.util&lt;/code&gt; 등)&lt;/td&gt;
&lt;td&gt;Java 프로그램 실행 환경. Java 10까지 별도 배포, &lt;strong&gt;Java 11부터 JDK에 통합&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JRE + 개발 도구(&lt;code&gt;javac&lt;/code&gt;, &lt;code&gt;jshell&lt;/code&gt;, &lt;code&gt;jdb&lt;/code&gt;, &lt;code&gt;jlink&lt;/code&gt;, &lt;code&gt;jar&lt;/code&gt; 등)&lt;/td&gt;
&lt;td&gt;개발·컴파일·디버깅. Java 11부터 유일한 배포판&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;JVM은 소프트웨어가 아니라 &lt;strong&gt;명세&lt;/strong&gt;다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se25/html/index.html&quot;&gt;(JVM Specification)&lt;/a&gt; 이 명세를 구현한 것이 HotSpot(OpenJDK/Oracle JDK 기본), Eclipse OpenJ9(IBM 계열), GraalVM(Oracle, AOT 컴파일 지원) 등이다. &amp;quot;JVM&amp;quot;이라는 말이 HotSpot을 지칭하는 경우가 많지만, 엄밀히는 구현이 아니라 명세다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;컴파일 — &lt;code&gt;.java&lt;/code&gt;에서 &lt;code&gt;.class&lt;/code&gt;로&lt;/h2&gt;
&lt;p&gt;Java 소스 코드가 실행되려면 두 단계를 거친다. 첫 번째는 &lt;code&gt;javac&lt;/code&gt;가 &lt;code&gt;.java&lt;/code&gt; 파일을 &lt;code&gt;.class&lt;/code&gt; 파일(바이트코드)로 컴파일하는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    SRC[&amp;quot;Hello.java&amp;lt;br/&amp;gt;(소스 코드)&amp;quot;] --&amp;gt;|javac| CLS[&amp;quot;Hello.class&amp;lt;br/&amp;gt;(바이트코드)&amp;quot;]
    CLS --&amp;gt;|java| JVM[&amp;quot;JVM&amp;lt;br/&amp;gt;(인터프리터 + JIT)&amp;quot;]
    JVM --&amp;gt;|실행| OUT[&amp;quot;콘솔 출력&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전통적인 방식으로 컴파일과 실행을 분리해 보자:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java 25 — Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println(&amp;quot;Hello, Java 25&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1단계: 컴파일 (.java -&amp;gt; .class)
javac Hello.java

# 2단계: 실행 (.class를 JVM에 로드)
java Hello&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Hello, Java 25&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;javac Hello.java&lt;/code&gt;를 실행하면 &lt;code&gt;Hello.class&lt;/code&gt; 파일이 생성된다. 이 파일은 소스 코드가 아니다 — 바이트코드(bytecode)라는 중간 표현이다. &lt;code&gt;.class&lt;/code&gt; 파일을 텍스트 편집기로 열면 읽을 수 없지만, &lt;code&gt;javap&lt;/code&gt;로 역어셈블하면 내용을 볼 수 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Hello.class의 바이트코드 역어셈블
javap -c Hello&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Compiled from &amp;quot;Hello.java&amp;quot;
public class Hello {
  public Hello();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object.&amp;quot;&amp;lt;init&amp;gt;&amp;quot;:()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #13 // String Hello, Java 25
       5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;getstatic&lt;/code&gt;, &lt;code&gt;ldc&lt;/code&gt;, &lt;code&gt;invokevirtual&lt;/code&gt; — 이것들이 JVM이 이해하는 명령어(바이트코드 opcode)다. &lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-6.html#jvms-6.5&quot;&gt;(JVM Spec §6.5 - Opcode 정의)&lt;/a&gt; 각 opcode는 1바이트로 표현되며, JVM이 실행할 수 있는 기본 단위다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;javap&lt;/code&gt;는 JDK에 포함된 도구다. 별도 설치 불필요. &lt;code&gt;javap -c&lt;/code&gt;는 바이트코드, &lt;code&gt;javap -v&lt;/code&gt;는 상수 풀(constant pool)까지 포함한 전체 덤프, &lt;code&gt;javap -p&lt;/code&gt;는 private 멤버까지 표시.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;&lt;code&gt;--release&lt;/code&gt; 플래그 — 컴파일 타임과 런타임 버전 불일치 함정&lt;/h3&gt;
&lt;p&gt;Java 9부터 &lt;code&gt;javac --release N&lt;/code&gt; 플래그가 도입됐다. 구버전용 &lt;code&gt;-source&lt;/code&gt;/&lt;code&gt;-target&lt;/code&gt; 조합을 대체하며, &lt;strong&gt;더 안전한 크로스 컴파일&lt;/strong&gt;을 보장한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Java 25 컴파일러로 Java 17 호환 바이트코드 생성
javac --release 17 Hello.java

# 구 방식 (Java 8 시절) — 위험: 컴파일은 되지만 런타임에 NoSuchMethodError 가능
javac -source 17 -target 17 Hello.java&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-source&lt;/code&gt;/&lt;code&gt;-target&lt;/code&gt;은 바이트코드 버전만 지정할 뿐, 컴파일러가 참조하는 API는 &lt;strong&gt;현재 JDK(Java 25)의 API&lt;/strong&gt;다. Java 25에서 추가된 메서드를 쓰고 &lt;code&gt;-target 17&lt;/code&gt;로 컴파일하면, 바이트코드는 Java 17 호환이지만 Java 17 런타임에서 &lt;code&gt;NoSuchMethodError&lt;/code&gt;가 발생한다. &lt;code&gt;--release 17&lt;/code&gt;은 컴파일러가 &lt;strong&gt;Java 17의 API만 참조&lt;/strong&gt;하므로 이 문제를 원천 차단한다.&lt;/p&gt;
&lt;h3&gt;바이트코드가 존재하는 이유 — 플랫폼 독립성&lt;/h3&gt;
&lt;p&gt;C/C++는 소스 코드를 각 OS의 기계어로 직접 컴파일한다. Linux에서 컴파일한 바이너리는 Windows에서 실행되지 않는다. Java는 다른 길을 택했다 — &lt;strong&gt;모든 플랫폼에서 동일한 바이트코드를 생성하고, 각 플랫폼의 JVM이 바이트코드를 해당 기계어로 번역한다&lt;/strong&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;C/C++&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;컴파일 대상&lt;/td&gt;
&lt;td&gt;OS별 기계어(x86-64, ARM 등)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;플랫폼 독립 바이트코드&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행&lt;/td&gt;
&lt;td&gt;OS가 기계어 직접 실행&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;JVM이 바이트코드를 기계어로 번역(인터프리터 + JIT) 후 실행&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과물 이식성&lt;/td&gt;
&lt;td&gt;컴파일 환경에 종속&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.class&lt;/code&gt; 파일은 어느 OS의 JVM에서든 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&amp;quot;Write Once Run Anywhere(WORA)&amp;quot;는 마케팅 구호가 아니다 — 바이트코드라는 중간 단계를 두어 &lt;strong&gt;컴파일 결과물(&lt;code&gt;.class&lt;/code&gt;)이 OS에 종속되지 않는다&lt;/strong&gt;는 정확한 기술적 설계를 가리키는 말이다. 물론 JVM 자체는 OS별로 다르다(Linux용 JVM, Windows용 JVM). 하지만 같은 &lt;code&gt;Hello.class&lt;/code&gt;를 어느 JVM에서든 실행할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 설계의 대가: 직접 기계어로 컴파일하는 C/C++에 비해 실행 시 추가 번역 단계(JVM)가 필요하다. 이 대가를 줄이기 위해 JIT 컴파일러가 존재한다(아래).&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;JVM이 바이트코드를 실행하는 방식 — 인터프리터와 JIT의 이중 구조&lt;/h2&gt;
&lt;p&gt;JVM이 &lt;code&gt;.class&lt;/code&gt; 파일의 바이트코드를 읽어 실행하는 과정은 단순한 인터프리터가 아니다. 두 가지 실행 방식을 &lt;strong&gt;상황에 따라 혼합&lt;/strong&gt;한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    LOAD[&amp;quot;1. 클래스 로딩&amp;lt;br/&amp;gt;ClassLoader가 .class 로드&amp;quot;] --&amp;gt; VERIFY[&amp;quot;2. 바이트코드 검증&amp;lt;br/&amp;gt;Verifier가 타입 안전성 확인&amp;quot;]
    VERIFY --&amp;gt; INTERP[&amp;quot;3. 인터프리터 실행&amp;lt;br/&amp;gt;(초기: 모든 코드를 한 줄씩 해석)&amp;quot;]
    INTERP --&amp;gt;|&amp;quot;자주 실행되는 코드 감지&amp;lt;br/&amp;gt;(hot spot)&amp;quot;| JIT[&amp;quot;4. JIT 컴파일&amp;lt;br/&amp;gt;HotSpot → 네이티브 기계어&amp;quot;]
    JIT --&amp;gt; NATIVE[&amp;quot;5. 네이티브 코드 실행&amp;lt;br/&amp;gt;(이후 해당 코드는 기계어로 직행)&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;인터프리터&lt;/strong&gt;는 바이트코드를 한 줄씩 읽어 기계어로 번역하며 실행한다. 장점은 즉시 실행(컴파일 대기 없음), 단점은 같은 코드를 반복 번역한다는 것 — 루프가 10,000번 돌면 같은 바이트코드를 10,000번 해석한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JIT(Just-In-Time) 컴파일러&lt;/strong&gt;는 이 비효율을 해결한다. JVM이 실행 중에 &lt;strong&gt;&amp;quot;hot spot&amp;quot; — 자주 실행되는 코드 — 를 감지&lt;/strong&gt;하면, 그 코드를 네이티브 기계어로 한 번에 컴파일한다. 이후 같은 코드를 만나면 인터프리터를 건너뛰고 컴파일된 기계어를 직접 실행한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;HotSpot JVM(OpenJDK/Oracle JDK의 기본 구현)이라는 이름 자체가 이 메커니즘에서 왔다 — &amp;quot;hot spot&amp;quot;을 찾아 JIT 컴파일하는 JVM. &lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/specs/man/java.html&quot;&gt;(HotSpot VM 문서)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;C1과 C2 — 두 단계 JIT 컴파일러&lt;/h3&gt;
&lt;p&gt;HotSpot JVM은 두 개의 JIT 컴파일러를 가진다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;JIT 컴파일러&lt;/th&gt;
&lt;th&gt;목표&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C1 (Client)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;빠른 컴파일&lt;/td&gt;
&lt;td&gt;적은 최적화, 빠른 시작. 짧은 실행 애플리케이션에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C2 (Server)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;극한 최적화&lt;/td&gt;
&lt;td&gt;aggressive 인라이닝, 루프 언롤링, escape analysis. 긴 실행 서버 애플리케이션에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Java 8부터 &lt;strong&gt;Tiered Compilation&lt;/strong&gt;이 기본으로 활성화돼 — 처음엔 C1으로 빠르게 컴파일하고, 코드가 충분히 hot 해지면 C2로 다시 컴파일한다. 두 컴파일러의 장점을 모두 취하는 방식이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;java -version&lt;/code&gt; 출력에서 &amp;quot;mixed mode&amp;quot;는 인터프리터 + JIT 혼합을 의미한다. &lt;code&gt;java -Xint -version&lt;/code&gt;은 인터프리터 전용(JIT 끔), &lt;code&gt;java -Xcomp -version&lt;/code&gt;은 JIT 우선 모드. 성능 차이를 직접 체감할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;GC — JVM이 메모리를 회수하는 방식&lt;/h3&gt;
&lt;p&gt;Java의 핵심 설계 결정 중 하나: &lt;strong&gt;개발자가 메모리를 직접 해제하지 않는다.&lt;/strong&gt; C/C++의 &lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt;, &lt;code&gt;new&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;에 대응하는 명시적 해제가 Java에는 없다. 대신 &lt;strong&gt;Garbage Collector(GC)&lt;/strong&gt; 가 더 이상 참조되지 않는 객체를 자동으로 회수한다.&lt;/p&gt;
&lt;p&gt;이것이 가능한 이유는 JVM이 모든 객체 참조를 추적하고 있기 때문이다 — GC root(스택 변수, static 필드 등)에서 도달할 수 없는 객체는 쓰레기로 판단한다. Java 25에서 기본 GC는 &lt;strong&gt;G1 GC&lt;/strong&gt;이며, &lt;strong&gt;ZGC&lt;/strong&gt;(JEP-439, 세대별 ZGC)와 &lt;strong&gt;Shenandoah&lt;/strong&gt;도 선택 가능하다. GC에 대한 자세한 내용은 &lt;code&gt;24-jvm-gc.md&lt;/code&gt;에서 다룬다.&lt;/p&gt;
&lt;h2&gt;OpenJDK vs Oracle JDK — 어떤 것을 쓸 것인가&lt;/h2&gt;
&lt;p&gt;&amp;quot;JDK를 설치하라&amp;quot;고 했는데, 어떤 JDK인가? Java 생태계에는 여러 JDK 배포판(distribution)이 있다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;배포판&lt;/th&gt;
&lt;th&gt;제공&lt;/th&gt;
&lt;th&gt;라이선스&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenJDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;커뮤니티(Oracle 주도)&lt;/td&gt;
&lt;td&gt;GPL v2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Java의 참조 구현&lt;/strong&gt;. 소스 코드의 단일 출처&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle JDK&lt;/td&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;Oracle No-Fee Terms (교육/개발 무료, 프로덕션은 유료 조건)&lt;/td&gt;
&lt;td&gt;OpenJDK 기반. 상업 지원 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eclipse Temurin(Adoptium)&lt;/td&gt;
&lt;td&gt;Eclipse 재단&lt;/td&gt;
&lt;td&gt;GPL v2&lt;/td&gt;
&lt;td&gt;OpenJDK 빌드. &lt;strong&gt;커뮤니티 표준 배포판&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon Corretto&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;GPL v2&lt;/td&gt;
&lt;td&gt;OpenJDK 빌드. AWS 환경 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azul Zulu&lt;/td&gt;
&lt;td&gt;Azul&lt;/td&gt;
&lt;td&gt;GPL v2 / 상업&lt;/td&gt;
&lt;td&gt;OpenJDK 빌드. Zing(상업 GC) 옵션&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Java 11부터 Oracle JDK와 OpenJDK는 &lt;strong&gt;본질적으로 같은 코드베이스&lt;/strong&gt;다. &lt;a href=&quot;https://blogs.oracle.com/java/post/oracle-jdk-and-openjdk&quot;&gt;(Oracle 발표)&lt;/a&gt; Oracle JDK는 상업 지원과 일부 WebStart 등 부가 기능을 더할 뿐, 코어는 동일하다. 프로덕션에서는 OpenJDK 기반 배포판(Temurin, Corretto 등)을 쓰는 것이 일반적이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Java 25 LTS — 왜 LTS인가, 무엇이 달라졌나&lt;/h2&gt;
&lt;p&gt;Java는 6개월마다 새 버전을 릴리스한다. 그중 &lt;strong&gt;LTS(Long-Term Support) 버전&lt;/strong&gt;은 장기 지원(최소 8년 보안 패치)이 보장된다. 비-LTS 버전은 다음 버전 나올 때까지만 지원된다(6개월).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;버전&lt;/th&gt;
&lt;th&gt;연도&lt;/th&gt;
&lt;th&gt;LTS&lt;/th&gt;
&lt;th&gt;핵심 변화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Java 8&lt;/td&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;LTS(종료 연장됨)&lt;/td&gt;
&lt;td&gt;Lambda, Stream, Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 11&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;LTS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;var&lt;/code&gt;, HttpClient, JEP-330(단일 파일 실행)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 17&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;LTS&lt;/td&gt;
&lt;td&gt;Sealed classes, Pattern Matching(switch), record 정식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 21&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;LTS&lt;/td&gt;
&lt;td&gt;Virtual Threads(JEP-444), Sequenced Collections, Pattern Matching for switch 정식&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Java 25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2025-09&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LTS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Compact Object Headers(JEP-519), Scoped Values(JEP-506 finalized), AOT Method Profiling(JEP-515), Module Import Declarations(JEP-511), Flexible Constructor Bodies(JEP-513), Generational Shenandoah(JEP-521)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 학습서의 기준은 &lt;strong&gt;Java 25 LTS&lt;/strong&gt;다. Spring Framework 7.0 / Spring Boot 4.0의 최소 요구 버전이 Java 17이며, 권장 LTS는 Java 25다. Java 21은 &amp;quot;이전 LTS&amp;quot;로, Java 8/11은 &amp;quot;역사적 환경&amp;quot;으로 다룬다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;실습 — Java 25에서 프로그램 실행하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전: Java 25 설치 확인(&lt;code&gt;java -version&lt;/code&gt;). 설치되지 않았다면 &lt;a href=&quot;https://adoptium.net/&quot;&gt;Adoptium(Temurin)&lt;/a&gt;에서 다운로드.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;방식 1 — 단일 파일 직접 실행 (Java 11+, JEP-330)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// Java 25 — Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println(&amp;quot;Hello, Java 25&amp;quot;);
        System.out.println(&amp;quot;JVM: &amp;quot; + System.getProperty(&amp;quot;java.vm.name&amp;quot;));
        System.out.println(&amp;quot;Java version: &amp;quot; + System.getProperty(&amp;quot;java.version&amp;quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 컴파일 없이 직접 실행 (JEP-330, Java 11+)
java Hello.java&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Hello, Java 25
JVM: OpenJDK 64-Bit Server VM
Java version: 25&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: &lt;code&gt;java Hello.java&lt;/code&gt;는 내부적으로 (1) 소스를 임시 바이트코드로 컴파일하고 (2) 메모리에 로드하여 실행한다. &lt;code&gt;.class&lt;/code&gt; 파일을 디스크에 생성하지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;[Java 11+] JEP-330으로 도입된 single-file source-code 실행은 &lt;strong&gt;학습·프로토타이핑 용도&lt;/strong&gt;다. 다중 파일 프로젝트는 여전히 &lt;code&gt;javac&lt;/code&gt;로 컴파일하거나 빌드 도구(Maven, Gradle)를 써야 한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;방식 2 — 전통적 컴파일 + 실행&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1단계: 컴파일 (.class 파일 생성)
javac Hello.java

# .class 파일 확인
ls Hello.class&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Hello.class&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: &lt;code&gt;javac&lt;/code&gt;가 &lt;code&gt;.class&lt;/code&gt; 파일을 생성한다. 이 파일은 OS에 독립적인 바이트코드다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 2단계: 바이트코드 역어셈블로 내부 확인
javap -c Hello&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Compiled from &amp;quot;Hello.java&amp;quot;
public class Hello {
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7  // Field java/lang/System.out
       3: ldc           #13 // String Hello, Java 25
       5: invokevirtual #15 // Method println
       8: return
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: &lt;code&gt;javap -c&lt;/code&gt;가 보여주는 것이 바이트코드다. &lt;code&gt;getstatic&lt;/code&gt;, &lt;code&gt;ldc&lt;/code&gt;, &lt;code&gt;invokevirtual&lt;/code&gt;은 JVM이 실행하는 명령어다.&lt;/p&gt;
&lt;h3&gt;방식 3 — JShell (Java 9+ REPL)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Java 25 — 대화형 실험
jshell&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;|  Welcome to JShell -- Version 25
|  For an introduction type: /help intro

jshell&amp;gt; var name = &amp;quot;Java 25&amp;quot;;
name ==&amp;gt; &amp;quot;Java 25&amp;quot;

jshell&amp;gt; System.out.println(&amp;quot;Hello, &amp;quot; + name);
Hello, Java 25

jshell&amp;gt; /exit
|  Goodbye&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: JShell로 컴파일 없이 Java 코드를 즉시 실험할 수 있다. API 동작 확인, 문법 테스트에 유용.&lt;/p&gt;
&lt;h2&gt;왜 이 구조인가 — Java 설계의 트레이드오프&lt;/h2&gt;
&lt;p&gt;지금까지의 모든 것이 세 가지 설계 결정으로 요약된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;왜 바이트코드 중간 단계를 두었나?&lt;/strong&gt; 직접 기계어로 컴파일하면 이식성을 포기해야 한다. 바이트코드 + JVM 구조는 &amp;quot;컴파일 한 번, 어디서든 실행&amp;quot;을 가능하게 한다. 대가: 실행 시 번역 오버헤드(JIT로 완화).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;왜 인터프리터와 JIT를 혼합했나?&lt;/strong&gt; 인터프리터만 쓰면 느리고, JIT만 쓰면 시작 지연이 길다(모든 코드를 미리 컴파일). 실행 초반은 인터프리터로 빠르게 시작하고, hot spot이 감지되면 JIT로 최적화 — 두 방식의 장점을 취한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;왜 GC를 언어에 내장했나?&lt;/strong&gt; 개발자가 메모리 해제를 직접 관리(C/C++ 방식)하면 use-after-free, double-free, memory leak이 빈번한다. GC는 이 클래스의 버그를 원천 제거한다. 대가: GC 실행 시 일시 정지(stop-the-world)와 메모리 오버헤드.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 세 가지는 독립이 아니다. 바이트코드이기 때문에 JIT가 같은 코드를 반복 최적화할 수 있고, GC가 있기 때문에 객체 참조 추적이 JVM 수준에서 가능하며, 이식성이 확보되기 때문에 같은 GC 알고리즘이 다양한 OS에서 동일하게 동작한다. 하나를 빼면 나머지의 근거가 흔들린다.&lt;/p&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JRE는 Java 11부터 별도 배포되지 않는다.&lt;/strong&gt; JDK가 유일한 배포판이며, 경량 런타임이 필요하면 &lt;code&gt;jlink&lt;/code&gt;로 맞춤형 이미지를 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;javac&lt;/code&gt;가 만드는 &lt;code&gt;.class&lt;/code&gt; 파일은 기계어가 아니라 바이트코드다.&lt;/strong&gt; 이 바이트코드는 OS에 독립적이며, 각 OS의 JVM이 실행 시 기계어로 번역한다. WORA는 이 중간 단계에서 나온다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JVM은 인터프리터와 JIT 컴파일러를 상황에 따라 혼합한다.&lt;/strong&gt; 실행 초반은 인터프리터로 빠르게 시작하고, hot spot은 JIT(C1 → C2)로 최적화한다. &amp;quot;Java가 느리다&amp;quot;는 평가는 JIT가 웜업되기 전의 상태를 가리키는 경우가 많다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenJDK와 Oracle JDK는 본질적으로 같은 코드베이스다.&lt;/strong&gt; 프로덕션에서는 OpenJDK 기반 배포판(Temurin, Corretto)이 일반적이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java 25 LTS는 현재 엔터프라이즈 표준이다.&lt;/strong&gt; Compact Object Headers, Scoped Values(finalized), AOT Method Profiling, Flexible Constructor Bodies가 정식으로 들어왔다. 이 학습서 전체의 기준 버전이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;java -Xint Hello&lt;/code&gt;(인터프리터 전용)와 &lt;code&gt;java Hello&lt;/code&gt;(혼합 모드)의 실행 시간을 비교해 보자. 어느 정도 차이가 나는가? JIT의 효과를 체감할 수 있는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;javap -v Hello.class&lt;/code&gt;를 실행해 상수 풀(constant pool)을 확인해 보자. &lt;code&gt;#7&lt;/code&gt;, &lt;code&gt;#13&lt;/code&gt;, &lt;code&gt;#15&lt;/code&gt;가 각각 무엇을 가리키는가?&lt;/li&gt;
&lt;li&gt;C/C++ 프로그램과 Java 프로그램을 각각 Linux에서 컴파일한 뒤, 그 결과물을 macOS에서 실행하려고 하면 어떤 일이 벌어지는가? 둘의 차이는 어디서 오는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output myjre&lt;/code&gt;로 생성한 런타임 이미지의 크기를 확인해 보자. 전체 JDK와 얼마나 차이가 나는가?&lt;/li&gt;
&lt;li&gt;Java 25에서 &lt;code&gt;java Hello.java&lt;/code&gt;를 실행할 때, &lt;code&gt;.class&lt;/code&gt; 파일이 생성되지 않는다면, JVM은 바이트코드를 어디에 보관하는가? (힌트: JEP-330의 구현 방식)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SecurityManager&lt;/code&gt;가 Java 17에서 deprecated 되고 Java 25에서 여전히 존재한다. 이것이 의미하는 바는 무엇이며, Java의 보안 모델이 어떻게 변화하고 있는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jvms/se25/html/index.html&quot;&gt;JVM Specification (Java 25)&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se25/html/index.html&quot;&gt;Java Language Specification (Java 25)&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/en/java/javase/25/docs/api/index.html&quot;&gt;Java 25 API Documentation&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/330&quot;&gt;JEP-330: Launch Single-File Source-Code Programs&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/282&quot;&gt;JEP-282: jlink: The Java Linker&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/444&quot;&gt;JEP-444: Virtual Threads&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/jeps/439&quot;&gt;JEP-439: Generational ZGC&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.org/&quot;&gt;OpenJDK&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blogs.oracle.com/java/post/oracle-jdk-and-openjdk&quot;&gt;Oracle JDK and OpenJDK announcement&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Develop Artifacts/Java</category>
      <category>bytecode</category>
      <category>Compilation</category>
      <category>Java</category>
      <category>JDK</category>
      <category>JIT</category>
      <category>jvm</category>
      <category>openjdk</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/25</guid>
      <comments>https://technical-architect.tistory.com/entry/Java-01-intro#entry25comment</comments>
      <pubDate>Fri, 10 Jul 2026 17:31:20 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 13. mirror maker 2</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-13-mirror-maker-2</link>
      <description>&lt;h1&gt;MirrorMaker 2 — 클러스터 경계를 넘는 복제, 그리고 offset 동기화의 함정&lt;/h1&gt;
&lt;p&gt;서울 리전의 Kafka 클러스터가 장애 났다. DR 사이트(도쿄)에 복제해둔 토픽 데이터는 살아 있다. 하지만 장애 복구팀이 가장 먼저 듣는 질문은 이것이다: &lt;strong&gt;&amp;quot;consumer offset은요?&amp;quot;&lt;/strong&gt; 데이터는 복제됐지만, consumer 그룹이 어디까지 읽었는지는 복제되지 않았다. 수천 개의 partition에 대해 consumer가 마지막으로 읽은 위치를 모른다 — 백로그를 처음부터 다시 처리하거나, 하루 전 체크포인트로 수동 복구해야 한다.&lt;/p&gt;
&lt;p&gt;이것이 MirrorMaker 1 시절의 현실이었다. &lt;strong&gt;MirrorMaker 2(MM2)&lt;/strong&gt;는 이 문제를 근본적으로 다시 설계했다 — 데이터만 복제하는 것이 아니라, consumer offset, 토픽 설정, ACL까지 함께 옮기는 Connect 기반 복제 프레임워크로. 이 글은 MM2가 무엇인지부터 시작해, 구성 방법, 멀티 리전 offset 동기화의 작동 원리와 함정, 그리고 프로덕션 운영에서 반드시 알아야 할 것들을 다룬다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 글은 Kafka의 클러스터 내 복제(intra-cluster replication, ISR/HW)가 아닌 &lt;strong&gt;클러스터 간 복제(inter-cluster replication)&lt;/strong&gt;를 다룬다. 클러스터 내 복제는 &lt;code&gt;06-replication.md&lt;/code&gt; 참조.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;MM2란 — Connect 위에 구축된 크로스 클러스터 복제 프레임워크&lt;/h2&gt;
&lt;p&gt;MM2는 &lt;strong&gt;서로 다른 Kafka 클러스터 간에 토픽 데이터, 토픽 설정, consumer 그룹 offset, ACL을 복제하는 도구&lt;/strong&gt;다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt; 핵심은 &lt;strong&gt;Kafka Connect 프레임워크 위에 구축&lt;/strong&gt;됐다는 점 — Connect의 task 분산, 장애 복구, 내부 topic 기반 상태 저장을 그대로 상속받는다. (&lt;code&gt;10-connect.md&lt;/code&gt; 참조)&lt;/p&gt;
&lt;p&gt;MM2는 세 가지 커넥터로 구성된다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    subgraph MM2[&amp;quot;MirrorMaker 2 (Connect 기반)&amp;quot;]
        SC[&amp;quot;MirrorSourceConnector&amp;lt;br/&amp;gt;토픽 데이터 + 설정 + ACL 복제&amp;quot;]
        CC[&amp;quot;MirrorCheckpointConnector&amp;lt;br/&amp;gt;consumer offset 체크포인트 + 변환&amp;quot;]
        HC[&amp;quot;MirrorHeartbeatConnector&amp;lt;br/&amp;gt;연결 상태 모니터링&amp;quot;]
    end
    SRC[&amp;quot;source cluster&amp;lt;br/&amp;gt;(예: us-west)&amp;quot;] --&amp;gt; SC
    SRC --&amp;gt; CC
    SRC --&amp;gt; HC
    SC --&amp;gt; TGT[&amp;quot;target cluster&amp;lt;br/&amp;gt;(예: us-east)&amp;quot;]
    CC --&amp;gt; TGT
    HC --&amp;gt; TGT&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;커넥터&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;기본 활성화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MirrorSourceConnector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;source topic의 record를 target으로 복제. 토픽 설정·ACL도 동기화&lt;/td&gt;
&lt;td&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MirrorCheckpointConnector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;source consumer 그룹의 offset을 target 기준으로 변환해 checkpoint topic에 기록. &lt;code&gt;sync.group.offsets.enabled=true&lt;/code&gt; 시 target의 &lt;code&gt;__consumer_offsets&lt;/code&gt;에 직접 기록&lt;/td&gt;
&lt;td&gt;예 (단, offset 동기화는 기본 &lt;strong&gt;비활성화&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MirrorHeartbeatConnector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;target cluster에 heartbeat을 주기적으로 발행하여 연결 상태를 추적 가능하게 함&lt;/td&gt;
&lt;td&gt;예&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;MirrorMaker 1은 단순한 &amp;quot;source consumer + target producer&amp;quot; 조합이었다. offset 동기화, ACL 복제, 자동 토픽 감지, 루프 방지 기능이 없었다. MM2는 KIP-382(Kafka 2.4)에서 도입됐다. &lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/KIP-382%3A+MirrorMaker+2.0&quot;&gt;(KIP-382)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;복제 흐름(replication flow) — 방향성이 있는 데이터 이동&lt;/h3&gt;
&lt;p&gt;MM2의 핵심 단위는 &lt;strong&gt;replication flow&lt;/strong&gt;다. &lt;code&gt;{source}-&amp;gt;{target}&lt;/code&gt; 형식으로 정의하며, &lt;strong&gt;방향성이 있다&lt;/strong&gt; — 양방향 복제가 필요하면 두 flow를 각각 활성화해야 한다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    subgraph patterns[&amp;quot;복제 토폴로지 패턴&amp;quot;]
        A1[&amp;quot;Active/Passive&amp;lt;br/&amp;gt;A-&amp;gt;B&amp;quot;] 
        A2[&amp;quot;Active/Active&amp;lt;br/&amp;gt;A-&amp;gt;B, B-&amp;gt;A&amp;quot;]
        A3[&amp;quot;Aggregation&amp;lt;br/&amp;gt;A-&amp;gt;K, B-&amp;gt;K, C-&amp;gt;K&amp;quot;]
        A4[&amp;quot;Fan-out&amp;lt;br/&amp;gt;K-&amp;gt;A, K-&amp;gt;B, K-&amp;gt;C&amp;quot;]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;가장 많이 쓰이는 두 패턴:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Active/Passive (A-&amp;gt;B)&lt;/strong&gt;: primary에서 secondary로 단방향 복제. DR(disaster recovery) 용도. 장애 시 secondary로 전환(failover).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active/Active (A-&amp;gt;B, B-&amp;gt;A)&lt;/strong&gt;: 양방향 복제. 두 리전 모두 생산·소비. 지연 허용 오차가 큰 워크로드.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Active/Active에서 복제 &amp;quot;루프&amp;quot;(A→B로 복제된 데이터가 다시 B→A로 복제되는 현상)는 &lt;strong&gt;같은 MM2 설정 파일에 두 flow를 정의하면 자동으로 방지&lt;/strong&gt;된다. MM2가 복제된 토픽(이름에 source alias가 붙은)을 식별해 되복제하지 않기 때문. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;복제된 토픽의 이름 규칙 — DefaultReplicationPolicy&lt;/h2&gt;
&lt;p&gt;MM2는 source topic을 target으로 복제할 때 &lt;strong&gt;이름 앞에 source cluster alias를 붙인다&lt;/strong&gt;. 기본 &lt;code&gt;DefaultReplicationPolicy&lt;/code&gt;에 따른 규칙: &lt;code&gt;{source_alias}.{topic_name}&lt;/code&gt; &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror-client/src/main/java/org/apache/kafka/connect/mirror/DefaultReplicationPolicy.java&quot;&gt;(DefaultReplicationPolicy.java)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;us-west (source)       us-east (target)
================       =================
foo-topic         --&amp;gt;  us-west.foo-topic
bar-topic         --&amp;gt;  us-west.bar-topic&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 이름 변형이 왜 필요한가 — &lt;strong&gt;같은 토픽 이름을 가진 데이터가 출처가 섞이는 것을 막기 위해&lt;/strong&gt;. Active/Active에서 &lt;code&gt;foo&lt;/code&gt; 토픽이 양쪽에 있다면, us-west의 &lt;code&gt;foo&lt;/code&gt;는 us-east에 &lt;code&gt;us-west.foo&lt;/code&gt;로, us-east의 &lt;code&gt;foo&lt;/code&gt;는 us-west에 &lt;code&gt;us-east.foo&lt;/code&gt;로 복제된다. 루프 없이 출처가 추적 가능하다.&lt;/p&gt;
&lt;p&gt;구분자(separator)는 기본 &lt;code&gt;.&lt;/code&gt;이지만 변경 가능:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# 커스텀 구분자
replication.policy.separator = _
# 결과: us-west_foo-topic&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;완전히 다른 이름 규칙이 필요하면 &lt;code&gt;replication.policy.class&lt;/code&gt;로 커스텀 &lt;code&gt;ReplicationPolicy&lt;/code&gt; 구현체를 지정할 수 있다. (IdentityReplicationPolicy를 쓰면 이름 변형을 하지 않지만, Active/Active에서는 루프 위험이 있으므로 주의.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이름 규칙이 consumer에게 영향을 준다 — target에서 소비하려면 원래 &lt;code&gt;foo-topic&lt;/code&gt;이 아니라 &lt;code&gt;us-west.foo-topic&lt;/code&gt;을 구독해야 한다. 이는 장애 전환(failover) 시 consumer 설정 변경이 필요하다는 의미이기도 하다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;MM2 구성 — 설정 파일 한 개로 모든 것을&lt;/h2&gt;
&lt;p&gt;MM2는 &lt;strong&gt;단일 설정 파일&lt;/strong&gt;(&lt;code&gt;connect-mirror-maker.properties&lt;/code&gt;)로 모든 복제를 정의한다. 이 파일에 cluster 정의, 복제 flow, Connect/producer/consumer 설정이 모두 들어간다.&lt;/p&gt;
&lt;h3&gt;전체 구성 예시 — Active/Passive&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# Kafka 4.3, KRaft
# cluster alias 정의
clusters = primary, secondary

# 각 cluster 연결 정보
primary.bootstrap.servers = broker1-primary:9092,broker2-primary:9092
secondary.bootstrap.servers = broker1-secondary:9092,broker2-secondary:9092

# 복제 flow 활성화 (primary -&amp;gt; secondary 단방향)
primary-&amp;gt;secondary.enabled = true
secondary-&amp;gt;primary.enabled = false

# 복제할 토픽 (정규식)
primary-&amp;gt;secondary.topics = .*

# 내부 topic 복제 팩터 (프로덕션: 3 이상 권장)
replication.factor = 3
checkpoints.topic.replication.factor = 3
heartbeats.topic.replication.factor = 3
offset-syncs.topic.replication.factor = 3

# Connect 내부 topic 복제 팩터
offset.storage.replication.factor = 3
status.storage.replication.factor = 3
config.storage.replication.factor = 3

# consumer 그룹 offset 동기화 활성화 (DR 핵심 설정)
sync.group.offsets.enabled = true
sync.group.offsets.interval.seconds = 60
emit.checkpoints.enabled = true
emit.checkpoints.interval.seconds = 60

# 복제에서 제외할 토픽 (내부 topic 제외)
topics.exclude = .*[\-\.]internal, .*\.replica, __.*&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/config/connect-mirror-maker.properties&quot;&gt;(connect-mirror-maker.properties 예시)&lt;/a&gt; &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorConnectorConfig.java&quot;&gt;(MirrorConnectorConfig.java)&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;설정 문법의 계층 구조&lt;/h3&gt;
&lt;p&gt;MM2 설정은 &lt;strong&gt;전역 기본값 → flow별 오버라이드&lt;/strong&gt; 계층을 가진다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# 전역 기본값 (모든 flow에 적용)
topics = .*
groups = .*

# flow별 오버라이드
primary-&amp;gt;secondary.topics = foo.*, bar.*
primary-&amp;gt;secondary.groups = important-.*&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또한 Connect, producer, consumer 설정을 cluster별로 커스터마이징할 수 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# source cluster consumer 설정
primary.consumer.isolation.level = read_committed

# target cluster producer 설정
secondary.producer.compression.type = gzip
secondary.producer.buffer.memory = 32768&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;실행&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka 4.3, KRaft 단일 노드 환경
# 전체 flow 실행
bin/connect-mirror-maker.sh config/connect-mirror-maker.properties

# 특정 target만 (consume from remote, produce to local)
bin/connect-mirror-maker.sh config/connect-mirror-maker.properties --clusters secondary&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;--clusters&lt;/code&gt; 옵션은 특정 target cluster에 가까운 위치에서만 실행할 때 사용. producer가 원격 cluster로 보낼 때 발생하는 지연(producer lag)을 줄이는 공식 권장 사항이다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;멀티 리전 offset 동기화 — 이 글의 핵심&lt;/h2&gt;
&lt;p&gt;DR 시나리오에서 가장 중요한 질문으로 돌아가자: &lt;strong&gt;장애 전환 후 consumer가 어디서부터 읽어야 하는가?&lt;/strong&gt; MM2는 이 문제를 offset 변환(translation)으로 푼다.&lt;/p&gt;
&lt;h3&gt;offset이 왜 다른가 — 복제 과정에서 발생하는 offset 불일치&lt;/h3&gt;
&lt;p&gt;source에서 offset 100인 record가 target으로 복제되면, target에서의 offset이 100이라는 보장이 없다. 이유:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;source와 target의 파티션 수가 다를 수 있다 (MM2는 partition 수를 동기화하려 하지만, 기존 토픽이 이미 있는 경우는 다를 수 있음).&lt;/li&gt;
&lt;li&gt;source에서 retention으로 일부 record가 삭제됐을 수 있다.&lt;/li&gt;
&lt;li&gt;복제 중 일부 record가 누락됐거나 재시도로 인해 순서가 다를 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;따라서 source offset 100이 target에서는 offset 97일 수도 있고 103일 수도 있다. &lt;strong&gt;이 매핑을 저장하고 변환하는 것이 offset 동기화의 본질이다.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;MM2의 offset 동기화 메커니즘&lt;/h3&gt;
&lt;p&gt;MM2는 offset 동기화를 &lt;strong&gt;두 단계&lt;/strong&gt;로 수행한다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    subgraph 1단계[&amp;quot;1단계: offset 매핑 기록&amp;quot;]
        S1[&amp;quot;MirrorSourceConnector&amp;lt;br/&amp;gt;record 복제&amp;quot;] --&amp;gt; OST[&amp;quot;offset-syncs topic&amp;lt;br/&amp;gt;source offset → target offset 매핑&amp;quot;]
    end
    subgraph 2단계[&amp;quot;2단계: checkpoint 발행 + offset 동기화&amp;quot;]
        CC[&amp;quot;MirrorCheckpointConnector&amp;lt;br/&amp;gt;source consumer 그룹 offset 읽기&amp;quot;] --&amp;gt; OST
        OST --&amp;gt; TR[&amp;quot;offset 변환&amp;lt;br/&amp;gt;source offset → target offset&amp;quot;]
        TR --&amp;gt; CKPT[&amp;quot;checkpoints topic&amp;lt;br/&amp;gt;(target에 저장)&amp;quot;]
        TR --&amp;gt;|&amp;quot;sync.group.offsets.enabled=true&amp;quot;| TCO[&amp;quot;target __consumer_offsets&amp;lt;br/&amp;gt;(직접 기록)&amp;quot;]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;1단계 — offset-syncs topic&lt;/strong&gt;: MirrorSourceConnector가 record를 복제할 때마다, source offset과 target offset의 매핑을 &lt;code&gt;mm2-offset-syncs.{target}.internal&lt;/code&gt; topic에 기록한다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorConnectorConfig.java&quot;&gt;(MirrorConnectorConfig.java)&lt;/a&gt; 이 topic은 기본적으로 &lt;strong&gt;source cluster&lt;/strong&gt;에 저장된다 (&lt;code&gt;offset-syncs.topic.location = source&lt;/code&gt;). &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorConnectorConfig.java&quot;&gt;(MirrorConnectorConfig.java)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2단계 — checkpoint + group offset 동기화&lt;/strong&gt;: MirrorCheckpointConnector가 source cluster에서 consumer 그룹의 현재 offset을 읽고, offset-syncs topic의 매핑을 이용해 target 기준 offset으로 변환한 뒤:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;checkpoints topic&lt;/strong&gt;(&lt;code&gt;{target}.checkpoints.internal&lt;/code&gt;)에 변환된 offset을 기록한다 — &amp;quot;source 그룹 X가 source offset 100까지 읽었고, 이는 target offset 97에 해당한다.&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sync.group.offsets.enabled = true&lt;/code&gt;인 경우&lt;/strong&gt;, 변환된 offset을 target cluster의 &lt;code&gt;__consumer_offsets&lt;/code&gt; topic에 &lt;strong&gt;직접 기록&lt;/strong&gt;한다. 이렇게 되면 target cluster로 consumer를 전환했을 때, source에서 읽던 위치부터 자연스럽게 이어서 읽을 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorCheckpointConfig.java&quot;&gt;(MirrorCheckpointConfig.java)&lt;/a&gt;에 따르면, &lt;code&gt;sync.group.offsets.enabled&lt;/code&gt;의 기본값은 &lt;strong&gt;&lt;code&gt;false&lt;/code&gt;&lt;/strong&gt;다. 의도하지 않은 offset 덮어쓰기를 막기 위해保守的に 기본값이 설정된 것이다. DR 목적이라면 반드시 &lt;code&gt;true&lt;/code&gt;로 설정해야 한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;offset 동기화의 치명적인 전제 조건&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sync.group.offsets.enabled = true&lt;/code&gt;가 동작하려면 &lt;strong&gt;&amp;quot;target cluster에 해당 consumer 그룹의 활성 consumer가 없어야 한다&amp;quot;&lt;/strong&gt; — &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorCheckpointConfig.java&quot;&gt;(MirrorCheckpointConfig.java)&lt;/a&gt;의 문서에 명시된 전제 조건이다.&lt;/p&gt;
&lt;p&gt;이 조건이 왜 중요한가:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;target에 활성 consumer가 있으면, MM2가 덮어쓰는 offset과 consumer가 자체적으로 관리하는 offset이 충돌한다.&lt;/li&gt;
&lt;li&gt;Active/Passive DR에서는 정상적으로 동작 — 평소 target에 consumer가 없으므로 MM2가 offset을 안전하게 동기화한다.&lt;/li&gt;
&lt;li&gt;Active/Active에서는 &lt;strong&gt;제약이 된다&lt;/strong&gt; — 양쪽에 consumer가 있으므로 offset 동기화가 제대로 동작하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이 제약 때문에 Active/Active에서는 offset 동기화 대신 &lt;strong&gt;checkpoints topic을 직접 조회&lt;/strong&gt;하여 애플리케이션 수준에서 offset을 복구하는 패턴이 필요하다. &lt;code&gt;MirrorClient&lt;/code&gt; API(&lt;code&gt;kafka-clients&lt;/code&gt;의 &lt;code&gt;org.apache.kafka.connect.mirror.MirrorClient&lt;/code&gt;)를 통해 checkpoint를 조회할 수 있다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;offset lag 허용 임계값 — &lt;code&gt;offset.lag.max&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;source offset을 target offset으로 변환할 때, 변환 데이터가 없는 offset(매핑이 아직 기록되지 않은 최신 영역)는 어떻게 처리되는가. &lt;code&gt;offset.lag.max&lt;/code&gt;(기본값: 100) 설정이 이를 제어한다 — 매핑이 없는 offset 차이가 이 값 이하면 가장 가까운 매핑을 사용하고, 초과하면 checkpoint를 생략한다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorConnectorConfig.java&quot;&gt;(MirrorConnectorConfig.java)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;너무 낮으면 복제 지연 시 checkpoint가 누락되고, 너무 높으면 정확도가 떨어진다. 처리량이 높은 토픽은 기본값(100)으로 충분하지만, 낮은 처리량 토픽은 조정이 필요할 수 있다.&lt;/p&gt;
&lt;h2&gt;Active/Active 구성 — 멀티 리전 양방향 복제&lt;/h2&gt;
&lt;p&gt;Active/Active는 두 리전 모두에서 읽기·쓰기를 수행하면서 데이터를 상호 복제한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# Kafka 4.3, KRaft
clusters = us-west, us-east

us-west.bootstrap.servers = broker1-west:9092,broker2-west:9092
us-east.bootstrap.servers = broker1-east:9092,broker2-east:9092

# 양방향 flow
us-west-&amp;gt;us-east.enabled = true
us-east-&amp;gt;us-west.enabled = true

# 모든 토픽 복제
topics = .*
topics.exclude = .*[\-\.]internal, .*\.replica, __.*

# 내부 topic 복제 팩터
replication.factor = 3
checkpoints.topic.replication.factor = 3
heartbeats.topic.replication.factor = 3
offset-syncs.topic.replication.factor = 3

# checkpoint 발행 (양쪽 consumer 모니터링)
emit.checkpoints.enabled = true
emit.checkpoints.interval.seconds = 60

# offset 동기화는 Active/Active에서는 제약이 있음 (위 참조)
sync.group.offsets.enabled = true
sync.group.offsets.interval.seconds = 60&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Active/Active에서 루프 방지를 위해 &lt;code&gt;topics.exclude&lt;/code&gt;를 별도로 설정할 필요는 없다 — 같은 설정 파일에 두 flow를 정의하면 MM2가 자동으로 &lt;code&gt;us-west.*&lt;/code&gt;, &lt;code&gt;us-east.*&lt;/code&gt; 패턴의 토픽(이미 복제된 토픽)을 되복제하지 않는다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Active/Active의 consumer 관점&lt;/h3&gt;
&lt;p&gt;양방향 복제 환경에서 consumer가 &lt;code&gt;foo&lt;/code&gt; 토픽을 구독하면, 같은 cluster의 원본 &lt;code&gt;foo&lt;/code&gt;와 반대쪽에서 복제된 &lt;code&gt;us-east.foo&lt;/code&gt;(또는 &lt;code&gt;us-west.foo&lt;/code&gt;) 모두를 구독해야 양쪽 데이터를 모두 받는다. 이를 위해 MM2는 consumer가 하나의 &amp;quot;논리적 토픽&amp;quot;으로 통합 구독할 수 있게 지원하지 않으므로 — 애플리케이션이 두 토픽을 모두 구독하거나, upstream에서 통합 토픽으로 다시 쓰는 별도 계층이 필요하다.&lt;/p&gt;
&lt;p&gt;이 한계는 MM2 설계의 트레이드오프다: 단순성(이름 규칙 기반 출처 추적)을 택하는 대신 consumer 복잡성을 감수한다. 대안으로 Kafka 3.9+에서 도입된 &lt;strong&gt;Cluster Linking&lt;/strong&gt;(Confluent 확장, ASF core 아님)이 이 문제를 다르게 접근하지만, 이 글의 범위를 벗어난다.&lt;/p&gt;
&lt;h2&gt;프로덕션 운영 고려사항&lt;/h2&gt;
&lt;h3&gt;consume from remote, produce to local&lt;/h3&gt;
&lt;p&gt;공식 문서가 명시하는 최우선 권장 사항이다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;First DC              Second DC
==========            =================
primary ------------- MM2 ---------&amp;gt; secondary
(remote / source)     (local / target)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kafka producer는 consumer보다 불안정한 네트워크에 취약하다 (ack 대기, 재시도, 타임아웃). 따라서 &lt;strong&gt;MM2 프로세스를 target cluster와 같은 위치에 배치&lt;/strong&gt;하고 &lt;code&gt;--clusters&lt;/code&gt; 옵션으로 로컬 target만 지정하는 것이 지연 최소화의 핵심이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# secondary DC에서 실행 — primary(원격)에서 읽고 secondary(로컬)로 쓰기
bin/connect-mirror-maker.sh mm2.properties --clusters secondary&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;tasks.max 설정&lt;/h3&gt;
&lt;p&gt;MM2는 Connect 기반이므로, &lt;code&gt;tasks.max&lt;/code&gt;로 병렬 task 수를 조절한다. 기본값은 1이며, 프로덕션에서는 &lt;strong&gt;최소 2 이상&lt;/strong&gt;을 권장한다 — topic-partition 수와 하드웨어 리소스에 따라 증가. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# 각 MM2 프로세스의 최대 task 수
tasks.max = 5&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;내부 topic 관리&lt;/h3&gt;
&lt;p&gt;MM2는 여러 내부 topic을 생성한다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/config/connect-mirror-maker.properties&quot;&gt;(connect-mirror-maker.properties)&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;topic&lt;/th&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;목적&lt;/th&gt;
&lt;th&gt;복제 팩터 설정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heartbeats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;target&lt;/td&gt;
&lt;td&gt;connector↔cluster 연결 상태&lt;/td&gt;
&lt;td&gt;&lt;code&gt;heartbeats.topic.replication.factor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{target}.checkpoints.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source (기본)&lt;/td&gt;
&lt;td&gt;변환된 offset checkpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;checkpoints.topic.replication.factor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mm2-offset-syncs.{target}.internal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;source (기본)&lt;/td&gt;
&lt;td&gt;source→target offset 매핑&lt;/td&gt;
&lt;td&gt;&lt;code&gt;offset-syncs.topic.replication.factor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect 내부 topic (&lt;code&gt;*-configs&lt;/code&gt;, &lt;code&gt;*-offsets&lt;/code&gt;, &lt;code&gt;*-status&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;target&lt;/td&gt;
&lt;td&gt;Connect 프레임워크 상태&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*.storage.replication.factor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;프로덕션에서는 모든 내부 topic의 복제 팩터를 &lt;strong&gt;3 이상&lt;/strong&gt;으로 설정해야 한다. 기본 예제 파일에서는 1로 설정되어 있으나, 이는 개발/테스트용이다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;설정 일관성 — 치명적인 함정&lt;/h3&gt;
&lt;p&gt;여러 MM2 프로세스가 &lt;strong&gt;같은 target cluster&lt;/strong&gt;를 향해 복제할 때, 설정이 다르면 &lt;strong&gt;마지막에 실행된 프로세스의 설정으로 덮어쓰기&lt;/strong&gt;된다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# process 1
A-&amp;gt;B.enabled = true
A-&amp;gt;B.topics = foo

# process 2 (같은 target B를 향함)
A-&amp;gt;B.enabled = true
A-&amp;gt;B.topics = bar
# 결과: foo 또는 bar 중 하나만 복제됨 (leader에 따라 결정)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;→ 모든 MM2 프로세스가 &lt;strong&gt;동일한 설정 파일&lt;/strong&gt;을 사용해야 한다. 조직 수준에서 단일 설정 파일을 관리하고 자동화 도구(Ansible, Kubernetes ConfigMap 등)로 배포하는 것이 안전하다.&lt;/p&gt;
&lt;h3&gt;exactly-once 지원 (Kafka 3.5+)&lt;/h3&gt;
&lt;p&gt;dedicated MM2 cluster에 대해 exactly-once semantic이 지원된다 (Kafka 3.5.0부터). &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# target cluster에 exactly-once 쓰기
secondary.exactly.once.source.support = enabled

# MM2 노드 간 내부 통신 활성화 (필수)
dedicated.mode.enable.internal.rest = true
listeners = http://localhost:8080

# source consumer에서 aborted transaction 필터링
primary.consumer.isolation.level = read_committed&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;기존 MM2 cluster를 exactly-once로 전환하려면 2단계 업그레이드가 필요하다: 먼저 &lt;code&gt;exactly.once.source.support = preparing&lt;/code&gt;으로 전체 노드 재시작, 이후 &lt;code&gt;enabled&lt;/code&gt;로 재재시작. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;모니터링 — replication-latency가 핵심 메트릭&lt;/h3&gt;
&lt;p&gt;MM2는 &lt;code&gt;kafka.connect.mirror&lt;/code&gt; 메트릭 그룹으로 JMX 메트릭을 발행한다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# MirrorSourceConnector 메트릭
record-count              # 복제된 record 수
record-rate               # records/sec
replication-latency-ms    # source → target 전파 지연 (핵심)
byte-rate                 # bytes/sec

# MirrorCheckpointConnector 메트릭
checkpoint-latency-ms     # consumer offset 복제 지연&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;replication-latency-ms&lt;/code&gt;는 source에서 record가 쓰인 시각과 target에 복제된 시각의 차이이다. DR 시나리오에서 이 값이 크면, 장애 전환 시 최근 데이터가 유실될 수 있음을 의미한다. Prometheus + Grafana로 &lt;code&gt;replication-latency-ms&lt;/code&gt;와 &lt;code&gt;checkpoint-latency-ms&lt;/code&gt;를 대시보드에 반드시 포함해야 한다.&lt;/p&gt;
&lt;h3&gt;console-consumer 테스트 시 주의&lt;/h3&gt;
&lt;p&gt;기본적으로 MM2는 &lt;code&gt;console-consumer-.*&lt;/code&gt;, &lt;code&gt;connect-.*&lt;/code&gt;, &lt;code&gt;__.*&lt;/code&gt; 패턴의 consumer 그룹을 &lt;strong&gt;복제에서 제외&lt;/strong&gt;한다 (&lt;code&gt;groups.exclude&lt;/code&gt; 기본값). 테스트로 console consumer를 실행한 그룹의 offset이 동기화되지 않는다면 이 설정이 원인이다. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;(geo-replication.md)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;테스트 시에는 임시로 제외 목록을 수정:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# 테스트용 — console-consumer 그룹도 동기화
groups.exclude = connect-.*, __.*&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;테스트 완료 후 반드시 원래대로 복구.&lt;/p&gt;
&lt;h2&gt;DR 장애 전환(failover) 실제 시나리오&lt;/h2&gt;
&lt;p&gt;Active/Passive DR에서 장애 전환은 다음 순서로 수행한다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart TD
    A[&amp;quot;1. primary 장애 감지&amp;quot;] --&amp;gt; B[&amp;quot;2. consumer를 secondary로 전환&amp;lt;br/&amp;gt;(target 토픽명 사용: primary.foo)&amp;quot;]
    B --&amp;gt; C[&amp;quot;3. 동기화된 offset으로 이어서 소비&amp;lt;br/&amp;gt;(sync.group.offsets=true였으므로 자동)&amp;quot;]
    C --&amp;gt; D[&amp;quot;4. producer를 secondary로 전환&amp;quot;]
    D --&amp;gt; E[&amp;quot;5. primary 복구 후&amp;lt;br/&amp;gt;역방향 flow로 데이터 역동기화&amp;quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전환 시 consumer가 구독하는 토픽 이름이 바뀐다 — &lt;code&gt;foo&lt;/code&gt; → &lt;code&gt;primary.foo&lt;/code&gt; (DefaultReplicationPolicy). 이를 자동화하려면 consumer 설정에서 토픽 이름을 환경 변수로 관리하고, failover 시 이 값을 교체하는 메커니즘이 필요하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;장애 복구(failback) 후 primary와 secondary 간 데이터 일관성을 맞추려면, 복구된 primary로 역방향 복제 flow를 임시 활성화해야 한다. 단, 이미 소비된 offset 이후의 데이터만 복제되도록 주의.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;MM2 vs 클러스터 내 복제 — 혼동하면 안 되는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;클러스터 내 복제 (ISR/HW)&lt;/th&gt;
&lt;th&gt;클러스터 간 복제 (MM2)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;단일 클러스터 내 broker 간 데이터 안전성&lt;/td&gt;
&lt;td&gt;서로 다른 클러스터/리전 간 데이터 복제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단위&lt;/td&gt;
&lt;td&gt;partition replica&lt;/td&gt;
&lt;td&gt;topic (데이터 + 설정 + offset + ACL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지연&lt;/td&gt;
&lt;td&gt;밀리초 (동기/비동기 복제)&lt;/td&gt;
&lt;td&gt;네트워크 RTT + 복제 파이프라인 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;offset 보존&lt;/td&gt;
&lt;td&gt;같은 cluster이므로 동일&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;변환 필요&lt;/strong&gt; (source ≠ target offset)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 조치&lt;/td&gt;
&lt;td&gt;자동 (리더 선출)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;수동 또는 반자동&lt;/strong&gt; (consumer/producer 전환 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MM2는 Connect 기반 크로스 클러스터 복제 프레임워크&lt;/strong&gt;다. 데이터뿐 아니라 토픽 설정, ACL, consumer offset까지 복제한다 — 이것이 MirrorMaker 1과의 근본적 차이.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복제된 토픽 이름에는 source alias가 붙는다&lt;/strong&gt; (&lt;code&gt;us-west.foo&lt;/code&gt;). 이 규칙이 루프 방지와 출처 추적을 가능하게 하지만, consumer는 target에서 다른 이름으로 구독해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;offset 동기화는 &lt;code&gt;sync.group.offsets.enabled = true&lt;/code&gt;로 명시적 활성화해야 한다&lt;/strong&gt; (기본값 false). 핵심 전제: target에 활성 consumer가 없어야 동작한다 — Active/Passive DR에는 적합, Active/Active에는 제약.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;consume from remote, produce to local&lt;/strong&gt; — MM2 프로세스는 target 근처에 배치한다. producer가 consumer보다 네트워크 지연에 취약하기 때문.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설정 일관성이 치명적이다&lt;/strong&gt; — 같은 target을 향하는 모든 MM2 프로세스는 동일한 설정 파일을 써야 한다. 다르면 리더 선출에 따라 일부 복제가 조용히 사라진다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DR 장애 전환은 자동이 아니다&lt;/strong&gt; — 토픽 이름 변경, consumer/producer 재연결, offset 검증을 포함한 런북(runbook)이 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Active/Active 환경에서 consumer가 source의 &lt;code&gt;foo&lt;/code&gt;와 복제된 &lt;code&gt;us-east.foo&lt;/code&gt;를 모두 구독하고 있다. 같은 record를 두 번 처리하지 않으려면 애플리케이션 수준에서 어떤 설계가 필요할까?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sync.group.offsets.enabled = true&lt;/code&gt;인데 DR 전환 후 consumer가 offset 0부터 다시 읽는다면, 어떤 원인을 의심해야 할까?&lt;/li&gt;
&lt;li&gt;세 개의 리전(west, east, north)이 있고 각 리전에 두 개의 클러스터가 있다. XDCR(Cross Data Center Replication)을 포함한 전체 복제 토폴로지에서 &lt;code&gt;--clusters&lt;/code&gt; 옵션 없이 실행하면 어떤 문제가 발생할까?&lt;/li&gt;
&lt;li&gt;MM2의 exactly-once가 활성화된 환경에서 source cluster의 producer가 transaction을 abort했다. target에서 이 abort된 데이터가 어떻게 처리되는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;offset-syncs.topic.location&lt;/code&gt;을 &lt;code&gt;source&lt;/code&gt;에서 &lt;code&gt;target&lt;/code&gt;으로 변경하면 어떤 장단점이 있을까?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/geo-replication-(cross-cluster-data-mirroring).md&quot;&gt;ASF 공식 문서 - Geo-Replication (Cross-Cluster Data Mirroring)&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/configuration/mirrormaker-configs.md&quot;&gt;MirrorMaker Configs&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorConnectorConfig.java&quot;&gt;MirrorConnectorConfig.java&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror/src/main/java/org/apache/kafka/connect/mirror/MirrorCheckpointConfig.java&quot;&gt;MirrorCheckpointConfig.java&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/connect/mirror-client/src/main/java/org/apache/kafka/connect/mirror/DefaultReplicationPolicy.java&quot;&gt;DefaultReplicationPolicy.java&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/config/connect-mirror-maker.properties&quot;&gt;connect-mirror-maker.properties 예시&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;li&gt;KIP-382: &lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/KIP-382%3A+MirrorMaker+2.0&quot;&gt;MirrorMaker 2.0&lt;/a&gt; - 접근 2026-07-10&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>connect</category>
      <category>geo-replication</category>
      <category>kafka</category>
      <category>mirrormaker2</category>
      <category>multi-region</category>
      <category>offset-sync</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/24</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-13-mirror-maker-2#entry24comment</comments>
      <pubDate>Fri, 10 Jul 2026 09:40:59 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 12. operations</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-12-operations</link>
      <description>&lt;h1&gt;운영과 보안 — 클러스터 건강 유지와 &amp;quot;누가 뭘 할 수 있는가&amp;quot; 통제&lt;/h1&gt;
&lt;p&gt;Kafka를 기본 설정으로 두면 통신이 평문(PLAINTEXT)이고, 아무나 topic에 쓸 수 있다. 개발/테스트에만 허용되는 상태다. 프로덕션에선 &lt;strong&gt;(1) 클러스터가 건강한지&lt;/strong&gt; 감시하고, &lt;strong&gt;(2) 누가 접속하고 무엇을 할 수 있는지&lt;/strong&gt; 통제해야 한다.&lt;/p&gt;
&lt;p&gt;이 글은 Kafka 운영의 두 축과, &lt;strong&gt;실제로 보안을 어떻게 적용하는지&lt;/strong&gt;(SASL_SSL/SCRAM 설정, 인증서, ACL)까지 다룬다. 읽고 나면 broker와 client에 보안을 설정할 수 있다.&lt;/p&gt;
&lt;h2&gt;운영의 두 축&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;클러스터 건강&lt;/strong&gt; — broker가 살아있고, 복제가 따라잡고, 리더가 정상인가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보안&lt;/strong&gt; — 인증(누가), 인가(뭘 할 수 있는가), 암호화(통신 노출 여부).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;클러스터 건강 — KRaft와 복제 감시&lt;/h2&gt;
&lt;p&gt;Kafka 4.x는 &lt;strong&gt;KRaft&lt;/strong&gt;로 메타데이터를 관리한다(장 01·06). 운영자가 봐야 할 것:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;URP(under-replicated partitions)&lt;/strong&gt; — ISR &amp;lt; 복제본 수. 복제가 안 따라잡아 장애 내성 저하. &lt;strong&gt;핵심 경보&lt;/strong&gt;(장 06).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISR shrink/expand 빈도&lt;/strong&gt; — 잦으면 broker 불건강(GC·디스크·네트워크).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;controller 활성 상태&lt;/strong&gt; — active controller는 1개여야 정상.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;request latency&lt;/strong&gt; — broker 응답 지연.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;JMX(보통 포트 9999)로 메트릭 수집(Prometheus/JMX exporter). URP/ISR 모니터링 없이는 장애 감지 불가.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;보안 3계층 — 암호화·인증·인가를 분리하는 이유&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;담당&lt;/th&gt;
&lt;th&gt;수단&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;암호화(encryption)&lt;/td&gt;
&lt;td&gt;통신 내용 보호&lt;/td&gt;
&lt;td&gt;SSL/TLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증(authentication)&lt;/td&gt;
&lt;td&gt;접속 주체 확인&lt;/td&gt;
&lt;td&gt;SASL(PLAIN/SCRAM/GSSAPI/OAUTHBEARER) 또는 mTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인가(authorization)&lt;/td&gt;
&lt;td&gt;권한 제어&lt;/td&gt;
&lt;td&gt;ACL(&lt;code&gt;kafka-acls.sh&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;왜 3계층을 분리하는가 — &lt;strong&gt;심층 방어(defense in depth)&lt;/strong&gt;. 한 계층이 뚫려도 다른 층이 방어.&lt;/p&gt;
&lt;h2&gt;SASL 메커니즘 — PLAIN은 평문이다&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메커니즘&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;PLAIN&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;자격증명 평문 전송&lt;/strong&gt; → 반드시 SSL과 함께(&lt;code&gt;SASL_SSL&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SCRAM-SHA-256/512&lt;/td&gt;
&lt;td&gt;자격증명이 broker에 저장(해시). SSL 없이도 상대적 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSSAPI(Kerberos)&lt;/td&gt;
&lt;td&gt;기업 환경, 복잡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAUTHBEARER&lt;/td&gt;
&lt;td&gt;JWT/OAuth 연동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;자주 틀리는 함정&lt;/strong&gt;: &amp;quot;SASL만 켜면 안전하다&amp;quot; — SASL/PLAIN은 평문 → SSL 없이면 도청 취약. &lt;strong&gt;반드시 &lt;code&gt;SASL_SSL&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;보안 프로토콜 조합&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;security.protocol&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;암호화&lt;/th&gt;
&lt;th&gt;인증&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PLAINTEXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;개발 전용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SSL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;mTLS(선택)&lt;/td&gt;
&lt;td&gt;TLS 암호화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SASL_PLAINTEXT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;SASL&lt;/td&gt;
&lt;td&gt;인증만 (평문)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SASL_SSL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;SASL&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;프로덕션 표준&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;SASL_SSL/SCRAM 적용 방법 — 단계별 가이드&lt;/h2&gt;
&lt;p&gt;이제 실제로 보안을 어떻게 켜는지 단계별로 보자. SCRAM-SHA-256 + SSL 조합을 예로.&lt;/p&gt;
&lt;h3&gt;1단계: SCRAM 자격증명 생성 (broker에 사용자 추가)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka 4.3 — broker에 SCRAM 사용자 &amp;#39;alice&amp;#39; 추가
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --add-config &amp;#39;SCRAM-SHA-256=[password=alice-secret]&amp;#39; \
  --entity-type users --entity-name alice

# 확인
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
  --describe --entity-type users --entity-name alice&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 자격증명은 KRaft 메타데이터에 저장된다(구 ZK와 다름).&lt;/p&gt;
&lt;h3&gt;2단계: broker 설정 (server.properties)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# 리스너 설정
listeners=SASL_SSL://:9092
advertised.listeners=SASL_SSL://broker1.example.com:9092

# SASL 메커니즘
sasl.enabled.mechanisms=SCRAM-SHA-256
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256

# 인가(Authorizer) 활성화
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer

# ACL 없을 때 기본 거부
allow.everyone.if.no.acl.found=false

# super.users (모든 권한)
super.users=User:admin

# SSL 설정
ssl.keystore.location=/etc/kafka/keystore.jks
ssl.keystore.password=changeit
ssl.key.password=changeit
ssl.truststore.location=/etc/kafka/truststore.jks
ssl.truststore.password=changeit&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3단계: SSL 인증서 준비 (keytool)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 키스토어 생성 (broker 인증서)
keytool -keystore /etc/kafka/keystore.jks -alias broker1 -validity 365 \
  -genkey -keyalg RSA -dname &amp;quot;CN=broker1.example.com&amp;quot; \
  -storepass changeit -keypass changeit

# 2. CA 인증서로 서명 (실제 환경)
# (자체 서명 인증서는 학습용; 프로덕션은 정식 CA 사용)

# 3. 트러스트스토어 생성 (client가 broker 인증서 검증용)
keytool -keystore /etc/kafka/truststore.jks -alias CARoot -import \
  -file ca-cert -storepass changeit -noprompt&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4단계: client 설정 (producer/consumer)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# 보안 프로토콜
security.protocol=SASL_SSL
sasl.mechanism=SCRAM-SHA-256

# JAAS (자격증명)
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
  username=&amp;quot;alice&amp;quot; \
  password=&amp;quot;alice-secret&amp;quot;;

# SSL (broker 인증서 검증)
ssl.truststore.location=/etc/kafka/client-truststore.jks
ssl.truststore.password=changeit&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 JAAS 파일 방식 (&lt;code&gt;kafka_client_jaas.conf&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;KafkaClient {
  org.apache.kafka.common.security.scram.ScramLoginModule required
  username=&amp;quot;alice&amp;quot;
  password=&amp;quot;alice-secret&amp;quot;;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5단계: ACL 부여&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# alice에게 my-topic 쓰기 권한
bin/kafka-acls.sh --bootstrap-server localhost:9092 \
  --add --allow-principal User:alice \
  --operation WRITE --operation READ \
  --topic my-topic

# alice에게 consumer group &amp;#39;my-group&amp;#39; 권한
bin/kafka-acls.sh --bootstrap-server localhost:9092 \
  --add --allow-principal User:alice \
  --operation READ \
  --group my-group

# ACL 확인
bin/kafka-acls.sh --bootstrap-server localhost:9092 --list&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6단계: 보안 적용 후 producer/consumer 실행&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 보안 설정이 담긴 client properties 파일로 실행
bin/kafka-console-producer.sh --topic my-topic \
  --producer.config /etc/kafka/client.properties \
  --bootstrap-server broker1.example.com:9092

bin/kafka-console-consumer.sh --topic my-topic \
  --consumer.config /etc/kafka/client.properties \
  --bootstrap-server broker1.example.com:9092&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;보안 설정은 broker 재기동이 필요하다. 학습 환경에서는 무거울 수 있으니, PLAINTEXT 환경에서 동작을 먼저 확인한 뒤 보안을 적용하라.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;ACL — 리소스와 작업&lt;/h2&gt;
&lt;p&gt;ACL은 리소스별로 &amp;quot;누가( principal) 무슨 작업(operation)을 할 수 있는가&amp;quot;를 정의한다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;리소스 종류&lt;/th&gt;
&lt;th&gt;작업 예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;READ, WRITE, CREATE, DELETE, ALTER, DESCRIBE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;READ, DESCRIBE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CLUSTER_ACTION, DESCRIBE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transactionalId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WRITE, DESCRIBE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# User:bob에게 orders topic 읽기 + my-group group 읽기
bin/kafka-acls.sh --add \
  --allow-principal User:bob \
  --operation READ --topic orders \
  --operation READ --group my-group \
  --bootstrap-server localhost:9092

# prefix 매칭 (모든 &amp;#39;logs-&amp;#39; topic에 쓰기)
bin/kafka-acls.sh --add \
  --allow-principal User:logger \
  --operation WRITE --topic logs- --resource-pattern-type prefixed \
  --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JMX 모니터링 설정&lt;/h2&gt;
&lt;p&gt;broker의 JMX 메트릭을 노출하려면:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# broker 기동 시 JMX 포트 지정
export JMX_PORT=9999
bin/kafka-server-start.sh config/server.properties&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심 지표:&lt;br&gt;| 지표 | 의미 |&lt;br&gt;|------|------|&lt;br&gt;| &lt;code&gt;kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions&lt;/code&gt; | URP 수 (0이 정상) |&lt;br&gt;| &lt;code&gt;kafka.server:type=ReplicaManager,name=IsrShrinksPerSec&lt;/code&gt; | ISR 축소 빈도 |&lt;br&gt;| &lt;code&gt;kafka.network:type=RequestMetrics,name=TotalTimeMs&lt;/code&gt; | 요청 지연 |&lt;br&gt;| &lt;code&gt;kafka.controller:type=KafkaController,name=ActiveControllerCount&lt;/code&gt; | 활성 controller (1이 정상) |&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Prometheus + JMX Exporter로 수집·Grafana로 시각화하는 게 운영 표준.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;KRaft 운영 — broker 추가/제거&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;broker 추가: 새 broker 기동 + partition 복제 이동(&lt;code&gt;kafka-reassign-partitions.sh&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;controller 추가/제거: quorum 재구성(KIP-853 동적 quorum, &lt;code&gt;controller.quorum.bootstrap.servers&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# partition reassignment (broker 추가 후 부하 분산)
bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --topics-to-move-json-file topics.json \
  --broker-list &amp;quot;0,1,2&amp;quot; --generate
# → 생성된 계획으로 --execute&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;SASL만 켜면 안전하다&amp;quot;&lt;/td&gt;
&lt;td&gt;PLAIN은 평문 자격증명 → &lt;code&gt;SASL_SSL&lt;/code&gt; 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;보안은 나중에 붙인다&amp;quot;&lt;/td&gt;
&lt;td&gt;재기동·호환성 비용. 설계 단계부터 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;ACL 없으면 다 허용이다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allow.everyone.if.no.acl.found&lt;/code&gt; 설정에 따라 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;JMX는 옵션&amp;quot;&lt;/td&gt;
&lt;td&gt;URP/ISR/지연 모니터링 없이는 장애 감지 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;KRaft는 ZK와 운영 명령이 같다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;controller.quorum.bootstrap.servers&lt;/code&gt;(KIP-853)가 최신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;mTLS는 클라이언트만&amp;quot;&lt;/td&gt;
&lt;td&gt;broker 간 통신에도 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;동적 broker 구성&lt;/strong&gt;: &lt;code&gt;kafka-configs.sh --entity-type brokers&lt;/code&gt;로 재기동 없이 설정 변경.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;partition reassignment throttle&lt;/strong&gt;: &lt;code&gt;--throttle&lt;/code&gt;로 대량 복제 시 운영 트래픽 보호.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mTLS(상호 TLS)&lt;/strong&gt;: 클라이언트가 X.509 인증서 제시 → broker truststore로 검증. broker 간 통신 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAUTHBEARER&lt;/strong&gt;: JWT/OAuth 토큰 기반 인증(KIP-255). 클라우드 환경에서 유용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;운영 2축&lt;/strong&gt;: 클러스터 건강(KRaft/복제) + 보안 3계층.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;건강 감시&lt;/strong&gt;: JMX(포트 9999)로 URP·ISR·controller·지연. URP가 핵심 경보.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;보안 3계층&lt;/strong&gt;: 암호화(SSL), 인증(SASL/mTLS), 인가(ACL).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SASL_SSL/SCRAM 적용&lt;/strong&gt;: (1) SCRAM 자격증명 생성 → (2) broker 설정(listeners/SASL/SSL/ACL) → (3) SSL 인증서(keytool) → (4) client 설정(JAAS+truststore) → (5) ACL 부여 → (6) 보안 적용 후 실행.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ACL&lt;/strong&gt;: &lt;code&gt;kafka-acls.sh&lt;/code&gt;. 리소스(topic/group/cluster) × 작업(READ/WRITE/...). 기본 거부 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KRaft 운영&lt;/strong&gt;: &lt;code&gt;controller.quorum.bootstrap.servers&lt;/code&gt;(KIP-853 동적). broker 추가 시 reassignment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;보안 3계층이 각각 해결하는 위협을 말하라.&lt;/li&gt;
&lt;li&gt;SASL/PLAIN을 SSL 없이 쓰면 왜 위험한가? SCRAM이 상대적으로 안전한 이유는?&lt;/li&gt;
&lt;li&gt;SCRAM 자격증명을 broker에 추가하는 명령을 써 보라.&lt;/li&gt;
&lt;li&gt;SASL_SSL을 적용하기 위한 broker 설정 항목 5가지를 말하라.&lt;/li&gt;
&lt;li&gt;ACL로 &amp;quot;User:alice가 orders topic에 쓰기·읽기&amp;quot; 권한을 부여하는 명령은?&lt;/li&gt;
&lt;li&gt;URP가 운영 경보의 핵심인 이유는? JMX로 뭘 봐야 하는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/security/security-overview.md&quot;&gt;Security Overview&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/security/security-model.md&quot;&gt;Security Model&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/security/authentication-using-sasl.md&quot;&gt;Authentication using SASL&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/security/ssl.md&quot;&gt;SSL encryption&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/security/authorization&quot;&gt;Authorization and ACLs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/kraft.md&quot;&gt;Operations — KRaft&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>ACL</category>
      <category>jmx</category>
      <category>kafka</category>
      <category>Kraft</category>
      <category>Operations</category>
      <category>SASL</category>
      <category>security</category>
      <category>SSL</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/23</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-12-operations#entry23comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:48:05 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 11. kafka streams</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-11-kafka-streams</link>
      <description>&lt;h1&gt;Kafka Streams — topic의 데이터를 가공(집계·조인·창)하는 라이브러리&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;orders&lt;/code&gt; topic에 주문 이벤트가 계속 쌓인다. &amp;quot;5분 단위로 주문 금액을 합산하고 싶다&amp;quot; — 직접 consumer + window 로직 + 상태 저장을 짜면, 장애 시 상태 복구·재처리가 지옥이다. &lt;strong&gt;Kafka Streams&lt;/strong&gt;는 집계·조인·창(windowing) + 상태 저장(state store) + 장애 복구(changelog)를 &lt;strong&gt;프레임워크가 알아서 처리&lt;/strong&gt;한다.&lt;/p&gt;
&lt;p&gt;이 글은 Streams가 뭔지부터 시작해, &lt;strong&gt;실제로 앱을 어떻게 만들고 실행하는지&lt;/strong&gt;(Maven 설정, topology 코드, 실행)까지 다룬다. 읽고 나면 간단한 스트림 처리 앱을 만들 수 있다.&lt;/p&gt;
&lt;h2&gt;Streams란 — 앱에 내장되는 스트림 처리 라이브러리&lt;/h2&gt;
&lt;p&gt;Kafka Streams는 &lt;strong&gt;별도 처리 클러스터가 아니라, 애플리케이션에 내장되는 클라이언트 라이브러리&lt;/strong&gt;다. Spark/Flink처럼 별도 클러스터를 세울 필요 없이, 당신의 앱 프로세스 안에서 동작한다.&lt;/p&gt;
&lt;p&gt;왜 라이브러리인 게 강점인가:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;배포 단순&lt;/strong&gt; — 그냥 앱(JAR)을 띄우면 됨. 별도 클러스터 운영 오버헤드 없음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;확장&lt;/strong&gt; — 인스턴스를 더 띄우면 consumer group처럼 partition 재분배로 부하 분산(장 05).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장애 복구&lt;/strong&gt; — 인스턴스가 죽으면 다른 인스턴스가 partition + state를 인계.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Connect(장 10)가 &lt;strong&gt;&amp;quot;데이터를 옮기는&amp;quot;&lt;/strong&gt; 거라면, Streams는 &lt;strong&gt;&amp;quot;데이터를 가공하는&amp;quot;&lt;/strong&gt; 도구다.&lt;/p&gt;
&lt;h2&gt;stream-table duality — 같은 데이터의 두 모습&lt;/h2&gt;
&lt;p&gt;Streams의 핵심 통찰: &lt;strong&gt;&amp;quot;stream과 table은 같은 것의 두 모습&amp;quot;&lt;/strong&gt;이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;stream&lt;/strong&gt; = 이벤트의 흐름(예: 주문이 발생하는 기록).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;table&lt;/strong&gt; = key별 최신 상태(예: 각 사용자의 현재 주문 합계).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;거래 내역을 시간순으로 적은 장부(stream)에서, 각 계좌별 &lt;strong&gt;마지막 잔액&lt;/strong&gt;만 모으면 잔액장(table)이 된다. 반대로 잔액장의 &lt;strong&gt;변경 이력&lt;/strong&gt;을 펼치면 다시 거래 내역(stream)이 된다.&lt;/p&gt;
&lt;p&gt;Streams는 이를 두 타입으로 모델링한다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KStream&lt;/strong&gt; — record 스트림. 각 이벤트가 독립.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KTable&lt;/strong&gt; — key→값의 최신 상태 테이블. 같은 key 갱신은 이전값 대체.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GlobalKTable&lt;/strong&gt; — 모든 인스턴스가 전체 복제. partition 무관 조인(참조 데이터용).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Streams 앱 만들기 — Maven 설정부터&lt;/h2&gt;
&lt;h3&gt;의존성 (pom.xml)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.kafka&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;kafka-streams&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.kafka&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;kafka-clients&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.3.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- JSON serde (예시) --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.kafka&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;kafka-streams-test-utils&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;4.3.0&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;앱 설정 (Properties)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, &amp;quot;order-aggregation-app&amp;quot;);
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, &amp;quot;localhost:9092&amp;quot;);
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
// EOS 켜기 (선택)
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, &amp;quot;exactly_once_v2&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;application.id&lt;/code&gt; — Streams 앱 식별자. &lt;strong&gt;changelog topic 이름의 prefix&lt;/strong&gt;가 됨. 같은 id의 인스턴스들이 consumer group 형성.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROCESSING_GUARANTEE&lt;/code&gt; — &lt;code&gt;at_least_once&lt;/code&gt;(기본) 또는 &lt;code&gt;exactly_once_v2&lt;/code&gt;(EOS, 장 08).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;topology 코드 — &amp;quot;처리 흐름&amp;quot; 정의&lt;/h2&gt;
&lt;p&gt;topology는 데이터 처리 흐름의 DAG(유향 비순환 그래프)다. DSL API로 선언적으로 정의한다.&lt;/p&gt;
&lt;h3&gt;예 1: 사용자별 주문 금액 합산&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// 1. 입력 topic → KStream
KStream&amp;lt;String, String&amp;gt; orders = builder.stream(&amp;quot;orders&amp;quot;);

// 2. key별 그룹화 + 금액 합산 → KTable
KTable&amp;lt;String, Long&amp;gt; userTotals = orders
    .groupByKey()
    .aggregate(
        () -&amp;gt; 0L,                          // 초기값
        (key, order, total) -&amp;gt; total + parseAmount(order),  // 집계 로직
        Materialized.as(&amp;quot;user-totals-store&amp;quot;)  // state store 이름
    );

// 3. 결과를 topic으로 내보내기
userTotals.toStream().to(&amp;quot;user-totals&amp;quot;, Produced.with(Serdes.String(), Serdes.Long()));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드가 하는 일:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;orders&lt;/code&gt; topic의 메시지를 key(사용자 ID)별로 그룹화.&lt;/li&gt;
&lt;li&gt;금액을 누적 합산(state store에 저장).&lt;/li&gt;
&lt;li&gt;결과를 &lt;code&gt;user-totals&lt;/code&gt; topic으로 내보냄.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;상태 저장&lt;/strong&gt;(aggregate)이므로 changelog topic이 자동 생성되어 장애 복구 지원.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;예 2: 필터링 (무상태)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;KStream&amp;lt;String, String&amp;gt; orders = builder.stream(&amp;quot;orders&amp;quot;);
KStream&amp;lt;String, String&amp;gt; bigOrders = orders.filter((key, value) -&amp;gt; parseAmount(value) &amp;gt; 1000);
bigOrders.to(&amp;quot;big-orders&amp;quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;무상태 연산(filter/map)은 state store나 changelog가 필요 없다 — 빠르고 단순.&lt;/p&gt;
&lt;h3&gt;예 3: 시간 창 집계 (windowing)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;KTable&amp;lt;Windowed&amp;lt;String&amp;gt;, Long&amp;gt; hourlyTotals = orders
    .groupByKey()
    .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofHours(1)))
    .count();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1시간 창으로 key별 개수 집계. event time 기준.&lt;/p&gt;
&lt;h3&gt;topology 시작&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Topology topology = builder.build();
KafkaStreams streams = new KafkaStreams(topology, props);
streams.start();

// 종료 훅
Runtime.getRuntime().addShutdownHook(new Thread(streams::close));&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;상태 저장 처리가 장애 후에도 정확히 복구되는 원리 — changelog&lt;/h2&gt;
&lt;p&gt;상태 저장 처리(집계 등)는 &amp;quot;별도 DB가 필요하다&amp;quot;고 생각하기 쉽다. &lt;strong&gt;Streams는 state store를 로컬(RocksDB)에 두고, 그 백업을 Kafka 내부 topic(changelog)에 영속&lt;/strong&gt;한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    IN[orders topic] --&amp;gt; S[&amp;quot;KStream&amp;quot;]
    S --&amp;gt; AGG[&amp;quot;aggregate&amp;lt;br/&amp;gt;(state store: RocksDB)&amp;quot;]
    AGG --&amp;gt; CH[&amp;quot;changelog topic&amp;lt;br/&amp;gt;(백업/복구)&amp;quot;]
    AGG --&amp;gt; OUT[user-totals topic]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;집계 상태는 &lt;strong&gt;로컬 state store(RocksDB)&lt;/strong&gt;에 유지.&lt;/li&gt;
&lt;li&gt;store의 모든 갱신은 &lt;strong&gt;changelog topic&lt;/strong&gt;에 기록.&lt;/li&gt;
&lt;li&gt;장애 시 changelog에서 state store 재구축 → &lt;strong&gt;별도 DB 없이 내결함 상태 처리&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;state store는 partition 단위로 분산 → 확장성.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;changelog topic 확인&lt;/h3&gt;
&lt;p&gt;Streams 앱 기동 후 자동 생성되는 내부 topic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bin/kafka-topics.sh --list --bootstrap-server localhost:9092
# order-aggregation-app-user-totals-store-changelog 같은 topic 생성 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 topic이 state store의 복구 원천이다 — &lt;strong&gt;삭제/retention 변경 금지&lt;/strong&gt;(상태 손상).&lt;/p&gt;
&lt;h2&gt;DSL vs Processor API&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;수준&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DSL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;고수준&lt;/td&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;/&lt;code&gt;filter&lt;/code&gt;/&lt;code&gt;aggregate&lt;/code&gt;/&lt;code&gt;join&lt;/code&gt;/&lt;code&gt;windowedBy&lt;/code&gt;. 선언적. 대부분 이걸로 충분.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Processor API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;저수준&lt;/td&gt;
&lt;td&gt;커스텀 processor + state store 직접 제어. 최대 유연성.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;초보자는 DSL로 시작하라 — 대부분의 집계·필터·조인이 DSL 한 줄로 된다.&lt;/p&gt;
&lt;h2&gt;Kafka 4.3 실습 — 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전: KRaft 단일 노드 기동. Streams는 Java 라이브러리 → Maven 프로젝트 필요.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;앱 빌드·실행&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Maven 프로젝트 빌드
mvn clean package

# 2. 앱 실행
java -jar target/my-streams-app.jar

# 3. 다른 터미널에서 입력 topic에 메시지 produce
bin/kafka-console-producer.sh --topic orders --bootstrap-server localhost:9092
# userA:{&amp;quot;amount&amp;quot;: 5000}
# userA:{&amp;quot;amount&amp;quot;: 3000}
# userB:{&amp;quot;amount&amp;quot;: 7000}

# 4. 출력 topic 확인
bin/kafka-console-consumer.sh --topic user-totals --from-beginning --bootstrap-server localhost:9092
# userA: 8000
# userB: 7000&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;changelog topic 관찰&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bin/kafka-topics.sh --list --bootstrap-server localhost:9092
# order-aggregation-app-user-totals-store-changelog 생성 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: 상태 저장 연산을 쓰면 changelog topic이 자동 생성.&lt;/p&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;Streams는 별도 클러스터다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;라이브러리&lt;/strong&gt;. 앱에 내장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;KStream과 KTable은 같다&amp;quot;&lt;/td&gt;
&lt;td&gt;다르다. stream=이벤트 흐름, table=최신 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;상태 저장 처리엔 외부 DB가 필요&amp;quot;&lt;/td&gt;
&lt;td&gt;로컬 state store(RocksDB) + changelog로 자체 해결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Connect와 Streams는 같은 역할&amp;quot;&lt;/td&gt;
&lt;td&gt;다르다. Connect=데이터 &lt;strong&gt;이동&lt;/strong&gt;, Streams=데이터 &lt;strong&gt;가공&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;EOS는 자동이다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;processing.guarantee=exactly_once_v2&lt;/code&gt; 설정 시. 기본은 at-least-once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;windowing은 processing time 기준&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;event time&lt;/strong&gt; 기준 권장 (지연 이벤트 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;changelog topic은 삭제해도 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;금지&lt;/strong&gt;. 상태 손상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;interactive queries&lt;/strong&gt;: state store를 앱이 직접 조회(별도 DB 동기화 없이 &amp;quot;현재 key X의 값&amp;quot; 반환). 단, partition 분산이라 다른 인스턴스의 key 조회엔 RPC 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GlobalKTable vs KTable 조인&lt;/strong&gt;: KTable-KStream join은 같은 key + 같은 partition 전제(co-partitioning). GlobalKTable은 partition 무관(참조 데이터용).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rebalance와 static membership&lt;/strong&gt;: Streams 앱 재시작 시 rebalance 비용. &lt;code&gt;group.instance.id&lt;/code&gt;(장 05)로 최소화.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EOS&lt;/strong&gt;: &lt;code&gt;processing.guarantee=exactly_once_v2&lt;/code&gt;로 consume-process-produce 자동 원자화(장 08).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kafka Streams = 클라이언트 라이브러리&lt;/strong&gt;(별도 클러스터 아님). 앱에 내장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stream-table duality&lt;/strong&gt;: stream = 이벤트 흐름, table = key별 최신 상태.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KStream&lt;/strong&gt;(이벤트) / &lt;strong&gt;KTable&lt;/strong&gt;(상태) / &lt;strong&gt;GlobalKTable&lt;/strong&gt;(전체 복제).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maven 의존성&lt;/strong&gt; + &lt;strong&gt;Properties 설정&lt;/strong&gt;(&lt;code&gt;application.id&lt;/code&gt; 핵심) + &lt;strong&gt;DSL topology&lt;/strong&gt;로 앱 구성.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;state store&lt;/strong&gt;(RocksDB) + &lt;strong&gt;changelog topic&lt;/strong&gt; = 내결함 상태 처리. 별도 DB 불필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DSL&lt;/strong&gt;(고수준) / &lt;strong&gt;Processor API&lt;/strong&gt;(저수준).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EOS&lt;/strong&gt;: &lt;code&gt;processing.guarantee=exactly_once_v2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connect ≠ Streams&lt;/strong&gt;: 이동 vs 가공.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Streams가 &amp;quot;라이브러리&amp;quot;라는 것이 운영상 어떤 의미인가?&lt;/li&gt;
&lt;li&gt;stream-table duality를 KStream/KTable로 설명하라.&lt;/li&gt;
&lt;li&gt;Maven 프로젝트에서 Streams 앱을 빌드하려면 어떤 의존성이 필요한가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application.id&lt;/code&gt;가 changelog topic 이름에 어떤 영향을 주는가?&lt;/li&gt;
&lt;li&gt;state store가 장애 후 정확히 복구되는 원리(changelog)는?&lt;/li&gt;
&lt;li&gt;Connect와 Streams의 역할 차이는? 둘을 같이 쓰는 시나리오를 상상해 보라.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/streams/core-concepts.md&quot;&gt;Streams — Core Concepts&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/streams/developer-guide/&quot;&gt;Streams Developer Guide&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/streams/developer-guide/config-streams.md&quot;&gt;Streams config (EOS, application.id)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/streams/examples/&quot;&gt;Streams code examples&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;KIP-447(exactly_once_v2)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>kafka</category>
      <category>kstream</category>
      <category>ktable</category>
      <category>state-store</category>
      <category>Streams</category>
      <category>Topology</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/22</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-11-kafka-streams#entry22comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:47:22 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 10. kafka connect</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-10-kafka-connect</link>
      <description>&lt;h1&gt;Kafka Connect — 코드 없이 DB·파일과 Kafka 사이 데이터를 옮기다&lt;/h1&gt;
&lt;p&gt;&amp;quot;DB의 주문 데이터를 Kafka로 옮기고 싶다.&amp;quot; producer 코드를 직접 짤 수도 있지만 — 재시작 시 어디까지 읽었는지, 병렬로 어떻게 나눌지, 장애 시 복구를 다 손봐야 한다. &lt;strong&gt;Kafka Connect&lt;/strong&gt;는 이 공통 작업을 &lt;strong&gt;프레임워크가 대신 처리&lt;/strong&gt;한다. 커넥터 설정 하나로 데이터 파이프라인이 완성된다.&lt;/p&gt;
&lt;p&gt;이 글은 Connect가 뭔지부터 시작해, &lt;strong&gt;실제로 어떻게 구성하고 실행하는지&lt;/strong&gt;(설정 파일, connector JSON, REST API 관리)까지 다룬다. 읽고 나면 &amp;quot;파일→Kafka→파일&amp;quot; 파이프라인을 직접 구성할 수 있다.&lt;/p&gt;
&lt;h2&gt;Connect란 — 커넥터 기반 데이터 파이프라인 프레임워크&lt;/h2&gt;
&lt;p&gt;Connect는 &lt;strong&gt;데이터를 Kafka와 외부 시스템(DB·파일·S3) 사이로 옮기는 프레임워크&lt;/strong&gt;다. 핵심 개념:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;connector&lt;/strong&gt; — &amp;quot;어디서→어디로, 무엇을&amp;quot;의 정의. 두 종류:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;source connector&lt;/strong&gt;: 외부 시스템 → Kafka (DB 변경을 topic으로).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;sink connector&lt;/strong&gt;: Kafka → 외부 시스템 (topic 데이터를 ES·S3로).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;task&lt;/strong&gt; — connector가 작업을 병렬 단위로 쪼갠 것. worker들이 나눠 실행.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;worker&lt;/strong&gt; — Connect 프로세스.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    DB[(Postgres DB)] --&amp;gt;|source connector| K[Kafka topic]
    K --&amp;gt;|sink connector| ES[(Elasticsearch)]
    K --&amp;gt;|sink connector| S3[(S3)]
    subgraph CW[Connect worker]
        C[connector] --&amp;gt;|작업 분할| T1[task 1]
        C --&amp;gt; T2[task 2]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Connect는 &lt;strong&gt;Apache Kafka core 배포판에 포함&lt;/strong&gt;된다(&lt;code&gt;bin/connect-*&lt;/code&gt;). 별도 설치 불필요. 단, Schema Registry는 Confluent 확장이라 별도(장 09).&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;standalone vs distributed — 어느 모드를 쓸까&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;상태 저장&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;standalone&lt;/td&gt;
&lt;td&gt;개발/테스트&lt;/td&gt;
&lt;td&gt;로컬 파일&lt;/td&gt;
&lt;td&gt;단일 프로세스. 단일 장애점.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;distributed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;프로덕션&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;내부 topic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;여러 worker가 task 공유. 장애 시 자동 재배치. REST API 관리.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;프로덕션은 무조건 distributed&lt;/strong&gt; — standalone은 단일 장애점이라 장애 시 데이터 이동이 멈춘다.&lt;/p&gt;
&lt;h2&gt;Connect 구성 방법 — 설정 파일부터 시작&lt;/h2&gt;
&lt;p&gt;Connect를 실행하려면 &lt;strong&gt;worker 설정 + connector 설정&lt;/strong&gt; 두 가지가 필요하다.&lt;/p&gt;
&lt;h3&gt;worker 설정 (standalone)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;config/connect-standalone.properties&lt;/code&gt; — Connect 프로세스 자체의 설정:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# Kafka 클러스터 연결
bootstrap.servers=localhost:9092

# 데이터 직렬화 포맷 (converter)
key.converter=org.apache.kafka.connect.json.JsonConverter
value.converter=org.apache.kafka.connect.json.JsonConverter
key.converter.schemas.enable=false
value.converter.schemas.enable=false

# offset/config/status 저장 위치 (standalone은 파일)
offset.storage.file.filename=/tmp/connect.offsets

# REST API 포트 (distributed에서 필수, standalone은 선택)
rest.port=8083

# 커넥터 플러그인 경로 (JAR 배치 위치)
plugin.path=/usr/local/share/kafka/plugins&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;key.converter&lt;/code&gt;/&lt;code&gt;value.converter&lt;/code&gt;가 핵심 — Connect가 Kafka에 쓸 때 바이트로 변환하는 포맷. JSON 또는 Avro(+Schema Registry). 잘못된 converter는 consumer가 못 읽는 바이트를 만든다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;worker 설정 (distributed)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;config/connect-distributed.properties&lt;/code&gt; — 차이점은 상태를 &lt;strong&gt;Kafka 내부 topic&lt;/strong&gt;에 저장:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;bootstrap.servers=localhost:9092
key.converter=org.apache.kafka.connect.json.JsonConverter
value.converter=org.apache.kafka.connect.json.JsonConverter

# 상태 저장용 내부 topic (distributed 모드 필수)
group.id=connect-cluster
offset.storage.topic=connect-offsets
config.storage.topic=connect-configs
status.storage.topic=connect-status

# 내부 topic 설정 (compact 권장 - 최신 설정만 유지)
offset.storage.replication.factor=3
config.storage.replication.factor=3
status.storage.replication.factor=3

rest.port=8083
plugin.path=/usr/local/share/kafka/plugins&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;connect-offsets&lt;/code&gt;/&lt;code&gt;connect-configs&lt;/code&gt;/&lt;code&gt;connect-status&lt;/code&gt;는 &lt;strong&gt;삭제·retention 변경 금지&lt;/strong&gt;. Connect가 상태를 의존한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;distributed 모드 기동&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka 4.3, KRaft 단일 노드 기동 후
bin/connect-distributed.sh config/connect-distributed.properties
# 여러 터미널에서 같은 설정으로 추가 기동 → 자동 클러스터 형성&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;connector 구성 — 어떻게 데이터 파이프라인을 정의하나&lt;/h2&gt;
&lt;h3&gt;방식 1: properties 파일 (standalone)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;config/connect-file-source.properties&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;name=file-source
connector.class=org.apache.kafka.connect.file.FileStreamSourceConnector
tasks.max=1
file=/tmp/test.txt
topic=connect-test&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;방식 2: REST API + JSON (distributed, 권장)&lt;/h3&gt;
&lt;p&gt;distributed 모드에선 REST API(포트 8083)로 connector를 관리한다:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;connector 생성&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST -H &amp;quot;Content-Type: application/json&amp;quot; \
  --data &amp;#39;{
    &amp;quot;name&amp;quot;: &amp;quot;file-source-demo&amp;quot;,
    &amp;quot;config&amp;quot;: {
      &amp;quot;connector.class&amp;quot;: &amp;quot;org.apache.kafka.connect.file.FileStreamSourceConnector&amp;quot;,
      &amp;quot;tasks.max&amp;quot;: &amp;quot;1&amp;quot;,
      &amp;quot;file&amp;quot;: &amp;quot;/tmp/test.txt&amp;quot;,
      &amp;quot;topic&amp;quot;: &amp;quot;connect-test&amp;quot;
    }
  }&amp;#39; \
  http://localhost:8083/connectors&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;JDBC source connector 예&lt;/strong&gt; (DB → Kafka):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST -H &amp;quot;Content-Type: application/json&amp;quot; \
  --data &amp;#39;{
    &amp;quot;name&amp;quot;: &amp;quot;postgres-orders-source&amp;quot;,
    &amp;quot;config&amp;quot;: {
      &amp;quot;connector.class&amp;quot;: &amp;quot;io.confluent.connect.jdbc.JdbcSourceConnector&amp;quot;,
      &amp;quot;connection.url&amp;quot;: &amp;quot;jdbc:postgresql://localhost:5432/mydb&amp;quot;,
      &amp;quot;connection.user&amp;quot;: &amp;quot;postgres&amp;quot;,
      &amp;quot;connection.password&amp;quot;: &amp;quot;secret&amp;quot;,
      &amp;quot;table.whitelist&amp;quot;: &amp;quot;orders&amp;quot;,
      &amp;quot;mode&amp;quot;: &amp;quot;incrementing&amp;quot;,
      &amp;quot;incrementing.column.name&amp;quot;: &amp;quot;id&amp;quot;,
      &amp;quot;topic.prefix&amp;quot;: &amp;quot;postgres-&amp;quot;,
      &amp;quot;tasks.max&amp;quot;: &amp;quot;1&amp;quot;
    }
  }&amp;#39; \
  http://localhost:8083/connectors&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 설정으로 Postgres &lt;code&gt;orders&lt;/code&gt; 테이블의 새 행이 &lt;code&gt;postgres-orders&lt;/code&gt; topic으로 자동 들어간다.&lt;/p&gt;
&lt;h2&gt;REST API — connector 생명주기 관리&lt;/h2&gt;
&lt;p&gt;distributed 모드의 REST API(포트 8083)로 모든 관리를 한다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업&lt;/th&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;목록&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /connectors&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;상세&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /connectors/{name}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;상태&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /connectors/{name}/status&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;생성&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /connectors&lt;/code&gt; (JSON body)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;설정 변경&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT /connectors/{name}/config&lt;/code&gt; (JSON body)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;일시정지&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT /connectors/{name}/pause&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;재개&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT /connectors/{name}/resume&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;재시작&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /connectors/{name}/restart&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;task 재시작&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /connectors/{name}/tasks/{taskId}/restart&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;삭제&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE /connectors/{name}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;플러그인 목록&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /connector-plugins&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;상태 확인 예&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:8083/connectors/file-source-demo/status&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;{&amp;quot;name&amp;quot;:&amp;quot;file-source-demo&amp;quot;,&amp;quot;connector&amp;quot;:{&amp;quot;state&amp;quot;:&amp;quot;RUNNING&amp;quot;,&amp;quot;worker_id&amp;quot;:&amp;quot;localhost:8083&amp;quot;},
 &amp;quot;tasks&amp;quot;:[{&amp;quot;state&amp;quot;:&amp;quot;RUNNING&amp;quot;,&amp;quot;worker_id&amp;quot;:&amp;quot;localhost:8083&amp;quot;}]}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: &lt;code&gt;RUNNING&lt;/code&gt;/&lt;code&gt;FAILED&lt;/code&gt;/&lt;code&gt;PAUSED&lt;/code&gt;. FAILED 시 &lt;code&gt;trace&lt;/code&gt; 필드로 원인 확인.&lt;/p&gt;
&lt;h3&gt;일시정지·재개&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 일시정지 (데이터 이동 중단, connector는 유지)
curl -X PUT http://localhost:8083/connectors/file-source-demo/pause
# 재개
curl -X PUT http://localhost:8083/connectors/file-source-demo/resume&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;플러그인 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:8083/connector-plugins | jq &amp;#39;.[].class&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;plugin.path&lt;/code&gt;에 배치한 커넥터 JAR들이 보여야 한다.&lt;/p&gt;
&lt;h2&gt;커넥터 설치 — 플러그인 관리&lt;/h2&gt;
&lt;p&gt;Connect는 &lt;code&gt;plugin.path&lt;/code&gt;에 있는 JAR을 자동으로 인식한다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 커넥터 JAR을 plugin.path에 배치
cp my-connector.jar /usr/local/share/kafka/plugins/
# Connect 재기동 (또는 분산 모드면 자동 감지)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Confluent Hub&lt;/strong&gt;로 커넥터 설치:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;confluent-hub install confluentinc/kafka-connect-jdbc:latest
# → plugin.path에 자동 설치&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대표 커넥터:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debezium&lt;/strong&gt; (CDC: MySQL/Postgres 변경 감지)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JDBC Source/Sink&lt;/strong&gt; (DB ↔ Kafka)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Elasticsearch Sink&lt;/strong&gt; (Kafka → ES)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S3 Sink&lt;/strong&gt; (Kafka → S3)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FileStream&lt;/strong&gt; (파일 ↔ Kafka, 학습용)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;converter와 SMT — 메시지 변환&lt;/h2&gt;
&lt;h3&gt;converter&lt;/h3&gt;
&lt;p&gt;Connect는 내부적으로 &lt;code&gt;SourceRecord&lt;/code&gt;/&lt;code&gt;SinkRecord&lt;/code&gt; 모델을 쓴다. Kafka에 쓸 땐 converter가 바이트로 변환:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;JsonConverter&lt;/code&gt; — JSON 직렬화 (디버깅 쉬움).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AvroConverter&lt;/code&gt; — Avro + Schema Registry 연동 (장 09).&lt;br&gt;잘못된 converter는 consumer가 못 읽는 바이트를 만든다 — &amp;quot;Unknown magic byte!&amp;quot; 에러의 원인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SMT (Single Message Transform)&lt;/h3&gt;
&lt;p&gt;메시지 하나씩 변환(필드명 변경·타입 변환·timestamp 추가):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;transforms&amp;quot;: &amp;quot;RenameField&amp;quot;,
&amp;quot;transforms.RenameField.type&amp;quot;: &amp;quot;org.apache.kafka.connect.transforms.ReplaceField$Value&amp;quot;,
&amp;quot;transforms.RenameField.renames&amp;quot;: &amp;quot;[\&amp;quot;old_name:new_name\&amp;quot;]&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;복잡 집계·조인은 &lt;strong&gt;불가&lt;/strong&gt; → 그건 Kafka Streams(장 11).&lt;/p&gt;
&lt;h2&gt;실습 — FileStream 파이프라인 구성하기&lt;/h2&gt;
&lt;h3&gt;standalone으로 파일→Kafka→파일&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 입력 파일 준비
echo &amp;quot;hello connect&amp;quot; &amp;gt; /tmp/test.txt

# 2. Connect 기동 (file-source + file-sink 동시)
bin/connect-standalone.sh config/connect-standalone.properties \
  config/connect-file-source.properties \
  config/connect-file-sink.properties

# 3. test.txt에 줄 추가 → connect-test topic → 출력 파일에 나타남
echo &amp;quot;second line&amp;quot; &amp;gt;&amp;gt; /tmp/test.txt
cat /tmp/test.sink.txt  # 출력 확인&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;distributed로 REST API로 connector 관리&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Connect distributed 기동
bin/connect-distributed.sh config/connect-distributed.properties

# 2. REST로 connector 생성 (위 JSON 예 참조)

# 3. 상태 확인
curl http://localhost:8083/connectors

# 4. 파일에 데이터 추가하고 topic/출력 파일 확인&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;Connect는 Kafka core가 아니다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;core 포함&lt;/strong&gt;. Schema Registry만 별도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;standalone으로 프로덕션 해도 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;단일 장애점. 프로덕션은 distributed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;converter는 아무거나 해도 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;source/consumer 호환 포맷이어야&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;SMT로 복잡 집계를 한다&amp;quot;&lt;/td&gt;
&lt;td&gt;SMT는 메시지 단위 경량 변환만. 집계는 Streams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;task 수는 많을수록 좋다&amp;quot;&lt;/td&gt;
&lt;td&gt;source/sink 대역폭·파티션 수에 bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;내부 topic은 삭제해도 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connect-offsets&lt;/code&gt;/&lt;code&gt;configs&lt;/code&gt;/&lt;code&gt;status&lt;/code&gt; 삭제 금지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DLQ (Dead Letter Queue)&lt;/strong&gt;: 처리 실패 메시지를 별도 topic(&lt;code&gt;errors.deadletterqueue.topic.name&lt;/code&gt;)으로 보내 분석.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exactly-once Connect&lt;/strong&gt;: source 커넥터는 at-least-once가 기본. sink는 멱등 쓰기 권장.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;커넥터 개발&lt;/strong&gt;: &lt;code&gt;Connector&lt;/code&gt; + &lt;code&gt;Task&lt;/code&gt; 인터페이스 구현으로 커스텀 커넥터 작성 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kafka Connect&lt;/strong&gt; = 코드 없는 데이터 이동 프레임워크(&lt;strong&gt;Kafka core 포함&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;source&lt;/strong&gt;(외부→Kafka) / &lt;strong&gt;sink&lt;/strong&gt;(Kafka→외부). connector가 작업을 task로 분할.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;worker 설정&lt;/strong&gt;: standalone(개발, 파일에 상태) / &lt;strong&gt;distributed&lt;/strong&gt;(프로덕션, 내부 topic에 상태, REST 8083).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;connector 구성&lt;/strong&gt;: properties 파일(standalone) 또는 REST API + JSON(distributed).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;REST API CRUD&lt;/strong&gt;: 생성·상태·일시정지·재개·재시작·삭제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;converter&lt;/strong&gt;: wire format 결정. SMT는 메시지 단위 변환.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;플러그인&lt;/strong&gt;: &lt;code&gt;plugin.path&lt;/code&gt;에 JAR 배치 또는 Confluent Hub 설치.&lt;/li&gt;
&lt;li&gt;Connect(데이터 &lt;strong&gt;이동&lt;/strong&gt;) ≠ Streams(데이터 &lt;strong&gt;가공&lt;/strong&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;source 커넥터와 sink 커넥터의 차이를 설명하라.&lt;/li&gt;
&lt;li&gt;standalone과 distributed의 차이. 프로덕션은 왜 distributed인가?&lt;/li&gt;
&lt;li&gt;REST API로 connector를 생성하는 JSON을 직접 작성해 보라.&lt;/li&gt;
&lt;li&gt;converter를 잘못 설정하면 어떤 일이 벌어지는가?&lt;/li&gt;
&lt;li&gt;SMT가 할 수 있는 것과 할 수 없는 것은? 안 되는 건 무엇으로 푸는가?&lt;/li&gt;
&lt;li&gt;distributed 모드의 내부 topic(connect-offsets 등)이 왜 중요한가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/kafka-connect/user-guide.md&quot;&gt;Kafka Connect — User Guide&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/kafka-connect/connector-development-guide.md&quot;&gt;Connector Development Guide&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/getting-started/quickstart.md&quot;&gt;Quickstart — FileStream 파이프라인&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>connect</category>
      <category>Connector</category>
      <category>converter</category>
      <category>kafka</category>
      <category>rest-api</category>
      <category>Sink</category>
      <category>Source</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/21</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-10-kafka-connect#entry21comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:46:33 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 09. schema registry</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-09-schema-registry</link>
      <description>&lt;h1&gt;Schema Registry — 메시지 구조를 안전하게 바꾸는 방법&lt;/h1&gt;
&lt;p&gt;producer가 &lt;code&gt;{&amp;quot;id&amp;quot;:1,&amp;quot;name&amp;quot;:&amp;quot;alice&amp;quot;}&lt;/code&gt;를 보내고 consumer가 읽는다. 그런데 producer가 필드를 추가했다 — &lt;code&gt;{&amp;quot;id&amp;quot;:1,&amp;quot;name&amp;quot;:&amp;quot;alice&amp;quot;,&amp;quot;email&amp;quot;:&amp;quot;...&amp;quot;}&lt;/code&gt;. 옛 consumer는 이걸 못 읽고 크래시난다. &lt;strong&gt;Kafka는 value를 바이트로만 저장하고 구조(schema)를 모른다.&lt;/strong&gt; producer와 consumer가 각자 해석하다가 구조가 어긋나면 깨진다. 이걸 푸는 게 &lt;strong&gt;Schema Registry&lt;/strong&gt;다.&lt;/p&gt;
&lt;p&gt;이 글은 Schema Registry가 뭔지부터 시작해, &lt;strong&gt;실제로 어떻게 설정하고 사용하는지&lt;/strong&gt;(Schema Registry 기동, Avro schema 정의, producer/consumer serde 설정, 호환성 검사)까지 다룬다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;중요 전제&lt;/strong&gt;: Schema Registry는 &lt;strong&gt;Confluent 확장&lt;/strong&gt;이며 &lt;strong&gt;Apache Kafka core가 아니다.&lt;/strong&gt; 별도 설치가 필요하다.&lt;/p&gt;
&lt;h2&gt;Kafka가 스키마를 모른다는 것 — 왜 문제인가&lt;/h2&gt;
&lt;p&gt;Kafka는 value를 바이트로만 저장한다. 그래서:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스키마(필드명·타입)는 producer/consumer &lt;strong&gt;코드에만&lt;/strong&gt; 존재.&lt;/li&gt;
&lt;li&gt;스키마 변경(필드 추가/삭제)이 양쪽에 동시 적용되지 않으면 consumer 크래시.&lt;/li&gt;
&lt;li&gt;&amp;quot;스키마 안 바꾸면 되지&amp;quot; → 현실에선 불가능(요구사항 변화). &lt;strong&gt;안전하게 바꾸는 규칙&lt;/strong&gt;이 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Schema Registry란 — 스키마를 별도로 두고 진화 규칙을 강제&lt;/h2&gt;
&lt;p&gt;Schema Registry는 &lt;strong&gt;스키마를 저장·버전 관리·호환성 검사하는 외부 서비스&lt;/strong&gt;다(기본 포트 8081). 메시지를 보낼 때:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;producer가 스키마를 Registry에 등록.&lt;/li&gt;
&lt;li&gt;Registry가 호환성을 검사 → 통과하면 &lt;strong&gt;schema ID&lt;/strong&gt; 발급.&lt;/li&gt;
&lt;li&gt;메시지엔 schema ID만 붙여 보냄(&lt;code&gt;0x00 + ID + payload&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;consumer가 ID로 Registry에서 스키마를 조회해 역직렬화.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 하면 &amp;quot;새 스키마가 옛 consumer를 깨뜨리는지&amp;quot;를 &lt;strong&gt;등록 시점에 검사&lt;/strong&gt;해서, 불호환 스키마 등록 자체를 거부한다. 단순 문서화가 아니라 &lt;strong&gt;운영 가드레일&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h3&gt;Avro — 스키마와 데이터를 분리하는 포맷&lt;/h3&gt;
&lt;p&gt;Avro의 핵심은 &lt;strong&gt;스키마와 데이터 분리&lt;/strong&gt;다. 데이터 자체엔 필드명이 안 들어감(스키마로 해석) → 크기 작음. 진화 규칙(필드 default 등)을 지원한다.&lt;/p&gt;
&lt;h2&gt;Schema Registry 설정 — 어떻게 기동하나&lt;/h2&gt;
&lt;p&gt;Schema Registry는 Confluent 확장이라 Kafka core 배포판에 없다. Docker로 기동하는 게 가장 쉽다:&lt;/p&gt;
&lt;h3&gt;Docker로 Schema Registry 기동&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka(KRaft)가 이미 localhost:9092에서 기동 중이라 가정

# Schema Registry 컨테이너 기동
docker run -d --name schema-registry \
  -p 8081:8081 \
  -e SCHEMA_REGISTRY_HOST_NAME=localhost \
  -e SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=PLAINTEXT://host.docker.internal:9092 \
  -e SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081 \
  confluentinc/cp-schema-registry:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 Confluent Platform 전체를 Docker Compose로:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# docker-compose.yml (발췌)
services:
  schema-registry:
    image: confluentinc/cp-schema-registry:latest
    ports:
      - &amp;quot;8081:8081&amp;quot;
    environment:
      SCHEMA_REGISTRY_HOST_NAME: schema-registry
      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: &amp;quot;kafka:9092&amp;quot;
      SCHEMA_REGISTRY_LISTENERS: &amp;quot;http://0.0.0.0:8081&amp;quot;
    depends_on:
      - kafka&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;기동 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:8081/subjects
# [] (빈 배열 — 아직 등록된 스키마 없음)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Avro 스키마 정의 — 메시지 구조를 JSON으로&lt;/h2&gt;
&lt;p&gt;Avro 스키마는 JSON으로 정의한다. &lt;code&gt;User&lt;/code&gt; 레코드 예:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;record&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;User&amp;quot;,
  &amp;quot;namespace&amp;quot;: &amp;quot;com.example&amp;quot;,
  &amp;quot;fields&amp;quot;: [
    {&amp;quot;name&amp;quot;: &amp;quot;id&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;int&amp;quot;},
    {&amp;quot;name&amp;quot;: &amp;quot;name&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;},
    {&amp;quot;name&amp;quot;: &amp;quot;email&amp;quot;, &amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;, &amp;quot;default&amp;quot;: &amp;quot;&amp;quot;}
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;type: record&lt;/code&gt; — Avro 레코드(구조체).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fields&lt;/code&gt; — 필드 배열. 각 필드는 이름·타입·(선택)default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;default: &amp;quot;&amp;quot;&lt;/code&gt; — 이 필드가 없는 옛 데이터를 읽을 때 기본값 사용 → &lt;strong&gt;호환성의 핵심&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;스키마 등록과 호환성 검사&lt;/h2&gt;
&lt;h3&gt;스키마 등록&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# v1 등록
curl -X POST -H &amp;quot;Content-Type: application/vnd.schemaregistry.v1+json&amp;quot; \
  --data &amp;#39;{&amp;quot;schema&amp;quot;: &amp;quot;{\&amp;quot;type\&amp;quot;:\&amp;quot;record\&amp;quot;,\&amp;quot;name\&amp;quot;:\&amp;quot;User\&amp;quot;,\&amp;quot;fields\&amp;quot;:[{\&amp;quot;name\&amp;quot;:\&amp;quot;id\&amp;quot;,\&amp;quot;type\&amp;quot;:\&amp;quot;int\&amp;quot;},{\&amp;quot;name\&amp;quot;:\&amp;quot;name\&amp;quot;,\&amp;quot;type\&amp;quot;:\&amp;quot;string\&amp;quot;}]}&amp;quot;}&amp;#39; \
  http://localhost:8081/subjects/users-value/versions
# → {&amp;quot;id&amp;quot;: 1}  (schema ID 발급)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;호환성 위반 시나리오&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;v1: &lt;code&gt;{id: int, name: string}&lt;/code&gt; (email 없음).&lt;/li&gt;
&lt;li&gt;v2 시도: &lt;code&gt;{id: int, name: string, email: string}&lt;/code&gt;(&lt;strong&gt;default 없음&lt;/strong&gt;) → &lt;strong&gt;BACKWARD 위반&lt;/strong&gt; → Registry가 &lt;strong&gt;거부&lt;/strong&gt;(409 Conflict).&lt;/li&gt;
&lt;li&gt;v2&amp;#39;: &lt;code&gt;{id: int, name: string, email: string, default: &amp;quot;&amp;quot;}&lt;/code&gt; → default로 BACKWARD 호환 → &lt;strong&gt;등록 성공&lt;/strong&gt;(새 ID 발급).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 &amp;quot;등록 시점 검사&amp;quot;가 단순 문서화가 아니라 &lt;strong&gt;운영 가드레일&lt;/strong&gt;인 이유다.&lt;/p&gt;
&lt;h3&gt;호환성 모드&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;전제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BACKWARD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;새 스키마가 &lt;strong&gt;옛 데이터&lt;/strong&gt;를 읽을 수 있음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;흔한 기본&lt;/strong&gt;. consumer 먼저 업그레이드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FORWARD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;옛 consumer가 &lt;strong&gt;새 데이터&lt;/strong&gt;를 읽을 수 있음&lt;/td&gt;
&lt;td&gt;producer 먼저 업그레이드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FULL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;backward + forward&lt;/td&gt;
&lt;td&gt;가장 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NONE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;검사 없음&lt;/td&gt;
&lt;td&gt;위험&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;subject 명명 전략&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TopicNameStrategy&lt;/strong&gt;(기본): &lt;code&gt;&amp;lt;topic&amp;gt;-key&lt;/code&gt; / &lt;code&gt;&amp;lt;topic&amp;gt;-value&lt;/code&gt;. 한 topic = 한 스키마 타입.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RecordNameStrategy&lt;/strong&gt;: Avro record 이름 기준. 한 topic에 여러 타입 허용(이벤트 소싱에 유리).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;producer/consumer Avro serde 설정&lt;/h2&gt;
&lt;p&gt;Schema Registry를 쓰려면 producer와 consumer가 Avro serde를 써야 한다.&lt;/p&gt;
&lt;h3&gt;producer 설정 (Java)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;# Avro 직렬화 + Schema Registry
value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer
schema.registry.url=http://localhost:8081

# (선택) 등록 시 호환성 확인 안 함(개발용)
auto.register.schemas=true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;producer가 &lt;code&gt;send()&lt;/code&gt;할 때 Avro 객체를 직렬화하며 자동으로 Schema Registry에 스키마를 등록/조회한다. 메시지엔 &lt;code&gt;0x00(magic byte) + schema ID(4바이트) + Avro payload&lt;/code&gt;가 붙는다.&lt;/p&gt;
&lt;h3&gt;consumer 설정 (Java)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-properties&quot;&gt;value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer
schema.registry.url=http://localhost:8081
specific.avro.reader=true  # GenericRecord 대신 생성된 클래스 사용&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Maven 의존성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.confluent&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;kafka-avro-serializer&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;7.7.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.apache.avro&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;avro&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.11.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;strong&gt;serde 불일치 주의&lt;/strong&gt;: Schema Registry serializer로 쓰고 plain deserializer로 읽으면(또는 역) &amp;quot;Unknown magic byte!&amp;quot; 에러. 양쪽 모두 Avro serde + 같은 Schema Registry여야 한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;전체 워크플로우 — schema에서 consume까지&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;스키마 정의&lt;/strong&gt;: Avro JSON으로 &lt;code&gt;User&lt;/code&gt; 레코드 작성(default 필드 포함).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema Registry 기동&lt;/strong&gt;: Docker로 localhost:8081에 기동.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스키마 등록&lt;/strong&gt;: &lt;code&gt;curl POST /subjects/users-value/versions&lt;/code&gt; → schema ID 발급.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;producer 설정&lt;/strong&gt;: &lt;code&gt;KafkaAvroSerializer&lt;/code&gt; + &lt;code&gt;schema.registry.url&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;produce&lt;/strong&gt;: producer가 Avro 객체를 보내면 자동 직렬화 + ID 부여.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;consumer 설정&lt;/strong&gt;: &lt;code&gt;KafkaAvroDeserializer&lt;/code&gt; + 같은 Registry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;consume&lt;/strong&gt;: consumer가 ID로 스키마 조회 → 역직렬화 → Avro 객체 획득.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스키마 진화&lt;/strong&gt;: v2에 default 필드 추가 → BACKWARD 호환 확인 → 등록.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;Schema Registry는 Kafka core의 일부&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Confluent 확장&lt;/strong&gt;. ASF 배포판에 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Avro는 압축 포맷&amp;quot;&lt;/td&gt;
&lt;td&gt;아님. 스키마 기반 직렬화 포맷&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;스키마 바꾸면 무조건 깨진다&amp;quot;&lt;/td&gt;
&lt;td&gt;호환성 모드 + 진화 규칙 지키면 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;magic byte는 장식&amp;quot;&lt;/td&gt;
&lt;td&gt;wire format 식별자. plain serde와 섞으면 &amp;quot;Unknown magic byte!&amp;quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Schema Registry 없이 Avro 써도 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;가능은 하나 schema ID/호환성 검사 불가 → 진화 관리가 수동&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Confluent wire format&lt;/strong&gt;: &lt;code&gt;0x00&lt;/code&gt; + schema ID(4B) + payload. 모든 메시지에 붙음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;_TRANSITIVE&lt;/code&gt; 모드&lt;/strong&gt;: 직전 버전뿐 아니라 모든 과거 버전과 호환.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avro 진화 규칙&lt;/strong&gt;: 필드 추가→default 필수, 타입 widening(int→long)만, 필드명 변경은 비호환(alias로 일부 완화).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비-Confluent 대안&lt;/strong&gt;: Apicurio Registry, AWS Glue Schema Registry.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kafka = value를 바이트로만 저장&lt;/strong&gt;. 스키마 모름 → 진화 문제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema Registry&lt;/strong&gt;(Confluent 확장, 비 core): 스키마 저장·버전·&lt;strong&gt;호환성 강제&lt;/strong&gt;(등록 시점 검사).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설정&lt;/strong&gt;: Docker로 기동(포트 8081), Kafka broker에 연결.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avro&lt;/strong&gt;: 스키마-데이터 분리(JSON 스키마 정의). 크기 작음. 진화 규칙(default).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;producer/consumer&lt;/strong&gt;: &lt;code&gt;KafkaAvroSerializer&lt;/code&gt;/&lt;code&gt;Deserializer&lt;/code&gt; + &lt;code&gt;schema.registry.url&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;호환성&lt;/strong&gt;: BACKWARD(흔한 기본)/FORWARD/FULL/NONE. 필드 추가 시 default 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;운영 표준&lt;/strong&gt;: BACKWARD + consumer 먼저 배포 + default 필수.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Kafka가 value를 바이트로만 본다는 것이 왜 스키마 문제를 만드는가?&lt;/li&gt;
&lt;li&gt;Schema Registry를 Docker로 기동하는 방법을 설명하라.&lt;/li&gt;
&lt;li&gt;Avro 스키마에서 필드 추가 시 default가 필요한 이유는?&lt;/li&gt;
&lt;li&gt;producer/consumer에 Avro serde를 설정하려면 어떤 프로퍼티가 필요한가?&lt;/li&gt;
&lt;li&gt;&amp;quot;Unknown magic byte!&amp;quot; 에러가 발생하는 원인과 해결책은?&lt;/li&gt;
&lt;li&gt;Schema Registry가 ASF core가 아닌 것이 왜 중요한가? 대안은?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.conduktor.io/kafka/what-is-apache-kafka&quot;&gt;Conduktor — Schema registry&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/schema-registry/index.html&quot;&gt;Confluent Schema Registry docs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/clients/serdes/index.html&quot;&gt;Confluent — Avro serializer 설정&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;Avro spec(avro.apache.org), Protobuf, JSON Schema&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>avro</category>
      <category>compatibility</category>
      <category>confluent</category>
      <category>kafka</category>
      <category>schema-registry</category>
      <category>serde</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/20</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-09-schema-registry#entry20comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:45:46 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 08. delivery semantics</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-08-delivery-semantics</link>
      <description>&lt;h1&gt;&amp;quot;Kafka는 exactly-once인가요&amp;quot; — 질문이 틀렸다&lt;/h1&gt;
&lt;p&gt;&amp;quot;Kafka는 exactly-once를 보장하나요?&amp;quot; — Kafka를 배우면 누구나 받는 질문이다. 답은 &amp;quot;무엇의 exactly-once인지에 따라 다르다&amp;quot;다. 단일 partition 쓰기(idempotent)는 exactly-once다. consume-process-produce 패턴(EOS)도 exactly-once다. 하지만 시스템 외부(DB)까지는 아니다. 이 질문이 틀린 이유는 &amp;quot;exactly-once&amp;quot;를 메시지 자체의 마법 같은 속성으로 생각하기 때문이다. 실제로는 &lt;strong&gt;&amp;quot;출력 메시지와 consumer offset을 한 트랜잭션으로 묶어 원자 commit&amp;quot;&lt;/strong&gt;하는 설계 패턴의 결과다.&lt;/p&gt;
&lt;p&gt;이 글은 Kafka의 세 가지 전달 보장 — at-most-once, at-least-once(기본), exactly-once — 의 정의, 달성 방법, 그리고 정확한 &lt;strong&gt;경계&lt;/strong&gt;를 다룬다. 중복을 그냥 두면 결제가 두 번 일어나고, 손실을 그냥 두면 주문이 사라진다. 전달 보장의 경계를 정확히 아는 것이 신뢰성 설계의 출발점이다.&lt;/p&gt;
&lt;h2&gt;세 가지 전달 보장 — 중복과 손실의 트레이드오프&lt;/h2&gt;
&lt;p&gt;택배에 비유하면 감이 온다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;at-most-once&lt;/strong&gt;: 문 앞에 두고 간다(비가 와도 책임 안 짐). 잃을 수 있지만 중복은 안 된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;at-least-once&lt;/strong&gt;: 수령증 받을 때까지 다시 온다. 같은 소포가 두 번 올 수 있다(중복).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exactly-once&lt;/strong&gt;: 수령증(출력 메시지)과 배송 완료 등록(offset commit)을 &lt;strong&gt;한 장부에 동시에&lt;/strong&gt; 적는다. 둘 중 하나만 일어나는 일은 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;보장&lt;/th&gt;
&lt;th&gt;어떻게&lt;/th&gt;
&lt;th&gt;Kafka 구현&lt;/th&gt;
&lt;th&gt;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;at-most-once&lt;/td&gt;
&lt;td&gt;확인 안 함, 재시도 안 함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acks=0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;at-least-once&lt;/td&gt;
&lt;td&gt;확인하고, 실패 시 재시도(중복 가능)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acks=all&lt;/code&gt; + 재시도, &lt;strong&gt;Kafka 기본&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;exactly-once&lt;/td&gt;
&lt;td&gt;중복 제거 + 원자 쓰기&lt;/td&gt;
&lt;td&gt;idempotent producer + transactions + read_committed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;높음&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Kafka의 전달 보장은 버전을 거치며 발전했다 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;(design.md — Message Delivery Semantics)&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;0.11 이전&lt;/strong&gt;: producer는 at-least-once(네트워크 에러 시 재전송 → 중복 가능).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0.11.0.0부터&lt;/strong&gt;: idempotent delivery(PID + sequence로 중복 제거) → 단일 partition exactly-once. transactions → 여러 partition 원자 쓰기.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;전달 보장 발전사 — 타임라인으로 보는 진화&lt;/h3&gt;
&lt;p&gt;이 발전을 시간순으로 보면 각 단계가 &amp;quot;이전의 어떤 갭을 메웠는지&amp;quot; 보인다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    A[&amp;quot;0.11 이전&amp;lt;br/&amp;gt;at-least-once만&amp;lt;br/&amp;gt;(중복 가능)&amp;quot;] --&amp;gt; B[&amp;quot;0.11 (2017)&amp;lt;br/&amp;gt;idempotent producer&amp;lt;br/&amp;gt;(단일 partition 중복 제거)&amp;quot;]
    B --&amp;gt; C[&amp;quot;0.11&amp;lt;br/&amp;gt;transactions&amp;lt;br/&amp;gt;(여러 partition 원자)&amp;quot;]
    C --&amp;gt; D[&amp;quot;2.5+&amp;lt;br/&amp;gt;exactly_once_v2 (KIP-447)&amp;lt;br/&amp;gt;(consume-process-produce EOS)&amp;quot;]
    D --&amp;gt; E[&amp;quot;3.0+&amp;lt;br/&amp;gt;idempotence 기본 true&amp;quot;]
    E --&amp;gt; F[&amp;quot;4.x&amp;lt;br/&amp;gt;KRaft + EOS 안정화&amp;quot;]&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;해결한 갭&lt;/th&gt;
&lt;th&gt;남은 한계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;at-least-once&lt;/td&gt;
&lt;td&gt;(기준선)&lt;/td&gt;
&lt;td&gt;중복&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;idempotent producer&lt;/td&gt;
&lt;td&gt;단일 partition 중복&lt;/td&gt;
&lt;td&gt;여러 partition은 미지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transactions&lt;/td&gt;
&lt;td&gt;여러 partition 원자&lt;/td&gt;
&lt;td&gt;consumer offset과 결합은 별도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EOS v2&lt;/td&gt;
&lt;td&gt;consume-process-produce 원자&lt;/td&gt;
&lt;td&gt;외부 시스템은 미지원, 성능 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;이 타임라인이 &amp;quot;Kafka는 exactly-once인가&amp;quot;라는 질문이 왜 범위를 정해야 하는지 보여준다 — &amp;quot;exactly-once&amp;quot;가 버전마다 의미하는 바가 달랐고, 각 단계가 좁은 범위의 정확성을 추가해왔다. 4.x의 EOS도 &amp;quot;같은 클러스터 내 consume-process-produce&amp;quot;에 한정된다.&lt;/p&gt;
&lt;h2&gt;exactly-once의 정확한 범위 — 오해 주의&lt;/h2&gt;
&lt;p&gt;&amp;quot;exactly-once&amp;quot;가 어디까지 보장인지를 정확히 해야 한다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;단일 partition 쓰기&lt;/strong&gt;: idempotent producer로 exactly-once(장 03). 4.x에선 기본 동작.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;consume-process-produce&lt;/strong&gt;(consumer가 읽고 처리하고 다른 topic에 쓰는 패턴): &lt;strong&gt;EOS&lt;/strong&gt;(Exactly-Once Semantics). 출력 메시지 + 입력 offset을 &lt;strong&gt;같은 트랜잭션&lt;/strong&gt;으로 묶어 commit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시스템 외부(DB 등)&lt;/strong&gt;: Kafka만으로는 보장 못 함. 외부 시스템도 트랜잭션 참여 또는 멱등 처리 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&amp;quot;Kafka는 exactly-once인가&amp;quot;가 틀린 질문인 이유 — 범위를 안 정하고 묻기 때문. 단일 partition 쓰기는 이미 exactly-once(4.x 기본), consume-process-produce는 EOS로 가능, DB 쓰기는 불가.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;EOS — consume-process-produce에서 exactly-once가 어떻게 나오나&lt;/h2&gt;
&lt;p&gt;여기서 핵심 질문: consume-process-produce에서 &amp;quot;출력과 offset을 한 트랜잭션으로&amp;quot;가 왜 exactly-once를 만드는가?&lt;/p&gt;
&lt;p&gt;exactly-once는 메시지 자체의 마법 같은 속성이 &lt;strong&gt;아니다&lt;/strong&gt;. &lt;strong&gt;&amp;quot;출력 메시지가 commit됐으면 입력 offset도 commit됐고, 입력 offset이 commit됐으면 출력도 commit됐다&amp;quot;&lt;/strong&gt;는 원자성에서 나온다. 둘 중 하나만 일어나는 상태를 원천 차단한다.&lt;/p&gt;
&lt;h3&gt;EOS의 세 기둥&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;consumer partition 배타 소유&lt;/strong&gt;: 한 consumer group 내 partition은 한 consumer만(장 05). 같은 입력을 두 consumer가 동시에 처리하는 일 없음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;producer 트랜잭션&lt;/strong&gt;: 출력 record 생산 + consumer offsets 갱신을 &lt;strong&gt;하나의 트랜잭션&lt;/strong&gt;으로 원자 commit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;consumer마다 producer 1개&lt;/strong&gt;: rebalance 시 소유권 이전을 정확히 처리.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;consume-process-produce 흐름&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant C as consumer (read_committed)
    participant T as transaction
    participant O as output topic
    participant OS as __consumer_offsets
    C-&amp;gt;&amp;gt;C: poll() → records
    C-&amp;gt;&amp;gt;T: beginTransaction()
    C-&amp;gt;&amp;gt;O: send(출력 메시지들)
    C-&amp;gt;&amp;gt;OS: sendOffsetsToTransaction(offsets, groupMetadata)
    C-&amp;gt;&amp;gt;T: commitTransaction()  // 출력과 offset이 원자적으로 commit
    Note over O,OS: 둘 다 commit 되거나 둘 다 abort — 중간 상태 없음&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;코드 패턴 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/examples/src/main/java/kafka/examples/ExactlyOnceMessageProcessor.java&quot;&gt;(ExactlyOnceMessageProcessor.java)&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;producer.initTransactions();          // 좀비 차단
consumer.subscribe(...);
while (running) {
    records = consumer.poll(...);
    producer.beginTransaction();
    for (record : records) producer.send(transform(record));
    producer.sendOffsetsToTransaction(offsets, consumer.groupMetadata());
    producer.commitTransaction();      // 원자 commit
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;좀비 차단 — transactional.id가 왜 필요한가&lt;/h2&gt;
&lt;p&gt;producer가 크래시 났는데 사실은 살아있어서(좀비) 같은 데이터를 다시 쓰면 중복이 생긴다. 이걸 막는 것이 &lt;strong&gt;&lt;code&gt;transactional.id&lt;/code&gt;&lt;/strong&gt;다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;initTransactions()&lt;/code&gt; 시 broker가 이 &lt;code&gt;transactional.id&lt;/code&gt;의 &lt;strong&gt;epoch를 올린다&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;이전 epoch(좀비)의 producer가 쓰려 하면 &lt;strong&gt;&lt;code&gt;ProducerFencedException&lt;/code&gt;&lt;/strong&gt; → 좀비 격리.&lt;/li&gt;
&lt;li&gt;이것이 &amp;quot;재시작해도 안전&amp;quot;을 보장하는 핵심.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;transactional.id&lt;/code&gt;는 안정적이고 고유해야 한다 — 재시작해도 같은 값이 유지되어야 좀비 차단이 작동한다. 아무 값이나 쓰면 안 된다.&lt;/p&gt;
&lt;h2&gt;isolation.level=read_committed — abort된 트랜잭션을 안 보게&lt;/h2&gt;
&lt;p&gt;EOS consumer는 &lt;code&gt;isolation.level=read_committed&lt;/code&gt;를 써야 한다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;consumer가 &lt;strong&gt;commit된 트랜잭션만&lt;/strong&gt; 읽는다. abort된 것은 보이지 않는다.&lt;/li&gt;
&lt;li&gt;EOS에서 offset은 트랜잭션으로 commit되므로 &lt;code&gt;enable.auto.commit=false&lt;/code&gt; 필수(자동 commit이 트랜잭션을 우회하면 안 됨).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;read_committed는 성능에 영향이 있다 — abort 처리·LISO(last stable offset) 추적으로 지연이 생긴다. 필요한 곳에만 쓴다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;LSO(last stable offset) — read_committed가 멈추는 지점&lt;/h3&gt;
&lt;p&gt;read_committed consumer는 &lt;strong&gt;LSO(last stable offset)&lt;/strong&gt;까지만 읽을 수 있다. LSO는 &amp;quot;아직 진행 중이거나 abort 안 된 가장 오래된 트랜잭션의 시작점&amp;quot; 전까지다. 즉, 한 트랜잭션이 진행 중이면 그 이후의 commit된 메시지도 consumer는 LSO 때문에 못 본다(잠시 대기).&lt;/p&gt;
&lt;p&gt;왜 이렇게까지 하는가 — abort될 수 있는 트랜잭션의 데이터를 consumer에게 보였다가 나중에 &amp;quot;사실 취소&amp;quot;할 수 없기 때문. LSO는 &amp;quot;이 지점까지는 모든 트랜잭션이 결정났다(또는 abort 처리됐다)&amp;quot;는 안전 경계다. 장기 실행 트랜잭션이 LSO를 멈추게 하므로(지연 유발), &lt;code&gt;transaction.timeout.ms&lt;/code&gt;로 트랜잭션 시간 상한을 둬야 한다 — 안 두면 하나의 느린 트랜잭션이 전체 consumer 지연을 만든다.&lt;/p&gt;
&lt;h2&gt;EOS 실패 모드 — 트랜잭션 중간에 크래시 나면?&lt;/h2&gt;
&lt;p&gt;EOS의 진짜 가치는 &lt;strong&gt;실패 상황에서 정확성을 유지&lt;/strong&gt;하는 데 있다. consumer가 트랜잭션 중간에 크래시 나는 시나리오를 보자:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[&amp;quot;beginTransaction&amp;quot;] --&amp;gt; B[&amp;quot;send(출력들)&amp;quot;]
    B --&amp;gt; C[&amp;quot;sendOffsetsToTransaction&amp;quot;]
    C --&amp;gt; D{&amp;quot;commitTransaction 도달?&amp;quot;}
    D --&amp;gt;|O 크래시 전| E[&amp;quot;트랜잭션 미완료 → abort 타임아웃 → 출력 안 보임(read_committed)&amp;quot;]
    D --&amp;gt;|O commit 직후 크래시| F[&amp;quot;commit 성공 → 출력+offset 영속 → 재시작 시 다음부터&amp;quot;]
    E --&amp;gt; G[&amp;quot;재시작: offset 안 넘어감 → 입력 재처리(at-least-once) → EOS 보장&amp;quot;]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;핵심: 트랜잭션이 &lt;strong&gt;commit되지 않고 끝나면&lt;/strong&gt;(크래시·타임아웃) broker가 그 트랜잭션을 &lt;strong&gt;abort&lt;/strong&gt;로 처리 → 출력 메시지는 read_committed consumer에게 안 보인다 → 마치 처음부터 없었던 것처럼. consumer offset도 commit 안 됐으므로, 재시작 시 같은 입력을 다시 처리한다(at-least-once 재시도). 결국 &amp;quot;모두 일어나거나 아무것도 안 일어나거나&amp;quot;가 보장된다.&lt;/p&gt;
&lt;h3&gt;transaction.timeout.ms — 좀비·지연 방지&lt;/h3&gt;
&lt;p&gt;트랜잭션에 시간 상한(&lt;code&gt;transaction.timeout.ms&lt;/code&gt;, 기본 60초)을 둔다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;producer가 이 시간 내에 commit/abort 안 하면 broker가 강제 abort → 장기 미완료 트랜잭션이 LSO를 멈추게 하는 걸 방지.&lt;/li&gt;
&lt;li&gt;좀비 producer도 같은 원리로 차단 — &lt;code&gt;transactional.id&lt;/code&gt;의 epoch가 올라가면 옛 epoch의 트랜잭션은 abort.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;좀비 차단 worked example — zombie fencing이 어떻게 동작하는가&lt;/h2&gt;
&lt;p&gt;좀비 시나리오를 구체적으로 보자. producer &lt;code&gt;P1&lt;/code&gt;(&lt;code&gt;transactional.id=order-processor-1&lt;/code&gt;)이 트랜잭션 도중 네트워크 분단으로 &amp;quot;죽은 줄 알았는데 살아있다(좀비)&amp;quot;. 한편 같은 &lt;code&gt;transactional.id&lt;/code&gt;를 가진 &lt;code&gt;P1&amp;#39;&lt;/code&gt;가 재시작됐다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;P1&amp;#39;&lt;/code&gt;가 &lt;code&gt;initTransactions()&lt;/code&gt; 호출 → broker가 &lt;code&gt;order-processor-1&lt;/code&gt;의 &lt;strong&gt;epoch를 5→6으로 올림&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;좀비 &lt;code&gt;P1&lt;/code&gt;(epoch=5)이 늦게 commit 시도 → broker가 &lt;strong&gt;&lt;code&gt;ProducerFencedException&lt;/code&gt;&lt;/strong&gt; 발생 → &lt;code&gt;P1&lt;/code&gt; 격리.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P1&amp;#39;&lt;/code&gt;(epoch=6)만 정상 동작 → 같은 데이터 두 번 쓰는 일 차단.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이게 &lt;code&gt;transactional.id&lt;/code&gt;가 &amp;quot;안정적이고 고유&amp;quot;해야 하는 이유다 — 같은 논리 역할(예: &lt;code&gt;order-processor-1&lt;/code&gt;)의 재시작은 같은 id를 써야 좀비가 잡힌다. 반대로 서로 다른 역할이 같은 id를 쓰면 서로를 fence해버리는 사고가 난다. 운영에선 id 할당 체계를 명확히 설계해야 한다.&lt;/p&gt;
&lt;h2&gt;at-least-once vs exactly-once — 비용 비교&lt;/h2&gt;
&lt;p&gt;&amp;quot;왜 모든 곳에 EOS를 안 쓰나&amp;quot; — 비용 때문이다. 두 보장의 실사용 비교:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;at-least-once (&lt;code&gt;acks=all&lt;/code&gt;+재시도)&lt;/th&gt;
&lt;th&gt;EOS (transactional + read_committed)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;처리량&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;낮음&lt;/strong&gt;(트랜잭션 오버헤드 10 ~ 20%+ 추정, 환경 의존)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지연&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;높음(LSO 대기·commit 비용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구현 복잡도&lt;/td&gt;
&lt;td&gt;낮음(재시도만)&lt;/td&gt;
&lt;td&gt;높음(transactional API·좀비 처리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중복 가능성&lt;/td&gt;
&lt;td&gt;있음(애플리케이션 멱등 설계로 완화)&lt;/td&gt;
&lt;td&gt;없음(단일 클러스터 내)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합&lt;/td&gt;
&lt;td&gt;로그·메트릭·멱등 처리 가능한 곳&lt;/td&gt;
&lt;td&gt;결제·주문·정확성 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;그래서 &amp;quot;무조건 EOS&amp;quot;가 정답이 아니다. 애플리케이션이 멱등 처리(DB upsert 등)로 at-least-once 중복을 흡수할 수 있으면, at-least-once가 훨씬 싸다. EOS는 정말 &amp;quot;정확히 한 번&amp;quot;이 요구되는 좁은 영역(결제·원장)에 쓴다.&lt;/p&gt;
&lt;h2&gt;전달 보장 결정 트리 — 어느 보장을 택할까&lt;/h2&gt;
&lt;p&gt;실제 설계에서 &amp;quot;우리는 뭘 써야 하나&amp;quot;를 결정하는 흐름:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    Q1{데이터 손실 허용?} --&amp;gt;|O| AMO[&amp;quot;at-most-once (acks=0)&amp;quot;]
    Q1 --&amp;gt;|X| Q2{&amp;quot;중복 처리가 안전한가?&amp;lt;br/&amp;gt;(멱등/DB upsert 가능?)&amp;quot;}
    Q2 --&amp;gt;|O| ALO[&amp;quot;at-least-once (acks=all+재시도)&amp;lt;br/&amp;gt;+ 애플리케이션 멱등&amp;quot;]
    Q2 --&amp;gt;|X| Q3{&amp;quot;consume-process-produce 패턴?&amp;lt;br/&amp;gt;같은 Kafka 클러스터 내?&amp;quot;}
    Q3 --&amp;gt;|O| EOS[&amp;quot;EOS (transactional + read_committed)&amp;quot;]
    Q3 --&amp;gt;|X 외부 DB/HTTP| OUTBOX[&amp;quot;at-least-once + outbox/멱등 키 패턴&amp;lt;br/&amp;gt;(Kafka만으로는 한계)&amp;quot;]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 트리가 보여주는 핵심: &lt;strong&gt;exactly-once는 항상 정답이 아니다&lt;/strong&gt;. 손실을 감내하면 at-most-once(로그), 중복을 애플리케이션이 흡수하면 at-least-once, consume-process-produce면 EOS, 외부 시스템이면 outbox 패턴. 각기 비용·복잡도가 다르다.&lt;/p&gt;
&lt;h2&gt;외부 시스템까지 exactly-once — outbox 패턴&lt;/h2&gt;
&lt;p&gt;Kafka 트랜잭션은 &lt;strong&gt;같은 클러스터 내&lt;/strong&gt;만 보장한다. &amp;quot;메시지 처리 후 DB에 쓰기&amp;quot;처럼 외부 DB까지 정확히 한 번이 필요하면, Kafka 트랜잭션이 DB를 묶을 수 없으므로 별도 패턴이 필요하다.&lt;/p&gt;
&lt;p&gt;대표적 해법 — &lt;strong&gt;transactional outbox&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;비즈니스 로직이 DB에 &amp;quot;보낼 메시지&amp;quot;를 &lt;strong&gt;outbox 테이블&lt;/strong&gt;에 같은 DB 트랜잭션으로 쓴다(비즈니스 데이터 + outbox가 원자).&lt;/li&gt;
&lt;li&gt;별도 프로세스(또는 Debezium CDC)가 outbox 테이블을 읽어 Kafka로 produce.&lt;/li&gt;
&lt;li&gt;produce 성공하면 outbox 행을 삭제/마킹.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 하면 &amp;quot;DB 쓰기 + Kafka produce&amp;quot;가 (outbox를 매개로) 원자가 된다. 핵심은 &lt;strong&gt;DB 트랜잭션과 Kafka 트랜잭션을 직접 묶지 않고, outbox라는 중간 매개로 우회&lt;/strong&gt;하는 것. 반대 방향(Kafka → DB)은 consumer가 DB에 멱등 쓰기(중복 키 허용)로 at-least-once 중복을 흡수한다.&lt;/p&gt;
&lt;p&gt;이 패턴이 Kafka &amp;quot;exactly-once&amp;quot;의 경계를 보여준다 — 클러스터 내는 EOS, 외부는 설계 패턴으로 보완해야 한다.&lt;/p&gt;
&lt;h2&gt;Kafka Streams가 EOS를 &amp;quot;자동&amp;quot;으로 만드는 것&lt;/h2&gt;
&lt;p&gt;EOS를 직접 구현하려면 transactional API(&lt;code&gt;initTransactions&lt;/code&gt;/&lt;code&gt;beginTransaction&lt;/code&gt;/&lt;code&gt;sendOffsetsToTransaction&lt;/code&gt;/&lt;code&gt;commitTransaction&lt;/code&gt;)를 다뤄야 한다. 복잡하고 실수하기 쉽다. Kafka Streams는 이걸 &lt;strong&gt;프레임워크가 자동 처리&lt;/strong&gt;한다 — &lt;code&gt;processing.guarantee=exactly_once_v2&lt;/code&gt; 한 줄이면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;내부적으로 transactional producer + read_committed consumer + offset 원자 commit을 자동 구성.&lt;/li&gt;
&lt;li&gt;rebalance 시 트랜잭션 정리도 자동.&lt;/li&gt;
&lt;li&gt;개발자는 topology 로직만 짠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이게 Streams가 EOS를 쓰는 애플리케이션에서 인기인 핵심 이유다 — 직접 트랜잭션 API를 다루는 복잡도를 프레임워크가 흡수한다. 단, &lt;code&gt;exactly_once&lt;/code&gt; (v1, KIP-98)은 폐지됐고 &lt;code&gt;exactly_once_v2&lt;/code&gt; (KIP-447, 2.5+)를 써야 한다 — 구형 자료 주의.&lt;/p&gt;
&lt;h2&gt;전달 보장 관련 자주 묻는 추가 질문&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;th&gt;답&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;read_committed가 처리량을 얼마나 깎나&amp;quot;&lt;/td&gt;
&lt;td&gt;LSO 대기·abort 처리로 지연. 정량값은 환경 의존(미검증), 보통 수 ~ 10%+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;EOS 없이 consume-process-produce 정확성?&amp;quot;&lt;/td&gt;
&lt;td&gt;at-least-once만 → 중복 발생. 애플리케이션 멱등으로 보완하거나 EOS 써야&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;transactional.id를 바꾸면?&amp;quot;&lt;/td&gt;
&lt;td&gt;다른 producer로 인식 → 좀비 차단 안 됨. 재시작은 같은 id 유지 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;EOS consumer와 일반 consumer 혼합?&amp;quot;&lt;/td&gt;
&lt;td&gt;read_committed consumer만 abort 제외. 일반(read_uncommitted)은 abort된 것도 봄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;외부 DB 쓰기까지 EOS?&amp;quot;&lt;/td&gt;
&lt;td&gt;불가(outbox 패턴 등 별도 설계). Kafka 클러스터 내 한정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Kafka 4.3 실습 — 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전: KRaft 단일 노드. EOS는 트랜잭션 API 코드가 필요 → console 도구로 완전 재현은 어렵다(미검증: console 한계).&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;transaction 상태 관찰&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Kafka 4.3 — 트랜잭션 내부 topic 확인(구조적)
bin/kafka-topics.sh --list --bootstrap-server localhost:9092
# __transaction_state 가 보임 (트랜잭션 상태 저장, compact 정책)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: &lt;code&gt;__transaction_state&lt;/code&gt; 내부 topic 존재(트랜잭션 로그). EOS 앱 기동 후 이 topic에 상태가 쓰인다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;완전한 EOS 검증(출력+offset 원자성)은 두 터미널에 producer/consumer + ExactlyOnceMessageProcessor 예제를 실행해야 한다. 본 노트에선 개념·내부 topic 확인으로 한계 명시. idempotence 효과(중복 제거) 시각화도 broker 재시작·네트워크 에러 시뮬레이션이 필요해 console 도구로는 어렵다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;EOS 검증 — 무엇을 어떻게 확인하는가&lt;/h3&gt;
&lt;p&gt;EOS가 진짜 동작하는지 검증하려면 &lt;strong&gt;고의 장애 주입&lt;/strong&gt;이 필요하다. 절차(개념):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;환경&lt;/strong&gt;: &lt;code&gt;ExactlyOnceMessageProcessor&lt;/code&gt; 예제 기동(input topic → 처리 → output topic, &lt;code&gt;transactional.id&lt;/code&gt; 설정).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정상 처리 확인&lt;/strong&gt;: input에 메시지 → output에 정확히 한 번 나타나는지 카운트.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장애 주입&lt;/strong&gt;: 처리 중간에 consumer 프로세스 kill(트랜잭션 도중 크래시 시뮬레이션).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;재시작 후 확인&lt;/strong&gt;: 같은 &lt;code&gt;transactional.id&lt;/code&gt;로 재기동. output에 &lt;strong&gt;중복 없이&lt;/strong&gt; 이어지는지(abort된 미완료 트랜잭션의 출력이 안 보이는지).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__transaction_state&lt;/code&gt; 관찰&lt;/strong&gt;: 트랜잭션 상태가 (Begin→PrepareCommit→Complete)로 전이하는지(구조적 확인).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;read_committed vs read_uncommitted 대비&lt;/strong&gt;: read_uncommitted consumer는 abort된 것도 보는지, read_committed는 안 보는지 교차 확인.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 절차가 &amp;quot;출력+offset 원자 commit&amp;quot;이 실제로 중복·손실을 막는지 눈으로 확인하는 방법이다. 단, 단일 노드 + console 도구로는 3 ~ 4번(장애 주입·재시작)이 어려워 애플리케이션 코드가 필수다.&lt;/p&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;Kafka는 항상 exactly-once다&amp;quot;&lt;/td&gt;
&lt;td&gt;기본은 at-least-once. exactly-once는 idempotence/transactions 명시 적용 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;exactly-once는 메시지 속성이다&amp;quot;&lt;/td&gt;
&lt;td&gt;아님. &lt;strong&gt;출력+offset 원자 commit&lt;/strong&gt;이라는 설계 패턴의 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;exactly-once가 DB 쓰기까지 보장한다&amp;quot;&lt;/td&gt;
&lt;td&gt;Kafka 내부 한정. 외부 DB는 별도 트랜잭션/멱등 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;idempotent producer = exactly-once&amp;quot;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;단일 partition&lt;/strong&gt;에 한정. 여러 partition/offset은 transactions 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;transactional.id는 아무 값이나 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;안정적이고 고유해야. 좀비 차단의 기준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;read_committed는 성능에 무관하다&amp;quot;&lt;/td&gt;
&lt;td&gt;abort 처리·LISO 지연 있음. 필요 시만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;exactly_once와 exactly_once_v2는 같다&amp;quot;&lt;/td&gt;
&lt;td&gt;구형(KIP-98)은 폐지, v2(KIP-447) 사용. 구형 자료 주의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;transaction.timeout.ms는 안 설정해도 된다&amp;quot;&lt;/td&gt;
&lt;td&gt;장기 트랜잭션이 LSO를 멈춰 전체 consumer 지연 유발. 반드시 상한 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;EOS를 쓰면 외부 DB도 자동 exactly-once&amp;quot;&lt;/td&gt;
&lt;td&gt;아니다. 클러스터 내 한정. 외부는 outbox/멱등 패턴 별도 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;idempotent producer는 4.x에서도 켜야 한다&amp;quot;&lt;/td&gt;
&lt;td&gt;4.x 기본 true. 별도 지정 불필요(단, 구형 자료는 false 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Kafka Streams의 EOS&lt;/strong&gt;: Streams는 consume-process-produce를 프레임워크 수준에서 자동화한다. &lt;code&gt;processing.guarantee=exactly_once_v2&lt;/code&gt;(KIP-447, 2.5+)로 EOS 활성화 — 내부적으로 transactional producer + read_committed consumer + offset 원자 commit을 자동 처리. 개발자가 직접 트랜잭션 API를 다루지 않아도 EOS 달성 → Streams가 인기인 이유 중 하나.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exactly-once의 경계&lt;/strong&gt;: 같은 Kafka 클러스터 내는 EOS 보장 가능. 다른 클러스터(MirrorMaker 등)는 source offset과 target 메시지를 한 트랜잭션으로 못 묶어 at-least-once가 한계(2.x 이후 일부 EOS 지원, 미검증). 외부 DB/HTTP는 Kafka 트랜잭션 밖 → 멱등 키·outbox 패턴 등 별도 설계.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기본 = at-least-once&lt;/strong&gt;(중복 가능). 0.11부터 exactly-once 옵션.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idempotent producer&lt;/strong&gt;(4.x 기본) = &lt;strong&gt;단일 partition exactly-once&lt;/strong&gt;(PID+sequence 중복 제거).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;transactions&lt;/strong&gt; = &lt;strong&gt;여러 partition 원자 쓰기&lt;/strong&gt; + consumer offsets과 결합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EOS(consume-process-produce)&lt;/strong&gt; = 출력 메시지 + 입력 offset을 &lt;strong&gt;한 트랜잭션&lt;/strong&gt;으로 원자 commit. &amp;quot;exactly-once는 메시지 속성이 아니라 이 원자성의 결과&amp;quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;좀비 차단&lt;/strong&gt; = &lt;code&gt;transactional.id&lt;/code&gt;의 epoch 증가 → &lt;code&gt;ProducerFencedException&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;isolation.level=read_committed&lt;/code&gt;&lt;/strong&gt; = commit된 트랜잭션만 consumer가 읽음(EOS 필수, 단 LSO 지연 비용).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streams EOS&lt;/strong&gt; = &lt;code&gt;processing.guarantee=exactly_once_v2&lt;/code&gt;. 프레임워크가 자동 처리(트랜잭션 API 복잡도 흡수).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;발전사&lt;/strong&gt;: at-least-once(0.11 이전) → idempotent(0.11) → transactions(0.11) → EOS v2(2.5+) → 기본 true(3.0+). 각 단계가 좁은 범위 정확성 추가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경계&lt;/strong&gt;: Kafka 클러스터 내만. 외부 DB/HTTP는 멱등 설계 필요. &amp;quot;Kafka가 exactly-once인가&amp;quot;는 범위를 정하고 물어야.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LSO(last stable offset)&lt;/strong&gt;: read_committed consumer가 읽을 수 있는 안전 경계. 진행 중 트랜잭션이 LSO를 멈추게 함 → &lt;code&gt;transaction.timeout.ms&lt;/code&gt;로 상한 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;transaction.timeout.ms&lt;/strong&gt;: 트랜잭션 시간 상한(기본 60초). 초과 시 강제 abort → LSO 정체·좀비 방지.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;outbox 패턴&lt;/strong&gt;: 외부 DB까지 exactly-once가 필요할 때 Kafka 트랜잭션 대신 outbox 테이블로 DB+produce를 원자화.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결정 트리&lt;/strong&gt;: 손실 허용→at-most-once, 중복 흡수 가능→at-least-once, consume-process-produce→EOS, 외부→outbox.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;at-most-once, at-least-once, exactly-once의 차이를 중복/손실 관점에서 설명하라.&lt;/li&gt;
&lt;li&gt;idempotent producer가 &amp;quot;exactly-once&amp;quot;인 정확한 범위는? 왜 &amp;quot;단일 partition&amp;quot; 한정인가?&lt;/li&gt;
&lt;li&gt;consume-process-produce에서 &amp;quot;출력과 offset을 한 트랜잭션으로&amp;quot;가 왜 exactly-once를 만드는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transactional.id&lt;/code&gt;가 좀비(zombie)를 차단하는 원리를 설명하라. 왜 안정적이어야 하나?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;read_committed&lt;/code&gt; consumer가 abort된 트랜잭션을 안 보는 것이 왜 중요한가? 비용은?&lt;/li&gt;
&lt;li&gt;Kafka만으로 외부 DB 쓰기까지 exactly-once로 만들 수 없는 이유는? 어떤 패턴으로 보완하는가?&lt;/li&gt;
&lt;li&gt;Kafka Streams가 EOS를 &amp;quot;자동&amp;quot;으로 달성한다는 것이 왜 강력한가?&lt;/li&gt;
&lt;li&gt;LSO(last stable offset)가 무엇이고, 왜 진행 중 트랜잭션이 consumer 지연을 만드는가?&lt;/li&gt;
&lt;li&gt;transactional.id를 재시작마다 바꾸면 어떤 일이 벌어지는가?&lt;/li&gt;
&lt;li&gt;외부 DB 쓰기까지 exactly-once가 필요할 때 outbox 패턴이 어떻게 동작하는가?&lt;/li&gt;
&lt;li&gt;at-least-once + 애플리케이션 멱등 vs EOS — 비용 관점에서 언제 어느 쪽을 택하는가?&lt;/li&gt;
&lt;li&gt;전달 보장 발전사에서 idempotent producer와 transactions가 각각 어떤 갭을 메웠는가?&lt;/li&gt;
&lt;li&gt;EOS 검증에서 &amp;quot;고의 장애 주입 후 output에 중복 없는지 확인&amp;quot;이 핵심인 이유는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;Design — Message Delivery Semantics / Using Transactions&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/examples/src/main/java/kafka/examples/ExactlyOnceMessageProcessor.java&quot;&gt;ExactlyOnceMessageProcessor.java&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/examples/src/main/java/kafka/examples/TransactionalClientDemo.java&quot;&gt;TransactionalClientDemo.java&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;KIP-98(transactions), KIP-447(exactly_once_v2), KIP-833(KRaft)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/producer/ProducerConfig.java&quot;&gt;ProducerConfig.java (transaction.timeout.ms 등)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>delivery-semantics</category>
      <category>EOS</category>
      <category>Exactly-Once</category>
      <category>kafka</category>
      <category>transactions</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/19</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-08-delivery-semantics#entry19comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:45:06 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 07. log retention</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-07-log-retention</link>
      <description>&lt;h1&gt;retention과 compaction — 메시지가 언제, 어떻게 사라지는가&lt;/h1&gt;
&lt;p&gt;Kafka는 메시지를 읽어도 지우지 않는다(장 01·02). 그럼 디스크는 계속 찰 수밖에 없나? 아니다 — topic마다 &lt;strong&gt;&amp;quot;언제, 무엇을 지울지&amp;quot;&lt;/strong&gt; 정책(retention/compaction)을 둬서 오래된 데이터를 정리한다. 이 글은 그 두 가지 규칙과 실제 설정 방법을 다룬다.&lt;/p&gt;
&lt;p&gt;주의할 점부터: &lt;strong&gt;&amp;quot;compaction&amp;quot;은 압축(zip)이 아니다.&lt;/strong&gt; 같은 key의 옛 메시지를 버리는 것이다. 이 용어 혼동이 운영 착오를 자주 만든다.&lt;/p&gt;
&lt;h2&gt;메시지는 영원히 남지 않는다 — 왜 retention이 필요한가&lt;/h2&gt;
&lt;p&gt;Kafka topic은 로그라 메시지가 계속 쌓인다. 하지만 디스크는 유한하다. 그래서 topic마다 정리 정책을 둔다. 두 가지:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;delete&lt;/strong&gt; — 오래된 것을 &lt;strong&gt;시간이나 크기 기준으로&lt;/strong&gt; 버린다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compact&lt;/strong&gt; — 같은 key의 &lt;strong&gt;옛 값을 버리고 최신값만&lt;/strong&gt; 남긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 둘은 &amp;quot;언제 지울지&amp;quot;의 차이가 아니라 &lt;strong&gt;&amp;quot;무엇을 지울지&amp;quot;&lt;/strong&gt;의 차이다.&lt;/p&gt;
&lt;h2&gt;segment — 로그를 이루는 파일 단위&lt;/h2&gt;
&lt;p&gt;partition 하나는 여러 &lt;strong&gt;segment&lt;/strong&gt;로 구성된다 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/implementation/log.md&quot;&gt;(log.md)&lt;/a&gt;. 각 segment는 세 파일로 이루어진다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 메시지 데이터(순차 append)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.index&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;offset → 파일 내 바이트 위치 매핑(빠른 임의 접근)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.timeindex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp → offset 매핑(시간 기반 조회·retention)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;segment는 &lt;code&gt;log.segment.bytes&lt;/code&gt;(기본 &lt;strong&gt;1 GiB&lt;/strong&gt;)에 도달하거나 &lt;code&gt;log.segment.ms&lt;/code&gt;(시간)가 지나면 새 segment로 넘어간다(&lt;strong&gt;rolling&lt;/strong&gt;). &lt;strong&gt;active segment&lt;/strong&gt;(마지막, 쓰기 진행 중)는 삭제 대상이 아니다 — rolling되어 읽기 전용이 된 segment만 삭제 후보.&lt;/p&gt;
&lt;h3&gt;segment가 왜 중요한가&lt;/h3&gt;
&lt;p&gt;&amp;quot;consumer가 offset 5000을 읽고 싶다&amp;quot;고 할 때, partition 전체를 처음부터 훑으면 느리다. &lt;code&gt;.index&lt;/code&gt; 파일이 offset 5000이 어느 바이트 위치에 있는지 알려주어 &lt;strong&gt;O(1)에 찾는다&lt;/strong&gt;. retention도 segment 단위로 일어나서 — 파일 중간을 수정할 필요 없이 &lt;strong&gt;segment 전체를 삭제&lt;/strong&gt;(append-only 유지).&lt;/p&gt;
&lt;h2&gt;delete 정책 — 오래된 segment를 통째로 버린다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cleanup.policy=delete&lt;/code&gt;(기본)는 오래된 segment를 통째로 버린다. 두 기준:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;시간 기반&lt;/strong&gt;: segment 내 &lt;strong&gt;가장 큰 timestamp&lt;/strong&gt;가 &lt;code&gt;retention.ms&lt;/code&gt;(기본 7일 = 604800000ms)를 넘으면 그 segment 삭제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;크기 기반&lt;/strong&gt;: partition 크기가 &lt;code&gt;retention.bytes&lt;/code&gt;(기본 -1 = 비활성) 한계를 넘으면 가장 오래된 segment부터 삭제.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;둘 다 켜면 &lt;strong&gt;어떤 쪽이든 임계에 닿으면 삭제&lt;/strong&gt;(먼저 닿는 쪽).&lt;/p&gt;
&lt;h3&gt;retention 설정 예&lt;/h3&gt;
&lt;p&gt;topic 생성 시 retention을 1시간으로 설정:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka 4.3
bin/kafka-topics.sh --create --topic short-lived \
  --partitions 3 --replication-factor 1 \
  --config retention.ms=3600000 \
  --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;topic별로 동적 변경:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bin/kafka-configs.sh --alter --topic short-lived \
  --add-config retention.ms=7200000,retention.bytes=1073741824 \
  --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;현재 설정 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bin/kafka-configs.sh --describe --topic short-lived --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;retention 시나리오 — 시간순 타임라인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;시각 0:    partition 생성, segment-0 활성화
시각 1h:   segment-0이 1GiB 도달 → segment-1로 rolling
시각 7d:   segment-0의 max timestamp가 7일(retention.ms) 초과 → segment-0 삭제
시각 8d:   segment-1 삭제...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이 흐름이 &amp;quot;7일 보관, segment 단위 삭제&amp;quot;가 실제로 어떻게 일어나는지 보여준다.&lt;/p&gt;
&lt;h2&gt;compact 정책 — 같은 key의 최신값만 남긴다&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cleanup.policy=compact&lt;/code&gt;는 key 기반이다. 같은 key의 메시지가 여러 개 있으면 &lt;strong&gt;가장 최근 것만 남기고 옛 것은 버린다&lt;/strong&gt; &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;(design.md — Log Compaction)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;예: topic에 &lt;code&gt;user1&lt;/code&gt;의 프로필이 v1, v2, v3로 업데이트되며 들어온다. compaction 후엔 &lt;code&gt;user1&lt;/code&gt;의 &lt;strong&gt;v3만 남는다&lt;/strong&gt;. 과거(v1, v2)는 버려진다. 이게 &amp;quot;사용자 최신 상태만 유지&amp;quot;가 필요한 topic에 유용한 이유다.&lt;/p&gt;
&lt;h3&gt;compaction ≠ 압축&lt;/h3&gt;
&lt;p&gt;&amp;quot;compaction = 압축(zip)&amp;quot;이라고 생각하면 안 된다. 압축은 용량을 줄이지만 데이터는 그대로고, compaction은 &lt;strong&gt;옛 데이터 자체를 버린다&lt;/strong&gt;. 용량은 줄지만 원리가 다르다.&lt;/p&gt;
&lt;h3&gt;tombstone — &amp;quot;이 key를 삭제하라&amp;quot; 표식&lt;/h3&gt;
&lt;p&gt;value가 &lt;code&gt;null&lt;/code&gt;인 메시지를 &lt;strong&gt;tombstone&lt;/strong&gt;이라 한다. compaction에서 &amp;quot;이 key를 더 이상 유지하지 마라&amp;quot;는 삭제 표식. 일정 기간 후 tombstone 자체도 제거된다.&lt;/p&gt;
&lt;p&gt;tombstone 사용 예:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# user1을 삭제하려면 value=null로 produce
bin/kafka-console-producer.sh --topic user-profiles \
  --property &amp;quot;parse.key=true&amp;quot; --property &amp;quot;key.separator=:&amp;quot; \
  --bootstrap-server localhost:9092
# 입력: user1:  (value 없음 = null = tombstone)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;dirty ratio — compaction 트리거&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;min.cleanable.dirty.ratio&lt;/code&gt;(기본 0.5) — 한 segment에서 옛 key 값이 일정 비율(dirty) 이상 쌓여야 compact 대상이 됨. 너무 낮으면 빈번 compact(CPU 비용), 너무 높으면 용량 비효율.&lt;/p&gt;
&lt;h3&gt;key가 없으면 compact는 무의미&lt;/h3&gt;
&lt;p&gt;key가 &lt;code&gt;null&lt;/code&gt;인 메시지는 compact 대상이 아니다 → delete와 혼합(&lt;code&gt;compact,delete&lt;/code&gt;) 운영이 권장된다.&lt;/p&gt;
&lt;h2&gt;delete vs compact — 무엇을 지울 것인가&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;정책&lt;/th&gt;
&lt;th&gt;무엇이 남나&lt;/th&gt;
&lt;th&gt;전형 용도&lt;/th&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기간 내 전체 이력&lt;/td&gt;
&lt;td&gt;이벤트 로그, 원시 데이터&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cleanup.policy=delete&lt;/code&gt;, &lt;code&gt;retention.ms=604800000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;각 key 최신값&lt;/td&gt;
&lt;td&gt;상태 스냅샷, config, 사용자 프로필&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cleanup.policy=compact&lt;/code&gt;, &lt;code&gt;min.cleanable.dirty.ratio=0.5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compact,delete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;key 최신 + 기간 제한&lt;/td&gt;
&lt;td&gt;상태 + tombstone 정리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cleanup.policy=compact,delete&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;Kafka 4.3 실습 — 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전: KRaft 단일 노드.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;retention 관찰&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Kafka 4.3 — 짧은 retention으로 topic 생성 (1분)
bin/kafka-topics.sh --create --topic ret-demo \
  --partitions 1 --replication-factor 1 \
  --config retention.ms=60000 \
  --bootstrap-server localhost:9092

# 메시지 produce 후 1분 대기 → describe로 segment 확인
bin/kafka-topics.sh --describe --topic ret-demo --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;compaction (key 기반)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# cleanup.policy=compact 로 topic 생성
bin/kafka-topics.sh --create --topic comp-demo \
  --partitions 1 --replication-factor 1 \
  --config cleanup.policy=compact \
  --bootstrap-server localhost:9092

# 같은 key에 여러 값 produce
bin/kafka-console-producer.sh --topic comp-demo \
  --property &amp;quot;parse.key=true&amp;quot; --property &amp;quot;key.separator=:&amp;quot; \
  --bootstrap-server localhost:9092
# user1:v1 / user1:v2 / user1:v3 입력&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: compact 실행 후 &lt;code&gt;user1&lt;/code&gt;의 최신값(v3)만 남는지(즉시 관찰은 어려움 — dirty ratio·시간 조건 충족 시 실행).&lt;/p&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;compaction = 압축(zip)&amp;quot;&lt;/td&gt;
&lt;td&gt;아님. key별 옛값 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;retention은 시간 OR 크기 중 하나만&amp;quot;&lt;/td&gt;
&lt;td&gt;둘 다 켜면 &lt;strong&gt;먼저 닿는 쪽&lt;/strong&gt; 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;key 없이 produce해도 compact된다&amp;quot;&lt;/td&gt;
&lt;td&gt;key 없으면 compact 의미 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;메시지 단위로 삭제된다&amp;quot;&lt;/td&gt;
&lt;td&gt;segment 단위로 통째 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;active segment도 retention 대상&amp;quot;&lt;/td&gt;
&lt;td&gt;아님. rolling 후 후보&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;tombstone은 영구 남는다&amp;quot;&lt;/td&gt;
&lt;td&gt;일정 기간 후 제거됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__consumer_offsets&lt;/code&gt;는 왜 compact인가&lt;/strong&gt;: consumer offset은 &lt;code&gt;(group,topic,partition)&lt;/code&gt;을 key로 저장. 같은 key의 옛 offset은 필요 없음 → compact로 최신만 유지(장 04).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tiered storage (3.x+)&lt;/strong&gt;: 로컬 디스크 외 원격(S3)에 오래된 segment 보관 → 비용 절감.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;log.segment.ms&lt;/code&gt;&lt;/strong&gt;: 시간 기반 segment rolling. 기본값은 7일(미검증). 짧게 두면 segment 수가 많아져 파일 핸들 증가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;segment&lt;/strong&gt;: partition을 이루는 파일 단위(&lt;code&gt;.log&lt;/code&gt; + &lt;code&gt;.index&lt;/code&gt; + &lt;code&gt;.timeindex&lt;/code&gt;). active segment만 쓰기.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;delete&lt;/strong&gt;: 시간(&lt;code&gt;retention.ms&lt;/code&gt;, 기본 7일) 또는 크기(&lt;code&gt;retention.bytes&lt;/code&gt;) — &lt;strong&gt;먼저 도달한 쪽&lt;/strong&gt;. segment 통째 삭제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;compact&lt;/strong&gt;: key별 &lt;strong&gt;최신값만&lt;/strong&gt; 유지. tombstone(&lt;code&gt;null&lt;/code&gt;)으로 key 삭제. &lt;strong&gt;key 없으면 무의미&lt;/strong&gt;. (압축 아님!)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;compact,delete&lt;/code&gt;&lt;/strong&gt;: 상태 유지 + 기간 제한 결합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설정&lt;/strong&gt;: topic별로 동적 변경(&lt;code&gt;kafka-configs.sh --alter --add-config&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;delete vs compact는 &amp;quot;무엇을&amp;quot; 지울지의 차이&lt;/strong&gt; — 시간/크기(delete) vs key별 옛값(compact).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;delete와 compact의 차이를 &amp;quot;무엇을 지우는가&amp;quot; 관점에서 설명하라.&lt;/li&gt;
&lt;li&gt;retention 시간과 크기를 둘 다 켜면 어떻게 동작하는가?&lt;/li&gt;
&lt;li&gt;segment를 통째로 삭제하는 이유(append-only)를 설명하라.&lt;/li&gt;
&lt;li&gt;compaction에서 tombstone(value=null)의 역할은? 어떻게 쓰는가?&lt;/li&gt;
&lt;li&gt;key 없이 produce하는데 cleanup.policy=compact인 topic에서 무슨 일이 벌어지는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__consumer_offsets&lt;/code&gt;가 compact 정책을 쓰는 이유는?&lt;/li&gt;
&lt;li&gt;retention.ms를 topic별로 동적 변경하는 명령은?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/implementation/log.md&quot;&gt;Implementation — log.md (segments, deletes)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;Design — Log Compaction&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/configuration/topic-configs.md&quot;&gt;Topic configs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>compaction</category>
      <category>kafka</category>
      <category>log</category>
      <category>retention</category>
      <category>segment</category>
      <category>Tombstone</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/18</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-07-log-retention#entry18comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:44:05 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 06. replication</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-06-replication</link>
      <description>&lt;h1&gt;broker가 죽어도 데이터가 안 사라지는 이유 — 복제와 ISR&lt;/h1&gt;
&lt;p&gt;broker 한 대에 모든 데이터를 두면 그 broker가 죽는 순간 전부 날아간다. 그래서 Kafka는 partition을 &lt;strong&gt;복제(replication)&lt;/strong&gt;한다. 하지만 &amp;quot;복제했다&amp;quot;로 끝이 아니다 — 얼마나 복제해야 안전한지, 복제가 안 끝난 메시지는 consumer에게 보여야 하는지, 리더가 죽으면 누가 이어받는지를 정의해야 한다. Kafka의 답은 &lt;strong&gt;ISR(In-Sync Replicas)&lt;/strong&gt;이라는 동적 쿼럼과 &lt;strong&gt;HW(high watermark)&lt;/strong&gt;라는 commit 경계다.&lt;/p&gt;
&lt;p&gt;이 글은 Kafka의 내결함성 모델 — &lt;code&gt;f+1&lt;/code&gt;개 복제본이 &lt;code&gt;f&lt;/code&gt;개 장애까지, 이미 commit된 메시지는 잃지 않는다는 보장 — 을 다룬다. &lt;code&gt;replication.factor=1&lt;/code&gt;로 두다가 broker 한 대에 모든 데이터가 날아가는 사고를 막으려면 이 메커니즘을 이해해야 한다.&lt;/p&gt;
&lt;h2&gt;partition은 복제된 로그다&lt;/h2&gt;
&lt;p&gt;Kafka partition은 &lt;strong&gt;복제된 로그(replicated log)&lt;/strong&gt;다 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;(design.md — Replication)&lt;/a&gt;. 핵심 구조:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;leader가 쓰기 순서를 결정&lt;/strong&gt;하고, follower는 leader에서 fetch해 복사한다.&lt;/li&gt;
&lt;li&gt;leader는 &lt;strong&gt;ISR&lt;/strong&gt;(In-Sync Replicas) 집합을 유지한다. ISR 조건을 잃으면(liveness 세션 상실 또는 &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt; 초과 지연) ISR에서 제거된다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;commit = ISR의 모든 복제본이 해당 메시지를 받음&lt;/strong&gt;. ISR 집합은 클러스터 메타데이터에 영속 → ISR 어느 것이든 leader 후보.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;복제는 동기 push가 아니다. follower가 leader에서 &lt;strong&gt;pull(fetch)&lt;/strong&gt; 한다 — consumer와 같은 방식. 그래서 leader가 follower를 밀어주지 않는다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;신뢰성 공식&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;_f+1 복제본으로 f 장애까지, *_이미 commit된 메시지는 잃지 않는다.***&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;복제본 수&lt;/th&gt;
&lt;th&gt;감내 장애 수&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1 (단일)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;broker 장애 = 데이터 손실&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0 (또는 1, 가용성 한계)&lt;/td&gt;
&lt;td&gt;권장 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;프로덕션 표준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;고신뢰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;ISR — &amp;quot;충분히 따라잡은&amp;quot; 복제본 집합&lt;/h2&gt;
&lt;p&gt;ISR은 leader를 포함해 &amp;quot;충분히 따라잡은&amp;quot; 복제본의 &lt;strong&gt;동적 집합&lt;/strong&gt;이다. &amp;quot;충분히&amp;quot;의 기준은 두 가지:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;liveness 세션이 살아있고,&lt;/li&gt;
&lt;li&gt;leader와의 지연이 &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt; 이내.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;follower가 이 기준을 어기면 ISR에서 &lt;strong&gt;제거&lt;/strong&gt;되고, 다시 따라잡으면 &lt;strong&gt;복귀&lt;/strong&gt;한다. ISR은 고정이 아니라 계속 변한다 — 그래서 &amp;quot;동적 쿼름&amp;quot;이다.&lt;/p&gt;
&lt;p&gt;ISR이 왜 leader 후보 집합인가 — ISR에 있다는 건 &amp;quot;최신 데이터를 갖고 있다&amp;quot;는 보증이므로, ISR 안의 복제본이 leader가 되면 commit된 데이터를 잃지 않는다.&lt;/p&gt;
&lt;h2&gt;HW와 LEO — commit 경계와 로그 끝&lt;/h2&gt;
&lt;p&gt;여기서 두 offset이 등장한다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LEO(log end offset)&lt;/strong&gt;: 각 복제본 로그의 끝 offset. leader와 follower 각자 자기 LEO를 갖는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HW(high watermark)&lt;/strong&gt;: &lt;strong&gt;모든 ISR이 받은&lt;/strong&gt; 마지막 offset. = min(모든 ISR의 LEO).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    L[&amp;quot;leader LEO=10&amp;quot;] --&amp;gt; HW[&amp;quot;HW = min(ISR LEO들)&amp;quot;]
    F1[&amp;quot;follower1 LEO=10&amp;quot;] --&amp;gt; HW
    F2[&amp;quot;follower2 LEO=8&amp;quot;] --&amp;gt; HW
    HW --&amp;gt; C[&amp;quot;HW=8: consumer에게 노출되는 commit 경계&amp;quot;]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;HW가 consumer 노출 경계다&lt;/strong&gt;: consumer는 &lt;strong&gt;HW 이하&lt;/strong&gt;의 offset만 읽을 수 있다. 왜 — leader 장애로 사라질 수 있는 미commit 메시지를 consumer가 못 보게 하려고. HW 너머(HW &amp;lt; offset ≤ LEO)의 메시지는 leader엔 있지만 아직 모든 ISR에 복제 안 된, &amp;quot;아직 commit 아닌&amp;quot; 메시지다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;HW가 &amp;quot;commit&amp;quot;을 정의한다는 게 핵심이다. Kafka에선 &amp;quot;commit = ISR 전원 수신&amp;quot;이고, 그 경계가 HW다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;acks=all + min.insync.replicas — 신뢰성 강제&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;acks=all&lt;/code&gt;은 &amp;quot;ISR 모두&amp;quot;를 기다린다. 하지만 ISR이 1개만 남아도 그 1개만 확인하면 성공 → &lt;strong&gt;사실상 신뢰성 없음&lt;/strong&gt;. 그래서 &lt;code&gt;min.insync.replicas&lt;/code&gt;(예: 2)를 설정하면, ISR 수가 이 미만이면 &lt;strong&gt;쓰기를 거부&lt;/strong&gt;(NotEnoughReplicas)한다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;ISR=3&lt;/th&gt;
&lt;th&gt;ISR=2(min.insync=2)&lt;/th&gt;
&lt;th&gt;ISR=1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acks=all&lt;/code&gt; 쓰기&lt;/td&gt;
&lt;td&gt;성공(전원 확인)&lt;/td&gt;
&lt;td&gt;성공(최소 충족)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;거부&lt;/strong&gt;(NotEnoughReplicas)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 broker 장애 감내&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O(replication.factor=3)&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가용성&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;균형&lt;/td&gt;
&lt;td&gt;낮음(쓰기 막힘)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;전형적 프로덕션: &lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, &lt;code&gt;acks=all&lt;/code&gt;. → 1 broker 장애까지 쓰기 가능 + commit 데이터 안전.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;leader 선출 — 리더가 죽으면 누가 이어받는가&lt;/h2&gt;
&lt;p&gt;리더가 장애 감지되면, controller(KRaft)가 새 leader를 선출한다. 기본 규칙: &lt;strong&gt;ISR 안의 follower만&lt;/strong&gt; leader 후보.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    L[&amp;quot;leader 장애 감지&amp;quot;] --&amp;gt; S1{&amp;quot;ISR에 살아있는 복제본?&amp;quot;}
    S1 --&amp;gt;|&amp;quot;있음&amp;quot;| EL1[&amp;quot;ISR에서 선출 (데이터 손실 없음)&amp;quot;]
    S1 --&amp;gt;|&amp;quot;없음&amp;quot;| S2{&amp;quot;ELR(4.0) 활성 또는 unclean 허용?&amp;quot;}
    S2 --&amp;gt;|&amp;quot;ELR 활성&amp;quot;| EL2[&amp;quot;ELR/마지막 leader 중 선출 (HW까지 보존)&amp;quot;]
    S2 --&amp;gt;|&amp;quot;unclean 허용&amp;quot;| EL3[&amp;quot;ISR 밖 선출 (데이터 손실 가능)&amp;quot;]
    S2 --&amp;gt;|&amp;quot;불가&amp;quot;| W[&amp;quot;쓰기 중단 — 가용성 포기, 신뢰성 선택&amp;quot;]&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;ELR(Eligible Leader Replicas) — Kafka 4.0의 가용성 개선&lt;/h3&gt;
&lt;p&gt;1차 출처: &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/eligible-leader-replicas.md&quot;&gt;eligible-leader-replicas.md&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kafka 4.0 도입, &lt;strong&gt;4.1부터 신규 클러스터 기본 활성화&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;ISR이 &lt;code&gt;min.insync.replicas&lt;/code&gt; 미만이라 HW가 멈춰 있을 때, &lt;strong&gt;ISR이 아닌 복제본도(조건 충족 시) leader가 될 수 있게&lt;/strong&gt; 허용.&lt;/li&gt;
&lt;li&gt;&amp;quot;엄격 min ISR&amp;quot; 규칙이 HW 전진을 막는 문제(가용성 손실)를 해소.&lt;/li&gt;
&lt;li&gt;controller 선출 우선순위: &lt;strong&gt;ISR &amp;gt; unfenced ELR &amp;gt; 마지막으로 알려진 unfenced leader&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;unclean leader election — 신뢰성 vs 가용성의 극단&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unclean.leader.election.enable=true&lt;/code&gt;면 ISR이 전멸해도 ISR 밖 복제본이 leader가 된다(가용성 회복). 단, &lt;strong&gt;HW 이후의 commit 데이터 손실&lt;/strong&gt;이 가능하다. unclean=false면 ISR 회복까지 &lt;strong&gt;쓰기 중단&lt;/strong&gt;(신뢰성 우선). 4.x에선 기본 false(데이터 손실 방지) 경향 — 미검증: 정확 기본값은 broker 설정 실측.&lt;/p&gt;
&lt;h2&gt;follower는 어떻게 복제하는가&lt;/h2&gt;
&lt;p&gt;follower는 leader에게 &lt;strong&gt;fetch 요청&lt;/strong&gt;을 보내 새 메시지를 당겨온다(pull). 사실상 consumer와 같은 방식. follower가 충분히 최신이면 ISR 유지, 뒤처지면(&lt;code&gt;replica.lag.time.max.ms&lt;/code&gt; 초과) ISR에서 제외.&lt;/p&gt;
&lt;p&gt;이 &amp;quot;비동기 pull 복제&amp;quot;가 중요한 함의를 갖는다 — leader가 follower의 보제 완료를 동기적으로 기다리는 게 아니라, follower가 알아서 따라온다. 그래서 &lt;code&gt;acks=all&lt;/code&gt;의 &amp;quot;all&amp;quot;은 &amp;quot;현재 ISR 전원이 fetch 완료&amp;quot;를 의미하지, &amp;quot;모든 복제본이 무조건&amp;quot;을 의미하진 않는다(뒤처진 복제본은 ISR에서 빠져서 all 대상이 아님).&lt;/p&gt;
&lt;h2&gt;leader epoch — leader가 바뀌어도 로그가 발산하지 않는 장치&lt;/h2&gt;
&lt;p&gt;leader가 바뀔 때마다 한 가지 위험이 있다 — 옛 leader가 잠깐 살아나서(네트워크 분단) 자기 로그에 쓰다가 새 leader와 다른 내용이 생기는 &amp;quot;로그 발산(log divergence)&amp;quot;이다. Kafka는 &lt;strong&gt;leader epoch&lt;/strong&gt;로 이를 막는다.&lt;/p&gt;
&lt;p&gt;leader epoch는 &lt;strong&gt;단조증가 정수&lt;/strong&gt;로, leader가 바뀔 때마다 1씩 올라간다. 각 로그 세그먼트는 (epoch, start-offset) 쌍을 메타데이터로 갖는다. 새 leader가 선출되면:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;자기 epoch를 올린다.&lt;/li&gt;
&lt;li&gt;follower가 fetch할 때 자기가 본 &lt;strong&gt;마지막 epoch&lt;/strong&gt;를 함께 보낸다.&lt;/li&gt;
&lt;li&gt;leader는 epoch 불일치를 감지하면 follower 로그의 &lt;strong&gt;잘못된(옛 leader가 쓴) 꼬리를 잘라내고(truncate)&lt;/strong&gt; 자기 로그로 다시 맞춘다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이게 KIP-101(Stop replicating after leader epoch)의 핵심이다. epoch가 없던 시절엔 HW 기준으로만 잘라내다가 모호한 경우가 있었는데, epoch로 &amp;quot;이 시점 이후는 옛 leader의 것이니 버려라&amp;quot;가 명확해졌다. replication 정확성의 근간이다.&lt;/p&gt;
&lt;h2&gt;복제 worked example — broker 장애 시 무슨 일이 일어나는가&lt;/h2&gt;
&lt;p&gt;추상 설명만으론 감이 안 온다. 3 broker(&lt;code&gt;b1,b2,b3&lt;/code&gt;), partition &lt;code&gt;p0&lt;/code&gt;(&lt;code&gt;replication.factor=3&lt;/code&gt;, leader=&lt;code&gt;b1&lt;/code&gt;, ISR=&lt;code&gt;{b1,b2,b3}&lt;/code&gt;, HW=100)에서 &lt;strong&gt;&lt;code&gt;b1&lt;/code&gt;이 장애&lt;/strong&gt;났다고 하자. 단계별로:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;장애 감지&lt;/strong&gt;: coordinator가 &lt;code&gt;b1&lt;/code&gt;의 heartbeat 단절을 감지(&lt;code&gt;session.timeout&lt;/code&gt; 또는 KRaft liveness). controller가 개입.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISR 재계산&lt;/strong&gt;: &lt;code&gt;b1&lt;/code&gt;이 ISR에서 제외 → ISR=&lt;code&gt;{b2,b3}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;새 leader 선출&lt;/strong&gt;: ISR 안의 &lt;code&gt;b2&lt;/code&gt;(또는 &lt;code&gt;b3&lt;/code&gt;)가 새 leader. epoch 증가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HW 유지&lt;/strong&gt;: HW는 &lt;code&gt;min(b2.LEO, b3.LEO)&lt;/code&gt;. &lt;code&gt;b1&lt;/code&gt;이 빠졌지만 둘 다 HW=100까지 복제했으므로 HW=100 유지 → &lt;strong&gt;commit된 메시지(≤100) 손실 없음&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쓰기 가용성&lt;/strong&gt;: &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, ISR=2 → 쓰기 계속 가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;b1&lt;/code&gt; 복구&lt;/strong&gt;: &lt;code&gt;b1&lt;/code&gt;이 돌아오면 자기 epoch가 옛이라는 걸 new leader에게 fetch하며 알게 되고, 옛 leader 시절 쓴(잘못된) 꼬리를 truncate한 뒤 새 leader에서 따라잡음 → ISR에 복귀.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 시나리오가 &lt;code&gt;f+1=3&lt;/code&gt; 복제본이 &lt;code&gt;f=1&lt;/code&gt; 장애를 견디는 것을 구체적으로 보여준다. 핵심은 &amp;quot;commit(HW=100) 이하 데이터는 손실 없다&amp;quot;는 보장이 ISR·HW·leader epoch의 조합으로 실제로 지켜진다는 점이다.&lt;/p&gt;
&lt;h3&gt;동시 장애 2대면 어떻게 되나&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;b1&lt;/code&gt;, &lt;code&gt;b2&lt;/code&gt;가 동시에 장애 → ISR=&lt;code&gt;{b3}&lt;/code&gt; 하나만. &lt;code&gt;min.insync.replicas=2&lt;/code&gt;이면 ISR(1) &amp;lt; min(2) → &lt;strong&gt;쓰기 거부&lt;/strong&gt;(NotEnoughReplicas). 읽기는 HW=100까지 가능. &lt;code&gt;b1&lt;/code&gt;이나 &lt;code&gt;b2&lt;/code&gt;가 복구해 ISR≥2가 되어야 쓰기 재개. 이게 신뢰성이 가용성을 이긴 순간이다. unclean 허용 시 &lt;code&gt;b3&lt;/code&gt;가 단독 leader가 되어 쓰기를 허용하되, HW 너머 &lt;code&gt;b3&lt;/code&gt;만 가진 미commit 데이터가 손실될 수 있다.&lt;/p&gt;
&lt;h2&gt;ISR 라이프사이클 — flap과 복구&lt;/h2&gt;
&lt;p&gt;ISR은 고정이 아니라 계속 변한다. follower가 느려졌다 빨라졌다(flapping) 하는 실제 운영 시나리오를 보자:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;follower &lt;code&gt;b3&lt;/code&gt;가 GC·디스크 지연으로 &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt; 초과 → &lt;strong&gt;ISR에서 제거&lt;/strong&gt;(shrink). 이 순간 partition은 URP가 된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b3&lt;/code&gt;가 따라잡아(leader의 HW까지 fetch 완료) → &lt;strong&gt;ISR에 복귀&lt;/strong&gt;(expand).&lt;/li&gt;
&lt;li&gt;이 shrink/expand가 잦으면 broker 불건강(GC·디스크·네트워크) 신호. JMX &lt;code&gt;ISRShrinkRate&lt;/code&gt;/&lt;code&gt;ISRExpandRate&lt;/code&gt;로 잡는다(장 12).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ISR 변동이 잦은 partition은 안정적이지 않다 — &lt;code&gt;min.insync.replicas&lt;/code&gt;에 자주 걸려 쓰기가 거부될 수 있고, 리더 선출 빈도가 올라간다. 운영에선 ISR 안정성 자체가 건강 지표다.&lt;/p&gt;
&lt;h2&gt;produce → commit 수명주기 — HW가 어떻게 전진하는가&lt;/h2&gt;
&lt;p&gt;복제가 정상 동작할 때 HW가 어떻게 자라는지 추적하면, &amp;quot;commit&amp;quot;이 동적으로 만들어지는 과정이 보인다. partition &lt;code&gt;p0&lt;/code&gt;(leader=&lt;code&gt;b1&lt;/code&gt;, ISR=&lt;code&gt;{b1,b2,b3}&lt;/code&gt;, 현재 HW=100)에서 producer가 batch를 보낸다고 하자:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant P as producer
    participant L as leader b1
    participant F2 as follower b2
    participant F3 as follower b3
    P-&amp;gt;&amp;gt;L: send batch[101..110] (acks=all)
    L-&amp;gt;&amp;gt;L: append (LEO=110)
    F2-&amp;gt;&amp;gt;L: fetch → b2도 101..110 수신 (b2.LEO=110)
    F3-&amp;gt;&amp;gt;L: fetch → b3도 101..110 수신 (b3.LEO=110)
    Note over L: 모든 ISR의 LEO=110 → HW=110으로 전진
    L--&amp;gt;&amp;gt;P: ack (HW 반영, commit 완료)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;핵심: &lt;strong&gt;HW는 leader가 모든 ISR의 fetch를 확인한 뒤에야 전진&lt;/strong&gt;한다. producer의 &lt;code&gt;acks=all&lt;/code&gt; 대기는 곧 &amp;quot;이 HW 전진이 끝날 때까지 기다린다&amp;quot;는 뜻이다. follower가 하나라도 느리면 그 follower의 LEO가 HW를 낮추므로, HW 전진이 늦어지고 producer ack도 늦어진다 → 처리량 저하로 이어진다. 그래서 ISR 안정성이 성능에도 직결된다.&lt;/p&gt;
&lt;h2&gt;복제 관련 주요 설정 — 튜닝 축&lt;/h2&gt;
&lt;p&gt;복제 동작을 다루는 핵심 설정(1차 출처: topic-configs/broker-configs):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;설정&lt;/th&gt;
&lt;th&gt;기본값&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replica.lag.time.max.ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30000(30초)&lt;/td&gt;
&lt;td&gt;follower가 이 시간 내에 leader를 따라잡지 못하면 ISR 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;min.insync.replicas&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1(권장 2)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acks=all&lt;/code&gt; 쓰기 허용 최소 ISR 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replica.fetch.max.bytes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1MB&lt;/td&gt;
&lt;td&gt;follower fetch 한 번당 최대 바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;num.replica.fetchers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;broker당 follower fetch 스레드 수(증가시 복제 병렬성↑)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unclean.leader.election.enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false(권장)&lt;/td&gt;
&lt;td&gt;ISR 전멸 시 ISR 밖 leader 허용 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;num.replica.fetchers&lt;/code&gt;를 늘리면 follower가 여러 스레드로 leader에서 당겨오므로, partition 수가 많고 복제가 따라잡지 못할 때 효과가 있다. 반대로 &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt;를 너무 크게 두면 느린 follower가 ISR에 오래 남아 처리량을 끌어내린다. 복제 튜닝은 ISR 안정성과 처리량의 균형을 잡는 일이다.&lt;/p&gt;
&lt;h3&gt;복제 throttle — 대규모 복구 시 디스크 보호&lt;/h3&gt;
&lt;p&gt;broker 장애 복구나 partition reassignment로 대량 복제가 몰리면, 네트워크·디스크가 포화돼 운영 트래픽까지 느려진다. &lt;code&gt;kafka-reassign-partitions.sh --throttle &amp;lt;bps&amp;gt;&lt;/code&gt;로 복제 대역폭을 제한해 운영 영향을 막는다(장 12). 복구 속도와 운영 안정성의 균형 — &amp;quot;최대한 빨리 복구&amp;quot;가 항상 정답은 아니다.&lt;/p&gt;
&lt;h2&gt;복제 모델 비교 — 왜 Kafka는 ISR 동적 쿼럼을 택했나&lt;/h2&gt;
&lt;p&gt;Kafka의 ISR 모델이 유일한 복제 방식은 아니다. 다른 접근과 비교하면 Kafka의 선택이 왜 이런지 보인다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;접근&lt;/th&gt;
&lt;th&gt;작동&lt;/th&gt;
&lt;th&gt;Kafka ISR과의 차이&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;동기 전 복제&lt;/strong&gt;(모든 쓰기마다 전원 동기 대기)&lt;/td&gt;
&lt;td&gt;쓰기마다 N 복제본 모두 확인&lt;/td&gt;
&lt;td&gt;Kafka도 &lt;code&gt;acks=all&lt;/code&gt;이 유사하지만, ISR(동적)만 대기. ISR 밖 느린 복제본은 기다리지 않음 → 가용성 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;정적 과반수 쿼럼&lt;/strong&gt;(Raft/Paxos)&lt;/td&gt;
&lt;td&gt;고정 N개 중 과반수&lt;/td&gt;
&lt;td&gt;Kafka는 ISR이 동적(느린 복제본 자동 제외). ELR(4.0)이 ISR 부재 시 HW 범위 쿼럼 확대로 가용성 회복&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;chain replication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;leader→follower1→follower2 선형 전파&lt;/td&gt;
&lt;td&gt;Kafka는 leader가 모든 follower에게서 pull(팬아웃). chain보다 병렬 fetch로 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;비동기 복제&lt;/strong&gt;(ack 안 기다림)&lt;/td&gt;
&lt;td&gt;fire-and-forget&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acks=0&lt;/code&gt;과 같음. 가장 빠르나 신뢰성 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Kafka의 설계 철학: &lt;strong&gt;신뢰성(&lt;code&gt;acks=all&lt;/code&gt; + ISR)과 가용성(ISR 동적 + ELR)의 균형&lt;/strong&gt;. 모든 복제본을 강제 동기 대기하면 느린 하나가 전체를 끌어내리고(가용성 손실), 반대로 비동기만 쓰면 장애 시 데이터 유실이다. Kafka는 &amp;quot;충분히 따라잡은 복제본(ISR)만 commit에 참여시키고, 뒤처진 건 제외해 처리량을 지킨다&amp;quot;는 중간 노선을 택했다. ELR은 이 균형을 가용성 쪽으로 더 밀어준 4.0의 개선이다.&lt;/p&gt;
&lt;h2&gt;복제 관련 자주 묻는 추가 질문&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;th&gt;답&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;ISR이 1개면 commit이 의미 있나&amp;quot;&lt;/td&gt;
&lt;td&gt;사실상 없음. leader만 확인하면 commit되므로 leader 장애 시 미commit 손실. 그래서 &lt;code&gt;min.insync.replicas≥2&lt;/code&gt; 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;follower가 전부 느리면 producer는?&amp;quot;&lt;/td&gt;
&lt;td&gt;HW가 전진 안 해 &lt;code&gt;acks=all&lt;/code&gt; 대기만 길어짐 → 타임아웃/처리량 저하. ISR 설정·broker 용량 점검 신호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;read도 leader만?&amp;quot;&lt;/td&gt;
&lt;td&gt;기본 yes(follower는 복제만). follower read는 별도 설정이 있으나 일반적이진 않음 — 순서/일관성 복잡도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;복제본 수를 나중에 바꿀 수 있나&amp;quot;&lt;/td&gt;
&lt;td&gt;가능(reassignment). 늘리는 건 온라인, 줄이는 건 즉시. 데이터 이동 동반&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;under-replicated partition (URP) — 운영 핵심 경보&lt;/h2&gt;
&lt;p&gt;ISR &amp;lt; 복제본 전체 수인 partition을 &lt;strong&gt;URP&lt;/strong&gt;라 한다. 즉 일부 복제본이 따라잡지 못해 장애 내성이 떨어진 상태. &lt;strong&gt;운영 핵심 경보 지표&lt;/strong&gt; — URP가 지속되면 복제 지연·broker 불건강 신호. 모니터링에서 URP 수를 잡는 게 Kafka 운영의 기본이다(장 12).&lt;/p&gt;
&lt;h2&gt;Kafka 4.3 실습 — 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;사전: &lt;strong&gt;다중 broker 필요&lt;/strong&gt;(3 broker KRaft). 단일 노드(&lt;code&gt;replication.factor=1&lt;/code&gt;)에선 ISR/복제가 의미 없음.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;복제 상태 확인&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Kafka 4.3, KRaft 다중 broker
bin/kafka-topics.sh --describe --topic rep-demo --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;Topic: rep-demo  Partition: 0  Leader: 1  Replicas: 1,2,3  Isr: 1,2,3
Topic: rep-demo  Partition: 1  Leader: 2  Replicas: 2,3,1  Isr: 1,2,3&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;확인할 것&lt;/strong&gt;: &lt;code&gt;Leader&lt;/code&gt;(partition별), &lt;code&gt;Replicas&lt;/code&gt;(전체 복제본), &lt;code&gt;Isr&lt;/code&gt;(동기화된 복제본). &lt;code&gt;Isr&lt;/code&gt;이 &lt;code&gt;Replicas&lt;/code&gt;와 같으면 정상, 일부 빠지면 URP.&lt;/p&gt;
&lt;h3&gt;broker 장애 시나리오 (개념)&lt;/h3&gt;
&lt;p&gt;한 broker 프로세스를 종료하고 &lt;code&gt;--describe&lt;/code&gt;로 &lt;code&gt;Leader&lt;/code&gt;/&lt;code&gt;Isr&lt;/code&gt; 변화 관찰:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;종료된 broker가 leader였던 partition → ISR 내 다른 broker가 leader 선출.&lt;/li&gt;
&lt;li&gt;종료된 broker가 ISR에서 제거됨.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min.insync.replicas=2&lt;/code&gt;, 복제본 3 → 1대 종료 후에도 쓰기 유지.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;단일 노드 환경에선 복제 실습 불가. 다중 broker 구성 필요(또는 Docker compose 3 broker).&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;broker 장애 시뮬레이션 — 단계별 관찰 가이드&lt;/h3&gt;
&lt;p&gt;다중 broker(3대) 환경에서 복제 동작을 직접 보는 절차:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;준비&lt;/strong&gt;: partition 3, &lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt; topic 생성. 메시지 produce.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;정상 상태 캡처&lt;/strong&gt;: &lt;code&gt;kafka-topics.sh --describe&lt;/code&gt;로 &lt;code&gt;Leader&lt;/code&gt;/&lt;code&gt;Replicas&lt;/code&gt;/&lt;code&gt;Isr&lt;/code&gt;가 모두 같은지(URP 아님) 확인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;leader broker 종료&lt;/strong&gt;: leader였던 broker 프로세스 kill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;즉시 describe&lt;/strong&gt;: &lt;code&gt;Leader&lt;/code&gt;가 ISR 안 다른 broker로 바뀌었는지, &lt;code&gt;Isr&lt;/code&gt;에서 종료된 broker가 빠졌는지 확인. epoch 증가는 메타데이터로 직접 보긴 어려우나 리더 변경으로 간접 확인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쓰기 계속&lt;/strong&gt;: producer로 메시지 보내 — &lt;code&gt;min.insync=2&lt;/code&gt;, ISR=2이므로 &lt;strong&gt;쓰기 성공&lt;/strong&gt;하는지.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;종료 broker 복구&lt;/strong&gt;: broker 재기동. ISR에 복귀하는지(&lt;code&gt;Isr&lt;/code&gt;에 다시 나타남) 확인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터 무결성&lt;/strong&gt;: &lt;code&gt;--describe&lt;/code&gt;의 &lt;code&gt;Isr == Replicas&lt;/code&gt; 복귀 + consumer로 누락·중복 없는지 최종 확인.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 절차가 &lt;code&gt;f+1&lt;/code&gt; 보장을 눈으로 확인하는 가장 확실한 방법이다. 핵심은 &amp;quot;leader 장애 → ISR 안 이관 → commit 데이터 무손실 → 복구 시 ISR 복귀&amp;quot; 흐름 전체를 한 번에 보는 것.&lt;/p&gt;
&lt;h2&gt;복제 건강 운영 체크리스트&lt;/h2&gt;
&lt;p&gt;복제를 프로덕션에서 운영할 때 매일 확인해야 할 것들:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;URP 수 = 0&lt;/strong&gt;(또는 일시적 스파이크만). 지속 URP는 복제 지연.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;ISR shrink/expand 빈도&lt;/strong&gt; 낮음. 잦으면 broker 불건강(GC·디스크·네트워크).&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;&lt;code&gt;min.insync.replicas&lt;/code&gt; ≤ ISR&lt;/strong&gt; 유지. ISR 하락 시 쓰기 거부 사전 경보.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;&lt;code&gt;replication.factor=3&lt;/code&gt;&lt;/strong&gt; (프로덕션). 1~2는 데이터 유실 위험.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;&lt;code&gt;acks=all&lt;/code&gt;&lt;/strong&gt; 기본. &lt;code&gt;acks=1&lt;/code&gt;은 미복제 손실 위험.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;복제 throttle&lt;/strong&gt; 적용(reassignment/복구 시 운영 트래픽 보호).&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;&gt; &lt;strong&gt;leader 분산&lt;/strong&gt; 고른 broker에. 한 broker에 leader 몰리면 핫스팟.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 체크리스트는 장 12(운영·JMX 지표)와 직결된다.&lt;/p&gt;
&lt;h2&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;acks=all이면 무조건 안전&amp;quot;&lt;/td&gt;
&lt;td&gt;ISR이 1개 남으면 사실상 신뢰성 없음. &lt;code&gt;min.insync.replicas&lt;/code&gt;와 세트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;복제는 동기 push다&amp;quot;&lt;/td&gt;
&lt;td&gt;follower가 leader에서 &lt;strong&gt;pull(fetch)&lt;/strong&gt;. 비동기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;replication.factor=2면 1대 장애 감내&amp;quot;&lt;/td&gt;
&lt;td&gt;가용성 한계. 권장 최소 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;leader 죽으면 데이터 손실&amp;quot;&lt;/td&gt;
&lt;td&gt;ISR 안 follower가 이어받으면 commit된 메시지 손실 없음. 손실 위험은 unclean 선출 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;ELR은 ISR을 무시한다&amp;quot;&lt;/td&gt;
&lt;td&gt;아님. ISR 우선, ISR 부재 시 &lt;strong&gt;HW 범위 내&lt;/strong&gt;에서 안전하게 선출 확대&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;min.insync.replicas 높을수록 좋다&amp;quot;&lt;/td&gt;
&lt;td&gt;너무 높으면 ISR 하락 시 쓰기 전면 거부(가용성 손실)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;HW와 LEO는 같다&amp;quot;&lt;/td&gt;
&lt;td&gt;HW = min(ISR LEO), LEO = 각 복제본 끝. consumer는 HW까지만 읽음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;더 깊이&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;unclean leader election 상세&lt;/strong&gt;: &lt;code&gt;unclean.leader.election.enable=true&lt;/code&gt;면 ISR 전멸 시 ISR 밖 복제본이 leader(가용성 회복, 단 HW 이후 commit 데이터 손실 가능). false면 ISR 회복까지 쓰기 중단(신뢰성 우선).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ELR이 &amp;quot;엄격 min ISR&amp;quot; 문제를 푸는 방식&lt;/strong&gt;: 기존은 ISR &amp;lt; min.insync.replicas → HW 멈춤 → 쓰기 거부 → 가용성 손실. ELR은 ISR이 아닌 복제본도 &lt;strong&gt;HW 범위 내에서&lt;/strong&gt; leader 후보에 추가 → HW를 전진시켜 가용성 회복, 동시에 이미 commit된 데이터(HW 이하)는 보존. 신뢰성-가용성 균형의 4.0 해법.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;요약 — 이 글의 결론&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;partition = 복제된 로그&lt;/strong&gt;. leader 결정 순서, follower가 pull로 복제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISR&lt;/strong&gt; = 충분히 따라잡은 복제본 집합(동적). leader 후보는 ISR.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;commit = ISR 전원 수신&lt;/strong&gt;. &lt;strong&gt;HW&lt;/strong&gt; = ISR 모두의 LEO 최소 = consumer 노출 경계. consumer는 HW까지만 읽음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;f+1 복제본 → f 장애까지 commit 메시지 보존&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;min.insync.replicas&lt;/code&gt; + &lt;code&gt;acks=all&lt;/code&gt;&lt;/strong&gt; = 신뢰성 강제. ISR 미달 시 쓰기 거부. 프로덕션 표준: &lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, &lt;code&gt;acks=all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ELR(4.0, 4.1 기본)&lt;/strong&gt;: ISR 부재 시 HW 범위 내 복제본까지 leader 후보 확대(가용성 회복).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;leader epoch&lt;/strong&gt;: leader 교체 시 로그 발산 방지(truncate 옛 leader 꼬리). replication 정확성 근간.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;produce→commit&lt;/strong&gt;: HW는 모든 ISR의 fetch 확인 후 전진. &lt;code&gt;acks=all&lt;/code&gt;은 이 HW 전진을 기다림.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URP&lt;/strong&gt;(under-replicated partition) = 핵심 운영 경보. ISR shrink/expand 빈도도 건강 지표.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;설계 철학&lt;/strong&gt;: ISR 동적 쿼럼으로 신뢰성(&lt;code&gt;acks=all&lt;/code&gt;+ISR)과 가용성(ISR 제외+ELR) 균형.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;복제 설정&lt;/strong&gt;: &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt;·&lt;code&gt;num.replica.fetchers&lt;/code&gt;·&lt;code&gt;min.insync.replicas&lt;/code&gt;로 ISR 안정성·처리량 튜닝.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;f+1&lt;/code&gt; 복제본이 &lt;code&gt;f&lt;/code&gt; 장애까지 commit 메시지를 잃지 않는 이유를 ISR 관점에서 설명하라.&lt;/li&gt;
&lt;li&gt;HW와 LEO의 차이. consumer가 HW까지만 읽는 이유는?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acks=all&lt;/code&gt; + &lt;code&gt;min.insync.replicas=2&lt;/code&gt; + &lt;code&gt;replication.factor=3&lt;/code&gt; 설정에서 1 broker 장애 시 쓰기는?&lt;/li&gt;
&lt;li&gt;follower가 ISR에서 제외되는 조건 두 가지를 말하라.&lt;/li&gt;
&lt;li&gt;ELR(4.0)이 해결한 &amp;quot;엄격 min ISR&amp;quot;의 문제는?&lt;/li&gt;
&lt;li&gt;복제가 동기 push가 아니라 비동기 pull인 것이 &lt;code&gt;acks=all&lt;/code&gt;의 의미에 어떤 영향을 주는가?&lt;/li&gt;
&lt;li&gt;URP가 운영 경보의 핵심인 이유는? ISR shrink/expand 빈도는 무엇을 시사하는가?&lt;/li&gt;
&lt;li&gt;leader epoch가 없다면 leader 교체 시 어떤 문제가 생기는가?&lt;/li&gt;
&lt;li&gt;동기 전 복제(모든 복제본 강제 대기) 대신 Kafka가 ISR 동적 쿼럼을 택한 이유는?&lt;/li&gt;
&lt;li&gt;broker 장애 시뮬레이션에서 &amp;quot;leader 이관 → 쓰기 유지 → ISR 복귀&amp;quot; 흐름이 f+1 보장을 어떻게 보여주는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;Design — Replication / Replicated Logs&lt;/a&gt; - 접근 2026-07-09 (ISR, HW, commit 정의, f+1)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/eligible-leader-replicas.md&quot;&gt;Eligible Leader Replicas (4.0)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/basic-kafka-operations.md&quot;&gt;Basic Operations — replication&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;KIP-392(ELR 계열), KIP-101(leader epoch로 복제 정확성), unclean leader election 관련 설정&lt;/li&gt;
&lt;li&gt;broker-configs.md(&lt;code&gt;replica.lag.time.max.ms&lt;/code&gt;, &lt;code&gt;num.replica.fetchers&lt;/code&gt; 등 복제 튜닝) - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>elr</category>
      <category>high-watermark</category>
      <category>ISR</category>
      <category>kafka</category>
      <category>leader-election</category>
      <category>replication</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/17</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-06-replication#entry17comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:26:32 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 05. consumer group</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-consumer-group</link>
      <description>&lt;h1&gt;consumer group &amp;mdash; topic을 여러 consumer가 나눠 읽는 팀&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orders&lt;/code&gt; topic에 주문이 초당 수만 건 쏟아진다. consumer 하나로는 다 못 읽는다. consumer를 여럿 띄워 병렬로 읽고 싶다 &amp;mdash; 하지만 같은 메시지를 두 consumer가 동시에 읽으면 중복이 생긴다. 이걸 해결하는 게 &lt;b&gt;consumer group&lt;/b&gt;이다. &lt;b&gt;여러 consumer가 topic의 partition을 서로 겹치지 않게 나눠 읽는 팀&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 consumer group이 뭔지, 어떻게 partition을 나누는지, consumer가 들락날락할 때 일어나는 &lt;b&gt;rebalance&lt;/b&gt;와 그 비용, 그리고 &lt;b&gt;실제로 어떻게 구성하고 관리하는지&lt;/b&gt;까지 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consumer group이란 &amp;mdash; partition을 나눠 읽는 consumer 팀&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer group은 &lt;b&gt;같은 group 이름을 가진 consumer들의 모임&lt;/b&gt;이다. 핵심 규칙은 한 줄이다:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한 consumer group 안에서, partition 1개는 정확히 한 consumer가 담당한다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구성&lt;/th&gt;
&lt;th&gt;무슨 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;partition 4개 + consumer 2명&lt;/td&gt;
&lt;td&gt;각 consumer가 2 partition씩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;partition 4개 + consumer 4명&lt;/td&gt;
&lt;td&gt;각 consumer가 1 partition씩 (최대 병렬)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;partition 4개 + consumer 6명&lt;/td&gt;
&lt;td&gt;4개만 일하고 &lt;b&gt;2개는 놀음(idle)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 &quot;1 partition = 1 consumer&quot;인가 &amp;mdash; 같은 partition을 두 consumer가 동시에 읽으면 순서가 꼬이고 offset commit이 충돌한다. 그래서 한 partition은 한 consumer만 단독 소유. &lt;b&gt;병렬성의 상한 = partition 수&lt;/b&gt;다(장 02).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;consumer group은 어떻게 쓰이나 &amp;mdash; worked scenario&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orders&lt;/code&gt; topic(partition 6개)을 consumer 3개로 확장:&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart TB
    T[&quot;orders topic&amp;lt;br/&amp;gt;(partition 0 ~ 5)&quot;] --&amp;gt; A[&quot;consumer A&amp;lt;br/&amp;gt;(p0, p1)&quot;]
    T --&amp;gt; B[&quot;consumer B&amp;lt;br/&amp;gt;(p2, p3)&quot;]
    T --&amp;gt; C[&quot;consumer C&amp;lt;br/&amp;gt;(p4, p5)&quot;]
    subgraph GP[&quot;group: order-processors&quot;]
        A
        B
        C
    end&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;consumer 3개를 같은 group(&lt;code&gt;order-processors&lt;/code&gt;)으로 띄운다.&lt;/li&gt;
&lt;li&gt;Kafka가 partition을 나눠 줌 &amp;mdash; A: p0,p1 / B: p2,p3 / C: p4,p5.&lt;/li&gt;
&lt;li&gt;각 consumer가 자기 partition만 읽어 처리 &amp;rarr; &lt;b&gt;3배 병렬, 중복 없음&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 group은 독립적&lt;/b&gt;: &lt;code&gt;order-processors&lt;/code&gt; group과 &lt;code&gt;analytics&lt;/code&gt; group은 서로 간섭 없이 같은 &lt;code&gt;orders&lt;/code&gt; topic을 전부 각자 읽는다. group마다 자기 offset을 따로 관리한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consumer group 설정 &amp;mdash; 어떻게 구성하나&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;consumer 설정 (Java)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# group 식별자 (핵심 &amp;mdash; 같은 group.id = 같은 팀)
group.id=order-processors

# broker 연결
bootstrap.servers=localhost:9092

# 역직렬화
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
value.deserializer=org.apache.kafka.common.serialization.StringDeserializer

# auto-commit (신뢰성 필요시 false, 장 04)
enable.auto.commit=false

# partition 할당 전략
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor

# rebalance 관련 (차세대 프로토콜 사용시 일부 무시)
session.timeout.ms=45000
heartbeat.interval.ms=3000
max.poll.interval.ms=300000&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition 할당 전략&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RangeAssignor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;topic별로 partition을 범위로 나눔 (오랜 기본)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RoundRobinAssignor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;round-robin으로 순환 분배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;StickyAssignor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;재할당 시 기존 할당 최대한 유지 (이동 최소화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CooperativeStickyAssignor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;sticky + 증분 교환 (KIP-429, 권장)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차세대 rebalance 프로토콜(4.0 GA) 활성 시 &lt;code&gt;partition.assignment.strategy&lt;/code&gt;는 사용 불가. 프로토콜이 자체 관리.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;rebalance &amp;mdash; consumer가 들락날락하면 partition을 다시 나눈다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer group의 구성원이 바뀌면 partition을 재분배한다(&lt;b&gt;rebalance&lt;/b&gt;):&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consumer &lt;b&gt;합류&lt;/b&gt;(새 consumer 띄움) 또는 &lt;b&gt;이탈&lt;/b&gt;(크래시&amp;middot;종료).&lt;/li&gt;
&lt;li&gt;구독 topic/partition &lt;b&gt;변경&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;rebalance의 비용 &amp;mdash; stop-the-world&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구형(eager) 프로토콜에선 rebalance 시작 시 &lt;b&gt;모든 consumer가 partition을 내려놓고&lt;/b&gt; 전체를 다시 나눈다 &amp;mdash; 끝날 때까지 &lt;b&gt;아무도 소비 못함&lt;/b&gt;. consumer가 많을수록 멈춤이 길어진다. &quot;consumer를 함부로 재시작하면 안 된다&quot;는 이유.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;cooperative rebalance &amp;mdash; 변경분만 교체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 4.0에선 &lt;b&gt;차세대 rebalance 프로토콜&lt;/b&gt;이 GA됐다 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/consumer-rebalance-protocol.md&quot;&gt;(consumer-rebalance-protocol.md)&lt;/a&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유지 가능한 partition은 그대로&lt;/b&gt;, &lt;b&gt;변경이 필요한 partition만 교체&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;나머지 consumer는 계속 소비 &amp;rarr; 멈춤 최소화.&lt;/li&gt;
&lt;li&gt;활성화: &lt;code&gt;group.protocol=consumer&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차세대 프로토콜 활성 시 &lt;code&gt;heartbeat.interval.ms&lt;/code&gt;, &lt;code&gt;session.timeout.ms&lt;/code&gt;, &lt;code&gt;partition.assignment.strategy&lt;/code&gt; 사용 불가.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ConsumerRebalanceListener &amp;mdash; 상태 정리 핵심 훅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partition이 할당/해제될 때 콜백을 받아 상태를 정리한다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;consumer.subscribe(Arrays.asList(&quot;orders&quot;), new ConsumerRebalanceListener() {
    @Override
    public void onPartitionsRevoked(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        // partition을 잃기 전에 처리 중인 offset을 commit (유실 방지)
        consumer.commitSync();
        // state store 정리 등
    }

    @Override
    public void onPartitionsAssigned(Collection&amp;lt;TopicPartition&amp;gt; partitions) {
        // 새 partition 할당 시 초기화 (예: offset seek, 캐시 갱신)
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 훅이 rebalance 중 &lt;b&gt;데이터 유실/중복을 막는 핵심&lt;/b&gt;이다. 멱등 처리와 함께 써야 at-least-once를 지킨다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka 4.3 실습 &amp;mdash; 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전: KRaft 단일 노드, &lt;code&gt;grp-demo&lt;/code&gt; topic(&lt;code&gt;--partitions 4&lt;/code&gt;), 메시지 다수 사전 produce.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;group 상태와 할당 확인&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# Kafka 4.3, KRaft 단일 노드
bin/kafka-consumer-groups.sh --describe --group g-state --state --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;COORDINATOR (ID)     ASSIGNMENT-STRATEGY   STATE     #MEMBERS
localhost:9092 (0)   range                 Stable    4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: &lt;code&gt;STATE&lt;/code&gt;(Stable/PreparingRebalance/CompletingRebalance), 멤버 수.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition 할당 분포&lt;/h3&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;bin/kafka-consumer-groups.sh --describe --group g-state --members --verbose --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중 consumer 병렬 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 3개를 열어 같은 group으로 consumer를 각각 띄운다:&lt;/p&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;# 터미널 1
bin/kafka-console-consumer.sh --topic grp-demo --group demo-group --bootstrap-server localhost:9092
# 터미널 2 (같은 group)
bin/kafka-console-consumer.sh --topic grp-demo --group demo-group --bootstrap-server localhost:9092
# 터미널 3 (같은 group)
bin/kafka-console-consumer.sh --topic grp-demo --group demo-group --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 produce하면 3개 consumer가 partition을 나눠 각자 다른 메시지를 받는다. 한 consumer를 종료(Ctrl+C)하면 &amp;rarr; rebalance &amp;rarr; 남은 consumer가 그 partition을 이어받는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;차세대 rebalance 프로토콜 활성화&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# consumer 설정에 추가
group.protocol=consumer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정으로 4.0 차세대 프로토콜 활성 &amp;rarr; rebalance 멈춤 최소화.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;consumer를 늘리면 무조건 빨라진다&quot;&lt;/td&gt;
&lt;td&gt;partition 수가 상한. 초과분은 idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;다른 consumer group은 메시지를 나눠 갖는다&quot;&lt;/td&gt;
&lt;td&gt;각 그룹이 &lt;b&gt;독립적으로&lt;/b&gt; 전체 소비&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;rebalance는 순식간이라 무시해도 된다&quot;&lt;/td&gt;
&lt;td&gt;구형에선 stop-the-world. 빈번 재시작은 처리량 저하&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;partition.assignment.strategy는 항상 설정해야&quot;&lt;/td&gt;
&lt;td&gt;차세대 프로토콜 활성 시 사용 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;static membership(KIP-345)&lt;/b&gt;: &lt;code&gt;group.instance.id&lt;/code&gt;로 고정 identity. 재시작 시 rebalance 최소화 &amp;rarr; 상태 저장 앱에 유리.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;assignment 전략 선택&lt;/b&gt;: CooperativeStickyAssignor(권장)가 가장 적은 이동으로 rebalance.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rebalance 리스너와 at-least-once&lt;/b&gt;: &lt;code&gt;onPartitionsRevoked&lt;/code&gt;에서 commit하지 않으면, partition 이관 시 미처리 메시지가 중복/유실.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;consumer group&lt;/b&gt; = 같은 group 이름의 consumer 모임. partition을 &lt;b&gt;겹치지 않게&lt;/b&gt; 나눠 읽음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 규칙&lt;/b&gt;: partition 1개 = consumer 1명. 병렬성 상한 = partition 수.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;group 간 독립&lt;/b&gt;: 각 group이 전체 topic을 따로 소비(자기 offset 관리).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정&lt;/b&gt;: &lt;code&gt;group.id&lt;/code&gt;가 팀 식별자. &lt;code&gt;partition.assignment.strategy&lt;/code&gt;로 할당 알고리즘 선택.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rebalance&lt;/b&gt;: 멤버십 변화 시 재분배. 구형=stop-the-world, &lt;b&gt;차세대(4.0 GA, &lt;code&gt;group.protocol=consumer&lt;/code&gt;)&lt;/b&gt;=변경분만 교체.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ConsumerRebalanceListener&lt;/b&gt;: &lt;code&gt;onPartitionsRevoked&lt;/code&gt;에서 commit+정리 &amp;rarr; rebalance 중 유실/중복 방지.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;consumer group이 &quot;partition을 겹치지 않게 나눠 읽는 팀&quot;이라는 것이 왜 필요한가?&lt;/li&gt;
&lt;li&gt;partition 6개에 consumer 4개. 각 consumer는 몇 partition? 8개로 늘리면?&lt;/li&gt;
&lt;li&gt;주문 처리 group과 분석 group이 같은 topic을 읽을 때 서로 간섭이 없는 이유는?&lt;/li&gt;
&lt;li&gt;rebalance가 일어나는 상황 세 가지. 구형에서 왜 &quot;stop-the-world&quot;가 문제인가?&lt;/li&gt;
&lt;li&gt;차세대 rebalance 프로토콜(4.0)을 활성화하는 설정은?&lt;/li&gt;
&lt;li&gt;ConsumerRebalanceListener의 &lt;code&gt;onPartitionsRevoked&lt;/code&gt;에서 해야 할 일은?&lt;/li&gt;
&lt;li&gt;CooperativeStickyAssignor가 기본 RangeAssignor보다 나은 점은?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumer.java&quot;&gt;KafkaConsumer.java (consumer group, rebalance)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/consumer-rebalance-protocol.md&quot;&gt;Consumer Rebalance Protocol (4.0 GA)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/documentation/consumer-configs.txt&quot;&gt;Consumer configs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;KIP-429(cooperative rebalance), KIP-345(static membership), KIP-848(차세대 rebalance protocol)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>assignment</category>
      <category>consumer-group</category>
      <category>cooperative</category>
      <category>kafka</category>
      <category>rebalance</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/16</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-consumer-group#entry16comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:24:46 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 04. consumer</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-consumer</link>
      <description>&lt;h1&gt;consumer &amp;mdash; topic에서 메시지를 읽는 주체&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orders&lt;/code&gt; topic에 주문 이벤트가 쌓인다. 이제 그 메시지를 읽어 처리해야 한다 &amp;mdash; 주문 처리 서비스가 읽어가고, 분석 서비스가 따로 읽는다. 이 &lt;b&gt;&quot;topic에서 메시지를 읽는 주체&quot;&lt;/b&gt;가 consumer다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 consumer가 뭔지부터 시작해, &lt;b&gt;실제 설정과 코드&lt;/b&gt;, auto-commit의 위험, LAG 모니터링까지 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consumer란 &amp;mdash; topic에서 메시지를 읽는 클라이언트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer도 producer처럼 &lt;b&gt;앱에 내장되는 클라이언트 라이브러리&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java consumer 기본 코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 1. 설정
Properties props = new Properties();
props.put(&quot;bootstrap.servers&quot;, &quot;localhost:9092&quot;);
props.put(&quot;group.id&quot;, &quot;order-processors&quot;);
props.put(&quot;key.deserializer&quot;, &quot;org.apache.kafka.common.serialization.StringDeserializer&quot;);
props.put(&quot;value.deserializer&quot;, &quot;org.apache.kafka.common.serialization.StringDeserializer&quot;);
props.put(&quot;enable.auto.commit&quot;, &quot;false&quot;);  // 수동 commit (신뢰성)

// 2. consumer 생성 + 구독
Consumer&amp;lt;String, String&amp;gt; consumer = new KafkaConsumer&amp;lt;&amp;gt;(props);
consumer.subscribe(Arrays.asList(&quot;orders&quot;));

// 3. poll loop
while (true) {
    ConsumerRecords&amp;lt;String, String&amp;gt; records = consumer.poll(Duration.ofMillis(500));
    for (ConsumerRecord&amp;lt;String, String&amp;gt; record : records) {
        processOrder(record.value());  // 비즈니스 처리
    }
    consumer.commitSync();  // 처리 후 commit (at-least-once)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;consumer는 어떻게 쓰이나 &amp;mdash; worked scenario&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 처리 서비스: &lt;code&gt;orders&lt;/code&gt; topic을 구독 &amp;rarr; &lt;code&gt;poll()&lt;/code&gt;로 새 주문 batch 당겨옴 &amp;rarr; 각 주문 처리(재고&amp;middot;결제&amp;middot;배송) &amp;rarr; offset commit.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    T[orders topic] --&amp;gt;|poll| C[consumer]
    C --&amp;gt;|처리| B[(재고&amp;middot;결제)]
    C --&amp;gt;|commit| OS[&quot;__consumer_offsets&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consumer는 pull이다 &amp;mdash; broker가 밀어넣지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;producer가 쓰면 consumer에게 push된다&quot;가 아니다. &lt;b&gt;consumer가 능동적으로 &lt;code&gt;poll()&lt;/code&gt;로 당겨온다.&lt;/b&gt; broker는 요청 없이 안 보낸다. 왜 pull &amp;mdash; consumer가 자기 속도 제어(backpressure), replay(과거부터 다시) 자연스러움.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;offset &amp;mdash; 읽기 커서, 영속 저장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consumer는 &quot;어디까지 읽었나&quot;를 &lt;b&gt;offset&lt;/b&gt;으로 기억:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;__consumer_offsets&lt;/code&gt; &lt;b&gt;내부 topic&lt;/b&gt;에 영구 저장(메모리 아님).&lt;/li&gt;
&lt;li&gt;compacted &amp;rarr; 같은 key의 최신 offset만 유지.&lt;/li&gt;
&lt;li&gt;consumer 장애 후 재시작해도 이어서 읽음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;auto-commit의 위험 &amp;mdash; 메시지가 조용히 사라진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값 &lt;code&gt;enable.auto.commit=true&lt;/code&gt;는 &lt;b&gt;poll 직후&lt;/b&gt;(처리 전)에 offset을 넘길 수 있다. 처리 중 크래시 &amp;rarr; 재시작 시 이미 commit된 offset부터 &amp;rarr; &lt;b&gt;그 메시지들 영영 유실&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sequenceDiagram
    participant C as consumer (auto-commit)
    participant B as broker
    C-&amp;gt;&amp;gt;B: poll() &amp;rarr; batch[m1,m2,m3]
    Note over C: auto-commit: offset=m4 (처리 전!)
    C-&amp;gt;&amp;gt;C: m1 처리 중 크래시
    Note over C: 재시작 &amp;rarr; 다음 poll은 m4부터
    Note over C: m1,m2,m3 유실&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해법&lt;/b&gt;: &lt;code&gt;enable.auto.commit=false&lt;/code&gt; + 처리 후 &lt;b&gt;수동 commit&lt;/b&gt; &amp;rarr; at-least-once. 단, 재시도 시 &lt;b&gt;중복 가능&lt;/b&gt; &amp;rarr; 처리 로직은 &lt;b&gt;멱등&lt;/b&gt;(같은 입력 여러 번 처리해도 같은 결과)이어야 함.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consumer.properties &amp;mdash; 전체 설정 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신뢰성 우선 (수동 commit)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;bootstrap.servers=localhost:9092
group.id=order-processors

key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
value.deserializer=org.apache.kafka.common.serialization.StringDeserializer

# 수동 commit (신뢰성)
enable.auto.commit=false

# poll loop 건강
max.poll.records=500
max.poll.interval.ms=300000
session.timeout.ms=45000
heartbeat.interval.ms=3000

# offset reset (committed 없을 때)
auto.offset.reset=earliest&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리량 튜닝&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;fetch.min.bytes=1048576    # 1MB까지 모아서 fetch
max.poll.records=1000&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;kafka-consumer-groups.sh &amp;mdash; group 관리 명령&lt;/h2&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# group 목록
bin/kafka-consumer-groups.sh --list --bootstrap-server localhost:9092

# group 상세 (offset, LAG)
bin/kafka-consumer-groups.sh --describe --group order-processors --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GROUP             TOPIC       PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
order-processors  orders      0          150             155             5
order-processors  orders      1          200             200             0&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LAG &amp;mdash; consumer가 못 따라가는 정도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LAG = LOG-END-OFFSET &amp;minus; CURRENT-OFFSET&lt;/code&gt;. LAG이 크고 자라면 consumer가 처리량 못 감당 &amp;rarr; consumer 추가(&amp;le; partition 수) 또는 처리 경량화.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;offset reset&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# group의 offset을 처음으로 reset
bin/kafka-consumer-groups.sh --reset-offsets --group order-processors \
  --topic orders --to-earliest --execute --bootstrap-server localhost:9092

# 특정 offset으로 reset
bin/kafka-consumer-groups.sh --reset-offsets --group order-processors \
  --topic orders:0 --to-offset 100 --execute --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka 4.3 실습 &amp;mdash; 직접 확인하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;읽기 + offset 확인&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# Kafka 4.3 &amp;mdash; 신규 group, 처음부터
bin/kafka-console-consumer.sh --topic cons-demo --group g1 \
  --from-beginning --bootstrap-server localhost:9092

# 같은 group으로 재실행 &amp;rarr; committed offset부터 (출력 없음, 이미 끝)
bin/kafka-console-consumer.sh --topic cons-demo --group g1 \
  --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: 같은 group이면 committed offset 이어받음(&lt;code&gt;--from-beginning&lt;/code&gt; 무시됨).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LAG 확인&lt;/h3&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;bin/kafka-consumer-groups.sh --describe --group g1 --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;consumer는 broker가 push&quot;&lt;/td&gt;
&lt;td&gt;pull(poll) 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;auto-commit이 안전하다&quot;&lt;/td&gt;
&lt;td&gt;반대. 처리 전 commit &amp;rarr; 유실 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;offset은 메모리에만&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__consumer_offsets&lt;/code&gt;에 영구 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;--from-beginning이 항상 처음부터&quot;&lt;/td&gt;
&lt;td&gt;committed offset 있으면 무시됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;처리가 느려도 괜찮다&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max.poll.interval.ms&lt;/code&gt; 초과 &amp;rarr; 그룹 퇴출&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;position vs committed offset&lt;/b&gt;: position(메모리, 다음 읽을 offset) vs committed(영구). 차이가 중복의 원인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;partition별 개별 commit&lt;/b&gt;: &lt;code&gt;commitSync(offsets)&lt;/code&gt;로 처리 완료한 partition만 부분 commit.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;auto.offset.reset&lt;/code&gt;&lt;/b&gt;: committed 없을 때만 동작(earliest/latest/none).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;consumer&lt;/b&gt; = 앱 내장 클라이언트. &lt;code&gt;poll()&lt;/code&gt;로 topic에서 읽어 처리. pull 기반.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;offset&lt;/b&gt; = 읽기 커서. &lt;code&gt;__consumer_offsets&lt;/code&gt;에 영구 저장.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;auto-commit 위험&lt;/b&gt;: 처리 전 commit &amp;rarr; 유실. &lt;code&gt;enable.auto.commit=false&lt;/code&gt; + 수동 commit &amp;rarr; at-least-once.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;at-least-once &amp;rarr; 중복 가능&lt;/b&gt; &amp;rarr; 처리 로직은 멱등 필수.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consumer.properties&lt;/b&gt;: group.id, enable.auto.commit=false, poll loop 설정.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LAG&lt;/b&gt; = LOG-END-OFFSET &amp;minus; CURRENT-OFFSET. consumer 지연 지표.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consumer-groups.sh&lt;/b&gt;: list, describe(LAG 확인), reset-offsets.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;consumer가 &quot;pull&quot;인 것이 왜 설계상 유리한가?&lt;/li&gt;
&lt;li&gt;offset이 영구 저장된다는 것이 왜 중요한가?&lt;/li&gt;
&lt;li&gt;auto-commit이 메시지 유실을 일으키는 과정을 설명하라.&lt;/li&gt;
&lt;li&gt;수동 commit으로 at-least-once를 달성하면 왜 중복이 생기는가?&lt;/li&gt;
&lt;li&gt;consumer.properties에서 신뢰성을 위한 핵심 설정 3가지는?&lt;/li&gt;
&lt;li&gt;LAG이 자라면 어떤 조치를 취할 수 있는가?&lt;/li&gt;
&lt;li&gt;kafka-consumer-groups.sh로 LAG을 확인하는 명령은?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumer.java&quot;&gt;KafkaConsumer.java (poll loop)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/implementation/distribution.md&quot;&gt;Implementation &amp;mdash; distribution.md (offset tracking)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/documentation/consumer-configs.txt&quot;&gt;Consumer configs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>commit</category>
      <category>config</category>
      <category>consumer</category>
      <category>kafka</category>
      <category>lag</category>
      <category>Offset</category>
      <category>poll</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/15</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-consumer#entry15comment</comments>
      <pubDate>Thu, 9 Jul 2026 17:23:56 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 03. producer</title>
      <link>https://technical-architect.tistory.com/entry/Kafka</link>
      <description>&lt;h1&gt;producer &amp;mdash; 메시지를 topic에 보내는 주체&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쇼핑몰 주문 API가 주문 이벤트를 &lt;code&gt;orders&lt;/code&gt; topic에 넣는다(장 02). 이때 &lt;b&gt;&quot;메시지를 topic에 보내는 주체&quot;&lt;/b&gt;가 producer다. 애플리케이션에 내장되는 클라이언트로, &lt;code&gt;producer.send(...)&lt;/code&gt; 한 줄로 메시지를 쏜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 producer가 뭔지부터 시작해, &lt;b&gt;실제 설정과 코드&lt;/b&gt;로 어떻게 쓰는지, &lt;code&gt;acks&lt;/code&gt;&amp;middot;idempotent producer로 신뢰성을 어떻게 확보하는지까지 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;producer란 &amp;mdash; topic에 메시지를 보내는 클라이언트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer는 &lt;b&gt;앱에 내장되는 클라이언트 라이브러리&lt;/b&gt;다. 별도 서버가 아니라, 주문 API&amp;middot;배치 잡&amp;middot;로그 수집기 같은 앱 프로세스 안에서 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java producer 기본 코드&lt;/h3&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;// 1. 설정
Properties props = new Properties();
props.put(&quot;bootstrap.servers&quot;, &quot;localhost:9092&quot;);
props.put(&quot;key.serializer&quot;, &quot;org.apache.kafka.common.serialization.StringSerializer&quot;);
props.put(&quot;value.serializer&quot;, &quot;org.apache.kafka.common.serialization.StringSerializer&quot;);
props.put(&quot;acks&quot;, &quot;all&quot;);
props.put(&quot;enable.idempotence&quot;, &quot;true&quot;);

// 2. producer 생성
Producer&amp;lt;String, String&amp;gt; producer = new KafkaProducer&amp;lt;&amp;gt;(props);

// 3. 메시지 전송
producer.send(new ProducerRecord&amp;lt;&amp;gt;(&quot;orders&quot;, &quot;order-123&quot;, &quot;주문 데이터&quot;));

// 4. 종료
producer.close();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;producer는 어떻게 쓰이나 &amp;mdash; worked scenario&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 &quot;결제&quot; 버튼을 누르면: 주문 API가 DB에 저장 &amp;rarr; producer로 &lt;code&gt;orders&lt;/code&gt; topic에 이벤트 전송 &amp;rarr; 주문 처리&amp;middot;분석&amp;middot;알림 서비스가 각자 소비(장 02 시나리오).&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    U[결제 클릭] --&amp;gt; API[주문 API]
    API --&amp;gt;|1. DB 저장| DB[(주문 DB)]
    API --&amp;gt;|2. producer.send| P[producer]
    P --&amp;gt;|메시지| T[orders topic]
    T --&amp;gt; C1[주문 처리]
    T --&amp;gt; C2[분석]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;send는 비동기다 &amp;mdash; 즉시 반환, 뒤에서 전송&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;send()&lt;/code&gt;는 &lt;b&gt;비동기&lt;/b&gt;. 즉시 &lt;code&gt;Future&lt;/code&gt;를 반환하고, 실제 전송은 &lt;b&gt;배경 스레드(sender)&lt;/b&gt;가 처리. 애플리케이션이 broker 응답을 기다리며 멈추지 않도록 한다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart LR
    A[&quot;send(record)&quot;] --&amp;gt; ACC[&quot;accumulator&amp;lt;br/&amp;gt;(batch 모음)&quot;]
    ACC --&amp;gt; SND[&quot;sender 스레드&amp;lt;br/&amp;gt;(배경 전송)&quot;]
    SND --&amp;gt; L[&quot;broker&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;send 콜백으로 성공/실패 확인&lt;/h3&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;producer.send(new ProducerRecord&amp;lt;&amp;gt;(&quot;orders&quot;, &quot;order-123&quot;, &quot;data&quot;), (metadata, exception) -&amp;gt; {
    if (exception == null) {
        System.out.println(&quot;전송 성공: &quot; + metadata.partition() + &quot;/&quot; + metadata.offset());
    } else {
        System.err.println(&quot;전송 실패: &quot; + exception.getMessage());
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;batch &amp;mdash; 왜 모아서 보내는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 하나씩 보내지 않고 &lt;b&gt;여러 개를 batch&lt;/b&gt;로 모아 보낸다. &lt;code&gt;linger.ms&lt;/code&gt;(대기)나 &lt;code&gt;batch.size&lt;/code&gt;(크기)에 도달하면 전송. 네트워크 왕복 비용을 줄인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;batch는 &lt;b&gt;압축과 짝&lt;/b&gt; &amp;mdash; batch로 뭉쳐야 압축 효율이 올라간다. &lt;code&gt;linger.ms=0&lt;/code&gt; + 압축은 무의미(메시지 1개씩이라 압축 안 걸림).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;acks &amp;mdash; &quot;얼마나 확실히 썼다고 볼 것인가&quot;&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;acks&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;신뢰성&lt;/th&gt;
&lt;th&gt;언제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;확인 안 기다림&lt;/td&gt;
&lt;td&gt;가장 낮음&lt;/td&gt;
&lt;td&gt;손실 감내 로그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;리더까지만 확인&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;성능 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;(&lt;code&gt;-1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;리더 + ISR 전원 확인&lt;/td&gt;
&lt;td&gt;가장 높음&lt;/td&gt;
&lt;td&gt;신뢰성 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;acks=all&lt;/code&gt;은 &lt;code&gt;min.insync.replicas&lt;/code&gt;(예: 2)와 세트여야 의미 &amp;mdash; ISR이 1개면 사실상 신뢰성 없음. 프로덕션 표준: &lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, &lt;code&gt;acks=all&lt;/code&gt;(장 06).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;idempotent producer &amp;mdash; 재시도해도 중복이 안 생기는 비밀&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시도를 켜면 같은 메시지가 두 번 쓰일 수 있다. &lt;b&gt;idempotent producer&lt;/b&gt;는 broker가 중복을 걸러낸다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;enable.idempotence=true&lt;/code&gt; &amp;rarr; broker가 PID 발급 &amp;rarr; 각 메시지에 sequence number &amp;rarr; 중복 검출.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4.x 기본값은 &lt;code&gt;true&lt;/code&gt;&lt;/b&gt;. 구형 자료(false 기본)와 혼동 주의.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;idempotent 모드는 &lt;code&gt;acks=all&lt;/code&gt;, &lt;code&gt;retries&amp;gt;0&lt;/code&gt;, &lt;code&gt;max.in.flight&amp;lt;=5&lt;/code&gt;를 자동 강제.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;producer.properties &amp;mdash; 전체 설정 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신뢰성 우선 (프로덕션 권장)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 연결
bootstrap.servers=localhost:9092

# 직렬화
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer

# 신뢰성
acks=all
enable.idempotence=true
retries=2147483647
max.in.flight.requests.per.connection=5

# 배치/처리량
linger.ms=10
batch.size=16384
compression.type=zstd

# 재시도 상한 (총)
delivery.timeout.ms=120000
request.timeout.ms=30000&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리량 우선 (로그 수집)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;acks=1
linger.ms=50
batch.size=131072
compression.type=zstd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 지연 수십 ms 희생, 처리량&amp;middot;네트워크 효율 최대.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지연 최소 (실시간 이벤트)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;acks=1
linger.ms=0
compression.type=none&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 처리량 희생, end-to-end 지연 최소.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka 4.3 실습 &amp;mdash; 직접 확인하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;console-producer&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# Kafka 4.3, KRaft 단일 노드
bin/kafka-console-producer.sh --topic prod-demo --bootstrap-server localhost:9092
&amp;gt;Hello
&amp;gt;World&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: 각 줄이 topic에 메시지로 들어감. 다른 터미널에서 consumer로 읽어 확인.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;key와 함께 produce&lt;/h3&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;bin/kafka-console-producer.sh --topic prod-demo \
  --property &quot;parse.key=true&quot; --property &quot;key.separator=:&quot; \
  --bootstrap-server localhost:9092
# userA:evt1 / userA:evt2 / userB:evt3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: 같은 key는 같은 partition으로 감(장 02).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;producer는 별도 서버다&quot;&lt;/td&gt;
&lt;td&gt;앱에 내장되는 &lt;b&gt;클라이언트&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;send() 반환됐으면 broker에 도달했다&quot;&lt;/td&gt;
&lt;td&gt;비동기. accumulator에 있을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;acks=1이면 안전하다&quot;&lt;/td&gt;
&lt;td&gt;리더 장애 시 미복제 손실 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;재시도하면 무조건 중복&quot;&lt;/td&gt;
&lt;td&gt;idempotent(4.x 기본)는 broker가 제거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;linger.ms=0이 항상 빠르다&quot;&lt;/td&gt;
&lt;td&gt;지연은 최소지만 처리량은 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;4.x도 idempotence=false 기본&quot;&lt;/td&gt;
&lt;td&gt;아니다. 4.x는 &lt;b&gt;기본 true&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;압축은 producer만 손해&quot;&lt;/td&gt;
&lt;td&gt;broker가 압축된 채 저장&amp;middot;전송&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;max.in.flight와 순서&lt;/b&gt;: 1보다 크면 재시도 시 순서 뒤바뀔 수 있음. idempotent 모드는 5 이하로 제한해 순서 보존.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;delivery.timeout.ms&lt;/b&gt;: 총 재시도 상한(기본 2분). 시간 기반 모델(무한 retries 아님).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;transactional producer&lt;/b&gt;: &lt;code&gt;transactional.id&lt;/code&gt;로 여러 partition 원자 쓰기 &amp;rarr; EOS(장 08).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JMX metrics&lt;/b&gt;: &lt;code&gt;record-send-rate&lt;/code&gt;, &lt;code&gt;retry-rate&lt;/code&gt;(높으면 불안정), &lt;code&gt;batch-size-avg&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;producer&lt;/b&gt; = 앱 내장 클라이언트. &lt;code&gt;send()&lt;/code&gt;로 topic에 메시지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 send&lt;/b&gt;: accumulator&amp;rarr;sender 배경 전송. 콜백으로 성공/실패 확인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;batch&lt;/b&gt;: &lt;code&gt;linger.ms&lt;/code&gt;/&lt;code&gt;batch.size&lt;/code&gt;로 모아 효율&amp;uarr;. 압축과 세트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;acks&lt;/code&gt;&lt;/b&gt;: 0/1/all. &lt;code&gt;all&lt;/code&gt;은 &lt;code&gt;min.insync.replicas&lt;/code&gt;와 세트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;idempotent producer(4.x 기본)&lt;/b&gt;: PID+sequence로 중복 제거 + 순서 보존.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 프로필&lt;/b&gt;: 신뢰성 우선(acks=all+idempotent) / 처리량 우선(linger 큼) / 지연 최소(linger=0).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;producer가 &quot;앱 안의 클라이언트&quot;라는 것이 운영상 어떤 의미인가?&lt;/li&gt;
&lt;li&gt;send()가 비동기인 것을 어떻게 확인하는가? (콜백)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;acks=0/1/all&lt;/code&gt;의 차이를 복제 관점에서 설명하라.&lt;/li&gt;
&lt;li&gt;idempotent producer가 중복을 어떻게 제거하는가? 4.x 기본값은?&lt;/li&gt;
&lt;li&gt;신뢰성 우선 producer.properties를 작성해 보라.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;linger.ms=0&lt;/code&gt; + compression이 무의미한 이유는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/documentation/producer-configs.txt&quot;&gt;Producer configs (producer-configs.txt)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/producer/ProducerConfig.java&quot;&gt;ProducerConfig.java (idempotence/delivery.timeout)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;KIP-98(idempotent producer/transactions), KIP-588(zombie fencing)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>acks</category>
      <category>Batch</category>
      <category>config</category>
      <category>idempotence</category>
      <category>kafka</category>
      <category>producer</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/14</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka#entry14comment</comments>
      <pubDate>Thu, 9 Jul 2026 16:35:39 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 02. topic and partition</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-topic-and-partition</link>
      <description>&lt;h1&gt;topic과 partition &amp;mdash; 메시지를 어디에, 어떻게 쌓을 것인가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쇼핑몰에서 주문이 들어온다. 이 주문 데이터를 어디에 쌓을까? Kafka에선 &lt;b&gt;topic&lt;/b&gt;이라는 곳에 넣는다. &quot;주문&quot; topic, &quot;클릭 로그&quot; topic, &quot;결제&quot; topic처럼 용도별로 topic을 나눠 메시지를 분류한다. 그런데 주문이 초당 수만 건씩 쏟아지면 topic 하나로는 감당이 안 된다 &amp;mdash; 그래서 topic을 여러 조각으로 쪼개는데, 그 조각이 &lt;b&gt;partition&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Kafka를 처음 접하는 사람이 topic과 partition을 &quot;확&quot; 이해하도록, 기초부터 차근차근 쌓아 올린다. topic이 뭔지, 어떻게 쓰이는지부터 시작해 partition이 왜 필요한지, key가 뭔 역할을 하는지, 그리고 &quot;partition 수를 나중에 바꾸면 생기는 일&quot;까지. 어려운 구현 디테일(murmur2 해시 등)은 마지막 심화로 미뤘다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;topic이란 &amp;mdash; 메시지를 담는 이름 붙인 상자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic은 가장 단순하게 말하면 &lt;b&gt;&quot;메시지를 담는, 이름 붙인 상자(또는 카테고리)&quot;&lt;/b&gt;다. 데이터베이스의 테이블이나 파일시스템의 폴더와 비슷하다 &amp;mdash; 용도별로 나눠 놓는 구분선.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;th&gt;topic에 대응&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;우체국의 &lt;b&gt;수신처란&lt;/b&gt;(주문/청구/문의)&lt;/td&gt;
&lt;td&gt;topic 이름(orders / clicks / payments)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;회사의 &lt;b&gt;사서함 번호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;topic 이름 &amp;mdash; 메시지가 여기로 배달됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터베이스의 &lt;b&gt;테이블&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;topic &amp;mdash; 한 종류의 데이터를 담는 그릇&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;topic의 구체 예&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스에선 이렇게 topic을 나눈다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;orders&lt;/code&gt; &amp;mdash; 주문 이벤트 (주문 생성, 취소, 상태 변경)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user-clicks&lt;/code&gt; &amp;mdash; 사용자 클릭 로그 (UX 분석용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;payments&lt;/code&gt; &amp;mdash; 결제 요청/완료&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shipping-events&lt;/code&gt; &amp;mdash; 배송 상태&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__consumer_offsets&lt;/code&gt; &amp;mdash; (내부용) consumer가 어디까지 읽었나&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic 이름은 용도를 드러내게 짓는 게 관례다. &lt;code&gt;t1&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt; 같은 이름은 운영에서 혼란을 만든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;topic의 핵심 성질 (장 01 복습)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic은 &lt;b&gt;로그(log)&lt;/b&gt;다 &amp;mdash; 메시지가 들어온 순서대로 계속 append되고, consumer가 읽어도 &lt;b&gt;지워지지 않는다&lt;/b&gt;(retention 정책이 닿기 전까지). 그래서 여러 consumer가 같은 topic을 각자 따로 읽을 수 있고, 나중에 다시 읽을(replay) 수도 있다. 이게 일반 &quot;큐&quot;(소비하면 사라짐)와 topic의 결정적 차이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;topic은 어떻게 쓰이나 &amp;mdash; worked scenario&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 정의만으론 감이 안 온다. 쇼핑몰의 &lt;code&gt;orders&lt;/code&gt; topic이 실제로 어떻게 쓰이는지 보자.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    P1[주문 API 서버] --&amp;gt;|주문 이벤트 produce| T[&quot;orders topic&quot;]
    P2[모바일 앱] --&amp;gt;|주문 이벤트 produce| T
    T --&amp;gt;|consume| C1[주문 처리 서비스&amp;lt;br/&amp;gt;재고 차감&amp;middot;결제]
    T --&amp;gt;|consume| C2[분석 파이프라인&amp;lt;br/&amp;gt;일별 매출 집계]
    T --&amp;gt;|consume| C3[알림 서비스&amp;lt;br/&amp;gt;주문 확인 이메일]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시나리오가 topic의 가치를 보여준다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;여러 곳에서 쓴다(multi-producer)&lt;/b&gt;: 주문 API 서버와 모바일 앱이 모두 &lt;code&gt;orders&lt;/code&gt; topic에 주문 이벤트를 쓴다. 어디서 왔든 같은 topic에 쌓인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 곳에서 읽는다(multi-consumer)&lt;/b&gt;: 주문 처리 서비스(재고 차감), 분석 파이프라인(매출 집계), 알림 서비스(이메일)가 &lt;b&gt;각자 독립적으로&lt;/b&gt; 같은 &lt;code&gt;orders&lt;/code&gt; topic을 읽는다. 한 consumer가 느려져도 다른 consumer는 영향 안 받는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나중에 다시 읽을 수 있다(replay)&lt;/b&gt;: 분석에서 버그를 발견하면, 지난주 주문 데이터를 &lt;code&gt;orders&lt;/code&gt; topic에서 다시 읽어 재처리할 수 있다(메시지가 남아있으므로).&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 topic을 단순 &quot;메시지 큐&quot;가 아니라 &lt;b&gt;이벤트의 영속 저장소&lt;/b&gt;로 쓰는 방식이다. 서비스들이 topic을 사이에 두고 느슨하게 연결되는 구조(이벤트 기반 아키텍처)의 기반이 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic을 만들 때: &lt;code&gt;bin/kafka-topics.sh --create --topic orders --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092&lt;/code&gt;. 여기서 &lt;code&gt;--partitions 3&lt;/code&gt;이 바로 다음에 볼 partition이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;topic 하나로 충분한가 &amp;mdash; 왜 partition이 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문이 초당 10건이면 topic 하나(partition 1개)로 충분하다. 하지만 주문이 &lt;b&gt;초당 수만 건&lt;/b&gt;으로 늘면? topic을 하나의 줄(로그)로만 처리하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쓰기가 한 곳에 몰림&lt;/b&gt; &amp;mdash; 한 broker만 바쁘고, 다른 broker는 한가함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽기도 한 줄&lt;/b&gt; &amp;mdash; consumer를 여럿 띄워도 한 줄을 순서대로 읽어야 하니 병렬 처리가 안 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해법: topic을 &lt;b&gt;여러 조각(partition)으로 쪼개&lt;/b&gt; 각 조각을 다른 broker에 두고, consumer도 각 조각을 나눠 읽는다. 이게 partition의 탄생 동기다. &lt;b&gt;&quot;topic은 논리적 분류, partition은 물리적 분할&quot;&lt;/b&gt;이라고 한 줄로 요약할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partition이란 &amp;mdash; topic을 쪼갠 물리적 조각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partition은 topic을 물리적으로 나눈 조각이다. topic &lt;code&gt;orders&lt;/code&gt;에 partition 3개를 두면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partition 0, partition 1, partition 2 &amp;mdash; 각각 독립된 append-only 로그.&lt;/li&gt;
&lt;li&gt;메시지는 (key 유무에 따라) 어느 partition으로 갈지 정해져 들어간다.&lt;/li&gt;
&lt;li&gt;각 partition은 (복제본 제외) 단일 broker에 저장되어 부하가 분산된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart TB
    T[&quot;topic: orders&amp;lt;br/&amp;gt;(논리적 분류)&quot;] --&amp;gt; P0[&quot;partition 0&amp;lt;br/&amp;gt;(물리적 로그)&quot;]
    T --&amp;gt; P1[&quot;partition 1&quot;]
    T --&amp;gt; P2[&quot;partition 2&quot;]
    P0 --&amp;gt; B0[&quot;broker 0&quot;]
    P1 --&amp;gt; B1[&quot;broker 1&quot;]
    P2 --&amp;gt; B2[&quot;broker 2&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition은 세 가지의 최소 단위다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partition은 Kafka에서 세 가지 의미의 최소 단위를 동시에 뜻한다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 분할 단위&lt;/b&gt; &amp;mdash; 각 partition은 단일 broker에 저장. topic 전체 데이터가 partition 수만큼 분산.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병렬성 단위&lt;/b&gt; &amp;mdash; consumer group 내에서 &lt;b&gt;partition 1개당 최대 consumer 1개&lt;/b&gt;가 병렬 소비. 즉 partition 수 = consumer 병렬성의 상한.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서 보장 단위&lt;/b&gt; &amp;mdash; &lt;b&gt;partition 내에서만&lt;/b&gt; 쓴 순서대로 읽힌다. topic 전체 순서 보장은 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/basic-kafka-operations.md&quot;&gt;(basic-kafka-operations.md)&lt;/a&gt;: partition 수가 &quot;데이터를 몇 개 로그로 샤딩할지&quot;와 &quot;consumer 최대 병렬성&quot;을 결정한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 셋이 한 몸이라는 게 핵심이다. 병렬성을 늘리려 partition을 늘리면 순서 보장 범위가 쪼개진다 &amp;mdash; partition 수 선택은 &lt;b&gt;병렬성 vs 순서 보장 범위&lt;/b&gt;의 트레이드오프다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;key가 partition을 고정한다 &amp;mdash; 같은 key는 같은 partition으로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer가 메시지에 &lt;b&gt;key&lt;/b&gt;를 주면, 같은 key는 항상 같은 partition으로 간다. 왜 필요한가 &amp;mdash; 같은 사용자의 주문이 여러 partition에 흩어지면 순서가 꼬인다. &quot;사용자 A의 주문&quot;이 항상 같은 partition에 있어야 그 사용자 이벤트를 순서대로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구체 예:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;key = &quot;userA&quot;&lt;/code&gt;인 메시지 &amp;rarr; 항상 partition 1로 감(예). &quot;userA&quot;의 모든 주문이 partition 1에 순서대로 쌓임.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key = &quot;userB&quot;&lt;/code&gt; &amp;rarr; 항상 partition 0으로 감(예).&lt;/li&gt;
&lt;li&gt;key 없음(&lt;code&gt;null&lt;/code&gt;) &amp;rarr; &lt;b&gt;sticky&lt;/b&gt;: 한 batch를 같은 partition에 몰아넣어 효율을 높임. key 기반 순서가 필요 없는 로그에 적합.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 partition이 정해지나 &amp;mdash; key를 해시해서 partition 수로 나눈다(자세한 공식은 심화 절에서). 핵심은 &lt;b&gt;같은 key &amp;rarr; 항상 같은 partition&lt;/b&gt;이라는 보장이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partition이 정해지는 방식을 모르면 &quot;key를 줬는데 왜 어떤 사용자는 빨리 처리되고 어떤 사용자는 늦지?&quot; 같은 현상의 원인(partition 쏠림)을 못 찾는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partition 수를 늘리면 같은 key가 다른 partition으로 간다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위에서 쌓은 기초가 필요한 부분이다. topic을 만들 때 partition 3개로 잡았다. 처리량이 부족해 5로 늘리기로 했다. &quot;빈 공간이 두 개 더 생기겠지&quot;라고 생각한다면 &amp;mdash; 위험하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partition을 늘리는 순간, &lt;b&gt;기존에 특정 partition에 가던 key가 다른 partition으로 갈 수 있다.&lt;/b&gt; key가 &lt;code&gt;&quot;userA&quot;&lt;/code&gt;라 partition 1에 가던 메시지가, partition이 5개가 되면 partition 3으로 갈 수 있다. 같은 key의 메시지가 partition 경계에서 순서가 끊기는 것이다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    A[&quot;partition 3개&amp;lt;br/&amp;gt;key 'userA' &amp;rarr; partition 1&quot;] --&amp;gt;|alter --partitions 5| B[&quot;partition 5개&quot;]
    B --&amp;gt; C[&quot;key 'userA' &amp;rarr; partition 3&amp;lt;br/&amp;gt;(매핑 변화)&quot;]
    C --&amp;gt; D[&quot;key 기반 순서 보장 파손&amp;lt;br/&amp;gt;(기존 메시지는 옮겨지지 않음)&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜? partition 결정이 &lt;code&gt;hash(key) % partition수&lt;/code&gt;라서, partition수가 바뀌면 결과가 달라지기 때문이다. 공식 문서가 명시하는 부작용 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/basic-kafka-operations.md&quot;&gt;(basic-kafka-operations.md)&lt;/a&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;key 분포 변화&lt;/b&gt;: Kafka Streams 등 key 기반 상태를 쓰는 앱의 순서&amp;middot;상태 가정이 깨짐.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consumer 데이터 누락 위험&lt;/b&gt;: &lt;code&gt;auto.offset.reset=latest&lt;/code&gt;인 consumer가 새 partition을 인지 못하는 찰나에 메시지를 놓칠 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메타데이터 전파 지연&lt;/b&gt;: 클러스터 전체에 새 partition 정보가 퍼지는 데 시간차.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의: partition을 늘려도 &lt;b&gt;기존 메시지는 옮겨지지 않는다&lt;/b&gt;. &lt;code&gt;userA&lt;/code&gt;의 과거 메시지는 partition 1에 그대로 있고, 새 메시지는 partition 3으로 간다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;절대 금지&lt;/b&gt;: 내부 topic(&lt;code&gt;__consumer_offsets&lt;/code&gt;, &lt;code&gt;__transaction_state&lt;/code&gt;, &lt;code&gt;__cluster_metadata&lt;/code&gt;)의 partition을 수동으로 늘리면 &lt;b&gt;시스템 장애&lt;/b&gt;. 이들은 Kafka가 스스로 관리한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partition은 왜 줄일 수 없는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그럼 모자라면 늘리고, 너무 많으면 줄이면 되겠지?&quot; &amp;mdash; &lt;b&gt;줄일 수 없다.&lt;/b&gt; Kafka는 partition 감소를 지원하지 않는다. 이유:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지는 이미 특정 partition에 영속됐다. 합치려면 offset을 다시 매겨야 하는데, 이미 commit된 consumer offset과 어긋난다.&lt;/li&gt;
&lt;li&gt;두 partition의 메시지 사이엔 정의된 전체 순서가 없었으므로(그게 partition의 전제), 합쳐서 시간순으로 다시 엮는 게 불가능하다.&lt;/li&gt;
&lt;li&gt;ISR 복제본들도 같은 재구성을 동시에 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 줄이려면 &lt;b&gt;새 topic을 만들고 데이터를 이주(migrate)&lt;/b&gt;하는 수밖에 없다. 설계 단계에서 넉넉히 잡되, key 매핑 깨짐을 감수할 수 없다면 partition 수를 사실상 고정으로 설계해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partition 수는 어떻게 정하나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 절대 권장값을 명시하지 않는다. 다음 기준으로 산정한다:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기준&lt;/th&gt;
&lt;th&gt;관계&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;처리량&lt;/td&gt;
&lt;td&gt;partition &amp;uarr; &amp;rarr; 병렬 쓰기/읽기 &amp;uarr; (broker 한계까지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;consumer 병렬성&lt;/td&gt;
&lt;td&gt;consumer 수 &amp;le; partition 수 (초과분은 idle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;broker 부하&lt;/td&gt;
&lt;td&gt;partition &amp;uarr; &amp;rarr; 파일 수&amp;middot;메모리 &amp;uarr;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 복구&lt;/td&gt;
&lt;td&gt;partition &amp;uarr; &amp;rarr; 복구 시간 &amp;uarr;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;순서 보장&lt;/td&gt;
&lt;td&gt;partition &amp;uarr; &amp;rarr; key 순서 보장 범위 세분화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사이징 예&lt;/b&gt;(수치는 가정): 초당 100MB 목표, partition당 ~ 10MB 측정 &amp;rarr; 최소 10 partition. consumer 병렬성 상한 10. 확장 여유 1.5 ~ 2배 &amp;rarr; 15 ~ 20으로 시작. (partition당 처리량은 실측 필수 &amp;mdash; 공식 문서는 절대 수치를 안 준다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;많을수록 좋다&quot;가 아니다 &amp;mdash; partition당 파일 핸들&amp;middot;메모리가 들고, broker 장애 시 복구가 느려진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka 4.3 실습 &amp;mdash; 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전: KRaft 단일 노드 기동.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;topic 만들고 메시지 넣기&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# Kafka 4.3, KRaft 단일 노드
bin/kafka-topics.sh --create --topic part-demo --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092
# key:value 형태로 produce (key가 partition 결정)
bin/kafka-console-producer.sh --topic part-demo --property &quot;parse.key=true&quot; --property &quot;key.separator=:&quot; --bootstrap-server localhost:9092
# 입력 예:
# userA:evt1
# userA:evt2
# userB:evt3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: 같은 key(&lt;code&gt;userA&lt;/code&gt;)는 항상 같은 partition에 기록되는가.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;bin/kafka-console-consumer.sh --topic part-demo --property &quot;print.key=true&quot; --property &quot;print.partition=true&quot; --from-beginning --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;partition: 1, key: userA, value: evt1
partition: 1, key: userA, value: evt2
partition: 2, key: userB, value: evt3&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 partition 번호는 해시 결과에 따라 달라진다. 핵심은 &lt;b&gt;같은 key &amp;rarr; 같은 partition&lt;/b&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition 추가와 key 매핑 변화 관찰&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;bin/kafka-topics.sh --alter --topic part-demo --partitions 5 --bootstrap-server localhost:9092
bin/kafka-console-producer.sh --topic part-demo --property &quot;parse.key=true&quot; --property &quot;key.separator=:&quot; --bootstrap-server localhost:9092
# userA:evt4  (새로 입력)
bin/kafka-console-consumer.sh --topic part-demo --property &quot;print.key=true&quot; --property &quot;print.partition=true&quot; --from-beginning --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: &lt;code&gt;userA&lt;/code&gt;의 &lt;code&gt;evt1/evt2&lt;/code&gt;는 partition 1에 있었지만, &lt;code&gt;evt4&lt;/code&gt;는 partition이 바뀔 수 있다 &amp;rarr; &lt;b&gt;같은 key의 순서가 partition 경계에서 끊긴다&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;describe로 partition 상태 보기&lt;/h3&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;bin/kafka-topics.sh --describe --topic part-demo --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;PartitionCount&lt;/code&gt; / &lt;code&gt;ReplicationFactor&lt;/code&gt;: topic 전체 설정.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Leader&lt;/code&gt; / &lt;code&gt;Replicas&lt;/code&gt; / &lt;code&gt;Isr&lt;/code&gt;: 각 partition의 복제 상태(단일 노드에선 모두 같음; 복제는 장 06).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;topic은 메시지 큐라서 소비하면 사라진다&quot;&lt;/td&gt;
&lt;td&gt;아니다. topic은 로그. 소비해도 retention 전엔 남음(장 01)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;partition 많을수록 항상 빠르다&quot;&lt;/td&gt;
&lt;td&gt;broker당 파일 핸들&amp;middot;메모리 한계. 과도하면 장애 복구 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;partition을 줄일 수 있다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;불가&lt;/b&gt;. 늘리기만. 줄이려면 새 topic으로 이주&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;key 순서는 영원히 보장된다&quot;&lt;/td&gt;
&lt;td&gt;partition 수가 변하지 않는 한 보장. &lt;b&gt;partition 증가 순간 깨짐&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;partition 수 = consumer 수로 맞추면 된다&quot;&lt;/td&gt;
&lt;td&gt;consumer 병렬성 상한일 뿐. 처리량&amp;middot;broker 부하도 함께 고려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;topic 생성 후 기본값(partition 1)으로 둬도 된다&quot;&lt;/td&gt;
&lt;td&gt;프로덕션에선 거의 항상 부족. 명시적 &lt;code&gt;--partitions&lt;/code&gt; 필수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;partition 늘리면 기존 메시지도 옮겨진다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아니다&lt;/b&gt;. 과거는 그대로, 새 메시지만 새 매핑 &amp;rarr; key 순서 끊김&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이 &amp;mdash; partition은 어떻게 정해지나 (murmur2)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 &quot;key를 해시해서 partition 수로 나눈다&quot;고만 했다. 정확한 공식이 궁금한 사람을 위해:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;key != null&lt;/code&gt;: &lt;code&gt;toPositive(murmur2(key)) % numPartitions&lt;/code&gt;. 같은 key는 항상 같은 partition(결정적).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/producer/internals/BuiltInPartitioner.java&quot;&gt;BuiltInPartitioner.java&lt;/a&gt;를 보면 &lt;code&gt;murmur2&lt;/code&gt; 해시를 쓰고, 그걸 partition 수로 나눈다(모듈로).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 consistent hash가 아닌가 &amp;mdash; partition 확장에 취약한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 파티셔너가 &lt;b&gt;consistent hash&lt;/b&gt;를 쓴다고 생각하는 경우가 많다. 아니다. consistent hash라면 partition 수가 변해도 대부분 key-매핑이 유지되지만, 모듈로 방식은 &lt;b&gt;partition 수가 변하면 거의 모든 key의 매핑이 바뀐다&lt;/b&gt;. 바로 이것이 위 &quot;partition 늘리면 key 매핑이 깨진다&quot;의 원인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;murmur2는 &lt;b&gt;비암호학적 해시&lt;/b&gt;다 &amp;mdash; 속도가 빠르고 분산이 고른 것이 목적이지, 보안이 아니다. &lt;code&gt;toPositive&lt;/code&gt;는 murmur2의 signed(음수 가능) 결과를 양수로 바꾸는 래퍼(음수 해시에 모듈로를 쓰면 partition 번호가 음수가 되므로).&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key 기반 상태(changelog, state store)를 쓰는 Kafka Streams 앱은 &quot;key X는 항상 partition P에 있다&quot;를 전제로 동작한다. partition 수가 변하면 이 전제가 깨져 상태 재구성이 일어난다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;topic&lt;/b&gt; = 메시지를 담는 이름 붙은 상자(카테고리). 용도별로 나눔(orders, clicks...). 로그라 소비해도 남고, 여러 consumer가 따로 읽고, replay 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;topic 사용&lt;/b&gt;: multi-producer(여러 곳에서 씀) + multi-consumer(여러 서비스가 따로 읽음). 이벤트 기반 아키텍처의 기반.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;partition&lt;/b&gt; = topic을 물리적으로 쪼갠 조각. topic 하나의 부하를 여러 broker로 분산.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;partition 세 의미&lt;/b&gt;: 데이터 분할 + 병렬성(=consumer 병렬성 상한) + 순서 보장(partition 내만).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;key &amp;rarr; 같은 partition&lt;/b&gt;: 같은 key는 항상 같은 partition. 같은 사용자 이벤트 순서 처리에 필수.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;partition은 늘리기만 가능&lt;/b&gt;(줄이기 불가). 늘리면 &lt;b&gt;key 매핑이 깨져 순서 가정 파손&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 topic partition 변경 절대 금지&lt;/b&gt; &amp;rarr; 시스템 장애.&lt;/li&gt;
&lt;li&gt;partition 수 산정은 처리량&amp;middot;컨슈머 병렬성&amp;middot;broker 부하&amp;middot;복구의 균형. &quot;많을수록 좋다&quot;가 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;topic을 &quot;메시지 큐&quot;가 아니라 &quot;로그&quot;로 부르는 것이 어떤 차이를 만드는가? (소비 후 삭제 vs 남음)&lt;/li&gt;
&lt;li&gt;쇼핑몰에서 &lt;code&gt;orders&lt;/code&gt; topic 하나를 주문 처리&amp;middot;분석&amp;middot;알림 서비스가 따로 읽는 것이 가능한 이유는?&lt;/li&gt;
&lt;li&gt;topic 하나(partition 1개)로 초당 수만 건을 처리하기 어려운 이유는? partition이 어떻게 해결하는가?&lt;/li&gt;
&lt;li&gt;partition 6개인 topic에 consumer 4개인 그룹이 있다. 각 consumer는 몇 partition을 소유하는가? consumer를 8개로 늘리면?&lt;/li&gt;
&lt;li&gt;partition을 3&amp;rarr;5로 늘리면 같은 key의 메시지 순서가 왜 끊기는가?&lt;/li&gt;
&lt;li&gt;partition을 줄일 수 없는 이유는? 줄여야 한다면 어떻게 해야 하는가?&lt;/li&gt;
&lt;li&gt;key 없이 produce하는데 cleanup.policy=compact인 topic에서 무슨 일이 벌어지는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/basic-kafka-operations.md&quot;&gt;Basic Operations &amp;mdash; Adding/Modifying topics&lt;/a&gt; - 접근 2026-07-09 (partition 증가 부작용, 내부 topic 금지)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/configuration/topic-configs.md&quot;&gt;Topic configurations&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/getting-started/introduction.md&quot;&gt;Introduction &amp;mdash; Partitioning&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/producer/internals/BuiltInPartitioner.java&quot;&gt;BuiltInPartitioner (murmur2)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>kafka</category>
      <category>partition</category>
      <category>Partitioning</category>
      <category>topic</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/13</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-topic-and-partition#entry13comment</comments>
      <pubDate>Thu, 9 Jul 2026 15:40:07 +0900</pubDate>
    </item>
    <item>
      <title>Kafka - 01. architecture</title>
      <link>https://technical-architect.tistory.com/entry/Kafka-architecture</link>
      <description>&lt;h1&gt;Kafka는 큐가 아니다 &amp;mdash; 영속적 분산 로그가 만든 모든 차이&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka를 처음 배우는 사람 대부분은 &quot;메시지 큐&quot;라는 설명을 듣고 RabbitMQ처럼 생각한다. 그래서 &quot;소비했는데 왜 데이터가 남지?&quot;, &quot;왜 메시지 순서가 섞이지?&quot; 같은 오해에 빠진다. 이 오해의 뿌리는 단 하나다 &amp;mdash; &lt;b&gt;Kafka는 소비하면 사라지는 큐가 아니라, 영속적이고 파티션 내 순서가 보장되는 분산 로그다.&lt;/b&gt; 이 하나의 차이가 Kafka의 모든 설계(복제, 컨슈머 그룹, retention, 전달 의미론)를 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 그 핵심 추상 &amp;mdash; partition = append-only log &amp;mdash; 을 중심으로 Kafka가 무엇인지, 왜 그렇게 설계됐는지, 그리고 왜 &quot;빠르다&quot;는 평가가 마법이 아니라 설계의 결과인지를 다룬다. retention&amp;middot;복제&amp;middot;rebalance&amp;middot;exactly-once가 암기 과목이 되지 않으려면, 이 로그 모델을 먼저 정확히 잡아야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka는 영속적 분산 로그다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 말해, Kafka의 partition 하나는 &lt;b&gt;끝에만 줄을 적는 장부(append-only ledger)&lt;/b&gt;다. 이미 적힌 줄은 번호(offset)가 매겨지고 고정된다. 읽는 사람(consumer)은 자기가 마지막으로 읽은 줄 번호를 기억해 두고, 다음 번호부터 다시 읽는다. 여러 사람이 같은 장부의 같은 번호를 여러 번 읽을 수 있다 &amp;mdash; 장부는 읽었다고 지워지지 않는다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    P[producer] --&amp;gt;|write| T[&quot;topic&amp;lt;br/&amp;gt;(multi-producer, multi-consumer)&quot;]
    T --&amp;gt; PART[&quot;partition 0&amp;lt;br/&amp;gt;partition 1&amp;lt;br/&amp;gt;partition 2&amp;lt;br/&amp;gt;(각각 append-only log)&quot;]
    PART --&amp;gt;|read| C1[consumer A]
    PART --&amp;gt;|read| C2[consumer B]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASF 공식 문서가 강조하는 핵심 사실들: &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/getting-started/introduction.md&quot;&gt;(introduction.md)&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;topic은 multi-producer, multi-consumer&lt;/b&gt;다. 여러 클라이언트가 같은 topic에 동시에 쓰고 읽는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소비해도 삭제되지 않는다.&lt;/b&gt; 전통 메시징과 달리 메시지는 consumer가 읽었다고 사라지지 않는다. 대신 topic별 &lt;b&gt;retention&lt;/b&gt;(시간/크기) 정책으로만 삭제된다. &amp;rarr; 같은 데이터를 여러 consumer 그룹이 독립적으로, 그리고 나중에 다시(replay) 읽을 수 있는 이유.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;같은 key의 메시지는 같은 partition&lt;/b&gt;에, partition 내에서는 &lt;b&gt;쓴 순서대로&lt;/b&gt; 읽힌다. (전체 topic 순서 보장은 아니다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 크기에 관계없이 성능이 일정&lt;/b&gt;하다 &amp;mdash; O(1) disk seek per read, 순차 I/O 덕분. 이것이 장기 보관을 가능하게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;record &amp;mdash; 데이터 단위의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 데이터 단위는 &lt;b&gt;record&lt;/b&gt;(event/message)다. 1차 출처 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/implementation/message-format.md&quot;&gt;message-format.md&lt;/a&gt;에 따른 구조:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파티셔닝 키(선택). 같은 key &amp;rarr; 같은 partition&lt;/td&gt;
&lt;td&gt;&lt;code&gt;null&lt;/code&gt; 가능 &amp;rarr; sticky partitioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;실제 페이로드&lt;/td&gt;
&lt;td&gt;바이트 배열. 형식 자유(JSON, Avro 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메시지 시각&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CreateTime&lt;/code&gt;(producer 지정) 또는 &lt;code&gt;LogAppendTime&lt;/code&gt;(broker 지정). topic 설정 &lt;code&gt;message.timestamp.type&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;headers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;키-값 메타데이터&lt;/td&gt;
&lt;td&gt;&lt;b&gt;KIP-82, Kafka 0.11(v2 포맷)에 추가&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key가 없으면(&lt;code&gt;null&lt;/code&gt;) producer는 &lt;b&gt;sticky partitioning&lt;/b&gt;을 쓴다 &amp;mdash; 한 batch의 메시지를 같은 partition으로 몰아 넣어 효율을 높인다. key가 있으면 &lt;code&gt;murmur2(key) % numPartitions&lt;/code&gt;로 partition이 결정된다(자세한 건 02장).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;큐가 아니라 로그라는 게, 구체적으로 뭐가 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Kafka는 큐가 아니다&quot;라는 말이 추상적이다. 전통 메시징(RabbitMQ 같은 AMQP 브로커)과 비교하면 차이가 선명해진다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;전통 메시징(큐)&lt;/th&gt;
&lt;th&gt;Kafka(로그)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;소비 후&lt;/td&gt;
&lt;td&gt;&lt;b&gt;즉시 삭제&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;남아있음&lt;/b&gt;(retention이 지나야 삭제)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;같은 메시지 재소비&lt;/td&gt;
&lt;td&gt;불가(지워졌으므로)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;가능&lt;/b&gt;(offset만 다시 돌리면 replay)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 소비자가 같은 데이터&lt;/td&gt;
&lt;td&gt;큐는 경쟁 소비(한 메시지=한 소비자)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;consumer group마다 독립적&lt;/b&gt;으로 전부 읽기 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;순서 보장 단위&lt;/td&gt;
&lt;td&gt;큐 전체(보통)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;partition 내만&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리량 한계&lt;/td&gt;
&lt;td&gt;메모리/인덱스 한계, 삭제 비용&lt;/td&gt;
&lt;td&gt;디스크 순차 I/O &amp;rarr; 대량 장기 보관에 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 차이는 &quot;소비 = 삭제&quot; 모델이냐 &quot;소비 = 읽기 커서 이동&quot; 모델이냐다. RabbitMQ에서 consumer가 메시지를 ack 하면 그 메시지는 사라진다. Kafka에서 consumer가 메시지를 읽으면 broker는 &lt;b&gt;아무것도 하지 않는다&lt;/b&gt; &amp;mdash; consumer가 자기 offset을 어디까지 읽었나 기억할 뿐이다. 그래서 같은 topic을 이벤트 소싱용으로도, 분석용으로도, 나중에 재처리용으로도 동시에 읽을 수 있다. 이 &quot;읽기가 파괴적이지 않다&quot;는 성질이 Kafka를 단순 메시지 브로커가 아니라 &lt;b&gt;이벤트 스트리밍 플랫폼&lt;/b&gt;으로 만든다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 로그인가 &amp;mdash; Kafka가 빠른 진짜 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Kafka가 빠르다&quot;는 평가는 마법이 아니라 세 가지 설계 결정의 결과다: &lt;b&gt;순차 디스크 I/O + 일괄(batch) + zero-copy&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;순차 디스크 I/O가 무작위 메모리 접근보다 빠를 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관과 달리, 동일한 디스크에서 &lt;b&gt;순차 쓰기는 무작위 읽기보다 훨씬 빠르다&lt;/b&gt;. Kafka는 이를 극단까지 밀어붙인다 &amp;mdash; 모든 쓰기는 로그 끝에 append, 읽기도 offset 기준 순차 탐색. &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;(design.md)&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순차 vs 무작위 디스크 접근: 같은 디스크에서 순차가 수십~수백 배 빠르다. 7200rpm HDD 기준 순차 읽기 ~100 MB/s, 무작위 읽기는 수백 KB/s 수준. SSD는 격차가 좁지만 여전히 순차가 유리하다. (수치는 미검증, design.md의 상대적 통찰에 근거)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 크기가 커져도 읽기 비용이 일정한 이유(O(1) per read)가 바로 이것이다 &amp;mdash; offset으로 위치를 계산해 순차로 읽기 때문에, 1TB든 1GB든 읽기 단가가 같다. retention 비용이 &quot;디스크 용량만 있으면 무한 보관 가능&quot;이 되는 이유이기도 하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;batch + 압축의 종단간 전달&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer는 메시지 여러 개를 한 묶음(batch)으로 모은다 &amp;mdash; &lt;code&gt;linger.ms&lt;/code&gt;/&lt;code&gt;batch.size&lt;/code&gt;까지 모아 한 번에 전송해 네트워크 왕복을 최소화한다. 여기에 &lt;b&gt;batch compression&lt;/b&gt;이 더해진다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;producer가 batch를 &lt;b&gt;압축해서&lt;/b&gt; broker로 보낸다.&lt;/li&gt;
&lt;li&gt;broker는 검증을 위해 잠깐 해제하지만, 디스크엔 &lt;b&gt;압축된 채&lt;/b&gt; 저장하고 consumer에게도 &lt;b&gt;압축된 채&lt;/b&gt; 전송한다.&lt;/li&gt;
&lt;li&gt;네트워크 비용이 종단간 절감된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;zero-copy &amp;mdash; 커널 페이지 캐시에서 소켓으로 곧장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 파일을 읽어 네트워크로 보내려면: 디스크 &amp;rarr; 커널 페이지 캐시 &amp;rarr; 사용자 공간(응용 메모리) &amp;rarr; 커널 소켓 버퍼 &amp;rarr; 네트워크 카드. 사용자 공간을 거치는 복사가 두 번 든다. Kafka는 &lt;b&gt;&lt;code&gt;sendfile&lt;/code&gt;(zero-copy)&lt;/b&gt; 로 이 복사를 건너뛴다 &amp;mdash; 데이터가 커널 페이지 캐시에서 소켓으로 곧장 간다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart LR
    subgraph 일반[&quot;일반 전송(복사 4단계)&quot;]
        D1[디스크] --&amp;gt; PC1[페이지 캐시] --&amp;gt; US1[사용자 공간] --&amp;gt; SK1[소켓 버퍼] --&amp;gt; N1[NIC]
    end
    subgraph ZC[&quot;Kafka zero-copy (sendfile)&quot;]
        D2[디스크] --&amp;gt; PC2[페이지 캐시] --&amp;gt; SK2[소켓 버퍼] --&amp;gt; N2[NIC]
    end&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 Kafka는 디스크 파일을 커널 페이지 캐시에 올려두고 그대로 소비한다 &amp;mdash; 별도의 응용 캐시를 두지 않는다. 그래서 남는 메모리가 곧 읽기 캐시가 된다. &quot;Kafka는 메모리를 적게 쓰면서도 빠르다&quot;는 평가의 진짜 이유.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대 처리량(예: &quot;초당 수백만 건&quot;)은 하드웨어&amp;middot;메시지 크기에 따라 폭넓게 변동한다. 공식 design 문서는 상대적 통찰(순차 &amp;raquo; 무작위, batch/zero-copy의 효과)만 제시한다. 구체 수치는 Confluent 벤치마크(2차 출처) 참조 시 출처 명시 필수.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;partition &amp;mdash; 병렬성과 순서의 트레이드오프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 로그는 단순하지만 병렬성이 없다. Kafka는 topic을 &lt;b&gt;partition&lt;/b&gt;으로 쪼개 이 문제를 푼다. 각 partition이 독립된 병렬 단위가 되어 쓰기도 분산되고 consumer도 분산된다. 대신 &lt;b&gt;전체 순서는 포기하고 partition 내 순서만 보장&lt;/b&gt;한다는 트레이드오프를 치른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partition 내: 쓴 순서대로 읽힌다(단일 로그의 순서 보장).&lt;/li&gt;
&lt;li&gt;partition 간: 순서 보장 없음.&lt;/li&gt;
&lt;li&gt;전체 topic 순서가 꼭 필요하면 partition=1로 둬야 한다 &amp;mdash; 병렬성을 포기하는代价로.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition 수는 어떻게 정하나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 자주 묻는 질문이고, 정답은 &quot;상황에 따라&quot;다. trade-off를 보자:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;partition을 늘리면&lt;/th&gt;
&lt;th&gt;대가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;병렬성&amp;uarr;(consumer 동시 처리&amp;uarr;)&lt;/td&gt;
&lt;td&gt;broker가 관리할 partition 파일&amp;middot;리더 수&amp;uarr;(파일 핸들&amp;middot;메모리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리량&amp;uarr;&lt;/td&gt;
&lt;td&gt;rebalance 비용&amp;uarr;(partition이 많을수록 재분배가 느림)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;리더 선출&amp;middot;복제 부하&amp;uarr;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서는 단일 정답을 주지 않는다 &amp;mdash; 목표 처리량&amp;middot;컨슈머 수&amp;middot;클러스터 규모로 산정한다. (구체 권장값은 없으니 &quot;미겕안&quot; 표식 없이 단정하지 말 것.) 일반적으로 &quot;당장 필요한 것보다 약간 여유&quot;로 시작하고, 모니터링하면 늘린다. partition 수를 &lt;b&gt;줄이는 것은 지원되지 않는다&lt;/b&gt;는 것도 기억하라 &amp;mdash; 늘리기만 가능하다(02장 상세).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;produce 경로 &amp;mdash; 메시지가 broker에 도달하기까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer가 &lt;code&gt;send(record)&lt;/code&gt;를 부르면 메시지가 즉시 broker로 가는 게 아니다. accumulator에 쌓였다가 batch로 묶여 간다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;sequenceDiagram
    participant P as producer
    participant ACC as accumulator(batch)
    participant L as partition leader broker
    participant F as ISR follower brokers
    P-&amp;gt;&amp;gt;ACC: send(record) &amp;mdash; 직렬화+파티셔닝 후 batch 적재
    ACC-&amp;gt;&amp;gt;L: batch 전송(압축)
    L-&amp;gt;&amp;gt;L: 로그 끝에 append
    L-&amp;gt;&amp;gt;F: 복제(팔로워 fetch)
    F--&amp;gt;&amp;gt;L: ACK (HW 갱신)
    L--&amp;gt;&amp;gt;P: ack (acks 설정에 따라)&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;일어나는 일&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;파티셔닝&lt;/td&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt; 있으면 &lt;code&gt;toPositive(murmur2(key)) % numPartitions&lt;/code&gt;로 partition 결정. &lt;code&gt;key&lt;/code&gt; 없으면 sticky&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/producer/internals/BuiltInPartitioner.java&quot;&gt;BuiltInPartitioner.java&lt;/a&gt;. &lt;b&gt;murmur2, consistent hash 아님&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;batch 적재&lt;/td&gt;
&lt;td&gt;&lt;code&gt;linger.ms&lt;/code&gt;/&lt;code&gt;batch.size&lt;/code&gt;까지 모아 한 번에 전송&lt;/td&gt;
&lt;td&gt;네트워크 왕복 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;leader append&lt;/td&gt;
&lt;td&gt;partition 리더 broker가 로그 끝에 기록&lt;/td&gt;
&lt;td&gt;순차 I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복제&lt;/td&gt;
&lt;td&gt;ISR 팔로워들이 리더에서 fetch해 복제&lt;/td&gt;
&lt;td&gt;장 06 상세&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ack&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acks&lt;/code&gt; 설정(0/1/all)에 따라 producer에게 확인&lt;/td&gt;
&lt;td&gt;장 03/08 상세&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;acks &amp;mdash; &quot;얼마나 확실히 썼다고 볼 것인가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer가 메시지를 보낸 뒤 broker에게 확인(ack)을 기다리는 강도를 &lt;code&gt;acks&lt;/code&gt;가 결정한다. 이 한 설정이 성능과 신뢰성의 균형을 잡는다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;acks&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;잃을 수 있는 것&lt;/th&gt;
&lt;th&gt;대표 용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;broker 응답 안 기다림(fire and forget)&lt;/td&gt;
&lt;td&gt;broker가 받기도 전에 네트워크에서 사라지면 유실&lt;/td&gt;
&lt;td&gt;로그&amp;middot;메트릭 등 손실 감내 가능한 고처리량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;리더&lt;/b&gt;만 확인(리더 장애 직전에 받고 복제 전이면 유실 가능)&lt;/td&gt;
&lt;td&gt;리더 장애 시 미복제 메시지 유실&lt;/td&gt;
&lt;td&gt;성능과 신뢰성 절충&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;(&lt;code&gt;-1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;ISR 전원&lt;/b&gt; 확인(복제까지 끝나야 성공)&lt;/td&gt;
&lt;td&gt;ISR 전원 장애가 아니면 유실 없음&lt;/td&gt;
&lt;td&gt;신뢰성 최우선(금융&amp;middot;주문)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;acks=all&lt;/code&gt;이 &quot;가장 안전&quot;이지만 단독으론 부족하다 &amp;mdash; ISR이 1개만 남으면 그 1개만 확인하면 성공이므로 사실상 신뢰성이 없다. 그래서 &lt;code&gt;min.insync.replicas&lt;/code&gt;(예: 2)와 세트로 써야 ISR 미달 시 쓰기를 거부하게 만든다. 이 조합(&lt;code&gt;replication.factor=3&lt;/code&gt;, &lt;code&gt;min.insync.replicas=2&lt;/code&gt;, &lt;code&gt;acks=all&lt;/code&gt;)이 프로덕션 표준이다(장 06&amp;middot;08 상세).&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;idempotent producer(&lt;code&gt;enable.idempotence=true&lt;/code&gt;)는 &lt;code&gt;acks=all&lt;/code&gt;을 자동 강제한다 &amp;mdash; 재시도 중 중복을 막으려면 복제 완료가 보장돼야 하므로. 이 설정 관계는 장 08(exactly-once)의 핵심이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;consume 경로 &amp;mdash; consumer가 데이터를 당기는 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 자주 틀린다. &quot;producer가 쓰면 consumer에게 push된다&quot;가 아니다. &lt;b&gt;consumer가 능동적으로 &lt;code&gt;poll()&lt;/code&gt;로 당겨온다.&lt;/b&gt; broker는 consumer가 요청하지 않으면 아무것도 보내지 않는다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;sequenceDiagram
    participant C as consumer
    participant L as partition leader broker
    participant OS as __consumer_offsets
    C-&amp;gt;&amp;gt;C: poll() 호출
    C-&amp;gt;&amp;gt;L: Fetch 요청(이전 offset부터)
    L--&amp;gt;&amp;gt;C: batch 반환(압축 해제)
    C-&amp;gt;&amp;gt;C: 애플리케이션 처리
    C-&amp;gt;&amp;gt;OS: offset commit(자동/수동)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consumer는 &lt;b&gt;poll loop&lt;/b&gt;로 당긴다. 한 번에 여러 메시지 batch가 반환된다.&lt;/li&gt;
&lt;li&gt;offset은 &lt;code&gt;__consumer_offsets&lt;/code&gt; 내부 topic에 저장된다(0.9부터 ZooKeeper에서 이관). 장 04 상세.&lt;/li&gt;
&lt;li&gt;같은 offset을 여러 번 읽을 수 있다(replay). consumer가 자기 offset만 기억하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 pull인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;broker가 consumer에게 push하는 대신 consumer가 당기게 한 이유는 세 가지다 &lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;(design.md: Push vs. pull)&lt;/a&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consumer가 자기 속도를 스스로 제어 &amp;rarr; 압도당하지 않는다(backpressure가 자연스럽다).&lt;/li&gt;
&lt;li&gt;broker가 consumer 상태를 추적할 부담이 줄어든다.&lt;/li&gt;
&lt;li&gt;replay(과거 offset부터 다시 읽기)가 자연스럽다 &amp;mdash; push 모델에선 불가능하거나 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;offset &amp;mdash; consumer의 읽기 커서, 그리고 로그가 사라지는 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 partition 내 각 메시지의 &lt;b&gt;단조증가 번호&lt;/b&gt;다. consumer는 &quot;어디까지 읽었나&quot;를 offset으로 기억하고, 다음 poll부터 그 다음 offset을 요청한다. 두 가지를 짚고 넘어가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;offset commit&lt;/b&gt; &amp;mdash; consumer가 읽은 위치를 영속화하는 행위. 자동(&lt;code&gt;enable.auto.commit=true&lt;/code&gt;, poll 주기마다)과 수동(애플리케이션이 처리 완료 후 명시 commit)이 있다. 자동이 편하지만 함정이 있다 &amp;mdash; &quot;읽고 commit 했는데 처리가 실패하면 그 메시지는 영영 사라진다(최소 한 번 at-least-once 위반 방향)&quot;. 신뢰성이 중요하면 수동 commit이 필요하다(장 04&amp;middot;08).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;retention &amp;mdash; 로그가 사라지는 조건&lt;/b&gt;. consumer가 읽든 안 읽든 상관없이, 메시지는 retention 정책이 도달하면 삭제된다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;retention.ms&lt;/code&gt;(시간, 기본 168시간=7일) 또는 &lt;code&gt;retention.bytes&lt;/code&gt;(크기) 중 &lt;b&gt;먼저 도달한 쪽&lt;/b&gt;이 삭제를 발동시킨다.&lt;/li&gt;
&lt;li&gt;둘 다 설정하면 OR가 아니라 &quot;먼저 닿은 쪽&quot;이다 &amp;mdash; 이걸 단일 조건으로 오해하면 보관 기간 추정이 틀린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retention은 장 07에서, 정확한 전달(수동 commit&amp;middot;exactly-once)은 장 08에서 다룬다. 여기선 &quot;소비해도 지워지지 않되, retention이 닿으면 지워진다&quot;는 두 축만 기억하라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 이렇게 설계됐나 &amp;mdash; 네 가지 핵심 설계 결정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 모든 것이 네 가지 질문에 대한 답으로 요약된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;왜 로그인가?&lt;/b&gt; 로그는 append&amp;middot;순차읽기에 최적화된 가장 단순한 자료구조다. 복제&amp;middot;재생산(replay)&amp;middot;순서 보장이 모두 자연스럽다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 partition인가?&lt;/b&gt; 단일 로그는 병렬성이 없다. partition으로 쪼개면 각 partition이 독립된 병렬 단위가 된다(쓰기도 분산, consumer도 분산). 대신 전체 순서를 포기하고 partition 내 순서만 보장한다는 트레이드오프.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 consumer group인가?&lt;/b&gt; 한 partition을 여러 consumer가 동시에 읽으면 순서가 꼬인다. 그래서 &lt;b&gt;한 consumer group 내에서 각 partition은 정확히 한 consumer가 소유&lt;/b&gt;한다. consumer가 늘어나면 partition이 재분배(rebalance). 장 05 상세.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 pull인가?&lt;/b&gt; consumer가 속도를 스스로 제어하고, broker 부담을 줄이고, replay를 자연스럽게.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지는 독립이 아니라 서로를 강화한다. 로그이기 때문에 partition으로 쪼개도 순서가 partition 내에 보존되고, partition이 병렬 단위이기 때문에 consumer group이 partition을 소유하는 모델이 말이 되며, pull이기 때문에 consumer group마다 자기 offset&amp;middot;속도로 독립 읽기가 가능하다. 하나를 빼면 전체가 흔들린다 &amp;mdash; 그래서 Kafka를 &quot;빠른 큐&quot;로만 보면 이 네 기둥의 시너지를 놓친다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ZK는 사라졌다 &amp;mdash; KRaft 시대&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 4.0(2025-03)부터 &lt;b&gt;ZooKeeper 모드가 완전히 제거&lt;/b&gt;되고 KRaft만 남았다. 메타데이터(어떤 topic, 어느 partition의 리더가 누구, ISR은 누구)를 더 이상 외부 코디네이터(ZK)에 맡기지 않고, &lt;b&gt;broker들 사이에서 Raft 합의&lt;/b&gt;로 스스로 관리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 ZK를 버렸나 &amp;mdash; 메타데이터 병목&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZK 시절 Kafka는 두 시스템을 운영해야 했다: 메시지를 다루는 broker와 메타데이터를 다루는 ZK 앙상블. 이중 운영은 그 자체로 부담이었지만, 진짜 문제는 &lt;b&gt;확장성&lt;/b&gt;이었다. partition 수가 늘어날수록 broker가 ZK에 갱신&amp;middot;구독해야 할 메타데이터가 선형으로 늘어났고, 수만 partition 규모에서 controller(ZK와 통신하는 특별 broker)가 병목이 됐다. controller 장애 시 메타데이터를 새 controller가 다시 로드하는 데 수분이 걸리는 일도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KRaft는 이 메타데이터 자체를 &lt;b&gt;Kafka의 로그 구조에 담아&lt;/b&gt; Raft로 복제한다. 그래서 메타데이터 갱신이 단일 Raft 로그로 선형화(linearizable)되고, controller 장애 시에도 다른 controller가 즉시 이어받는다(수분이 아니라 수초). &quot;Kafka로 Kafka의 메타데이터를 관리한다&quot;는 자기참조적 설계다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;역할 분리&lt;/b&gt;: 각 서버는 &lt;code&gt;process.roles&lt;/code&gt;로 &lt;code&gt;broker&lt;/code&gt;, &lt;code&gt;controller&lt;/code&gt;, 또는 &lt;code&gt;broker,controller&lt;/code&gt;(combined) 중 하나. 단일 노드 학습은 combined.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;controller quorum&lt;/b&gt;: controller들이 Raft로 활성 컨트롤러를 선출. 과반수(quorum)가 살아있으면 메타데이터 일관성 유지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이득&lt;/b&gt;: 외부 의존성(ZK) 제거, 메타데이터 갱신 선형화, 대규모 클러스터(partition 수만 개)에서 확장 용이, controller 장애 복구 시간 단축.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일 노드 vs 프로덕션 토폴로지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글(와 이 서브프로젝트 전체)의 실습은 &lt;b&gt;combined 모드 단일 노드&lt;/b&gt;다 &amp;mdash; 한 프로세스가 broker와 controller 역할을 모두 한다. 학습엔 충분하지만 내결함성은 없다(그 프로세스가 죽으면 클러스터 끝). 프로덕션 표준 토폴로지는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;broker &lt;code&gt;replication.factor=3&lt;/code&gt;(장 06) &amp;mdash; 1 broker 장애까지 데이터 손실 없이 감내.&lt;/li&gt;
&lt;li&gt;controller 3개(quorum) &amp;mdash; 1개 장애까지 감내(과반수 2 유지).&lt;/li&gt;
&lt;li&gt;역할은 분리(controller-only 노드 + broker-only 노드)하거나 combined하거나, 규모에 따라 선택.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 노드 환경에선 복제&amp;middot;ISR&amp;middot;leader 선출이 의미 없다. 복제 실습은 최소 3 broker 환경이 필요하다(장 06&amp;middot;gotchas.md).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZK 시절(3.x 이하)은 역사적 배경으로만. 4.x 자료가 아니면 &lt;code&gt;--zookeeper&lt;/code&gt;&amp;middot;&lt;code&gt;zookeeper.connect&lt;/code&gt;&amp;middot;&lt;code&gt;config/kraft/&lt;/code&gt; 같은 명령/설정이 등장하는데, 이것들은 4.x에서 동작하지 않는다. (장 12 운영&amp;middot;gotchas.md 참조)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;core 위에 생태계가 올라간다 &amp;mdash; 범위 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 본 broker&amp;middot;topic&amp;middot;partition&amp;middot;producer&amp;middot;consumer가 &lt;b&gt;Apache Kafka core&lt;/b&gt;(ASF 산하)다. 이 로그 모델 위에 몇 가지가 더 올라간다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kafka Connect&lt;/b&gt;(ASF core): source/sink 커넥터로 DB&amp;middot;파일 &amp;harr; Kafka 이동. 장 10.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka Streams&lt;/b&gt;(ASF core): topic 간 스트림 처리 라이브러리(topology&amp;middot;state store). 장 11.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Schema Registry&lt;/b&gt;(Confluent 제공, &lt;b&gt;ASF core 아님&lt;/b&gt;): Avro/JSON/Protobuf 스키마를 저장&amp;middot;호환성 검사. 장 09. core에 포함되지 않으므로 별도 설치&amp;middot;별도 문서.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 서브프로젝트는 core를 중심으로 하되 생태계는 &quot;어떤 문제를 푸는가&amp;middot;core와 어디서 만나는가&quot;로 다룬다. core 사실과 Confluent 확장 사실을 섞지 않는 게 핵심 규칙이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka 4.3 실습 &amp;mdash; 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전: KRaft 단일 노드 기동(&lt;code&gt;harness/commands.md&lt;/code&gt; 또는 &lt;code&gt;kafka-verify&lt;/code&gt; 스킬). 단일 노드라 복제 동작은 일부만 관찰된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;topic이 &quot;삭제되지 않는 로그&quot;임 확인&lt;/h3&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# Kafka 4.3, KRaft 단일 노드
bin/kafka-topics.sh --create --topic arch-demo --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092
bin/kafka-console-producer.sh --topic arch-demo --bootstrap-server localhost:9092
# (콘솔에 메시지 몇 줄 입력 후 Ctrl-D)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: producer가 종료돼도 메시지가 broker에 남아 있다(로그 모델).&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;bin/kafka-console-consumer.sh --topic arch-demo --from-beginning --bootstrap-server localhost:9092
# 같은 메시지를 처음부터 다시 읽을 수 있다(replay)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partition 분산과 offset 확인&lt;/h3&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;bin/kafka-topics.sh --describe --topic arch-demo --bootstrap-server localhost:9092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: &lt;code&gt;Partition: 0/1/2&lt;/code&gt;, 각 partition의 &lt;code&gt;Leader&lt;/code&gt;, &lt;code&gt;Replicas&lt;/code&gt;, &lt;code&gt;Isr&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Topic: arch-demo  PartitionCount: 3  ReplicationFactor: 1
  Topic: arch-demo  Partition: 0  Leader: 1  Replicas: 1  Isr: 1
  Topic: arch-demo  Partition: 1  Leader: 1  Replicas: 1  Isr: 1
  Topic: arch-demo  Partition: 2  Leader: 1  Replicas: 1  Isr: 1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key 없이 produce하면 sticky로 한 partition에 몰릴 수 있어 위 describe만으로는 분산이 안 보일 수 있다. key를 주고 produce하면 &lt;code&gt;murmur2(key) % 3&lt;/code&gt;로 고정 partition이 결정되는지 교차 확인하라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Kafka는 큐라서 소비하면 사라진다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;로그&lt;/b&gt;다. 소비해도 retention이 지나기 전엔 남는다. 그래서 replay&amp;middot;여러 consumer 그룹 독립 읽기 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Kafka는 전체 순서를 보장한다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;partition 내&lt;/b&gt; 순서만. 여러 partition 섞으면 순서 보장 없음. 전체 순서 필요하면 partition=1(병렬성 포기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Kafka는 실시간 push다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;pull 기반&lt;/b&gt;. consumer가 &lt;code&gt;poll()&lt;/code&gt;로 당긴다. broker가 밀어넣지 않는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;key만 주면 순서 보장 + 병렬 처리 둘 다&quot;&lt;/td&gt;
&lt;td&gt;같은 key는 같은 partition &amp;rarr; 순서 보장. 하지만 key 종류가 적으면 partition이 쏠려 병렬성이 떨어짐(cardinality 트레이드오프)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;partition은 줄일 수 있다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;줄일 수 없다&lt;/b&gt;. 늘리기만 가능(02장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Kafka가 빠른 건 메모리 DB라서&quot;&lt;/td&gt;
&lt;td&gt;아니다. 디스크 기반 로그 + 순차 I/O + zero-copy + batch 덕분&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;ZK 기반 자료 그대로 써도 된다&quot;&lt;/td&gt;
&lt;td&gt;4.x에선 &lt;code&gt;--zookeeper&lt;/code&gt; 동작 안 함. 무조건 &lt;code&gt;--bootstrap-server&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Kafka = 분산 커밋 로그&lt;/b&gt;. 소비해도 retention 전엔 남는다(재생산&amp;middot;다중 소비 가능). 이게 &quot;큐가 아닌 로그&quot;라는 한 문장이 모든 설계를 설명한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;partition = append-only log + 순서 보장 단위 + 병렬 단위&lt;/b&gt;. 전체 순서 포기, partition 내 순서 보장. partition은 늘릴 수만 있고 줄일 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;record&lt;/b&gt; = &lt;code&gt;key&lt;/code&gt; + &lt;code&gt;value&lt;/code&gt; + &lt;code&gt;timestamp&lt;/code&gt; + &lt;code&gt;headers&lt;/code&gt;(0.11+). 같은 key &amp;rarr; &lt;code&gt;murmur2(key) % numPartitions&lt;/code&gt;(consistent hash 아님).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;producer 쓰기&lt;/b&gt;: batch &amp;rarr; leader append &amp;rarr; ISR 복제 &amp;rarr; ack(&lt;code&gt;acks&lt;/code&gt;). 압축은 종단간.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;consumer 읽기&lt;/b&gt;: pull(&lt;code&gt;poll()&lt;/code&gt;) &amp;rarr; offset 기반 &amp;rarr; &lt;code&gt;__consumer_offsets&lt;/code&gt;에 commit. replay 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빠른 이유&lt;/b&gt;: 순차 I/O + batch 압축 + zero-copy(&lt;code&gt;sendfile&lt;/code&gt;). 마법 아님 &amp;mdash; 페이지 캐시를 읽기 캐시로 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;KRaft&lt;/b&gt;: ZK 제거(4.0), broker들이 Raft로 메타데이터 자체 관리. ZK 시절 명령/설정은 4.x에서 동작 안 함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설계의 네 기둥&lt;/b&gt;: 로그(단순+순차) &amp;middot; partition(병렬, 순서 트레이드오프) &amp;middot; consumer group(한 partition=한 consumer) &amp;middot; pull(속도 제어+replay).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Kafka가 &quot;큐가 아니라 로그&quot;라는 말의 의미를, retention 관점에서 설명하라.&lt;/li&gt;
&lt;li&gt;같은 key의 메시지가 어떻게 같은 partition에 도달하는지, 파티셔너 알고리즘(murmur2)을 포함해 설명하라.&lt;/li&gt;
&lt;li&gt;consumer가 broker에 데이터를 &quot;요청&quot;한다는 것이 왜 설계상 유리한가(pull vs push)?&lt;/li&gt;
&lt;li&gt;partition을 늘리면 얻는 것과 잃는 것을 각각 말하라. 왜 줄일 수 없는가?&lt;/li&gt;
&lt;li&gt;Kafka가 디스크 기반인데 빠른 이유를 세 가지 설계 결정으로 설명하라.&lt;/li&gt;
&lt;li&gt;zero-copy(sendfile)가 없었다면 consumer 읽기 경로에서 구체적으로 어떤 비용이 추가되는가?&lt;/li&gt;
&lt;li&gt;KRaft가 ZooKeeper를 대체한 핵심 이유 하나를 말하라.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/getting-started/introduction.md&quot;&gt;Introduction &amp;mdash; Main Concepts&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/design/design.md&quot;&gt;Design &amp;mdash; Motivation &amp;amp; Persistence&lt;/a&gt; - 접근 2026-07-09 (순차 I/O, zero-copy, push vs pull, batch compression)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/implementation/message-format.md&quot;&gt;Record message format&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/clients/src/main/java/org/apache/kafka/clients/producer/internals/BuiltInPartitioner.java&quot;&gt;BuiltInPartitioner.java (murmur2)&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/trunk/docs/operations/kraft.md&quot;&gt;KRaft &amp;mdash; operations/kraft.md&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;KIP-833(KRaft production ready), KIP-853(동적 controller quorum), KIP-82(record headers)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Kafka</category>
      <category>Architecture</category>
      <category>kafka</category>
      <category>Kraft</category>
      <category>log</category>
      <category>partition</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/12</guid>
      <comments>https://technical-architect.tistory.com/entry/Kafka-architecture#entry12comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:52:15 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 11. services</title>
      <link>https://technical-architect.tistory.com/entry/Linux-services</link>
      <description>&lt;h1&gt;메일은 한 번에 가지 않는다 &amp;mdash; 인터넷 서비스의 역할 분담&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메일을 보냈다. 바로 도착할까? 아니다. 메일은 &lt;b&gt;송신자 &amp;rarr; 송신 서버(MTA) &amp;rarr; 수신 서버(MTA) &amp;rarr; 메일함(MDA) &amp;rarr; 수신자(MUA)&lt;/b&gt; 의 다단계 흐름을 거친다. 어디쯤 걸려 지연되는지 알려면 이 흐름을 알아야 한다. 단일 &quot;메일 서버&quot;라는 말이 감추는 것이 바로 이 분업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 주된 용도가 바로 이 서비스들이다 &amp;mdash; 웹, 이름 해석(DNS), 메일, 파일 공유, 데이터베이스. 각각 고유한 &lt;b&gt;프로토콜&amp;middot;포트&amp;middot;패키지&amp;middot;설정 파일&lt;/b&gt;을 갖는다. 이 글은 그 매핑과, 서비스가 &quot;설치했는데 외부 접속이 안 될 때&quot;의 3단계 진단을 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서비스는 역할별 부서다 &amp;mdash; 포트가 내선 번호다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 하나의 회사, 각 서비스는 부서이자 안내 데스크다. 각 부서에 고유 내선(포트)이 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flowchart TB
    CLIENT[클라이언트] --&amp;gt; WEB[웹: 80/443&amp;lt;br/&amp;gt;httpd / nginx]
    CLIENT --&amp;gt; DNS[DNS: 53&amp;lt;br/&amp;gt;BIND named]
    CLIENT --&amp;gt; MAIL[메일: 25/465/993&amp;lt;br/&amp;gt;Postfix + Dovecot]
    CLIENT --&amp;gt; FILE[파일공유: 445/2049/21&amp;lt;br/&amp;gt;Samba / NFS / vsftpd]
    CLIENT --&amp;gt; DB[DB: 3306/5432&amp;lt;br/&amp;gt;MariaDB / PostgreSQL]&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;프로토콜&lt;/th&gt;
&lt;th&gt;포트&lt;/th&gt;
&lt;th&gt;Rocky 10 패키지&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;웹&lt;/td&gt;
&lt;td&gt;HTTP/HTTPS&lt;/td&gt;
&lt;td&gt;80 / 443&lt;/td&gt;
&lt;td&gt;httpd, &lt;b&gt;nginx 1.26&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;DNS(UDP/TCP)&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;bind(named)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메일 송신&lt;/td&gt;
&lt;td&gt;SMTP&lt;/td&gt;
&lt;td&gt;25, 465(SMTPS), 587&lt;/td&gt;
&lt;td&gt;&lt;b&gt;postfix&lt;/b&gt;(기본 MTA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메일 수신&lt;/td&gt;
&lt;td&gt;IMAP/POP3&lt;/td&gt;
&lt;td&gt;143/993, 110/995&lt;/td&gt;
&lt;td&gt;dovecot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일(윈도 공유)&lt;/td&gt;
&lt;td&gt;SMB/CIFS&lt;/td&gt;
&lt;td&gt;445, 139&lt;/td&gt;
&lt;td&gt;samba&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일(유닉스 공유)&lt;/td&gt;
&lt;td&gt;NFS&lt;/td&gt;
&lt;td&gt;2049&lt;/td&gt;
&lt;td&gt;nfs-utils&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일(FTP)&lt;/td&gt;
&lt;td&gt;FTP&lt;/td&gt;
&lt;td&gt;21(제어), 20(데이터)&lt;/td&gt;
&lt;td&gt;vsftpd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;MySQL 프로토콜&lt;/td&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MariaDB 10.11&lt;/b&gt; / MySQL 8.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PostgreSQL 16.8&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부서 비유는 &quot;역할별 부서 + 고유 내선(포트)&quot;만 잡는다. 서비스는 동시 다수 연결을 처리하고(멀티스레드/이벤트), 메일은 여러 서버를 거친다는 점에서 비유를 넘어선다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹 서버 &amp;mdash; httpd(Apache) / nginx&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청을 받아 정적/동적 콘텐츠를 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;httpd(Apache)&lt;/b&gt;: 전통 모듈 구조(prefork/worker/event MPM). &lt;code&gt;.htaccess&lt;/code&gt;, 가상호스트.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nginx&lt;/b&gt;: 이벤트 기반, 고동시성에 강, 리버스 프록시/로드밸런서로도 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo dnf install -y httpd       # 또는 nginx
sudo systemctl enable --now httpd
sudo firewall-cmd --permanent --add-service={http,https}
sudo firewall-cmd --reload&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;패키지&lt;/th&gt;
&lt;th&gt;주 설정&lt;/th&gt;
&lt;th&gt;가상호스트&lt;/th&gt;
&lt;th&gt;문서루트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;httpd&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/httpd/conf/httpd.conf&lt;/code&gt;, &lt;code&gt;/etc/httpd/conf.d/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/httpd/conf.d/vhost.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/www/html&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nginx&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;, &lt;code&gt;/etc/nginx/conf.d/*.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/nginx/conf.d/vhost.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/usr/share/nginx/html&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비표준 포트/디렉토리를 쓰면 SELinux가 차단한다 &amp;mdash; &lt;code&gt;semanage&lt;/code&gt;로 컨텍스트/포트 허용 추가(10장).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DNS &amp;mdash; BIND(named)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 이름 &amp;harr; IP 변환. 영역(zone) 단위로 관리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데몬: &lt;code&gt;named&lt;/code&gt;. 포트 53(UDP/TCP).&lt;/li&gt;
&lt;li&gt;마스터(Primary)/슬레이브(Secondary) 영역 복제.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 레코드 종류:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레코드&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;A&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;도메인 &amp;rarr; IPv4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;AAAA&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;도메인 &amp;rarr; IPv6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MX&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메일 교환 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CNAME&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;별칭(다른 이름으로 리디렉션)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;네임서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TXT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;임의 텍스트(SPF, DKIM 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SOA&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;영역 시작&amp;middot;관리 정보&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;sudo dnf install -y bind bind-utils
sudo systemctl enable --now named&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/etc/named.conf&lt;/code&gt; &amp;mdash; 주 설정(옵션, zone 선언, ACL).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/named/&lt;/code&gt; &amp;mdash; zone 파일(&lt;code&gt;example.com.zone&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메일 &amp;mdash; 왜 한 번에 안 가는가 (다단계 흐름)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도입부의 질문으로 돌아가자. 메일은 한 번에 가지 않는다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    S[송신자 MUA] --&amp;gt;|SMTP| MTA1[MTA - Postfix&amp;lt;br/&amp;gt;송신 서버]
    MTA1 --&amp;gt;|SMTP| MTA2[MTA - 수신 서버]
    MTA2 --&amp;gt;|LDA| MDA[MDA - Dovecot&amp;lt;br/&amp;gt;메일함 저장]
    R[수신자 MUA] --&amp;gt;|IMAP/POP3| MDA&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MTA&lt;/b&gt;(Mail Transfer Agent): 서버 간 메일 송수신. SMTP. Rocky 10 기본 = &lt;b&gt;postfix&lt;/b&gt;(sendmail은 사실상 사장).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MDA&lt;/b&gt;(Mail Delivery Agent): 메일함에 저장. Dovecot이 겸함.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MUA&lt;/b&gt;(Mail User Agent): 사용자 클라이언트(Thunderbird). IMAP/POP3로 Dovecot에 접속.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분업 덕에 송신 서버와 수신 서버가 다를 수 있고, 수신자는 나중에 메일함에서 읽을 수 있다. &quot;메일이 지연된다&quot;면 이 흐름의 어느 단계(MTA&amp;harr;MTA 전송? MDA 저장? MUA 조회?)인지를 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SMTP 포트의 분담도 이 분업을 반영한다: &lt;b&gt;25번은 서버 간&lt;/b&gt;(MTA&amp;harr;MTA), &lt;b&gt;587/465는 사용자 제출&lt;/b&gt;(MUA&amp;rarr;MTA, 인증). 25번을 사용자 송신용으로 쓰지 않는다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo dnf install -y postfix dovecot
sudo systemctl enable --now postfix&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/etc/postfix/main.cf&lt;/code&gt; &amp;mdash; Postfix 주 설정(myhostname, mydomain, myorigin, inet_interfaces).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/dovecot/dovecot.conf&lt;/code&gt; &amp;mdash; Dovecot 주 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 공유 &amp;mdash; Samba / NFS / vsftpd&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;th&gt;클라이언트&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Samba&lt;/b&gt;(SMB/CIFS)&lt;/td&gt;
&lt;td&gt;윈도우와 공유&lt;/td&gt;
&lt;td&gt;Windows 탐색기, smbclient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;NFS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;유닉스/리눅스 간 공유&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mount -t nfs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;vsftpd&lt;/b&gt;(FTP)&lt;/td&gt;
&lt;td&gt;범용 파일 전송(레거시)&lt;/td&gt;
&lt;td&gt;ftp 클라이언트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Samba: &lt;code&gt;/etc/samba/smb.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;NFS: &lt;code&gt;/etc/exports&lt;/code&gt;(내보낼 디렉토리). &lt;code&gt;exportfs -rav&lt;/code&gt;로 적용.&lt;/li&gt;
&lt;li&gt;vsftpd: &lt;code&gt;/etc/vsftpd/vsftpd.conf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NFSv3와 NFSv4는 다르다. NFSv4는 단일 포트(2049)로 방화벽이 쉽지만, v3는 동적 다수 포트를 쓴다. Rocky 10은 v4가 기본. FTP는 평문 비밀번호 문제로 레거시 &amp;mdash; 신규는 SFTP/SCP를 권장한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NFS 마운트(클라이언트):&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;sudo mount -t nfs server:/shared /mnt&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스 &amp;mdash; MariaDB / PostgreSQL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화 데이터 저장&amp;middot;질의. LAMP/LEMP 스택의 핵심이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MariaDB&lt;/b&gt;: MySQL 포크. Rocky 10 기본 DB(10.11). MySQL 8.4도 선택 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PostgreSQL&lt;/b&gt;: 객체-관계형 DB. Rocky 10 = 16.8.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MariaDB에 &lt;code&gt;mysql&lt;/code&gt; 명령으로 접속하는 건 이상해 보이지만 자연스럽다 &amp;mdash; MariaDB가 MySQL의 포크라 클라이언트 호환성을 유지하기 때문. 명령은 &lt;code&gt;mysql&lt;/code&gt;이지만 실제는 MariaDB다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# MariaDB
sudo dnf install -y mariadb-server
sudo systemctl enable --now mariadb
sudo mysql_secure_installation      # 초기 보안 설정

# PostgreSQL
sudo dnf install -y postgresql-server
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장. 서비스 시작 전 항상 SELinux/firewalld를 고려하라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 패키지 버전 확인
dnf info nginx | grep Version          # 1.26.x
dnf info mariadb-server | grep Version # 10.11
dnf info postgresql-server | grep Version  # 16.8&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 서비스 운영 공통
sudo dnf install -y &amp;lt;pkg&amp;gt;
sudo systemctl enable --now &amp;lt;svc&amp;gt;   # 부팅 자동 + 즉시 시작
sudo systemctl status &amp;lt;svc&amp;gt;
sudo systemctl reload &amp;lt;svc&amp;gt;         # 끊김 없이 재적용(httpd/nginx/postfix)
sudo journalctl -u &amp;lt;svc&amp;gt; -f         # 실시간 로그&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 웹 서버 기동 확인
curl -I http://localhost            # HTTP/1.1 200
ss -tlnp | grep -E ':80|:443'       # 리스닝 확인&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 방화벽에 서비스 개방
sudo firewall-cmd --permanent --add-service={http,https,dns,smtp,imap}
sudo firewall-cmd --reload
firewall-cmd --list-services&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;MariaDB와 MySQL은 다른 DB&quot;&lt;/td&gt;
&lt;td&gt;MariaDB가 MySQL &lt;b&gt;포크&lt;/b&gt;. &lt;code&gt;mysql&lt;/code&gt; 명령으로 접속하지만 실제는 MariaDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;메일 25번이 사용자용&quot;&lt;/td&gt;
&lt;td&gt;25는 &lt;b&gt;서버 간&lt;/b&gt;(MTA&amp;harr;MTA). 587/465가 사용자 제출(MUA&amp;rarr;MTA, 인증)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;FTP가 파일 공유 표준&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;레거시&lt;/b&gt;. 평문 비번 문제. 신규는 SFTP/SCP 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;NFSv3와 NFSv4는 같다&quot;&lt;/td&gt;
&lt;td&gt;NFSv4는 단일 포트(2049)로 방화벽 쉬움. v3는 동적 다수 포트. Rocky 10은 v4 기본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;서비스 설정 파일 위치는 다 비슷&quot;&lt;/td&gt;
&lt;td&gt;아니다. 각기 다름(표 참조). 반드시 매핑 숙지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;서비스 안 되면 패키지만 재설치&quot;&lt;/td&gt;
&lt;td&gt;9할은 &lt;b&gt;SELinux/firewalld&lt;/b&gt;. 설정 파일&amp;middot;포트&amp;middot;컨텍스트 확인 먼저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;포트만 열면 외부 접속된다&quot;&lt;/td&gt;
&lt;td&gt;서비스가 &lt;code&gt;0.0.0.0&lt;/code&gt;에 바인딩돼야(&lt;code&gt;127.0.0.1&lt;/code&gt;이면 로컬만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;DB 데이터는 어디든 둬도 된다&quot;&lt;/td&gt;
&lt;td&gt;SELinux가 기본 경로(&lt;code&gt;/var/lib/mysql&lt;/code&gt;, &lt;code&gt;/var/lib/pgsql&lt;/code&gt;) 외엔 차단. &lt;code&gt;semanage fcontext&lt;/code&gt;로 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가상화&amp;middot;컨테이너&lt;/b&gt;: KVM/libvirt(네이티브 하이퍼바이저, VM 실행), Podman 5(Docker 호환, daemonless/rootless, systemd quadlet 통합).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증 서비스&lt;/b&gt;: LDAP(중앙 계정 디렉토리), Kerberos(티켓 기반 강력 인증), SSSD(로컬과 IdM/AD/LDAP 연결).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NTP/chrony&lt;/b&gt;: 시간 동기화. Rocky 10 기본 = &lt;b&gt;chrony&lt;/b&gt;. 인증서(Kerberos), 로그 정렬, 분산 시스템에 필수. &lt;code&gt;chronyc sources&lt;/code&gt;로 상태 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스별 포트&lt;/b&gt;: 80/443(웹), 53(DNS), 25/465/587(SMTP), 143/993(IMAP), 445(SMB), 2049(NFS), 21(FTP), 3306(MariaDB), 5432(PostgreSQL).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹&lt;/b&gt;: httpd(&lt;code&gt;/etc/httpd/&lt;/code&gt;), nginx(&lt;code&gt;/etc/nginx/&lt;/code&gt;). Rocky 10 nginx 1.26.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNS&lt;/b&gt;: bind(named), &lt;code&gt;/etc/named.conf&lt;/code&gt;. 레코드: A/AAAA/MX/CNAME/NS/TXT/SOA.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메일&lt;/b&gt;: postfix(MTA) + dovecot(MDA/IMAP). &lt;code&gt;/etc/postfix/main.cf&lt;/code&gt;. &lt;b&gt;한 번에 안 간다&lt;/b&gt; &amp;mdash; MUA&amp;rarr;MTA&amp;rarr;MTA&amp;rarr;MDA&amp;rarr;MUA 다단계. 25=서버 간, 587/465=사용자 제출.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일&lt;/b&gt;: Samba(SMB/윈도), NFS(유닉스, v4 기본), vsftpd(FTP/레거시).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB&lt;/b&gt;: MariaDB 10.11(Rocky 10 기본, MySQL 포크), PostgreSQL 16.8.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 안 될 때 3단계&lt;/b&gt;: (1) 서비스 상태 (2) firewalld 포트 (3) SELinux 컨텍스트.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;웹/DNS/메일(SMTP)/파일(SMB)/DB(PostgreSQL)의 기본 포트를 말하라.&lt;/li&gt;
&lt;li&gt;httpd와 nginx의 핵심 설정 파일 경로와 기본 문서루트를 비교하라.&lt;/li&gt;
&lt;li&gt;DNS 레코드 A, MX, CNAME, NS의 의미를 말하라.&lt;/li&gt;
&lt;li&gt;MTA, MDA, MUA의 역할과 메일 흐름을 설명하라. 메일이 &quot;한 번에 안 가는&quot; 이유는?&lt;/li&gt;
&lt;li&gt;SMTP 25와 587/465의 용도 차이는?&lt;/li&gt;
&lt;li&gt;Samba와 NFS의 주요 차이. 각각 어떤 환경에 어울리는가?&lt;/li&gt;
&lt;li&gt;MariaDB와 MySQL의 관계는? 왜 &lt;code&gt;mysql&lt;/code&gt; 명령으로 접속하는가?&lt;/li&gt;
&lt;li&gt;서비스가 설치됐는데 외부 접속이 안 될 때 확인 3단계는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://rockylinux.org/news/rocky-linux-10-0-ga-release&quot;&gt;Rocky Linux 10 Release Notes &amp;mdash; Notable packages&lt;/a&gt; - 접근 2026-07-09 (nginx 1.26, MariaDB 10.11, PostgreSQL 16.8)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/&quot;&gt;RHEL 10 문서&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man httpd&lt;/code&gt;, &lt;code&gt;man nginx&lt;/code&gt;, &lt;code&gt;man named&lt;/code&gt;, &lt;code&gt;man postfix&lt;/code&gt;, &lt;code&gt;man smb.conf&lt;/code&gt;, &lt;code&gt;man exports(5)&lt;/code&gt;, &lt;code&gt;man mysqld&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>Database</category>
      <category>dns</category>
      <category>Linux</category>
      <category>mail</category>
      <category>NFS</category>
      <category>services</category>
      <category>web</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/11</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-services#entry11comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:36:19 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 10. security</title>
      <link>https://technical-architect.tistory.com/entry/Linux-security</link>
      <description>&lt;h1&gt;설정은 다 맞는데 403이 난다 &amp;mdash; 9할은 SELinux다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;httpd 설정을 다 맞췄다. 파일도 &lt;code&gt;/var/www/html&lt;/code&gt;에 넣었다. 그런데 브라우저에서 403 Forbidden. Apache 설정만 뒤지다 한참을 헤맨다 &amp;mdash; 그런데 진짜 원인은 &lt;b&gt;SELinux&lt;/b&gt;인 경우가 압도적으로 많다. 파일을 &lt;code&gt;cp&lt;/code&gt;/&lt;code&gt;mv&lt;/code&gt;로 옮기면 보안 레이블(컨텍스트)이 잘못되어 SELinux가 접근을 막는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 보안은 &lt;b&gt;다층 방어&lt;/b&gt;(defense in depth)다. 전통 권한(DAC) 위에 강제 접근 제어(SELinux), 네트워크 검문(firewalld), 인증(PAM)이 올려져 있고, 각 층이 독립적으로 통과돼야 접근이 허용된다. 이 장을 모르면 &quot;설정 다 맞는데 왜 안 되지?&quot;의 원인을 못 찾고, 방화벽&amp;middot;인증 정책도 못 짠다. 이 글은 그 다층 구조를 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;접근 제어의 두 층 &amp;mdash; DAC와 MAC, 둘 다 통과해야 한다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;보안 층&lt;/th&gt;
&lt;th&gt;건물 비유&lt;/th&gt;
&lt;th&gt;리눅스&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DAC&lt;/b&gt;(권한 rwx)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;출입증 소유자가&lt;/b&gt; 자기 사무실 출입 권한 부여&lt;/td&gt;
&lt;td&gt;전통 UNIX 퍼미션(05장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MAC&lt;/b&gt;(SELinux)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;건물 보안 정책이&lt;/b&gt; 출입증과 무관하게 추가 검문&lt;/td&gt;
&lt;td&gt;SELinux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;firewalld&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;건물 &lt;b&gt;정문 검문대&lt;/b&gt;(외부인 통제)&lt;/td&gt;
&lt;td&gt;네트워크 패킷 필터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;PAM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;신분증 확인 절차&lt;/b&gt;(로그인&amp;middot;sudo)&lt;/td&gt;
&lt;td&gt;인증 프레임워크&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모델&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;th&gt;예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DAC&lt;/b&gt;(임의)&lt;/td&gt;
&lt;td&gt;소유자가 권한 결정&lt;/td&gt;
&lt;td&gt;전통 UNIX 퍼미션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;MAC&lt;/b&gt;(강제)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;정책이&lt;/b&gt; 접근 강제. 소유자 권한 무관&lt;/td&gt;
&lt;td&gt;&lt;b&gt;SELinux&lt;/b&gt;, AppArmor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심: &lt;b&gt;DAC가 허용해도 MAC(SELinux)이 거부하면 접근 불가. 둘 다 통과해야 동작한다.&lt;/b&gt; 이것이 &quot;서비스 안 될 때 SELinux를 의심해야 하는 이유&quot;다. 권한(&lt;code&gt;chmod&lt;/code&gt;)만 보고 &quot;다 됐는데&quot;라고 착각하기 쉽기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SELinux &amp;mdash; 모든 것에 레이블을 붙인다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NSA가 기여한 MAC 구현이다. 모든 프로세스와 파일에 &lt;b&gt;레이블(컨텍스트)&lt;/b&gt;을 붙이고, 정책이 허용한 관계만 접근을 허용한다. &quot;웹 서버 프로세스가 웹 콘텐츠 디렉토리만 읽게&quot; 같은 세밀한 통제가 가능하다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;모드&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;enforcing&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;정책 위반 &lt;b&gt;차단 + 로그&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;운영 기본(Rocky 10 기본)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;permissive&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;위반 &lt;b&gt;허용 + 로그만&lt;/b&gt;(감사/디버그)&lt;/td&gt;
&lt;td&gt;문제 원인 파악&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;disabled&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;완전 비활성&lt;/td&gt;
&lt;td&gt;권장 안 함(재부팅 필요, 보안 약화)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책은 RHEL 10에서 &lt;b&gt;targeted&lt;/b&gt;(기본, 주요 서비스만 confinement)이 표준이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SELinux 컨텍스트(레이블)&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;user:role:type:mls
예: unconfined_u:object_r:httpd_sys_content_t:s0
     │           │        │                  │
     │           │        │                  └ MLS 레벨
     │           │        └ 타입(가장 중요, 정책 매칭 단위)
     │           └ 롤
     └ 사용자&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;type&lt;/b&gt;(&lt;code&gt;_t&lt;/code&gt; 접미사)가 정책 적용의 핵심이다. &lt;code&gt;httpd_t&lt;/code&gt; 프로세스는 &lt;code&gt;httpd_sys_content_t&lt;/code&gt; 파일만 읽도록 허용된다. 파일을 &lt;code&gt;cp&lt;/code&gt;/&lt;code&gt;mv&lt;/code&gt;로 &lt;code&gt;/var/www/html&lt;/code&gt;에 넣으면 이 type 레이블이 잘못되고, SELinux가 차단한다 &amp;mdash; 도입부의 403 사태.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;firewalld &amp;mdash; 네트워크 검문대&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 방화벽 관리 데몬. RHEL 10 백엔드는 &lt;b&gt;nftables&lt;/b&gt;(iptables는 unmaintained, 단계적 제거).&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;flowchart LR
    NIC[네트워크 인터페이스] --&amp;gt; ZONE[Zone&amp;lt;br/&amp;gt;trusted/public/dmz...]
    ZONE --&amp;gt; RULE[규칙: 서비스/포트/소스]
    RULE --&amp;gt; BACKEND[nftables 백엔드]
    BACKEND --&amp;gt; KERNEL[커널 패킷 필터]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Zone&lt;/b&gt;: 신뢰 수준별 규칙 묶음. 인터페이스를 zone에 할당. &lt;code&gt;public&lt;/code&gt;(기본), &lt;code&gt;trusted&lt;/code&gt;(모두 허용), &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;dmz&lt;/code&gt;, &lt;code&gt;home&lt;/code&gt;, &lt;code&gt;work&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Service&lt;/b&gt;: 미리 정의된 포트/프로토콜 묶음(&lt;code&gt;http&lt;/code&gt;, &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;dns&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Runtime vs Permanent&lt;/b&gt;: 기본은 임시(재시작 시 사라짐). &lt;code&gt;--permanent&lt;/code&gt;로 영구화한 뒤 &lt;code&gt;--reload&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PAM과 sudo &amp;mdash; 인증과 권한 위임&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PAM(Pluggable Authentication Modules)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증을 모듈화한 프레임워크. 로그인&amp;middot;sudo&amp;middot;ssh 등이 PAM을 통해 인증한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;flowchart LR
    APP[응용: login, sshd, sudo] --&amp;gt; PAM[PAM 라이브러리]
    PAM --&amp;gt; CONF[/etc/pam.d/&amp;lt;service&amp;gt;/]
    CONF --&amp;gt; MOD[모듈: pam_unix, pam_sss, ...]
    MOD --&amp;gt; BACKEND[로컬/LDAP/Kerberos/IdM]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정: &lt;code&gt;/etc/pam.d/&amp;lt;서비스&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;관리 도구: &lt;b&gt;authselect&lt;/b&gt;(RHEL 10). PAM/NSS 설정을 프로필 단위로 관리. &lt;b&gt;직접 &lt;code&gt;/etc/pam.d&lt;/code&gt; 편집 금지&lt;/b&gt;(authselect가 덮어씀).&lt;/li&gt;
&lt;li&gt;백엔드 통합: SSSD &amp;rarr; IdM/AD/LDAP.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;sudo &amp;mdash; 권한 위임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 사용자가 &lt;b&gt;root 권한 명령을 제한적으로&lt;/b&gt; 실행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정: &lt;code&gt;/etc/sudoers&lt;/code&gt;와 &lt;code&gt;/etc/sudoers.d/*&lt;/code&gt;. &lt;b&gt;반드시 &lt;code&gt;visudo&lt;/code&gt;로 편집&lt;/b&gt;(문법 검증).&lt;/li&gt;
&lt;li&gt;일반 사용자를 &lt;code&gt;wheel&lt;/code&gt; 그룹에 넣으면 sudo 사용 가능(RHEL 기본).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/etc/sudoers&lt;/code&gt;를 일반 에디터로 고치다 문법을 틀리면 &lt;b&gt;모든 sudo가 막힌다&lt;/b&gt; &amp;mdash; root조차 못 된다. 그래서 &lt;code&gt;visudo&lt;/code&gt;가 저장 시 문법을 검증한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 전체 암호화 정책(Crypto Policies)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RHEL은 TLS/SSH/IPsec/DNSSEC/Kerberos 허용 알고리즘을 &lt;b&gt;전역 정책&lt;/b&gt;으로 통일 관리한다: &lt;b&gt;DEFAULT&lt;/b&gt;(운영 기본, 현대 알고리즘), LEGACY(구형 호환), FUTURE(더 강력), FIPS(FIPS 140 준수).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# SELinux &amp;mdash; 서비스 안 될 때 가장 먼저
getenforce                       # 현재 모드
sestatus                         # 상세(모드, 정책)
sudo setenforce 0                # Permissive(임시)
sudo setenforce 1                # Enforcing

ls -Z /var/www/html/index.html   # 파일 컨텍스트
ps -eZ | grep httpd              # 프로세스 컨텍스트

# AVC 거부 로그(서비스 안 될 때)
sudo ausearch -m AVC -ts recent
sudo journalctl -t setroubleshoot&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELinux 문제 해결 흐름:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;sudo ausearch -m AVC -ts recent | tail          # 1. 거부 로그에서 AVC 확인
sudo restorecon -Rv /var/www/html               # 2. 컨텍스트 복구(잘못된 레이블 교정)
# 비표준 경로/포트는 semanage로 레이블 추가
sudo semanage fcontext -a -t httpd_sys_content_t &quot;/custom/web(/.*)?&quot;
sudo restorecon -Rv /custom/web
# 반복 거부는 정책 모듈 자동 생성
sudo grep &quot;...denied&quot; /var/log/audit/audit.log | audit2allow -M mypol
sudo semodule -i mypol.pp&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# firewalld
sudo systemctl status firewalld
firewall-cmd --get-default-zone
firewall-cmd --list-all                 # 현재 zone 전체

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload              # 영구 &amp;rarr; 런타임 반영

sudo firewall-cmd --permanent --change-interface=enp0s3 --zone=public&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;public (active)
  services: cockpit dhcpv6-client ssh
  ports: 8080/tcp&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;# PAM / authselect
authselect current                    # 현재 프로필
cat /etc/pam.d/system-auth            # authselect 관리(직접 편집 금지)
sudo authselect select sssd --update  # 프로필 변경&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# sudo
sudo -l                               # 내가 실행 가능한 명령
sudo visudo                           # 설정 편집(반드시 visudo)
echo &quot;alice ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx&quot; | sudo tee /etc/sudoers.d/alice&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 암호화 정책
update-crypto-policies --show         # DEFAULT
sudo update-crypto-policies --set FUTURE&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;설정 맞는데 안 되면 Apache 설정을 뒤진다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;9할 SELinux&lt;/b&gt;. permissive로 전환 후 AVC 로그 먼저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;SELinux 귀찮으니 disabled로&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;금지&lt;/b&gt;. 보안 무력화 + 재부팅 시 레이블 재작성(매우 느림). 차라리 permissive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;firewall-cmd --add-service=http&lt;/code&gt; 영구다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아니다&lt;/b&gt;. &lt;code&gt;--permanent&lt;/code&gt; 없으면 재시작 시 사라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;/etc/pam.d/system-auth&lt;/code&gt; 직접 고친다&quot;&lt;/td&gt;
&lt;td&gt;authselect가 다음 업데이트에 덮어씀. &lt;b&gt;authselect로&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;/etc/sudoers&lt;/code&gt;를 에디터로 고친다&quot;&lt;/td&gt;
&lt;td&gt;문법 틀리면 &lt;b&gt;모든 sudo 막힘&lt;/b&gt;(root 못 됨). &lt;b&gt;반드시 &lt;code&gt;visudo&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;SELinux는 무작위로 차단&quot;&lt;/td&gt;
&lt;td&gt;아니다. 컨텍스트 매칭 안 되면. &lt;code&gt;restorecon&lt;/code&gt;으로 교정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;firewalld 임시 규칙도 부팅 후 유지&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;사라짐&lt;/b&gt;. 운영은 &lt;code&gt;--permanent&lt;/code&gt; + &lt;code&gt;--reload&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;포트 열면 서비스 접속된다&quot;&lt;/td&gt;
&lt;td&gt;SELinux가 비표준 포트 차단 가능. &lt;code&gt;semanage port&lt;/code&gt;로 허용 추가 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;SELinux boolean&lt;/b&gt;: 세밀한 토글 스위치. &lt;code&gt;setsebool -P httpd_can_network_connect on&lt;/code&gt;(웹 서버가 네트워크 연결 허용). &lt;code&gt;getsebool -a&lt;/code&gt;로 전체 확인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;auditd와 침입 탐지&lt;/b&gt;: &lt;code&gt;auditd&lt;/code&gt;는 시스템 호출&amp;middot;파일 접근을 상세 로깅한다(&lt;code&gt;/var/log/audit/audit.log&lt;/code&gt;). &lt;code&gt;ausearch&lt;/code&gt;/&lt;code&gt;aureport&lt;/code&gt;로 분석. 침해 사후 조사의 핵심.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IdM(Identity Management)&lt;/b&gt;: RHEL의 중앙 인증(LDAP+Kerberos+DNS+CA 통합). Active Directory와 신뢰 관계 구성 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DAC&lt;/b&gt;(소유자 결정) + &lt;b&gt;MAC&lt;/b&gt;(정책 강제, SELinux). &lt;b&gt;둘 다 통과해야 접근&lt;/b&gt; &amp;mdash; 이게 &quot;권한 줬는데 왜 안 되지?&quot;의 답.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SELinux&lt;/b&gt;: 레이블(컨텍스트 &lt;code&gt;user:role:type:mls&lt;/code&gt;) 기반. &lt;b&gt;type&lt;/b&gt;(&lt;code&gt;_t&lt;/code&gt;)이 핵심. 모드는 enforcing(기본)/permissive/disabled(금지). &quot;설정 맞는데 403&quot;은 9할 컨텍스트 문제 &amp;rarr; &lt;code&gt;restorecon&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;firewalld&lt;/b&gt;: zone(public/trusted/dmz) + service. 백엔드 &lt;b&gt;nftables&lt;/b&gt;(Rocky 10). &lt;code&gt;--permanent&lt;/code&gt; + &lt;code&gt;--reload&lt;/code&gt;로 영구화.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PAM&lt;/b&gt;: &lt;code&gt;/etc/pam.d/&lt;/code&gt;. &lt;b&gt;authselect&lt;/b&gt;로 관리(직접 편집 금지).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sudo&lt;/b&gt;: &lt;code&gt;visudo&lt;/code&gt;만. &lt;code&gt;wheel&lt;/code&gt; 그룹이 기본 sudo 권한. 에디터로 고치면 문법 틀렸을 때 전체 마비.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Crypto policies&lt;/b&gt;: DEFAULT/LEGACY/FUTURE/FIPS.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서비스 안 될 때 3단계&lt;/b&gt;: (1) 서비스 상태 (2) firewalld 포트 (3) SELinux 컨텍스트.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DAC와 MAC의 차이. 둘 다 통과해야 하는 이유는?&lt;/li&gt;
&lt;li&gt;SELinux의 enforcing/permissive/disabled 차이. 왜 disabled를 피하는가?&lt;/li&gt;
&lt;li&gt;SELinux 컨텍스트에서 가장 중요한 필드는? 왜?&lt;/li&gt;
&lt;li&gt;&quot;httpd 설정 다 맞는데 403&quot;일 때 가장 먼저 확인할 것은?&lt;/li&gt;
&lt;li&gt;firewalld의 runtime과 permanent 차이. 운영에선 어떻게?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/pam.d/system-auth&lt;/code&gt;를 직접 고치면 안 되는 이유는?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/sudoers&lt;/code&gt;를 일반 에디터로 고치면 위험한 이유는? 안전한 도구는?&lt;/li&gt;
&lt;li&gt;Rocky 10의 firewalld 백엔드는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/pdf/using_selinux/Red_Hat_Enterprise_Linux-10-Using_SELinux-en-US.pdf&quot;&gt;Using SELinux &amp;mdash; RHEL 10&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html-single/configuring_firewalls_and_packet_filters/index&quot;&gt;Configuring firewalls &amp;mdash; RHEL 10&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/pdf/configuring_authentication_and_authorization_in_rhel/Red_Hat_Enterprise_Linux-10-Configuring_authentication_and_authorization_in_RHEL-en-US.pdf&quot;&gt;Configuring authentication &amp;mdash; RHEL 10&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man selinux(8)&lt;/code&gt;, &lt;code&gt;man sestatus&lt;/code&gt;, &lt;code&gt;man firewall-cmd&lt;/code&gt;, &lt;code&gt;man authselect&lt;/code&gt;, &lt;code&gt;man sudoers&lt;/code&gt;, &lt;code&gt;man pam(8)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>firewalld</category>
      <category>Linux</category>
      <category>PAM</category>
      <category>security</category>
      <category>selinux</category>
      <category>sudo</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/10</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-security#entry10comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:35:38 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 09. network</title>
      <link>https://technical-architect.tistory.com/entry/Linux-network</link>
      <description>&lt;h1&gt;DNS를 바꾸려고 resolv.conf를 고쳤다가 사라졌다 &amp;mdash; 네트워크 관리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 서버를 바꾸려고 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;를 직접 편집했다. 잘 되는 듯했는데 재부팅하니 원래대로 돌아갔다. 왜? &lt;b&gt;Rocky 10에선 NetworkManager가 &lt;code&gt;/etc/resolv.conf&lt;/code&gt;를 자동 관리&lt;/b&gt;한다. 직접 편집하면 덮어쓴다. DNS 변경은 &lt;code&gt;nmcli&lt;/code&gt;로 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 함정이 네트워크 관리엔 많다. 서버의 대부분은 네트워크로 통신하고, IP 설정&amp;middot;포트 점검&amp;middot;DNS 오류&amp;middot;라우팅 문제는 운영 일상이다. 이 글은 컴퓨터가 서로 통신하는 세 가지 기반 &amp;mdash; 서로를 식별할 &lt;b&gt;주소(IP)&lt;/b&gt;, 데이터를 전달할 &lt;b&gt;경로(라우팅)&lt;/b&gt;, 이름을 주소로 바꿀 &lt;b&gt;DNS&lt;/b&gt; &amp;mdash; 과, Rocky 10이 이걸 NetworkManager로 관리하는 방식을 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;우편 시스템으로 보는 네트워크 &amp;mdash; 주소&amp;middot;포트&amp;middot;이름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 통신은 우편 시스템과 정확히 대응된다(오래된 표준 비유):&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;네트워크&lt;/th&gt;
&lt;th&gt;우편&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;호스트 식별&lt;/td&gt;
&lt;td&gt;&lt;b&gt;IP 주소&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;우편번호 + 집 주소&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;서비스 구분&lt;/td&gt;
&lt;td&gt;&lt;b&gt;포트 번호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;건물 내 호수&lt;/b&gt;(한 건물에 여러 수신자)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;패킷 전달&lt;/td&gt;
&lt;td&gt;&lt;b&gt;라우팅&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;우체부 경로&lt;/b&gt;(목적지까지 거쳐가는 우체국들)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이름&amp;rarr;주소&lt;/td&gt;
&lt;td&gt;&lt;b&gt;DNS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;전화번호부&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;우체부 규칙&lt;/td&gt;
&lt;td&gt;TCP/UDP&lt;/td&gt;
&lt;td&gt;&lt;b&gt;등기(TCP, 확실)&lt;/b&gt; / &lt;b&gt;엽서(UDP, 빠름)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우편 비유는 &quot;주소-경로-이름해석&quot; 구조만 잡는다. 패킷은 여러 홉을 거치며 분할/재조립되고, 포트는 동시 다수 연결이 가능하다는 점에서 비유를 넘어선다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IP 주소와 CIDR&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IPv4&lt;/b&gt;: 32비트, 4옥텟(&lt;code&gt;192.168.1.10&lt;/code&gt;). 약 43억 개.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CIDR 표기&lt;/b&gt;: &lt;code&gt;192.168.1.10/24&lt;/code&gt; = 앞 24비트가 네트워크(&lt;code&gt;255.255.255.0&lt;/code&gt;).
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/24&lt;/code&gt; &amp;rarr; 호스트 &lt;b&gt;254개&lt;/b&gt;(네트워크&amp;middot;브로드캐스트 주소 제외, 256이 아님). &lt;code&gt;/30&lt;/code&gt; &amp;rarr; 2개(링크용). &lt;code&gt;/16&lt;/code&gt; &amp;rarr; 65,534개.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;대역&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;10.0.0.0/8&lt;/code&gt;, &lt;code&gt;172.16.0.0/12&lt;/code&gt;, &lt;code&gt;192.168.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;사설&lt;/b&gt;(인터넷 직접 불가, NAT 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;127.0.0.0/8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;루프백(loopback, &lt;code&gt;localhost&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;169.254.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;링크 로컬(DHCP 실패 시 자동)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TCP vs UDP&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;TCP&lt;/th&gt;
&lt;th&gt;UDP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;연결&lt;/td&gt;
&lt;td&gt;&lt;b&gt;연결 지향&lt;/b&gt;(3-way handshake)&lt;/td&gt;
&lt;td&gt;비연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;신뢰성&lt;/td&gt;
&lt;td&gt;&lt;b&gt;보장&lt;/b&gt;(재전송, 순서)&lt;/td&gt;
&lt;td&gt;보장 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;느림&lt;/td&gt;
&lt;td&gt;&lt;b&gt;빠름&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;용도&lt;/td&gt;
&lt;td&gt;웹(HTTP), 메일(SMTP), SSH&lt;/td&gt;
&lt;td&gt;DNS, DHCP, 스트리밍, VoIP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;포트&lt;/b&gt;는 16비트(0~65535). 잘 알려진 포트(&amp;lt;1024)는 root 권한이 필요하다. 22(SSH), 25(SMTP), 53(DNS), 80, 443(HTTPS).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TCP/IP 계층 &amp;mdash; 4개로 압축&lt;/h2&gt;
&lt;pre class=&quot;accesslog&quot;&gt;&lt;code&gt;flowchart TB
    L4[4. 응용 Application&amp;lt;br/&amp;gt;HTTP, SSH, DNS, SMTP] --&amp;gt; L3[3. 전송 Transport&amp;lt;br/&amp;gt;TCP, UDP]
    L3 --&amp;gt; L2[2. 인터넷 Internet&amp;lt;br/&amp;gt;IP, ICMP]
    L2 --&amp;gt; L1[1. 링크 Link&amp;lt;br/&amp;gt;이더넷, Wi-Fi]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 7계층을 실용적으로 4개로 압축. 각 계층은 하위 계층을 이용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라우팅과 DNS &amp;mdash; 경로와 이름 해석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷이 목적지까지 가는 경로 결정. 커널은 &lt;b&gt;라우팅 테이블&lt;/b&gt;에서 가장 길게 매칭되는(longest prefix match) 엔트리를 선택한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;게이트웨이&lt;/b&gt;(next hop): 다음 라우터.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기본 라우트(default, 0.0.0.0/0)&lt;/b&gt;: 매칭 엔트리가 없을 때 쓰는 탈출구.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DNS와 이름 해석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호스트 이름 &amp;rarr; IP 변환. 해석 순서는 &lt;code&gt;/etc/nsswitch.conf&lt;/code&gt;의 &lt;code&gt;hosts:&lt;/code&gt; 줄이 결정한다(보통 &lt;code&gt;files dns&lt;/code&gt;).&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;/etc/hosts&lt;/code&gt;: 정적 로컬 매핑(가장 먼저).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/resolv.conf&lt;/code&gt;: DNS 서버 목록 &amp;mdash; 그런데 Rocky 10에선 NetworkManager가 자동 관리.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핑은 되는데 DNS가 안 된다면? 핑은 IP 연결만 확인한다. DNS는 53번 포트로 따로 동작하니 &lt;code&gt;dig&lt;/code&gt;로 확인해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NetworkManager &amp;mdash; Rocky 10의 네트워크 데몬&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 데몬이 유선/무선/VPN/브리지/본딩을 통합 관리한다. 설정은 &lt;b&gt;연결 프로필&lt;/b&gt;(key file) 단위.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;flowchart LR
    NM[NetworkManager 데몬] --&amp;gt; CLI[nmcli / nmtui]
    NM --&amp;gt; PROF[연결 프로필&amp;lt;br/&amp;gt;/etc/NetworkManager/system-connections/]
    NM --&amp;gt; KERNEL[커널 네트워크 스택&amp;lt;br/&amp;gt;ip 명령으로 확인]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 파일: &lt;code&gt;/etc/NetworkManager/system-connections/&amp;lt;name&amp;gt;.nmconnection&lt;/code&gt;(key-file).&lt;/li&gt;
&lt;li&gt;구 &lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-*&lt;/code&gt;는 더 이상 권장 아니다(RHEL 9부터 단계적 제거).&lt;/li&gt;
&lt;li&gt;인터페이스 명명: &lt;code&gt;eth0&lt;/code&gt; 구식 &amp;rarr; &lt;b&gt;&lt;code&gt;enp0s3&lt;/code&gt;&lt;/b&gt;(버스 기반 예측 가능 이름)이 현대 기본. &lt;code&gt;en&lt;/code&gt;=이더넷, &lt;code&gt;p0s3&lt;/code&gt;=버스 위치.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 인터페이스&amp;middot;주소 (ip - 저수준)
ip addr show              # 모든 인터페이스 + 주소 (ip a)
ip -4 addr show           # IPv4만
ip link show              # 링크 상태(up/down)
ip route show             # 라우팅 테이블 (ip r)
ip route show default     # 기본 게이트웨이
ip neigh show             # ARP/이웃 테이블&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2: enp0s3: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 ...
    link/ether 08:00:27:ab:cd:ef brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.50/24 brd 192.168.1.255 scope global enp0s3&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;default via 192.168.1.1 dev enp0s3 proto static metric 100
192.168.1.0/24 dev enp0s3 proto kernel scope link src 192.168.1.50&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# NetworkManager (nmcli) &amp;mdash; 권장 설정
nmcli general status               # NM 상태
nmcli device status                # 장치 목록
nmcli connection show              # 모든 연결 프로필

# 정적 IP 설정
sudo nmcli connection modify &quot;enp0s3&quot; \
    ipv4.method manual \
    ipv4.addresses 192.168.1.50/24 \
    ipv4.gateway 192.168.1.1 \
    ipv4.dns &quot;8.8.8.8 1.1.1.1&quot;
sudo nmcli connection up &quot;enp0s3&quot;  # 활성화

# DHCP 복귀
sudo nmcli connection modify &quot;enp0s3&quot; ipv4.method auto&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;# 소켓/포트 (ss - netstat 대체)
ss -tlnp                # TCP 리스닝 + 프로세스
ss -ulnp                # UDP 리스닝
ss -tunap               # TCP+UDP 모든 소켓 + 프로세스
ss -t state established # established만
ss -s                   # 요약&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;State   Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN  0      128    0.0.0.0:22          0.0.0.0:*         users:((&quot;sshd&quot;,pid=1234,fd=3))
LISTEN  0      511    *:80                *:*               users:((&quot;nginx&quot;,pid=5678,fd=6))&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;netstat&lt;/code&gt;는 &lt;b&gt;deprecated&lt;/b&gt;. &lt;code&gt;ss&lt;/code&gt;를 써라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 진단 도구
ping -c 4 8.8.8.8                   # 연결성/지연
ping -c 4 example.com               # DNS+연결 동시 확인
tracepath example.com               # 경로 추적(traceroute 대체)
dig example.com                     # DNS 상세
dig +short example.com              # IP만
dig @8.8.8.8 example.com            # 특정 DNS 서버
getent hosts example.com            # nsswitch 전체 조회
curl -I https://example.com         # HTTP 헤더만
curl -v https://example.com         # 상세&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 로컬 이름 해석
cat /etc/hosts                      # 정적 매핑
cat /etc/nsswitch.conf | grep hosts # 해석 순서
cat /etc/resolv.conf                # DNS 서버(NM 관리, 직접 편집 금지)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;/etc/resolv.conf&lt;/code&gt; 직접 고치면 된다&quot;&lt;/td&gt;
&lt;td&gt;Rocky 10에선 NM이 덮어씀. &lt;b&gt;nmcli로&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;netstat&lt;/code&gt;를 쓴다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;deprecated&lt;/b&gt;. &lt;code&gt;ss&lt;/code&gt; 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;인터페이스는 &lt;code&gt;eth0&lt;/code&gt;&quot;&lt;/td&gt;
&lt;td&gt;현대는 &lt;code&gt;enp0s3&lt;/code&gt;, &lt;code&gt;ens33&lt;/code&gt;(예측 가능 이름). e=이더넷, w=무선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;서비스가 &lt;code&gt;127.0.0.1&lt;/code&gt;에만 묶어도 외부 접속된다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아니다&lt;/b&gt;. &lt;code&gt;0.0.0.0&lt;/code&gt;(모든 IF)이나 특정 IP로 바인딩해야&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;/24&lt;/code&gt;면 호스트 256개&quot;&lt;/td&gt;
&lt;td&gt;254개(네트워크&amp;middot;브로드캐스트 주소 제외)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;핑 되면 DNS도 된다&quot;&lt;/td&gt;
&lt;td&gt;핑은 IP 연결만. DNS는 53번 따로. &lt;code&gt;dig&lt;/code&gt;로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;DNS는 캐시된다&quot;&lt;/td&gt;
&lt;td&gt;리눅스는 보통 시스템 DNS 캐시가 &lt;b&gt;없음&lt;/b&gt;(nscd/systemd-resolved 안 쓰면). 매번 쿼리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;CIDR &lt;code&gt;/30&lt;/code&gt;은 쓸모 없다&quot;&lt;/td&gt;
&lt;td&gt;링크용 2개 호스트(라우터 간). 실제로 자주 씀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IPv6&lt;/b&gt;: IPv4 고갈 대안. 128비트(&lt;code&gt;2001:db8::1&lt;/code&gt;). /64가 표준 서브넷. SLAAC(자동 설정) 또는 DHCPv6. Rocky 10은 기본 듀얼스택(IPv4+IPv6).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;본딩&amp;middot;브리지&lt;/b&gt;: 본딩은 여러 NIC를 하나의 논리 인터페이스로(이중화/대역폭, mode 1 active-backup&amp;middot;mode 4 LACP). 브리지는 L2 스위치 역할(가상화/컨테이너가 호스트 LAN에 직접 참여).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방화벽과 nftables&lt;/b&gt;: Rocky 10의 firewalld 백엔드는 nftables. 패킷 필터링 규칙은 nmcli가 아니라 &lt;code&gt;firewall-cmd&lt;/code&gt;로(10장 참조).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IP/CIDR&lt;/b&gt;: &lt;code&gt;/24&lt;/code&gt; = 255.255.255.0 = 호스트 &lt;b&gt;254개&lt;/b&gt;(256 아님). 사설대역(10/172.16/192.168)은 NAT 없이 인터넷 불가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP&lt;/b&gt;(신뢰, 연결, 3-way handshake) vs &lt;b&gt;UDP&lt;/b&gt;(빠름, 비연결).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;포트&lt;/b&gt;: 잘 알려진 &amp;lt;1024(root 필요). 22/25/53/80/443.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라우팅&lt;/b&gt;: longest prefix match. 기본 라우트(0.0.0.0/0)가 탈출구.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNS 해석 순서&lt;/b&gt;: &lt;code&gt;/etc/hosts&lt;/code&gt; &amp;rarr; DNS(&lt;code&gt;/etc/nsswitch.conf&lt;/code&gt; &lt;code&gt;hosts:&lt;/code&gt; 줄).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NetworkManager(nmcli)&lt;/b&gt;: Rocky 10 중심. &lt;code&gt;/etc/resolv.conf&lt;/code&gt;를 자동 관리하니 직접 편집 금지. 인터페이스 이름은 &lt;code&gt;enp0s3&lt;/code&gt;(예측 가능), 구 &lt;code&gt;eth0&lt;/code&gt; 아님.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;ss&lt;/code&gt; &amp;gt; &lt;code&gt;netstat&lt;/code&gt;&lt;/b&gt;(deprecated). &lt;b&gt;바인딩&lt;/b&gt;: &lt;code&gt;127.0.0.1&lt;/code&gt;(로컬만) vs &lt;code&gt;0.0.0.0&lt;/code&gt;(모든 IF, 외부 접속 가능).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;IP 주소&amp;middot;포트&amp;middot;라우팅&amp;middot;DNS를 우편 시스템에 비유해 설명하라.&lt;/li&gt;
&lt;li&gt;TCP와 UDP의 차이. 어떤 서비스가 각각 어울리는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/24&lt;/code&gt; 서브넷에서 사용 가능한 호스트 수는? 왜 256이 아닌가?&lt;/li&gt;
&lt;li&gt;사설 IP 대역 3가지를 말하고, 왜 인터넷에 직접 못 나가는가?&lt;/li&gt;
&lt;li&gt;Rocky 10에서 DNS 서버를 바꾸려면 어떻게 하는가? 왜 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 직접 편집이 위험한가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ss -tlnp&lt;/code&gt; 출력에서 &lt;code&gt;127.0.0.1:80&lt;/code&gt;과 &lt;code&gt;0.0.0.0:80&lt;/code&gt;의 차이는?&lt;/li&gt;
&lt;li&gt;핑은 되는데 DNS가 안 될 때 의심할 곳은?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enp0s3&lt;/code&gt;라는 인터페이스 이름의 의미는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/configuring_and_managing_networking/configuring-an-ethernet-connection&quot;&gt;Configuring an Ethernet connection &amp;mdash; RHEL 10&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rockylinux.org/10/gemstones/network/network_manager/&quot;&gt;NetworkManager &amp;mdash; Rocky Linux 10&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man nmcli&lt;/code&gt;, &lt;code&gt;man ip(8)&lt;/code&gt;, &lt;code&gt;man ss(8)&lt;/code&gt;, &lt;code&gt;man dig&lt;/code&gt;, &lt;code&gt;man resolv.conf(5)&lt;/code&gt;, &lt;code&gt;man nsswitch.conf(5)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>dns</category>
      <category>Linux</category>
      <category>network</category>
      <category>NetworkManager</category>
      <category>TCP-IP</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/9</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-network#entry9comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:33:59 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 08. package</title>
      <link>https://technical-architect.tistory.com/entry/Linux-package</link>
      <description>&lt;h1&gt;&lt;code&gt;rpm -i&lt;/code&gt;로 설치하면 왜 의존성 에러가 나는가 &amp;mdash; RPM과 dnf의 역할 분담&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx를 설치하려고 &lt;code&gt;rpm -i nginx.rpm&lt;/code&gt;을 쳤더니 의존성 에러로 실패한다. 왜? &lt;b&gt;RPM은 의존성을 풀어주지 않는다.&lt;/b&gt; 단순 설치만 한다. 의존성 해결은 dnf의 일이다. 그래서 로컬 &lt;code&gt;.rpm&lt;/code&gt; 파일도 &lt;code&gt;dnf install ./pkg.rpm&lt;/code&gt;으로 설치해야 dnf가 의존성까지 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Rocky 10이 패키지를 어떻게 관리하는지 다룬다 &amp;mdash; RPM(포장 형식 + 저수준 도구)과 dnf(앱스토어 관리자)의 역할 분담, 그리고 Rocky 10의 두 가지 큰 변화: &lt;b&gt;dnf5 전환&lt;/b&gt;과 &lt;b&gt;modularity 폐지&lt;/b&gt;. 이 변화를 모르면 구형 자료의 명령이 Rocky 10에서 동작하지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RPM과 dnf는 역할이 다르다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;역할&lt;/th&gt;
&lt;th&gt;비유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;RPM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;단일 &lt;code&gt;.rpm&lt;/code&gt; 패키지 파일 + 저수준 설치 도구&lt;/td&gt;
&lt;td&gt;&lt;b&gt;포장된 상자&lt;/b&gt;(내용물 + 설명서 + 조립법)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;dnf5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;저장소 관리 + 의존성 자동 해결 + 설치/업데이트&lt;/td&gt;
&lt;td&gt;&lt;b&gt;앱스토어 관리자&lt;/b&gt;(검색&amp;middot;배달&amp;middot;세트물 해결)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPM은 패키지 &lt;b&gt;형식&lt;/b&gt;(&lt;code&gt;.rpm&lt;/code&gt;)이자 저수준 도구다. dnf는 그 위에서 저장소 관리와 의존성 자동 해결을 한다. 의존성 지옥(openssl, pcre... 각각 컴파일하던 시절)에서 벗어나게 한 것이 dnf의 핵심 가치다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NEVRA &amp;mdash; 패키지 식별자&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;nginx-1.26.3-1.el10.x86_64.rpm
  │     │     │  │     │
  │     │     │  │     └ 아키텍처(x86_64, aarch64, noarch)
  │     │     │  └ 배포 태그(el10 = Enterprise Linux 10)
  │     │     └ 릴리스(같은 버전의 빌드 회차)
  │     └ 버전(업스트림)
  └ 이름&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;noarch&lt;/code&gt;는 아키텍처 무관(스크립트/문서). RPM 파일 내부는 헤더(메타데이터) + 페이로드(cpio 압축) + 서명(GPG)으로 이루어진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 스크립트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지는 설치/제거 시점에 스크립트를 실행한다:&lt;br /&gt;| 스크립트 | 실행 시점 |&lt;br /&gt;|----------|-----------|&lt;br /&gt;| &lt;code&gt;%pre&lt;/code&gt; / &lt;code&gt;%post&lt;/code&gt; | 설치 &lt;b&gt;전&lt;/b&gt; / &lt;b&gt;후&lt;/b&gt; |&lt;br /&gt;| &lt;code&gt;%preun&lt;/code&gt; / &lt;code&gt;%postun&lt;/code&gt; | 제거 &lt;b&gt;전&lt;/b&gt; / &lt;b&gt;후&lt;/b&gt; |&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10의 큰 변화 &amp;mdash; dnf5와 modularity 폐지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky 10은 dnf4(구)에서 &lt;b&gt;dnf5&lt;/b&gt;로 전환했다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;flowchart LR
    D[dnf5 명령] --&amp;gt; LIB[libdnf5&amp;lt;br/&amp;gt;C++ 라이브러리]
    LIB --&amp;gt; REPO[저장소 메타데이터]
    LIB --&amp;gt; RPM[RPM DB&amp;lt;br/&amp;gt;/var/lib/rpm]&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;dnf4(구)&lt;/th&gt;
&lt;th&gt;&lt;b&gt;dnf5(Rocky 10)&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구현&lt;/td&gt;
&lt;td&gt;Python + libdnf(C++)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;libdnf5(전체 C++ 재작성)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 의존&lt;/td&gt;
&lt;td&gt;필수&lt;/td&gt;
&lt;td&gt;&lt;b&gt;제거&lt;/b&gt;(컨테이너에 유리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;속도&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;&lt;b&gt;더 빠름&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;modularity(&lt;code&gt;dnf module&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;지원&lt;/td&gt;
&lt;td&gt;&lt;b&gt;폐지&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;filelists metadata&lt;/td&gt;
&lt;td&gt;기본 다운로드&lt;/td&gt;
&lt;td&gt;&lt;b&gt;기본 생략&lt;/b&gt;(필요 시 자동/수동)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;사용자 캐시&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.cache/dnf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.cache/libdnf5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;modularity 폐지 &amp;mdash; 구형 자료가 동작하지 않는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky 8/9에선 특정 버전을 설치하려고 &lt;code&gt;dnf module enable nginx:1.14 &amp;amp;&amp;amp; dnf module install nginx:1.14&lt;/code&gt;를 썼다. &lt;b&gt;Rocky 10에선 이 명령이 안 통한다.&lt;/b&gt; modularity가 폐지됐기 때문이다. 대신 버전을 직접 지정한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;작업&lt;/th&gt;
&lt;th&gt;구 방식(Rocky 8/9)&lt;/th&gt;
&lt;th&gt;&lt;b&gt;새 방식(Rocky 10)&lt;/b&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;특정 버전 설치&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf module enable nginx:1.14 &amp;amp;&amp;amp; dnf module install nginx:1.14&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf install nginx-1.26.3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버전 목록&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf module list nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf repoquery nginx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구버전(Rocky 8/9)에서는 구 방식이 정상 동작한다. 버전에 따라 명령이 다르니, 자료의 Rocky 버전을 반드시 확인하라. &lt;code&gt;nginx-1.26.3&lt;/code&gt;(하이픈)과 &lt;code&gt;nginx:1.26&lt;/code&gt;(콜론, module 방식)은 다르다 &amp;mdash; Rocky 10은 하이픈 방식만 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dnf4와 dnf5는 같은 RPM DB(&lt;code&gt;/var/lib/rpm&lt;/code&gt;)를 공유하지만, &lt;b&gt;트랜잭션 히스토리 DB는 다르다&lt;/b&gt;. dnf5로 깐 패키지를 dnf4로 보면 &quot;user installed&quot;로 보이고 그 반대도 마찬가지. Rocky 10은 dnf5가 기본이라 혼용할 일은 거의 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;저장소와 GPG 검증 &amp;mdash; 변조를 막는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지와 메타데이터를 제공하는 원격/로컬 소스가 &lt;b&gt;저장소(repository)&lt;/b&gt;다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정: &lt;code&gt;/etc/yum.repos.d/*.repo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;기본 저장소(Rocky 10): BaseOS, AppStream, Extras.&lt;/li&gt;
&lt;li&gt;메타데이터: &lt;code&gt;repodata/&lt;/code&gt;(primary, filelists, updateinfo).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.repo&lt;/code&gt; 파일 예시:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[example-base]
name=Example Base
baseurl=https://example.com/linux/10/BaseOS/$basearch/os/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-example&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소/패키지의 &lt;b&gt;GPG 서명&lt;/b&gt;으로 변조 여부를 확인한다. &lt;code&gt;gpgcheck=1&lt;/code&gt;이면 서명 안 된 패키지 설치를 거부한다. 저장소 추가 시 공개 키를 먼저 등록: &lt;code&gt;rpm --import &amp;lt;keyfile&amp;gt;&lt;/code&gt;. gpgcheck를 끄면 편하지만 위험하다 &amp;mdash; 변조된 패키지가 설치될 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;dnf --version    # dnf5 버전 출력&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# 패키지 조회
dnf list installed              # 설치된 패키지
dnf info nginx                  # 패키지 상세
dnf search &quot;web server&quot;         # 이름/요약 검색
dnf provides /usr/bin/curl      # 파일이 어느 패키지인지
dnf repoquery nginx             # 저장소의 모든 버전(modularity 대체)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;sudo dnf install -y nginx             # -y 자동 yes
sudo dnf install -y nginx-1.26.3      # 특정 버전(새 방식)
sudo dnf remove nginx
sudo dnf update                       # 전체 업데이트(= upgrade)
sudo dnf check-update                 # 업데이트 가능 목록만
sudo dnf downgrade nginx&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;# 저장소 관리
dnf repolist                         # 활성 저장소
dnf repolist all                     # 전체
sudo dnf config-manager --add-repo https://example.com/repo.repo
sudo dnf config-manager --disable example-base
sudo dnf clean all                   # 캐시 삭제&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# 히스토리 &amp;mdash; 롤백
dnf history list                     # 트랜잭션 목록
dnf history info 5                   # 5번 상세
sudo dnf history undo 5              # 5번 취소(롤백)
sudo dnf history rollback 3          # 3번 이후 모두 취소&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dnf5는 서브명령이 필수: &lt;code&gt;dnf history&lt;/code&gt;만 치면 안 되고 &lt;code&gt;dnf history list&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# 저수준 rpm (dnf로 안 될 때)
rpm -qa | grep nginx                 # 설치된 패키지 검색
rpm -qi nginx                        # 패키지 정보
rpm -ql nginx                        # 설치한 파일 목록
rpm -qf /usr/bin/curl                # 파일 소속 패키지
rpm -Va nginx                        # 검증(변조/무결성)
sudo rpm -ivh pkg.rpm                # 설치(로컬, 의존성 안 풀림)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;dnf group list
sudo dnf group install &quot;Server with GUI&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;rpm -i&lt;/code&gt;로 설치하면 된다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의존성 안 풀림&lt;/b&gt;. &lt;code&gt;dnf install ./pkg.rpm&lt;/code&gt; 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Rocky 10에선 &lt;code&gt;dnf module&lt;/code&gt;이 된다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;안 됨&lt;/b&gt;(폐지). &lt;code&gt;dnf install pkg-ver&lt;/code&gt;로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;filelists는 항상 받아진다&quot;&lt;/td&gt;
&lt;td&gt;Rocky 10은 &lt;b&gt;기본 생략&lt;/b&gt;. 필요 시 자동/&lt;code&gt;optional_metadata_types=filelists&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;dnf update&lt;/code&gt;와 &lt;code&gt;dnf upgrade&lt;/code&gt;는 다르다&quot;&lt;/td&gt;
&lt;td&gt;현대 dnf에선 &lt;b&gt;동일&lt;/b&gt;(upgrade가 alias)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;-y&lt;/code&gt;는 항상 좋다&quot;&lt;/td&gt;
&lt;td&gt;운영에선 의존성 변경 &lt;b&gt;먼저 확인&lt;/b&gt; 권장. 자동 스크립트만 &lt;code&gt;-y&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;EPEL은 기본 저장소다&quot;&lt;/td&gt;
&lt;td&gt;아니다. &lt;b&gt;서드파티&lt;/b&gt;. 필요 시 별도 설치(&lt;code&gt;dnf install epel-release&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;gpgcheck 끄면 편하다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;위험&lt;/b&gt;. 변조 패키지 설치 가능. 공식 외엔 더 엄격히&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;dnf5 히스토리에 과거가 다 보인다&quot;&lt;/td&gt;
&lt;td&gt;dnf4&amp;rarr;5 전환 시 과거 히스토리 &lt;b&gt;마이그레이션 안 됨&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EPEL(Extra Packages for Enterprise Linux)&lt;/b&gt;: Fedora 커뮤니티가 RHEL/Rocky용으로 제공하는 추가 패키지 저장소. RHEL 기본에 없는 도구(htop, nginx 최신 등) 설치에 유용. &lt;code&gt;dnf install epel-release&lt;/code&gt;로 활성화. 단, 공식 지원 아님(운영 환경 주의).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dnf5 명령 변경&lt;/b&gt;: &lt;code&gt;dnf shell&lt;/code&gt; &amp;rarr; &lt;code&gt;dnf5 do&lt;/code&gt;, &lt;code&gt;dnf updateinfo&lt;/code&gt; &amp;rarr; &lt;code&gt;dnf5 advisory summary&lt;/code&gt;, &lt;code&gt;dnf mark install&lt;/code&gt; &amp;rarr; &lt;code&gt;dnf5 mark user&lt;/code&gt; 등 서브명령이 바뀌었다. 자동화 스크립트 작성 시 dnf5 문서 확인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RPM 직접 빌드&lt;/b&gt;: &lt;code&gt;rpmbuild&lt;/code&gt; + &lt;code&gt;.spec&lt;/code&gt; 파일로 커스텀 패키지를 생성. 소스 + 패치 + 설치 스크립트 + 메타데이터를 묶어 배포. 고급 주제.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RPM&lt;/b&gt; = 패키지 형식(&lt;code&gt;.rpm&lt;/code&gt;) + 저수준 도구. &lt;b&gt;의존성을 안 푼다&lt;/b&gt; &amp;mdash; 그래서 &lt;code&gt;rpm -i&lt;/code&gt;는 실패하고 &lt;code&gt;dnf install ./pkg.rpm&lt;/code&gt;을 써야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dnf5(Rocky 10)&lt;/b&gt; = libdnf5(C++ 재작성), Python 의존 제거, 더 빠름.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NEVRA&lt;/b&gt;: Name-Version-Release.Architecture.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;modularity 폐지&lt;/b&gt;: &lt;code&gt;dnf module&lt;/code&gt;이 안 통한다. &lt;b&gt;&lt;code&gt;dnf install pkg-ver&lt;/code&gt;&lt;/b&gt;로 버전 지정(하이픈). 구버전(8/9)은 콜론 module 방식.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;filelists 기본 생략&lt;/b&gt;(필요 시 자동/&lt;code&gt;optional_metadata_types=filelists&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장소&lt;/b&gt;: &lt;code&gt;/etc/yum.repos.d/*.repo&lt;/code&gt;. GPG 서명(&lt;code&gt;gpgcheck=1&lt;/code&gt;)으로 변조 방지 &amp;mdash; 끄면 위험.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;rollback&lt;/b&gt;: &lt;code&gt;dnf history undo N&lt;/code&gt; / &lt;code&gt;dnf history rollback N&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dnf update = upgrade&lt;/b&gt;(동일).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;RPM과 dnf의 역할 분담을 설명하라. 왜 &lt;code&gt;rpm -i&lt;/code&gt;로 설치하면 안 되는가?&lt;/li&gt;
&lt;li&gt;NEVRA의 각 자리가 의미하는 바를 풀어 설명하라.&lt;/li&gt;
&lt;li&gt;dnf5가 dnf4보다 나은 점 3가지는?&lt;/li&gt;
&lt;li&gt;Rocky 10에서 특정 버전 패키지를 설치하는 명령은? 구버전(8/9) 방식과 비교하라.&lt;/li&gt;
&lt;li&gt;filelists metadata가 기본 생략된 이유와, 필요할 때 어떻게 하는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/bin/curl&lt;/code&gt;이 어느 패키지 소속인지 확인하는 두 명령은?&lt;/li&gt;
&lt;li&gt;설치를 롤백하려면 어떤 명령을 쓰는가? gpgcheck를 끄면 왜 위험한가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rockylinux.org/10/release_notes/10_0/&quot;&gt;Rocky Linux 10 Release Notes &amp;mdash; DNF/Modularity&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/considerations_in_adopting_rhel_10/software-management&quot;&gt;RHEL 10 Considerations &amp;mdash; Software management&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dnf5.readthedocs.io/en/stable/changes_from_dnf4.7.html&quot;&gt;Changes between DNF and DNF5&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dnf5.readthedocs.io/en/latest/migrating_to_dnf5.7.html&quot;&gt;Migrating from DNF to DNF5&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man dnf5&lt;/code&gt;, &lt;code&gt;man rpm&lt;/code&gt;, &lt;code&gt;man dnf5.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>dnf5</category>
      <category>Linux</category>
      <category>modularity</category>
      <category>package</category>
      <category>RPM</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/8</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-package#entry8comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:30:34 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 07. shell script</title>
      <link>https://technical-architect.tistory.com/entry/Linux-shell-script</link>
      <description>&lt;h1&gt;&lt;code&gt;ls *.txt&lt;/code&gt;에서 별을 처리하는 건 ls가 아니다 &amp;mdash; 셸과 확장&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;ls *.txt&lt;/code&gt;를 친다. &lt;code&gt;ls&lt;/code&gt;가 &lt;code&gt;*.txt&lt;/code&gt;를 처리할까? &lt;b&gt;아니다.&lt;/b&gt; 셸이 먼저 &lt;code&gt;*.txt&lt;/code&gt;를 &lt;code&gt;a.txt b.txt c.txt&lt;/code&gt;로 펼쳐서(확장) &lt;code&gt;ls&lt;/code&gt;에 넘긴다. &lt;code&gt;ls&lt;/code&gt;는 이미 펼쳐진 파일 이름만 받는다. 이 사실을 모르면 셸 스크립트의 이상한 동작 원인을 평생 못 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸은 단순한 명령 실행기가 아니다. &lt;b&gt;명령을 해석하고, 변수&amp;middot;glob&amp;middot;산술을 치환(확장)하며, 파이프&amp;middot;리다이렉션&amp;middot;제어문까지 처리하는 인터프리터&lt;/b&gt;다. cron, systemd unit, 배포 스크립트, 설정 자동화 &amp;mdash; 리눅스 관리의 상당수가 셸 기반이다. 이 글은 셸이 명령을 어떻게 해석하는지, 그리고 그 해석을 스크립트로 어떻게 묶는지를 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셸은 명령 번역사다&lt;/h2&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;flowchart LR
    U[사용자/스크립트] --&amp;gt; SH[셸 - bash]
    SH --&amp;gt; K[커널 시스템 콜]
    K --&amp;gt; HW[하드웨어]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자(또는 스크립트)가 명령을 치면 &amp;rarr; 셸이 &lt;b&gt;해석&lt;/b&gt; &amp;rarr; 커널 시스템 콜 &amp;rarr; 실행.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;bash&lt;/b&gt;(Bourne Again Shell): GNU 프로젝트, 대부분 리눅스의 기본 로그인 셸. Bourne shell(&lt;code&gt;sh&lt;/code&gt;) 호환 + 확장.&lt;/li&gt;
&lt;li&gt;Rocky 10의 &lt;code&gt;/bin/sh&lt;/code&gt;는 &lt;b&gt;bash&lt;/b&gt;를 가리킨다(RHEL 전통). 단, &lt;code&gt;sh&lt;/code&gt;로 호출하면 POSIX 제한 모드로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셸이 명령을 실행하기 전 거치는 확장 &amp;mdash; 따옴표가 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셸은 명령을 실행하기 전 여러 &lt;b&gt;확장(expansion)&lt;/b&gt;을 거친다. 이 순서를 모르면 이상한 동작의 원인을 못 찾는다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;브레이스 확장&lt;/b&gt;: &lt;code&gt;a{b,c}&lt;/code&gt; &amp;rarr; &lt;code&gt;ab ac&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;틸드 확장&lt;/b&gt;: &lt;code&gt;~&lt;/code&gt; &amp;rarr; 홈 디렉토리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변수/명령/산술 확장&lt;/b&gt;: &lt;code&gt;$VAR&lt;/code&gt;, &lt;code&gt;$(cmd)&lt;/code&gt;, &lt;code&gt;$((1+2))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;워드 분리&lt;/b&gt;(IFS 기준)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로명 확장(glob)&lt;/b&gt;: &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;[abc]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리다이렉션/파이프&lt;/b&gt; 적용&lt;/li&gt;
&lt;li&gt;최종 명령 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 이름에 공백이 있을 때 재앙이 일어나는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자가 자주 겪는 재앙: &lt;code&gt;rm $file&lt;/code&gt;을 쳤는데 파일 이름에 공백이 있었다. 셸이 워드 분리를 해버려 &lt;code&gt;my file.txt&lt;/code&gt;가 &lt;code&gt;my&lt;/code&gt;와 &lt;code&gt;file.txt&lt;/code&gt; 두 인자로 쪼개지고, 각각을 지우려다 사고가 난다. &lt;b&gt;항상 &lt;code&gt;&quot;$file&quot;&lt;/code&gt;(큰따옴표)로 감싸야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&quot; &quot;&lt;/code&gt;(큰따옴표): 변수 확장 O, glob X.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;' '&lt;/code&gt;(작은따옴표): &lt;b&gt;모든 확장 X&lt;/b&gt;(문자 그대로).&lt;/li&gt;
&lt;li&gt;따옴표 없음: glob&amp;middot;워드 분리 다 됨(위험).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변수 &amp;mdash; 환경변수만 자식에게 간다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;종류&lt;/th&gt;
&lt;th&gt;범위&lt;/th&gt;
&lt;th&gt;선언&lt;/th&gt;
&lt;th&gt;예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;셸 변수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;현재 셸만&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VAR=value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NAME=rocky&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;환경변수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자식 프로세스에 상속&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export VAR=value&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export PATH=...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 프로세스는 &lt;b&gt;환경변수만&lt;/b&gt; 물려받는다. 스크립트 안에서 설정한 셸 변수는 호출한 셸에 영향을 주지 않는다. 부모 셸에 적용하려면 &lt;code&gt;source&lt;/code&gt;로 실행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 환경변수:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;변수&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;명령 검색 디렉토리 목록(&lt;code&gt;:&lt;/code&gt; 구분)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 사용자 홈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;USER&lt;/code&gt; / &lt;code&gt;LOGNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;사용자명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SHELL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로그인 셸 경로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PWD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 작업 디렉토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LANG&lt;/code&gt; / &lt;code&gt;LC_*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;로케일&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PATH와 명령 검색&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령을 치면 셸은 &lt;code&gt;PATH&lt;/code&gt;의 각 디렉토리를 순서대로 찾는다. 없으면 &quot;command not found&quot;. 현재 디렉토리(&lt;code&gt;.&lt;/code&gt;)는 보통 &lt;b&gt;PATH에 없다&lt;/b&gt;(보안상 &amp;mdash; 공격자가 현재 디렉토리에 가짜 &lt;code&gt;ls&lt;/code&gt;를 두면 실행될 수 있으므로). 그래서 &lt;code&gt;./script.sh&lt;/code&gt;로 명시한다. 명령 위치는 &lt;code&gt;which cmd&lt;/code&gt;나 &lt;code&gt;type cmd&lt;/code&gt;로 확인. 한 번 찾은 경로는 &lt;code&gt;hash&lt;/code&gt; 캐시에 기억되니, 새 위치로 옮긴 명령이 안 될 땐 &lt;code&gt;hash -r&lt;/code&gt;.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;입출력 리다이렉션&amp;middot;파이프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 디스크립터: 0=stdin, 1=stdout, 2=stderr.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표현&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd &amp;gt; file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표준출력을 file로(덮어쓰기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd &amp;gt;&amp;gt; file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표준출력을 file에 &lt;b&gt;추가&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd &amp;lt; file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;file에서 표준입력 읽기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd 2&amp;gt; file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표준에러를 file로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표준에러를 표준출력으로 합침&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd &amp;amp;&amp;gt; file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표준출력+에러 모두 file로(bash)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd &amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모두 폐기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파이프&lt;/b&gt; &lt;code&gt;cmd1 | cmd2&lt;/code&gt;: cmd1의 표준출력을 cmd2의 표준입력으로 연결. &lt;code&gt;ls | grep txt | wc -l&lt;/code&gt;. &lt;code&gt;tee&lt;/code&gt;는 화면과 파일에 동시 출력(&lt;code&gt;cmd | tee out.log&lt;/code&gt;).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타문자&amp;middot;연산자&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기호&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt; / &lt;code&gt;?&lt;/code&gt; / &lt;code&gt;[abc]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;glob(0개 이상 / 1개 / 문자집합)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{a,b,c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;브레이스 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;홈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;명령 순차 실행(앞 결과 무관)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앞 명령 &lt;b&gt;성공 시&lt;/b&gt; 다음(단락 평가)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;||&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;앞 명령 &lt;b&gt;실패 시&lt;/b&gt; 다음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$(cmd)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;명령 치환(&lt;code&gt;`cmd`&lt;/code&gt; 대신 권장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$((expr))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;산술 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;주석&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스크립트 문법 &amp;mdash; 제어 흐름 묶기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shebang과 실행&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
# 더 이식성 높게: #!/usr/bin/env bash&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 권한을 주고 &lt;code&gt;./script.sh&lt;/code&gt;, 또는 &lt;code&gt;bash script.sh&lt;/code&gt;(실행 권한 불필요). shebang이 없으면 현재 셸로 해석되어 동작이 달라질 수 있으니 항상 명시하라.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;변수&amp;middot;인자&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
greeting=&quot;Hello&quot;          # 할당 (= 양옆 공백 없음!)
name=$1                    # 첫 번째 인자
count=$#                   # 인자 개수
all=&quot;$@&quot;                   # 모든 인자(따옴표 시 각각 보존)
exit_code=$?               # 직전 명령 종료 코드 (0=성공)
echo &quot;$greeting, $name&quot;    # 큰따옴표 권장&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;조건문 &amp;mdash; &lt;code&gt;[ ]&lt;/code&gt; vs &lt;code&gt;[[ ]]&lt;/code&gt;&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;if grep -q &quot;root&quot; /etc/passwd; then
    echo &quot;root 존재&quot;
elif [ ... ]; then
    echo &quot;다른 경우&quot;
else
    echo &quot;나머지&quot;
fi

[[ $a == *.txt ]]           # 패턴 매칭([[ ]]만)
[ -f /etc/hosts ]           # 파일 존재
[ -z &quot;$var&quot; ]               # 빈 문자열
[ $num -gt 5 ]              # 정수 비교(-eq -ne -lt -le -gt -ge)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 bash에선 &lt;b&gt;&lt;code&gt;[[ ]]&lt;/code&gt;를 권장&lt;/b&gt;한다 &amp;mdash; 단어 분리가 안 되고, 패턴/정규식을 지원하고, 더 안전하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반복문&amp;middot;case&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;for f in *.txt; do echo &quot;$f&quot;; done
for ((i=0; i&amp;lt;5; i++)); do echo &quot;$i&quot;; done
while [ &quot;$count&quot; -lt 10 ]; do count=$((count+1)); done
while IFS= read -r line; do echo &quot;$line&quot;; done &amp;lt; input.txt

case &quot;$1&quot; in
    start)   systemctl start nginx ;;
    stop)    systemctl stop nginx ;;
    *)       echo &quot;usage: $0 {start|stop}&quot;; exit 1 ;;
esac&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수&lt;/h3&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;greet() {
    local name=&quot;$1&quot;          # local: 함수 내 한정
    echo &quot;Hi, $name&quot;
    return 0                 # 종료 코드만(0~255)
}
msg=$(greet &quot;world&quot;)         # 출력을 변수로 회수&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;return&lt;/code&gt;은 종료 코드만 반환한다. 데이터를 돌려받으려면 &lt;code&gt;echo&lt;/code&gt; + 명령 치환(&lt;code&gt;$(...)&lt;/code&gt;).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안전 모드 &amp;mdash; 모든 스크립트에 권장&lt;/h3&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;set -e             # 오류 시 즉시 종료
set -u             # 미정의 변수 사용 시 오류
set -o pipefail    # 파이프 어느 단계 실패 시 전체 실패
# 한 줄: set -Eeuo pipefail&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;code&gt;set -e&lt;/code&gt;는 &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;while&lt;/code&gt; 조건문 안의 실패나 파이프 마지막 단계만 검사한다. 파이프 전체를 잡으려면 &lt;code&gt;set -o pipefail&lt;/code&gt;로 보완.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). 표준 bash라 Ubuntu/WSL에서도 대부분 동일.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;echo &quot;$SHELL&quot;             # /bin/bash
echo &quot;$BASH_VERSION&quot;      # 5.x
ps -p $$ -o comm=         # 현재 셸
cat /etc/shells           # 사용 가능 셸 목록&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;env | sort | head
echo &quot;$PATH&quot;
which systemctl
export PATH=&quot;$PATH:/opt/myapp/bin&quot;   # 현재 세션 임시 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실용 스크립트 예(백업):&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
set -Eeuo pipefail

src=&quot;${1:?usage: $0 &amp;lt;src&amp;gt; &amp;lt;dest&amp;gt;}&quot;
dest=&quot;${2:?usage: $0 &amp;lt;src&amp;gt; &amp;lt;dest&amp;gt;}&quot;
ts=$(date +%Y%m%d-%H%M%S)

if [[ ! -d &quot;$src&quot; ]]; then
    echo &quot;error: $src not a directory&quot; &amp;gt;&amp;amp;2
    exit 1
fi

tar -czf &quot;${dest}/backup-${ts}.tar.gz&quot; -C &quot;$src&quot; .
echo &quot;backup done: ${dest}/backup-${ts}.tar.gz&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# ~/.bashrc 에 추가해 별칭&amp;middot;함수 영구화
alias ll='ls -lh'
mkcd() { mkdir -p &quot;$1&quot; &amp;amp;&amp;amp; cd &quot;$1&quot;; }
source ~/.bashrc            # 적용&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;VAR = value&lt;/code&gt;&quot;&lt;/td&gt;
&lt;td&gt;오류. &lt;b&gt;&lt;code&gt;VAR=value&lt;/code&gt;&lt;/b&gt;(공백 없음)만 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;rm $file&lt;/code&gt; 안전하다&quot;&lt;/td&gt;
&lt;td&gt;파일명에 공백 있으면 재앙. &lt;b&gt;항상 &lt;code&gt;&quot;$file&quot;&lt;/code&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;[ $a &amp;gt; $b ]&lt;/code&gt; 정수 비교&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;리다이렉션&lt;/b&gt;으로 해석(위험). &lt;code&gt;-gt&lt;/code&gt;/&lt;code&gt;-lt&lt;/code&gt; 또는 &lt;code&gt;(( a &amp;gt; b ))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;$?&lt;/code&gt;는 마지막 결과&quot;&lt;/td&gt;
&lt;td&gt;직전 명령 코드. 사이에 다른 명령 끼면 덮어씀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;는 항상 다음 실행&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단락 평가&lt;/b&gt;. 앞 실패 시 다음 안 됨. 정리 무조건 필요하면 trap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;스크립트 안 &lt;code&gt;export&lt;/code&gt;가 부모 셸에 영향&quot;&lt;/td&gt;
&lt;td&gt;아니다. 자식에만. 부모에 적용은 &lt;code&gt;source&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;shebang 없어도 된다&quot;&lt;/td&gt;
&lt;td&gt;현재 셸로 해석될 수 있어 동작 다를 수 있음. 항상 shebang 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;set -e&lt;/code&gt;는 모든 실패를 잡는다&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if&lt;/code&gt;/&lt;code&gt;while&lt;/code&gt; 조건문 안 실패나 파이프 마지막 단계만 검사. &lt;code&gt;set -o pipefail&lt;/code&gt;로 보완&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로세스 치환&lt;/b&gt;: &lt;code&gt;&amp;lt;(cmd)&lt;/code&gt;는 명령 출력을 임시 파일처럼 쓰게 한다 &amp;mdash; &lt;code&gt;diff &amp;lt;(ls dir1) &amp;lt;(ls dir2)&lt;/code&gt;. 임시 파일 없이 두 명령 출력을 비교.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배열&amp;middot;연관배열&lt;/b&gt;: &lt;code&gt;arr=(a b c)&lt;/code&gt;, &lt;code&gt;declare -A m; m[name]=&quot;alice&quot;&lt;/code&gt; (bash 4+).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;trap으로 정리&lt;/b&gt;: &lt;code&gt;trap 'cleanup' EXIT&lt;/code&gt;로 스크립트 종료 시(정상/오류/시그널) 항상 정리 함수를 실행. 임시 파일 제거&amp;middot;잠금 해제에 필수.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;셸(bash)&lt;/b&gt; = 명령 해석기 + 스크립트 언어. &lt;code&gt;/bin/sh&lt;/code&gt;는 bash(RHEL).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 순서&lt;/b&gt;: 브레이스 &amp;rarr; 틸드 &amp;rarr; 변수/명령/산술 &amp;rarr; 워드분리 &amp;rarr; glob &amp;rarr; 리다이렉션 &amp;rarr; 실행. &lt;code&gt;ls *.txt&lt;/code&gt;에서 별을 펼치는 건 셸이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;따옴표&lt;/b&gt;: &lt;code&gt;&quot; &quot;&lt;/code&gt;(변수 O, glob X), &lt;code&gt;' '&lt;/code&gt;(모두 X). &lt;b&gt;파일명엔 항상 &lt;code&gt;&quot;...&quot;&lt;/code&gt;&lt;/b&gt; &amp;mdash; 공백 재앙을 막는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;환경변수&lt;/b&gt;(&lt;code&gt;export&lt;/code&gt;)만 자식 상속. 셸 변수는 현재 셸만. &lt;code&gt;PATH&lt;/code&gt;엔 &lt;code&gt;.&lt;/code&gt;이 없다(보안).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리다이렉션&lt;/b&gt;: &lt;code&gt;&amp;gt;&lt;/code&gt;(덮어쓰기), &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;(추가), &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;(에러를 출력으로), &lt;code&gt;&amp;amp;&amp;gt;&lt;/code&gt;(모두).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;[[ ]]&lt;/code&gt; &amp;gt; &lt;code&gt;[ ]&lt;/code&gt;&lt;/b&gt;: 더 안전, 패턴 매칭 지원.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안전 모드&lt;/b&gt;: &lt;code&gt;set -Eeuo pipefail&lt;/code&gt;. 단 &lt;code&gt;set -e&lt;/code&gt;는 pipefail로 보완해야 파이프 실패를 잡는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변수 할당&lt;/b&gt;: &lt;code&gt;VAR=value&lt;/code&gt;(공백 없음). &lt;code&gt;$?&lt;/code&gt;는 직전 종료 코드.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;ls *.txt&lt;/code&gt;를 칠 때 &lt;code&gt;*&lt;/code&gt;를 처리하는 주체는 누구인가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;'...'&lt;/code&gt;와 &lt;code&gt;&quot;...&quot;&lt;/code&gt;의 차이는? 파일 이름에 공백이 있을 때 왜 &lt;code&gt;&quot;...&quot;&lt;/code&gt;를 써야 하는가?&lt;/li&gt;
&lt;li&gt;셸 변수와 환경변수의 차이. 자식 프로세스는 무엇을 상속받는가?&lt;/li&gt;
&lt;li&gt;현재 디렉토리가 &lt;code&gt;PATH&lt;/code&gt;에 없는 이유(보안)는?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cmd &amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;의 의미를 풀어 설명하라.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;와 &lt;code&gt;;&lt;/code&gt;의 차이는?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[ ]&lt;/code&gt;와 &lt;code&gt;[[ ]]&lt;/code&gt;의 차이. 현대 bash에서 어느 것을 권장하는가?&lt;/li&gt;
&lt;li&gt;스크립트에 &lt;code&gt;set -e&lt;/code&gt;가 있을 때의 효과와 한계는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/bash.html&quot;&gt;Bash Reference Manual &amp;mdash; GNU&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tldp.org/LDP/abs/html/&quot;&gt;Advanced Bash-Scripting Guide&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man bash&lt;/code&gt;, &lt;code&gt;man test&lt;/code&gt;, &lt;code&gt;man hier&lt;/code&gt;, &lt;code&gt;man printf(1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;POSIX Shell: &lt;a href=&quot;https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html&quot;&gt;pubs.opengroup.org&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>bash</category>
      <category>Expansion</category>
      <category>Linux</category>
      <category>SCRIPT</category>
      <category>Shell</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/7</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-shell-script#entry7comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:29:50 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 06. io device</title>
      <link>https://technical-architect.tistory.com/entry/Linux-io-device</link>
      <description>&lt;h1&gt;SSD, 키보드, 네트워크 카드를 같은 방식으로 다룬다 &amp;mdash; 장치 추상화와 LVM&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 디스크를 하나 더 꽂았다. 이제 공간이 모자라니 LVM으로 볼륨을 늘리고 싶다. 그런데 스크립트가 &lt;code&gt;/dev/sda1&lt;/code&gt;을 찾는데 실제는 &lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;이라 실패한다 &amp;mdash; 왜 이름이 다르지? 또 모든 드라이버를 커널에 넣지 않고도 필요할 때만 쓸 수 있는데, 어떻게?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 리눅스가 수십~수백 종의 하드웨어를 일관되게 다루는 방식(장치 파일 추상화)과, 고정된 물리 디스크를 유연하게 쪼개고 합치는 방식(LVM)을 다룬다. 서버 운영 일상 &amp;mdash; 디스크 추가, LVM 확장, 드라이버 적재 &amp;mdash; 의 기반을 잡는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하드웨어도 파일이다 &amp;mdash; 장치 파일과 major/minor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 &quot;모든 것은 파일&quot; 원칙에 따라 &lt;b&gt;하드웨어를 &lt;code&gt;/dev&lt;/code&gt; 아래 파일로 노출&lt;/b&gt;한다. 그래서 디스크는 &lt;code&gt;cat&lt;/code&gt;으로, 키보드는 &lt;code&gt;read&lt;/code&gt;로 접근할 수 있다(물론 권한이 있어야). 각 장치는 &lt;b&gt;major:minor 번호&lt;/b&gt;로 식별된다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;major&lt;/b&gt;: 장치 &lt;b&gt;종류/드라이버&lt;/b&gt;(예: 8 = SCSI/SATA 디스크)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;minor&lt;/b&gt;: 같은 드라이버 내 &lt;b&gt;개별 장치&lt;/b&gt;(예: 0 = 첫 디스크, 1 = 첫 파티션)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ls -l /dev/sda1
# brw-rw---- 1 root disk 8, 1 ...   &amp;larr; 'b'=블록, major=8, minor=1&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블록 장치 vs 문자 장치&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;블록 장치(&lt;code&gt;b&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;문자 장치(&lt;code&gt;c&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;접근&lt;/td&gt;
&lt;td&gt;&lt;b&gt;랜덤 접근&lt;/b&gt;(임의 블록)&lt;/td&gt;
&lt;td&gt;순차 &lt;b&gt;스트림&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단위&lt;/td&gt;
&lt;td&gt;블록(512B/4KiB)&lt;/td&gt;
&lt;td&gt;바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예&lt;/td&gt;
&lt;td&gt;디스크(&lt;code&gt;/dev/sda&lt;/code&gt;, NVMe), 램디스크&lt;/td&gt;
&lt;td&gt;직렬 포트(&lt;code&gt;/dev/tty&lt;/code&gt;), &lt;code&gt;/dev/null&lt;/code&gt;, &lt;code&gt;/dev/random&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자주 쓰는 &lt;code&gt;/dev&lt;/code&gt; 노드&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;노드&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;읽으면 EOF, 쓰면 버림(출력 폐기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/zero&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;읽으면 0x00 무한 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/random&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;엔트로피 기반 난수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/urandom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;의사난수(블록 안 됨, 빠름)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/sda&lt;/code&gt;, &lt;code&gt;/dev/nvme0n1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;디스크 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/sda1&lt;/code&gt;, &lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;디스크 파티션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev/mapper/rl-root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;LVM 논리 볼륨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LVM &amp;mdash; 레고 블록으로 조립하는 유연한 디스크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리 디스크는 고정되어 있다. 디스크 2개(각 1TB)를 합쳐 2TB 하나처럼 못 쓰면 공간이 비효율적이고, 공간이 모자랄 때 파티션을 옮기지 않고는 확장할 수도 없다. LVM(Logical Volume Manager)이 이를 푼다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;LVM&lt;/th&gt;
&lt;th&gt;레고 블록&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기본 단위&lt;/td&gt;
&lt;td&gt;PV(물리 디스크)&lt;/td&gt;
&lt;td&gt;레고 베이스판&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모으기&lt;/td&gt;
&lt;td&gt;VG(볼륨 그룹)&lt;/td&gt;
&lt;td&gt;베이스판 여러 개 연결 &amp;rarr; 큰 작업판&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;잘라 쓰기&lt;/td&gt;
&lt;td&gt;LV(논리 볼륨)&lt;/td&gt;
&lt;td&gt;작업판에서 원하는 크기로 조립&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레고 비유는 &quot;작은 것을 모아 크고 유연하게&quot;만 잡는다. LVM은 PE(Physical Extent, 보통 4 MiB) 단위의 논리 매핑이고, LV는 데이터를 유지하며 확장된다(축소는 위험).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;flowchart LR
    PV1[PV&amp;lt;br/&amp;gt;/dev/sda] --&amp;gt; VG[VG&amp;lt;br/&amp;gt;volgroup]
    PV2[PV&amp;lt;br/&amp;gt;/dev/sdb] --&amp;gt; VG
    VG --&amp;gt; LV1[LV&amp;lt;br/&amp;gt;root]
    VG --&amp;gt; LV2[LV&amp;lt;br/&amp;gt;swap]
    VG --&amp;gt; LV3[LV&amp;lt;br/&amp;gt;data]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PV(Physical Volume)&lt;/b&gt;: LVM용으로 초기화한 디스크/파티션(&lt;code&gt;pvcreate&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VG(Volume Group)&lt;/b&gt;: PV들의 풀. 여러 PV를 묶어 하나의 가상 디스크처럼(&lt;code&gt;vgcreate&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LV(Logical Volume)&lt;/b&gt;: VG에서 잘라낸 논리 볼륨. 파일 시스템을 올리는 단위(&lt;code&gt;lvcreate&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PE/LE&lt;/b&gt;: PV의 최소 할당 단위 = LV의 최소 단위. 보통 4 MiB.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LVM이 주는 것: 여러 물리 디스크를 하나의 거대 볼륨으로, &lt;b&gt;온라인 크기 조정&lt;/b&gt;(fs 언마운트 불필요), 스냅샷&amp;middot;thin provisioning&amp;middot;mirroring.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RHEL 9+/Rocky 10의 LVM은 &lt;b&gt;devices file&lt;/b&gt;(&lt;code&gt;/etc/lvm/devices/system.devices&lt;/code&gt;)로 사용 디스크를 추적한다. &lt;code&gt;lvmdevices&lt;/code&gt;로 관리. 기존 filter 방식보다 명시적이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky 10 기본 설치 구조:&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;sda
├─sda1 &amp;rarr; /boot (파티션)
└─sda2 &amp;rarr; rl VG
         ├─rl-root LV (17G) &amp;rarr; /
         └─rl-swap LV (2G)  &amp;rarr; [SWAP]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파티션 &amp;mdash; MBR vs GPT, 그리고 NVMe 이름의 비밀&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;MBR(MSDOS)&lt;/th&gt;
&lt;th&gt;GPT(GUID)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;최대 디스크&lt;/td&gt;
&lt;td&gt;&lt;b&gt;2 TiB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;사실상 무제한&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최대 파티션&lt;/td&gt;
&lt;td&gt;4개 주(확장 시 더)&lt;/td&gt;
&lt;td&gt;128개(기본)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부팅&lt;/td&gt;
&lt;td&gt;BIOS/Legacy&lt;/td&gt;
&lt;td&gt;&lt;b&gt;UEFI&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;권장&lt;/td&gt;
&lt;td&gt;구 시스템&lt;/td&gt;
&lt;td&gt;&lt;b&gt;현대(Rocky 10 기본)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크가 2 TiB 이상이거나 UEFI면 &lt;b&gt;GPT가 필수&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NVMe vs SATA 이름 &amp;mdash; 자주 걸리는 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자는 디스크 파티션을 무조건 &lt;code&gt;/dev/sda1&lt;/code&gt;로 생각한다. 하지만 &lt;b&gt;NVMe는 &lt;code&gt;/dev/nvme0n1&lt;/code&gt;&lt;/b&gt;(컨트롤러0 네임스페이스1), 파티션은 &lt;b&gt;&lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;&lt;/b&gt;(p가 붙는다). SATA/SCSI는 &lt;code&gt;/dev/sda1&lt;/code&gt;. 이 차이를 모르면 스크립트가 NVMe 시스템에서 실패한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커널 모듈 &amp;mdash; 런타임에 드라이버를 끼웠다 뺐다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 드라이버를 커널에 넣으면 커널이 비대해지고 메모리가 낭비된다. 해법은 &lt;b&gt;필요할 때만 로드&lt;/b&gt;하는 것이다. 커널 기능/드라이버는 &lt;b&gt;런타임에 동적 적재 가능한 코드&lt;/b&gt;(&lt;code&gt;.ko&lt;/code&gt;)로, &lt;code&gt;/lib/modules/&amp;lt;커널버전&amp;gt;/&lt;/code&gt;에 있다. 재부팅 없이 기능을 추가&amp;middot;제거할 수 있어 메모리를 절약한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;insmod vs modprobe &amp;mdash; 왜 modprobe를 써야 하나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자가 자주 &lt;code&gt;insmod&lt;/code&gt;를 쓴다. &lt;b&gt;&lt;code&gt;modprobe&lt;/code&gt;가 낫다.&lt;/b&gt; &lt;code&gt;insmod&lt;/code&gt;는 &lt;b&gt;의존성을 무시&lt;/b&gt;한다 &amp;mdash; 단일 파일을 강제 로드하며, 의존 모듈이 빠지면 실패. 반면 &lt;code&gt;modprobe&lt;/code&gt;는 의존 모듈까지 자동으로 처리한다(&lt;code&gt;depmod&lt;/code&gt;가 만든 &lt;code&gt;modules.dep&lt;/code&gt; 기반). 항상 &lt;code&gt;modprobe&lt;/code&gt;를 써라.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;udev &amp;mdash; 장치를 자동으로 인식한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd-udevd가 &lt;b&gt;장치 이벤트&lt;/b&gt;(핫플러그)를 감지해 &lt;code&gt;/dev&lt;/code&gt; 노드를 자동 생성하고 이름을 붙인다. 디스크를 꽂으면 자동으로 &lt;code&gt;/dev/sdb&lt;/code&gt;가 생기고, 영구 이름도 만든다: &lt;code&gt;/dev/disk/by-uuid/&lt;/code&gt;, &lt;code&gt;/dev/disk/by-id/&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;fstab/GRUB에 UUID를 쓰는 이유&lt;/b&gt;: &lt;code&gt;/dev/sdb&lt;/code&gt;는 부팅마다 바뀔 수 있다. UUID나 by-id는 고정되어 마운트 실패를 막는다. &lt;code&gt;lsblk&lt;/code&gt;는 sysfs + udev DB를 읽어 정보를 표시한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;lsblk                    # 트리 형태
lsblk -f                 # 파일시스템/UUID 포함
blkid                    # UUID/타입/라벨
findmnt                  # 마운트 트리&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
sda             8:0    0   20G  0 disk
├─sda1          8:1    0    1G  0 part /boot
└─sda2          8:2    0   19G  0 part
  ├─rl-root   253:0    0   17G  0 lvm  /
  └─rl-swap   253:1    0    2G  0 lvm  [SWAP]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 파티션 관리
sudo fdisk -l                    # 전체 디스크/파티션
sudo parted /dev/sdb print       # GPT/MBR 무관 범용
sudo fdisk /dev/sdb              # 파티션 생성(대화형)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; LVM 확장(자주 쓰는 절차)
sudo pvcreate /dev/sdb                 # 1. 새 디스크를 PV로
sudo vgextend rl /dev/sdb              # 2. 기존 VG에 추가
sudo lvextend -L +10G /dev/rl/root     # 3. LV 확장(+10G)
sudo xfs_growfs /                      # 4. 파일 시스템까지 확장(XFS)
# sudo resize2fs /dev/rl/root          #    (ext4)
pvs &amp;amp;&amp;amp; vgs &amp;amp;&amp;amp; lvs                      # 상태 확인&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 커널 모듈
lsmod                            # 적재된 모듈
modinfo xfs                      # 모듈 정보
sudo modprobe zram num_devices=1 # 모듈 로드(의존성 자동)
sudo modprobe -r zram            # 제거
ls /lib/modules/$(uname -r)/     # 모듈 디렉토리&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 영구 식별자: fstab에 안전하게 쓰려면
ls -l /dev/disk/by-uuid/ | grep sda1
ls -l /dev/disk/by-id/
sudo udevadm monitor             # 실시간 장치 이벤트&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;NVMe 파티션은 &lt;code&gt;/dev/nvme0n11&lt;/code&gt;&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;code&gt;/dev/nvme0n1p1&lt;/code&gt;&lt;/b&gt;(p 붙음). SATA는 &lt;code&gt;/dev/sda1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;insmod&lt;/code&gt;와 &lt;code&gt;modprobe&lt;/code&gt;는 같다&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;insmod&lt;/code&gt;는 &lt;b&gt;의존성 무시&lt;/b&gt;. 항상 &lt;code&gt;modprobe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;LVM 축소는 확장처럼 쉽다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;데이터 손실 위험&lt;/b&gt;. fs 먼저 축소 후 LV 축소. XFS는 축소 자체 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;파티션 생성하면 바로 쓴다&quot;&lt;/td&gt;
&lt;td&gt;아니다. 위에 &lt;b&gt;파일 시스템 생성(&lt;code&gt;mkfs.xfs&lt;/code&gt;)&lt;/b&gt; 별도, 마운트도 별도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;/dev/sdb&lt;/code&gt;는 항상 같은 디스크&quot;&lt;/td&gt;
&lt;td&gt;부팅마다 바뀔 수 있음. &lt;b&gt;fstab엔 UUID/by-id 사용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;LVM은 예전 기술이다&quot;&lt;/td&gt;
&lt;td&gt;현대 RHEL/Rocky 기본. thin pool, 스냅샷, vdo 등 계속 진화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;커널 모듈은 재부팅해야 로드된다&quot;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;modprobe&lt;/code&gt;로 런타임 로드 가능. &lt;code&gt;/etc/modules-load.d/&lt;/code&gt;로 영구화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;LVM thin provisioning&lt;/b&gt;: 일반 LV는 할당 즉시 공간을 예약하지만, thin pool은 &lt;b&gt;실제 쓰는 만큼만&lt;/b&gt; 할당한다(over-provisioning 가능). 가상화&amp;middot;컨테이너 환경에 유리. 단, over-provision 시 풀 고갈에 주의.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;device mapper&lt;/b&gt;: LVM은 커널 &lt;b&gt;device mapper(DM)&lt;/b&gt; 계층 위에 구현된다. DM은 블록 장치를 매핑&amp;middot;재조합하는 범용 계층(LVM, multipath, dm-crypt가 모두 DM 사용). &lt;code&gt;dmsetup&lt;/code&gt;으로 저수준 확인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VDO(Virtual Data Optimizer)&lt;/b&gt;: 중복 제거&amp;middot;압축으로 디스크 공간을 절약. 백업 스토리지에 유리.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장치 파일&lt;/b&gt;: 하드웨어를 &lt;code&gt;/dev&lt;/code&gt; 아래 파일로. major:minor 번호로 식별. 블록(랜덤, 디스크) vs 문자(스트림, 터미널/null).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LVM&lt;/b&gt;: PV &amp;rarr; VG &amp;rarr; LV. 레고 블록처럼 모으고 자른다. 온라인 확장 가능(축소는 위험). Rocky 10은 devices file로 디스크 추적.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파티션&lt;/b&gt;: MBR(2 TiB 한계, BIOS) vs &lt;b&gt;GPT(대용량, UEFI, Rocky 10 기본)&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NVMe 명명 함정&lt;/b&gt;: &lt;code&gt;/dev/nvme0n1&lt;/code&gt;, 파티션은 &lt;code&gt;nvme0n1p1&lt;/code&gt;(p 붙음). SATA는 &lt;code&gt;/dev/sda1&lt;/code&gt;. 스크립트가 여기서 자주 실패.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커널 모듈&lt;/b&gt;: 런타임 적재. &lt;b&gt;&lt;code&gt;modprobe&lt;/code&gt;(의존성 자동) &amp;gt; &lt;code&gt;insmod&lt;/code&gt;&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;udev&lt;/b&gt;: 핫플러그 자동 인식, &lt;code&gt;/dev&lt;/code&gt; 노드 생성. 영구 이름(by-uuid/by-id) 제공 &amp;mdash; fstab엔 &lt;code&gt;/dev/sdX&lt;/code&gt; 말고 &lt;b&gt;UUID&lt;/b&gt;를 써라(부팅마다 이름이 바뀔 수 있으니).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;블록 장치와 문자 장치의 차이와 각각 예를 말하라.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/null&lt;/code&gt;, &lt;code&gt;/dev/zero&lt;/code&gt;, &lt;code&gt;/dev/random&lt;/code&gt;의 용도를 말하라.&lt;/li&gt;
&lt;li&gt;LVM에서 PV, VG, LV의 관계를 그려보라. 왜 LVM이 일반 파티션보다 유연한가?&lt;/li&gt;
&lt;li&gt;NVMe 디스크의 첫 파티션 장치 이름은? SATA와 왜 다른가?&lt;/li&gt;
&lt;li&gt;MBR과 GPT의 주요 차이. 언제 GPT가 필수인가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insmod&lt;/code&gt; 대신 &lt;code&gt;modprobe&lt;/code&gt;를 써야 하는 이유는?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/sdb&lt;/code&gt; 대신 fstab에 UUID를 써야 하는 이유는?&lt;/li&gt;
&lt;li&gt;LVM LV를 축소할 때 주의할 점은? XFS는 왜 축소 불능인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/devices.html&quot;&gt;Linux allocated devices &amp;mdash; Kernel docs&lt;/a&gt; - 접근 2026-07-09 (장치 major/minor)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man.archlinux.org/man/lvm.8.en&quot;&gt;lvm(8) manual&lt;/a&gt; - 접근 2026-07-09 (PV/VG/LV)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man8/lvmdevices.8.html&quot;&gt;lvmdevices(8) &amp;mdash; man7&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man lsblk&lt;/code&gt;, &lt;code&gt;man blkid&lt;/code&gt;, &lt;code&gt;man fdisk&lt;/code&gt;, &lt;code&gt;man parted&lt;/code&gt;, &lt;code&gt;man lvcreate&lt;/code&gt;, &lt;code&gt;man modprobe&lt;/code&gt;, &lt;code&gt;man udev(7)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>device</category>
      <category>io</category>
      <category>kernel-module</category>
      <category>Linux</category>
      <category>lvm</category>
      <category>udev</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/6</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-io-device#entry6comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:27:09 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 05. filesystem</title>
      <link>https://technical-architect.tistory.com/entry/Linux-filesystem</link>
      <description>&lt;h1&gt;inode에는 파일 이름이 없다 &amp;mdash; 파일 시스템이 감추는 것&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Permission denied&quot;가 떴다. 파일 권한을 고쳤는데도 안 된다. 왜? 종종 진짜 원인은 파일이 아니라 &lt;b&gt;부모 디렉토리&lt;/b&gt;에 있다. 또 디스크에 공간이 남았는데 파일이 안 만들어진다 &amp;mdash; 이번엔 inode가 다 떨어졌기 때문이다. 이런 일들은 파일 시스템이 실제로 어떻게 돌아가는지를 모르면 진단할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 &quot;모든 것은 파일&quot;이라고 한다. 그런데 디스크는 그냥 0과 1의 블록 덩어리다. 이 블록들을 어떻게 &quot;이름 있는 파일&quot;로 조직하고, 누가 접근할지 통제하는가 &amp;mdash; 이 글이 푸는 질문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;모든 것은 파일&quot; &amp;mdash; VFS 추상화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스는 디스크 파일, 하드웨어 장치, 프로세스 정보, 네트워크 소켓까지 &lt;b&gt;모두 파일&lt;/b&gt;로 다룬다. 그래서 같은 도구(&lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;)로 모든 것을 조작할 수 있다. 이 통일을 가능하게 하는 커널 계층이 &lt;b&gt;VFS(Virtual File System)&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;flowchart TB
    APP[응용 프로그램&amp;lt;br/&amp;gt;open/read/write] --&amp;gt; VFS[VFS 계층&amp;lt;br/&amp;gt;공통 인터페이스]
    VFS --&amp;gt; XFS[XFS 드라이버]
    VFS --&amp;gt; EXT[ext4 드라이버]
    VFS --&amp;gt; PROC[procfs/proc]
    VFS --&amp;gt; SYS[sysfs/sys]
    VFS --&amp;gt; DEV[devtmpfs/dev]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VFS는 각 파일 시스템(XFS/ext4/proc/sysfs 등)의 &lt;b&gt;차이를 감추고 동일한 시스템 콜&lt;/b&gt;을 제공한다. 덕분에 &lt;code&gt;cat&lt;/code&gt;/&lt;code&gt;ls&lt;/code&gt;가 XFS든 procfs든 같은 방식으로 동작하고, &lt;code&gt;/proc/meminfo&lt;/code&gt;를 &lt;code&gt;cat&lt;/code&gt;으로 읽는 것이 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;inode에는 이름이 없다 &amp;mdash; 도서관 카드 카탈로그&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 초보자가 &quot;inode에 파일 이름이 들어있겠지&quot;라고 생각한다. &lt;b&gt;틀렸다.&lt;/b&gt; &lt;b&gt;inode에는 파일 이름이 없다.&lt;/b&gt; 이름은 &lt;b&gt;디렉토리&lt;/b&gt;가 보관한다(이름 &amp;rarr; inode 번호 매핑 표).&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;파일 시스템&lt;/th&gt;
&lt;th&gt;도서관&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;파일 이름&lt;/td&gt;
&lt;td&gt;&quot;report.pdf&quot;&lt;/td&gt;
&lt;td&gt;책 제목&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;inode&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;메타데이터 + 데이터 블록 포인터&lt;/td&gt;
&lt;td&gt;&lt;b&gt;카드 카탈로그&lt;/b&gt;(위치&amp;middot;저자&amp;middot;페이지 수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 블록&lt;/td&gt;
&lt;td&gt;실제 파일 내용&lt;/td&gt;
&lt;td&gt;실제 책&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디렉토리&lt;/td&gt;
&lt;td&gt;이름 &amp;rarr; inode 매핑 표&lt;/td&gt;
&lt;td&gt;&lt;b&gt;색인함&lt;/b&gt;(제목 &amp;rarr; 카드 위치)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서관 비유는 &quot;이름은 카드에 없고 색인함에 있다&quot;는 관계만 잡는다. 파일 시스템은 같은 inode에 이름 여러 개(하드 링크)가 가능하고, 디스크는 블록 단위 비연속 할당이라는 점에서 비유를 넘어선다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 inode가 아니라 디렉토리에 있다는 설계 덕분에:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 inode에 &lt;b&gt;여러 이름&lt;/b&gt;(하드 링크)을 가질 수 있다.&lt;/li&gt;
&lt;li&gt;파일 이름 바꾸기(&lt;code&gt;mv&lt;/code&gt;)는 데이터 복사 없이 디렉토리 항목만 변경 &amp;rarr; 즉시 완료.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 보자:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;flowchart LR
    BS[Boot Sector] --&amp;gt; SB[Superblock&amp;lt;br/&amp;gt;전체 메타정보]
    SB --&amp;gt; IT[Inode Table&amp;lt;br/&amp;gt;파일 메타데이터]
    IT --&amp;gt; DB[Data Blocks&amp;lt;br/&amp;gt;실제 파일 내용]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Superblock&lt;/b&gt;: 파일 시스템 전체 정보(크기, 타입, 마운트 상태). 손상 시 복구 대상.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;inode&lt;/b&gt;: 파일 1개당 1개. 권한&amp;middot;소유자&amp;middot;크기&amp;middot;타임스탬프 + 데이터 블록 포인터.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Data block&lt;/b&gt;: 실제 파일 데이터.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;inode 고갈의 함정&lt;/b&gt;: 디스크 여유 공간이 있어도 inode가 다 떨어지면 파일 생성이 안 된다(&lt;code&gt;df -i&lt;/code&gt;로 확인). 작은 파일 수십만 개(메일 큐 등)가 있는 시스템에서 발생한다. &quot;공간은 있는데 파일이 안 만들어진다&quot;의 원인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;XFS vs ext4 &amp;mdash; Rocky 10의 기본은 XFS&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;&lt;b&gt;XFS(RHEL 10 기본)&lt;/b&gt;&lt;/th&gt;
&lt;th&gt;ext4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발&lt;/td&gt;
&lt;td&gt;SGI, 1990년대 초&lt;/td&gt;
&lt;td&gt;리눅스 전통(ext2/3 진화)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장&lt;/td&gt;
&lt;td&gt;&lt;b&gt;성장만 가능&lt;/b&gt;(grow), &lt;b&gt;축소 불가&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;성장/축소(축소는 오프라인) 모두&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inode&lt;/td&gt;
&lt;td&gt;&lt;b&gt;동적 할당&lt;/b&gt;, 64-bit(2^32 한계 없음)&lt;/td&gt;
&lt;td&gt;고정, &lt;b&gt;2^32 한계&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대용량/IOPS&lt;/td&gt;
&lt;td&gt;대형 파일&amp;middot;고동시성에 강&lt;/td&gt;
&lt;td&gt;단일 스레드/소규모에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특수 기능&lt;/td&gt;
&lt;td&gt;&lt;b&gt;reflink&lt;/b&gt;(COW 복사), xattr, project quota&lt;/td&gt;
&lt;td&gt;익숙함, 단순&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성/복구&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mkfs.xfs&lt;/code&gt; / &lt;code&gt;xfs_repair&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mkfs.ext4&lt;/code&gt; / &lt;code&gt;e2fsck&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 이유 없으면 &lt;b&gt;XFS&lt;/b&gt;(RHEL 기본)를 써라. 현대 서버는 대용량&amp;middot;고동시성이고, XFS는 대형 파일(수백 MB 이상)&amp;middot;수천 IOPS 환경에서 ext4보다 일관되게 우수하다. 또 reflink(COW 복사)로 스냅샷&amp;middot;백업이 효율적이다. 단, &lt;b&gt;축소 불가&lt;/b&gt;는 단점 &amp;mdash; 줄이려면 백업&amp;rarr;재생성해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FHS &amp;mdash; 디렉토리 계층 표준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 디렉토리 구조는 FHS(Filesystem Hierarchy Standard)로 표준화되어 있다. 핵심 디렉토리:&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;경로&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;최상위 루트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bin&lt;/code&gt;, &lt;code&gt;/sbin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기본 명령. &lt;i&gt;RHEL 8+부터 &lt;code&gt;/usr/bin&lt;/code&gt;, &lt;code&gt;/usr/sbin&lt;/code&gt;으로 심볼릭 통합&lt;/i&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/boot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커널&amp;middot;initramfs&amp;middot;GRUB 부트 파일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/etc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;시스템 설정 파일(editable text configuration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/usr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UNIX System Resources &amp;mdash; 사용자 명령&amp;middot;라이브러리&amp;middot;문서&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/var&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가변 데이터 &amp;mdash; 로그, 스풀, 메일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;, &lt;code&gt;/root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;일반 사용자 홈 / root 홈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/tmp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;임시 파일(부팅 시 초기화 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;장치 파일(블록/문자 장치)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/proc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가상 fs &amp;mdash; 커널/프로세스 정보(&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/&lt;/code&gt;, &lt;code&gt;/proc/meminfo&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/sys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;가상 fs &amp;mdash; 하드웨어/커널 모듈 정보(sysfs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/mnt&lt;/code&gt;, &lt;code&gt;/media&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;임시 마운트 / 이동식 미디어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;런타임 데이터(부팅마다 초기화, 과거 &lt;code&gt;/var/run&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;권한 &amp;mdash; 9비트가 누가 뭘 할 수 있는지를 결정한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 파일/디렉토리는 3개 주체 &amp;times; 3개 권한 = &lt;b&gt;9비트&lt;/b&gt;(+특수 3비트)로 통제된다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주체&lt;/th&gt;
&lt;th&gt;기호&lt;/th&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;권한&lt;/th&gt;
&lt;th&gt;파일&lt;/th&gt;
&lt;th&gt;디렉토리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;소유자&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r&lt;/code&gt;(4)&lt;/td&gt;
&lt;td&gt;내용 읽기&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ls&lt;/code&gt;(목록 조회)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그룹&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w&lt;/code&gt;(2)&lt;/td&gt;
&lt;td&gt;내용 수정&lt;/td&gt;
&lt;td&gt;파일 생성/삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;그 외&lt;/td&gt;
&lt;td&gt;&lt;code&gt;o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;(1)&lt;/td&gt;
&lt;td&gt;실행&lt;/td&gt;
&lt;td&gt;&lt;b&gt;진입(&lt;code&gt;cd&lt;/code&gt;)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8진수: &lt;code&gt;rwx&lt;/code&gt; = 4+2+1 = 7. 예: &lt;code&gt;755&lt;/code&gt; = &lt;code&gt;rwxr-xr-x&lt;/code&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디렉토리 x권한의 비밀&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자가 자주 틀리는 부분: &quot;디렉토리에 &lt;code&gt;x&lt;/code&gt;가 있으면 실행? 이상하다.&quot; &amp;mdash; 디렉토리에서 &lt;code&gt;x&lt;/code&gt;는 &lt;b&gt;'진입'(&lt;code&gt;cd&lt;/code&gt;) 권한&lt;/b&gt;이다. 디렉토리에 &lt;code&gt;r&lt;/code&gt;만 있고 &lt;code&gt;x&lt;/code&gt;가 없으면 &lt;code&gt;ls&lt;/code&gt;는 되지만 &lt;b&gt;&lt;code&gt;cd&lt;/code&gt;/내부 파일 접근이 안 된다&lt;/b&gt;. &lt;code&gt;r&lt;/code&gt;은 목록 보기, &lt;code&gt;x&lt;/code&gt;는 들어가기.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특수 비트&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;비트&lt;/th&gt;
&lt;th&gt;기호&lt;/th&gt;
&lt;th&gt;8진수 자리&lt;/th&gt;
&lt;th&gt;효과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;setuid&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;(user 자리)&lt;/td&gt;
&lt;td&gt;첫 자리 4&lt;/td&gt;
&lt;td&gt;실행 시 &lt;b&gt;소유자 권한&lt;/b&gt;으로 동작(예: &lt;code&gt;/usr/bin/passwd&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;setgid&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s&lt;/code&gt;(group 자리)&lt;/td&gt;
&lt;td&gt;첫 자리 2&lt;/td&gt;
&lt;td&gt;파일: 그룹 권한으로 실행. &lt;b&gt;디렉토리&lt;/b&gt;: 새 파일이 디렉토리 그룹 상속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;sticky&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;t&lt;/code&gt;(others 자리)&lt;/td&gt;
&lt;td&gt;첫 자리 1&lt;/td&gt;
&lt;td&gt;공유 디렉토리에서 &lt;b&gt;본인 소유 파일만 삭제&lt;/b&gt;(&lt;code&gt;/tmp&lt;/code&gt;가 대표)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4자리 8진수: &lt;code&gt;4755&lt;/code&gt; = setuid + &lt;code&gt;rwxr-xr-x&lt;/code&gt;. &lt;code&gt;1777&lt;/code&gt; = sticky + &lt;code&gt;rwxrwxrwt&lt;/code&gt;(= &lt;code&gt;/tmp&lt;/code&gt;).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 삭제 권한의 함정 &amp;mdash; 도입부의 수수께끼 풀기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 지우려면 &lt;b&gt;파일 자체의 &lt;code&gt;w&lt;/code&gt; 비트가 아니라 부모 디렉토리의 &lt;code&gt;w&lt;/code&gt; 비트&lt;/b&gt;가 필요하다. 읽기 전용 파일(&lt;code&gt;-r--r--r--&lt;/code&gt;)도 부모 디렉토리에 &lt;code&gt;w&lt;/code&gt;가 있으면 지울 수 있다. &quot;Permission denied&quot;의 진짜 원인이 파일이 아니라 디렉토리인 이유다. &lt;code&gt;/tmp&lt;/code&gt;는 sticky 비트 덕분에 남의 파일을 못 지운다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마운트, fstab, 링크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 시스템을 디렉토리 트리에 연결하는 게 &lt;b&gt;마운트&lt;/b&gt;다. &lt;code&gt;/etc/fstab&lt;/code&gt;에 자동 마운트를 정의한다:&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 장치              마운트포인트  타입  옵션        dump fsck
/dev/mapper/rl-root /             xfs   defaults    0    0
UUID=...           /boot         xfs   defaults    0    0
/dev/mapper/rl-swap none         swap  defaults    0    0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼 6개: 장치 / 마운트포인트 / 타입 / 옵션 / dump / fsck 순서. 부팅 시 자동 마운트되며, 변경 후엔 &lt;code&gt;sudo mount -a&lt;/code&gt;로 검증한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하드 링크 vs 심볼릭 링크&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;하드 링크&lt;/th&gt;
&lt;th&gt;심볼릭 링크&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;명령&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ln 원본 링크&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ln -s 원본 링크&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;본질&lt;/td&gt;
&lt;td&gt;같은 inode를 가리키는 &lt;b&gt;또 하나의 이름&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;원본 경로를 담은 &lt;b&gt;별개의 파일&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;inode&lt;/td&gt;
&lt;td&gt;원본과 &lt;b&gt;동일&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;다름&lt;/b&gt;(자체 inode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;원본 삭제&lt;/td&gt;
&lt;td&gt;데이터 유지(링크 남음)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;깨진 링크&lt;/b&gt;(dangling)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디렉토리&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다른 fs&lt;/td&gt;
&lt;td&gt;불가&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;inode에 이름이 없다&quot;를 떠올려라. 하드 링크는 같은 inode에 이름을 하나 더 추가하는 것이다. 그래서 원본을 &quot;지워도&quot; inode 참조가 남아있으면 데이터는 살아있다. 단, 하드 링크는 백업이 아니다 &amp;mdash; 같은 inode를 가리킬 뿐, 복사가 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;df -hT            # -h 인간판독, -T 타입
df -i             # inode 사용량(공간 있는데 안 만들어지면 확인)
du -sh /var/log   # 특정 디렉토리 합계&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Filesystem              Type   Size  Used Avail Use% Mounted on
/dev/mapper/rl-root     xfs     17G  4.5G   13G  27% /&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; inode 직접 확인: 이름은 inode에 없다
ls -i /etc/hosts            # inode 번호
stat /etc/hosts             # 메타데이터 전체&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;  File: /etc/hosts
  Inode: 1234567     Links: 1
  Access: (0644/-rw-r--r--)  Uid: (0/root)  Gid: (0/root)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: stat 출력에 파일 &lt;b&gt;이름은 없다&lt;/b&gt;. inode는 번호로만 존재한다.&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 권한 관리
chmod 755 script.sh          # 8진수
chmod u+x,g-w file.txt       # 기호
chmod -R 644 /var/www        # 재귀
sudo chown user:group file   # 소유권 변경
sudo chmod 4755 /usr/bin/myapp    # setuid
sudo chmod 1777 /shared           # sticky (=/tmp처럼)
umask                            # 신규 파일 기본 권한 마스크(보통 022)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 마운트 관리
findmnt                       # 트리 형태
sudo mount /dev/sdb1 /mnt     # 수동 마운트
sudo mount -a                 # fstab 수정 후 문법 검증&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 하드 vs 심볼릭 체험
ln /etc/file /root/hardlink       # 하드(같은 inode)
ln -s /etc/file /root/softlink    # 심볼릭(다른 inode)
ls -li /root/                     # inode 확인(-i)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;golo&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; setuid 파일 점검(보안)
find / -perm -4000 -type f 2&amp;gt;/dev/null   # setuid 파일(정기 점검 권장)
find /var -name &quot;*.log&quot; -mtime -1        # 1일 이내 .log
find /home -size +100M                    # 100MB 초과&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;디렉토리에 &lt;code&gt;x&lt;/code&gt;는 의미 없다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;진입(&lt;code&gt;cd&lt;/code&gt;) 권한&lt;/b&gt;. &lt;code&gt;r&lt;/code&gt;만 있고 &lt;code&gt;x&lt;/code&gt; 없으면 내부 접근 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;파일 지우려면 파일 &lt;code&gt;w&lt;/code&gt; 필요&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;부모 디렉토리 &lt;code&gt;w&lt;/code&gt;&lt;/b&gt; 필요. 파일 자체는 읽기전용이어도 디렉토리 &lt;code&gt;w&lt;/code&gt; 있으면 지움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;inode에 파일 이름이 있다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;없다&lt;/b&gt;. 이름은 디렉토리가 보관. 그래서 하드링크 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;mv&lt;/code&gt;는 데이터를 복사한다&quot;&lt;/td&gt;
&lt;td&gt;같은 파일 시스템 내에선 &lt;b&gt;inode 그대로, 디렉토리 항목만 변경&lt;/b&gt; &amp;rarr; 즉시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;XFS는 ext4보다 무조건 좋다&quot;&lt;/td&gt;
&lt;td&gt;대용량/고동시성에 강하지만 &lt;b&gt;축소 불가&lt;/b&gt;, 메타데이터 CPU 더 씀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;inode 고갈은 안 일어난다&quot;&lt;/td&gt;
&lt;td&gt;작은 파일 수백만 개(메일 큐) 시스템에선 발생. &lt;code&gt;df -i&lt;/code&gt; 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;/bin&lt;/code&gt;과 &lt;code&gt;/usr/bin&lt;/code&gt;은 다르다&quot;&lt;/td&gt;
&lt;td&gt;RHEL 8+부턴 &lt;code&gt;/bin&lt;/code&gt; &amp;rarr; &lt;code&gt;/usr/bin&lt;/code&gt; 심볼릭. 사실상 같음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;하드 링크는 백업이다&quot;&lt;/td&gt;
&lt;td&gt;같은 inode 가리킬 뿐. 복사 아님. rsync/reflink이 진짜 복사&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;setuid의 보안 위험&lt;/b&gt;: setuid 파일은 권한 승격 통로다. &lt;code&gt;/usr/bin/passwd&lt;/code&gt;(일반 사용자가 root 권한으로 &lt;code&gt;/etc/shadow&lt;/code&gt; 변경)가 정당한 사례지만, setuid가 걸린 의심스러운 바이너리는 침해 통로. &lt;code&gt;find / -perm -4000&lt;/code&gt;로 정기 점검하라.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;reflink(XFS COW 복사)&lt;/b&gt;: &lt;code&gt;cp --reflink=always&lt;/code&gt;는 데이터 복사 없이 같은 블록을 참조(COW). 스냅샷&amp;middot;백업이 거의 즉시. Stratis 같은 스냅샷 기반 백업 솔루션의 기반.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;atime/noatime&lt;/b&gt;: 파일 접근 시간(atime) 기록이 매 읽기마다 디스크 쓰기를 유발. 성능 중요 서버에선 &lt;code&gt;noatime&lt;/code&gt;으로 끈다. RHEL 최근 기본은 &lt;code&gt;relatime&lt;/code&gt;(필요 시만 갱신).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;VFS&lt;/b&gt;: 모든 파일 시스템을 통일 인터페이스로 추상화. &quot;모든 것은 파일&quot;의 기반.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;inode&lt;/b&gt;: 파일 1개당 1개. 메타데이터 + 데이터 블록 포인터. &lt;b&gt;이름은 inode에 없다&lt;/b&gt;(디렉토리가 보관) &amp;mdash; 이 설계가 하드 링크&amp;middot;빠른 &lt;code&gt;mv&lt;/code&gt;를 가능하게 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;XFS(RHEL 10 기본)&lt;/b&gt;: 대용량/고동시성 강, reflink. &lt;b&gt;축소 불가&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;권한 9비트&lt;/b&gt;: rwx &amp;times; (user/group/other). 디렉토리 &lt;code&gt;x&lt;/code&gt; = 진입. &lt;b&gt;파일 삭제는 부모 디렉토리 &lt;code&gt;w&lt;/code&gt;&lt;/b&gt;가 결정 &amp;mdash; 이게 도입부 &quot;Permission denied&quot;의 진짜 원인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특수 비트&lt;/b&gt;: setuid(4), setgid(2), sticky(1). &lt;code&gt;/tmp&lt;/code&gt; = sticky(1777)라 남의 파일 삭제가 안 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;inode 고갈&lt;/b&gt;: 공간이 있어도 파일 생성 불가. &lt;code&gt;df -i&lt;/code&gt;로 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;inode에 파일 이름이 있는가? 없다면 이름은 어디에 있고, 그 설계의 이점은?&lt;/li&gt;
&lt;li&gt;디스크 공간이 있는데 파일 생성이 안 될 때 무엇을 확인하는가?&lt;/li&gt;
&lt;li&gt;디렉토리에 &lt;code&gt;r&lt;/code&gt;만 있고 &lt;code&gt;x&lt;/code&gt; 없을 때 일어나는 일은?&lt;/li&gt;
&lt;li&gt;읽기전용 파일(&lt;code&gt;-r--r--r--&lt;/code&gt;)을 지울 수 있는가? 조건은?&lt;/li&gt;
&lt;li&gt;setuid, setgid, sticky 비트의 8진수 값과 효과를 말하라. &lt;code&gt;/tmp&lt;/code&gt;는 왜 남의 파일 삭제가 안 되는가?&lt;/li&gt;
&lt;li&gt;하드 링크와 심볼릭 링크의 차이. 원본 삭제 시 각각 어떻게 되나?&lt;/li&gt;
&lt;li&gt;VFS가 없다면 &lt;code&gt;cat /proc/meminfo&lt;/code&gt;가 안 되는 이유는?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/managing_file_systems/overview-of-available-file-systems&quot;&gt;Managing file systems &amp;mdash; RHEL 10&lt;/a&gt; - 접근 2026-07-09 (XFS/ext4 비교)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/managing_file_systems/getting-started-with-xfs&quot;&gt;Getting started with XFS &amp;mdash; RHEL 10&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rockylinux.org/10/books/admin_guide/07-file-systems/&quot;&gt;Rocky Linux 10 Admin Guide - File System&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man hier&lt;/code&gt;(FHS), &lt;code&gt;man chmod&lt;/code&gt;, &lt;code&gt;man stat&lt;/code&gt;, &lt;code&gt;man find&lt;/code&gt;, &lt;code&gt;man inode(7)&lt;/code&gt;, &lt;code&gt;man fstab&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>Filesystem</category>
      <category>inode</category>
      <category>Linux</category>
      <category>permission</category>
      <category>vfs</category>
      <category>xfs</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/5</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-filesystem#entry5comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:25:45 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 04. memory</title>
      <link>https://technical-architect.tistory.com/entry/Linux-memory</link>
      <description>&lt;h1&gt;각 프로세스는 수십 TB 메모리를 독점한다고 착각한다 &amp;mdash; 가상 메모리의 속임&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 &lt;code&gt;free&lt;/code&gt;를 치면 &quot;free가 거의 없다&quot;고 나온다. 메모리 부족인가, 패닉할 일인가? 보통 아니다. 또 누군가 &lt;code&gt;malloc(1GB)&lt;/code&gt;를 호출하면 즉시 1GB RAM이 사라질까? 그것도 아니다. 두 사실 모두 리눅스 메모리 관리가 만들어내는 환상의 결과다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 그 환상을 다룬다 &amp;mdash; 각 프로세스가 &quot;자기만의 거대한 주소 공간&quot;을 독점하는 듯 보이게 만드는 &lt;b&gt;가상 메모리&lt;/b&gt;, RAM이 부족할 때 디스크까지 끌어쓰는 &lt;b&gt;swap&lt;/b&gt;, 그리고 모조리 고갈됐을 때 시스템을 살리기 위해 프로세스를 죽이는 &lt;b&gt;OOM killer&lt;/b&gt;까지. 이 셋을 모르면 &lt;code&gt;free&lt;/code&gt;/&lt;code&gt;vmstat&lt;/code&gt; 숫자를 읽을 수 없고, &quot;서버가 느린데 CPU는 한가하다&quot; 같은 메모리 병목을 진단하지 못한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가상 메모리 &amp;mdash; 각자 자기만의 주소 세계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로세스는 물리 RAM과 무관한 &lt;b&gt;고유의 가상 주소 공간&lt;/b&gt;을 본다. x86_64에서 약 &lt;b&gt;256 TiB&lt;/b&gt;(4-level) 또는 128 PiB(5-level). 물리 RAM은 8GB여도, 프로세스는 자기가 256 TB를 쓸 수 있다고 믿는다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart LR
    V[&quot;가상 주소&amp;lt;br/&amp;gt;프로세스 관점&quot;] --&amp;gt; MMU[&quot;MMU + TLB&quot;]
    MMU --&amp;gt; PT[&quot;페이지 테이블&amp;lt;br/&amp;gt;(커널 관리)&quot;]
    PT --&amp;gt; P[&quot;물리 페이지 프레임&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이 속임수를 쓸까. 세 가지 문제를 한꺼번에 푼다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;격리/보안&lt;/b&gt; &amp;mdash; 프로세스 A의 &quot;주소 100&quot;과 프로세스 B의 &quot;주소 100&quot;은 물리적으로 다른 곳. 서로 침범 불가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RAM보다 큰 공간&lt;/b&gt; &amp;mdash; demand paging(필요할 때만 할당)으로 실제 RAM을 넘는 주소 공간을 다룬다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;효율적 공유&lt;/b&gt; &amp;mdash; 같은 물리 페이지를 여러 프로세스가 공유(COW, 공유 라이브러리).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호텔 방 번호와 비슷하다: 각 층에 101호가 있지만 물리적 위치는 다르다. 단, 가상 메모리는 페이지가 디스크(swap)로도 갈 수 있고 여러 프로세스가 같은 물리 페이지를 공유할 수 있다는 점에서 호텔 비유를 넘어선다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;페이지, 페이지 테이블, TLB &amp;mdash; 변환의 비용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리를 다루는 최소 단위는 &lt;b&gt;페이지&lt;/b&gt;(x86_64 기본 4 KiB)다. 가상 주소 &amp;rarr; 물리 주소 변환 정보는 &lt;b&gt;페이지 테이블&lt;/b&gt;(계층 구조: PGD &amp;rarr; P4D &amp;rarr; PUD &amp;rarr; PMD &amp;rarr; PTE)에 있고, 이 변환을 캐시하는 하드웨어가 &lt;b&gt;TLB&lt;/b&gt;(CPU MMU 내)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 TLB가 중요한가. 수치로 보자.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;지연&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;L1 캐시 접근&lt;/td&gt;
&lt;td&gt;~0.5 ns&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TLB 히트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;~1 ns&lt;/td&gt;
&lt;td&gt;변환 캐시에서 즉시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메인 메모리 접근&lt;/td&gt;
&lt;td&gt;~100 ns&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TLB 미스 &amp;rarr; 페이지 워크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;~60-100 ns&lt;/td&gt;
&lt;td&gt;페이지 테이블을 RAM에서 탐색&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLB 미스는 매 접근마다 메모리 탐색을 강제한다. 최악의 경우 CPU가 99% 시간을 주소 변환에 낭비할 수도 있다. 그래서 &lt;b&gt;huge page&lt;/b&gt;(2 MiB/1 GiB)로 TLB 커버리지를 넓힌다 &amp;mdash; 4 KiB 페이지 64개 엔트리는 256 KiB를 커버하지만, 2 MiB huge page 64개는 &lt;b&gt;128 MiB&lt;/b&gt;를 커버. 대형 메모리 작업(DB)에 필수다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;malloc은 약속일 뿐이다 &amp;mdash; demand paging&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 &lt;code&gt;malloc(1GB)&lt;/code&gt;로 1GB를 요청하면, 즉시 1GB RAM이 할당될까? &lt;b&gt;아니다.&lt;/b&gt; &lt;code&gt;malloc&lt;/code&gt;은 &quot;이 주소 구간을 쓸 수 있다&quot;는 &lt;b&gt;약속&lt;/b&gt;만 할 뿐, 실제 물리 페이지는 &lt;b&gt;그 주소에 처음 접근하는 순간에야&lt;/b&gt; 할당된다. 이걸 &lt;b&gt;demand paging&lt;/b&gt;(요구 시 할당)이라 한다. 1GB를 &lt;code&gt;malloc&lt;/code&gt;하고 1MB만 쓰면, 실제 RAM은 1MB만 점유한다. &quot;오버 커밋&quot;의 원리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근 순간 페이지가 없으면 &amp;rarr; &lt;b&gt;페이지 폴트&lt;/b&gt; 예외 &amp;rarr; 커널이 그제야 물리 페이지를 확보하고 매핑 &amp;rarr; 프로세스는 모르고 계속 실행.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart TD
    A[프로세스 메모리 접근] --&amp;gt; B{TLB hit?}
    B --&amp;gt;|예| RUN[실행]
    B --&amp;gt;|아니오| C[페이지 워크]
    C --&amp;gt; D{PTE 존재?}
    D --&amp;gt;|예| RUN
    D --&amp;gt;|아니오| FAULT[페이지 폴트]
    FAULT --&amp;gt; T{유형}
    T --&amp;gt;|minor| M[zero/COW 페이지 할당&amp;lt;br/&amp;gt;디스크 I/O 없음]
    T --&amp;gt;|major| MA[디스크 읽기&amp;lt;br/&amp;gt;파일/swap-in]&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;폴트 유형&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;비용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;minor&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;COW, lazy alloc, 최초 zero 페이지&lt;/td&gt;
&lt;td&gt;낮음(메모리만)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;major&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파일 mmap 미스 또는 swap-in &amp;rarr; &lt;b&gt;디스크 I/O&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;높음(&lt;del&gt;150&amp;mu;s&lt;/del&gt;10ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGSEGV&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;매핑 자체가 없거나 권한 위반&lt;/td&gt;
&lt;td&gt;프로세스 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;COW(Copy-On-Write) &amp;mdash; fork가 빠른 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;fork()&lt;/code&gt; 시 부모의 모든 페이지를 복사하면 느리다. 리눅스는 &lt;b&gt;PTE를 읽기 전용으로 공유&lt;/b&gt;한다. 어느 한쪽이 쓰기하면 그때 minor 폴트로 그 페이지만 복사. fork 비용을 극적으로 낮춘다(03장 참조).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;free가 적은 건 정상이다&quot; &amp;mdash; 페이지 캐시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보자가 &lt;code&gt;free&lt;/code&gt;를 보고 &quot;free가 거의 없다, 메모리 부족!&quot;이라 패닉하는 게 가장 흔한 실수다. &lt;b&gt;틀렸다.&lt;/b&gt; 리눅스는 &lt;b&gt;남는 RAM을 자동으로 디스크 캐시(페이지 캐시)로 쓴다.&lt;/b&gt; 빈 RAM은 낭비라는 철학 때문이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 읽기/쓰기는 페이지 캐시를 거친다(direct I/O가 아니면).&lt;/li&gt;
&lt;li&gt;RAM 여유가 많으면 캐시가 커진다 &amp;rarr; 다음 읽기가 빨라진다.&lt;/li&gt;
&lt;li&gt;메모리 압력이 오면 커널이 캐시를 회수해 프로세스에 준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 판단은 &lt;code&gt;free&lt;/code&gt; 컬럼이 아니라 &lt;b&gt;&lt;code&gt;available&lt;/code&gt; 컬럼&lt;/b&gt;으로 해야 한다. &lt;code&gt;free -h&lt;/code&gt;의 &lt;code&gt;buff/cache&lt;/code&gt;가 바로 이 영역. &lt;code&gt;available&lt;/code&gt;이 충분하면 free가 낮아도 정상이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RAM이 모자랄 때 &amp;mdash; swap과 zram&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAM이 꽉 찼다. 새 할당 요청이 오면? (1) 캐시 회수 (2) 그래도 모자라면 &amp;mdash; 사용 빈도 낮은(cold) 페이지를 디스크로 쫓아낸다. 이게 &lt;b&gt;swap&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;swap 파티션 또는 swap 파일.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vm.swappiness&lt;/code&gt;(0~200, 기본 60): 커널이 anonymous 페이지(프로세스 힙/스택)를 swap할 적극성. 높을수록 swap 선호.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;zram&lt;/b&gt;은 RAM 일부를 &lt;b&gt;압축하여 swap 장치로 사용&lt;/b&gt;하는 블록 장치(&lt;code&gt;/dev/zramN&lt;/code&gt;)다. 디스크 I/O 없이 RAM 내에서 압축/해제 &amp;rarr; 일반 swap보다 훨씬 빠르다. 메모리가 작은 시스템(라즈베리파이, 임베디드)에서 특히 효과적. Rocky 10 서버 설치 시 기본은 swap 파티션이며, zram은 별도 설정(&lt;code&gt;zram-generator&lt;/code&gt;)으로 추가한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 swap이 느린가 &amp;mdash; 수치로&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;지연&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;메모리 접근&lt;/td&gt;
&lt;td&gt;~100 ns&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zram swap-in&lt;/td&gt;
&lt;td&gt;~수 &amp;mu;s&lt;/td&gt;
&lt;td&gt;RAM 내 압축 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSD swap-in&lt;/td&gt;
&lt;td&gt;~150 &amp;mu;s&lt;/td&gt;
&lt;td&gt;디스크 읽기(메모리의 1500배)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HDD swap-in&lt;/td&gt;
&lt;td&gt;~10 ms&lt;/td&gt;
&lt;td&gt;디스크 탐색(메모리의 &lt;b&gt;10만 배&lt;/b&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap이 잦으면 시스템이 이 수치 차이만큼 느려진다. 특히 HDD 서버에서 스와핑 폭주(thrashing)는 치명적이다. 잦은 스와핑은 RAM 증설 신호다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap을 0으로 두면? 고갈 시 즉시 OOM kill이 발생한다. 안정성을 위해 일정 swap을 권장한다. swappiness=0도 &quot;안 하려 할 뿐&quot;이지 절대 금지가 아니다 &amp;mdash; 극한엔 여전히 swap한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최후의 방어 &amp;mdash; OOM killer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;swap까지 썼는데도 메모리가 완전 고갈되면, 시스템 전체가 멈추기 직전이 된다. 이때 시스템 보존을 위해 프로세스를 강제 종료하는 커널의 최후 방어가 &lt;b&gt;OOM(Out-Of-Memory) killer&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    LOW[메모리 고갈] --&amp;gt; RECL[페이지 회수&amp;lt;br/&amp;gt;캐시/swap]
    RECL --&amp;gt;|부족| OOM[OOM killer 호출]
    OOM --&amp;gt; SCORE[각 프로세스 oom_score 계산]
    SCORE --&amp;gt; KILL[최고 점수 프로세스에 SIGKILL]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;oom_score&lt;/b&gt; &amp;asymp; RSS + 0.5 &amp;times; swap 사용량에 가중치.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/oom_score_adj&lt;/code&gt;&lt;/b&gt;(-1000 ~ 1000)로 점수 조정: &lt;code&gt;-1000&lt;/code&gt;은 OOM kill 대상에서 사실상 제외(systemd가 journald/dbus에 적용), &lt;code&gt;1000&lt;/code&gt;은 최우선 kill 대상.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OOM은 무작위가 아니다. RSS가 큰(메모리 많이 쓰는) 프로세스가 희생된다. DB처럼 큰 메모리 프로세스가 잡히기 쉬우므로 &lt;code&gt;oom_score_adj=-1000&lt;/code&gt;으로 보호하는 게 정상이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 진짜 부족한가
free -h&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;판단&lt;/b&gt;: &lt;code&gt;free&lt;/code&gt;가 낮아도 &lt;code&gt;available&lt;/code&gt;이 충분하면 정상(캐시가 차지한 것). 진짜 부족 신호는 &lt;code&gt;available&lt;/code&gt; 낮음 + 지속적 swap I/O.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;               total   used   free   shared  buff/cache  available
Mem:            3.8Gi   1.2Gi  512Mi  120Mi   2.1Gi       2.2Gi
Swap:           2.0Gi   0.0Gi  2.0Gi&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;vmstat 1 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;읽는 법&lt;/b&gt;: &lt;code&gt;si&lt;/code&gt;(swap-in)/&lt;code&gt;so&lt;/code&gt;(swap-out)가 계속 0보다 크면 스와핑 발생 &amp;rarr; 메모리 부족 신호.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;cat /proc/meminfo | head -20
# MemTotal/MemFree/MemAvailable/Cached/SwapTotal/SwapFree&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; swap 관리
swapon --show
sudo swapoff /dev/sdXN
cat /proc/sys/vm/swappiness       # 보통 60
sudo sysctl vm.swappiness=10      # 임시 변경
# 영구: /etc/sysctl.d/99-swappiness.conf&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; zram 설정(옵션)
sudo modprobe zram num_devices=1
sudo zramctl /dev/zram0 --algorithm zstd --size 2G
sudo mkswap /dev/zram0
sudo swapon -p 100 /dev/zram0   # 우선순위 높게(디스크 swap보다 먼저)
zramctl&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; OOM 점수: 누가 희생될까
for p in $(pgrep -f postgres); do
  echo &quot;$p: $(cat /proc/$p/oom_score)&quot;
done
# 중요 프로세스 보호 (예: PID 1234)
echo -1000 | sudo tee /proc/1234/oom_score_adj
# OOM 이력
journalctl -k | grep -i &quot;out of memory&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;cat /sys/kernel/mm/transparent_hugepage/enabled   # [always] madvise never
grep Huge /proc/meminfo&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;free가 적으면 메모리 부족&quot;&lt;/td&gt;
&lt;td&gt;틀림. &lt;code&gt;buff/cache&lt;/code&gt;가 차지한 것. &lt;b&gt;&lt;code&gt;available&lt;/code&gt;로 판단&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;swap은 쓰면 안 좋다, 0으로 두자&quot;&lt;/td&gt;
&lt;td&gt;swap 0이면 고갈 시 즉시 OOM kill. 안정성 위해 일정 swap 권장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;OOM은 무작위로 잡는다&quot;&lt;/td&gt;
&lt;td&gt;아니다. RSS 큰 프로세스 우선. DB가 희생되기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;swappiness=0이면 swap 안 한다&quot;&lt;/td&gt;
&lt;td&gt;&quot;안 하려 할 뿐&quot;이지 절대 금지 아님. 극한엔 여전히 swap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;malloc(1GB)&lt;/code&gt;하면 1GB 즉시 할당&quot;&lt;/td&gt;
&lt;td&gt;아니다. demand paging. 실제 접근 전까지 물리 페이지 안 줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;fork는 느리다(전체 복사)&quot;&lt;/td&gt;
&lt;td&gt;COW로 사실상 거의 안 복사. 매우 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;페이지 캐시는 낭비다&quot;&lt;/td&gt;
&lt;td&gt;아니다. 빈 RAM이 낭비. 캐시가 있어야 다음 읽기가 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 깊이&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;overcommit 정책&lt;/b&gt;: &lt;code&gt;vm.overcommit_memory&lt;/code&gt; &amp;mdash; 0(휴리스틱 기본), 1(항상 승인), 2(엄격, CommitLimit 초과 시 실패). overcommit=1이면 &lt;code&gt;malloc&lt;/code&gt;은 성공하지만 접근 시 OOM 발생 가능. 메모리 집약 서버는 2(엄격) 고려.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NUMA&lt;/b&gt;: 다소켓 서버는 CPU 소켓마다 가까운/먼 메모리가 있다. &lt;code&gt;numactl&lt;/code&gt;로 바인딩. 단일 소켓 시스템은 무관.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가상 메모리&lt;/b&gt;: 각 프로세스는 자기만의 거대 주소 공간(수십 TB)을 본다. 물리 RAM과 무관. 격리&amp;middot;보호&amp;middot;공유의 기반.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이지(4 KiB) + 페이지 테이블 + TLB&lt;/b&gt;. TLB 미스는 비싸므로 huge page로 커버리지를 넓힌다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;demand paging&lt;/b&gt;: 접근할 때 물리 페이지 할당. &lt;code&gt;malloc&lt;/code&gt;은 약속일 뿐이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이지 폴트&lt;/b&gt;: minor(메모리만), major(디스크 I/O), SIGSEGV(권한/매핑 없음).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COW&lt;/b&gt;: fork 시 페이지 공유, 쓰기 시 복사 &amp;rarr; fork가 빠르다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;페이지 캐시&lt;/b&gt;: 남는 RAM은 디스크 캐시. 판단은 &lt;code&gt;available&lt;/code&gt;(free 아님). &quot;free 적은 건 정상&quot;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;swap&lt;/b&gt;: RAM 부족 시 디스크로. zram은 RAM 내 압축 swap(더 빠름). swappiness로 조절.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OOM killer&lt;/b&gt;: 고갈 시 RSS 큰 프로세스를 SIGKILL. &lt;code&gt;oom_score_adj=-1000&lt;/code&gt;으로 보호.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;체감&lt;/b&gt;: 메모리 100ns vs SSD 150&amp;mu;s(1500배) vs HDD 10ms(10만 배). swap이 잦으면 이 차이만큼 느려진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가상 메모리가 없다면 어떤 3가지 문제가 생기는가?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;malloc(1GB)&lt;/code&gt; 직후와 그 1GB에 실제로 접근한 후의 물리 메모리 점유량 차이는? 왜?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;free&lt;/code&gt;에서 &lt;code&gt;free&lt;/code&gt;가 낮은데 시스템이 정상인 이유는? 무엇을 봐야 하는가?&lt;/li&gt;
&lt;li&gt;COW가 fork 비용을 어떻게 낮추는가?&lt;/li&gt;
&lt;li&gt;swap이 잦으면 왜 시스템이 느려지는가? (수치로)&lt;/li&gt;
&lt;li&gt;OOM killer가 RSS 큰 프로세스를 우선 잡는 이유는? 중요 프로세스는 어떻게 보호하는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/mm/concepts.html&quot;&gt;Memory Management Concepts &amp;mdash; Linux Kernel docs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/next/mm/page_tables.html&quot;&gt;Page Tables &amp;mdash; Linux Kernel docs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/admin-guide/blockdev/zram.html&quot;&gt;zram &amp;mdash; Linux Kernel docs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sre.google/static/pdf/rule-of-thumb-latency-numbers-letter.pdf&quot;&gt;Latency Numbers &amp;mdash; Google SRE&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man free&lt;/code&gt;, &lt;code&gt;man vmstat&lt;/code&gt;, &lt;code&gt;man swapon&lt;/code&gt;, &lt;code&gt;man proc(5)&lt;/code&gt;, &lt;code&gt;man mmap(2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>Linux</category>
      <category>memory</category>
      <category>OOM</category>
      <category>swap</category>
      <category>virtual-memory</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/4</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-memory#entry4comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:22:18 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 03. process</title>
      <link>https://technical-architect.tistory.com/entry/Linux-process</link>
      <description>&lt;h1&gt;수백 개 프로그램이 코어 몇 개를 나눠 쓰는 법 &amp;mdash; 프로세스와 스케줄러&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터를 켜면 웹 브라우저, 셸, SSH 서버, 시스템 모니터까지 수십 ~ 수백 개 프로그램이 &quot;동시에&quot; 돌아간다. 하지만 CPU 코어는 보통 4 ~ 16개뿐이다. 어떻게 가능할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정답은 &lt;b&gt;CPU를 매우 빠르게 번갈아 쓰기 때문&lt;/b&gt;이다. 1초에 수천 번 문맥(context)을 바꿔가며 각 프로그램에 조금씩 CPU를 나눠준다. 사람 눈엔 &quot;동시에&quot; 보이지만, 실은 빠른 교대 실행일 뿐이다. 이 교대를 가능하게 하는 두 추상 &amp;mdash; &lt;b&gt;프로세스&lt;/b&gt;(독립된 메모리 방을 가진 실행 단위)와 &lt;b&gt;스케줄러&lt;/b&gt;(수백 개 중 지금 누구를 CPU에 올릴지 고르는 커널의 판관) &amp;mdash; 가 이 글의 주제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘을 모르면 &quot;프로세스가 응답 없다&quot;, &quot;좀비가 쌓인다&quot;, &quot;CPU를 하나만 잡아먹는다&quot; 같은 문제를 진단할 수 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스와 스레드 &amp;mdash; 격리의 단위가 갈린다&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;프로세스&lt;/th&gt;
&lt;th&gt;스레드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;비유&lt;/td&gt;
&lt;td&gt;&lt;b&gt;독립된 단독 주택&lt;/b&gt; &amp;mdash; 자기 마당&amp;middot;부엌&amp;middot;창고&lt;/td&gt;
&lt;td&gt;&lt;b&gt;쉐어하우스의 방&lt;/b&gt; &amp;mdash; 거실&amp;middot;부엌&amp;middot;창고는 공유, 방만 내 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리&lt;/td&gt;
&lt;td&gt;자기만의 가상 주소 공간(남이 못 건드림)&lt;/td&gt;
&lt;td&gt;같은 프로세스의 주소 공간을 서로 공유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;안전&lt;/td&gt;
&lt;td&gt;하나가 죽어도 다른 건 안전&lt;/td&gt;
&lt;td&gt;한 스레드가 공유 메모리를 망치면 형제도 사고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;만들기 비용&lt;/td&gt;
&lt;td&gt;큼(주소 공간 복사)&lt;/td&gt;
&lt;td&gt;작음(스택만 추가)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비유는 &quot;메모리 격리&quot;만 잡는다. 스레드의 진짜 핵심 &amp;mdash; 스케줄링 단위가 커널의 &lt;code&gt;task&lt;/code&gt;라는 것 &amp;mdash; 은 비유 너머에 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확한 사실을 보자. 리눅스 커널은 프로세스와 스레드를 &lt;b&gt;모두 같은 &lt;code&gt;task_struct&lt;/code&gt; 구조체&lt;/b&gt;로 다룬다. 차이는 &lt;b&gt;주소 공간을 공유하느냐&lt;/b&gt;뿐이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로세스: 자기 &lt;code&gt;task_struct&lt;/code&gt; + 자기 &lt;code&gt;mm_struct&lt;/code&gt;(주소 공간)&lt;/li&gt;
&lt;li&gt;스레드: 자기 &lt;code&gt;task_struct&lt;/code&gt; + &lt;b&gt;형제의 &lt;code&gt;mm_struct&lt;/code&gt;를 공유&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입장에선 프로세스/스레드 구분. 커널 입장에선 둘 다 &quot;task&quot;다. 그래서 리눅스에서 스레드를 &quot;lightweight process&quot;라 부르기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성 비용의 체감: &lt;code&gt;fork()&lt;/code&gt;로 프로세스를 만드는 건 ~ 수백 &amp;mu;s(주소 공간 복사, 실제론 COW로 지연), &lt;code&gt;pthread_create()&lt;/code&gt;로 스레드를 만드는 건 ~ 수십 &amp;mu;s(스택만). 스레드가 한 자릿수 빠르다. 그래서 수천~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수만 동시 처리(웹 서버)에선 스레드나 이벤트 방식을 쓴다. 단, 격리&amp;middot;안전이 필요하면 프로세스가 낫다 &amp;mdash; 상황에 따라. (수치는 규모의 직관용이지 정확값은 시스템마다 다름.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;새 프로그램은 fork와 exec, 두 단계로만 띄운다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 많이 틀린다. &quot;bash가 &lt;code&gt;ls&lt;/code&gt;를 실행한다 = bash가 ls 코드를 직접 부른다&quot;라고 생각하기 쉽다. &lt;b&gt;아니다.&lt;/b&gt; 실제로는:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;bash가 &lt;b&gt;자기 복사본을 하나 더 만들고&lt;/b&gt;(fork)&lt;/li&gt;
&lt;li&gt;그 복사본이 &lt;b&gt;자기 자신을 &lt;code&gt;ls&lt;/code&gt;로 덮어쓴다&lt;/b&gt;(exec)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ls&lt;/code&gt;가 끝나면 복사본은 사라진다(exit)&lt;/li&gt;
&lt;li&gt;bash가 그 종료 코드를 회수한다(wait)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래의 bash는 그대로 살아있다. 이것이 리눅스에서 새 프로그램을 띄우는 &lt;b&gt;유일한&lt;/b&gt; 방법이다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;sequenceDiagram
    participant B as bash (PID 100)
    participant K as 커널
    participant L as ls (PID 101)
    B-&amp;gt;&amp;gt;K: fork()
    K--&amp;gt;&amp;gt;B: 새 PID 101 생성 (bash 복사본)
    K--&amp;gt;&amp;gt;L: PID 101 = bash 복제, 아직 bash 코드 실행 중
    L-&amp;gt;&amp;gt;K: exec(&quot;ls&quot;)
    K--&amp;gt;&amp;gt;L: PID 101의 메모리를 ls로 덮어씀 (PID는 그대로 101)
    L-&amp;gt;&amp;gt;L: ls 코드 실행, 결과 출력
    L-&amp;gt;&amp;gt;K: exit(0)
    K--&amp;gt;&amp;gt;B: SIGCHLD + 종료코드 대기
    B-&amp;gt;&amp;gt;K: wait()
    K--&amp;gt;&amp;gt;B: 종료코드 0 회수 &amp;rarr; PID 101 소멸&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;시스템 콜&lt;/th&gt;
&lt;th&gt;무슨 일&lt;/th&gt;
&lt;th&gt;PID 변화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;복제&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fork()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;부모의 거의 완전한 복사본 생성(COW)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;새 PID&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;교체&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exec()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;현재 프로세스의 코드/메모리를 새 프로그램으로 교체&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PID 그대로&lt;/b&gt;(중요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exit()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;프로세스 종료, 종료코드를 커널에 저장&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;회수&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wait()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;부모가 자식의 종료코드 회수&lt;/td&gt;
&lt;td&gt;사라짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 fork와 exec을 분리했는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 분리는 Unix의 천재적 설계다. 두 단계 &lt;b&gt;사이&lt;/b&gt;에 부모는 자식의 환경(파일 기술자, 환경변수, 권한)을 자유롭게 조작할 수 있다. 셸의 리다이렉션(&lt;code&gt;&amp;gt; file&lt;/code&gt;), 파이프(&lt;code&gt;|&lt;/code&gt;), 백그라운드(&lt;code&gt;&amp;amp;&lt;/code&gt;)가 전부 이 &quot;사이&quot;에서 일어난다. 단일 호출이었다면 이 유연성이 불가능했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좀비(Z) &amp;mdash; 회수되지 않은 종료자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식이 &lt;code&gt;exit()&lt;/code&gt; 했는데 부모가 &lt;code&gt;wait()&lt;/code&gt;을 안 하면? 자식은 &lt;b&gt;종료했지만 사라지지 못한 상태&lt;/b&gt;로 남는다. 이게 좀비다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔한 오해: &quot;좀비는 시스템을 위협하는 괴물&quot; &amp;mdash; 보통 아니다. 좀비는 메모리를 거의 안 쓴다(커널에 종료코드 한 줄만 보관). 진짜 문제는 &lt;b&gt;PID를 소모&lt;/b&gt;한다는 것. PID 한계(보통 32768~약 400만 개)에 도달하면 새 프로세스를 못 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀비 해법: 좀비를 직접 죽일 수 없다(SIGKILL도 안 통함). 부모가 &lt;code&gt;wait()&lt;/code&gt;하게 하거나, 부모를 죽여서 init(systemd, PID 1)이 회수하게 해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로세스 상태 다섯 가지 &amp;mdash; ps 출력이 말하는 것&lt;/h2&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;flowchart LR
    R((R&amp;lt;br/&amp;gt;실행/실행대기)) --&amp;gt;|이벤트 대기| S((S&amp;lt;br/&amp;gt;수면))
    S --&amp;gt;|이벤트 도착| R
    R --&amp;gt;|시그널/트레이스| T((T&amp;lt;br/&amp;gt;정지))
    T --&amp;gt;|SIGCONT| R
    R --&amp;gt;|exit| Z((Z&amp;lt;br/&amp;gt;좀비))
    Z --&amp;gt;|부모 wait| X((소멸))
    S -.디스크 I/O 대기.-&amp;gt; D((D&amp;lt;br/&amp;gt;불가중 수면))
    D -.I/O 완료.-&amp;gt; R&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기호&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;한 줄&lt;/th&gt;
&lt;th&gt;시그널로 빠져나올 수 있나&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;R&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;running/runnable&lt;/td&gt;
&lt;td&gt;CPU에서 돌고 있거나 돌 차례를 기다리는 중&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;S&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;interruptible sleep&lt;/td&gt;
&lt;td&gt;이벤트(타이머, I/O 완료) 기다리는 수면&lt;/td&gt;
&lt;td&gt;O(대부분 시그널)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;D&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;uninterruptible sleep&lt;/td&gt;
&lt;td&gt;디스크 I/O 등 &lt;b&gt;강제&lt;/b&gt; 수면&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X(SIGKILL도 안 통함)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;T&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;stopped&lt;/td&gt;
&lt;td&gt;시그널(Ctrl+Z)이나 디버거로 멈춤&lt;/td&gt;
&lt;td&gt;O(SIGCONT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Z&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;zombie&lt;/td&gt;
&lt;td&gt;종료했지만 부모가 아직 회수 안 함&lt;/td&gt;
&lt;td&gt;X(직접 처리 불가)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;D 상태의 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;D&lt;/code&gt; 상태 프로세스가 있으면 &lt;b&gt;root도 &lt;code&gt;kill -9&lt;/code&gt;로 못 죽인다.&lt;/b&gt; D는 커널이 디스크(또는 NFS) 응답을 기다리는 동안 프로세스를 &quot;절대 깨우지 마&quot; 모드로 놓은 것이다. 보통 디스크가 느리면 잠깐 D &amp;rarr; 해결이지만, &lt;b&gt;NFS 서버가 죽었는데 마운트가 안 풀리면&lt;/b&gt; D가 무한 지속 &amp;rarr; 서버 재부팅 외엔 답이 없다. D가 오래 지속되면 저장소 문제를 의심하라.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시그널 &amp;mdash; 비동기 알림과 &quot;왜 SIGTERM을 먼저 써야 하나&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시그널은 커널이나 다른 프로세스가 프로세스에 던지는 작은 비동기 알림이다. 프로세스는 받으면 하던 일을 멈추고 정해진 동작을 수행한다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시그널&lt;/th&gt;
&lt;th&gt;번호&lt;/th&gt;
&lt;th&gt;보통 어떻게 쓰나&lt;/th&gt;
&lt;th&gt;무시 가능?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGTERM&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;&quot;정리하고 종료해라&quot;(&lt;code&gt;kill&lt;/code&gt;의 기본)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGINT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&quot;중단해라&quot;(Ctrl+C)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGHUP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&quot;설정 다시 읽어라&quot;(데몬 관례)&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGKILL&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&quot;당장 죽어라&quot;&lt;/b&gt;(강제, 최후 수단)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X(무조건 죽음)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGSTOP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;&quot;멈춰라&quot;(Ctrl+Z와 유사)&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X(무조건 멈춤)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGCONT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;&quot;다시 돌아가라&quot;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;SIGCHLD&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;&quot;자식이 죽었어&quot;(부모에게)&lt;/td&gt;
&lt;td&gt;O(보통 무시)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위험한 습관: &quot;프로세스가 응답 없으면 &lt;code&gt;kill -9&lt;/code&gt;부터&quot; &amp;mdash; SIGKILL(9)은 프로세스에게 정리 기회를 안 준다. 열어둔 파일이 flush 안 되어 데이터가 깨지거나, 잠금(lock)이 안 풀려 다른 프로세스가 멈출 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;올바른 순서&lt;/b&gt;: &lt;code&gt;kill 1234&lt;/code&gt;(SIGTERM) &amp;rarr; 30초 대기 &amp;rarr; 그래도 안 죽으면 &lt;code&gt;kill -9 1234&lt;/code&gt;. 정리할 기회를 먼저 줘라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대 무시 불가능한 시그널은 SIGKILL(9)과 SIGSTOP(19)뿐. 그래서 프로세스가 미친 듯이 돌 때 최후의 수단으로 쓴다. 단, D 상태 프로세스에는 SIGKILL도 대기 행렬에 들어갈 뿐 즉시 효과가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;nice &amp;mdash; 우선순위인데 왜 반대일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로세스가 같은 우선권이면 급한 것과 안 급한 것을 구분 못 한다. 누구를 조금 더 자주 CPU에 올릴 것인가? 여기서 &lt;b&gt;nice&lt;/b&gt;가 우선순위를 조절한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 꼭 기억할 것: &lt;b&gt;nice 값은 &quot;작을수록 우선&quot;이다.&lt;/b&gt; 일반적 직관(클수록 좋다)과 &lt;b&gt;반대&lt;/b&gt;다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;nice&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-20&lt;/td&gt;
&lt;td&gt;&lt;b&gt;최우선&lt;/b&gt;(root만 설정 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;기본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+19&lt;/td&gt;
&lt;td&gt;최하위&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 사용자는 nice를 &lt;b&gt;높이기만&lt;/b&gt; 가능(우선순위 낮추기만). 자기 프로세스를 더 우선시하려면 root 권한이 필요하다. 범위는 -20 ~ +19.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 반대일까? &quot;nice&quot;라는 이름 자체가 &lt;b&gt;&quot;다른 사람에게 양보(nice)한다&quot;&lt;/b&gt;에서 왔다. nice 값이 높다 = 남에게 더 양보한다 = 내 우선순위가 낮아진다. 어원을 알면 헷갈리지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커널은 누구에게 CPU를 줄까 &amp;mdash; EEVDF가 CFS 자리를 빼앗은 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수백 개 프로세스가 코어 몇 개를 두고 줄을 선다. &quot;공평&quot;이란 대체 무엇일까. 2004년부터 커널은 CFS로 답했다 &amp;mdash; &quot;가장 오래 기다린 순&quot;을 고른다. 그런데 2023년(kernel 6.6) 그 자리를 &lt;b&gt;EEVDF&lt;/b&gt;가 빼앗았다. Rocky 10(커널 6.12)은 EEVDF를 쓴다. 왜 바뀌었을까.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EEVDF &amp;mdash; 짧게 끝나는 일을 먼저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EEVDF의 핵심은 한 줄로 간다: &lt;b&gt;공평함은 유지하되, &quot;짧게 끝나는 일(응답이 중요한 일)&quot;을 더 빨리 처리한다.&lt;/b&gt; CFS는 &quot;누가 더 기다렸나&quot;만 봤다면, EEVDF는 &quot;누가 더 기다렸나 + 이 작업이 얼마나 짧나&quot;를 함께 본다. 짧게 끝날 작업의 deadline이 더 빠르게 잡히고, 그게 먼저 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동은 세 단계(굳이 외우지 않아도 된다):&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 작업의 &lt;b&gt;lag&lt;/b&gt;(자기 몫 대비 덜/더 받은 정도) 계산&lt;/li&gt;
&lt;li&gt;lag &amp;ge; 0인 작업(eligible, 자기 차례 됨)만 후보&lt;/li&gt;
&lt;li&gt;후보 중 &lt;b&gt;deadline이 가장 빠른&lt;/b&gt; 작업 선택&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EEVDF는 비유로 가르치기 어렵다(은행 줄&amp;middot;급한 사람 같은 비유는 정반대 직관을 심는다). 수치와 작동으로 이해하라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실용적 의미&lt;/b&gt;: 데스크탑/대화형 작업(마우스 클릭, 오디오, 터미널 응답)이 더 빠르게 반응한다. 일반 서버 워크로드는 체감이 어렵다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역사 박스 &amp;mdash; CFS는 어떻게 작동했고, 왜 시대에 뒤처졌나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CFS(Completely Fair Scheduler)의 목표는 단순했다: &quot;모든 프로세스에게 CPU를 공평하게 나눠준다.&quot; 각 프로세스마다 &lt;b&gt;가상 시계(vruntime)&lt;/b&gt;를 하나씩 들려준다. CPU를 쓴 만큼 이 시계가 앞으로 간다. 스케줄러는 &lt;b&gt;시계가 가장 뒤처진 프로세스&lt;/b&gt;를 다음으로 실행한다. 그러면 자연스럽게 모두의 시계가 비슷하게 맞춰진다 &amp;rarr; 공평.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;프로세스 A: vruntime = 100   &amp;larr; 가장 작음, 다음 차례
프로세스 B: vruntime = 250
프로세스 C: vruntime = 300&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nice가 낮으면(-20) 시계가 느리게 감 &amp;rarr; 더 자주 차례가 옴 &amp;rarr; 우선&lt;/li&gt;
&lt;li&gt;nice가 높으면(+19) 시계가 빠르게 감 &amp;rarr; 덜 차례가 옴 &amp;rarr; 양보&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CFS는 &lt;b&gt;rbtree(레드블랙트리)&lt;/b&gt;로 vruntime 순 정렬 &amp;rarr; 가장 작은 것을 O(1)에 선택했다. 매우 효율적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 데스크탑&amp;middot;대화형 워크로드의 요구가 변했다. &quot;공평&quot;만으로는 터미널 입력 한 번에 반응이 느릴 수 있었다. EEVDF는 공평성(vruntime/lag)은 계승하되, 짧은 작업 우선(deadline)을 더해 대화형 반응을 끌어올렸다. 한 줄 요약: &lt;b&gt;CFS = &quot;가장 오래 기다린 순&quot;, EEVDF = &quot;차례가 된 것 중 가장 짧게 끝날 순&quot;.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;체감 수치 &amp;mdash; 왜 이 복잡함이 필요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 복잡함(프로세스 분리, 스케줄러, 컨텍스트 스위치)이 왜 필요한지 수치로 체감하자.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;연산&lt;/th&gt;
&lt;th&gt;대략 지연&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU 레지스터 연산&lt;/td&gt;
&lt;td&gt;~1 ns&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L1 캐시 접근&lt;/td&gt;
&lt;td&gt;~0.5 ns&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메인 메모리 접근&lt;/td&gt;
&lt;td&gt;~100 ns&lt;/td&gt;
&lt;td&gt;L1의 약 200배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;컨텍스트 스위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;~3-5 &amp;mu;s (3000-5000 ns)&lt;/td&gt;
&lt;td&gt;캐시를 어느 정도 무효화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSD 4KB 읽기&lt;/td&gt;
&lt;td&gt;~150 &amp;mu;s&lt;/td&gt;
&lt;td&gt;메모리의 1500배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디스크 탐색&lt;/td&gt;
&lt;td&gt;~10 ms (10,000,000 ns)&lt;/td&gt;
&lt;td&gt;메모리의 &lt;b&gt;10만 배&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: Jeff Dean/Peter Norvig &quot;Latency Numbers Every Programmer Should Know&quot; &amp;mdash; 규모의 직관용이지 정확값은 시스템마다 다름. 접근 2026-07-09.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미: 컨텍스트 스위치 한 번 = 메모리 접근 수십 번 비용. 그래서 같은 주소 공간을 공유해 캐시를 유지하는 스레드가 프로세스 스위치보다 싸다. 디스크 I/O는 메모리의 10만 배 &amp;mdash; 그래서 디스크 접근을 최소화하려고 페이지 캐시와 스와핑이 존재한다(04장).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;# Rocky 10
ps -ef --forest | head&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: PID 1이 systemd이고, 모든 프로세스가 systemd의 자손이라는 사실. fork의 연쇄가 보인다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;UID   PID  PPID  ... CMD
root    1     0  ... /usr/lib/systemd/systemd --system
root    2     0  ... [kthreadd]
...&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;ps -eo pid,ppid,stat,nice,comm --sort=-stat | head -15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: STAT의 첫 글자(R/S/D/T/Z)가 상태, 두 번째 글자(&lt;code&gt;s&lt;/code&gt;=세션 리더, &lt;code&gt;+&lt;/code&gt;=포그라운드)는 부가 정보.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Rocky 10
bash -c 'echo &quot;내 PID: $$&quot;; exec echo &quot;exec 후 같은 PID: $$&quot;'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: 두 PID가 &lt;b&gt;같다&lt;/b&gt;. &quot;exec은 PID를 바꾸지 않는다&quot;를 눈으로 확인.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# Rocky 10 (권장 순서)
kill 1234           # SIGTERM - 정리 기회 부여
# 30초 대기 후에도 살아있으면
kill -9 1234        # SIGKILL - 강제&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# Rocky 10
nice -n 10 ./heavy_job &amp;amp;     # 낮은 우선순위로 백그라운드
sudo renice -n -5 -p 1234    # 실행 중인 프로세스 우선순위 높이기(root)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;ps aux | awk '$8 ~ /^Z/'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀비가 보이면 부모를 찾아라: &lt;code&gt;ps -o ppid= -p &amp;lt;좀비PID&amp;gt;&lt;/code&gt;. 부모를 재시작하거나 죽여야 회수된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;uname -r   # 6.6+ = EEVDF, 4.x = CFS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky 10(6.12)은 EEVDF. 구형 자료의 &quot;CFS&quot; 설명은 역사적 배경으로 읽어라.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;nice가 높으면 더 우선이다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;반대&lt;/b&gt;. 낮을수록(-20 방향) 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;프로세스가 안 죽으면 kill -9부터&quot;&lt;/td&gt;
&lt;td&gt;위험. SIGTERM(15) 먼저, 정리 기회 줄 것&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;좀비는 위험한 괴물&quot;&lt;/td&gt;
&lt;td&gt;보통 아님. 메모리 거의 안 씀. PID 소모가 유일한 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;좀비를 kill -9로 없앤다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;불가능&lt;/b&gt;. 부모를 wait하게 하거나 부모를 죽여야&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;D 상태는 kill -9로 해결&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;안 됨&lt;/b&gt;. 디스크/NFS 문제 해결이 근본&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;bash가 ls를 직접 부른다&quot;&lt;/td&gt;
&lt;td&gt;아니다. bash가 fork하고 그 자식이 exec으로 ls가 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;스레드는 프로세스보다 무조건 좋다&quot;&lt;/td&gt;
&lt;td&gt;상황에 따라. 격리&amp;middot;안전이 필요하면 프로세스가 나음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;모든 CPU 코어를 100% 쓰면 빠르다&quot;&lt;/td&gt;
&lt;td&gt;컨텍스트 스위치 비용이 크면 병렬이 역효과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Rocky 10도 CFS를 쓴다&quot;&lt;/td&gt;
&lt;td&gt;아니다. EEVDF(kernel 6.6+). CFS는 구버전(4.x) 이야기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로세스&lt;/b&gt; = 실행 중인 프로그램 + 독립된 메모리 공간. &lt;b&gt;스레드&lt;/b&gt; = 같은 주소 공간을 공유하는 실행 흐름. 커널은 둘 다 &lt;code&gt;task_struct&lt;/code&gt;로 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;새 프로그램 실행&lt;/b&gt; = fork(복제) + exec(교체, PID 유지) + exit + wait. 이 분리 덕에 리다이렉션&amp;middot;파이프가 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 5종&lt;/b&gt;: R/S/D/T/Z. D(디스크 대기)와 Z(회수 안 된 종료자)는 시그널로 직접 해결 불가 &amp;mdash; 근본 원인을 봐야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시그널&lt;/b&gt;: SIGKILL(9), SIGSTOP(19)만 무시 불가. 나머지는 처리 가능. &quot;응답 없다&quot;고 해서 바로 -9 금지, TERM 먼저.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nice&lt;/b&gt;: -20~19, &lt;b&gt;낮을수록 우선&lt;/b&gt;. 일반 사용자는 높이기만(양보만) 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스케줄러&lt;/b&gt;: Rocky 10은 &lt;b&gt;EEVDF&lt;/b&gt; &amp;mdash; 공평함은 유지하되 짧게 끝나는 일(대화형)을 우선. CFS(vruntime 공평)는 구버전 이야기이자 EEVDF가 계승한 전신.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;bash가 &lt;code&gt;ls&lt;/code&gt;를 실행할 때 일어나는 일을 fork/exec/exit/wait로 설명하라.&lt;/li&gt;
&lt;li&gt;PID 100의 프로세스가 &lt;code&gt;exec()&lt;/code&gt;하면 PID는 어떻게 되는가? 왜?&lt;/li&gt;
&lt;li&gt;SIGTERM과 SIGKILL의 차이, 그리고 &quot;왜 TERM을 먼저 써야 하는가&quot;를 설명하라.&lt;/li&gt;
&lt;li&gt;nice 값 -20, 0, +19 중 가장 우선인 것은? 왜 반대인가?&lt;/li&gt;
&lt;li&gt;좀비 프로세스를 직접 kill할 수 있는가? 없다면 어떻게 해결하는가?&lt;/li&gt;
&lt;li&gt;D 상태 프로세스에 SIGKILL을 보내면 어떻게 되는가?&lt;/li&gt;
&lt;li&gt;CFS가 &quot;공평&quot;을 달성한 방식(vruntime)과 EEVDF가 거기에 더한 것을 비교하라.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/scheduler/sched-eevdf.html&quot;&gt;EEVDF Scheduler &amp;mdash; Linux Kernel docs&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lwn.net/Articles/925371/&quot;&gt;An EEVDF CPU scheduler for Linux &amp;mdash; LWN&lt;/a&gt; - 접근 2026-07-09 (CFS&amp;rarr;EEVDF 전환 배경)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/scheduler/sched-CFS.html&quot;&gt;CFS Documentation &amp;mdash; Linux Kernel docs&lt;/a&gt; - 접근 2026-07-09 (역사적 전신)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sre.google/static/pdf/rule-of-thumb-latency-numbers-letter.pdf&quot;&gt;Latency Numbers &amp;mdash; Google SRE&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man ps&lt;/code&gt;, &lt;code&gt;man kill&lt;/code&gt;, &lt;code&gt;man signal(7)&lt;/code&gt;, &lt;code&gt;man nice&lt;/code&gt;, &lt;code&gt;man fork(2)&lt;/code&gt;, &lt;code&gt;man execve(2)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>eevdf</category>
      <category>Linux</category>
      <category>Process</category>
      <category>Scheduler</category>
      <category>Signal</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/3</guid>
      <comments>https://technical-architect.tistory.com/entry/Linux-process#entry3comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:17:51 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 02. boot systemd</title>
      <link>https://technical-architect.tistory.com/entry/02-boot-systemd</link>
      <description>&lt;h1&gt;전원 ON부터 로그인까지 30초 &amp;mdash; 그 사이에 무슨 일이 일어나는가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 전원을 켰다. 30초~1분 뒤 SSH 로그인 프롬프트가 뜬다. 그 사이 30초 동안 무슨 일이 벌어지는가? 대부분 &quot;부팅되겠지&quot; 하고 넘긴다. 하지만 서비스가 부팅 후 자동으로 안 올라오거나, 부팅이 응급 모드(emergency)에 빠져 멈추거나, 부팅이 너무 느릴 때 &amp;mdash; 이 연쇄를 모르면 어디서 막혔는지조차 진단할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 전원 ON부터 로그인 프롬프트까지의 연쇄를 따라간다. 핵심 인물은 &lt;b&gt;systemd&lt;/b&gt;, PID 1 &amp;mdash; 모든 서비스의 출발점이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부팅은 여섯 단계 연쇄다&lt;/h2&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;flowchart LR
    P1[&quot;1. 펌웨어&amp;lt;br/&amp;gt;BIOS/UEFI + POST&quot;] --&amp;gt; P2[&quot;2. 부트로더&amp;lt;br/&amp;gt;GRUB2 2.12&quot;]
    P2 --&amp;gt; P3[&quot;3. 커널 로드&amp;lt;br/&amp;gt;vmlinuz + initramfs&quot;]
    P3 --&amp;gt; P4[&quot;4. systemd&amp;lt;br/&amp;gt;PID 1&quot;]
    P4 --&amp;gt; P5[&quot;5. target 도달&amp;lt;br/&amp;gt;default.target&quot;]
    P5 --&amp;gt; P6[&quot;6. 로그인 프롬프트&amp;lt;br/&amp;gt;getty / 디스플레이 매니저&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하자면 아침에 건물을 여는 절차와 비슷하다 &amp;mdash; 경비원 출근(펌웨어) &amp;rarr; 안내데스크가 관리실 열쇠 전달(GRUB2) &amp;rarr; 임시 관리실 개방(initramfs) &amp;rarr; 건물 관리자 출근(systemd) &amp;rarr; 층별 전원 병렬 점등(target) &amp;rarr; 로비 개방(로그인). 단, 건물은 순차적이지만 systemd는 &lt;b&gt;병렬&lt;/b&gt;로 서비스를 올린다(의존성만 충족하면 동시).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계를 좇아보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 펌웨어(BIOS/UEFI)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;POST(Power-On Self-Test)&lt;/b&gt; 로 하드웨어를 점검한다(CPU&amp;middot;RAM&amp;middot;저장소가 살아있나). 그리고 부트 디바이스를 찾아 부트로더를 메모리에 올린다. UEFI 시스템이면 ESP(EFI System Partition)의 &lt;code&gt;grubx64.efi&lt;/code&gt;를 실행한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. GRUB2(부트로더)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky 10은 &lt;b&gt;GRUB2 2.12&lt;/b&gt;. 디스크에서 커널 이미지(&lt;code&gt;vmlinuz-&amp;lt;버전&amp;gt;&lt;/code&gt;)와 initramfs를 메모리에 적재하고, 커널 명령줄 매개변수(&lt;code&gt;root=&lt;/code&gt;, &lt;code&gt;crashkernel=&lt;/code&gt;, &lt;code&gt;rd.lvm.lv=&lt;/code&gt; 등)를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트 엔트리는 과거의 거대한 &lt;code&gt;grub.cfg&lt;/code&gt; 스크립트가 아니라 &lt;b&gt;BLS(Boot Loader Specification)&lt;/b&gt; 방식 &amp;mdash; 커널마다 독립된 &lt;code&gt;.conf&lt;/code&gt; 스니펫으로 모듈화되어 있다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;title Rocky Linux (6.12.0-xxx.el10.x86_64)
version 6.12.0-xxx.el10.x86_64
linux /vmlinuz-6.12.0-xxx.el10.x86_64
initrd /initramfs-6.12.0-xxx.el10.x86_64.img
options root=/dev/mapper/rl-root ro crashkernel=2G-64G:256M rd.lvm.lv=rl/root&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BLS가 왜 생겼나: 구 GRUB은 &lt;code&gt;/boot/grub2/grub.cfg&lt;/code&gt; 하나에 모든 엔트리가 스크립트로 들어가 커널 업데이트마다 재생성이 필요했고 오류가 잦았다. BLS는 커널별 독립 스니펫으로 모듈화해 이 문제를 풀었다. &lt;code&gt;grubby&lt;/code&gt;가 관리한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 커널 + initramfs&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널이 메모리에서 initramfs(압축 CPIO 아카이브)를 tmpfs로 풀어 임시 루트를 만들고, 제어를 systemd로 넘긴다. 왜 굳이 임시 루트를 거치는지는 바로 다음 절에서.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. systemd(PID 1) &amp;rarr; target &amp;rarr; 로그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널이 시작하는 첫 사용자 공간 프로세스가 systemd다. systemd가 target(동기화 체크포인트)을 차례로 도달하며 서비스를 올리고, 마침내 로그인 서비스(getty/sshd/GDM)가 뜬다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flowchart TD
    SYS[&quot;systemd 시작&quot;] --&amp;gt; SYSINIT[&quot;sysinit.target&amp;lt;br/&amp;gt;(체크포인트)&quot;]
    SYSINIT --&amp;gt; BASIC[&quot;basic.target&amp;lt;br/&amp;gt;(체크포인트)&quot;]
    BASIC --&amp;gt; DEFAULT[&quot;default.target&amp;lt;br/&amp;gt;(multi-user 또는 graphical)&quot;]
    DEFAULT --&amp;gt; LOGIN[&quot;로그인 서비스&amp;lt;br/&amp;gt;getty/sshd/GDM&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sysinit.target&lt;/code&gt;, &lt;code&gt;basic.target&lt;/code&gt;은 체크포인트다. 여기서 실패하면 &lt;code&gt;emergency.target&lt;/code&gt;(응급 모드)로 빠진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커널은 왜 곧바로 루트를 마운트하지 못할까 &amp;mdash; initramfs의 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 의문이 든다: 커널이 임시 루트(initramfs)를 거치지 않고 곧바로 진짜 루트(&lt;code&gt;/&lt;/code&gt;)를 마운트하면 안 되나? 귀찮게 왜 임시 루트를 거치는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 사실: &lt;b&gt;커널은 만능이 아니다.&lt;/b&gt; 루트 파티션이 LVM이면, RAID면, 디스크 암호화면, NVMe/iSCSI 특수 드라이버가 필요하면 &amp;mdash; 커널이 &lt;b&gt;혼자서 마운트할 수 없다.&lt;/b&gt; 필요한 드라이버와 도구가 있어야 한다. 그 도구를 담은 임시 환경이 initramfs다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;커널이 initramfs를 tmpfs로 풀어 임시 루트 구성.&lt;/li&gt;
&lt;li&gt;initramfs가 LVM/RAID/암호화/특수 드라이버 초기화.&lt;/li&gt;
&lt;li&gt;진짜 루트 장치가 준비되면 &lt;code&gt;/sysroot&lt;/code&gt;에 마운트 &amp;rarr; &lt;code&gt;switch_root&lt;/code&gt;로 전환.&lt;/li&gt;
&lt;li&gt;제어를 systemd로 넘김.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구 용어 &lt;code&gt;initrd&lt;/code&gt;와 &lt;code&gt;initramfs&lt;/code&gt;가 혼용되지만 기술적 구분이 있다: &lt;b&gt;initrd&lt;/b&gt; = 블록 장치 기반 ramdisk(구식), &lt;b&gt;initramfs&lt;/b&gt; = tmpfs 기반 CPIO(현대). 현대 리눅스는 모두 initramfs. LVM/RAID/암호화 루트면 initramfs 없이는 부팅 자체가 불가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;systemd가 모든 것을 총괄한다 &amp;mdash; PID 1&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd는 RHEL/Rocky 7부터 기본 init 시스템이며, PID 1으로 모든 서비스를 관리한다. 핵심 설계 네 가지:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;병렬 서비스 시작&lt;/b&gt; &amp;mdash; 의존성만 충족하면 동시에 올린다 &amp;rarr; 부팅 속도 향상.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cgroup 기반 자원 관리&lt;/b&gt; &amp;mdash; 서비스별 CPU/메모리를 통제. Rocky 10은 &lt;b&gt;cgroup v2 강제&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;socket/D-bus activation&lt;/b&gt; &amp;mdash; 요청이 올 때까지 서비스를 안 띄운다 &amp;rarr; 메모리 절약. 첫 연결이 오면 그제야 서비스를 시작하고, 서비스가 죽어도 소켓이 요청을 큐에 담아둬 재시작 시 이어받는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통합 로그(journald)&lt;/b&gt; &amp;mdash; 커널&amp;middot;서비스&amp;middot;syslog 로그를 구조화해 한곳에 수집.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Rocky 10(systemd 257)의 변화&lt;/b&gt;: cgroup v1을 폐기하고 항상 v2를 쓴다. System V(&lt;code&gt;init.d&lt;/code&gt;) 서비스 스크립트는 deprecated(향후 제거 &amp;rarr; 네이티브 unit 파일 권장). 기본 설정 파일 위치가 &lt;code&gt;/etc/systemd/&lt;/code&gt;에서 &lt;b&gt;&lt;code&gt;/usr/lib/systemd/&lt;/code&gt;&lt;/b&gt;로 이동했다(&lt;code&gt;/etc/&lt;/code&gt;나 드롭인으로 오버라이드).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cgroup v2 강제의 함정: 구 모니터링 도구&amp;middot;컨테이너 런타임이 cgroup v1 경로(&lt;code&gt;/sys/fs/cgroup/memory/...&lt;/code&gt;)를 쓰면 동작하지 않는다. v2 통일 경로(&lt;code&gt;/sys/fs/cgroup/&lt;/code&gt;)를 써야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;unit과 target &amp;mdash; systemd가 세상을 보는 단위&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;systemd가 관리하는 모든 것은 &lt;b&gt;unit&lt;/b&gt;이다. 접미사로 종류가 갈린다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;unit 종류&lt;/th&gt;
&lt;th&gt;접미사&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;데몬/서비스 (예: &lt;code&gt;sshd.service&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;동기화 지점/상태 (예: &lt;code&gt;multi-user.target&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;socket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.socket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;소켓 activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cron 대체 스케줄러&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mount&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.mount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;파일시스템 마운트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;device&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;커널 장치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slice&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.slice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cgroup 계층 그룹&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;unit 파일 우선순위&lt;/b&gt;(높은 순): &lt;code&gt;/etc/systemd/system/&lt;/code&gt;(관리자 수정, 최우선) &amp;gt; &lt;code&gt;/run/systemd/system/&lt;/code&gt;(런타임) &amp;gt; &lt;code&gt;/usr/lib/systemd/system/&lt;/code&gt;(패키지 기본, Rocky 10부터 여기가 기본).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;target &amp;mdash; runlevel의 현대적 대체&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 runlevel은 숫자였다(0~6). systemd는 이름 있는 &lt;b&gt;target&lt;/b&gt;으로 대체했다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;target&lt;/th&gt;
&lt;th&gt;구 runlevel&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;poweroff.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rescue.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1, s, single&lt;/td&gt;
&lt;td&gt;단일 사용자(구조)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multi-user.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;텍스트 다중 사용자(서버 표준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;graphical.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;GUI 다중 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reboot.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;재부팅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;emergency.target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;최소 응급(루트만, 거의 아무것도 마운트 안 함)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;service unit 파일 구조&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# /usr/lib/systemd/system/sshd.service (예시 형태)
[Unit]
Description=OpenSSH server daemon
After=network.target auditd.service       # 의존 순서

[Service]
Type=notify                                # 시작 완료 신호 방식
ExecStart=/usr/sbin/sshd -D $OPTIONS       # 시작 명령
ExecReload=/bin/kill -HUP $MAINPID         # 재로드 명령
Restart=on-failure

[Install]
WantedBy=multi-user.target                 # enable 시 어느 target에 걸지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Type&lt;/b&gt;이 서비스 시작을 어떻게 판정하느냐를 결정한다: &lt;code&gt;simple&lt;/code&gt;(기본, 즉시 시작으로 간주), &lt;code&gt;forking&lt;/code&gt;(fork 후 부모가 종료되면 시작 완료), &lt;code&gt;notify&lt;/code&gt;(sd_notify로 준비 신호 받아야 시작 완료), &lt;code&gt;oneshot&lt;/code&gt;(일회성).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부팅 시간은 어디서 걸리나 &amp;mdash; systemd-analyze&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;systemd-analyze&lt;/code&gt;는 부팅 시간을 세 단계로 쪼개 보여준다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;대략 시간&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kernel&lt;/td&gt;
&lt;td&gt;1-5초&lt;/td&gt;
&lt;td&gt;커널 자체 초기화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;initrd&lt;/td&gt;
&lt;td&gt;2-10초&lt;/td&gt;
&lt;td&gt;initramfs에서 루트 마운트(LVM/암호화면 더 김)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;userspace&lt;/td&gt;
&lt;td&gt;5-30초&lt;/td&gt;
&lt;td&gt;systemd가 서비스 올리는 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Startup finished in 3.1s (kernel) + 5.5s (initrd) + 12.8s (userspace) = 21.4s
graphical.target reached after 12.5s in userspace&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;userspace가 길면 &lt;code&gt;systemd-analyze blame&lt;/code&gt;으로 어떤 서비스가 느린지 찾아라. 보통 NetworkManager(네트워크 대기)나 초기화가 무거운 DB가 병목이다. 부팅 최적화의 첫 단추는 &quot;어디서 걸리나&quot;를 아는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미검증(출처 인용). Rocky 10 VM에서 직접 실행 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; UEFI인가 BIOS인가
[ -d /sys/firmware/efi ] &amp;amp;&amp;amp; echo &quot;UEFI&quot; || echo &quot;BIOS/Legacy&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 어느 커널로 부팅하나 (BLS 엔트리)
ls /boot/loader/entries/
cat /boot/loader/entries/$(ls /boot/loader/entries/ | head -1)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;systemctl --version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: Rocky 10은 systemd 257.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;systemd 257 (257.x)
+PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP ...&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 서비스 운영
systemctl status sshd              # 현재 상태
sudo systemctl start sshd          # 즉시 시작
sudo systemctl enable sshd         # 부팅 시 자동 시작 등록
systemctl is-enabled sshd          # 자동 시작 여부&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;● sshd.service - OpenSSH server daemon
     Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled)
     Active: active (running) since ...
   Main PID: 1234 (sshd)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; target 관리
systemctl get-default              # 현재 기본 target (보통 multi-user.target)
sudo systemctl set-default graphical.target   # 기본 변경
sudo systemctl isolate rescue.target          # 즉시 전환(로그아웃 발생)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;# Rocky 10 &amp;mdash; 부팅 로그 분석 (어디서 느리거나 막혔나)
journalctl -b                      # 현재 부팅 전체 로그
systemd-analyze                    # 부팅 시간 요약
systemd-analyze blame | head       # 서비스별 시작 시간(느린 순)
systemd-analyze critical-chain     # 병목 서비스
journalctl -u sshd                 # 특정 서비스 로그
journalctl -p err -b               # 이번 부팅 에러만&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;enable&lt;/code&gt;하면 지금 시작된다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아니다&lt;/b&gt;. enable은 부팅 시 자동 시작 &lt;b&gt;등록만&lt;/b&gt;. 즉시 시작은 &lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;start&lt;/code&gt;하면 부팅 후에도 자동이다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아니다&lt;/b&gt;. start는 즉시 시작만. 재부팅 후엔 자동 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;unit 파일을 고치면 바로 적용된다&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;아니다&lt;/b&gt;. &lt;code&gt;systemctl daemon-reload&lt;/code&gt;로 systemd가 다시 읽게 해야&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;구 &lt;code&gt;/etc/sysconfig/network-scripts/&lt;/code&gt;를 써야 한다&quot;&lt;/td&gt;
&lt;td&gt;Rocky 10은 NetworkManager(nmcli) 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;code&gt;service sshd restart&lt;/code&gt;는 안 된다&quot;&lt;/td&gt;
&lt;td&gt;된다(호환 래퍼). 단 System V 스크립트 자체는 deprecated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;모든 서비스가 순차 시작한다&quot;&lt;/td&gt;
&lt;td&gt;아니다. systemd는 &lt;b&gt;병렬&lt;/b&gt; 시작(의존성만 충족하면 동시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;initramfs는 안 쓰는 게 빠르다&quot;&lt;/td&gt;
&lt;td&gt;LVM/RAID/암호화 루트면 initramfs 없이 부팅 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Rocky 10도 cgroup v1을 쓴다&quot;&lt;/td&gt;
&lt;td&gt;아니다. systemd 257이 cgroup v2 강제. v1 경로는 동작 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;부팅 6단계&lt;/b&gt;: 펌웨어(POST) &amp;rarr; GRUB2 &amp;rarr; 커널+initramfs &amp;rarr; &lt;b&gt;systemd(PID 1)&lt;/b&gt; &amp;rarr; target &amp;rarr; 로그인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;initramfs&lt;/b&gt; = 임시 루트. 커널이 혼자 마운트 못 하는 루트(LVM/RAID/암호화) 환경을 제공. 없으면 부팅 불가.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;systemd&lt;/b&gt; = PID 1. 병렬 시작 + cgroup 자원 관리 + socket activation + journald 통합 로그.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;unit/target&lt;/b&gt;: systemd의 관리 단위(unit)와 동기화 지점(target=runlevel 대체, multi-user=3, graphical=5).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;enable vs start&lt;/b&gt;: enable=부팅 자동 등록, start=즉시 시작. 보통 둘 다 필요. unit 수정 후엔 반드시 &lt;code&gt;daemon-reload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rocky 10 특이&lt;/b&gt;: systemd 257, &lt;b&gt;cgroup v2 강제&lt;/b&gt;(v1 폐기), System V deprecated, BLS 부트 엔트리, 설정 기본 위치 &lt;code&gt;/usr/lib/systemd/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부팅 병목&lt;/b&gt;은 &lt;code&gt;systemd-analyze blame&lt;/code&gt;으로 잡는다 &amp;mdash; userspace가 길면 서비스 탓.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;전원 ON부터 systemd가 시작되기까지의 단계를 순서대로 말하라.&lt;/li&gt;
&lt;li&gt;왜 커널이 곧바로 루트 파티션을 마운트하지 않고 initramfs를 거치는가?&lt;/li&gt;
&lt;li&gt;systemd의 PID는 왜 1인가? (03장의 fork 연쇄와 연결해 생각해 보라)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enable&lt;/code&gt;과 &lt;code&gt;start&lt;/code&gt;의 차이. 두 단계 모두 필요한 이유는?&lt;/li&gt;
&lt;li&gt;unit 파일을 수정한 뒤 반드시 해야 하는 명령은? 안 하면 어떻게 되는가?&lt;/li&gt;
&lt;li&gt;부팅이 느릴 때 어느 명령으로 병목 서비스를 찾는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rockylinux.org/10/books/admin_guide/10-boot/&quot;&gt;Rocky Linux 10 Admin Guide - System Startup&lt;/a&gt; - 접근 2026-07-09 (부팅 흐름 1차)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/documentation/red_hat_enterprise_linux/10/html/10.0_release_notes/new-features-and-enhancements&quot;&gt;RHEL 10 Release Notes - systemd 257&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/managing_monitoring_and_updating_the_kernel/configuring-kernel-command-line-parameters&quot;&gt;RHEL 10 - Configuring kernel command-line parameters&lt;/a&gt; - 접근 2026-07-09 (grubby/BLS)&lt;/li&gt;
&lt;li&gt;man page: &lt;code&gt;man systemctl&lt;/code&gt;, &lt;code&gt;man systemd.unit&lt;/code&gt;, &lt;code&gt;man systemd.target&lt;/code&gt;, &lt;code&gt;man journalctl&lt;/code&gt;, &lt;code&gt;man bootup(7)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>Boot</category>
      <category>grub</category>
      <category>initramfs</category>
      <category>Linux</category>
      <category>systemd</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/2</guid>
      <comments>https://technical-architect.tistory.com/entry/02-boot-systemd#entry2comment</comments>
      <pubDate>Thu, 9 Jul 2026 14:08:52 +0900</pubDate>
    </item>
    <item>
      <title>Linux - 01. overview</title>
      <link>https://technical-architect.tistory.com/entry/01-%EB%A6%AC%EB%88%85%EC%8A%A4-%EA%B0%9C%EC%9A%94</link>
      <description>&lt;h1&gt;&quot;리눅스&quot;라 부르는 것의 반은 리눅스가 아니다&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RHEL(Red Hat Enterprise Linux)은 연 수백만 원짜리 유료 구독이다. 그런데 Rocky Linux는 RHEL과 거의 똑같이 생겼으면서 무료다. 같은 소스에서 나왔는데 한쪽만 돈을 받는다. 처음 보면 모순처럼 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모순은 &quot;리눅스&quot;라는 단어가 숨기고 있는 것을 한 겹 벗기면 사라진다. 이 글은 그 한 겹을 다룬다 &amp;mdash; 정확히 &quot;리눅스&quot;가 가리키는 것이 무엇인지, 그리고 왜 같은 코드에서 유료와 무료가 동시에 나올 수 있는지.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;리눅스&quot;는 사실 커널이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 말해 &lt;b&gt;&quot;리눅스(Linux)&quot;는 커널 그 자체&lt;/b&gt;를 가리킨다. 우리가 &quot;리눅스 OS&quot;라 부르는 것은 사실 커널 하나가 아니라, 커널 + 사용자 공간 도구 + 패키지 관리자를 한 묶음으로 포장한 &lt;b&gt;배포판(distribution)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 커널 단독으로는 부팅조차 안 된다. 셸도, 명령어도, 라이브러리도 없기 때문이다. 그래서 커널만으로는 쓸 수 없고, GNU 도구(bash, coreutils, gcc)와 결합해야 비로소 운영체제가 된다. FSF가 &quot;GNU/Linux&quot;라 부르자고 고집하는 이유가 여기에 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;커널&lt;/th&gt;
&lt;th&gt;배포판&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;비유&lt;/td&gt;
&lt;td&gt;자동차의 &lt;b&gt;엔진&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;완성된 &lt;b&gt;자동차 모델&lt;/b&gt;(세단, SUV, 트럭)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단독으로&lt;/td&gt;
&lt;td&gt;의미 없음 &amp;mdash; 엔진만으론 못 굴러&lt;/td&gt;
&lt;td&gt;바로 운전 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;같은 것으로 다양화&lt;/td&gt;
&lt;td&gt;같은 엔진 &amp;rarr; 여러 차종&lt;/td&gt;
&lt;td&gt;같은 커널 &amp;rarr; Rocky, Ubuntu, RHEL&amp;hellip;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비유는 &quot;핵심 부품 vs 완성품&quot; 관계만 잡는다. 자동차는 물리적 1대라 복제 불가지만 배포판은 무한 복제되고, 자동차 엔진 교체는 매우 어렵지만 커널은 비교적 독립적으로 교체&amp;middot;업그레이드된다. 정확한 구조는 아래에서.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분을 못 하면 뒷글의 어휘가 전부 흔들린다. &quot;리눅스에서 dnf가 안 돼요&quot;라는 말은 정확히는 &quot;RHEL 계열 배포판에서&quot;라는 뜻이지, 커널 수준의 이야기가 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커널 하나인데 왜 배포판은 수백 개일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;리눅스&quot; 커널은 사실상 하나다. 그런데 Rocky, Ubuntu, Debian, Arch&amp;hellip; 배포판은 수백 개다. 같은 엔진으로 만든 여러 자동차 모델이라 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람이 &quot;배포판 = 패키지 관리자&quot;로 요약하지만, 그건 표면이다. 진짜 차이는 세 가지에서 온다: &lt;b&gt;패키지 관리 방식&lt;/b&gt;, &lt;b&gt;릴리스 철학&lt;/b&gt;, &lt;b&gt;누구를 위한 배포판인가&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;flowchart TD
    ROOT[독립 루트] --&amp;gt; RH[Red Hat 계열&amp;lt;br/&amp;gt;RPM / dnf]
    ROOT --&amp;gt; DEB[Debian 계열&amp;lt;br/&amp;gt;.deb / apt]
    ROOT --&amp;gt; ARCH[Arch 계열&amp;lt;br/&amp;gt;pacman, 롤링]
    ROOT --&amp;gt; SUSE[SUSE 계열&amp;lt;br/&amp;gt;RPM / zypper]

    RH --&amp;gt; FED[Fedora &amp;mdash; 상류, 최신]
    FED --&amp;gt; RHEL[RHEL &amp;mdash; 상용, 장기지원]
    RHEL --&amp;gt; CST[CentOS Stream &amp;mdash; 개발 브랜치]
    RHEL --&amp;gt; RL[Rocky Linux]
    RHEL --&amp;gt; AL[AlmaLinux]

    DEB --&amp;gt; DEB1[Debian &amp;mdash; 안정 중심]
    DEB1 --&amp;gt; UB[Ubuntu]&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;가문&lt;/th&gt;
&lt;th&gt;패키지 관리자&lt;/th&gt;
&lt;th&gt;패키지 형식&lt;/th&gt;
&lt;th&gt;대표&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Red Hat&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dnf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.rpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RHEL, &lt;b&gt;Rocky&lt;/b&gt;, Alma, Fedora&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debian&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apt&lt;/code&gt;/&lt;code&gt;dpkg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.deb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debian, Ubuntu, Mint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pacman&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;Arch, Manjaro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUSE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zypper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.rpm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;openSUSE, SLES&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 관리자는 표면이다 &amp;mdash; 릴리스 철학이 진짜 갈린다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 RPM을 써도 Fedora와 RHEL은 완전히 다른 동물이다. 차이는 &lt;b&gt;얼마나 자주, 얼마나 오래&lt;/b&gt; 갱신하느냐에 있다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;갱신 속도&lt;/th&gt;
&lt;th&gt;지원 기간&lt;/th&gt;
&lt;th&gt;무엇을 포기하는가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;롤링(Arch, Tumbleweed)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;계속 최신&lt;/td&gt;
&lt;td&gt;없음(계속 굴리면 영원)&lt;/td&gt;
&lt;td&gt;안정성. 한 번 업데이트가 시스템을 깰 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;포인트 릴리스(Debian, Ubuntu)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6개월~2년 주기&lt;/td&gt;
&lt;td&gt;LTS 5년&lt;/td&gt;
&lt;td&gt;최신 기능. 검증된 버전에 머무름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;엔터프라이즈(RHEL, Rocky)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;보수적, 큰 점프만&lt;/td&gt;
&lt;td&gt;&lt;b&gt;10년&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최신성. 안정&amp;middot;패치 SLA가 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &quot;안정&quot;이란 &quot;안 죽는다&quot;가 아니라 &lt;b&gt;&quot;API가 안 바뀐다&quot;&lt;/b&gt;다. RHEL/Rocky는 10년 동안 같은 커널&amp;middot;라이브러리 주버전을 유지하며, 보안 패치만 역이식(backport)해 넣는다. 그래서 5년 전에 짠 서비스가 업그레이드 없이 계속 돈다. 대가는 &amp;mdash; 커널은 6.12 쓰면서도 사용자 공간 도구는 구버전에 머무를 수 있다는 모순(이건 Rocky 10의 함정이기도 하다, 08장).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상류에서 하류로 &amp;mdash; 코드는 어디서 흐르는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Red Hat 계열은 &lt;b&gt;단방향 파이프라인&lt;/b&gt;이다. 위쪽에서 실험하고, 아래로 갈수록 굳어진다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart LR
    UP[&quot;상류(upstream)&amp;lt;br/&amp;gt;커널&amp;middot;프로젝트 원본&quot;] --&amp;gt; FED[&quot;Fedora&amp;lt;br/&amp;gt;실험장, 최신&quot;]
    FED --&amp;gt; CST[&quot;CentOS Stream&amp;lt;br/&amp;gt;RHEL의 다음 버전 양산&quot;]
    CST --&amp;gt; RHEL[&quot;RHEL&amp;lt;br/&amp;gt;안정화, 상용&quot;]
    RHEL --&amp;gt; RL[&quot;Rocky / Alma&amp;lt;br/&amp;gt;RHEL 재구축(무료)&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Fedora&lt;/b&gt;가 새 기능을 시험한다(커널&amp;middot;systemd&amp;middot;GNOME 최신).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CentOS Stream&lt;/b&gt;이 RHEL의 &lt;i&gt;다음&lt;/i&gt; 마일스톤을 굴리며(RHEL의 상류 겸 개발 브랜치).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RHEL&lt;/b&gt;이 그 중 검증된 것을 10년 지원 상용으로 얼린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rocky/Alma&lt;/b&gt;가 그 얼려진 RHEL을 소스에서 그대로 다시 묶는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 같은 코드가 흐르되, &lt;i&gt;어느 단계&lt;/i&gt;에서 끊어 가져오느냐가 배포판의 정체성이다. 이 흐름을 모르면 &quot;Rocky가 RHEL보다 빠르냐 느리냐&quot; 같은 질문에 헷갈린다(답: 같다. 같은 RHEL 소스에서 나왔으니).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이 저장소는 Rocky 10인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습엔 안정성도 필요하지만, &lt;b&gt;너무 구버전이면 안 된다.&lt;/b&gt; Ubuntu LTS는 편하지만 커널&amp;middot;도구가 RHEL 계열과 달라 실무(특히 국내 서버 시장의 RHEL 환경)와 괴리된다. 반대로 Arch는 너무 날카롭다. Rocky는 &lt;b&gt;RHEL과 바이너리 호환이면서 무료&lt;/b&gt; &amp;mdash; 엔터프라이즈 급 안정성을 공짜로 쓸 수 있는 유일한 선택지다. 그래서 이 저장소 전체의 실습 기준이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 RHEL은 유료이고 Rocky는 무료인가 &amp;mdash; GPL의 역설 풀기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 처음의 모순으로 돌아가자. 답은 리눅스 커널의 라이선스인 &lt;b&gt;GPL&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 오해 하나: &quot;리눅스는 무료&quot;라고들 한다. &lt;b&gt;틀렸다.&lt;/b&gt; 리눅스는 무료(free of charge)가 아니라 &lt;b&gt;자유(freedom)&lt;/b&gt;다. GPL은 비용이 아니라 &lt;b&gt;소스코드 공개 의무&lt;/b&gt;를 다룬다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPL은 판매를 &lt;b&gt;금지하지 않는다.&lt;/b&gt; 팔아도 된다.&lt;/li&gt;
&lt;li&gt;대신 수정&amp;middot;재배포할 때 &lt;b&gt;소스코드를 공개&lt;/b&gt;해야 한다(copyleft).&lt;/li&gt;
&lt;li&gt;그 공개된 소스로 누구나 똑같이 재구축할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RHEL은 GPL을 지키면서도 돈을 받는다. 돈을 받는 것은 코드가 아니라 &lt;b&gt;지원&amp;middot;인증&amp;middot;안정성 보증&amp;middot;패치 SLA&lt;/b&gt; 때문이다. 그리고 RHEL이 공개한 GPL 소스를 그대로 받아 RHEL과 바이너리 호환되게 다시 묶은 무료 배포판이 &lt;b&gt;Rocky Linux, AlmaLinux&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모순이 아니라 GPL이 설계한 대로 작동하는 것이다. &quot;자유&quot;는 &quot;공짜&quot;를 뜻하지 않는다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;현재 커널 라이선스&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPL-2.0-only&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;핵심&lt;/td&gt;
&lt;td&gt;copyleft &amp;mdash; 2차 저작물도 동일 라이선스로 공개 의무&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;허용&lt;/td&gt;
&lt;td&gt;자유 복제&amp;middot;수정&amp;middot;재배포&amp;middot;상업 이용(판매 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의무&lt;/td&gt;
&lt;td&gt;수정&amp;middot;재배포 시 소스코드 공개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;copyleft가 실제로 강제하는 것과 아닌 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;copyleft가 전염된다(viral)&quot;는 표현이 자주 나오지만, 오해를 낳는다. GPL이 강제하는 것은 정확히 &lt;b&gt;&quot;배포(distribution)할 때&quot;&lt;/b&gt;뿐이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;강제한다&lt;/b&gt;: GPL 코드를 가져다 수정한 뒤 &lt;i&gt;남에게 배포&lt;/i&gt;하면, 그 수정본의 소스도 GPL로 공개.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강제하지 않는다&lt;/b&gt;: 회사 안에서만 쓰고 밖으로 내보내지 않으면? 공개 의무 &lt;b&gt;없음&lt;/b&gt;. GPL 코드를 고쳐서 사내 서버에만 굴려도 소스를 공개할 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;강제하지 않는다&lt;/b&gt;: GPL 코드와 별도 프로세스로 통신(예: 명령행으로 GPL 도구를 호출)하는 자기 코드는 GPL에 물들지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 GPL은 &quot;전염&quot;이 아니라 &quot;배포 조건&quot;이다. 이 선을 알아야 &quot;회사에서 GPL 쓰면 소스 다 공개해야 하나?&quot; 같은 공포가 사라진다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 커널은 영원히 GPL-2.0-only인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널 라이선스 표기에 &quot;only&quot;가 붙는 이유가 있다. 2007년 GPLv3가 나왔지만, 리눅스 커널은 &lt;b&gt;이전할 수 없다.&lt;/b&gt; 이유는 단순하다 &amp;mdash; 수만 명의 기여자 각각이 자기 코드의 저작권을 갖고 있어, 전원 동의 없이는 라이선스를 바꿀 수 없다. 일부 기여자는 이미 사망했고, 연락이 안 닿는 경우도 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게다가 Torvalds 본인이 &quot;GPLv2로 충분하다&quot;며 이전에 부정적이다. 그래서 커널은 GPLv2에 영원히 고정되고, 새 라이선스 논쟁이 나와도 &quot;GPL-2.0-only&quot; 표기는 변하지 않는다. 이것은 결함이 아니라, 커널이 거대한 협업 결과물이기에 치르는 비용이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;syscall 예외 &amp;mdash; 왜 독점 프로그램이 리눅스에서 돌아는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL은 2차물에 같은 라이선스를 강제한다. 그렇다면 리눅스에서 돌아가는 모든 상용 프로그램(Oracle DB, 게임, 독점 드라이버)도 소스를 공개해야 하는 게 아닌가? 그렇지 않다. 커널은 &lt;b&gt;syscall 경계&lt;/b&gt;를 예외로 둔다 &amp;mdash; 시스템 콜로 커널을 이용하는 응용은 GPL 강제 대상이 아니다(커널 소스 &lt;code&gt;COPYING&lt;/code&gt; 파일에 명시).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용과 커널은 &quot;한 프로그램&quot;이 아니라 &quot;정의된 인터페이스(syscall ABI)로 대화하는 별개의 프로그램&quot;으로 본다. 응용은 GPL 영향을 받지 않고, 커널 자체만 GPL로 보호되는 구조다. 이 예외가 없었다면 상용 생태계가 리눅스를 외렸을 것이다. 리눅스 생태계가 폭발적으로 성장한 핵심 설계 결정 중 하나다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;30년이 한 줄로 &amp;mdash; 잠깐의 역사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 것의 시작은 1991년이다. 핵심 날짜만 짚는다.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;flowchart LR
    A[&quot;1983&amp;lt;br/&amp;gt;GNU 프로젝트&amp;lt;br/&amp;gt;Stallman&quot;] --&amp;gt; B[&quot;1991-08-25&amp;lt;br/&amp;gt;Torvalds 발표&amp;lt;br/&amp;gt;comp.os.minix&quot;]
    B --&amp;gt; C[&quot;1992&amp;lt;br/&amp;gt;GPLv2 도입&quot;]
    C --&amp;gt; D[&quot;1994-03-14&amp;lt;br/&amp;gt;1.0 정식&quot;]
    D --&amp;gt; F[&quot;현재&amp;lt;br/&amp;gt;GPL-2.0-only&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1991-08-25&lt;/b&gt;: Linus Torvalds(21세, 헬싱키대)가 &lt;code&gt;comp.os.minix&lt;/code&gt;에 &quot;취미 프로젝트&quot;라며 올린 글. 리눅스의 공식 생일이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1992&lt;/b&gt;: 처음엔 상업적 사용을 금지하는 자체 라이선스였다가 &lt;b&gt;GPLv2&lt;/b&gt;로 바꿨다. Torvalds는 훗날 이를 &quot;내 인생 최고의 결정&quot;이라 불렀다. 이 결정이 없었다면 RHEL/Rocky 이야기도 없었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1994-03-14&lt;/b&gt;: 1.0 정식 릴리스(약 17만 줄). 오늘날 커널은 수천만 줄, 전 세계 수천 명이 하루에도 수백 커밋을 넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 1991년에 리눅스가 나왔는가. 당시 상용 유닉스(Sun Solaris, HP-UX, IBM AIX)는 하드웨어 회사마다 비싸게 팔아 학생이나 소규모 연구자는 엄두를 못 냈다. 마침 GNU 프로젝트(Stallman, 1983)가 셸&amp;middot;컴파일러&amp;middot;명령어는 다 만들어놓고 &lt;b&gt;커널(Hurd)만 못 만들고 있었다.&lt;/b&gt; Torvalds가 만든 커널이 그 빈자리를 채웠다. &quot;GNU 도구 + 리눅스 커널&quot;이 자연스럽게 결합해 오늘날의 리눅스 OS가 됐다. 이 역사가 &quot;왜 배포판이 커널+도구 묶음인가&quot;의 답이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;취미 프로젝트가 어떻게 상용 유닉스를 이겼는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1991년 &quot;취미 프로젝트&quot;가 2020년대에 슈퍼컴퓨터 TOP500의 100%, 안드로이드폰 전체, 클라우드 인프라 대부분을 지배하게 된 과정은 한 사람의 천재로 설명되지 않는다. 핵심은 &lt;b&gt;개발 모델&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리눅스 커널은 &lt;b&gt;계층적 메인테이너(hierarchical maintainer) 구조&lt;/b&gt;로 굴러간다. Torvalds가 최종 병합권을 쥐고, 그 아래 서브시스템별 중간 메인테이너(lieutenant)들이 각 영역(파일시스템&amp;middot;네트워크&amp;middot;스케줄러 등)의 품질을 검수해 Torvalds에게 올린다. 한 명이 전부 읽는 게 아니라, &lt;b&gt;신뢰의 계단&lt;/b&gt;을 통해 수천 명의 기여를 걸러 올린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 &lt;b&gt;&quot;충분한 눈이 있으면 모든 버그는 얕다&quot;(Linus's law)&lt;/b&gt; &amp;mdash; Eric Raymond가 명명한 통찰이 작동한다. 소스가 공개되어 누구나 볼 수 있으면, 버그가 전 세계 수만 명의 눈에 빠르게 노출되어 잡힌다. 닫힌 상용 유닉스가 수십 년 못 잡은 버그를 리눅스는 몇 주 만에 잡는 일이 흔해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 개 &amp;mdash; 계층적 검수 + 열린 시야 &amp;mdash; 가 &quot;혼자 만든 커널&quot;을 &quot;인류가 함께 만드는 인프라&quot;로 바꿨다. 1.0(1994, 약 17만 줄)이 오늘날 커널 6.x(수천만 줄, 전 세계 수천 명 기여, 하루 수백 커밋)이 될 수 있었던 이유다. 단일 기업이 이 규모를 따라잡는 건 불가능해졌고, 그게 곧 상용 유닉스의 패배 본질이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CentOS는 어디로 갔나 &amp;mdash; Rocky가 그 자리를 이었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rocky가 왜 존재하는지 이해하려면 CentOS 이야기를 짚고 넘어가야 한다. 과거 &lt;b&gt;구 CentOS&lt;/b&gt;는 RHEL의 무료 바이너리 클론이었다. 2021년 Red Hat이 CentOS를 종료시키고 &lt;b&gt;CentOS Stream&lt;/b&gt;(RHEL의 상류 개발 브랜치)으로 방향을 바꿨다. 그 빈자리 &amp;mdash; &quot;무료 RHEL 호환 배포판&quot; &amp;mdash; 를 메운 것이 Rocky Linux와 AlmaLinux다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Fedora&lt;/b&gt;: Red Hat 후원 커뮤니티 상류. 최신 기능 선별. RHEL의 실험장(proving ground).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RHEL&lt;/b&gt;: Fedora에서 파생된 상용 엔터프라이즈 OS. 약 10년 장기 지원.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CentOS Stream&lt;/b&gt;: RHEL의 상류&amp;middot;개발 브랜치. (구 CentOS와 다르다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rocky Linux&lt;/b&gt;: 2021년 Gregory Kurtzer(CentOS 공동 창립자)가 시작한 RHEL 바이너리 호환 무료 재구축. 이름은 고인이 된 공동 창립자 &lt;b&gt;Rocky McGaugh&lt;/b&gt;에게서. &lt;b&gt;이 저장소의 실습 환경이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2023년, Red Hat이 소스를 막았다 &amp;mdash; Rocky는 어떻게 살아남았나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽으면 한 가지 의문이 든다. &quot;Rocky가 RHEL 소스를 그대로 받아 재구축한다고 했는데, Red Hat이 그 소스를 안 주면?&quot; 그 일이 &lt;b&gt;실제로 일어났다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023년 6월, Red Hat은 RHEL 소스 코드의 &lt;b&gt;공개 재배포를 중단&lt;/b&gt;했다. 더 이상 누구나 ftp에서 SRPM(source RPM)을 받아갈 수 없게 했다. RHEL 소스는 (1) 유료 고객에게만 Red Hat Customer Portal로, (2) 일반에는 &lt;b&gt;CentOS Stream&lt;/b&gt; 경로로만 제공한다고 발표했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표면적 이유는 &quot;무료 재구축 배포판이 Red Hat의 상업 모델을 무임승차한다&quot;는 불만이었다. 하지만 GPL의 관점에서 보면 미묘하다 &amp;mdash; GPL은 소스 &lt;i&gt;공개&lt;/i&gt;를 의무로 하지만, &lt;b&gt;누구에게나&lt;/b&gt; 공개하라고는 명시하지 않는다. &quot;배포 대상(고객)에게는 소스를 줘야 한다&quot;는 게 GPL의 요지다. Red Hat은 고객에게는 소스를 주되, 재배포를 사실상 막는(고객 계약상) 방식으로 copyleft의 글자를 지키면서 의도는 희석했다. 커뮤니티는 이를 &quot;GPL 정신 위반&quot;과 &quot;GPL 글자 준수&quot; 사이의 회색지대로 받아들였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Rocky와 AlmaLinux는 죽지 않았다.&lt;/b&gt; 그들은 방식을 바꿨다 &amp;mdash; CentOS Stream의 소스(공개됨)를 뼈대로 삼고, Red Hat이 공개하는 패치와 공개 채널의 소스를 조합해 RHEL과 바이너리 호환을 유지한다. 완전히 동일한 경로는 아니지만, 실사용에 문제없는 호환성을 유지하고 있다(2026년 현재 Rocky 10이 정상 출시된 것이 증거).&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사건이 시사하는 것: &quot;무료&quot; RHEL 재구축은 Red Hat의 자비가 아니라 GPL이라는 &lt;b&gt;계약&lt;/b&gt; 위에 서 있다. 계약의 글자를 지키면서 의도를 비틀 수는 있어도, 완전히 없앨 수는 없다. 리눅스 생태계의 자유는 도덕이 아니라 라이선스라는 법적 구조로 보장된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rocky 10에서 직접 확인하기&lt;/h2&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Rocky 10
cat /etc/redhat-release&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: &quot;Red Quartz&quot;라는 코드명과 버전 10.x. Rocky가 Red Hat 계열임을 시스템이 스스로 선언한다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;Rocky Linux release 10.x (Red Quartz)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;uname -r&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: 6.12.x. 이 커널 버전으로 EEVDF 스케줄러 사용 여부를 판별한다(03장).&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;6.12.0-xxx.el10.x86_64&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;cat /etc/os-release&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인할 것&lt;/b&gt;: &lt;code&gt;ID_LIKE=&quot;rhel centos fedora&quot;&lt;/code&gt; 줄. Rocky가 RHEL 계열임을 스스로 선언하며, 패키지 호환성 판단에 쓰인다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;NAME=&quot;Rocky Linux&quot;
VERSION=&quot;10.x (Red Quartz)&quot;
ID=&quot;rocky&quot;
ID_LIKE=&quot;rhel centos fedora&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;흔히 묻는 것, 흔히 틀리는 것&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;오해&lt;/th&gt;
&lt;th&gt;정정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&quot;리눅스 = 운영체제&quot;&lt;/td&gt;
&lt;td&gt;리눅스는 &lt;b&gt;커널&lt;/b&gt;이다. OS가 되려면 배포판(커널+GNU 도구+패키지관리자)이 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;리눅스는 무료니까 RHEL이 유료인 건 모순&quot;&lt;/td&gt;
&lt;td&gt;GPL은 자유(freedom)지 무료(free)가 아님. RHEL은 GPL 소스를 공개하면서 유료 판매 정상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;CentOS = Rocky&quot;&lt;/td&gt;
&lt;td&gt;구 CentOS는 종료. CentOS Stream은 RHEL 상류(개발). Rocky/Alma가 구 CentOS 자리 계승&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;GPL은 무조건 무료 배포 강제&quot;&lt;/td&gt;
&lt;td&gt;아니다. 팔아도 됨. 단 소스 공개 의무&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;GPL 코드를 쓰면 내 코드도 무조건 공개해야&quot;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;배포할 때만&lt;/b&gt;. 사내에서만 쓰고 밖으로 안 내보내면 공개 의무 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;GPL은 전염성(viral)이다&quot;&lt;/td&gt;
&lt;td&gt;&quot;전염&quot;이 아니라 &quot;배포 조건&quot;. syscall 경계 밖 응용은 GPL에 물들지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;배포판이 다르면 완전히 다른 OS&quot;&lt;/td&gt;
&lt;td&gt;아니다. 커널은 같고 패키지 관리자&amp;middot;철학만 다름. 개념은 공통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Rocky가 RHEL을 복사한 불법&quot;&lt;/td&gt;
&lt;td&gt;아니다. GPL이 허용하는 정상적 재구축. 2023년 소스 제한 이후엔 경로만 바뀜&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약 &amp;mdash; 이 글의 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;리눅스&quot;는 커널이다.&lt;/b&gt; 단독 부팅 불가. 우리가 쓰는 건 커널+GNU 도구+패키지관리자 묶음인 &lt;b&gt;배포판&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포판은 커널 하나로 수백 개.&lt;/b&gt; 진짜 차이는 패키지 관리자가 아니라 &lt;b&gt;릴리스 철학&lt;/b&gt;(롤링 vs 포인트 vs 엔터프라이즈)과 상류-하류 코드 흐름. Rocky 10은 Red Hat 계열(RPM/dnf, 10년 지원).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPL은 자유지 무료가 아니다.&lt;/b&gt; 판매 가능, 단 소스 공개 의무(배포 시만). 이게 RHEL(유료)과 Rocky(무료)가 같은 소스에서 공존하는 이유.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커널은 GPL-2.0에 영원히 고정&lt;/b&gt; &amp;mdash; 수만 기여자 동의 없이 GPLv3로 못 옮김. 그래서 &quot;GPL-2.0-only&quot;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;syscall 예외&lt;/b&gt; 덕에 독점 응용도 리눅스에서 동작 &amp;mdash; 커널만 GPL 보호, 응용은 자유. 생태계 폭발의 핵심 설계.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rocky는 구 CentOS의 빈자리&lt;/b&gt;를 메운 RHEL 바이너리 호환 무료 재구축. 2023년 Red Hat의 소스 공개 제한에도 CentOS Stream 경로로 살아남았다 &amp;mdash; 리눅스의 자유는 도덕이 아니라 GPL이라는 &lt;b&gt;계약&lt;/b&gt; 위에 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리눅스가 이긴 이유&lt;/b&gt;는 천재 한 명이 아니라 계층적 메인테이너 구조 + &quot;충분한 눈&quot;의 개방 모델이 만들어낸 협업 규모.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;생각해 볼 문제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;누군가 &quot;리눅스는 공짜 운영체제&quot;라고 말한다. 한 문장으로 정정하면?&lt;/li&gt;
&lt;li&gt;같은 RHEL 소스에서 RHEL(유료)과 Rocky(무료)가 나온다. RHEL 구독자가 돈을 내는 것은 정확히 무엇일까?&lt;/li&gt;
&lt;li&gt;syscall 예외가 없었다면 리눅스 생태계에 구체적으로 어떤 일이 벌어졌을까?&lt;/li&gt;
&lt;li&gt;회사에서 GPL 라이브러리를 고쳐 쓰려 한다. &quot;소스 공개 안 하려면?&quot; 조건을 세 가지 말하라.&lt;/li&gt;
&lt;li&gt;배포판이 100개인데 &quot;리눅스를 배운다&quot;는 말은 무엇을 배운다는 뜻일까 &amp;mdash; 커널을, 배포판을, 둘 다를?&lt;/li&gt;
&lt;li&gt;Red Hat이 2023년 RHEL 소스 공개를 막았는데 Rocky가 살아남은 것은, GPL이 결국 &quot;글자&quot;를 지킨 것인가 &quot;정신&quot;을 지킨 것인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/History_of_Linux&quot;&gt;History of Linux - Wikipedia&lt;/a&gt; - 접근 2026-07-09 (역사/날짜 교차검증)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Linux_kernel_version_history&quot;&gt;Linux kernel version history - Wikipedia&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Comparison_of_Linux_distributions&quot;&gt;Comparison of Linux distributions - Wikipedia&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rockylinux.org/release_notes/&quot;&gt;Rocky Linux Release Notes&lt;/a&gt; - 접근 2026-07-09 (Rocky 10 사양)&lt;/li&gt;
&lt;li&gt;커널 라이선스&amp;middot;syscall 예외: 커널 소스 &lt;code&gt;COPYING&lt;/code&gt; 파일, 또는 &lt;code&gt;man 7 copyright&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;copyleft&amp;middot;GPL 해석: &lt;a href=&quot;https://www.gnu.org/licenses/old-licenses/gpl-2.0.html&quot;&gt;GPLv2 원문(gnu.org)&lt;/a&gt;, &lt;a href=&quot;https://www.gnu.org/licenses/gpl-faq.html&quot;&gt;FSF FAQ&lt;/a&gt; - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;2023 RHEL 소스 정책 변경: Red Hat 공식 발표(2023-06-21, &quot;Furthering the evolution of CentOS Stream&quot;), Rocky Linux/AlmaLinux 공식 응답문 - 접근 2026-07-09&lt;/li&gt;
&lt;li&gt;Linus's law&amp;middot;개발 모델: Eric S. Raymond, &lt;i&gt;The Cathedral and the Bazaar&lt;/i&gt;(1997)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech Artifacts/Linux</category>
      <category>Distribution</category>
      <category>kernel</category>
      <category>license</category>
      <category>Linux</category>
      <author>dongka</author>
      <guid isPermaLink="true">https://technical-architect.tistory.com/1</guid>
      <comments>https://technical-architect.tistory.com/entry/01-%EB%A6%AC%EB%88%85%EC%8A%A4-%EA%B0%9C%EC%9A%94#entry1comment</comments>
      <pubDate>Wed, 8 Jul 2026 16:57:12 +0900</pubDate>
    </item>
  </channel>
</rss>