FastAPI를 사용하는 개발자들은 대부분 DB 접근을 위해 SQLAlchemy 모델을 정의하고 응답을 위한 DTO를 Pydantic으로 작성합니다. 이때, FastAPI에서 router에 response_model을 사용해 SQLAlchemy 객체를 DTO로 자동 매핑해서 사용하지만, 때로는 직접 매핑(수동 변환)이 필요할 때가 있습니다.
저는 직접 매핑을 시도하던 중 Pydantic 오류를 마주했고, 이를 계기로 자동 매핑과 직접 매핑 간의 차이점을 제대로 이해해야겠다고 생각했습니다.
마침 Pydantic v2(2024-06-30 릴리스)에서 ORM 객체를 다루는 방식이 일부 바뀌었는데, 기존 orm_mode가 deprecated 상태가 되면서 from_attributes 설정과 model_validate() 메서드가 새롭게 등장했죠. 이번 글에서는 이 변화를 중심으로 자동 매핑과 수동 매핑의 차이를 살펴보겠습니다.
router에 response_model을 설정해두면, 중첩된 객체(예: Child)까지 자동으로 변환돼 잘 동작합니다. 그런데 직접 Pydantic 모델을 만들어 매핑하려고 하면, Pydantic 에러가 발생합니다.
# SQLAlchemy 모델 예시
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
name = Column(String(255))
children = relationship("Child", back_populates="parent")
class Child(Base):
__tablename__ = 'children'
id = Column(Integer, primary_key=True)
name = Column(String(255))
parent_id = Column(Integer, ForeignKey('parents.id'))
parent = relationship("Parent", back_populates="children")
# Pydantic 모델 예시
class ChildDto(BaseModel):
id: int
name: str
class ParentDto(BaseModel):
id: int
name: str
children: list[ChildDto]
위 모델들을 이용해, 자동 매핑과 직접 매핑을 각각 시도해봅시다.
@app.get(*"/parent", response_model=ParentDto)
def get_parent():
# ORM 객체 생성
parent_orm = Parent(id=1, name="John")
child_orm1 = Child(id=1, name="Alice", parent_id=1)
child_orm2 = Child(id=2, name="Bob", parent_id=1)
parent_orm.children = [child_orm1, child_orm2]
return parent_orm # FastAPI가 내부적으로 "Parent -> ParentDto" 변환
# Response body
{
"id": 1,
"name": "John",
"children": [
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
]
}
이처럼 response_model을 설정해두면, FastAPI가 알아서 Parent(SQLAlchemy 객체)를 ParentDto(Pydantic 모델)로 변환해줍니다.
# ORM 객체 생성
parent_orm = Parent(id=1, name="John")
child_orm1 = Child(id=1, name="Alice", parent_id=1)
child_orm2 = Child(id=2, name="Bob", parent_id=1)
parent_orm.children = [child_orm1, child_orm2]
# 부모 객체를 직접 Pydantic 모델로 변환
parent_dto = ParentDto(
id=parent_orm.id,
name=parent_orm.name,
children=parent_orm.children # 이 부분에서 Pydantiic 에러 발생
)
# Error Message
Input should be a valid dictionary or instance of ParentDto [type=model_type, input_value=<app.main.Parent object at 0x7fc36122a780>, input_type=Parent]
중첩된 ORM 객체(Child)를 처리하지 못해, "Input should be a valid dictionary or instance of ..." 라는 오류가 발생한 상황입니다.
자동 매핑(response_model)
FastAPI 내부적으로 반환된 SQLAlchemy 객체를 파이썬 dict로 변환한 뒤, 이를 Pydantic 모델(예: ParentDto)에 매핑합니다.
relationship(중첩된 필드)도 자동으로 접근·변환하며, SQLAlchemy 객체가 아니라 dict로 취급해 직렬화 로직을 돌리기 때문에 가능한 것입니다.
직접 매핑(수동 변환)
개발자가 명시적으로 Pydantic 모델 인스턴스를 생성할 때는, Pydantic이 ORM 객체(예: Parent)를 직접 다루는 법을 알지 못합니다.
"이건 단순 dict가 아니라 SQLAlchemy 객체야"라는 사실을 Pydantic 쪽에 별도로 알려주지 않으면, 중첩된 Child 객체를 어떻게 변환해야 할지 몰라서 에러가 납니다.
Pydantic v2에서는 orm_mode가 deprecated 상태가 되었고, 그 대신 아래 두 가지 요소를 도입했습니다.
from_attributes = True
Pydantic 모델(예: ChildDto, ParentDto)에 model_config로 설정
“이 모델은 속성 기반(ORM 객체)에서 데이터를 가져올 수 있다”는 걸 Pydantic에 알려줍니다.
model_validate()
Pydantic v1의 from_orm()의 대체 메서드
“이 ORM 객체(혹은 다른 객체)를 검증해, Pydantic 모델 인스턴스로 변환하겠다”라는 의도를 명시합니다.
ORM 객체를 다루려면, Pydantic 모델에 from_attributes = True가 선행되어야 합니다.
# Pydantic 모델 예시
class ChildDto(BaseModel):
id: int
name: str
model_config = {
"from_attributes": True # ORM 객체 속성 사용 가능
}
class ParentDto(BaseModel):
id: int
name: str
children: list[ChildDto]
model_config = {
"from_attributes": True # ORM 객체 속성 사용 가능
}
이렇게 model_config에 "from_attributes": True를 추가해주면, 직접 매핑 시에도 Pydantic이 ORM 객체 속성을 인식할 수 있습니다.
# ORM 객체 생성
parent_orm = Parent(id=1, name="John")
child_orm1 = Child(id=1, name="Alice", parent_id=1)
child_orm2 = Child(id=2, name="Bob", parent_id=1)
parent_orm.children = [child_orm1, child_orm2]
# 부모 객체를 직접 Pydantic 모델로 변환
parent_dto = ParentDto(
id=parent_orm.id,
name=parent_orm.name,
children=parent_orm.children # from_attributes=True로 인해 에러 발생 X
)
print(parent_dto)
# 출력
id=1 name='John' children=[ChildDto(id=1, name='Alice'), ChildDto(id=2, name='Bob')]
"from_attributes": True를 설정했기에, 중첩된 자식 ORM 객체들도 알아서 ChildDto로 변환됩니다.
# ORM 객체 생성
parent_orm = Parent(id=1, name="John")
child_orm1 = Child(id=1, name="Alice", parent_id=1)
child_orm2 = Child(id=2, name="Bob", parent_id=1)
parent_orm.children = [child_orm1, child_orm2]
# 부모 객체를 직접 Pydantic 모델로 변환(model_validate() 사용)
parent_dto = ParentDto.model_validate(parent_orm)
print(parent_dto)
# 출력
id=1 name='John' children=[ChildDto(id=1, name='Alice'), ChildDto(id=2, name='Bob')]
model_validate()를 쓰면, 수동 매핑 로직이 더 간결해지고 명시적입니다.
오랜만에 FastAPI 최신 공식문서를 들어가니 많은 것들이 추가된 것을 확인했습니다. CLI 기능을 강화한 fastapi-cli와 SQLAlchemy와 Pydantic을 통합한 SQLModel이 적극적으로 권장되고 있었습니다. 이후 포스팅에서 FastAPI의 업데이트된 최신 기능들에 대해서 다뤄보겠습니다.
참고자료
Pydantic 공식문서
🙇🏻 잘못된 정보는 댓글을 통해 알려주시면 감사하겠습니다.