개발
개발 카테고리의 글
- 주목이라는 연산 — 트랜스포머로
시리즈의 마지막 편입니다. 신경망 위에 마지막 블록, 어텐션을 올려 트랜스포머를 완성하고, 밑바닥 벡터에서 시작한 한 줄기를 여기서 닫습니다. 어텐션의 핵심은 놀랍도록 익숙합니다. 각 토큰이 다른 토큰들을 얼마나 참고할지를 1편의 내적으로 재고, 3편의 소프트맥스로 합이 1인 주목 분포를 만든 뒤, 그 분포로 값 벡터들을 가중합하는 것이죠. 쿼리·키·밸류를 세우고, 스케일드 닷프로덕트 어텐션 한 식을 유도하며, 왜 루트 d로 나누는지를 4편의 그래디언트 관점으로 설명합니다. 멀티헤드로 여러 관점을 병렬로 보는 이유, 잔차 연결과 층 정규화와 위치 인코딩이 붙은 트랜스포머 블록의 구조, 그리고 이 거대한 모델도 여전히 6편의 역전파와 4편의 경사하강과 3편의 크로스 엔트로피로 다음 토큰을 예측하며 배운다는 것을 봅니다. 이차 복잡도, 위치 인코딩의 한계, 어텐션이 이해가 아니라 통계적 패턴이라는 점을 정직하게 짚고, 일곱 편을 하나로 되짚으며 시리즈를 닫습니다. 'AI의 수학적 기초' 7편이자 마지막 편입니다.
- 비선형으로 — 신경망과 역전파
5편의 선형회귀는 직선(초평면)밖에 못 그렸습니다. 1편에서 예고한 문제, 선형변환을 아무리 합성해도 여전히 선형이라는 벽이 마침내 무대에 오릅니다. 이 편에서는 그 벽을 넘습니다. 먼저 선형 합성이 왜 표현력을 못 늘리는지를 행렬곱으로 증명하고, 그 사이에 비선형 활성화 함수를 끼우면 어떻게 휘어진 함수를 담게 되는지, 시그모이드·tanh·ReLU를 세웁니다. 층을 쌓은 다층 퍼셉트론과 만능근사정리를 짚은 뒤, 순전파로 입력에서 손실까지 계산하고, 이 편의 백미인 역전파를 4편의 연쇄법칙에서 직접 유도합니다. 출력의 오차 신호를 층층이 거슬러 곱해 모든 가중치의 그래디언트를 순전파 한 번의 비용으로 한꺼번에 얻는 그 메커니즘, 그리고 그것이 4편의 경사하강과 만나 완성되는 학습 루프까지요. 비볼록성과 국소최소, 그래디언트 소실·폭발, 만능근사가 학습 가능성을 뜻하지 않는다는 점, 블랙박스 문제를 정직하게 짚습니다. 'AI의 수학적 기초' 6편입니다.
- 가장 단순한 학습 — 선형회귀를 끝까지
1편부터 4편까지 벡터·거리·확률·경사하강을 따로따로 세웠습니다. 이 편에서 그 넷을 처음으로 하나의 완결된 학습에 합칩니다. 선형회귀입니다. 학습은 늘 세 박자예요. 모델을 놓고, 손실을 정의하고, 그 손실을 최소화한다. 선형회귀는 그 삼박자가 가장 투명하게 보이는 학습이죠. 데이터 행렬로 선형 모델을 놓고, 잔차의 제곱노름으로 손실을 정의한 뒤, 왜 하필 제곱오차인지를 3편의 가우시안 오차 가정과 최대가능도로 유도합니다. 그리고 그 손실을 최소화하는 두 길을 끝까지 갑니다. 미분해서 0으로 놓아 얻는 정확해(정규방정식)와, 그 해가 2편의 투영, 즉 정답 벡터를 특징들의 열공간에 내린 그림자라는 기하학적 의미, 그리고 4편의 경사하강으로 같은 답에 이르는 반복적 길입니다. MSE가 볼록이라 경사하강이 전역최소에 닿는다는 것, 두 길을 언제 어떻게 고르는지, 과적합과 릿지·라쏘 정규화가 2편의 L1·L2 단위원과 어떻게 이어지는지까지 봅니다. 선형 가정의 한계, 다중공선성, 이상치 취약성, 외삽 위험을 정직하게 짚습니다. 'AI의 수학적 기초' 5편입니다.
- 미분과 경사하강 — 기계가 '배우는' 방식
3편에서 학습의 목적을 얻었습니다. 손실을 최소로, 로그가능도를 최대로. 그런데 '최소로 만들라'는 목적과 '실제로 최소를 찾는' 방법은 다릅니다. 이 편에서는 그 방법, 기계가 배우는 메커니즘을 세웁니다. 미분을 순간 변화율이자 접선 기울기로 정의하고, 다변수로 확장해 그래디언트를 세운 뒤, 방향도함수가 그래디언트와의 내적이라는 사실과 2편의 코시-슈바르츠 부등식을 이어 왜 그래디언트가 가장 가파르게 증가하는 방향인지를 손으로 증명합니다. 그 반대로 한 걸음씩 내려가는 경사하강법과 학습률의 딜레마, 볼록성과 국소최소·안장점, 전체 데이터 대신 미니배치로 기울기를 근사하는 확률적 경사하강(SGD)이 왜 편향 없는 추정인지를 3편의 기댓값으로 잇고, 합성함수의 연쇄법칙이 어떻게 신경망의 역전파가 되는지까지 예고합니다. 미분 가능성 가정, 학습률 튜닝의 고통, 그래디언트 소실·폭발, 볼록성은 예외라는 점을 정직하게 짚습니다. 'AI의 수학적 기초' 4편입니다.
- 확률과 불확실성 — 분포·기댓값·베이즈
1·2편의 벡터가 '무엇을 아는가'의 언어였다면, 확률은 '무엇을 모르는가'의 언어입니다. AI는 세상을 확실히 아는 게 아니라 확률로 믿고, 데이터를 보며 그 믿음을 갱신하죠. 이 편에서는 그 언어를 밑바닥부터 세웁니다. 확률의 세 공리에서 출발해 확률변수와 분포(이산 pmf·연속 pdf, 베르누이·정규), 기댓값과 분산을 정의하고 분산이 왜 E[X제곱] 빼기 평균제곱인지 손으로 유도합니다. 결합·주변·조건부 확률과 독립을 세운 뒤, 조건부 확률에서 베이즈 정리를 직접 이끌어 내 학습을 '사전 믿음을 데이터로 갱신해 사후 믿음을 얻는 일'로 다시 봅니다. 그리고 가능도와 최대가능도추정(MLE), 로그가능도를 세워, 뒤 편에서 만날 손실함수와 크로스 엔트로피가 왜 여기서 태어나는지를 예고합니다. 빈도주의와 베이지안의 해석 논쟁, 독립 가정과 iid의 위험, 밀도는 확률이 아니라는 함정, 모델 확률이 실제 신뢰도와 다른 보정 문제까지 정직하게 짚습니다. 'AI의 수학적 기초' 3편입니다.
- 거리와 유사도 — 노름과 내적이 재는 것
1편에서 데이터를 벡터로 놓았습니다. 그런데 '두 점이 가깝다', '두 벡터가 닮았다'를 재는 자는 하나가 아닙니다. 이 편에서는 그 자들을 제대로 세웁니다. 노름의 여러 얼굴(L1 맨해튼, L2 유클리드, L-무한대 체비쇼프)과 각 노름의 단위원 모양이 왜 학습의 성격(특히 희소성)을 가르는지, 거리가 진짜 '자'가 되려면 만족해야 하는 거리 공리(비음수·대칭·삼각부등식)가 무엇인지, 그리고 코시-슈바르츠 부등식이 어떻게 코사인 유사도를 -1과 1 사이에 가두는지를 유도합니다. 내적이 만드는 기하로 더 들어가 투영을 정의하고, 단위 벡터에서 유클리드 거리의 제곱이 2(1-코사인)이 된다는 관계를 손으로 증명해 '방향'과 '위치'라는 두 유사도를 하나로 잇습니다. 스케일 문제와 표준화, 고차원에서 거리가 서로 비슷해지는 거리 집중 현상, 내적은 유사도지 거리가 아니라는 점까지 정직하게 짚습니다. 'AI의 수학적 기초' 2편입니다.
- 세상을 벡터로 놓다 — AI가 데이터를 보는 첫 언어
새 시리즈를 엽니다. '에이전트 직접 짜기'가 AI를 어떻게 만드는지의 실전이었다면, 이 시리즈는 AI가 왜 도는지를 수식으로 차근차근 쌓는 이론입니다. 코드가 아니라 수학으로 증명하며 가죠. 그 첫 질문은 이겁니다. AI는 세상을 대체 어떻게 표현하는가? 답은 벡터입니다. 집 한 채든 단어 하나든 이미지 한 장이든, AI는 그것을 실수의 나열, 즉 고차원 공간의 한 점으로 놓습니다. 이 편에서는 벡터가 무엇인지 두 관점으로 세우고, 데이터가 왜 벡터가 되는지, 선형결합·벡터공간·기저로 '표현'이 무엇인지, 내적이 어떻게 유사도의 언어가 되는지, 노름이 어떻게 거리를 재는지, 그리고 행렬이 어떻게 데이터이자 선형변환이 되는지를 유도합니다. 임베딩·신경망 한 층·어텐션이 모두 이 벡터·내적·행렬 위에 선다는 것까지 조망하고, 선형이라는 가정의 한계와 고차원의 저주, 벡터 표현이 곧 모델링 선택이라는 점을 정직하게 짚습니다. 선형대수·확률·미적분의 필요한 조각을 그때그때 처음부터 세우는 'AI의 수학적 기초' 1편입니다.
- 매번 다시 읽히지 않게 — 프롬프트 캐싱으로 재전송 비용 줄이기
3편에서 대화 크기는 요약으로 줄였지만, 매 스텝 다시 보내는 게 하나 더 있었습니다. 안 변하는 접두부 — 도구 선언과 시스템 프롬프트요. 이건 매 요청 재전송될 뿐 아니라 매번 다시 처리(입력 토큰 과금)됩니다. 도구 스키마 JSON은 꽤 크고, 스텝마다 똑같이 재과금되죠. 이번 편에 프롬프트 캐싱으로 이걸 줄입니다. cache_control breakpoint를 도구 선언에 직접 붙여, 접두부를 캐시에 올려두고 다음 요청부터 캐시에서 싸게 읽게 합니다. raw HTTP라 SDK가 감추던 cache_control을 손으로 드러내고, 응답 usage의 cache_read_input_tokens로 캐시가 진짜 히트했는지 증명합니다. 여기서 눈에 안 보이던 전제 하나가 버그로 드러납니다 — 캐시는 접두부가 바이트까지 같아야 히트하는데, Go의 map 순회는 순서가 랜덤이라 도구 선언이 매 요청 달라져 캐시가 절대 히트하지 못했습니다. 정렬로 순서를 고정해 고칩니다. 캐싱이 3편 요약·4편 메모리 주입과 맺는 긴장까지 정직하게 짚습니다. 'AI 에이전트 직접 짜기' 6편입니다.
- 무엇을 직접 짜고 무엇을 맡길까 — 손으로 짠 골격을 공식과 나란히
1편부터 4편까지 에이전트의 뼈대를 프레임워크 없이 손으로 짰습니다. while 루프, 도구와 예산, 컨텍스트 요약, 메모리까지요. 이제 반대 방향으로 봅니다. 원리를 아는 지금, Anthropic이 공식으로 내놓은 것들(SDK의 Tool Runner, Managed Agents, Claude Agent SDK)이 각각 무엇을 감춰 주는지, 그리고 무엇을 직접 소유하고 무엇을 맡기는 게 좋은지를 견줍니다. 시리즈 정신대로 이것도 코드로 증명합니다. 모델 호출을 LLM 인터페이스 하나로 숨겨, 우리가 짠 루프와 정책은 그대로 두고 트랜스포트만 공식 SDK로 갈아 끼울 수 있게 경계를 긋습니다. HTTP 서버 없는 순수 fake로 그 이음매가 진짜 갈린다는 걸 테스트로 보입니다. '직접 짜기'와 '맡기기'는 양자택일이 아니라, 어디에 선을 긋느냐의 문제입니다. 'AI 에이전트 직접 짜기' 5편입니다.
- 요약이 지운 것을 붙잡다 — 에이전트에게 메모리를 주기
3편에서 대화를 요약해 접었지만, 요약은 손실 압축이라 정말 잃으면 안 되는 사실(파일 경로, 확정된 결정)이 함께 날아갈 수 있었습니다. 이번 편에 그걸 붙잡습니다. 에이전트에게 대화 밖에 사는 key-value 메모리를 주고, memory_write·memory_read 도구로 스스로 적고 읽게 합니다. 대화가 접혀도 메모리는 남죠. 그리고 적어 둔 키 목록을 매 스텝 시스템 프롬프트에 주입해, 접힌 뒤에도 모델이 '내가 무엇을 적어 뒀는지' 알게 합니다. 여기서 2편의 병렬 실행과 교차하는 함정이 하나 — 메모리 도구가 병렬로 불리면 map 동시 쓰기로 죽으므로 mutex가 필수이고, go test -race로 증명합니다. 요약은 흐름을, 메모리는 사실을 맡는 역할 분담까지 정리합니다. 'AI 에이전트 직접 짜기' 4편입니다.
- 대화가 눈덩이처럼 불어난다 — 컨텍스트를 요약해 접기
에이전트 루프는 한 바퀴 돌 때마다 대화(messages)에 모델의 말과 도구 결과를 이어 붙입니다. 그런데 매 스텝 그 전체를 다시 API로 보내죠. 그래서 대화는 눈덩이처럼 불어나 토큰이 O(n²)으로 커지고, 비싸지고 느려지다 결국 컨텍스트 윈도우를 넘겨 터집니다. 이번 편에 이걸 능동적으로 다스립니다. 대화 길이를 어림하고, 예산을 넘으면 앞부분을 모델에게 요약시켜 한 줄로 접는 compaction을 Go로 직접 짭니다. Claude Code가 실제로 하는 그 메커니즘이죠. 함정은 tool_use와 tool_result 쌍을 쪼개면 API가 400을 낸다는 것 — 경계를 안전하게 미는 로직까지 테스트로 못 박습니다. 'AI 에이전트 직접 짜기' 3편입니다.
- 도구를 여럿 쥐여주고, 루프가 폭주하지 않게 — 예산과 병렬
1편에서 에이전트가 while 루프 하나임을 봤지만, 그 루프엔 구멍이 둘 있었습니다. 도구가 하나뿐이었고, 종료 조건이 "모델이 멈출 때"뿐이라 모델이 도구를 끝없이 부르면 루프가 안 끝나 비용 폭탄이 되죠. 이번 편에 둘을 메웁니다. 도구를 레지스트리에 여럿 등록해 모델이 알아서 고르게 하고, 한 응답의 여러 도구 호출을 고루틴으로 병렬 실행하며, MaxSteps 예산으로 무한 루프를 막습니다. go test -race로 병렬에 데이터 레이스가 없음까지 증명합니다. 'AI 에이전트 직접 짜기' 2편입니다.
- 에이전트는 결국 while 루프다 — 프레임워크 없이 직접 짜기
AI 에이전트가 요즘 화두지만, LangChain 같은 프레임워크가 그 실체를 두껍게 감싸고 있습니다. Flutter 시리즈에서 provider를 40줄로, flutter_bloc을 60줄로 직접 짜 봤듯, 에이전트도 프레임워크를 걷어내고 직접 짜 보면 정체가 드러납니다. 실은 while 루프 + 도구 호출 + JSON이죠. Go와 raw HTTP로 Claude Messages API 위에 tool-use 루프를 손으로 짜고, mock 서버로 API 키 없이 그 루프를 검증합니다. 새 'AI 에이전트 직접 짜기' 시리즈 1편입니다.
- CRUD의 빈칸 채우기 — 수정도 값 객체로 재검증한다
지금까지 앱은 추가·토글·삭제는 했지만 제목 수정이 없었습니다. CRUD의 U(Update)가 비어 있었죠. 이번 편에 그 빈칸을 채웁니다. 핵심은 수정도 추가와 똑같이 도메인 값 객체(TodoTitle)로 재검증한다는 것 — 빈 제목·과길이는 편집에서도 막혀야 하니까요. 그리고 편집 다이얼로그가 왜 bloc에 직접 접근하지 못하는지(showDialog는 새 라우트라 InheritedWidget 밖), 그래서 다이얼로그는 텍스트만 모으고 타일이 이벤트를 던지는 단방향 패턴까지, 돌아가는 코드와 테스트로 봅니다. Flutter 실전 시리즈 12편입니다.
- LIKE를 버리고 FTS5로 — 진짜 전문 검색
7편부터 계속 미뤄온 숙제입니다. 제목 검색을 LIKE '%…%'로 짜면서 매 편 "정직하게" 절에 "이건 인덱스를 못 타니 진짜 검색은 FTS의 몫"이라고 적어 왔죠. 이번 편에 그걸 갚습니다. SQLite의 FTS5 가상 테이블로 제목을 역색인하고, 트리거로 원본과 동기화하고, LIKE를 MATCH로 바꿉니다. 이미 깔린 DB엔 마이그레이션으로 FTS를 얹고 기존 행을 백필하죠. 그리고 한글 토크나이저의 함정까지 — 돌아가는 테스트와 함께 봅니다. Flutter 실전 시리즈 11편입니다.
- 이미 깔린 DB에 인덱스 더하기 — 스키마 마이그레이션
9편에서 keyset 페이징을 짜며 "인덱스가 있으면 O(log n), 근데 인덱스를 더하려면 스키마 버전을 올려야 하고 그건 다음 숙제"라고 미뤘습니다. 이번 편에 그걸 갚습니다. keyset 정렬 키에 복합 인덱스를 거는 것 자체는 CREATE INDEX 한 줄이지만, 이미 사용자 폰에 깔려 있는 DB엔 그냥 못 넣습니다. onCreate는 새 설치만 타니까요. 그래서 버전을 v1→v2로 올리고 onUpgrade로 데이터를 지키며 인덱스를 더하는 마이그레이션을, 실제로 v1을 v2로 올려 보는 테스트까지 함께 봅니다. Flutter 실전 시리즈 10편입니다.
- offset은 왜 밀리나 — keyset 페이징으로 갈아타기
8편에서 페이징을 offset(LIMIT/OFFSET)으로 짰고, "정직하게" 절에 이렇게 적었습니다 — offset은 페이지 사이에 목록이 바뀌면 항목을 건너뛰거나 중복하고, 그럴 땐 keyset이 답이라고요. 이번 편에서 그 keyset(cursor) 페이징을 실제로 구현합니다. 위치(offset)가 아니라 "마지막으로 본 항목의 정렬 키 이후"를 조회하는 발상, SQLite 행 값 비교로 커서를 한 줄에 담는 법, 그리고 페이지 사이에 앞 항목이 삭제돼도 건너뛰지 않는 걸 돌아가는 테스트로 증명합니다. Flutter 실전 시리즈 9편입니다.
- 전부 올리지 않는다 — 페이징과 무한 스크롤
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 통합·조직 문화·안티패턴까지, 실전에서 통하는 비용 최적화의 전 과정을 담은 교과서형 딥다이브입니다.
- 컨테이너는 어떻게 동작하는가 — namespaces, cgroups, 그리고 이미지의 내부
컨테이너는 가벼운 VM이라는 흔한 오해에서 출발해, docker run 한 줄 뒤에서 리눅스 커널이 실제로 하는 일을 밑바닥부터 파고듭니다. 무엇을 보는지 격리하는 namespaces, 얼마나 쓰는지 제한하는 cgroups, 이미지를 이루는 union 파일시스템과 OCI 표준, runc·containerd로 이어지는 런타임 구조, 그리고 공유 커널이라는 보안의 한계와 그 강화까지 정리한 교과서형 딥다이브입니다.
- 관찰성(Observability) 깊이 이해하기 — 제어이론부터 메트릭·로그·추적, 카디널리티, OpenTelemetry, SLO까지
관찰성을 개요가 아니라 밑바닥부터 파고듭니다. 제어이론에서 온 어원과 정의, 메트릭·로그·추적 각각의 내부 동작(히스토그램과 분위수, 컨텍스트 전파, 샘플링), 관찰성의 심장인 카디널리티, "세 기둥" 모델의 한계와 그 너머, OpenTelemetry의 구조, SLI·SLO·에러 예산과 번레이트 알림, 그리고 비용의 경제학까지 한 편으로 정리한 교과서형 딥다이브입니다.
- 분산 합의, 밑바닥부터 이해하기 — 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로 만드는 실전 전략을 정리합니다.