Post

Loopers: 10주간의 여정 회고

Loopers: 10주간의 여정 회고

시작하며

10주가 끝났습니다. Loopers라는 이름처럼 반복하고, 부수고, 다시 쌓았습니다. 매주 새로운 문제를 마주했고, 매주 다른 방식으로 틀렸습니다. 그 과정을 돌아봅니다.

Week 1-3: 설계의 중요성

Facade 패턴과 계층 분리

처음 3주는 설계에 대한 감을 잡는 시간이었습니다.

Facade 패턴을 처음 접했을 때는 왜 이렇게 복잡하게 만드는지 이해하지 못했습니다. 멘토의 “주먹 서비스” 비유를 듣고 나서야 설계 의도가 명확해졌습니다. 도메인 서비스는 순수하게 유지하고, 조합 로직은 Facade에 집중시킵니다. 이 분리가 테스트 용이성과 재사용성을 동시에 가져다줍니다.

Entity, VO, Domain Service 구분도 이론이 아닌 코드 품질의 문제였습니다. int amount 대신 Money amount로 표현했을 때, 음수 금액 버그가 객체 생성 시점에 차단됐습니다. 타입이 의도를 표현합니다.

클래스 다이어그램과 시퀀스 다이어그램을 그리고 구현에 들어가면 속도가 달라집니다. 구조가 머릿속에 있으니 코드를 작성할 때 망설임이 없어집니다.

Week 1-3의 교훈: 설계 문서는 코드 작성 전에 작성합니다. 코드를 먼저 쓰고 나중에 맞추는 건 뒤에서 달리는 것입니다.

Week 4-6: 동시성과 현실의 벽

Race Condition, 락, Circuit Breaker

이론으로 알던 동시성 문제를 실제로 마주쳤습니다.

재고, 쿠폰, 포인트를 모두 비관적 락으로 처리하다가 성능 문제를 발견했습니다. 도메인마다 충돌 확률이 다르다는 걸 이해하고 나서 쿠폰에만 낙관적 락으로 전환했습니다. “모든 락은 같다”는 생각이 틀렸습니다.

Circuit Breaker는 이론으로는 간단합니다. PG 장애가 전체 시스템을 마비시키는 걸 보고 나서야 실제로 필요성을 느꼈습니다. 타임아웃으로 실패했는데 PG에선 결제가 완료된 경우를 처리하는 Reconcile 스케줄러까지 만들고 나서야 장애 대응의 복잡성을 실감했습니다.

Week 4-6의 교훈: 동시성 제어는 도메인의 특성을 이해한 후 선택해야 합니다. 장애 대응은 하나의 솔루션으로 끝나지 않습니다.

Week 7-9: 이벤트 기반 확장

ApplicationEvent, Kafka, Outbox Pattern, 멱등성

저는 이 구간에서 가장 많이 배웠습니다.

2.8초짜리 트랜잭션을 ApplicationEvent로 280ms로 줄였습니다. 핵심과 부가를 분리하는 기준이 생겼습니다. 그런데 메모리 기반 이벤트의 한계가 바로 보였습니다. 서버 재시작 시 유실, 순서 보장 불가, 재시도 없음.

Kafka를 도입하면서 새로운 문제가 생겼습니다.

전환점: “메시지는 언제든 실패할 수 있다”

이 사실을 받아들이고 나서 설계가 달라졌습니다. 실패를 막으려는 게 아니라, 실패해도 올바르게 복구할 수 있는 구조를 만들어야 합니다.

Outbox Pattern 도입 이유

DB 트랜잭션과 Kafka 발행은 원자적으로 처리할 수 없습니다. 두 시스템이 같은 트랜잭션을 공유하지 않기 때문입니다.

선택지는 두 가지였습니다.

1. 2PC (Two-Phase Commit)

  • 분산 트랜잭션
  • 구현 복잡도 매우 높음
  • 성능 저하

2. Transactional Outbox Pattern

  • DB에 이벤트를 함께 저장 (같은 트랜잭션)
  • 스케줄러가 Outbox를 읽어 Kafka에 발행
  • 구현 비용 적당
  • At Least Once 보장

Outbox Pattern을 선택했습니다. 완벽한 원자성 대신 “Eventually Consistent”를 선택한 겁니다. Consumer의 멱등성으로 중복 처리를 막으면, At Least Once + Idempotency = Exactly Once Semantics가 됩니다.

멱등성의 중요성

같은 이벤트가 두 번 처리되면 어떻게 되는가? 이 질문을 모든 컨슈머에게 던졌습니다.

  • 쿠폰 사용: 두 번 처리하면 에러 발생 → event_handled 테이블로 해결
  • 결제 데이터 전송: 두 번 전송되면 중복 → 결제 키 기준 중복 체크
  • 데이터 웨어하우스: 두 번 삽입되면 집계 오류 → UPSERT 사용

모든 컨슈머가 멱등해야 한다는 원칙을 팀 전체가 공유했습니다.

Week 10: 배치 시스템

배치 처리는 실시간 처리와 다른 트레이드오프를 가집니다.

실시간 vs 배치 하이브리드

모든 걸 실시간으로 처리하려는 욕심을 버렸습니다. 분석용 집계 데이터는 배치로, 사용자에게 즉각 보여야 하는 데이터는 실시간으로. 각각의 장점을 취하는 하이브리드 구조가 현실적입니다.

실시간 랭킹도 하이브리드였습니다. 점수 업데이트는 실시간(Redis), 집계와 콜드 스타트 처리는 스케줄러(배치).

마치며

10주 동안 가장 많이 한 말은 “이게 왜 안 되지?”였습니다. 그리고 이유를 찾고 나면 “아, 그래서 이렇게 설계하는구나”가 따라왔습니다.

실무는 완벽한 코드가 아니라 적절한 Trade-off입니다.

  • 비관적 락 vs 낙관적 락: 완벽한 답은 없습니다. 도메인에 따라 다릅니다.
  • Exactly Once vs At Least Once + Idempotency: 전자가 더 강한 보장이지만, 후자가 더 현실적입니다.
  • 실시간 vs 배치: 둘 다 쓰는 게 답일 때가 많습니다.

완벽한 설계를 처음부터 찾으려는 욕심을 내려놓고, 현재 문제를 가장 단순하게 해결하는 방법을 찾는 게 더 빠릅니다. 그리고 문제가 커지면 그때 더 나은 방법으로 교체하면 됩니다.

10주 동안 함께한 팀원들과 멘토에게 감사합니다. 혼자였다면 절반도 못 배웠을 것입니다.

This post is licensed under CC BY 4.0 by the author.