핵심 한 줄

기능을 하나 더 만드는 게 아니라 틀리기 쉬운 지점을 골라 반복한다. 백엔드 설계는 결국 세 개의 질문으로 압축된다 — “불변식을 누가 지키나(L1·L2)”, “동시에 오면(L3 경합)”, “두 번 오면 / 트랜잭션 밖으로 나가면(L3 경계)“. 이 시리즈는 값 객체 하나에서 시작해 카타가 진행되는 만큼 계속 자란다. 완결이 아니라 진행 중인 기록이다.

시리즈 — 커머스 백엔드를 카타로 쌓다 (계속 연재)


1. 왜 기능이 아니라 카타인가

토이 프로젝트를 하나 더 만드는 건 쉽다. CRUD를 붙이고, 화면을 그리고, “동작한다”고 말한다. 하지만 동작하는 것과 틀리지 않는 것은 다르다. 커머스 백엔드가 실제로 무너지는 지점은 화면이 아니라 보이지 않는 곳이다 — 두 사람이 마지막 재고 하나를 동시에 사고, 결제 취소 요청이 네트워크 재시도로 두 번 도착하고, 트랜잭션이 외부 PG 응답을 기다리며 커넥션을 붙잡는다.

그래서 기능을 넓히는 대신 틀리기 쉬운 지점 하나하나를 카타(kata, 반복 연습)로 골랐다. 각 편은 “이런 걸 만들었다”가 아니라 **“여기서 이렇게 틀렸고, 그래서 이렇게 바꿨다”**의 기록이다.

2. 세 개의 축 — 난이도는 기능이 아니라 사고

카타를 난이도로 나누는 기준은 코드량이 아니라 어떤 사고를 요구하는가였다.

  • L1 — 값과 불변식. 돈을 double로 두지 않기. 값 객체가 스스로를 방어하기. “이 값이 깨질 수 있나?”
  • L2 — 애그리거트. 여러 값이 모인 덩어리의 일관성을 누가 지키나. 불변식이 서비스로 새지 않게. “경계는 어디인가?”
  • L3 — 경합과 경계. 여기서부터 진짜다. “동시에 오면? 두 번 오면? 트랜잭션 밖으로 나가면?” 동시성·멱등성·외부호출이 전부 이 축에 있다.

이 세 질문이 시리즈의 등뼈다. 뒤로 갈수록 답이 하나가 아니라 트레이드오프가 된다.

3. 지금까지 밟은 길

각 편은 앞 편이 드러낸 문제에 떠밀려 나온다:

문제배우는 것
1돈을 long으로 더하다값 객체·불변·반올림
2총액 계산이 서비스로 샌다애그리거트 루트
3재고 100에 주문 150이 동시에낙관/비관/원자적 UPDATE
4취소 요청이 두 번 도착멱등성·유니크 제약
5save()가 INSERT를 안 했다Persistable·merge 함정
6중복이 락에서 기다린다IN_PROGRESS 상태기계
7트랜잭션이 HTTP를 기다린다외부호출 경계
8타임아웃 = 실패?UNKNOWN이라는 세 번째 상태
9목록 하나에 쿼리가 1+NN+1·fetch join
105만 행 풀스캔EXPLAIN 읽기·인덱스
11같은 걸 계속 다시 묻는다캐시 두 층

여기서 멈추지 않는다. 날짜 경계(반품 기한), 할인 배분의 잔돈, 주문 상태머신의 전이 가드 — 카타가 이어지는 만큼 편도 이어진다. 이 표는 앞으로 늘어난다.

4. 전제

  • 스택은 Java 21 · Spring Boot · JPA · MySQL. 하지만 원리는 스택을 넘는다 — 유니크 제약으로 중복을 막는 발상, “느린 건 트랜잭션 밖으로”라는 감각, “응답 못 받음 ≠ 실패”라는 외부 세계의 불확실성은 언어가 바뀌어도 남는다.
  • 모든 편은 테스트나 실행계획으로 검증한다. “그럴 것이다”가 아니라 “이렇게 나왔다”로 말한다.

시작은 가장 작은 벽돌 — 이다.

01.돈을 primitive로 두면 언젠가 샌다