펜타시스템/입사 후 첫 프로젝트 공부 내용

Kafka Topic, Consumer, Schema Registry 정리

dev-lee 2026. 10. 6. 17:38

1. Topic

Topic은 메시지를 종류별로 나눠 저장하는 논리적 채널이다.

1.1 구성 요소

  • Partition : Topic을 나눈 단위. 병렬 처리와 확장의 기준이며, 순서는 Partition 안에서만 보장됨
  • Offset : Partition 안에서 각 메시지가 가진 순번(0, 1, 2…)
  • Key 파티셔닝 : 같은 Key는 해시 결과에 따라 항상 같은 Partition으로 들어감. user_id를 Key로 쓰면 사용자별 순서 유지 가능
  • Replication : Partition마다 Leader 1개와 Follower 여러 개로 복제됨. Leader를 충분히 따라잡고 있는 복제본 목록이 ISR
  • Retention : 읽어도 지워지지 않고, 기간(retention.ms)이나 크기를 넘으면 삭제됨. cleanup.policy=compact면 Key별 최신 값만 남김

Kafka는 큐가 아니라 로그이다. 읽는 쪽이 자기 위치(offset)를 관리하므로 여러 시스템이 같은 데이터를 독립적으로 읽을 수 있음


2. Consumer

Consumer는 Topic에서 메시지를 pull 방식으로 가져와 처리하는 클라이언트다.

2.1 Consumer Group

  • Consumer Group : 같은 group.id를 쓰는 Consumer끼리 Partition을 나눠 읽음. 한 Partition은 그룹 안에서 Consumer 1개에게만 할당됨
  • 병렬성 상한 : Consumer 수가 Partition 수보다 많으면 남는 Consumer는 놀게 됨. Partition 수가 곧 처리량의 상한
  • 여러 그룹 : 서로 다른 그룹은 같은 Topic을 각자 독립적으로 처음부터 끝까지 읽음. 적재용 그룹과 실시간 알림용 그룹을 따로 둘 수 있음

2.2 Offset 관리

  • Offset Commit : 어디까지 읽었는지 내부 Topic __consumer_offsets에 기록함. 자동 커밋은 유실·중복 위험이 있어 처리 후 수동 커밋이 일반적
  • 전달 보장 : 처리 후 커밋은 at-least-once(중복 가능), 커밋 후 처리는 at-most-once(유실 가능). exactly-once는 트랜잭션이나 멱등 처리 필요
  • auto.offset.reset : 커밋된 offset이 없을 때 earliest는 처음부터, latest는 지금부터 읽음

2.3 운영

  • Rebalancing : Consumer가 들어오거나 나가거나 응답이 없으면(session.timeout.ms, max.poll.interval.ms 초과) Partition을 다시 배분함. Cooperative Sticky Assignor로 중단 시간 단축 가능
  • Consumer Lag : 최신 offset과 커밋된 offset의 차이. 처리 지연을 판단하는 핵심 지표

3. Schema Registry 기본 동작

Schema Registry는 Kafka 옆에 붙어 있는 메시지 형식 사전이다. Apache Kafka가 아니라 Confluent가 만든 별도 컴포넌트다.

3.1 필요한 이유

  • 문제 : Kafka는 메시지를 바이트로만 저장하고 내용을 모름. Producer와 Consumer가 같은 형식을 공유해야 해석 가능
  • 해결 : 형식(스키마)을 Registry에 등록해두고 번호로 주고받음

3.2 메시지 흐름

  • Producer : 스키마를 Registry에 등록하고 ID(예: 7번)를 받음. 메시지에는 스키마 전체가 아니라 번호만 붙여서 보냄
  • Consumer : 메시지 앞의 7번을 보고 Registry에서 스키마를 조회함(한 번 받으면 캐싱). 그 스키마로 바이트를 해석함
  • 검문소 역할 : 바뀐 스키마를 등록하려 하면 기존 쪽이 깨지지 않는지 검사하고, 위험하면 등록을 거부함
[magic byte][schema ID][Avro 바이너리]
  • 지원 포맷 : Avro, Protobuf, JSON Schema
  • Subject : 스키마 버전을 묶는 단위. 기본 규칙에서는 <topic>-key, <topic>-value

Registry는 스키마 번호를 나눠줘서 메시지를 작게 만들고, 깨지는 변경을 등록 단계에서 막는다


4. 스키마와 앱 코드의 관계

Registry에 새 스키마를 등록해도 실행 중인 Producer와 Consumer는 바뀌지 않는다. 스키마는 각 앱의 코드 안에 들어 있다.

4.1 Registry는 저장소일 뿐 배포하지 않음

  • 스키마의 위치 : 각 앱의 스키마는 그 앱의 코드 안에 있음. 보통 빌드할 때 스키마 파일로 클래스를 생성해서 씀
  • Registry의 역할 : 보관과 검사만 함. 새 버전을 앱에 밀어주지 않음
  • 실제 변경 시점 : 각 앱을 새 코드로 재배포하는 순간. 그 전까지는 계속 예전 스키마를 씀
  • 등록 시점 : 보통 업그레이드된 Producer가 처음 메시지를 보낼 때 자동 등록됨(auto.register.schemas=true)
  • 새 스키마만 등록된 경우 : Producer와 Consumer가 모두 예전 스키마를 쓰고 있다면 아무 일도 일어나지 않음. 기록만 되어 있는 상태

4.2 예전 스키마는 대체되지 않고 누적됨

Subject: orders-value
  v1 (ID 1)
  v2 (ID 7)
  v3 (ID 12)  ← latest
  • 지우지 않는 이유 : Topic에 ID 1이 붙은 과거 메시지가 남아 있고, 이걸 읽으려면 v1이 필요함
  • latest : 가장 최근 버전이라는 표시일 뿐

4.3 Writer와 Reader는 코드로 정해짐

구분  정하는 쪽 정하는 코드 (Python confluent-kafka) 바뀌는 시점
Writer 스키마 Producer AvroSerializer(sr, writer_schema) Producer 재배포 시. 이미 쓴 메시지는 영원히 그대로
Reader 스키마 Consumer AvroDeserializer(sr, reader_schema) Consumer 재배포 시
  • Reader 생략 시 : AvroDeserializer(sr)처럼 Reader를 넘기지 않으면 Writer 스키마를 그대로 씀. 해석 없이 메시지마다 생긴 대로 받으므로 필드 유무를 코드가 직접 처리해야 함
  • Java의 경우 : specific.avro.reader=true로 생성된 클래스를 쓰면 그 클래스의 스키마가 Reader가 됨

앱마다 배포 시점이 달라서 예전 버전과 새 버전이 섞여 돌아가는 기간이 생긴다. 호환성 모드는 이 기간에 누가 깨지지 않을지를 정한다


5. Writer와 Reader의 해석 규칙

과거 데이터를 읽는 건 마법이 아니라, Consumer의 역직렬화기가 Writer 스키마와 Reader 스키마를 필드 이름으로 대조하는 과정이다. (Avro 기준)

5.1 대조 과정

  • Writer 스키마 확보 : 메시지 앞의 ID로 Registry에서 가져옴
  • Reader 스키마 : Consumer 코드에 있는 스키마
  • 대조 : 두 스키마를 필드 이름 기준으로 맞춰봄
상황  처리
양쪽에 다 있는 필드 그대로 읽음
Writer에만 있는 필드 버림 → 문제없음
Reader에만 있고 default 있음 default로 채움
Reader에만 있고 default 없음 에러
타입이 다름 int→long 같은 승격만 허용, 나머지 에러

5.2 삭제된 필드는 무시되지 않음

Reader (예전) : { id, name, email }
Writer (새것) : { id, name }          ← email 삭제
  • 처음 생각한 것 : 새 스키마에서 삭제했으니 Reader가 읽을 때 없으면 무시될 것
  • 실제 동작 : 삭제된 email은 Reader에만 있는 필드가 됨. Reader 코드는 여전히 email을 기대하므로 값을 채워야 함. 데이터에 값이 없으니 default가 없으면 에러
  • 무시되는 경우 : 반대로 Writer에 필드를 추가했을 때. Reader가 모르는 필드라서 버림

남는 데이터는 버리면 되지만, 없는 데이터는 지어낼 수 없다. Reader에만 있는 필드는 반드시 default가 있어야 함


6. 호환성 모드

호환성 모드는 5번의 해석 규칙을 등록 시점에 미리 돌려보고, 에러가 나는 스키마를 거부하는 장치다.

6.1 모드를 정한다는 의미

스키마를 v1에서 v2로 바꾸면 두 가지 상황이 생긴다.

  • 상황 A : 업그레이드된 v2 Consumer가 v1 메시지를 읽음
  • 상황 B : 업그레이드 안 된 v1 Consumer가 v2 메시지를 읽음

모드는 이 중 어느 상황을 보장할지 고르는 설정이다.

모드  검사 방식 보장 상황 전제
BACKWARD (기본값) 새 스키마(Reader)로 예전 데이터(Writer)를 읽을 수 있는지 상황 A Consumer 먼저 업그레이드
FORWARD 예전 스키마(Reader)로 새 데이터(Writer)를 읽을 수 있는지 상황 B Producer 먼저 업그레이드, Consumer는 그대로
FULL 양방향 모두 A와 B 모두 순서 무관
NONE 검사 안 함 없음 개발·테스트용

6.2 BACKWARD의 동작

  • 검사 시점 : 업그레이드된 Producer가 v2를 등록하려 할 때. Registry가 v2를 Reader로, v1을 Writer로 놓고 해석이 되는지 검사함
  • 통과 : v2 등록, 새 ID 발급, 정상 전송
  • 실패 : 등록 거부(409 Conflict), Producer 직렬화 단계에서 에러, 메시지 전송 불가
  • 보장하는 것 : 새 스키마로 업그레이드된 Consumer가 예전 스키마로 쓰인 메시지를 읽을 수 있음. 아직 업그레이드 안 된 Producer가 지금 보내는 메시지와 Topic에 이미 쌓인 과거 메시지 모두 해당
  • 보장하지 않는 것 : 업그레이드 안 된 예전 Consumer가 새 메시지를 읽는 경우. 그래서 Consumer를 먼저 업그레이드해야 함
  • 기본값인 이유 : Kafka는 데이터를 보관하므로 재처리(replay)나 새 Consumer Group이 earliest부터 읽을 때 과거 메시지를 새 스키마로 읽을 수 있어야 함

6.3 FULL과 NONE

  • FULL의 효과 : 배포 순서 조율이 필요 없음. Producer 팀과 Consumer 팀이 각자 일정대로 배포하고, 한쪽을 롤백해도 다른 쪽이 깨지지 않음
  • NONE의 의미 : Registry의 안전장치를 끄는 것. 깨지는 변경도 등록됨. 운영에서는 NONE 대신 새 Topic(orders-v2)으로 이전하는 방식이 안전

6.4 헷갈리기 쉬운 지점

  • 배포 순서 강제 여부 : BACKWARD가 Producer를 먼저 업그레이드하면 에러 나게 막아두는 것은 아님. Registry는 누가 먼저 배포하는지 모르고, 검사는 등록 시점에만 일어남. 배포 순서는 운영자가 지켜야 하는 규칙
  • BACKWARD의 전제 : Consumer를 건드리지 않는다는 전제가 아니라 Consumer를 업그레이드한다는 전제. Consumer를 그대로 두는 전제는 FORWARD
  • FULL의 제한 범위 : 필드 추가·삭제가 아예 안 되는 것은 아님. default가 있는 필드는 추가·삭제 가능하고, 막히는 건 default 없는 필드뿐

6.5 모드별 허용 변경

모드  Reader 필드 추가 필드 삭제
BACKWARD 새 스키마 default 필요 자유
FORWARD 예전 스키마 자유 예전 필드에 default 필요
FULL 양쪽 default 필요 default 필요

6.6 변경 예시

v1 : { id, name }

 

v2 변경 BACKWARD FORWARD FULL
email 추가, default 있음 통과 통과 통과
email 추가, default 없음 거부 통과 거부
name 삭제 (v1에서 default 없음) 통과 거부 거부

name 삭제는 BACKWARD에서 통과하지만, 이 상태로 Producer를 먼저 배포하면 v1 Consumer가 깨진다. BACKWARD에서 Consumer를 먼저 배포해야 하는 이유

6.7 추가로 알아둘 것

  • TRANSITIVE : 기본 모드는 직전 버전과만 비교함. 모든 과거 버전과 비교하려면 BACKWARD_TRANSITIVE, FULL_TRANSITIVE 사용. Retention이 길면 이쪽이 안전
  • 포맷 차이 : Protobuf는 필드 이름이 아니라 필드 번호로 맞추고 모르는 필드는 무시함. JSON Schema는 additionalProperties 설정이 호환성에 영향을 줌

7. 참고 문서

주제   문서
Topic, Consumer 개념 Apache Kafka 문서 Design (Consumer Position, Consumer Group, Delivery Semantics, Replication, Log Compaction)
Topic, Consumer 설정값 Apache Kafka 문서 Configuration (Topic Configs, Consumer Configs)
호환성 모드 Confluent 문서 Schema Evolution and Compatibility
Writer/Reader 해석 규칙 Avro 명세 Schema Resolution

요약

구성요소  역할  핵심
Topic 메시지 저장 단위 Partition, Offset, Key 파티셔닝, Replication, Retention
Consumer 메시지 읽기·처리 Consumer Group, Offset Commit, Rebalancing, Lag
Schema Registry 스키마 보관과 변경 검사 Schema ID, 버전 누적, 등록 시점 호환성 검사
Writer / Reader 실제 해석 Producer 코드와 Consumer 코드가 각각 정함. Reader에만 있는 필드는 default 필요
BACKWARD 기본 호환성 모드 Consumer 먼저 업그레이드. 업그레이드 안 된 Producer의 메시지와 과거 메시지를 새 Consumer가 읽을 수 있음

Registry는 변경을 허락하고 기록할 뿐이고, 실제 변경은 각 앱을 재배포할 때 일어난다. 호환성 모드는 예전 버전과 새 버전이 섞여 도는 동안 누가 깨지지 않을지를 미리 정해두는 규칙이다