Pydantic과 의존성 주입 - FastAPI의 핵심 두 가지
Pydantic으로 데이터를 검증하고, Depends()로 의존성을 주입하는 방법을 이해한다.
Pydantic이란?
Pydantic은 데이터 검증 라이브러리다. FastAPI가 내부적으로 사용하며, 요청/응답 데이터의 타입과 형식을 자동으로 검사해준다.
쉽게 말하면 이런 역할이다.
클라이언트가 보낸 데이터
└── "name은 문자열인가?"
└── "email 형식이 맞나?" ← Pydantic이 자동으로 검사
└── "age가 숫자인가?"
└── 문제 있으면 자동으로 400 오류 응답
BaseModel - 데이터 구조 정의
Pydantic을 쓰려면 BaseModel을 상속한 클래스를 만든다.
from pydantic import BaseModel
class CreateUserRequest(BaseModel):
name: str
email: str
age: int
이게 전부다. 이 클래스를 라우터 함수의 파라미터 타입으로 쓰면, FastAPI가 알아서 요청 바디를 이 구조로 파싱하고 검증한다.
@router.post("/users")
def create_user(request: CreateUserRequest):
# request.name, request.email, request.age 바로 사용 가능
# 타입이 틀리면 이 함수 실행 전에 이미 오류가 난다
return request
검증 실패 시 자동 오류
클라이언트가 잘못된 데이터를 보내면 FastAPI가 자동으로 400 오류를 보낸다.
// 잘못된 요청: age에 문자열을 보낸 경우
{
"name": "김철수",
"email": "chul@test.com",
"age": "스물다섯"
}
// 자동 응답 (400 Bad Request)
{
"detail": [
{
"loc": ["body", "age"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
개발자가 직접 검증 코드를 쓸 필요가 없다.
필드 옵션
Field를 써서 필드에 세부 조건을 붙일 수 있다.
from pydantic import BaseModel, Field, EmailStr
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=50)
email: EmailStr # 이메일 형식 자동 검증
age: int = Field(ge=0, le=150) # 0 이상 150 이하
nickname: str | None = None # 선택 필드 (없어도 됨)
| 옵션 | 의미 |
|---|---|
min_length | 최소 문자 길이 |
max_length | 최대 문자 길이 |
ge (greater or equal) | 이상 |
le (less or equal) | 이하 |
EmailStr | 이메일 형식 검증 |
str | None = None | 선택 필드 |
요청 스키마와 응답 스키마 분리
요청 데이터와 응답 데이터는 구조가 다를 수 있다. 예를 들어 요청에는 id가 없지만 응답에는 있다.
# schemas.py
# 요청: 클라이언트 → 서버
class CreateUserRequest(BaseModel):
name: str
email: str
# 응답: 서버 → 클라이언트
class UserResponse(BaseModel):
id: int
name: str
email: str
class Config:
from_attributes = True # DB 모델 → Pydantic 모델 변환 허용
from_attributes = True는 SQLAlchemy 같은 ORM 객체를 Pydantic 모델로 자동 변환할 때 필요하다.
라우터에서 response_model을 지정하면 응답 구조가 자동으로 맞춰진다.
@router.post("/users", response_model=UserResponse)
def create_user(request: CreateUserRequest):
# ... 유저 생성 로직
# UserResponse 구조에 맞게 자동 필터링해서 응답
의존성 주입(Dependency Injection)이란?
의존성 주입이란 클래스나 함수가 필요로 하는 객체(의존성)를 직접 만들지 않고, 외부에서 받아서 쓰는 방식이다.
직접 만드는 경우(의존성 주입 없음):
def get_user(user_id: int):
repository = UserRepository() # 직접 생성
service = UserService(repository) # 직접 생성
return service.get_user(user_id)
문제점:
- 라우터 함수 안에서 Service, Repository를 직접 만든다
- 같은 코드가 모든 라우터 함수에 반복된다
- 테스트할 때 실제 DB 대신 가짜 객체를 넣기 어렵다
Depends() - FastAPI의 의존성 주입
FastAPI는 Depends()로 의존성 주입을 지원한다.
from fastapi import Depends
# 의존성 함수 - "필요한 객체를 만들어서 줘"
def get_user_service():
repository = UserRepository()
return UserService(repository)
# 라우터 함수 - Depends()로 받아서 씀
@router.get("/users/{user_id}")
def get_user(user_id: int, service: UserService = Depends(get_user_service)):
return service.get_user(user_id)
Depends(get_user_service)는 "이 파라미터의 값은 get_user_service() 함수를 실행해서 만들어줘"라는 의미다.
FastAPI가 라우터 함수를 실행하기 전에 get_user_service()를 먼저 실행하고 그 결과를 service 파라미터에 넣어준다.
Spring Boot와 비교
// Spring Boot - @Autowired (생성자 주입)
@RestController
class UserController(
private val userService: UserService // Spring이 자동으로 주입
) {
@GetMapping("/users/{id}")
fun getUser(@PathVariable id: Long) = userService.getUser(id)
}
# FastAPI - Depends()
@router.get("/users/{user_id}")
def get_user(user_id: int, service: UserService = Depends(get_user_service)):
return service.get_user(user_id) # FastAPI가 자동으로 주입
둘 다 "객체를 직접 만들지 말고, 프레임워크가 만들어서 넣어줄게"라는 같은 원리다.
의존성 체이닝
의존성이 또 다른 의존성을 가질 수 있다.
# DB 세션을 만들어주는 의존성
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# DB 세션을 받아서 Repository를 만드는 의존성
def get_user_repository(db = Depends(get_db)):
return UserRepository(db)
# Repository를 받아서 Service를 만드는 의존성
def get_user_service(repo = Depends(get_user_repository)):
return UserService(repo)
# 라우터는 최종 결과만 받는다
@router.get("/users/{user_id}")
def get_user(user_id: int, service = Depends(get_user_service)):
return service.get_user(user_id)
FastAPI가 체인을 따라 get_db → get_user_repository → get_user_service 순서로 실행하고 결과를 주입한다.
라우터 함수 실행
↑
get_user_service() 실행
↑
get_user_repository() 실행
↑
get_db() 실행 ← 가장 먼저 실행
yield를 쓰면 요청 처리 후 정리 작업도 할 수 있다. get_db()에서 yield db 이후의 db.close()는 요청이 끝나면 자동으로 실행된다.
공통 의존성 - 인증 예시
Depends()는 공통 처리에도 쓸 수 있다. 예를 들어 인증 확인:
def require_auth(token: str = Header(None)):
if token != "valid-token":
raise HTTPException(status_code=401, detail="인증 필요")
return token
# 이 라우터는 인증이 필요하다
@router.get("/users", dependencies=[Depends(require_auth)])
def get_users():
return []
모든 라우터에 같은 인증 로직을 복사할 필요 없이, Depends(require_auth) 하나로 처리한다.
정리
| 개념 | 설명 |
|---|---|
BaseModel | Pydantic 데이터 구조 정의. 자동 타입 검증 |
Field | 필드에 세부 조건 (min_length, ge 등) 추가 |
response_model | 라우터 응답 스키마 지정 |
Depends() | 의존성 주입. 함수 파라미터에 객체를 자동으로 넣어줌 |
yield 의존성 | 요청 전/후 처리 (DB 세션 열고 닫기 등) |
실제 유저 API를 만들면서 이 개념들을 코드로 보는 것은 다음 편에서 다룬다.
시리즈: Python FastAPI 입문
- 시작하기 전에
- FastAPI 구조 - 라우터, 3계층 아키텍처
- Pydantic과 의존성 주입 ← 현재 글
- REST API 만들기
- SQLAlchemy와 데이터베이스 연동