LabHub
배우기 러닝패스 코스

FastAPI — Types Are the Contract

Prevent lost updates with ETags

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

두 사용자가 같은 초안을 수정할 때 SQL 조건부 갱신과 HTTP 응답으로 유실을 막습니다.

왜 중요한가

정상 요청 한 번의 성공은 경계값과 장애 복구를 보장하지 않습니다. 이 실습은 각 함수의 계약을 작게 구현하거나 테스트한 뒤 실제 실행으로 연결합니다. 코드가 존재하는지나 보고서 문구만 보지 않고 결과·예외·저장 상태를 검사합니다. 앞 단계의 코드를 유지하며 다음 단계로 진행하세요.

단계

  1. /root/work/fa-conditional-lab/service.py에서 init_db(path)는 SQLite 파일에 notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)를 생성하세요. 반복 호출해도 기존 데이터를 지우지 않습니다. 첫 준비는 다음 명령으로 합니다.
mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab
  1. /root/work/fa-conditional-lab/service.py에서 create_note(path, note_id, title)는 버전 1로 메모를 INSERT하고 커밋하세요. 같은 id는 sqlite3.IntegrityError로 거절하며 기존 제목을 덮어쓰지 않습니다.
  2. /root/work/fa-conditional-lab/service.py에서 read_note(path, note_id)는 {id, title, version} 딕셔너리 또는 None을 반환하세요. SQL에 id를 문자열 연결하지 말고 바인딩합니다.
  3. /root/work/fa-conditional-lab/service.py에서 etag(version)는 정수 버전을 큰따옴표까지 포함한 문자열로 반환하세요. 예: 버전 3은 문자열 '"v3"'입니다.
  4. /root/work/fa-conditional-lab/service.py에서 parse_match(header)는 큰따옴표 안 v와 양의 십진 정수 하나인 태그만 받아 버전을 반환하세요. None, weak 태그, 와일드카드, 목록, v0 등은 ValueError입니다. 예: '"v12"' → 12.
  5. /root/work/fa-conditional-lab/service.py에서 update_note(path, note_id, title, version)는 id와 version이 모두 일치할 때만 제목을 갱신하고 버전을 1 올리세요. 변경 행이 하나면 True, 없으면 False입니다. 동시 요청에도 승자는 하나여야 합니다.
  6. /root/work/fa-conditional-lab/service.py에서 put_note(path, note_id, title, header)는 없는 id 404, 조건 누락 428, 지원하지 않는 조건·버전 불일치 412, 성공 204를 반환하세요. 거절 시 DB를 바꾸지 않습니다.
  7. /root/work/fa-conditional-lab/service.py에서 create_app(path)는 GET /notes/{note_id}와 PUT /notes/{note_id}를 제공하세요. GET은 메모 JSON과 ETag, PUT은 {title: 1~100자 문자열}·If-Match를 받아 위 결과 코드로 빈 응답을 반환합니다. 빈 제목은 422, 없는 GET은 404입니다.

참고

버전을 가진 저장소를 만든다

/root/work/fa-conditional-lab/service.py에서 init_db(path)는 SQLite 파일에 notes(id TEXT PRIMARY KEY, title TEXT NOT NULL, version INTEGER NOT NULL)를 생성하세요. 반복 호출해도 기존 데이터를 지우지 않습니다. 첫 준비는 다음 명령으로 합니다.

mkdir -p /root/work/fa-conditional-lab
cp /opt/fixtures/practice_depth/fa-conditional-lab/* /root/work/fa-conditional-lab/
cd /root/work/fa-conditional-lab

CREATE TABLE IF NOT EXISTS를 쓰고 with sqlite3.connect(path)로 커밋 경계를 정합니다.

채점은 bash /opt/lab/checks/fa-conditional-lab/01-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

첫 저장의 버전을 고정한다

/root/work/fa-conditional-lab/service.py에서 create_note(path, note_id, title)는 버전 1로 메모를 INSERT하고 커밋하세요. 같은 id는 sqlite3.IntegrityError로 거절하며 기존 제목을 덮어쓰지 않습니다.

INSERT OR REPLACE는 중복 생성을 갱신으로 바꿉니다. 이 단계의 계약은 새 메모 생성입니다.

채점은 bash /opt/lab/checks/fa-conditional-lab/02-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

없는 메모와 있는 메모를 구분한다

/root/work/fa-conditional-lab/service.py에서 read_note(path, note_id)는 {id, title, version} 딕셔너리 또는 None을 반환하세요. SQL에 id를 문자열 연결하지 말고 바인딩합니다.

fetchone이 None인지 먼저 확인합니다. 튜플 그대로 반환하지 마세요.

채점은 bash /opt/lab/checks/fa-conditional-lab/03-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

강한 ETag를 만든다

/root/work/fa-conditional-lab/service.py에서 etag(version)는 정수 버전을 큰따옴표까지 포함한 문자열로 반환하세요. 예: 버전 3은 문자열 '"v3"'입니다.

HTTP 태그의 큰따옴표는 설명용 표기가 아니라 실제 헤더 값의 일부입니다.

채점은 bash /opt/lab/checks/fa-conditional-lab/04-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

지원하는 조건 문법만 받는다

/root/work/fa-conditional-lab/service.py에서 parse_match(header)는 큰따옴표 안 v와 양의 십진 정수 하나인 태그만 받아 버전을 반환하세요. None, weak 태그, 와일드카드, 목록, v0 등은 ValueError입니다. 예: '"v12"' → 12.

re.fullmatch를 쓰면 태그 뒤의 추가 문자열이 묵인되지 않습니다. 이 실습은 HTTP 태그 전체 문법 구현이 아닙니다.

채점은 bash /opt/lab/checks/fa-conditional-lab/05-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

비교와 쓰기를 하나의 SQL로 묶는다

/root/work/fa-conditional-lab/service.py에서 update_note(path, note_id, title, version)는 id와 version이 모두 일치할 때만 제목을 갱신하고 버전을 1 올리세요. 변경 행이 하나면 True, 없으면 False입니다. 동시 요청에도 승자는 하나여야 합니다.

UPDATE ... WHERE id=? AND version=?의 rowcount를 봅니다. SELECT 후 무조건 UPDATE는 경쟁을 막지 못합니다.

채점은 bash /opt/lab/checks/fa-conditional-lab/06-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

실패를 HTTP 결과로 분류한다

/root/work/fa-conditional-lab/service.py에서 put_note(path, note_id, title, header)는 없는 id 404, 조건 누락 428, 지원하지 않는 조건·버전 불일치 412, 성공 204를 반환하세요. 거절 시 DB를 바꾸지 않습니다.

없는 리소스 판정을 먼저 하고 조건을 해석합니다. 재조회로 실패를 성공처럼 바꾸지 마세요.

채점은 bash /opt/lab/checks/fa-conditional-lab/07-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.

오래된 화면 저장을 HTTP로 재현한다

/root/work/fa-conditional-lab/service.py에서 create_app(path)는 GET /notes/{note_id}와 PUT /notes/{note_id}를 제공하세요. GET은 메모 JSON과 ETag, PUT은 {title: 1~100자 문자열}·If-Match를 받아 위 결과 코드로 빈 응답을 반환합니다. 빈 제목은 422, 없는 GET은 404입니다.

Header로 If-Match를 받고 TestClient로 GET 후 같은 태그를 두 번 보내 보세요. 첫 수정만 적용되고 새 GET은 v2여야 합니다.

채점은 bash /opt/lab/checks/fa-conditional-lab/08-contract.sh로 직접 재현할 수 있습니다. 파일 저장 후 다시 실행하세요.