LabHub
学习 学习路径 课程

테스트 도구 실전 · 예외를 주입해 자원 누수를 찾는다 · 讲解

예외를 주입해 자원 누수를 찾는다의 설계 원리

在 LabHub 中继续学习

한 줄 요약

정상·중복 종료·실패 경로와 앱 lifespan의 시작·정리를 관측합니다.

概念图: 한 줄 요약 · 왜 이게 필요했나 · 어떻게 동작하나 · 계약을 읽고 실패를 예측하는 워크시트

왜 이게 필요했나

요청이 성공하면 연결을 닫는 코드는 있었지만 예외가 발생하면 열린 연결이 남았다. 성공 응답만 확인하는 테스트에서는 이 누수가 드러나지 않았다. 수명 주기 테스트는 반환값보다 자원이 언제 열리고 닫혔는지에 집중해야 한다.

어떻게 동작하나

시험마다 독립 자원 객체를 만든다. start·stop 횟수와 open 상태를 확인하고 context 안에서 의도적으로 예외를 던진다. TestClient를 with로 사용해 lifespan을 실제 실행하며 with 종료 이후 상태까지 단언한다.

학생 테스트 → 정상 구현: 실제 시험 모두 통과           └→ 계약 위반 구현: 해당 동작에서 실패수집 실패·0개 실행·강제 종료 ≠ 결함 검출

계약을 읽고 실패를 예측하는 워크시트

다음은 구현을 통째로 외우는 답안이 아니라 단계별 코드 리뷰입니다. 각 변경 조각은 의도적으로 계약을 깨뜨립니다. 변경 후에도 정상 사례가 통과할 수 있다는 점에 주의하세요. 실행 전에 어느 입력·예외·상태를 관측하면 차이가 드러날지 예상하고, 구현 후에는 그 예상과 결과를 비교합니다.

1. 자원 상태를 독립적으로 만든다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: new_resource()는 {open:False, events:[]}인 새 딕셔너리이며 호출끼리 events를 공유하지 않습니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 변경 가능한 리스트를 전역이나 기본 인자로 공유하지 않습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

"open":True

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

2. 중복 시작을 거절한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: start(resource)는 이미 열려 있으면 ValueError, 아니면 open=True로 바꾸고 events에 'open'을 추가합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 두 번 시작해 자원 하나를 잃어버리는 동작을 거절합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

if False:        raise

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

3. 종료를 멱등하게 만든다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: stop(resource)는 열려 있을 때만 open=False로 바꾸고 'close'를 events에 추가합니다. 이미 닫혀 있으면 그대로 둡니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 여러 정리 경로가 겹쳐도 중복 close 이벤트가 나오지 않아야 합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

resource["events"].append("closed")

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

4. 닫힌 자원의 사용을 막는다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: read(resource)는 닫혀 있으면 RuntimeError, 열려 있으면 {ready:True}를 반환합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 준비 상태와 객체 존재 여부는 다릅니다. 객체가 있어도 닫혀 있을 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

if False:

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

5. 예외 경로에 finally를 둔다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: scope(resource)는 contextmanager입니다. 진입 시 start, 블록 안에는 resource를 yield하고 블록의 성공·실패 모두 stop으로 닫습니다. 블록 예외는 전파합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: yield 뒤에만 close를 쓰면 예외가 난 경우 그 줄에 도달하지 못합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

pass

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

6. 앱 수명주기와 자원을 연결한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: lifespan_for(resource)는 asynccontextmanager 함수 lifespan(app)을 반환합니다. scope(resource) 안에서 app.state.resource를 설정하고 yield합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: lifespan 함수 자체를 호출하는 것이 아니라 FastAPI 생성자에 전달합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

app.state.resource = dict(resource)

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

7. 준비 상태를 실제 요청으로 읽는다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: create_app(resource)는 lifespan_for를 사용합니다. GET /ready는 app.state.resource를 read한 결과를 반환합니다. context 종료 시 자원을 닫아야 합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: with TestClient를 사용해야 lifespan 시작과 종료를 모두 실행합니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

FastAPI()

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

8. 요청 이후의 실패도 정리한다 — 테스트

제공된 service.py의 다음 공개 계약을 테스트하세요: exercise(resource, fail=False)는 with TestClient(create_app(resource)) 안에서 GET /ready를 호출합니다. fail=True면 그 안에서 RuntimeError를 내고, 아니면 응답 JSON을 반환합니다. 두 경우 모두 자원이 닫혀야 합니다. 정상 구현에서는 통과하고 이 계약을 어긴 구현에서는 실제 테스트 본문의 실패로 검출해야 합니다. 앞 단계 테스트를 유지하며 test_ 함수를 추가하세요.

판단의 근거: 정상 경로와 예외 경로를 같은 정리 구조로 묶으면 빠진 종료 경로를 줄일 수 있습니다. 구현 파일은 수정하지 않습니다. pytest.raises로 예상 예외를 확인하고 정상 결과에는 구체적인 예상값을 단언하세요.

리뷰할 잘못된 변경 조각:

if False:

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

현장에서 만나는 모습

교육용 자원 사전은 실제 DB 연결 풀을 대신하는 관측 장치다. 운영에서는 부분 초기화 실패, 연결 풀의 동시성, 취소 처리와 종료 제한 시간도 설계해야 한다. 이벤트 문자열을 보고서에 적는 것이 아니라 학습자 코드가 실행하며 바꾼 객체 상태를 검사한다. 제공 구현은 읽어도 되지만 채점은 별도 사본을 사용한다. 소스 문구 검사나 파일 수정으로 결함을 우회하지 말고 공개 인터페이스의 실행 결과를 검사한다.

다음 실습에서 할 것

여덟 단계가 하나의 실행 가능한 결과물로 이어집니다. 자원 상태를 독립적으로 만든다 — 테스트 → 중복 시작을 거절한다 — 테스트 → 종료를 멱등하게 만든다 — 테스트 → 닫힌 자원의 사용을 막는다 — 테스트 → 예외 경로에 finally를 둔다 — 테스트 → 앱 수명주기와 자원을 연결한다 — 테스트 → 준비 상태를 실제 요청으로 읽는다 — 테스트 → 요청 이후의 실패도 정리한다 — 테스트.

각 단계는 함수나 파일이 존재한다는 사실이 아니라 실제 반환값·예외·상태 변화를 검사합니다. 정답을 본 뒤에는 일부러 경계 비교나 정리 코드를 바꾸어 어떤 시험이 실패하는지 확인하세요. 앞선 시험이 다음 단계에서도 유지되는 이유를 설명하고, 이 실습이 보장하지 않는 운영 조건을 한 가지 적어 보세요.