Python FastAPI 입문(3/5)
Python/FastAPI

Pydantic과 의존성 주입 - FastAPI의 핵심 두 가지

Pydantic으로 데이터를 검증하고, Depends()로 의존성을 주입하는 방법을 이해한다.

2026-03-25
8 min read
#Python#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) 하나로 처리한다.


정리

개념설명
BaseModelPydantic 데이터 구조 정의. 자동 타입 검증
Field필드에 세부 조건 (min_length, ge 등) 추가
response_model라우터 응답 스키마 지정
Depends()의존성 주입. 함수 파라미터에 객체를 자동으로 넣어줌
yield 의존성요청 전/후 처리 (DB 세션 열고 닫기 등)

실제 유저 API를 만들면서 이 개념들을 코드로 보는 것은 다음 편에서 다룬다.


시리즈: Python FastAPI 입문

  1. 시작하기 전에
  2. FastAPI 구조 - 라우터, 3계층 아키텍처
  3. Pydantic과 의존성 주입 ← 현재 글
  4. REST API 만들기
  5. SQLAlchemy와 데이터베이스 연동