FastAPI 프로젝트 구조 — APIRouter 활용
라우트가 스무 개를 넘어가면 main.py 한 파일은 스크롤 지옥이 된다. 기능별로 파일을 나누고 다시 하나로 합치는 도구, APIRouter.
5편까지 오면서 우리는 경로·쿼리 매개변수, Pydantic 모델, 그리고 의존성 주입까지 손에 익혔다. 기능은 점점 그럴듯해지는데, 정작 코드를 적는 파일은 줄곧 하나였다 — main.py. 처음엔 다섯 줄이던 그 파일이 어느새 사용자, 상품, 주문, 인증 라우트를 전부 끌어안고 300 줄을 넘기기 시작한다.
6편의 주제는 프로젝트 구조 다. 지금까지가 "기능을 어떻게 짜는가" 였다면, 이번 편은 "그 기능들을 어떻게 정리해 두는가" 에 대한 이야기다. 핵심 도구는 두 가지 — 라우트를 파일로 쪼개는 APIRouter, 그리고 모든 요청 앞뒤로 끼어드는 미들웨어. 이 편을 마치면 협업해도 충돌 안 나고, 6개월 뒤 다시 봐도 길을 잃지 않는 폴더 구조를 짤 수 있게 된다.
1. main.py 가 비대해지는 문제
처음 FastAPI 를 배울 때는 모든 코드를 main.py 한 곳에 적는 게 자연스럽다. 파일 하나만 열면 전부 보이니 오히려 편하다. 문제는 이 편안함의 유통기한이 짧다는 것이다. 라우트가 늘면 같은 파일에 이런 것들이 한꺼번에 쌓인다.
코드 자체는 틀린 게 없다. 그런데 파일이 비대해지면 세 가지가 동시에 나빠진다. 첫째, 원하는 함수를 찾으려면 매번 길게 스크롤해야 한다. 둘째, 둘 이상이 같이 작업하면 한 파일을 동시에 건드려 병합 충돌(merge conflict) 이 끊이지 않는다. 셋째, 사용자 관련 코드와 결제 관련 코드가 뒤섞여 있어, 한 기능만 떼어 내거나 테스트하기가 어려워진다.
해법의 방향은 분명하다 — 관련된 라우트끼리 묶어 별도 파일로 떼어 내자. 사용자 관련은 users.py, 상품 관련은 items.py 식으로. 그런데 라우트는 원래 @app.get 처럼 app 객체에 붙는다. 파일을 나누면 그 파일에는 app 이 없다. 이 딜레마를 풀어 주는 게 바로 APIRouter 다.
2. APIRouter 로 라우트 분리하기
APIRouter 는 한마디로 "app 의 미니 버전" 이다. app 없이도 라우트를 정의할 수 있는 작은 라우트 묶음을 만들어 두고, 나중에 본체 app 에 통째로 끼워 넣는 방식이다. 먼저 사용자 라우트를 별도 파일로 옮겨 보자.
달라진 점은 딱 하나 — @app.get 대신 @router.get 을 쓴다는 것이다. 나머지 문법은 지금까지 배운 그대로다. 여기서 진가를 발휘하는 게 APIRouter 에 넘긴 두 인자다. prefix="/users" 덕분에 이 파일 안에서는 경로를 /users/list 처럼 매번 반복할 필요 없이 / 만 적으면 된다. 자동으로 앞에 /users 가 붙는다. tags=["users"] 는 /docs 자동 문서에서 이 라우트들을 "users" 라는 접이식 그룹으로 묶어 준다.
이제 이 라우터를 본체에 연결할 차례다. main.py 는 다시 가벼워진다.
app.include_router(users.router) 한 줄이 users.py 안의 모든 라우트를 app 에 그대로 등록한다. 마치 라우터 묶음을 통째로 본체에 꽂아 넣는 셈이다. 이제 main.py 는 "어떤 라우터들을 합칠지" 만 선언하고, 실제 엔드포인트 로직은 각자의 파일에 흩어진다.
prefix·tags 는 APIRouter() 에 줄 수도 있고, include_router() 쪽에 줄 수도 있다. 예를 들어 app.include_router(users.router, prefix="/api/v1") 라고 하면 라우터에 적힌 /users 앞에 다시 /api/v1 이 더 붙어 최종 경로가 /api/v1/users 가 된다. API 버전 관리 를 이렇게 깔끔하게 처리할 수 있다.
3. 권장 프로젝트 구조
라우터를 나누는 법을 알았으니, 이제 프로젝트 전체를 어떻게 펼쳐 놓을지 정해 보자. FastAPI 공식 문서가 권하는, 그리고 실무에서 가장 흔히 보이는 구조는 다음과 같다. 정답이 하나는 아니지만, 처음이라면 이 틀을 그대로 따라가는 게 안전하다.
각 파일이 맡는 역할을 분명히 나눠 두는 게 이 구조의 핵심이다. 한 줄씩 보자.
| 파일 | 역할 |
|---|---|
| main.py | app = FastAPI() 를 만들고, 각 라우터를 include_router 로 합치는 조립 지점. 로직은 거의 없다. |
| routers/ | 기능별 라우트 묶음. 사용자·상품·주문처럼 도메인 단위로 파일을 쪼갠다. |
| models.py | 데이터베이스 테이블 구조 정의. SQLAlchemy 의 ORM 클래스가 여기 모인다 (7편 주제). |
| schemas.py | API 가 주고받는 데이터 형태. 요청 본문·응답 모델 같은 Pydantic 클래스. |
| database.py | DB 접속 엔진과 세션을 만드는 곳. 연결 정보를 한 곳에 모은다. |
| dependencies.py | 5편에서 배운 공통 Depends 함수들. 인증·DB 세션·페이지네이션 등. |
왜 굳이 models 와 schemas 를 따로 두느냐는 질문이 자주 나온다. 둘 다 "데이터 모양" 을 정의하지만 대상이 다르다. models.py 는 데이터베이스에 저장될 행(row)의 모양이고, schemas.py 는 API 바깥과 주고받는 JSON 의 모양이다. 예를 들어 비밀번호는 DB 모델에는 있어야 하지만, 사용자 정보를 돌려주는 응답 스키마에는 빠져야 한다. 이렇게 안과 밖의 데이터 형태를 분리해 두면 보안과 유연성을 동시에 챙길 수 있다.
app/ 과 routers/ 폴더 안에는 빈 __init__.py 파일이 하나씩 있어야 파이썬이 그 폴더를 패키지 로 인식한다. 이게 없으면 from routers import users 같은 import 가 ModuleNotFoundError 로 실패한다. 실행은 프로젝트 최상위에서 fastapi dev app/main.py 로 한다.
4. 미들웨어 & CORS
구조를 갖췄으니 마지막으로 미들웨어(middleware) 를 보자. 미들웨어는 모든 요청이 라우트 함수에 닿기 전과, 응답이 나가기 직전에 한 번씩 거치는 공통 통로 다. 건물 입구의 검문소를 떠올리면 된다. 누가 어느 방으로 가든 들어올 때와 나갈 때 반드시 그 검문소를 지난다.
요청마다 처리 시간을 재서 응답 헤더에 붙이는 간단한 미들웨어를 직접 만들어 보자. 모든 엔드포인트에 일일이 코드를 넣지 않아도, 이 한 곳이 전체 요청을 감싼다.
흐름을 따라가 보자. 요청이 들어오면 call_next(request) 앞 코드가 먼저 돌고(시작 시각 기록), call_next 가 실제 라우트 함수를 실행한 뒤, 그 뒤 코드가 응답을 받아 헤더를 붙이고 로그를 찍는다. 라우트가 100개여도 이 로깅·타이밍 로직은 단 한 곳에만 존재한다.
실무에서 거의 항상 마주치는 미들웨어가 하나 더 있다 — CORS. 브라우저는 보안상 "내가 띄운 페이지의 주소" 와 "API 서버의 주소" 가 다르면 요청을 막는다. React 개발 서버(localhost:3000)에서 FastAPI(localhost:8000)를 부르는 순간 바로 이 벽에 부딪힌다. FastAPI 가 기본 제공하는 CORSMiddleware 로 허용할 출처를 열어 준다.
직접 만든 미들웨어는 @app.middleware("http") 데코레이터로, FastAPI 가 제공하는 기성 미들웨어는 app.add_middleware(클래스, 옵션...) 형태로 등록한다. 방식만 다를 뿐, 둘 다 "모든 요청을 감싸는 공통 통로" 라는 점은 같다.
allow_origins=["*"] 로 모든 출처를 여는 건 개발 중에만. 배포 단계에서는 반드시 우리 프론트엔드의 실제 도메인만 명시해야 한다. 또 미들웨어는 등록한 순서의 역순 으로 요청을 감싸므로, 여러 개를 쓸 때는 순서가 동작에 영향을 준다는 점을 기억해 두자.
요약
6편을 한 줄로 — 커지는 main.py 를 APIRouter 로 기능별 파일로 쪼개고, include_router 로 다시 합친다. prefix 와 tags 로 경로와 문서를 깔끔하게 정리하고, routers·models·schemas·database·dependencies 로 역할을 나눈 디렉터리가 협업과 유지보수를 동시에 살린다. 미들웨어는 모든 요청을 감싸는 공통 통로로, 로깅·타이밍 같은 횡단 관심사와 CORS 처리를 한 곳에서 끝낸다. 구조를 잡았으니, 이제 진짜 데이터를 다룰 차례다.
다음 편 예고 — 데이터베이스 연동 (SQLAlchemy)
방금 비워 둔 models.py·database.py 를 채운다. SQLAlchemy 로 테이블을 정의하고, 의존성으로 DB 세션을 주입받아 실제 데이터를 저장·조회한다.