캡스톤: 조직 경계를 지키는 멱등 주문 API
목표
주문 API 하나를 프로덕션 책임까지 붙여서 완성합니다. 데이터 계약, 트랜잭션 멱등성, 조직 경계 인가, 실행되는 HTTP, 세 관측 신호, 안전한 배포 객체, 그리고 남의 손에 넘길 수 있는 문서까지 한 프로젝트로 잇습니다.
여덟 단계가 각각 다른 채점 축(정확성·보안·문제해결·운영·문서)을 봅니다.
시간과 저장에 대한 경고 — 먼저 읽으세요
- 세션은 60분에 시작하고
+시간으로 총 180분까지만 연장됩니다. - 실습 파드에는 볼륨이 없습니다. 세션이 끝나면
/root/work가 통째로 사라지고, 다음 세션은 빈 파드로 시작합니다.
그래서 한 번에 앉아서 끝내는 것을 전제로 설계돼 있습니다. 시간이 모자랄
것 같으면 8단계까지 얕게라도 한 바퀴 돌아 두는 편이, 3단계까지 깊게 하다 잃는
것보다 낫습니다. 중간에 나가야 한다면 프로젝트 폴더를 tar 로 묶어 손으로
내려받아 두세요.
환경
PostgreSQL 16 이 이미 떠 있습니다.
postgresql://lab@127.0.0.1:5432/labdb
프로젝트 뿌리는 /root/work/backend-capstone 이고, 채점기는 이 경로만
봅니다. 먼저 만들어 두세요.
mkdir -p /root/work/backend-capstone
cd /root/work/backend-capstone
mkdir -p migrations tests docs deploy
만들 파일
app.py psycopg 로 붙고 --host/--port 로 뜬다
migrations/001_create_orders.sql BEGIN/COMMIT 으로 감싼 순방향 마이그레이션
tests/test_integration.py 재시도와 다른 조직 반례를 행동으로 검증
deploy/deployment.yaml JSON 으로도 읽히는 YAML
docs/ADR-001.md status/context/alternatives/decision/consequences
docs/incident-report.md impact/timeline/root cause/detection/corrective actions
portfolio.json backend-capstone-v2 스키마
모든 단계에 먼저 걸리는 검사 하나
채점기는 단계 검사 전에 프로젝트 전체를 훑어 비밀을 찾습니다. 하나라도 걸리면 어느 단계를 채점하든 똑같은 문구로 실패하므로, 원인을 그 단계에서 찾다가 시간을 버리기 쉽습니다.
비밀번호가 들어간 접속 문자열 postgresql://lab:1234@...
클러스터 내부 주소 *.svc.cluster.local
Bearer 토큰 원문 Authorization: Bearer eyJ...
password/api_key/secret 대입 password = "hunter2"
개인 키 -----BEGIN PRIVATE KEY-----
접속 정보는 환경변수로 받고, 예제 토큰은 <token> 처럼 자리표시자로 쓰세요.
크기 제한도 함께 봅니다 — 파일 4,096개 · 한 파일 1MB · 합계 8MB 안쪽입니다.
.git 과 __pycache__ 는 세지 않습니다.
단계
- 데이터 계약 —
orders표에 identity PK,amount_cents > 0검사,owner_id·org_id,created_at timestamptz,unique(org_id, idempotency_key), 그리고(org_id, owner_id)조회 인덱스. 전체를BEGIN;/COMMIT;으로 감쌉니다. - 트랜잭션 멱등성 —
ON CONFLICT (org_id, idempotency_key)로 재시도를 흡수하고, 예외에서 롤백합니다. 값은 반드시 매개변수로 바인딩합니다. - 인가 —
authenticate는 신원을 만들고,authorize(identity, order)는 조직을 먼저 본 뒤 소유자·관리자를 봅니다. 이 파일은 import 만 해도 아무것도 출력하지 않아야 합니다. - HTTP —
python3 app.py --host 127.0.0.1 --port <포트>로 떠야 합니다.POST /orders·GET /orders/{id}·POST /orders/{id}/cancel·/healthz, 그리고 모든 응답에X-Trace-Id. 상태 코드는 201/200/400/401/403 을 구분합니다. - 관측 —
orders_created_total,http_request_duration_seconds, 그리고event·route·status·duration_ms·trace_id를 담은 JSON 로그.authorization헤더와 내부 DNS 는 남기지 않습니다. - 배포 —
deploy/deployment.yaml에 비루트,seccompProfile: RuntimeDefault,allowPrivilegeEscalation: false, 읽기 전용 루트, requests/limits, 두 프로브,secretKeyRef, 그리고 다이제스트로 고정한 이미지. - 문서 — ADR 과 사고 보고서. 사고 보고서에는 조치마다 담당자와 기한을 적습니다.
- 포트폴리오 —
portfolio.json에 허용된 다섯 요약만 넣습니다. 원본 헤더·토큰·사설 IP·내부 DNS·터미널 기록은 넣지 않습니다.
참고
3단계의 "import 해도 조용해야 한다" 는 조건이 매번 걸리는 자리입니다. 모듈
맨 위에서 데이터베이스에 붙거나 print 를 하면 채점기가 그 출력을 봅니다.
붙는 일은 main() 안으로, 진단은 로거로 옮기세요.
PostgreSQL 16 스키마를 실행한다
데이터 계약 — orders 표에 identity PK, amount_cents > 0 검사,
owner_id·org_id, created_at timestamptz, unique(org_id, idempotency_key),
그리고 (org_id, owner_id) 조회 인덱스. 전체를 BEGIN;/COMMIT; 으로 감쌉니다.
migrations/001_create_orders.sql에 identity PK, 양수 금액, owner/org, 조직 범위 멱등 UNIQUE, TIMESTAMPTZ와 조회 인덱스를 만들고 트랜잭션으로 감싸세요. app.py는 psycopg를 사용해야 합니다.
트랜잭션 멱등 생성을 구현한다
트랜잭션 멱등성 — ON CONFLICT (org_id, idempotency_key) 로 재시도를
흡수하고, 예외에서 롤백합니다. 값은 반드시 매개변수로 바인딩합니다.
psycopg transaction/cursor와 매개변수 바인딩, ON CONFLICT (org_id, idempotency_key)를 사용하고 예외 시 rollback하세요. 재시도와 다른 조직 반례를 행동 테스트로 작성하세요.
인증과 소유자·조직 인가를 분리한다
인가 — authenticate 는 신원을 만들고, authorize(identity, order) 는
조직을 먼저 본 뒤 소유자·관리자를 봅니다. 이 파일은 import 만 해도
아무것도 출력하지 않아야 합니다.
authenticate는 Bearer 신원을 만들고 authorize(identity, order)는 조직을 먼저, 그다음 owner/admin 관계를 판단하세요. import와 오류 출력에 토큰을 남기지 마세요.
실제 HTTP API 계약을 연다
HTTP — python3 app.py --host 127.0.0.1 --port <포트> 로 떠야 합니다.
POST /orders · GET /orders/{id} · POST /orders/{id}/cancel · /healthz,
그리고 모든 응답에 X-Trace-Id. 상태 코드는 201/200/400/401/403 을 구분합니다.
표준 라이브러리 또는 설치된 런타임으로 app.py --host ... --port ...를 지원하세요. POST /orders, GET /orders/id, POST /orders/id/cancel, health/readiness와 상태·JSON·X-Trace-Id 계약을 구현하세요.
세 관측 신호를 요청에 연결한다
관측 — orders_created_total, http_request_duration_seconds, 그리고
event·route·status·duration_ms·trace_id 를 담은 JSON 로그.
authorization 헤더와 내부 DNS 는 남기지 않습니다.
orders_created_total, http_request_duration_seconds, JSON 로그의 event/route/status/duration_ms와 응답 trace ID를 연결하세요. Authorization과 내부 DNS는 기록하지 마세요.
파싱 가능한 안전한 배포 객체를 만든다
배포 — deploy/deployment.yaml 에 비루트, seccompProfile: RuntimeDefault,
allowPrivilegeEscalation: false, 읽기 전용 루트, requests/limits,
두 프로브, secretKeyRef, 그리고 다이제스트로 고정한 이미지.
deploy/deployment.yaml은 JSON-compatible YAML로 작성하세요. 비루트/seccomp/권한 상승 금지/읽기 전용 루트, 리소스, 두 프로브, Secret 참조, digest 이미지를 정확한 객체 경로에 둡니다.
ADR과 사고 학습을 연결한다
문서 — ADR 과 사고 보고서. 사고 보고서에는 조치마다 담당자와 기한을 적습니다.
ADR에 status/context/alternatives/decision/consequences를, 사고 보고서에 impact/timeline/root cause/detection/corrective actions와 담당자·기한을 쓰고 비밀과 내부 주소는 제거하세요.
허용 목록 포트폴리오를 발행한다
포트폴리오 — portfolio.json 에 허용된 다섯 요약만 넣습니다.
원본 헤더·토큰·사설 IP·내부 DNS·터미널 기록은 넣지 않습니다.
portfolio.json을 backend-capstone-v2로 고정하고 test_summary, metric_names, deployment_summary, adr_summary, incident_summary만 넣으세요. 원본 헤더·토큰·사설 IP·내부 DNS·터미널 로그는 금지합니다.