LabHub
배우기 러닝패스 코스

FastAPI — Types Are the Contract

Close resources even when requests fail

LabHub 에서 이어서 보기

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

목표

시작·종료·예외 경로를 분리하고 FastAPI lifespan을 실제로 실행합니다.

왜 중요한가

테스트는 통과했지만 운영 재시작 때 연결이 남았다. TestClient를 context manager 없이 사용해 시작·종료 코드가 실행되지 않았던 것이다. 정상 응답 한 번을 보는 테스트만으로는 앱이 자원을 언제 열고 닫는지 알 수 없다. 여기서는 외부 연결 대신 이벤트를 기록하는 작은 자원으로 생명주기를 관찰한다.

단계

  1. /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
  1. /root/work/fa-resource-lifecycle-lab/service.py에서 start(resource)는 이미 열려 있으면 ValueError, 아니면 open=True로 바꾸고 events에 'open'을 추가합니다.

  2. /root/work/fa-resource-lifecycle-lab/service.py에서 stop(resource)는 열려 있을 때만 open=False로 바꾸고 'close'를 events에 추가합니다. 이미 닫혀 있으면 그대로 둡니다.

  3. /root/work/fa-resource-lifecycle-lab/service.py에서 read(resource)는 닫혀 있으면 RuntimeError, 열려 있으면 {ready:True}를 반환합니다.

  4. /root/work/fa-resource-lifecycle-lab/service.py에서 scope(resource)는 contextmanager입니다. 진입 시 start, 블록 안에는 resource를 yield하고 블록의 성공·실패 모두 stop으로 닫습니다. 블록 예외는 전파합니다.

  5. /root/work/fa-resource-lifecycle-lab/service.py에서 lifespan_for(resource)는 asynccontextmanager 함수 lifespan(app)을 반환합니다. scope(resource) 안에서 app.state.resource를 설정하고 yield합니다.

  6. /root/work/fa-resource-lifecycle-lab/service.py에서 create_app(resource)는 lifespan_for를 사용합니다. GET /ready는 app.state.resource를 read한 결과를 반환합니다. context 종료 시 자원을 닫아야 합니다.

  7. /root/work/fa-resource-lifecycle-lab/service.py에서 exercise(resource, fail=False)는 with TestClient(create_app(resource)) 안에서 GET /ready를 호출합니다. fail=True면 그 안에서 RuntimeError를 내고, 아니면 응답 JSON을 반환합니다. 두 경우 모두 자원이 닫혀야 합니다.

참고

자원 상태를 독립적으로 만든다

/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로 확인하세요.