이 글은 스타트업 환경에서 FastAPI 기반 서버에 단위 테스트 체계를 새로 도입하면서 겪은 문제와 해결 과정을 기록한 것이다. 회사 코드나 구조는 모두 난독화했으며, 상황과 해결 중심으로 작성했다.
기존 프로젝트에는 .http 파일만 있었고 테스트 폴더가 없었다.
엔드포인트를 수동으로 호출해가며 기능을 확인하는 방식이라, 로직을 수정할 때마다 직접 테스트를 반복해야 했다.
로컬에서 바로 돌릴 수 있는 단위 테스트 환경을 만들었다.
테스트를 도입하는 이유는 다음과 같다.
1. 테스트 기반 개발에 익숙하다.
2. 회귀 테스트를 자동화할 수 있다. 리팩토링할 때 특히 좋다.
3. AI로 테스트 코드 생성이 가능해 작성 부담이 거의 없다.
4. 명확한 테스트 규칙을 만들어두면 AI에게 맡기기도 좋다.
5. 스타트업에서 혼자 백엔드를 맡는 상황이라 실수를 줄일 수 있는 장치가 필요하다.
아무 가이드도 없는 프로젝트에 테스트를 새롭게 도입하면 보통 이렇게 된다.
특히 스타트업에서는 테스트를 ‘예쁘게’ 하는 것보다 필요한 로직만 빠르게 검증하는 게 중요하다.
아래 네 가지만 지키기로 했다.
1. 커버리지 신경 쓰지 않는다
2. HTTP 라우터, 스키마, 엔티티(testing ORM) 같은 건 테스트하지 않는다
3. 오직 비즈니스 로직만 테스트한다. 문서 역할도 겸한다
4. 단순한 getter/setter는 테스트하지 않는다
이 기준은 충분히 단순하면서도 스타트업에서 필요한 부분은 정확히 커버한다.
또한 모킹을 남발하는 방식도 피하기로 했다. 이전에 모킹 떡칠하다가 테스트 작성하는 데 더 시간이 많이 걸린 적이 있었기 때문이다.
외부 IO를 직접 호출할 수 없다면 필요한 부분만 최소한으로 대체하는 방식으로 테스트한다.
아래는 실제 로직 없이, 서비스 레이어의 메서드 시그니처만 포함된 순수 함수 형태 예시다.
모든 함수는 구조만 보여주기 위해 pass 처리했다.
# service/foo_service.py
from uuid import UUID
from typing import Iterable, Optional
from .domain_errors import FooPermissionError
def validate_foo_permissions(
requested_ids: set[UUID],
allowed_ids: set[UUID],
) -> None:
pass
def merge_foo_settings(
foo_objects: Iterable[object],
saved_settings: Iterable[object],
) -> list[dict]:
pass
설명
service 레이어는 순수 함수로 구성 : FastAPI, HTTPException, DB, request context 등 외부 요소에 의존하지 않는다. 메서드 파라미터, 리턴 값 모두, 타입을 외부 요소랑 관련 없는 걸로 지정한다.
도메인 예외만 발생 : FooPermissionError 같은 예외는 서비스의 도메인 규칙을 표현하며, HTTPException은 라우터에서만 발생한다.
모듈 간 결합이 낮아 테스트하기 쉽다
비즈니스 로직이 순수 함수라 테스트에서 모킹도 거의 필요 없다.
이 방식은 서비스 레이어를 도입할 때 구조적 부담이 적고, 로직 변경에도 안정적으로 대응할 수 있다.
기존 구조는 crud, schemas, models, utils, routers로 구성된 전형적인 FastAPI 스타일이었다.
도메인 레이어나 서비스 레이어가 없었기 때문에, 나는 새로 service/ 디렉토리를 도입했다.
여기서 문제는 비즈니스 로직이 FastAPI의 HTTPException에 의존하게 되는 구조가 생긴다는 점이었다.
HTTP와 전혀 상관없는 도메인 로직에 프레임워크 예외가 들어가는 게 싫었다.
서비스 레이어에서 쓰는 예외를 별도 정의했다.
class ~~Error(Exception):
def __init__(self, detail: str):
self.detail = detail
그리고 FastAPI 라우터는 이 예외를 HTTPException으로 변환하기만 한다.
try:
service.validate~~(ids)
except MachinePermissionError as e:
raise HTTPException(status_code=400, detail=e.detail)
FastAPI 기반 서버에 테스트를 새롭게 도입하면서 다음 원칙만 지키면 부담 없이 운영할 수 있다.
이렇게 구성하면 테스트 작성과 실행이 가벼워지고, 로직 변경 시 불안도 크게 줄어든다.
앞으로도 이런 테스트 패턴을 기반으로 서비스 레이어와 도메인 구조를 확장할 예정이다.