FastAPI — Types Are the Contract
Close resources even when requests fail
한국어 원문으로 표시합니다.
목표
시작·종료·예외 경로를 분리하고 FastAPI lifespan을 실제로 실행합니다.
왜 중요한가
테스트는 통과했지만 운영 재시작 때 연결이 남았다. TestClient를 context manager 없이 사용해 시작·종료 코드가 실행되지 않았던 것이다. 정상 응답 한 번을 보는 테스트만으로는 앱이 자원을 언제 열고 닫는지 알 수 없다. 여기서는 외부 연결 대신 이벤트를 기록하는 작은 자원으로 생명주기를 관찰한다.
단계
/root/work/fa-resource-lifecycle-lab/service.py에서 new_resource()는 {open:False, events:[]}인 새 딕셔너리이며 호출끼리 events를 공유하지 않습니다.
처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
-
/root/work/fa-resource-lifecycle-lab/service.py에서 start(resource)는 이미 열려 있으면 ValueError, 아니면 open=True로 바꾸고 events에 'open'을 추가합니다. -
/root/work/fa-resource-lifecycle-lab/service.py에서 stop(resource)는 열려 있을 때만 open=False로 바꾸고 'close'를 events에 추가합니다. 이미 닫혀 있으면 그대로 둡니다. -
/root/work/fa-resource-lifecycle-lab/service.py에서 read(resource)는 닫혀 있으면 RuntimeError, 열려 있으면 {ready:True}를 반환합니다. -
/root/work/fa-resource-lifecycle-lab/service.py에서 scope(resource)는 contextmanager입니다. 진입 시 start, 블록 안에는 resource를 yield하고 블록의 성공·실패 모두 stop으로 닫습니다. 블록 예외는 전파합니다. -
/root/work/fa-resource-lifecycle-lab/service.py에서 lifespan_for(resource)는 asynccontextmanager 함수 lifespan(app)을 반환합니다. scope(resource) 안에서 app.state.resource를 설정하고 yield합니다. -
/root/work/fa-resource-lifecycle-lab/service.py에서 create_app(resource)는 lifespan_for를 사용합니다. GET /ready는 app.state.resource를 read한 결과를 반환합니다. context 종료 시 자원을 닫아야 합니다. -
/root/work/fa-resource-lifecycle-lab/service.py에서 exercise(resource, fail=False)는 with TestClient(create_app(resource)) 안에서 GET /ready를 호출합니다. fail=True면 그 안에서 RuntimeError를 내고, 아니면 응답 JSON을 반환합니다. 두 경우 모두 자원이 닫혀야 합니다.
참고
- 인터넷과 패키지 설치 없이 기존 lab-dev 환경에서 수행합니다.
- 각 단계는 45초 채점 예산 안에서 실행됩니다. 실제 sleep이나 네트워크 호출을 추가하지 마세요.
- 채점은 제출 모듈을 새로 불러오고 독립 입력과 임시 DB로 검사합니다. 예상값을 상수로 반환하는 대신 계약을 구현하세요.
- FastAPI 공식 문서 · pytest 공식 문서 · Python sqlite3
- 한계: 교육용 자원 사전은 실제 DB 연결 풀을 대신하는 관측 장치다. 운영에서는 부분 초기화 실패, 연결 풀의 동시성, 취소 처리와 종료 제한 시간도 설계해야 한다. 이벤트 문자열을 보고서에 적는 것이 아니라 학습자 코드가 실행하며 바꾼 객체 상태를 검사한다.
자원 상태를 독립적으로 만든다
/root/work/fa-resource-lifecycle-lab/service.py에서 new_resource()는 {open:False, events:[]}인 새 딕셔너리이며 호출끼리 events를 공유하지 않습니다.
처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.
mkdir -p /root/work/fa-resource-lifecycle-lab
test -e /root/work/fa-resource-lifecycle-lab/service.py || cp /opt/fixtures/ten_labs/fa-resource-lifecycle-lab/service.py /root/work/fa-resource-lifecycle-lab/service.py
cd /root/work/fa-resource-lifecycle-lab
변경 가능한 리스트를 전역이나 기본 인자로 공유하지 않습니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/01-contract.sh로 확인하세요.
중복 시작을 거절한다
/root/work/fa-resource-lifecycle-lab/service.py에서 start(resource)는 이미 열려 있으면 ValueError, 아니면 open=True로 바꾸고 events에 'open'을 추가합니다.
두 번 시작해 자원 하나를 잃어버리는 동작을 거절합니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/02-contract.sh로 확인하세요.
종료를 멱등하게 만든다
/root/work/fa-resource-lifecycle-lab/service.py에서 stop(resource)는 열려 있을 때만 open=False로 바꾸고 'close'를 events에 추가합니다. 이미 닫혀 있으면 그대로 둡니다.
여러 정리 경로가 겹쳐도 중복 close 이벤트가 나오지 않아야 합니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/03-contract.sh로 확인하세요.
닫힌 자원의 사용을 막는다
/root/work/fa-resource-lifecycle-lab/service.py에서 read(resource)는 닫혀 있으면 RuntimeError, 열려 있으면 {ready:True}를 반환합니다.
준비 상태와 객체 존재 여부는 다릅니다. 객체가 있어도 닫혀 있을 수 있습니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/04-contract.sh로 확인하세요.
예외 경로에 finally를 둔다
/root/work/fa-resource-lifecycle-lab/service.py에서 scope(resource)는 contextmanager입니다. 진입 시 start, 블록 안에는 resource를 yield하고 블록의 성공·실패 모두 stop으로 닫습니다. 블록 예외는 전파합니다.
yield 뒤에만 close를 쓰면 예외가 난 경우 그 줄에 도달하지 못합니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/05-contract.sh로 확인하세요.
앱 수명주기와 자원을 연결한다
/root/work/fa-resource-lifecycle-lab/service.py에서 lifespan_for(resource)는 asynccontextmanager 함수 lifespan(app)을 반환합니다. scope(resource) 안에서 app.state.resource를 설정하고 yield합니다.
lifespan 함수 자체를 호출하는 것이 아니라 FastAPI 생성자에 전달합니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/06-contract.sh로 확인하세요.
준비 상태를 실제 요청으로 읽는다
/root/work/fa-resource-lifecycle-lab/service.py에서 create_app(resource)는 lifespan_for를 사용합니다. GET /ready는 app.state.resource를 read한 결과를 반환합니다. context 종료 시 자원을 닫아야 합니다.
with TestClient를 사용해야 lifespan 시작과 종료를 모두 실행합니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/07-contract.sh로 확인하세요.
요청 이후의 실패도 정리한다
/root/work/fa-resource-lifecycle-lab/service.py에서 exercise(resource, fail=False)는 with TestClient(create_app(resource)) 안에서 GET /ready를 호출합니다. fail=True면 그 안에서 RuntimeError를 내고, 아니면 응답 JSON을 반환합니다. 두 경우 모두 자원이 닫혀야 합니다.
정상 경로와 예외 경로를 같은 정리 구조로 묶으면 빠진 종료 경로를 줄일 수 있습니다.
저장 후 bash /opt/lab/checks/fa-resource-lifecycle-lab/08-contract.sh로 확인하세요.