-
The Log: 분산 시스템을 이해하는 가장 중요한 개념이코에코(Eco²) 제작 문서 및 리포트/Foundations 2025. 12. 21. 08:46
원문: The Log: What every software engineer should know about real-time data's unifying abstraction - Jay Kreps (LinkedIn Engineering, 2013)
들어가며


2013년, LinkedIn의 엔지니어 Jay Kreps가 작성한 이 글은 분산 시스템을 이해하는 데 있어 주요 아이디어로 꼽힌다.
Jay Kreps는 이후 Apache Kafka를 만들고 Confluent를 창업한 인물로, 이 글에서 그가 설명하는 "로그"의 개념은 Kafka의 핵심 설계 철학이 되었다.
"로그(Log)"라고 하면 대부분 애플리케이션 로그(console.log,logger.info)를 떠올리지만, 이 글에서 말하는 로그는 데이터 구조로서의 로그다. 이 개념을 이해하면 분산 MSA 큐잉, Kafka, 이벤트 소싱, CDC, 분산 데이터베이스의 동작 원리를 이해하는데 도움이 된다.로그(Log)란 무엇인가?
가장 단순한 저장소
로그는 놀라울 정도로 단순한 데이터 구조다. 세 가지 특징만 기억하면 된다:
- Append-only: 데이터는 끝에만 추가된다 (중간 삽입, 수정 불가)
- 순서 보장: 모든 레코드는 고유한 순번(offset)을 가진다
- 불변성: 한 번 기록된 데이터는 변경되지 않는다
┌─────────────────────────────────────────────────────────────┐ │ 로그 구조 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 시간 흐름 → │ │ │ │ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ │ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ │ ← 새 데이터 │ │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │ │ ↑ ↑ │ │ 가장 오래된 데이터 가장 최신 데이터 │ │ │ │ 각 칸 = 하나의 레코드 │ │ 숫자 = offset (순번) │ │ │ └─────────────────────────────────────────────────────────────┘이 단순한 구조가 왜 중요할까? 바로 무엇이 먼저 일어났는가?라는 질문에 명확히 답할 수 있기 때문이다.
시간 문제의 해결
분산 시스템에서 가장 어려운 문제 중 하나가 시간이다.
서버 A의 시계와 서버 B의 시계가 미세하게 다르면, 두 이벤트 중 어떤 것이 먼저 발생했는지 판단하기 어렵다.
로그는 이 문제를 우아하게 해결한다. 물리적 시계(wall clock) 대신 로그의 순번(offset)으로 순서를 결정하면 된다:- offset 5의 이벤트는 offset 4보다 항상 나중에 발생
- 이 순서는 모든 참여자가 동의할 수 있는 합의된 순서
이것이 Kafka, Raft, Paxos 같은 분산 시스템들이 내부적으로 로그를 사용하는 이유다.
데이터베이스와 로그의 관계
Write-Ahead Log (WAL)
데이터베이스를 써본 사람이라면 트랜잭션이라는 개념을 알 것이다. 돈을 송금할 때, A 계좌에서 출금하고 B 계좌에 입금하는 두 작업은 둘 다 성공하거나 둘 다 실패해야 한다.
데이터베이스는 이를 어떻게 보장할까? 바로 Write-Ahead Log(WAL)를 통해서다:- 변경 내용을 먼저 로그에 기록 (commit log)
- 로그 기록이 성공하면 실제 테이블 업데이트
- 장애 발생 시, 로그를 재생하여 복구
┌─────────────────────────────────────────────────────────────┐ │ 데이터베이스의 WAL 동작 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 트랜잭션 시작: "A에서 B로 10만원 이체" │ │ │ │ 2. 로그에 먼저 기록 │ │ ┌─────────────────────────────────────────────┐ │ │ │ [1001] A 잔액 100만→90만 │ │ │ │ [1002] B 잔액 50만→60만 │ │ │ │ [1003] 트랜잭션 커밋 │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 3. 로그 기록 성공 → 실제 테이블 업데이트 │ │ │ │ 4. 만약 3번 도중 서버 다운? │ │ → 재시작 시 로그를 읽어서 복구! │ │ │ └─────────────────────────────────────────────────────────────┘로그 ↔ 테이블의 이중성
여기서 중요한 통찰이 나온다. 로그와 테이블은 같은 데이터의 다른 표현이다.
- 로그: 변경의 전체 이력 (과거 + 현재)
- 테이블: 현재 상태의 스냅샷
이 둘은 서로 변환 가능하다:
┌─────────────────────────────────────────────────────────────┐ │ Log ↔ Table 이중성 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 로그 (이벤트 시퀀스): │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ [1] SET user_1.name = "김철수" │ │ │ │ [2] SET user_1.email = "kim@test.com" │ │ │ │ [3] SET user_2.name = "이영희" │ │ │ │ [4] SET user_1.name = "김철호" ← 이름 변경 │ │ │ │ [5] DELETE user_2 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 로그를 재생하면 │ │ ↓ │ │ 테이블 (현재 상태): │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ user_1: { name: "김철호", email: "kim@test.com" } │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 로그를 처음부터 재생하면 테이블의 현재 상태를 복원할 수 있다. │ │ 반대로, 테이블의 모든 변경을 기록하면 로그가 된다. │ │ │ └─────────────────────────────────────────────────────────────┘이 개념은 Event Sourcing의 이론적 기반이 되며, CDC(Change Data Capture)가 작동하는 원리이기도 하다.
분산 시스템에서 로그의 역할
합의(Consensus)의 핵심
분산 시스템에서 여러 노드가 같은 상태를 유지하려면, 무엇이 일어났는지에 대해 모두가 동의해야 한다. 이것이 합의(Consensus) 문제다.
Raft, Paxos 같은 합의 알고리즘은 본질적으로 로그를 어떻게 복제할 것인가를 결정한다.┌─────────────────────────────────────────────────────────────┐ │ 분산 시스템의 로그 복제 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 리더 (Leader) │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 로그: [1] [2] [3] [4] [5] │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ 복제 │ │ ┌────────────┼────────────┐ │ │ ↓ ↓ ↓ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 팔로워 A │ │ 팔로워 B │ │ 팔로워 C │ │ │ │ [1][2][3][4] │ │ [1][2][3][4][5]│ │ [1][2][3] │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ 모든 팔로워가 같은 로그를 가지면 → 같은 상태 │ │ 팔로워 C가 뒤처짐 → 나중에 [4][5] 받아서 따라잡음 │ │ │ └─────────────────────────────────────────────────────────────┘핵심 아이디어는 간단하다:
- 리더가 모든 변경을 로그에 기록
- 팔로워들이 리더의 로그를 복제
- 모든 노드가 같은 로그를 재생하면 → 같은 상태
데이터 통합 문제
N×M 연결의 악몽
LinkedIn은 성장하면서 수많은 데이터 시스템을 도입했다.
Oracle, MySQL, Hadoop, Elasticsearch, 캐시 서버... 문제는 이 시스템들이 서로 데이터를 주고받아야 한다는 것이었다.
N개의 데이터 소스와 M개의 데이터 목적지가 있으면, 최악의 경우 N×M개의 커스텀 파이프라인이 필요하다:┌─────────────────────────────────────────────────────────────┐ │ N×M 연결의 복잡성 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 데이터 소스 데이터 목적지 │ │ │ │ Oracle ─────┬──────────▶ Hadoop │ │ │ │ │ MySQL ──────┼──────────▶ Elasticsearch │ │ │ │ │ MongoDB ────┼──────────▶ 캐시 서버 │ │ │ │ │ API 서버 ───┴──────────▶ 분석 DB │ │ │ │ 4개 소스 × 4개 목적지 = 16개 파이프라인 │ │ 각 파이프라인마다: │ │ - 다른 데이터 포맷 │ │ - 다른 전송 프로토콜 │ │ - 다른 에러 처리 │ │ - 다른 모니터링 │ │ │ │ 시스템 하나 추가할 때마다 4개의 새 파이프라인 필요! │ │ │ └─────────────────────────────────────────────────────────────┘로그를 중앙 허브로
로그를 중앙에 두면 문제가 단순해진다. 모든 소스는 로그에 쓰기만 하고, 모든 목적지는 로그에서 읽기만 하면 된다:
┌─────────────────────────────────────────────────────────────┐ │ 로그 중심 아키텍처 (N+M) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 데이터 소스 │ │ ┌──────────┐ │ │ │ Oracle │───┐ │ │ └──────────┘ │ │ │ ┌──────────┐ │ │ │ │ MySQL │───┼──▶┌─────────────────┐ │ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │ 중앙 로그 │ │ │ │ MongoDB │───┼──▶│ (Kafka) │ │ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ └────────┬────────┘ │ │ │ API 서버 │───┘ │ │ │ └──────────┘ │ │ │ │ │ │ ┌─────────┼─────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌────────┐┌────────┐┌────────┐ │ │ │Hadoop ││Elastic ││Analytics│ │ │ └────────┘└────────┘└────────┘ │ │ │ │ 4개 소스 + 4개 목적지 = 8개 연결 (16개 → 8개!) │ │ │ │ 새 시스템 추가 = 로그와의 연결 1개만 추가 │ │ │ └─────────────────────────────────────────────────────────────┘
Kafka의 핵심 아이디어다. Kafka는 분산 커밋 로그로서, 모든 데이터 흐름의 중앙 허브 역할을 한다.각 소비자의 독립성
로그 기반 아키텍처의 또 다른 장점은 Consumer의 독립성이다.
- 각 소비자는 자신만의 offset(읽기 위치)을 관리
- 소비자 A가 느려도 소비자 B에게 영향 없음
- 새 소비자가 추가되어도 기존 소비자에게 영향 없음
- 장애 복구 시 마지막 offset부터 다시 읽으면 됨
┌─────────────────────────────────────────────────────────────┐ │ 소비자별 독립적 offset │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 로그: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] │ │ ↑ ↑ ↑ │ │ │ │ │ │ │ 소비자 A: offset=1 (느림) │ │ │ │ │ │ │ │ 소비자 B: offset=4 (보통) │ │ │ │ │ │ 소비자 C: offset=8 (빠름) │ │ │ │ 각자 자기 속도로 읽고, 서로 영향 없음 │ │ │ └─────────────────────────────────────────────────────────────┘스트림 처리와 배치 처리의 통합
배치 vs 실시간의 잘못된 이분법
흔히 데이터 처리를 배치와 실시간으로 나눈다:
- 배치: 하루치 데이터를 모아서 야간에 처리 (Hadoop, Spark)
- 실시간: 데이터가 들어오는 즉시 처리 (Kafka Streams, Flink)
하지만 Jay Kreps는 이것이 본질적인 차이가 아니라고 주장한다.
둘 다 로그를 읽어서 처리하고 결과를 내보내는 동일한 패턴이나, 언제 처리하냐의 차이다.┌─────────────────────────────────────────────────────────────┐ │ 배치와 스트림: 같은 패턴, 다른 지연시간 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 배치 처리: │ │ 로그 ──▶ [일 배치 처리] ──▶ 결과 (지연: 24시간) │ │ ↑ │ │ └── 하루에 한 번 실행 │ │ │ │ 마이크로 배치: │ │ 로그 ──▶ [시간별 처리] ──▶ 결과 (지연: 1시간) │ │ ↑ │ │ └── 한 시간에 한 번 실행 │ │ │ │ 스트림 처리: │ │ 로그 ──▶ [연속 처리] ──▶ 결과 (지연: 밀리초) │ │ ↑ │ │ └── 데이터 도착 즉시 실행 │ │ │ │ → 같은 로직을 지연시간만 다르게 실행할 수 있다! │ │ │ └─────────────────────────────────────────────────────────────┘이 통찰은 Lambda Architecture 비판의 근거가 되었고, 이후 Kappa Architecture라는 더 단순한 아키텍처로 이어졌다.
Unbundling the Database - 데이터베이스 해체하기
데이터베이스의 구성 요소
전통적인 관계형 데이터베이스는 사실 여러 기능의 번들이다:
- 저장소: 데이터를 디스크에 저장
- 인덱스: 빠른 조회를 위한 자료구조 (B-Tree 등)
- 캐시: 자주 쓰는 데이터를 메모리에
- 복제: 여러 노드에 데이터 복사
- 쿼리 엔진: SQL 파싱, 최적화, 실행
이 모든 기능이 하나의 시스템에 묶여 있다.
로그를 중심으로 분리하기
Jay Kreps의 제안: 이 기능들을 로그를 중심으로 분리하면 어떨까?
┌─────────────────────────────────────────────────────────────┐ │ Unbundled Database │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 기존: 모놀리식 데이터베이스 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ PostgreSQL / Oracle / MySQL │ │ │ │ [저장소] [인덱스] [캐시] [복제] [쿼리엔진] │ │ │ │ 모두 한 시스템에 번들 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Unbundled: 로그 중심 분리 │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 중앙 로그 │ │ │ │ (Source of Truth) │ │ │ └──────────────────────┬──────────────────────────────┘ │ │ │ │ │ ┌─────────────────┬┴─────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ Search │ │ Cache │ │ Graph │ │ │ │ (Elastic) │ (Redis) │ │ (Neo4j)│ │ │ │ 검색 최적화 │ 읽기 최적화 │ │ 관계 최적화│ │ │ └────────┘ └────────┘ └────────┘ │ │ │ │ 각 시스템이 로그를 구독하여 자신만의 뷰를 구축 │ │ → 각 용도에 최적화된 시스템 선택 가능 │ │ │ └─────────────────────────────────────────────────────────────┘
이 아이디어는 현대 마이크로서비스 아키텍처에서 실현되고 있다:- Kafka: 중앙 로그
- Elasticsearch: 검색용 뷰
- Redis: 캐싱용 뷰
- PostgreSQL: 트랜잭션 처리
- 각 시스템이 Kafka의 이벤트를 구독하여 자신의 데이터 유지
전통적 메시지 큐와의 차이
RabbitMQ vs Kafka
메시지 큐라고 하면 RabbitMQ를 떠올리기 쉽다.
하지만 Kafka(로그 기반)와 RabbitMQ(전통적 큐)는 철학이 다르다:메시지 보존 소비되면 삭제 설정 기간 동안 보존 재소비 불가능 offset만 변경하면 가능 소비자 독립성 메시지를 두고 경쟁 각자 독립적 offset 순서 보장 제한적 파티션 내 완전 보장 주 사용 사례 작업 큐 (Task Queue) 이벤트 스트리밍
중요한 것은 어떤 것이 더 좋다가 아니라 용도가 다르다는 점이다:- 작업 분배 (예: 이미지 리사이징, 이메일 발송): RabbitMQ가 적합
- 이벤트 스트리밍 (예: 로그 수집, 실시간 분석): Kafka가 적합
왜 이 개념이 중요한가?
1. 현대 아키텍처의 기반
Event Sourcing, CQRS, CDC, Kafka, 마이크로서비스 간 통신...
이 모든 개념이 로그라는 기본 아이디어에서 출발한다.
로그를 이해하면 이들이 왜 그렇게 설계되었는지 자연스럽게 이해된다.2. 복잡한 시스템의 단순화
N×M 연결 문제를 N+M으로 줄이는 아이디어는 시스템 복잡성 관리의 핵심 원칙이다. 규모가 커질수록 이 차이는 극적으로 벌어진다.
3. 장애 복구와 디버깅
로그가 있으면:
- 어떤 이벤트가 언제 발생했는지 추적 가능
- 장애 시점으로 되돌리기 가능
- 재생을 통한 상태 복구 가능
핵심 개념 정리
로그 Append-only, 순서 보장된 불변 레코드 시퀀스 Log ↔ Table 로그를 재생하면 테이블, 테이블 변경을 기록하면 로그 중앙 로그 데이터 통합의 허브, N×M → N+M 소비자 독립성 각 소비자가 자신의 offset으로 독립적 읽기 Unbundling 데이터베이스를 로그 중심으로 분리된 특화 시스템으로 더 읽을 자료
- I Heart Logs - Jay Kreps (이 글의 확장판, 무료 eBook)
- Designing Data-Intensive Applications - Martin Kleppmann
- Kafka: The Definitive Guide - Confluent
부록: Eco² 적용 포인트
ext-authz 블랙리스트 동기화
현재 ext-authz는 JWT 검증과 블랙리스트 조회만 담당한다 (라우팅은 Istio VirtualService가 담당).
로그 중심 아키텍처를 적용하면:┌─────────────────────────────────────────────────────────────┐ │ 로그 기반 블랙리스트 동기화 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ [현재] │ │ auth-api → Redis (저장) │ │ ext-authz → Redis (매 요청마다 조회) ← 병목 │ │ │ │ [로그 기반 개선] │ │ auth-api → Redis + MQ (이벤트 발행) │ │ ↓ │ │ ┌─────────────┐ │ │ │ 중앙 로그 │ │ │ │ (RabbitMQ) │ │ │ └──────┬──────┘ │ │ │ 구독 │ │ ┌──────────┼──────────┐ │ │ ↓ ↓ ↓ │ │ ext-authz-1 ext-authz-2 ext-authz-N │ │ (로컬 캐시) (로컬 캐시) (로컬 캐시) │ │ │ │ 각 파드가 로그를 구독하여 로컬 캐시 유지 │ │ → Redis 조회 없이 로컬에서 검증 │ │ │ └─────────────────────────────────────────────────────────────┘
ext-authz의 경우 Blacklist 읽기 작업만 수행한다.
ext-authz 성능 튜닝에서 살펴봤듯, 대부분의 병목은 Redis Single Queue에 쌓인 읽기 Task다.
병목인 Redis를 서버에서 분리하고, 삭제/삽입을 로그에 기록한다.
ext-authz는 해당 작업이 발생해 로그가 변경되면 로컬 캐시를 동기화한다.
이 경우, ext-authz는 Redis에게 작업을 묻지 않고 로컬 캐시만으로도 이코에코 시스템의 Blacklist 검증을 수행할 수 있게 된다.Character → My 도메인 동기화
Log ↔ Table 이중성을 적용:
[character 도메인] CharacterGranted 이벤트 → 로그(MQ) ↓ [my 도메인] 로그 구독 → user_characters 테이블 업데이트 (Projection)- character 도메인의 변경이 로그로 기록
- my 도메인은 로그를 구독하여 자신의 테이블 구축
- 장애 시 로그를 재생하면 상태 복구
'이코에코(Eco²) 제작 문서 및 리포트 > Foundations' 카테고리의 다른 글
Domain-Driven Design: Aggregate와 트랜잭션 경계 (0) 2025.12.21 CQRS: Command와 Query의 책임 분리 (0) 2025.12.21 Enterprise Integration Patterns: 메시징 시스템의 설계 원칙 (2) 2025.12.21 Uber DOMA: 마이크로서비스 관리 방법론 (1) 2025.12.21 Event Sourcing: 상태(state)에서 이벤트로 (1) 2025.12.21