[Live Event Stream #4] 회고

2026. 8. 15. 18:43ㆍPROJECT/Toy Project

[Live Event Stream #4] 학습 목표, 요구사항, 바이브 코딩 전략 고찰

1. 시작하며

앞선 세 글에서는 Live Event Stream 프로젝트를 시작하기 전에 무엇을 배우고, 무엇을 만들고, 어떤 방식으로 AI와 개발할지를 정리했다.

당시에는 프로젝트를 시작하기 전이었기 때문에 대부분이 계획에 가까웠다.

예를 들어 다음과 같은 질문을 직접 확인해보려고 했다.

Producer가 Consumer보다 빠르면 어떻게 될까?

Partition을 늘리면 처리량이 늘어날까?

Consumer를 늘리면 무조건 빨라질까?

같은 Event가 두 번 처리되면 어떻게 해야 할까?

실패 Event는 어떻게 격리해야 할까?

Consumer가 죽었다가 살아나면 어디서부터 다시 처리할까?

그리고 Playback Gate에서의 경험을 바탕으로 개발 방식도 조금 더 명확하게 정했다.

문제 재현
   ↓
측정
   ↓
원인 분석
   ↓
대안 비교
   ↓
Human Gate
   ↓
기술 선택
   ↓
구현
   ↓
재측정

Phase 9까지 모두 끝난 지금은 처음에 세웠던 목표와 원칙이 실제 개발 과정에서도 유지됐는지 확인해볼 수 있게 됐다.

이번 글에서는 단순히 프로젝트를 잘 끝냈다는 내용보다는,

처음 세웠던 가설 중 무엇이 맞았고, 무엇이 틀렸으며, 바이브 코딩 전략이 실제로 어느 정도 도움이 됐는지

를 중심으로 정리해보려고 한다.


2. 최종 구조부터 보면

최종 Live Event Stream의 구조는 다음과 같이 정리할 수 있다.

JSON + schemaVersion

Kafka
Partition 3

Consumer
concurrency 3

Message Key
memberId

Idempotency
processed_event.event_id UNIQUE

Failure Handling
Backoff
   ↓
user-events-dlt

Schema Registry 없음

Retry Topic 없음

Redis 없음

처음 프로젝트를 시작할 때 흔히 Kafka 프로젝트에서 떠올릴 수 있는 기술을 모두 적용한 구조는 아니다.

예를 들어 처음에는 다음 기술들도 후보로 생각했었다.

Redis Deduplication

Retry Topic

Schema Registry

Batch Consumer

하지만 실제 실험 결과에서는 모두 필요하지 않았다.

이 부분은 #3에서 정리했던 원칙과 가장 잘 맞았다고 생각한다.

처음 예상한 기술이 최종 답이 아닐 수 있다.

이번 프로젝트에서도 기술을 먼저 정한 뒤 문제를 끼워 맞추기보다는, 문제를 확인한 뒤 필요한 구조만 남기는 방식으로 진행했다.


3. 학습 목표는 실제로 얼마나 달성했을까?

#1에서 가장 중요하게 생각했던 것은 단순히 Kafka Producer와 Consumer를 만들어보는 것이 아니었다.

직접 부하를 발생시키고 Kafka의 상태를 확인하면서 왜 이런 구조가 필요한지를 이해하는 것이 목표였다.

결과적으로 대부분의 목표는 실제 실험까지 해볼 수 있었다.


3.1 Producer가 Consumer보다 빠르면 어떻게 될까?

가장 먼저 확인한 것은 Consumer Lag이었다.

초기 Baseline은 다음 구조였다.

Partition 1
Consumer 1

Producer에서 초당 약 500개의 Event를 발생시켰을 때 Consumer는 약 250개 수준을 처리했다.

Producer
약 500 events/sec
       ↓
     Kafka
       ↓
Consumer
약 250 events/sec

당연히 처리하지 못한 Event가 Kafka에 계속 쌓이기 시작했다.

20초 동안 부하를 발생시켰을 때:

Consumer Lag
4,252

까지 증가했다.

Event의 End-to-End 처리시간 평균도 약:

8.58초

까지 늘어났다.

부하를 중단하면 Consumer가 밀려 있던 Event를 다시 처리하면서 약 10초 뒤 Lag이 0으로 돌아왔다.

이를 통해 단순히:

Consumer가 느리다.

정도로 보는 것이 아니라,

Producer Throughput
       >
Consumer Throughput

↓

Lag 증가

↓

Event 처리 지연 증가

라는 관계를 실제 Metric으로 확인할 수 있었다.


3.2 Partition만 늘리면 빨라질까?

처음에는 Partition을 늘리면 처리량도 자연스럽게 증가할 것이라고 생각하기 쉽다.

하지만 실제 결과는 그렇지 않았다.

P1 C1
Partition 1
Consumer 1

에서:

P3 C1
Partition 3
Consumer 1

로 Partition만 늘렸을 때 오히려 결과가 더 좋지 않았다.

최대 Consumer Lag은:

8,667

까지 증가했다.

Partition이 세 개 있어도 이를 실제로 병렬 처리할 Consumer가 하나뿐이었기 때문이다.

결국 중요한 것은:

Partition 증가
≠
자동적인 처리량 증가

였다.

Partition을 늘렸다면 그것을 실제로 병렬 처리할 Consumer도 함께 고려해야 했다.


3.3 Consumer를 계속 늘리면 빨라질까?

다음으로 Partition과 Consumer 수를 같이 변경해봤다.

최종적으로 이번 환경에서는:

Partition 3
Consumer concurrency 3

구성이 500 events/sec 부하를 따라갈 수 있었다.

하지만 Consumer를 더 늘린다고 계속 빨라지지는 않았다.

Partition 3
Consumer 6

으로 실행했을 때는 세 Consumer만 실제 Partition을 할당받았다.

나머지 세 Consumer는:

Idle

상태였다.

Kafka에서 하나의 Consumer Group 안에서는 하나의 Partition을 동시에 두 Consumer가 처리하지 않기 때문이다.

즉:

Partition 3

↓

실제로 일할 수 있는 Consumer 최대 3

라는 관계를 직접 확인할 수 있었다.


3.4 Hot Partition도 직접 확인했다

초기 Kafka Key는 memberId였다.

같은 사용자의 Event 순서를 유지하기 위한 선택이었다.

memberId = 100

PLAY_START
PLAY_PAUSE
PLAY_END

↓

동일 Partition

일반적인 Event 분포에서는 P3 C3 구조가 정상적으로 Event를 분산했다.

하지만 테스트 Event의 memberId를 모두 하나로 고정하면 상황이 달라졌다.

memberId = 1
memberId = 1
memberId = 1
memberId = 1
...

모든 Event가 동일한 Key를 가지기 때문에 하나의 Partition에 집중됐다.

결과적으로:

P3 C3

구조임에도 실제 처리 형태는:

P1 C1

과 거의 같아졌다.

이 실험을 통해 Partition 수뿐만 아니라 Key의 분포가 병렬 처리 성능에 직접적인 영향을 준다는 것을 확인할 수 있었다.

그렇다고 Key를 바로 바꾸지는 않았다.

memberId를 Key로 사용하는 이유는 같은 사용자의 Event Ordering을 유지하기 위해서였기 때문이다.

즉:

균등한 Partition 분배
vs
동일 사용자의 Event Ordering

이라는 Trade-off가 존재한다.

이번 프로젝트에서는 Ordering을 유지하는 쪽을 선택했다.


4. At-least-once와 중복 처리를 직접 확인해봤다

#1에서 가장 직접적으로 경험해보고 싶었던 것 중 하나가 At-least-once에서 발생할 수 있는 중복 처리였다.

초기에는 의도적으로 다음 구조를 사용했다.

DB INSERT

↓

Offset Commit

그리고 eventId에 UNIQUE Constraint를 걸지 않았다.

덕분에:

DB INSERT 성공
        ↓
Offset Commit 이전 실패
        ↓
같은 Event 재전달
        ↓
DB INSERT

상황에서 실제로 같은 eventId의 Row가 여러 개 생성되는 것을 확인할 수 있었다.

즉 Kafka에서 Event가 한 번만 전달된다고 가정해서는 안 된다는 것을 코드로 확인한 것이다.


4.1 Idempotency는 무엇을 선택했는가?

중복 문제가 확인된 뒤 해결 후보를 비교했다.

대표적인 후보는:

DB Unique Constraint

Processed Event Table

Redis Deduplication

이었다.

최종적으로는 가장 단순한:

processed_event.event_id UNIQUE

방식을 선택했다.

이번 프로젝트에서는 하나의 MySQL Event Store에 결과를 저장하고 있었고, 별도의 Redis까지 추가할 만큼 복잡한 요구사항이 없었다.

그래서:

Redis를 써보고 싶으니까 Redis Deduplication

이 아니라:

현재 문제
   ↓
DB에서 이미 해결 가능
   ↓
추가 인프라 필요 없음
   ↓
UNIQUE 선택

이 되었다.

Idempotency 적용 이후 같은 Event를 다시 전달해도 DB의 결과는 한 건으로 유지됐다.

성능 역시 500 events/sec 수준에서는 문제가 되지 않았다.

처음에는 UNIQUE Constraint가 INSERT 성능에 상당한 영향을 줄 수도 있다고 생각했지만, 현재 테스트 범위에서는 Consumer가 충분히 Producer를 따라갈 수 있었다.


5. Retry와 DLT에서는 예상과 다른 결과가 나왔다

실패 처리 실험은 이번 프로젝트에서 예상과 실제 결과가 가장 크게 달랐던 부분 중 하나다.

처음에는 Poison Event가 들어오면:

정상 Event
정상 Event
Poison Event
정상 Event
정상 Event

Consumer가 Poison Event에서 계속 실패하면서 같은 Partition의 뒤 Event까지 처리하지 못할 것이라고 예상했다.

그런데 Spring Kafka의 기본 Error Handler 동작을 확인해보니 예상과 달랐다.

일정 횟수 재시도한 뒤 해당 Event를:

Skip

하고 다음 Event를 처리했다.

즉 Pipeline 자체가 영원히 막히지는 않았다.

대신 더 큰 문제가 있었다.

실패 Event가 사라진다.

는 점이었다.


5.1 문제는 Blocking이 아니라 유실이었다

처음 예상:

Poison Event

↓

계속 실패

↓

Partition Blocking

실제:

Poison Event

↓

Retry

↓

Skip

↓

뒤 Event 계속 처리

↓

실패 Event 유실

가 되었다.

이 결과를 확인한 뒤 실패 Event를 별도의 곳에 보관할 필요가 생겼다.

그래서 최종적으로:

Consumer 실패

↓

Backoff Retry

↓

계속 실패

↓

user-events-dlt

구조를 선택했다.

별도의 Retry Topic까지는 만들지 않았다.

현재 요구사항에서는 일정 횟수 Backoff 후 DLT로 보내는 것만으로 충분했기 때문이다.


6. 역직렬화 실패는 또 다른 문제였다

실패 처리 실험을 하면서 Listener 내부에서 발생하는 예외와 Kafka Record 자체를 읽지 못하는 실패가 다르다는 것도 확인했다.

예를 들어 정상적으로 역직렬화된 Event가 Consumer 내부 로직에서 실패하면 우리가 구성한 DLT 흐름을 탈 수 있었다.

하지만 존재하지 않는 Event Type처럼 역직렬화 자체가 실패하는 Event는 Listener까지 도달하지 못했다.

Kafka Record
    ↓
Deserialize
    ↓
실패
    ↓
Listener 진입 불가

결과적으로 이 Event는 현재 DLT 구조로 격리되지 않고 Partition 처리를 막을 수 있었다.

처음 #1에서 Retry / DLQ를 이야기할 때는:

실패 Event를 DLT로 보내면 된다.

정도로 생각했지만 실제로는 실패 위치에 따라 처리 방식이 달랐다.

이 부분은 현재 프로젝트에서 해결하지 않았다.

ErrorHandlingDeserializer

같은 추가 방식도 검토할 수 있지만, 이번 프로젝트에서는 실제로 확인된 한계로 남겨두었다.


7. Consumer Recovery와 Replay

Consumer를 직접 종료시키고 다시 실행하는 실험도 진행했다.

Consumer가 멈춘 상태에서도 Producer는 Event를 계속 발생시켰다.

Producer
   ↓
Kafka
   ↓
Consumer 종료

그동안 Consumer Lag은:

50

까지 증가했다.

이후 Consumer를 다시 실행하자 마지막 Commit Offset 이후부터 처리를 재개했다.

Lag이 다시 0으로 돌아오는 데 걸린 시간은 약:

428ms

였고 Event 유실은 없었다.


7.1 Replay와 Idempotency

Kafka Offset을 이전 위치로 되돌려 이미 처리한 Event를 다시 읽는 Replay도 수행했다.

Replay를 하면 같은 Event가 다시 Consumer에 전달된다.

하지만 Phase 5에서 적용한 UNIQUE Constraint 때문에:

같은 Event 여러 번 전달

↓

DB 결과

1 Row

로 유지됐다.

이를 통해 Replay와 Idempotency가 서로 별개의 문제가 아니라 연결된 문제라는 것도 확인할 수 있었다.


8. Schema Evolution은 생각보다 단순하게 끝났다

처음에는 Event Schema가 변경되면 Schema Registry가 필요할 수도 있다고 생각했다.

하지만 이번 프로젝트에서는 JSON + schemaVersion만 사용했다.

예를 들어 기존 Event에 Optional Field를 추가했을 때:

{
  "eventId": "event-001",
  "eventType": "PLAY_START",
  "memberId": 100,
  "schemaVersion": 1
}

에서:

{
  "eventId": "event-001",
  "eventType": "PLAY_START",
  "memberId": 100,
  "schemaVersion": 2,
  "payload": {
    "deviceType": "MOBILE"
  }
}

형태로 변경하더라도 기존 Consumer는 문제없이 처리할 수 있었다.

따라서 이번 프로젝트 범위에서는 Schema Registry를 도입하지 않았다.


8.1 다만 schemaVersion을 실제로 판단하지는 않는다

여기에는 한계도 있다.

schemaVersion이라는 필드는 존재하지만 Consumer가:

지원 Version인가?

를 확인하지는 않는다.

예를 들어:

schemaVersion = 99

인 Event도 현재 Consumer는 그대로 저장한다.

즉:

Schema Version 정보는 존재

하지만

Schema Version 정책은 존재하지 않음

상태다.

요구사항을 글자 그대로 본다면 이 부분은 완전히 만족했다고 보기 어렵다.

현재 구조에서는 JSON의 하위 호환성을 확인하는 수준에서 실험을 종료했다.


9. 최종 Baseline과 Final 비교

프로젝트 마지막에는 Phase 2에서 사용했던 것과 같은 조건으로 다시 부하를 발생시켰다.

500 events/sec
20초

Baseline에서는:

Partition 1
Consumer 1

↓

최대 Lag
4,252

가 발생했다.

최종 구조에서는:

Partition 3
Consumer concurrency 3

↓

최종 Lag
0

까지 따라갈 수 있었다.

즉 같은 환경에서:

Baseline
Lag 4,252

↓

Final
Lag 0

라는 차이를 확인했다.

다만 이것을:

Kafka 성능을 개선해서 모든 문제를 해결했다.

라고 표현하면 안 된다.

실제로 1000 events/sec에서는 다시 한계가 나타났고, 하나의 memberId에 Event가 몰리는 Hot Key 상황에서도 병렬 처리 효과가 사라졌다.

따라서 최종적으로 확인한 것은:

현재 환경에서 P3 C3 구조는 500 events/sec를 처리할 수 있지만, 이것이 시스템의 일반적인 최대 성능을 의미하는 것은 아니다.

정도다.


10. 요구사항은 얼마나 지켰을까?

#2에서 가장 중요하게 생각했던 것은 Baseline을 처음부터 완성된 구조로 만들지 않는 것이었다.

초기에는 다음 기능을 넣지 않기로 했다.

Retry Topic

DLQ

Idempotency

Redis Deduplication

Batch Consumer

복잡한 Partition 전략

Schema Registry

Replay API

Phase 1에서는 실제로 이 기능들을 하나도 넣지 않았다.

이후:

중복 재현
→ UNIQUE

실패 재현
→ Backoff + DLT

Consumer Lag 재현
→ Partition / Consumer 조정

순으로 필요한 기능만 추가했다.

반대로 다음 기술은 끝까지 사용하지 않았다.

Redis

Schema Registry

Retry Topic

Batch Consumer

Replay API

필요하지 않았기 때문이다.

이 부분은 처음 세웠던 요구사항과 꽤 잘 맞았다.


10.1 완전히 닫지 못한 요구사항도 있다

모든 요구사항을 완전히 만족한 것은 아니다.

대표적으로 Schema Version 처리다.

요구사항에서는 Consumer가 지원 가능한 Schema인지 판단할 수 있어야 한다고 적었다.

하지만 현재는:

schemaVersion 존재

↓

판단하지 않음

상태다.

또 Consumer 확장도 하나의 JVM에서:

concurrency = 3

으로 Consumer Group Member를 늘리는 실험은 했지만, 실제 애플리케이션 프로세스를 여러 개 실행해서 Rebalance를 확인하는 실험까지는 하지 않았다.

실패 Event 격리 역시 Listener 내부 Poison Event는 DLT로 보내지만, 역직렬화 단계의 실패는 아직 격리하지 못한다.

즉 최종 시스템도 완성형은 아니다.


11. 바이브 코딩 전략은 실제로 효과가 있었을까?

#3에서는 Playback Gate에서 아쉬웠던 부분을 기반으로 바이브 코딩 전략을 다시 정했다.

가장 중요하게 잡은 원칙은:

AI가 기술을 먼저 선택하지 않는다.

문제를 먼저 재현한다.

측정 결과를 본다.

후보를 비교한다.

중요한 결정은 Human Gate를 거친다.

였다.

이 원칙은 이번 프로젝트에서 꽤 잘 작동했다.


11.1 문서 역할을 처음부터 구분했다

이번에는 프로젝트 시작 시점부터:

REQUIREMENTS.md
→ 무엇을 만들어야 하는가

DESIGN.md
→ 현재 시스템 구조

ROADMAP.md
→ 문제 확인 순서

TASKS.md
→ 현재 Phase 작업

adr/
→ 기술 선택 이유

experiments/
→ 실제 측정 결과

로 역할을 나눴다.

Playback Gate에서 문서 역할이 뒤섞였던 것과 비교하면 훨씬 관리하기 편했다.

특히 DESIGN.md에는 미래 구조를 미리 추가하지 않았다.

예를 들어 ROADMAP에 DLT가 등장한다고 해서 처음부터:

Kafka
 ↓
Consumer
 ↓
DLT

구조로 작성하지 않았다.

실제 Phase 6에서 DLT를 선택한 뒤에야 DESIGN에 반영했다.

UNIQUE와 P3 C3도 마찬가지였다.


12. 문제를 먼저 재현한다는 원칙

이번 프로젝트에서 가장 잘 지켜진 원칙이라고 생각한다.

Consumer Lag
→ 먼저 P1 C1에서 쌓이는 것을 확인

Duplicate Event
→ UNIQUE 없이 실제 중복 확인

Failure
→ 기본 Error Handler에서 실제 Skip 확인

Schema
→ Registry 없이 JSON이 어디까지 버티는지 확인

문제를 확인하기 전에 AI가 해결책을 미리 추가하지 않도록 한 것이 중요했다.

그 덕분에 예상과 다른 결과도 많이 얻었다.

예를 들어:

Partition을 늘리면 빨라질 것이다.
→ P3 C1은 오히려 더 안 좋았다.

Consumer를 많이 늘리면 빨라질 것이다.
→ Partition보다 많으면 Idle이 생겼다.

Poison Event는 Partition을 계속 막을 것이다.
→ Listener 실패는 기본적으로 Skip됐다.

UNIQUE는 성능 부담이 클 수 있다.
→ 500 events/sec에서는 충분히 따라갔다.

Schema Registry가 필요할 수 있다.
→ 현재 범위에서는 JSON으로 충분했다.

이런 결과들은 처음부터 기술을 적용했다면 확인하기 어려웠을 것 같다.


13. Human Gate도 필요한 곳에서만 사용했다

모든 코드를 만들 때마다 내가 확인한 것은 아니다.

DTO나 단순 테스트 같은 작업은 AI에게 맡겼다.

대신 시스템의 의미가 달라지는 지점에서는 멈췄다.

실제로 Human Gate를 거친 주요 결정은 다음과 같다.

결정최종 선택

Partition / Consumer P3 C3
Kafka Key memberId 유지
Idempotency event_id UNIQUE
Failure Handling Backoff + DLT
Retry Topic 사용하지 않음
Schema Registry 사용하지 않음

중요한 것은 AI가 추천안을 주는 것 자체를 막는 것이 아니었다.

AI
→ 후보와 장단점 제시

개발자
→ 현재 실험 결과를 보고 선택

이라는 구조를 유지하는 것이 목적이었다.


14. Git을 Experiment History로 사용하는 것은 절반 정도 성공했다

Playback Gate에서는 프로젝트를 거의 다 끝낸 뒤 GitHub에 올렸기 때문에 변화 과정이 Commit History에 잘 남지 않았다.

이번에는 이를 개선하려고 했다.

실제 Commit은 대략 다음 흐름으로 남았다.

feat: implement baseline event pipeline

experiment: measure baseline event pipeline

experiment: reproduce consumer lag

experiment: compare partition and consumer scaling

fix: add idempotent event handling

feat: add failure handling

experiment: verify consumer recovery and replay

experiment: verify schema evolution

docs: complete live event stream experiments

Playback Gate에 비하면:

Baseline
   ↓
문제
   ↓
실험
   ↓
해결

과정이 훨씬 잘 보인다.

다만 #3에서 계획했던 것만큼 완전히 분리되지는 않았다.

원래는:

문제 존재
   ↓
Commit
   ↓
문제 재현
   ↓
Commit
   ↓
해결
   ↓
Commit

정도로 만들고 싶었다.

Consumer Lag은 비교적 잘 나뉘었지만 중복 처리와 실패 처리에서는 문제 재현과 해결이 하나의 Commit에 가까운 형태로 묶였다.

Git에서 해결 직전의 상태를 바로 Checkout해서 재현하기에는 아쉬움이 남는다.

다음 프로젝트에서는:

문제 재현이 성공한 상태 자체를 하나의 checkpoint로 반드시 Commit한다.

정도로 규칙을 더 단순하게 가져가도 될 것 같다.


15. README는 이번에도 늦었다

Playback Gate를 끝내면서 다음 프로젝트에서는 README를 처음부터 Harness의 진입점으로 사용해보려고 했다.

예를 들어 처음 GitHub Repository를 본 사람이:

어떻게 실행하지?

테스트는 어떻게 하지?

Event는 어떻게 넣지?

Consumer Lag은 어디서 확인하지?

Experiment는 어디 있지?

를 README에서 바로 확인할 수 있게 만들려고 했다.

최종 README는 이 역할을 한다.

하지만 실제 개발 과정에서는 Gradle 테스트 방법이나 Event 호출 방법 같은 내용이 프로젝트 후반에 보강됐다.

결국 Playback Gate에서 했던 패턴을 완전히 바꾸지는 못했다.

다음 프로젝트에서는 Phase 1 완료 조건에:

README에서

실행 방법

테스트 방법

기본 요청 방법

확인 가능

을 아예 포함시키는 편이 나을 것 같다.


16. Skill이라고 했지만 사실 Rule에 가까웠다

이번 프로젝트에서는:

.cursor/
└── skills/
    └── live-event-stream/
        └── SKILL.md

를 두고 다음과 같은 규칙을 적었다.

현재 Phase를 벗어나지 않는다.

문제를 미리 해결하지 않는다.

Kafka 설정을 임의로 변경하지 않는다.

Human Gate 없이 중요한 설계를 바꾸지 않는다.

측정 없이 성능이 개선됐다고 하지 않는다.

프로젝트를 진행하고 보니 이것은 특정 작업을 수행하는 방법이라기보다 프로젝트 전체에서 항상 지켜야 하는 Rule에 더 가까웠다.

Skill이라면 오히려:

Consumer Lag 분석

Failure Experiment

Experiment 결과 정리

처럼 특정 작업을 수행할 때 불러오는 Workflow에 더 적합하다.

이번 프로젝트에서는 이미 Skill 방식으로 시작했기 때문에 그대로 유지했다.

하지만 다음 프로젝트에서 같은 구조를 사용한다면:

docs/
+
Project Rule

을 기본 Harness로 두고,

반복되는 작업이 실제로 생겼을 때:

Rule
   ↓
반복 Workflow 발견
   ↓
Skill 추출

방식으로 가져가는 것이 더 자연스러울 것 같다.

이 부분도 실제로 사용해보고 나서야 구분이 명확해졌다.


17. Graph Engineering은 이번에도 필요하지 않았다

처음부터 여러 Agent를 연결하지 않았다.

Planner
   ↓
Coder
   ↓
Test Agent
   ↓
Performance Agent

같은 구조도 만들지 않았다.

실제로 필요한 자동화는:

measure.sh

lag-experiment.sh

같은 반복 실험 Script 정도였다.

개발 과정은 계속:

Harness
+
Human-controlled Loop

형태로 진행했다.

현재 프로젝트 규모에서는 이 정도가 더 단순했다.

여러 Agent를 연결하기 위해 Graph를 만드는 것 자체가 새로운 복잡도가 될 가능성이 더 컸다.


18. 현재 시스템에 남아 있는 한계

Phase 9까지 끝났다고 해서 모든 문제를 해결한 것은 아니다.

실험 과정에서 다음 한계를 확인했다.

1000 events/sec에서는 다시 Consumer Lag 발생

Hot memberId에서는 P3 C3 병렬 처리 효과 감소

역직렬화 실패는 현재 DLT 경로 밖에 존재

schemaVersion을 실제로 검증하지 않음

여러 프로세스 Consumer 확장 실험 없음

processed_event가 계속 증가하면 INSERT 비용 증가 가능

하지만 이번 프로젝트에서는 이것들을 모두 해결하지 않았다.

이전 같았으면:

ErrorHandlingDeserializer 추가

Schema Registry 추가

DB Partitioning 추가

Redis 추가

같은 다음 기술을 바로 생각했을 수도 있다.

하지만 이번에는 현재 실험에서 확인한 한계로만 남겼다.

새로운 요구사항이나 새로운 실험이 필요해졌을 때 다시 문제를 재현하고 Human Gate를 여는 것이 지금의 개발 방식과 더 맞다고 생각한다.


19. Playback Gate에서 고치려고 했던 것은 얼마나 고쳤나?

Playback Gate 회고를 바탕으로 이번 프로젝트에서는 몇 가지를 바꿔보려고 했다.

결과를 정리하면 다음과 같다.

항목목표결과

문서 이름과 역할 시작부터 명확하게 지킴
ROADMAP 시작부터 사용 지킴
Skill 하나만 사용 지킴
Experiment 이름 기술이 아닌 문제 중심 지킴
문제 재현 해결 전에 먼저 확인 지킴
Git 실험 기준점마다 Commit 대체로 지킴
문제 재현/해결 Commit 분리 별도 History 유지 일부 부족
README 처음부터 진입점 후반 보강
Human Gate 중요한 설계 변경에만 적용 지킴
Graph 필요할 때만 사용하지 않음

가장 크게 좋아진 부분은 문서 구조와 문제 재현 방식이었다.

가장 덜 바뀐 부분은 README와 Git checkpoint 습관이었다.


20. 정리

Live Event Stream을 시작하면서 가장 중요하게 생각했던 것은 Kafka 기능을 많이 사용해보는 것이 아니었다.

직접 문제를 만들고:

왜 Lag이 생기는가?

왜 Partition만 늘려서는 안 되는가?

왜 Consumer를 무한히 늘릴 수 없는가?

왜 Event가 중복될 수 있는가?

왜 실패 Event를 따로 보관해야 하는가?

왜 Replay에는 Idempotency가 필요한가?

를 실제 동작과 수치로 설명할 수 있게 되는 것이 목표였다.

Phase 9까지 진행한 지금은 적어도 이 질문들에는 직접 실험한 결과를 가지고 답할 수 있게 됐다.

특히 같은 500 events/sec / 20초 조건에서:

Baseline

Partition 1
Consumer 1

Consumer Lag
4,252


↓

Final

Partition 3
Consumer 3

Consumer Lag
0

이라는 결과를 얻었다.

하지만 가장 의미 있었던 것은 Lag을 0으로 만든 것 자체보다는 그 과정이었다.

Partition을 늘렸는데 더 느려지기도 했고

Consumer를 늘렸는데 Idle이 생기기도 했고

Poison Event가 막힐 줄 알았는데 Skip되기도 했고

Redis가 필요할 줄 알았는데 필요하지 않았고

Schema Registry도 결국 사용하지 않았다.

처음 세운 가설이 틀렸을 때 그것을 실패라고 보지 않고 Experiment 결과로 남긴 것이 이번 프로젝트에서 가장 잘 지킨 원칙 중 하나였다.

바이브 코딩 방식도 같은 결론이다.

AI에게 코드를 많이 작성시키는 것 자체는 어렵지 않았다.

오히려 중요한 것은:

무엇을 만들 것인지 제한하고

↓

현재 문제를 먼저 재현하고

↓

실제 수치를 확인하고

↓

기술 후보를 비교하고

↓

중요한 결정에서는 사람이 개입하고

↓

결과를 다시 검증하는 것

이었다.

다음 프로젝트에서도 기본 원칙 자체는 그대로 가져갈 생각이다.

다만 두 가지는 조금 더 명확하게 가져가려고 한다.

문제 재현이 끝난 상태는
해결 Commit과 반드시 분리한다.

README의 실행 / 검증 안내는
Phase 1부터 관리한다.

그리고 이번에 사용해본 SKILL.md 역시 실제 성격은 프로젝트 Rule에 더 가까웠다.

다음에는 Rule로 기본 제약을 두고, 반복되는 Workflow가 실제로 생겼을 때 Skill로 분리하는 방식도 적용해볼 수 있을 것 같다.

결국 Playback Gate부터 Live Event Stream까지 계속 유지하고 있는 원칙은 같다.

문제를 직접 재현하고, 측정하고, 후보를 비교한 뒤 현재 상황에 맞는 선택을 하고 그 결과를 다시 검증한다.

이번 프로젝트에서는 이 원칙이 Kafka 기반 비동기 Event Pipeline에서도 충분히 작동한다는 것을 확인할 수 있었다.

반응형