개발 › 실전·튜토리얼
개발 › 실전·튜토리얼 카테고리의 글
- 평문을 지우며 — 시크릿 관리, 그리고 시리즈를 닫으며
지금껏 배포 매니페스트에 DB 비밀번호·JWT 키를 평문으로 커밋하며 "⚠️ 데모용"으로 넘어갔습니다. 마지막 편에서 그 부채를 갚습니다. 시크릿을 SOPS+age로 암호화해 git에 두되, 값만 암호화하고 개인키는 절대 커밋하지 않으며, 복호화는 배포 시점 파이프로만 합니다. 그리고 암호화만으론 부족하니 — 운영에서 기본 JWT 키로 뜨면 부팅을 거부하는 심층 방어까지 넣습니다. 마지막으로, DDD 설계부터 여기까지 온 이 시리즈를 회고하며 닫습니다. 이벤트 기반 쇼핑몰 고도화 29편(완결)입니다.
- 돌려놓고 보이게 — 이벤트 메트릭·k8s 배포·부하 게이트 CI
24~27편에서 "정확히 N번째"를 정확·견고·분산·다중 인스턴스로 만들었습니다. 이제 운영으로 넘깁니다 — 이벤트가 잘 돌아가는지 메트릭·대시보드로 보이게 하고, 여러 대를 쿠버네티스에 배포하고, 성능 회귀를 CI 부하 게이트로 자동으로 막습니다. 기술 지표(RED)만으론 "이벤트가 잘 되고 있나"를 못 봅니다. 응모율·현재 순번·처리 지연·당첨·종료를 비즈니스 메트릭으로 노출하고, promotion을 replicas 2로 올리고, 부하 테스트에 p99·오류 임계값을 걸어 넘으면 빌드를 깨뜨립니다. 이벤트 기반 쇼핑몰 고도화 28편입니다.
- 정말 여러 대로 — 내구 접수·SSE 팬아웃·분산락 종료 배치
26편에서 레이트리밋을 Redis로 공유하고 결과를 SSE로 밀었습니다. 그런데 "여러 대"에는 아직 구멍 셋이 남습니다 — 큐 접수가 재시작에 유실될 수 있고, SSE는 인스턴스 경계를 못 넘고, 종료 배치는 여러 대가 중복으로 돕니다. 이번 편에서 셋을 닫습니다. 접수를 JetStream 스트림에 영속시켜 재시작에도 잃지 않고(그 과정에서 스트림 누락으로 발행이 안 되던 잠재 버그도 잡고), 배정 결과를 코어 pub/sub로 팬아웃해 어느 인스턴스에 SSE가 붙어 있든 닿게 하고, Redis 분산락으로 종료 배치를 정확히 한 대만 돌립니다. 이벤트 기반 쇼핑몰 고도화 27편입니다.
- 여러 대로 나눠도 — 분산 레이트리밋·SSE 통지·부하로 재본 큐 완충
25편에서 아웃박스·큐 완충·레이트리밋으로 이벤트를 견고하게 만들었습니다. 그런데 서버를 여러 대로 늘리면 인메모리 레이트리밋은 인스턴스마다 따로 세어 구멍이 납니다. 이번 편에서 셋을 운영급으로 올립니다. 레이트리밋을 Redis 토큰 버킷으로 옮겨 replica 들이 한도를 공유하게 하고, 큐 모드의 202 접수 뒤 당첨 결과를 폴링 대신 SSE로 실시간 push하고, 마지막으로 부하 테스트로 큐 완충이 정말 효과가 있는지 숫자로 확인합니다 — sync p99 130ms vs queue p99 7ms. 이벤트 기반 쇼핑몰 고도화 26편입니다.
- 당첨자를 놓치지 않으려면 — 아웃박스·큐 완충·어뷰징 방어
24편에서 "정확히 1000번째"를 빈틈없는 카운터로 잡았습니다. 그런데 실서비스로 밀어 보면 구멍 세 개가 남습니다 — 당첨 통지가 유실될 수 있고(커밋 후 발행), 시작 순간 한 행에 스파이크가 몰리며, 상금을 노린 어뷰징이 들어옵니다. 이번 편에서 셋을 닫습니다. 당첨 이벤트를 응모와 같은 트랜잭션에서 아웃박스에 적재해 "커밋됐으면 반드시 발행"으로 만들고, 접수와 순번 배정을 큐로 분리해 스파이크를 흡수하고, 토큰 버킷 레이트 리밋으로 한 주체의 요청 폭주를 막습니다. 이벤트 기반 쇼핑몰 고도화 25편입니다.
- 정확히 1000번째 — 이벤트 당첨자를 어떻게 정할까
특정 시각부터 시작해, 정확히 1000번째로 응모한 사람에게 10만 원을 준다." 말은 쉬운데, 스파이크가 몰리는 순간 이건 분산 시스템의 함정이 됩니다. 무엇이 '1000번째'인지부터, DB 시퀀스가 왜 틀리는지(롤백 gap), 낙관적 락의 재시도 폭주, Redis INCR·큐 직렬화·샤딩 카운터의 장단점을 하나씩 따져 봅니다. 그리고 빈틈없는(gapless) 카운터 행으로 정확성을 잡고, 멱등성과 당첨 통지의 정확히-한번까지 마무리합니다. 이벤트 기반 쇼핑몰 고도화 24편입니다.
- 동시성 — 재고에 몰린 요청의 레이스 컨디션
다 만들어 놓고 문득 스스로에게 물었습니다 — "취소와 새 주문 여러 개가 같은 재고에 동시에 몰리면 어떻게 되지?" 확인해 보니 진짜 레이스가 있었습니다. 1000건 동시 예약에 예약 86건이 유실됐죠. 12편에서는 이 레이스를 레이스 감지기로 재현하고, 원자적 Update(인메모리 뮤텍스, Postgres 행 잠금)로 뿌리부터 고칩니다. 동시성은 터지기 전엔 안 보입니다. 이벤트 기반 쇼핑몰 고도화 시리즈 12편입니다.
- 환불과 반품 — 배송 뒤에 시작되는 사가
고도화 2편의 보상은 주문이 완성되기 "전"의 실패를 되돌렸습니다. 하지만 배송까지 끝난 주문을 손님이 반품하겠다면? 11편에서는 배송 완료(SHIPPED) 주문에 대한 사후 보상 사가를 붙입니다 — 반품을 요청하면 결제는 환불하고 재고는 다시 채우고 주문은 REFUNDED가 되도록. 주문 흐름 중의 롤백과, 완결된 주문을 되감는 반품이 어떻게 다른지 봅니다. 이벤트 기반 쇼핑몰 고도화 시리즈 11편입니다.
- 검색과 집계 — 하나의 스트림, 여러 읽기 모델
7편의 읽기 모델은 "내 주문 목록"을 답했습니다. 하지만 운영자는 "배송중인 주문만", "이번까지 매출은?" 같은 걸 묻습니다. 10편에서는 같은 주문 이벤트 스트림에서 상태 필터 검색과 상태별 건수·매출 집계 뷰를 더합니다. 집계는 이벤트가 흐를 때마다 증분으로 유지되는 또 하나의 프로젝션입니다. 하나의 이벤트 흐름에서 여러 읽기 모델을 뽑는 CQRS의 진짜 힘을 봅니다. 이벤트 기반 쇼핑몰 고도화 시리즈 10편입니다.
- 아웃박스 신뢰성 — 다중 replica에서 중복 없이 발행하기
주문 서비스를 replica 2개로 굴리면, 아웃박스 릴레이도 둘이 됩니다. 둘이 같은 이벤트를 두 번 발행하면? 지금까지는 소비자 멱등성이 겨우 흡수했지만, 낭비이자 위험입니다. 이 편에서 뿌리부터 잡습니다 — JetStream 발행측 dedup(Nats-Msg-Id)과 FOR UPDATE SKIP LOCKED 행 잠금으로, 여러 릴레이가 경쟁해도 각 이벤트가 정확히 한 번만 나가게 합니다. 그리고 그 과정에서 만난 동시 스키마 생성 경합까지. 이벤트 기반 쇼핑몰 고도화 시리즈 9편입니다.
- 분산추적 — OpenTelemetry로 흐름을 눈으로 보다
8편에서 상관 ID로 로그를 이었지만, 그건 흩어진 로그를 손으로 꿰는 일이었습니다. OpenTelemetry 분산추적으로 이를 정식화합니다 — trace/span을 만들고 W3C traceparent를 HTTP와 이벤트 경계 모두로 전파해, 하나의 주문이 여러 서비스를 지나는 전체 흐름을 Jaeger에서 한눈에 봅니다. 특히 비동기 이벤트 경계를 넘는 추적이 핵심입니다. 이벤트 기반 쇼핑몰 고도화 시리즈 8편입니다.
- 읽기 모델 강화 — CQRS로 "내 주문 목록"을 싸게
회원과 장바구니까지 붙였지만, 정작 "내 주문 목록"을 보여줄 방법이 없습니다. 주문 서비스는 주문 ID로만 저장하니 회원별 조회가 비싸죠. 7편에서는 CQRS의 읽기 쪽을 본격적으로 세웁니다 — 주문 이벤트를 모아 조회에 최적화된 비정규화 뷰를 만드는 읽기 모델 서비스를 두어, 쓰기와 읽기를 분리합니다. 이벤트 기반 쇼핑몰 고도화 시리즈 7편입니다.
- 회원과 장바구니 — 사용자를 다시 주인공으로
지금까지 우리 쇼핑몰엔 정작 손님이 없었습니다. 아무 customer_id나 지어내 곧바로 주문부터 시작했죠. 6편에서는 회원(customer)과 장바구니(cart) 컨텍스트를 더해 "가입 → 담기 → 결제(checkout)"라는 진짜 쇼핑 흐름을 얹고, 그 결제가 기존 이벤트 사가로 이어지게 합니다. 그리고 여기서 배웁니다 — 모든 통합이 이벤트일 필요는 없다는 걸. 이벤트 기반 쇼핑몰 고도화 시리즈 6편입니다.
- 영속 스트림 — JetStream과 내구 소비자
아웃박스로 이벤트가 브로커까지 확실히 나가게 했지만, 브로커 자체(core NATS)는 여전히 무영속입니다. 소비자가 죽어 있는 사이 발행된 이벤트는 그냥 사라지죠. 이 편에서 NATS JetStream으로 컨텍스트별 영속 스트림을 만들고, 내구 소비자(durable consumer)로 각 서비스가 자기 위치를 기억하게 하여, 늦게 붙거나 잠깐 죽어도 놓치지 않는 진짜 at-least-once 전달을 완성합니다. 이벤트 기반 쇼핑몰 고도화 시리즈 5편입니다.
- 신뢰할 수 있는 이벤트 — 아웃박스와 멱등성 (고도화 완결)
이벤트 기반 시스템에는 두 개의 조용한 폭탄이 있습니다. 상태를 저장하고 이벤트를 발행하는 사이에 죽으면 둘이 어긋나는 dual-write 문제, 그리고 재전송으로 같은 이벤트가 두 번 처리되는 문제. 마지막 편에서 트랜잭셔널 아웃박스로 저장과 발행을 원자적으로 묶고, 이벤트 ID 기반 멱등 소비로 중복을 견디게 하여, 쇼핑몰을 진짜 신뢰할 수 있게 다듬으며 시리즈를 마무리합니다. 이벤트 기반 쇼핑몰 고도화 시리즈 완결편입니다.
- 상품 카탈로그 — 가격의 주인은 누구인가
지금까지 상품은 그냥 문자열 ID였고, 가격은 주문 요청이 직접 실어 보냈습니다. 클라이언트가 가격을 정한다는 건 사실 위험한 구멍이죠. 3편에서는 상품과 가격을 소유하는 카탈로그 컨텍스트를 만들고, 주문 서비스가 클라이언트 가격을 믿는 대신 카탈로그의 권위 있는 가격을 조회하도록 리팩터링합니다. 동기 호출 대신 이벤트로 갱신되는 로컬 가격 프로젝션(읽기 모델·CQRS)을 두어, 결합을 느슨하게 유지합니다. 이벤트 기반 쇼핑몰 고도화 시리즈 3편입니다.
- 배송과 완전한 보상 — 주문을 배송까지, 실패는 되돌리기
고도화 1편에서 주문을 확정까지 흐르게 했지만, 두 구멍이 남았습니다. 결제가 실패해도 잡아 둔 재고가 그대로였고, 배송이 없어 주문이 CONFIRMED에서 멈췄죠. 2편에서 배송 서비스를 붙여 주문을 SHIPPED까지 보내고, 재고 서비스가 예약을 기억하게 만들어 결제 실패 시 예약 재고까지 되돌리는 완전한 보상으로 사가를 양방향 모두 닫습니다. 이벤트 기반 쇼핑몰 고도화 시리즈 2편입니다.
- 사가를 닫다 — 결제 서비스와 주문의 완주
8편 시리즈로 만든 쇼핑몰은 사실 주문이 PLACED에서 멈춰 있었습니다. 재고는 예약되지만 그 뒤로 아무 일도 일어나지 않았죠. 이 심화 시리즈 1편에서 결제(목업) 서비스를 붙이고 주문 컨텍스트가 이벤트에 반응하게 만들어, 주문이 PLACED→PAID→CONFIRMED로 실제로 완주하고 재고 부족 시 자동 취소되도록 사가를 닫습니다. 코레오그래피 사가와 보상 트랜잭션을 진짜로 도는 코드로 확인합니다.
- CI/CD와 관찰성 — 자동화하고, 꿰뚫어 보기 (완결)
시리즈의 마지막 편입니다. 지금까지 시스템은 돌고, 확장되고, 상태를 지키게 됐지만 배포는 수작업이고 무슨 일이 일어나는지 들여다볼 눈이 없었습니다. 관찰성으로 상관 ID를 이벤트에 실어 주문 하나를 여러 서비스에 걸쳐 추적하고 Prometheus 메트릭을 노출하며, GitHub Actions로 빌드·테스트·이미지까지 파이프라인을 세웁니다. 그리고 8편에 걸친 여정을 관통한 하나의 원칙을 돌아봅니다. 이벤트 기반 쇼핑몰 시리즈 완결편입니다.
- 상태·설정·스케일링 — 404를 없애고 자동으로 늘리기
6편에서 replica를 2개로 늘리자 인메모리 저장소 탓에 GET이 404를 뱉던 문제를, 7편에서 정면으로 풉니다. 주문 저장소를 PostgreSQL로 옮겨(StatefulSet·PVC) replica가 상태를 공유하게 하고, ConfigMap·Secret으로 설정을 분리하고, liveness·readiness probe로 건강을 확인하고, HPA로 부하에 따라 파드를 자동으로 늘립니다. testcontainers로 진짜 Postgres에 대해 저장소를 TDD하는 법까지. 이벤트 기반 쇼핑몰 시리즈 7편입니다.
- 로컬 쿠버네티스에 배포 — Deployment·Service·Ingress
5편에서 만든 컨테이너 이미지를 로컬 쿠버네티스(kind)에 올립니다. Pod·Deployment·Service·Ingress가 각각 무엇을 하는지, 이미지를 kind에 로드하는 법, 서비스가 DNS 이름으로 서로를 찾는 법을 실제로 배포하며 짚습니다. 그리고 replica를 2개로 늘리자 드러난 인메모리 상태의 한계 — 같은 주문을 GET하면 404가 섞이는 현상까지 정직하게 마주합니다. 이벤트 기반 쇼핑몰 시리즈 6편입니다.
- 컨테이너에 담기 — 멀티스테이지로 작고 안전하게
4편까지 만든 두 서비스는 아직 제 노트북에서 go run 으로만 돕니다. 5편에서는 멀티스테이지 Dockerfile로 두 서비스를 정적 바이너리 이미지로 만들고, distroless로 16MB짜리 비루트 이미지를 얻습니다. 레이어 캐시, BuildKit 캐시 마운트, 하나의 Dockerfile을 ARG로 재사용하기, 그리고 compose로 브로커까지 한 번에 띄워 컨테이너 안에서 이벤트 흐름을 확인합니다. 쿠버네티스로 가는 첫걸음, 이벤트 기반 쇼핑몰 시리즈 5편입니다.
- 이벤트로 컨텍스트 잇기 — EDD와 결과적 일관성
3편까지 만든 주문 서비스의 이벤트는 로그로 찍히기만 할 뿐 아무도 듣지 않았습니다. 4편에서는 NATS 메시지 브로커를 붙여 OrderPlaced 이벤트를 발행하고, 새 재고(inventory) 바운디드 컨텍스트가 이를 구독해 재고를 예약하도록 잇습니다. event-carried state transfer, 컨텍스트 간 계약, 보상 트랜잭션(saga), 결과적 일관성, 그리고 임베디드 NATS로 이벤트 흐름을 TDD하는 법까지. 이벤트 기반 쇼핑몰 시리즈 4편입니다.
- 유스케이스와 API — 도는 서비스 완성하기
2편에서 테스트로 덮은 순수한 도메인 바깥에, 이번엔 애플리케이션 계층(유스케이스)·인프라 어댑터(저장소·이벤트 발행)·HTTP API·조립 루트를 얹어 진짜로 요청이 도는 서비스를 완성합니다. 포트와 어댑터(헥사고날)로 도메인을 프레임워크로부터 지키고, 이번에도 테스트를 먼저 쓰는 TDD로 갑니다. 이벤트 기반 쇼핑몰 시리즈 3편입니다.
- TDD로 Go 도메인 모델링하기 — 테스트가 이끄는 애그리거트
1편에서 그린 DDD 설계를, 이번엔 TDD(테스트 주도 개발)로 Go 코드로 옮깁니다. 실패하는 테스트를 먼저 쓰고(Red), 통과시키고(Green), 다듬는(Refactor) 사이클로 값 객체와 Order 애그리거트의 불변식을 하나씩 쌓아 올립니다. 도메인 규칙을 실행 가능한 명세로 만드는, 이벤트 기반 쇼핑몰 시리즈 2편입니다.
- DDD로 쇼핑몰 도메인 설계하기 — 코드 한 줄 전에 그리는 것들
이벤트 기반 쇼핑몰을 Go로 직접 만드는 실전 시리즈의 첫 편입니다. 코드를 짜기 전에, 이벤트 스토밍으로 도메인을 탐색하고 유비쿼터스 언어를 정하고 bounded context(상품·주문·재고·결제·배송)를 나눕니다. 그리고 Order 애그리거트의 불변식과, 주문이 흐르는 도메인 이벤트·사가까지 설계합니다. DDD와 이벤트 기반 설계를 손으로 익히는 출발점입니다.