pydantic v2 마이그레이션, 실제로 어디서 코드가 깨지는지 정리해봤습니다

pydantic v2 마이그레이션

pydantic 공식 마이그레이션 가이드에는 변경 항목이 20개 넘게 나열되어 있습니다. 그런데 실제로 서비스 코드를 pydantic v1에서 v2로 옮겨보면, 이 중 대부분은 타입 힌트만 스치고 지나가고 딱 몇 군데에서 에러가 집중적으로 터집니다. 이번 글에서는 그 몇 군데—Config 클래스, validator 데코레이터, dict()/json() 메서드, Optional 필드 기본값—를 실제 코드 예시로 짚어보고, 호환성을 지키면서 넘어가는 방법도 함께 정리해보겠습니다.

전제부터 밝히면, 여기서 다루는 내용은 pydantic 2.0 이상(2025년 기준 2.x 최신 버전)과 Python 3.8 이상 환경을 기준으로 합니다. FastAPI를 함께 쓰신다면 0.100 버전 이상이어야 pydantic v2를 온전히 지원한다는 점도 미리 알아두시면 좋습니다.

Config 클래스가 가장 먼저 깨집니다

v1에서는 모델 안에 내부 클래스 class Config:를 두는 방식이 표준이었습니다. v2에서는 이 방식이 아예 제거되지는 않았지만 권장 방식이 아니게 되었고, model_config = ConfigDict(...) 형태로 바뀌었습니다.

# v1 방식
class User(BaseModel):
    name: str
    class Config:
        orm_mode = True
        allow_population_by_field_name = True

# v2 방식
from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    model_config = ConfigDict(from_attributes=True, populate_by_name=True)
    name: str

주의할 점은 옵션 이름도 같이 바뀌었다는 것입니다. orm_modefrom_attributes로, allow_population_by_field_namepopulate_by_name으로 이름이 변경됐습니다. 옛날 이름을 그대로 ConfigDict에 넣으면 TypeError가 아니라 조용히 무시되는 경우도 있어서, 코드 리뷰 때 눈으로 한 번 더 확인하시는 게 안전합니다.

validator 데코레이터, 이름과 동작 방식이 통째로 바뀌었나요?

파이썬 코드가 표시된 어두운 배경의 코드 편집기 화면

네, 여기가 pydantic v2 마이그레이션에서 가장 많은 에러가 몰리는 지점입니다. v1의 @validator, @root_validator는 v2에서 @field_validator, @model_validator로 이름이 바뀌었고, 데코레이터 인자와 함수 시그니처도 함께 바뀌었습니다.

# v1
from pydantic import BaseModel, validator

class Product(BaseModel):
    name: str
    price: float

    @validator('price')
    def price_must_be_positive(cls, v):
        if v <= 0:
            raise ValueError('price must be positive')
        return v

# v2
from pydantic import BaseModel, field_validator

class Product(BaseModel):
    name: str
    price: float

    @field_validator('price')
    @classmethod
    def price_must_be_positive(cls, v: float) -> float:
        if v <= 0:
            raise ValueError('price must be positive')
        return v

@classmethod를 빼먹어도 당장은 동작하는 것처럼 보이지만, pydantic 공식 문서는 @field_validator@classmethod를 명시적으로 붙이길 권장합니다. root_validator도 마찬가지로 mode='before' 또는 mode='after'를 지정하는 model_validator로 바뀌면서, 이전 값을 values: dict로 받던 방식에서 모델 인스턴스 자체를 받는 방식으로 동작이 달라졌습니다. 여러 필드를 한꺼번에 검증하던 로직이 있다면 이 부분은 꼭 다시 테스트해보셔야 합니다.

.dict()와 .json() 메서드를 쓰던 코드는 전부 손봐야 합니다

v1에서 흔히 쓰던 .dict(), .json(), .parse_obj() 같은 메서드는 v2에서 각각 .model_dump(), .model_dump_json(), .model_validate()로 이름이 바뀌었습니다. 기존 메서드도 당장 사라지진 않고 deprecation 경고와 함께 남아 있는 경우가 많지만, 이후 버전에서 제거될 예정이라 마이그레이션 시점에 같이 바꿔두는 게 낫습니다.

v1 메서드 v2 메서드 비고
.dict() .model_dump() 반환값은 여전히 dict
.json() .model_dump_json() 반환값은 여전히 JSON 문자열
.parse_obj(data) .model_validate(data) 클래스 메서드
.parse_raw(json_str) .model_validate_json(json_str) JSON 문자열 전용
__fields__ model_fields 클래스 속성

이름만 바뀐 게 아니라 내부 동작도 달라진 부분이 있습니다. 예를 들어 .dict()가 남아 있더라도 커스텀 직렬화 로직(json_encoders 등)의 처리 순서가 달라질 수 있어서, datetime이나 Decimal처럼 커스텀 인코더에 의존하던 필드는 출력값을 직접 비교해서 확인해보시는 걸 권합니다.

Optional 필드 기본값 처리, 왜 조용히 에러가 나나요?

v1에서는 name: Optional[str]처럼 타입만 Optional로 선언하면 기본값이 자동으로 None으로 설정됐습니다. v2에서는 이 암묵적 동작이 사라졌습니다. Optional[str]이라고 써도 기본값을 명시하지 않으면 그 필드는 필수 값으로 취급됩니다.

소프트웨어 버전 업그레이드와 마이그레이션을 나타내는 추상적 이미지

# v1에서는 통과, v2에서는 ValidationError
class Item(BaseModel):
    name: str
    description: Optional[str]  # v2에서는 필수 필드 취급

# v2에서 의도대로 동작하려면
class Item(BaseModel):
    name: str
    description: Optional[str] = None

이 차이는 코드를 눈으로 봐서는 잘 안 잡히고, 실제로 데이터를 넣어봐야 ValidationError가 뜨면서 드러납니다. 에러 메시지 형식 자체도 바뀌어서, v1의 문자열 기반 에러 대신 v2는 loc, msg, type, input을 담은 구조화된 딕셔너리를 반환합니다. 로그 파싱이나 에러 응답 포맷을 만들던 코드가 있다면 이 구조 변경도 함께 확인하셔야 합니다.

pydantic.v1 호환 레이어로 점진적으로 넘어가는 방법

한 번에 모든 모델을 고치기 어려운 대형 프로젝트라면, pydantic v2 패키지 안에 포함된 pydantic.v1 네임스페이스를 활용할 수 있습니다. from pydantic.v1 import BaseModel처럼 import 경로만 바꾸면 v1 문법을 그대로 쓰면서 pydantic v2 패키지를 설치할 수 있는 방식입니다.

다만 이 호환 레이어에는 트레이드오프가 있습니다. pydantic.v1로 만든 모델과 순수 v2 모델을 서로 필드로 참조하면 검증이 깨지는 경우가 있고, FastAPI처럼 pydantic에 강하게 의존하는 프레임워크에서는 두 버전 모델이 섞였을 때 스키마 생성이 실패하는 사례도 보고되어 있습니다. 즉 호환 레이어는 '전환 기간 동안 버티는 용도'로 보시는 게 맞고, 최종적으로는 전체를 v2 문법으로 옮기는 계획을 세워두시는 걸 권합니다.

자동 변환 도구로는 pydantic 팀이 공식 배포하는 bump-pydantic이 있습니다. validatorfield_validator로, .dict().model_dump()로 바꾸는 식의 기계적인 치환은 상당 부분 자동으로 처리해주지만, root_validatormode 지정이나 Optional 필드 기본값처럼 동작 자체가 바뀐 부분은 도구가 자동으로 판단하지 못하는 경우가 많아서 변환 후 diff를 직접 검토하셔야 합니다.

pydantic 공식 마이그레이션 가이드에는 이 글에서 다루지 않은 세부 변경 사항(strict 모드, JSON Schema 생성 방식 변경 등)까지 전부 정리되어 있으니, 프로젝트 규모가 크다면 한 번은 원문을 훑어보시는 걸 권합니다. pydantic 프로젝트 자체에 대한 배경은 위키백과 Pydantic 문서에서도 간단히 확인하실 수 있습니다.

정리하면, pydantic v2 마이그레이션은 타입 힌트 문법이 아니라 Config 클래스·validator 데코레이터·dict()/json() 메서드·Optional 기본값, 이 네 지점에서 실제 에러가 발생합니다. 프로젝트가 작다면 bump-pydantic으로 기계적 치환을 먼저 돌리고 남은 부분을 손으로 고치는 순서를, 프로젝트가 크다면 pydantic.v1 네임스페이스로 모듈 단위로 나눠 옮기는 순서를 추천드립니다. 어느 쪽이든 마이그레이션 직후에는 검증 실패 로그를 평소보다 며칠 더 유심히 살펴보시는 게 좋습니다.

파이썬 GIL 때문에 안 빨라질 때, 스레드·프로세스·async 고르는 기준

Leave a Comment