Blog
인프라 · DevOps · 개발, 그리고 이런저런 기록
- 전부 올리지 않는다 — 페이징과 무한 스크롤
7편의 마지막 숙제였던 페이징을 구현합니다. 목록이 수천 건이면 한 번에 다 올릴 수 없으니, LIMIT/OFFSET으로 한 페이지씩 끊어 오고 바닥에 닿으면 이어 붙이죠. "다음 페이지가 있는지"를 별도 카운트 없이 N+1로 아는 기법, ScrollController로 바닥을 감지하는 법, 그리고 변경 뒤 왜 첫 페이지로 돌아가는지 — 오프셋 페이징의 정합성 한계와 keyset 페이징이라는 대안까지, 돌아가는 코드와 테스트로 봅니다. Flutter 실전 시리즈 8편입니다.
- 미뤄 둔 것을 구현할 때 — SQL 검색과 디바운스
6편에서 "목록이 커지면 검색을 SQL로 내리고 디바운스가 필요하다"고 미뤄 뒀습니다. 이번 편에서 그걸 실제로 구현합니다. 조회 조건을 도메인 값(TodoQuery)으로 만들어 계약을 따라 내려보내고, sqflite가 WHERE/LIKE로 걸러 오게 하고, 2편에서 일부러 안 짰던 이벤트 트랜스포머를 디바운스로 직접 짭니다. 미뤄 둔 결정을 실제로 구현할 때 계층이 그 변경을 감당하는지, 그리고 injectable이 Duration을 주입하려 해서 실제로 데인 함정까지, 돌아가는 코드와 테스트로 봅니다. Flutter 실전 시리즈 7편입니다.
- 검색·필터를 SQL로 안 내리고 화면에 둔 이유
시리즈를 닫았다고 했는데, 앱을 쓰다 보니 검색과 필터가 필요해졌습니다. 지금까지의 반사신경은 "규칙은 도메인에, 저장은 SQL에"였죠(1·4편). 그런데 이번엔 반대로, 검색·필터를 아래 계층으로 내리지 않고 화면(bloc)에 뒀습니다. 목록이 이미 손안에 있는데 키 입력마다 DB를 때릴 이유가 없거든요. 모든 관심사를 밑으로 미는 게 능사가 아니라는 것, 그리고 검색창이 타이핑 중에도 포커스를 안 잃는 게 지하 탐사의 그 재조정 덕이라는 것을, 실제 코드와 테스트로 봅니다. Flutter 실전 시리즈 6편입니다.
- 조립된 앱이 진짜로 도는지 — 통합 테스트로 시리즈를 닫다
계층마다 테스트는 다 초록이었습니다. 그런데 실제 컨테이너로 조립하고 실제 SQLite에 붙였을 때 전부 맞물려 도는지는, 따로 증명해야 합니다. 실제 조립 루트로 앱을 부팅해 bloc→유스케이스→repository→sqflite까지 관통하고, 파일 DB로 '재시작'해 데이터가 살아남는지까지 확인합니다. 그 과정에서 pumpAndSettle이 실제 IO를 못 몰아 무한히 도는 함정도 정직하게 짚고, 다섯 편으로 쌓은 앱을 회고하며 실전 시리즈를 닫습니다. Flutter 실전 시리즈 5편(마지막)입니다.
- 저장소를 sqflite로 갈아 끼우는데 도메인은 한 줄도 안 고쳤다
앱을 끄면 할 일이 사라졌습니다. 인메모리였으니까요. 이제 진짜 SQLite(sqflite)를 붙입니다. 그런데 저장소를 통째로 바꾸는데 도메인도, 유스케이스도, bloc도, 화면도 한 줄을 안 고쳤습니다 — 1편에서 prod/fake로 갈라 둔 그 자리에 데이터소스만 꽂았거든요. 계약을 행 단위(INSERT/UPDATE/DELETE)로 바꾸고, DB 여는 비동기를 @preResolve로 주입하고, 진짜 SQL을 ffi로 테스트하는 과정을, 클린 아키텍처가 값을 하는 순간으로 봅니다. Flutter 실전 시리즈 4편입니다.
- 화면을 붙이다 — BlocProvider를 직접 짜서 트리에 얹기
1편에서 뼈대를, 2편에서 손수 짠 bloc을 만들었지만 둘 다 화면이 없었습니다. 이제 붙입니다. 그런데 bloc을 화면 깊은 곳의 위젯이 어떻게 집고, StreamController는 누가 닫을까요? 답은 지하 탐사에서 이미 팠습니다 — InheritedWidget으로 트리에 얹고(BlocProvider의 정체), StatefulWidget의 수명으로 dispose하죠. StreamBuilder로 sealed 상태를 그리고, 텍스트 필드·체크박스는 이벤트만 되던지는 단방향 흐름을, 실제 코드와 화면째 돌아가는 위젯 테스트로 완성합니다. Flutter 실전 시리즈 3편입니다.
- flutter_bloc을 지우고 BLoC을 60줄로 짠 이유
상태 관리 하면 으레 flutter_bloc을 import하지만, 이번엔 지웁니다. BLoC 패턴의 본질은 패키지가 아니라 "이벤트가 sink로 들어가고 → UI 없는 로직이 돌고 → 상태가 stream으로 나온다" 이 세 줄이거든요. StreamController 두 개로 직접 짜면, 패키지가 감춰 두던 순차 처리·상태 방출이 전부 눈에 보입니다. 3편에서 Provider를 40줄로 짰듯, BLoC을 60줄로 짜서 원리를 드러내고, 날것 입력이 값 객체를 만나는 지점까지 실제 코드와 돌아가는 테스트로 봅니다. Flutter 실전 시리즈 2편입니다.
- UI를 한 줄도 안 짜고 앱을 시작하는 이유 — 뼈대부터 세우기
할 일 앱을 만드는데 첫 편에서 화면을 한 장도 안 그립니다. 대신 도메인·데이터·의존성 주입이라는 뼈대를 먼저 세우죠. 왜냐고요? UI부터 짠 앱은 커질수록 무너지거든요. 실패를 예외가 아니라 값(Either)으로 흘리고, 잘못된 값은 값 객체에서 막고, 시간과 id는 주입해서 테스트를 결정적으로 만드는 — 작지만 완성된 앱의 골격을 실제 코드와 돌아가는 테스트로 세웁니다. Flutter 실전 시리즈 1편입니다.
- 부모 밖 버튼이 안 눌리는 이유 — 히트 테스트
부모 밖으로 삐져나온 버튼은 왜 안 눌리고, Stack에서 위 위젯이 왜 탭을 먼저 먹고, IgnorePointer와 AbsorbPointer는 뭐가 다를까요? 전부 히트 테스트에서 나옵니다 — 포인터가 내려오면 RenderObject 트리를 훑어 "무엇을 맞혔나"를 정하죠. size 안인가 → 자식(앞부터) → 나 자신, 이 세 줄의 규칙과 그 결과가 8편의 아레나로 가는 흐름을 실제 소스와 돌아가는 테스트로 봅니다. Flutter 지하 탐사 시리즈 9편입니다.
- 끌면 탭이 취소되는 이유 — 제스처 아레나
스크롤 리스트 안에서 항목은 탭이 되는데, 끌면 탭이 취소되고 스크롤이 되죠. GestureDetector 두 개가 겹치면 누가 이기고, Listener는 GestureDetector와 뭐가 다를까요? 전부 한 메커니즘에서 나옵니다 — 제스처 아레나. 포인터 하나를 두고 여러 인식기(tap·drag·scroll)가 경쟁하고, 움직임이 승부를 가릅니다. 원시 포인터부터 아레나 resolve/sweep, HitTestBehavior까지 실제 소스와 돌아가는 테스트로 봅니다. Flutter 지하 탐사 시리즈 8편입니다.
- 애니메이션이 도는 법 — Ticker와 AnimationController
AnimationController에 왜 vsync를 넘기고, 왜 dispose를 해야 하고, Timer로 애니메이션을 하면 왜 끊길까요? 전부 한 사실에서 나옵니다 — 애니메이션은 별도 스레드가 아니라, 매 프레임(vsync)마다 값을 조금씩 바꾸는 것입니다. Ticker가 프레임마다 깨우고, AnimationController가 흐른 시간을 0..1 값으로, Tween과 Curve가 그 값을 화면으로 옮기죠. 6편의 프레임 파이프라인 위에서 애니메이션이 어떻게 도는지, 실제 소스와 돌아가는 테스트로 봅니다. Flutter 지하 탐사 시리즈 7편입니다.
- setState부터 픽셀까지 — 한 프레임의 여정
setState를 했는데 화면이 즉시 안 바뀌고, build에서는 위젯 크기를 못 읽고, addPostFrameCallback은 뭔가 나중에 돌죠. 전부 한 사실에서 나옵니다 — 화면은 정해진 순서의 파이프라인으로, 한 프레임에 한 번 만들어집니다. dirty 표시 + 프레임 예약부터 build→layout→paint→합성까지, WidgetsBinding.drawFrame의 실제 순서를 소스와 테스트로 따라가고, 네 번째 트리(Layer)와 16ms 예산까지 봅니다. 앞선 다섯 편이 왜 전부 이 예산을 지키기 위한 것이었는지, 시리즈를 묶는 6편입니다.
- UI가 얼어붙는 이유 — 이벤트 루프와 아이솔레이트
무거운 계산을 하면 UI가 얼어붙고(jank), async를 붙여도 안 풀리고, print 순서는 이상하죠. 전부 한 사실에서 나옵니다 — Dart는 한 스레드(이벤트 루프 하나)에서 돕니다. async는 스레드가 아니라 그 루프에 "이어서 실행"을 예약하는 것일 뿐이에요. 실행 순서(동기→마이크로태스크→이벤트), async 함수의 동기 첫 줄, 프레임이 왜 밀리는지, 그리고 진짜 병렬을 위한 아이솔레이트까지 — 결정적인 순서를 돌아가는 테스트로 증명합니다. Flutter 지하 탐사 시리즈 5편입니다.
- 레이아웃이 터지는 이유 — RenderObject와 제약
A RenderFlex overflowed", "Vertical viewport was given unbounded height", "Column 안에 ListView를 넣으면 터진다"… Flutter 레이아웃 에러는 하나같이 무섭게 생겼지만, 전부 한 규약에서 나옵니다 — 제약(constraints)은 내려가고, 크기(size)는 올라오고, 위치는 부모가 정한다. build 아래의 셋째 트리인 RenderObject 층으로 내려가, 레이아웃 프로토콜·tight와 loose 제약·relayout boundary·RepaintBoundary를 실제 rendering 소스(3.44.8)와 커스텀 RenderBox 테스트로 파고듭니다. Flutter 지하 탐사 시리즈 4편입니다.
- Provider를 40줄로 만들기 — watch와 read의 정체
Provider·Riverpod은 대체 무엇을 해주는 걸까요? 사실 재료는 1편(InheritedWidget 구독)과 2편(동일 인스턴스 스킵)에서 이미 다 봤습니다. 그래서 이번엔 파고들지 않고 직접 만듭니다. 상태관리의 알맹이를 40줄로 구현해, context.watch와 context.read가 사실 프레임워크 메서드 두 개(dependOn / getInherited)일 뿐임을 확인합니다. 실제 동작하는 미니 Provider와 "watch한 것만 리빌드된다"는 증명 테스트까지. Flutter 지하 탐사 시리즈 3편입니다.
- State가 엉뚱한 행에 남는 이유 — 재조정과 Key
리스트에서 항목을 지우거나 순서를 바꿨더니 엉뚱한 행의 입력값·상태가 남는 그 버그. "리스트엔 Key를 주세요", "const가 빠르다" 같은 조언들. 전부 한 메커니즘에서 나옵니다 — 재조정(reconciliation). 부모가 리빌드할 때 새 Widget을 옛 Element에 어떻게 이어 붙이는지, updateChild의 세 갈래와 canUpdate, 리스트의 key 매칭, GlobalKey의 재부모화를 실제 framework.dart(3.44.8) 소스와 돌아가는 테스트로 파고듭니다. Flutter 지하 탐사 시리즈 2편입니다.
- BuildContext 지하 탐사 — Element, 트리, 그리고 단일 스레드
BuildContext는 Flutter에서 가장 자주 쓰면서 가장 덜 이해되는 것입니다. "await 뒤엔 context 쓰지 마라", "of()는 build에서만" 같은 규칙들을 외워서 지키지만 왜 그런지는 모르죠. 이 규칙들은 전부 한 사실에서 나옵니다 — BuildContext는 사실 Element다. 그 Element를 따라 지하까지 내려가, 규칙들이 왜 그렇게 생겼는지 실제 framework.dart 소스와 돌아가는 테스트로 밝혀 봅니다. Flutter 3.44.8 기준. Flutter 지하 탐사 시리즈 1편입니다.
- 평문을 지우며 — 시크릿 관리, 그리고 시리즈를 닫으며
지금껏 배포 매니페스트에 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편입니다.
- 이벤트가 브라우저까지 — 폴링을 SSE로 바꾸다
21편에서 폴링으로 실시간을 흉내 냈습니다. 2초마다 당기는 방식은 지연도 있고 낭비도 있죠. 이번엔 진짜 push로 바꿉니다 — 사가의 이벤트를 브라우저까지 밀어냅니다. NATS→readmodel 프로젝터→SSE 허브→BFF 프록시→브라우저 EventSource. 그리고 그 과정에서 관찰성 미들웨어가 Flusher를 안 물려줘 스트리밍이 죽던 버그를, SSE 샌티 체크가 잡아냈습니다. 이벤트 기반 쇼핑몰 고도화 시리즈 23편입니다.
- 역할이 필요해질 때 — 관리자 페이지와 RBAC
지금까지 모든 사용자는 동등했습니다. 그런데 관리자 페이지를 만들려니 "누가 관리자인가"를 정해야 했습니다 — 역할(role)이 처음 필요해진 순간입니다. JWT에 role을 심어 인증(누구냐)에 인가(무엇을 할 수 있냐)를 더하고, 게이트를 서버(SSR)와 BFF 두 겹으로 두고, 읽기 모델의 집계를 통계 대시보드로, 카탈로그 write를 상품 관리로 끌어왔습니다. 이벤트 기반 쇼핑몰 고도화 시리즈 22편입니다.
- 프런트가 백엔드를 비춘다 — 실시간·검색·재고·세션
핵심 여정(상품→결제→주문)은 있었으니, 이제 백엔드의 강점을 화면에서 살릴 네 가지를 붙였습니다. 실시간 주문 상태로 사가가 움직이는 걸 새로고침 없이 보여주고, 이벤트 전용이던 재고 서비스에 조회 엔드포인트를 열어 품절을 표시하고, URL 쿼리로 검색·정렬을 SSR 하고, 서버 컴포넌트가 세션을 읽어 헤더에 로그아웃·장바구니 배지를 그립니다. 각 기능이 백엔드 개념 하나를 화면으로 끌어냈습니다. 이벤트 기반 쇼핑몰 고도화 시리즈 21편입니다.
- 테스트는 하나의 렌즈가 아니다 — 접근성·시각 회귀·부하
19편에서 기능 테스트로 "동작하는가"는 봤습니다. 그런데 "누구나 쓸 수 있나(접근성), 모양이 안 깨졌나(시각), 부하에 버티나(성능)"는 전혀 다른 렌즈였습니다. 세 가지를 더 붙였습니다 — axe 로 접근성을 스캔하다 라벨 없는 입력을 잡았고, Playwright 스냅샷으로 시각 회귀를 세우다 무작위 상품 순서 때문에 깨져 정렬로 고쳤고, autocannon 으로 부하를 재 p99 24ms 를 확인했습니다. 각 렌즈의 도구와 함정을 정리합니다. 이벤트 기반 쇼핑몰 고도화 시리즈 20편입니다.
- 테스트가 아키텍처를 드러낸다 — Next.js를 모든 층위에서 테스트하기
프런트를 붙였으니(17·18편) 이제 이걸 어떻게 믿을까요. "모든 종류의 테스트를 다 해보자" 마음먹고 시작했는데, 곧 진짜 교훈을 만났습니다 — 테스트가 쉬우려면 구조가 좋아야 한다. 라우트 핸들러가 fetch 를 직접 부르던 코드를, 순수 로직·API 어댑터·세션·얇은 핸들러로 계층을 나눴더니 테스트가 술술 붙었습니다. 유닛부터 컴포넌트(RTL), 라우트 통합, 그리고 Playwright E2E 까지 피라미드를 세운 이야기입니다. 이벤트 기반 쇼핑몰 고도화 시리즈 19편입니다.
- docker compose up 하나로 — Next.js를 풀스택 컴포즈에 넣기
프런트를 붙였으니(17편) 이제 팀 누구든 git clone 하고 docker compose up 하나로 전체를 띄우게 하고 싶었습니다. Go 8서비스 + NATS + Jaeger 에 Next.js 를 한 식구로 넣는 이야기입니다. output standalone 으로 Next 이미지를 작게 뽑고, 멀티스테이지 Dockerfile 로 굽고, 같은 코드가 컨테이너에선 내부 DNS 로, 로컬 개발에선 호스트 포트로 붙게 만드는 한 줄 트릭까지. 그리고 standalone 이 static 을 안 챙겨 화면이 벗겨지는 함정 같은, 직접 데어본 것들도 적었습니다. 이벤트 기반 쇼핑몰 고도화 시리즈 18편입니다.
- 백엔드에 프런트를 붙이니 보이는 것들 — BFF로 CORS를 지우다
지금까지 이 쇼핑몰은 API 뿐이었습니다. curl 로만 두드렸죠. 그런데 진짜 브라우저 프런트엔드(Next.js)를 붙이자마자 첫 벽이 나타났습니다 — CORS. 브라우저는 catalog·customer·cart·ordering 을 제각각 부를 수 없습니다. 흔한 답은 모든 서비스에 CORS 헤더를 바르고 게이트웨이를 세우는 것이지만, Next.js SSR 과 docker-compose 를 만나면 더 깔끔한 길이 있습니다 — BFF. 서버 컴포넌트는 컨테이너 안에서 내부 DNS 로 부르고, 변경은 Next Route Handler 로 통과시켜, CORS 를 코드 한 줄 없이 지웁니다. 이벤트 기반 쇼핑몰 고도화 시리즈 17편입니다.
- 메트릭과 SLO — 무엇을 재고, 어디까지 괜찮은가
8편에서 분산추적으로 "이 요청이 어디서 느렸나"를 봤습니다. 그런데 추적은 개별 사건이라, "지금 시스템 전체가 괜찮은가?"는 한눈에 안 보였습니다. 그 답이 메트릭입니다. 16편에서는 RED(요청률·에러·지연)와 비즈니스·신뢰성 지표를 Prometheus 로 노출하고 Grafana 대시보드로 세운 뒤, SLO·에러 예산·번 레이트로 "어디까지가 괜찮은가"를 숫자로 못박습니다. 이벤트 기반 쇼핑몰 고도화 시리즈 16편입니다.
- 이벤트 스키마 진화 — 옛 이벤트를 어떻게 읽을까
order.placed 이벤트에 필드 하나를 더하려다 문득 멈췄습니다. JetStream 스트림에는 예전 모양의 order.placed 가 이미 잔뜩 쌓여 있고, 읽기 모델은 시작할 때 그 과거를 전부 재생합니다. 이벤트는 DB 처럼 ALTER 로 고쳐 쓸 수 없는 불변의 기록인데, 모양이 바뀌면 옛 이벤트는 어떻게 읽어야 할까요? 15편에서는 두 가지로 답합니다 — 더하기 변경은 관용적 리더로 그냥 소화하고, 깨는 변경은 버전을 올려 업캐스팅으로 옛 이벤트를 최신 모양으로 끌어올립니다. 이벤트 기반 쇼핑몰 고도화 시리즈 15편입니다.
- 죽은 편지함 — 실패한 이벤트는 어디로 가나
이벤트 소비자가 처리에 실패하면 어떻게 될까요? 지금까지 이 쇼핑몰은 그냥 Nak — 무한 재전송이었습니다. 그런데 영원히 실패하는 독성 메시지 하나가 들어오면, 소비자는 그걸 붙들고 CPU만 태우며 뒤를 막습니다. 14편에서는 두 축으로 이 문제를 풉니다 — 일시적 장애는 지수 백오프로 재시도해서 흡수하고, 끝내 실패하는 메시지는 죽은 편지함(DLQ)으로 보내 격리합니다. 잃지도, 막히지도 않게. 이벤트 기반 쇼핑몰 고도화 시리즈 14편입니다.
- 인증과 인가 — 로그인·JWT·본인 것만
지금까지 이 쇼핑몰은 누구나 남의 이름으로 주문하고 남의 주문을 들여다볼 수 있었습니다. customer_id 를 클라이언트가 그냥 적어 보냈으니까요. 13편에서는 이 구멍을 막습니다 — 비밀번호는 bcrypt 로, 로그인은 표준 라이브러리만으로 만든 HS256 JWT 로, 그리고 신원은 오직 토큰에서만. 주문·장바구니를 미들웨어로 감싸 "본인 것만" 만지게 하고, 장바구니→주문 호출에는 토큰을 그대로 이어 흘려 신원을 전파합니다. 이벤트 기반 쇼핑몰 고도화 시리즈 13편입니다.
- 동시성 — 재고에 몰린 요청의 레이스 컨디션
다 만들어 놓고 문득 스스로에게 물었습니다 — "취소와 새 주문 여러 개가 같은 재고에 동시에 몰리면 어떻게 되지?" 확인해 보니 진짜 레이스가 있었습니다. 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와 이벤트 기반 설계를 손으로 익히는 출발점입니다.
- 쿠버네티스 스토리지는 어떻게 동작하는가 — 사라지는 컨테이너에 영속 데이터 붙이기
컨테이너는 사라지면 데이터도 사라지는데, DB 같은 워크로드는 상태를 지켜야 합니다. 이 간극을 메우는 쿠버네티스 스토리지를 밑바닥부터 파고듭니다. 볼륨의 수명, PV와 PVC의 관심사 분리, StorageClass와 동적 프로비저닝, CSI 표준, 볼륨이 파드에 붙기까지의 여정(provision·attach·mount), 접근 모드, 그리고 StatefulSet까지 정리한 교과서형 딥다이브입니다.
- 쿠버네티스 네트워킹은 어떻게 동작하는가 — 패킷이 파드 A에서 파드 B까지
파드 A의 패킷이 다른 노드의 파드 B까지 실제로 어떻게 도달하는지를 밑바닥부터 따라갑니다. 쿠버네티스의 flat 네트워크 모델, network namespace와 veth, 같은 노드·다른 노드 통신(오버레이 vs 라우팅), CNI, Service와 kube-proxy(iptables·IPVS·eBPF), DNS·Ingress·NetworkPolicy까지 정리한 교과서형 딥다이브입니다.
- 쿠버네티스는 내부에서 어떻게 동작하는가 — 컨트롤 플레인과 조정 루프
kubectl apply 한 줄 뒤에서 쿠버네티스가 실제로 하는 일을 밑바닥부터 따라갑니다. 선언적 API와 조정 루프라는 핵심 철학부터 etcd·API server·스케줄러·컨트롤러·kubelet의 역할, 그리고 파드 하나가 뜨기까지의 end-to-end 여정, 나아가 이 설계가 왜 자가 치유와 확장(오퍼레이터)을 가능하게 하는지까지 정리한 교과서형 딥다이브입니다.
- FinOps 깊이 이해하기 — 클라우드 비용을 엔지니어링하는 법
클라우드는 비용의 성격 자체를 바꿔놓았습니다. 쓴 만큼 내는 대신 모르는 만큼 새는 시대에, 비용을 엔지니어링 문제로 다루는 FinOps를 밑바닥부터 정리합니다. 가시성(태깅·단위 경제성)·최적화(rightsizing·커밋먼트·스팟·유휴 자원)·쿠버네티스 FinOps·정책과 IaC 통합·조직 문화·안티패턴까지, 실전에서 통하는 비용 최적화의 전 과정을 담은 교과서형 딥다이브입니다.
- 사고파는 규율 — 매매와 리스크 관리
무엇을 살지 아는 것과 실제로 사고파는 것은 다릅니다. 타이밍을 맞히려는 유혹, 팔기가 사기보다 어려운 이유, 손절과 포지션 크기 같은 리스크 관리, 하락장에서 감정을 다스리는 장치까지 — 투자의 성패를 가르는 마지막 조각을 정리하며 「주식 투자 기초」 시리즈를 마무리합니다.
- ETF·인덱스 투자 — 시장을 통째로 사는 법
개별 종목을 고르는 대신 시장 전체를 통째로 사는 인덱스·ETF 투자를 정리했습니다. 대부분의 액티브 투자가 시장을 못 이기는 이유, ETF를 고를 때 봐야 할 운용보수·추적오차·규모, 위험한 레버리지·인버스 ETF, 그리고 왜 초보에게 유리한지까지. 「주식 투자 기초」 시리즈 5편입니다.
- 컨테이너는 어떻게 동작하는가 — namespaces, cgroups, 그리고 이미지의 내부
컨테이너는 가벼운 VM이라는 흔한 오해에서 출발해, docker run 한 줄 뒤에서 리눅스 커널이 실제로 하는 일을 밑바닥부터 파고듭니다. 무엇을 보는지 격리하는 namespaces, 얼마나 쓰는지 제한하는 cgroups, 이미지를 이루는 union 파일시스템과 OCI 표준, runc·containerd로 이어지는 런타임 구조, 그리고 공유 커널이라는 보안의 한계와 그 강화까지 정리한 교과서형 딥다이브입니다.
- 관찰성(Observability) 깊이 이해하기 — 제어이론부터 메트릭·로그·추적, 카디널리티, OpenTelemetry, SLO까지
관찰성을 개요가 아니라 밑바닥부터 파고듭니다. 제어이론에서 온 어원과 정의, 메트릭·로그·추적 각각의 내부 동작(히스토그램과 분위수, 컨텍스트 전파, 샘플링), 관찰성의 심장인 카디널리티, "세 기둥" 모델의 한계와 그 너머, OpenTelemetry의 구조, SLI·SLO·에러 예산과 번레이트 알림, 그리고 비용의 경제학까지 한 편으로 정리한 교과서형 딥다이브입니다.
- 분산과 포트폴리오 — 계란을 어떻게 나눌까
계란을 한 바구니에 담지 말라는 말의 진짜 의미를 상관관계로 풀고, 종목·업종·자산군·지역·시간이라는 분산의 층위, 과분산의 함정, 자산배분과 리밸런싱까지 정리했습니다. 위험을 다스리는 가장 기본적인 도구를 다루는 「주식 투자 기초」 시리즈 4편입니다.
- 성장주는 다르게 봐야 한다 — PER이 안 통할 때
PER 100배, 심지어 적자인데도 주가가 오르는 성장주. 왜 전통적인 가치 지표가 성장주에는 잘 안 통하는지, 그리고 매출 성장률·시장 규모·단위 경제성·PEG·PSR 같은 다른 렌즈로 성장주를 어떻게 봐야 하는지, 그 위험까지 함께 정리했습니다. 「주식 투자 기초」 시리즈 3편입니다.
- 주식이 싼지 비싼지 어떻게 아나 — PER·PBR·ROE 읽는 법
주가 숫자만으로는 싼지 비싼지 알 수 없습니다. 기업의 이익·자산·수익성 대비로 보는 세 가지 기본 지표 PER·PBR·ROE의 뜻과 계산, 그리고 각 지표가 숨기는 함정을 예시와 함께 정리했습니다. 첫 투자 글에서 말한 "장기엔 가치"를 실제로 가늠하는 첫걸음입니다.
- 요즘 주식, 시작하기 전에 — 무엇을 사느냐보다 어떻게 생각하느냐
주식에 처음 관심이 생긴 분들을 위해, 종목 추천이 아니라 "가격은 왜 움직이는가", "투자와 투기의 차이", "초보가 빠지기 쉬운 함정"과 그 대신 붙잡을 원칙들을 정리했습니다. 개발하다 투자에 관심이 생긴 사람의 시선으로 쓴, 사기 전에 한 번 읽어두면 좋은 글입니다.
- 분산 합의, 밑바닥부터 이해하기 — FLP 불가능성부터 Raft의 안전성 증명, 그리고 etcd까지
분산 시스템에서 "모두가 하나의 값에 동의한다"는 합의가 왜 필요하고 왜 어려운지를 FLP 불가능성과 정족수 원리부터 짚고, Raft의 리더 선출·로그 복제·안전성을 그 증명의 직관까지 깊게 파고듭니다. 멤버십 변경·스냅샷·선형화 가능한 읽기 같은 실전 확장과, etcd·쿠버네티스가 이를 어떻게 쓰는지까지 한 편으로 정리한 교과서형 딥다이브입니다.
- 모놀리식을 쪼개며 배운 것들 — MSA 전환 회고
모놀리식을 마이크로서비스로 분리하며 실제로 부딪힌 문제들을 정리합니다. 서비스 경계를 잘못 그었던 대가, 공유 DB를 나누는 일의 어려움, saga로 분산 트랜잭션을 다룬 방식, 장애 전파와 관찰성, 그리고 다시 한다면 무엇을 다르게 할지까지 — 개요가 아니라 경험으로 적었습니다.
- 서버리스, Lambda는 언제 답인가 — cold start·비용·동시성으로 따져보기
서버리스는 만능이 아니라 트레이드오프입니다. Lambda가 빛나는 워크로드와 오히려 독이 되는 워크로드를, cold start·비용 모델·동시성·실행시간 관점에서 따져보고 의사결정 기준을 정리합니다.
- 쿠버네티스 오토스케일링 제대로 쓰기 — requests·limits부터 HPA·VPA·Cluster Autoscaler까지
requests/limits를 대충 잡으면 오토스케일링이 흔들립니다. QoS 클래스, HPA·VPA·Cluster Autoscaler가 맞물리는 원리, 그리고 CPU limit 논쟁·메모리 OOM 회피 같은 실전 튜닝을 정리합니다.
- GitOps를 ArgoCD로 시작하기 — 선언적 배포와 지속적 동기화
GitOps는 "Git을 인프라의 유일한 진실로 삼고, 클러스터가 그 상태로 스스로 수렴하게 만드는" 운영 모델입니다. ArgoCD의 Application 모델부터 sync wave, ApplicationSet을 통한 규모 확장, 시크릿 관리와 흔한 함정까지 정리합니다.
- 쿠버네티스 멀티테넌시 — 네임스페이스 격리만으로 충분한가
멀티테넌시는 "어디까지 격리하느냐"의 스펙트럼 문제입니다. 네임스페이스가 주는 격리와 못 주는 격리를 RBAC·리소스·네트워크·노드·커널 다섯 축으로 분해하고, 위협 모델에 맞는 격리 수준과 테넌트 온보딩 자동화까지 정리합니다.
- 프로덕션에서 AWS CDK 길들이기 — escape hatch, Aspects, 그리고 안정화 전략
CDK의 L2·L3 추상화는 편하지만 프로덕션에선 발목을 잡습니다. escape hatch로 추상화를 뚫고, Aspects로 횡단 정책을 강제하고, logical ID와 drift를 다스려 CDK를 운영 가능한 IaC로 만드는 실전 전략을 정리합니다.
- 블로그를 시작하며 — 인프라·DevOps 기록을 남깁니다
칸코테크 개발 블로그를 엽니다. 앞으로 인프라와 DevOps를 중심으로, 중급 개발자가 실무에서 부딪히는 문제와 해결 과정을 기록합니다.