[FastAPI 시리즈 12편] 중간 점검: 미니 서비스 통합

English version

이번 편은 FastAPI 시리즈 전반부의 정확한 중간 지점(12/20) 입니다. 지금까지 배운 의존성, SQLModel, 인증, 환경 설정을 하나의 학습용 작업 관리 API로 묶어 보면서 빈틈을 점검합니다. 이 미니 서비스는 앞으로 20편 캡스톤으로 가기 전에 숨을 고르며 "이 정도 구성은 반복해서 만들 수 있다"는 감각을 다지는 단계입니다.

준비: 우리가 완성할 첫 서비스

  1. /auth/token으로 로그인(도장 받기)
  2. /tasks에 할 일 추가하기
  3. 새로고침해도 데이터가 남아 있는지 확인하기

이 짧은 플로우가 이미 "설정 + DB + 인증"을 모두 거친 결과물입니다. 아래 섹션에서 이 세 단계가 어디에서 조립되는지 확인합니다.

이번 글에서 새로 나오는 용어

  1. 미니 서비스: 한 팀이 독립적으로 만들고 배포할 수 있는 작지만 완결된 API 묶음으로, 인증·DB·설정이 모두 들어 있는 이번 실습의 목표입니다.
  2. core/config: pydantic-settings로 환경 값을 모아 두는 영역으로, 토큰 만료 시간·DB URL 같은 공통 설정을 한곳에서 관리합니다.
  3. api/deps: FastAPI Depends에 연결할 공통 함수들을 모아 둔 모듈로, 세션·현재 사용자 같은 값이 모든 라우터에 주입됩니다.
  4. lifespan 이벤트: FastAPI 앱이 시작·종료될 때 실행되는 훅으로, 여기서 init_db()를 호출해 첫 요청 전에 테이블을 생성합니다.
  5. 토큰 보호 라우터: get_current_user 의존성을 통해 JWT를 검증한 뒤에만 접근을 허용하는 엔드포인트로, /tasks CRUD를 안전하게 감싸는 방법입니다.

실습 카드

  • 예상 소요 시간: 75분
  • 사전 준비: 9~11편 예제 코드, Docker/uvicorn 실행 경험
  • 실습 목표: 설정·DB·인증 라우터를 통합해 학습용 Todo 서비스를 완성한다

폴더 구조 재점검

app/
├─ core/
│  ├─ config.py
│  └─ security.py
├─ db/
│  ├─ models.py
│  ├─ session.py
│  └─ init.py
├─ api/
│  ├─ deps.py
│  ├─ routes/
│  │  ├─ auth.py
│  │  └─ tasks.py
│  └─ __init__.py
└─ main.py

폴더는 관심사를 기준으로 나눕니다. 이렇게 나눠야 테스트에서도 필요한 모듈만 바꿔 끼울 수 있습니다.

환경 변수 / .envcore/config.pycore/security.pydb/session.pyapi/routes/auth.pyapi/routes/tasks.pySQLModel 테이블CLI/웹 값 주입secret_keydatabase_url토큰 발급사용자 조회CRUD 트랜잭션JWT 응답Authorization 헤더get_current_usersub 검증

이 다이어그램 하나로 구성 요소들이 어디서 만나는지 설명할 수 있어야 합니다. 팀 동료에게 구조를 설명할 때도 이 순서를 그대로 따라가며 문서화해 보세요. 막히면 맨 앞에서 정리한 3단계 흐름을 다시 떠올리고, 각 단계가 어느 파일에 연결되는지 짝지어 봅니다.

설정과 보안 모듈

core/config.py:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    secret_key: str
    access_token_expire_minutes: int = 30
    database_url: str = "sqlite:///./todo.db"

    model_config = {"env_file": ".env", "env_prefix": "APP_"}

settings = Settings()

core/security.py에는 비밀번호 해시와 JWT 생성 로직을 둡니다. 보안을 한 파일에 모으면 테스트와 점검이 쉬워집니다.

DB 세션과 모델

db/session.py에서 엔진과 Session 의존성을 정의합니다. db/models.py에서는 User, Task 모델을 선언합니다.

class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    email: str = Field(index=True, unique=True)
    hashed_password: str

class Task(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    done: bool = False
    owner_id: int = Field(foreign_key="user.id")

api/deps.py에는 get_session, get_current_user 등 공통 의존성을 모읍니다. 같은 함수를 모든 라우터에서 재사용해 코드 중복을 줄입니다.

라우터 구성

api/routes/auth.py는 10편에서 다룬 토큰 발급 흐름을 담당합니다. api/routes/tasks.py는 사용자별 CRUD를 제공합니다.

tasks_router = APIRouter(prefix="/tasks", tags=["tasks"])

@tasks_router.get("", response_model=list[TaskRead])
def list_tasks(
    current_user: User = Depends(get_current_user),
    session: Session = Depends(get_session),
):
    statement = select(Task).where(Task.owner_id == current_user.id)
    return session.exec(statement).all()

@tasks_router.post("", response_model=TaskRead, status_code=201)
def create_task(
    payload: TaskCreate,
    current_user: User = Depends(get_current_user),
    session: Session = Depends(get_session),
):
    record = Task(**payload.model_dump(), owner_id=current_user.id)
    session.add(record)
    session.commit()
    session.refresh(record)
    return record

app/main.py에서는 라우터를 묶고 lifespan 이벤트로 init_db()를 호출합니다. 앱 시작 때 DB가 준비돼야 첫 요청이 실패하지 않습니다. 학생 입장에서는 "서버 켜질 때 책상 정리하기" 정도로 이해하면 됩니다.

@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()
    yield

app = FastAPI(title="Todo API", lifespan=lifespan)
app.include_router(auth_router)
app.include_router(tasks_router)

시나리오 기반 테스트

  1. /auth/register로 사용자 생성 혹은 초기 사용자 스크립트 실행.
  2. /auth/token에서 토큰 발급.
  3. Authorization 헤더를 붙여 /tasks CRUD 호출.
  4. .env에는 APP_DATABASE_URL=sqlite:///./todo.db를 두고, 배포 환경에서는 PostgreSQL URL을 주입.

이 시나리오는 Pytest TestClientdependency_overrides로 자동화할 수 있습니다. 인증 과정을 단축한 더미 의존성으로 테스트 속도도 잡습니다.

기대 출력 확인

중간 점검 서비스는 아래 흐름이 재현되면 합격입니다.

:::terminal{title="미니 서비스 검증 흐름", showFinalPrompt="false"}

[
  { "cmd": "http POST :8000/auth/token [email protected] password=secret", "output": "HTTP/1.1 200 OK\n{\n  \"access_token\": \"eyJhbGciOi...\",\n  \"token_type\": \"bearer\"\n}", "delay": 500 },
  { "cmd": "http GET :8000/tasks", "output": "HTTP/1.1 401 Unauthorized\n{\n  \"detail\": \"Not authenticated\"\n}", "delay": 400 },
  { "cmd": "http GET :8000/tasks \"Authorization:Bearer eyJhbGciOi...\"", "output": "HTTP/1.1 200 OK\n[]", "delay": 400 },
  { "cmd": "http POST :8000/tasks title='API 정리' \"Authorization:Bearer eyJhbGciOi...\"", "output": "HTTP/1.1 201 Created\n{\n  \"id\": 1,\n  \"title\": \"API 정리\",\n  \"done\": false\n}", "delay": 400 }
]

:::

  • 확인할 점: 토큰 없이는 401이 나오는지
  • 확인할 점: 같은 경로가 토큰을 넣으면 200으로 바뀌는지
  • 확인할 점: 생성 결과가 JSON으로 명확히 돌아오는지

(선택) 배포 체크리스트

  • Docker 이미지 빌드 → 컨테이너 레지스트리에 푸시.
  • DB 마이그레이션 스크립트 실행.
  • uvicorn을 systemd 서비스 혹은 컨테이너 오케스트레이션으로 실행.
  • 로그와 헬스체크 엔드포인트를 모니터링 도구에 연결.

실습

  • 따라 하기: 구조 예시대로 폴더를 만들고 설정/DB/라우터 모듈을 연결해 /tasks CRUD가 토큰 인증 뒤에만 열리게 한다.
  • 확장하기: Pytest 시나리오를 작성해 회원가입 → 토큰 발급 → 작업 생성 흐름을 자동 검증한다.
  • 디버깅: .env 값을 바꿨을 때 DB 연결이 실패하면 로그를 확인해 원인을 기록한다.
  • 완료 기준: 하나의 커맨드로 서버가 뜨고, 토큰 없이 요청 시 401·토큰 포함 시 200이 재현된다.

(선택) 다음 확장 아이디어

  1. WebSocket 엔드포인트로 실시간 작업 업데이트 제공.
  2. Celery, RQ 같은 작업 큐와 연계해 장기 실행 작업 처리.
  3. OpenAPI 문서에서 OAuth2 스코프를 정의해 권한별 토큰을 발급.
  4. 프런트엔드(React/Vue/Svelte)와 연동해 실제 사용자 인터페이스 제공.

마무리

이번 편은 시리즈 전반부의 통합 체크포인트일 뿐입니다. 설정·인증·DB 흐름을 한 번에 다시 짚어 본 만큼, 이제 13~19편에서 파일, 스트리밍, 테스트, 관측 가능성 같은 확장 기능을 차근히 쌓을 차례입니다. 이 토대를 단단히 다지면 20편 캡스톤에서 진짜 완성본을 조립할 때 훨씬 수월해집니다.

💬 댓글

이 글에 대한 의견을 남겨주세요