FastAPI 프로젝트가 커질 때 main.py 하나로 관리하기 어려워집니다. APIRouter, schemas, models, database 파일을 어떤 기준으로 나누면 좋은지 초보자 눈높이에서 정리합니다.
FastAPI 프로젝트 구조는 처음부터 복잡하게 잡을 필요는 없습니다. 다만 API가 늘어나고 데이터베이스, 요청 모델, 응답 모델이 생기면 main.py 하나로 관리하기 어려워집니다.
처음에는 한 파일로 시작해도 괜찮습니다.
하지만 어느 순간 이런 문제가 생깁니다.
main.py 안에
- API 코드
- Pydantic 모델
- DB 연결 코드
- 테이블 모델
- 예외 처리
- 비즈니스 로직
이 전부 섞여 있음
이렇게 되면 코드를 고치기 어려워집니다.
새로운 API를 추가할 때마다 main.py를 계속 열어야 하고, 어디에 무엇이 있는지도 헷갈립니다.
FastAPI 공식 문서에서도 작은 예제는 한 파일로 작성할 수 있지만, 실제 웹 API를 만들다 보면 여러 파일로 나누는 경우가 많다고 설명합니다. 이때 핵심이 되는 기능이 APIRouter입니다.
처음에는 main.py 하나로도 충분하다
입문 단계에서는 아래처럼 작성해도 문제 없습니다.
fastapi-basic/
└── main.py
예를 들어 간단한 API만 만들 때는 main.py 하나가 더 이해하기 쉽습니다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello FastAPI"}
이 단계에서 억지로 폴더를 많이 나누면 오히려 헷갈립니다.
초보자가 처음부터 아래처럼 구조를 만들 필요는 없습니다.
app/
├── api/
├── core/
├── db/
├── models/
├── schemas/
├── services/
└── repositories/
이 구조가 틀렸다는 뜻은 아닙니다.
다만 아직 API가 몇 개 없는 상태라면 과한 구조입니다.
프로젝트 구조는 “처음부터 멋있게”가 아니라, 코드가 많아졌을 때 “덜 헷갈리게” 만드는 도구에 가깝습니다.
FastAPI 프로젝트는 언제 나눠야 할까?
아래 상황이 생기면 파일 분리를 고민할 만합니다.
- API 주소가 5개 이상으로 늘어남
- users, items, posts처럼 기능이 나뉘기 시작함
- Pydantic 모델이 많아짐
- 데이터베이스 연결 코드가 들어감
- 인증, 예외 처리, 설정값 관리가 필요해짐
- main.py가 너무 길어져서 스크롤이 많아짐
특히 main.py에서 아래 코드들이 한꺼번에 보이기 시작하면 분리할 시점입니다.
app = FastAPI()
class UserCreate(BaseModel):
...
class UserResponse(BaseModel):
...
engine = create_engine(...)
@app.get("/users")
def get_users():
...
@app.post("/users")
def create_user():
...
이런 코드는 처음에는 편합니다.
하지만 프로젝트가 커질수록 수정하기 어려워집니다.
입문자에게 적당한 FastAPI 프로젝트 구조
처음 파일을 나눠본다면 아래 정도가 적당합니다.
fastapi-project/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ ├── schemas.py
│ └── routers/
│ ├── __init__.py
│ └── users.py
└── requirements.txt
이 구조는 너무 복잡하지 않으면서도, FastAPI 프로젝트에서 자주 등장하는 역할을 분리해 줍니다.
FastAPI 공식 문서의 “Bigger Applications” 예시도 app/main.py, dependencies.py, routers/ 같은 방식으로 파일을 나누는 흐름을 보여줍니다. 특히 routers 폴더 안에 기능별 API 파일을 두는 방식이 핵심입니다.
각 파일의 역할
위 구조에서 각 파일은 대략 이런 역할을 합니다.
app/main.py → FastAPI 앱 생성, 라우터 등록
app/database.py → 데이터베이스 연결 설정
app/models.py → DB 테이블 모델
app/schemas.py → 요청/응답 데이터 모델
app/routers/users.py → users 관련 API
하나씩 보면 어렵지 않습니다.
main.py: 앱의 시작점
main.py는 FastAPI 앱이 시작되는 파일입니다.
from fastapi import FastAPI
from app.routers import users
app = FastAPI()
app.include_router(users.router)
@app.get("/")
def read_root():
return {"message": "FastAPI project structure"}
여기서 중요한 코드는 이 부분입니다.
app.include_router(users.router)
users.py에 작성한 API를 main.py에 연결하는 코드입니다.
즉, main.py는 모든 API 코드를 직접 들고 있는 파일이 아닙니다.
여러 라우터를 모아서 앱에 등록하는 역할을 합니다.
routers/users.py: 기능별 API 파일
routers/users.py에는 사용자와 관련된 API를 작성합니다.
from fastapi import APIRouter
router = APIRouter(
prefix="/users",
tags=["users"]
)
@router.get("/")
def get_users():
return [
{"id": 1, "name": "kim"},
{"id": 2, "name": "lee"}
]
@router.get("/{user_id}")
def get_user(user_id: int):
return {"id": user_id, "name": "kim"}
여기서는 FastAPI() 대신 APIRouter()를 사용합니다.
router = APIRouter(
prefix="/users",
tags=["users"]
)
prefix="/users"를 지정했기 때문에 아래 API는
@router.get("/")
def get_users():
...
실제로는 다음 주소로 동작합니다.
GET /users/
그리고 아래 API는
@router.get("/{user_id}")
def get_user(user_id: int):
...
다음 주소로 동작합니다.
GET /users/1
이 방식의 장점은 명확합니다.
사용자 API는 users.py, 게시글 API는 posts.py, 상품 API는 products.py처럼 기능별로 나눌 수 있습니다.
FastAPI의 APIRouter는 이런 식으로 여러 API를 모듈 단위로 나눌 때 사용하는 도구입니다. Flask를 써본 사람이라면 Blueprint와 비슷한 역할로 이해할 수 있습니다.
prefix와 슬래시 경로를 헷갈리지 말자
여기서 한 가지 주의할 점이 있습니다.
prefix="/users"를 지정한 상태에서 라우터 내부 경로를 어떻게 쓰느냐에 따라 실제 주소가 조금 달라질 수 있습니다.
router = APIRouter(prefix="/users")
@router.get("")
def get_users():
return []
이 경우 경로는 보통 아래처럼 이해하면 됩니다.
GET /users
반면 아래처럼 작성하면
router = APIRouter(prefix="/users")
@router.get("/")
def get_users():
return []
경로는 다음처럼 됩니다.
GET /users/
둘 중 하나가 무조건 정답은 아닙니다.
다만 프로젝트 안에서 기준을 섞으면 헷갈릴 수 있습니다.
예를 들어 어떤 파일에서는 @router.get("")를 쓰고, 다른 파일에서는 @router.get("/")를 쓰면 주소 끝의 슬래시 때문에 예상과 다르게 동작한다고 느낄 수 있습니다.
입문 단계에서는 아래처럼 하나의 기준을 정해두는 편이 좋습니다.
목록 조회 API는 @router.get("/") 형태로 작성한다.
prefix는 /users, /posts처럼 기능 단위로 지정한다.
이 글의 예제도 눈으로 보기 쉬운 @router.get("/") 방식을 기준으로 설명합니다.
schemas.py: 요청과 응답 모델 분리
schemas.py에는 Pydantic 모델을 작성합니다.
from pydantic import BaseModel
class UserCreate(BaseModel):
name: str
email: str
class UserResponse(BaseModel):
id: int
name: str
email: str
이 파일은 API에서 주고받는 데이터 모양을 정의하는 곳입니다.
예를 들어 회원가입 API가 있다면 요청 데이터는 이런 형태일 수 있습니다.
{
"name": "kim",
"email": "kim@example.com"
}
이때 UserCreate 모델을 사용하면 FastAPI가 요청 데이터의 구조를 검사해 줍니다.
@router.post("/")
def create_user(user: UserCreate):
return user
여기서 중요한 점은 schemas.py가 데이터베이스 테이블을 정의하는 파일은 아니라는 것입니다.
schemas.py는 API 요청과 응답의 모양을 담당합니다.
models.py: 데이터베이스 테이블 모델
models.py에는 데이터베이스 테이블과 연결되는 모델을 작성합니다.
예를 들어 SQLAlchemy를 사용한다면 이런 코드가 들어갈 수 있습니다.
from sqlalchemy import Column, Integer, String
from app.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
email = Column(String, unique=True, index=True, nullable=False)
schemas.py와 models.py를 헷갈리기 쉽습니다.
간단히 구분하면 이렇습니다.
| 파일 | 역할 |
|---|---|
schemas.py |
API 요청/응답 데이터 구조 |
models.py |
데이터베이스 테이블 구조 |
실제로는 둘 다 “모델”처럼 보이기 때문에 초보자가 자주 헷갈립니다.
하지만 역할이 다릅니다.
schemas.py는 클라이언트와 주고받는 데이터에 가깝고,models.py는 데이터베이스에 저장되는 구조에 가깝습니다.
database.py: DB 연결 설정
database.py에는 데이터베이스 연결 코드를 둡니다.
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine
)
Base = declarative_base()
SQLite를 사용할 때는 위처럼 sqlite:///./test.db 형태로 DB 파일을 지정할 수 있습니다.
FastAPI 공식 SQL 데이터베이스 문서에서도 입문 예제에서는 SQLite를 사용합니다. SQLite는 하나의 파일로 동작하고, Python에서 기본적으로 지원하기 때문에 예제와 실습에 적합합니다. 다만 실제 운영 환경에서는 PostgreSQL 같은 데이터베이스 서버를 사용할 수 있다고 안내합니다.
여기서 중요한 건 DB 연결 코드를 main.py에 직접 넣지 않는다는 점입니다.
DB 설정은 프로젝트 전체에서 재사용될 가능성이 높습니다.
따라서 database.py처럼 따로 분리하는 편이 관리하기 쉽습니다.
전체 코드 흐름으로 보면 더 쉽다
위 구조를 흐름으로 보면 이렇습니다.
main.py
└── users.router 등록
routers/users.py
└── /users API 작성
schemas.py
└── 요청/응답 데이터 구조 정의
models.py
└── DB 테이블 구조 정의
database.py
└── DB 연결 설정
즉, main.py가 모든 일을 하는 구조에서 벗어나 각 파일이 역할을 나눠 갖는 방식입니다.
이렇게 나누면 코드를 찾기 쉬워집니다.
사용자 API를 수정하고 싶으면 routers/users.py를 보면 됩니다.
요청 데이터 구조를 수정하고 싶으면 schemas.py를 보면 됩니다.
DB 연결 설정을 바꾸고 싶으면 database.py를 보면 됩니다.
기능이 더 많아지면 어떻게 나눌까?
프로젝트가 조금 더 커지면 routers 안에 파일을 추가할 수 있습니다.
app/
├── main.py
├── database.py
├── models.py
├── schemas.py
└── routers/
├── __init__.py
├── users.py
├── posts.py
└── comments.py
그리고 main.py에서 각각 등록합니다.
from fastapi import FastAPI
from app.routers import users, posts, comments
app = FastAPI()
app.include_router(users.router)
app.include_router(posts.router)
app.include_router(comments.router)
각 라우터 파일은 자기 기능만 담당합니다.
users.py → 사용자 API
posts.py → 게시글 API
comments.py → 댓글 API
이 정도만 해도 입문자 프로젝트에서는 충분히 깔끔합니다.
더 큰 프로젝트에서는 기능별 폴더로 나누기도 한다
프로젝트가 더 커지면 아래처럼 기능별로 폴더를 나누는 방식도 사용할 수 있습니다.
app/
├── main.py
├── database.py
└── users/
├── router.py
├── schemas.py
├── models.py
└── service.py
또는 여러 기능을 이렇게 나눌 수도 있습니다.
app/
├── main.py
├── database.py
├── users/
│ ├── router.py
│ ├── schemas.py
│ └── service.py
├── posts/
│ ├── router.py
│ ├── schemas.py
│ └── service.py
└── comments/
├── router.py
├── schemas.py
└── service.py
이 구조는 기능 단위가 명확한 프로젝트에서 유용합니다.
예를 들어 사용자 기능이 복잡해져서 사용자 API, 사용자 요청 모델, 사용자 서비스 로직이 많아졌다면 users/ 폴더로 묶는 편이 낫습니다.
다만 처음부터 이 구조로 시작할 필요는 없습니다.
입문 단계에서는 오히려 파일을 찾는 데 시간이 더 걸릴 수 있습니다.
services.py는 언제 필요할까?
초보자 프로젝트에서는 처음부터 services.py를 만들지 않아도 됩니다.
하지만 API 함수 안에 로직이 길어지면 분리하는 편이 좋습니다.
예를 들어 아래 코드는 API 함수가 너무 많은 일을 합니다.
@router.post("/")
def create_user(user: UserCreate):
# 이메일 중복 확인
# 비밀번호 암호화
# DB 저장
# 응답 데이터 생성
return result
이런 경우에는 실제 처리 로직을 서비스 함수로 분리할 수 있습니다.
@router.post("/")
def create_user(user: UserCreate):
return user_service.create_user(user)
그러면 API 파일은 요청을 받고 응답을 돌려주는 역할에 집중할 수 있습니다.
다만 이 단계는 프로젝트가 어느 정도 커졌을 때 고민해도 늦지 않습니다.
dependencies.py는 언제 필요할까?
FastAPI에는 의존성 주입 기능이 있습니다.
FastAPI의 Dependency Injection 시스템은 다른 구성 요소를 쉽게 통합하고 재사용할 수 있도록 설계되어 있습니다.
예를 들어 DB 세션을 여러 API에서 반복해서 사용한다면 아래처럼 의존성 함수를 만들 수 있습니다.
from app.database import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
이런 함수는 dependencies.py에 둘 수 있습니다.
app/
├── main.py
├── database.py
├── dependencies.py
└── routers/
└── users.py
그리고 API에서 이렇게 사용합니다.
from fastapi import Depends
from sqlalchemy.orm import Session
from app.dependencies import get_db
@router.get("/")
def get_users(db: Session = Depends(get_db)):
return []
처음에는 낯설 수 있지만, DB 연결이나 인증 처리처럼 여러 API에서 반복되는 코드를 줄일 때 유용합니다.
__init__.py는 왜 필요할까?
폴더 구조 예시를 보면 __init__.py 파일이 자주 보입니다.
app/
├── __init__.py
└── routers/
├── __init__.py
└── users.py
__init__.py는 해당 폴더를 Python 패키지로 인식하게 해주는 파일입니다.
최근 Python에서는 일부 상황에서 없어도 동작할 수 있지만, 입문자라면 일단 만들어두는 편이 덜 헷갈립니다.
특히 아래처럼 import를 할 때 도움이 됩니다.
from app.routers import users
파일 안에는 아무 코드가 없어도 됩니다.
# 비워둬도 됩니다.
초보자가 피하면 좋은 구조
FastAPI 프로젝트를 처음 나눌 때 흔히 하는 실수가 있습니다.
너무 빨리 구조를 복잡하게 만드는 것입니다.
예를 들어 API가 3개뿐인데 아래처럼 나누면 오히려 불편합니다.
app/
├── api/
│ └── v1/
│ └── endpoints/
├── core/
├── crud/
├── db/
├── models/
├── schemas/
├── services/
├── repositories/
└── utils/
이런 구조는 실무 프로젝트에서 쓸 수는 있습니다.
하지만 입문자 입장에서는 “코드를 어디에 넣어야 하지?”라는 고민만 늘어납니다.
처음에는 아래 정도면 충분합니다.
app/
├── main.py
├── database.py
├── models.py
├── schemas.py
└── routers/
└── users.py
프로젝트 구조는 실력 과시용이 아닙니다.
수정하기 쉬운 코드를 만들기 위한 최소한의 정리입니다.
추천하는 단계별 구조
처음부터 최종 구조를 정하려고 하지 말고, 단계별로 확장하는 편이 좋습니다.
1단계: 가장 작은 구조
fastapi-project/
└── main.py
간단한 API 테스트나 학습용 예제에 적합합니다.
2단계: 라우터 분리
fastapi-project/
├── app/
│ ├── main.py
│ └── routers/
│ └── users.py
└── requirements.txt
API가 기능별로 나뉘기 시작할 때 적합합니다.
3단계: DB와 모델 분리
fastapi-project/
├── app/
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ ├── schemas.py
│ └── routers/
│ └── users.py
└── requirements.txt
SQLite, SQLAlchemy, Pydantic 모델이 들어가는 CRUD 프로젝트에 적합합니다.
4단계: 기능별 폴더 구조
fastapi-project/
├── app/
│ ├── main.py
│ ├── database.py
│ ├── users/
│ │ ├── router.py
│ │ ├── schemas.py
│ │ └── service.py
│ └── posts/
│ ├── router.py
│ ├── schemas.py
│ └── service.py
└── requirements.txt
기능이 많아지고 각 기능별 코드가 커질 때 적합합니다.
FastAPI 프로젝트 구조를 잡을 때 기준
파일을 나눌 때는 이름보다 기준이 더 중요합니다.
아래 기준으로 판단하면 됩니다.
main.py
→ 앱 생성과 라우터 등록만 남긴다.
routers/
→ API 주소별 처리 코드를 둔다.
schemas.py
→ 요청/응답 데이터 모델을 둔다.
models.py
→ DB 테이블 모델을 둔다.
database.py
→ DB 연결 설정을 둔다.
dependencies.py
→ 여러 API에서 반복해서 쓰는 의존성 함수를 둔다.
services.py
→ API 안에서 처리하기엔 긴 비즈니스 로직을 둔다.
처음부터 모든 파일이 필요하지는 않습니다.
예를 들어 DB를 쓰지 않는 프로젝트라면 database.py, models.py는 필요 없습니다.
서비스 로직이 거의 없다면 services.py도 없어도 됩니다.
필요할 때 추가하면 됩니다.
처음 구조는 단순한 게 좋다
FastAPI 프로젝트 구조에 정답은 없습니다.
하지만 입문자라면 아래 구조부터 시작하는 게 가장 무난합니다.
app/
├── main.py
├── database.py
├── models.py
├── schemas.py
└── routers/
└── users.py
이 구조만 이해해도 FastAPI 프로젝트를 한 파일에서 벗어나 관리할 수 있습니다.
여기서 중요한 기준은 하나입니다.
파일을 많이 나누는 것이 좋은 구조가 아니라, 코드를 찾고 고치기 쉬운 구조가 좋은 구조입니다.
API가 적으면 main.py 하나로 시작해도 됩니다.
API가 늘어나면 routers로 나누면 됩니다.
DB와 요청 모델이 생기면 database.py, models.py, schemas.py를 분리하면 됩니다.
처음부터 실무형 구조를 따라 하기보다, 프로젝트가 커지는 속도에 맞춰 한 단계씩 나누는 편이 더 안전합니다.
'개발 > FastAPI' 카테고리의 다른 글
| FastAPI CORS 설정 방법: 프론트엔드 연동할 때 막히는 이유 (0) | 2026.05.29 |
|---|---|
| FastAPI JWT 로그인 구현 기초: 토큰 인증 흐름 이해하기 (0) | 2026.05.29 |
| FastAPI CRUD API 만들기: GET, POST, PUT, DELETE 한 번에 이해하기 (0) | 2026.05.27 |
| FastAPI SQLite 연결하기: SQLModel로 DB 저장 API 만들기 (0) | 2026.05.26 |
| FastAPI Swagger 문서 자동 생성, `/docs` 화면이 만들어지는 원리 (0) | 2026.05.26 |