FastAPI에서 response_model vs 수동 매핑: Pydantic v2로 알아보는 ORM 객체 처리

Dior·2025년 1월 5일
0

[FastAPI]

목록 보기
1/1
post-thumbnail

서론

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 모델 예시

# 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 모델 예시

# 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 ..." 라는 오류가 발생한 상황입니다.

왜 에러가 발생할까?

  1. 자동 매핑(response_model)

    • FastAPI 내부적으로 반환된 SQLAlchemy 객체를 파이썬 dict로 변환한 뒤, 이를 Pydantic 모델(예: ParentDto)에 매핑합니다.

    • relationship(중첩된 필드)도 자동으로 접근·변환하며, SQLAlchemy 객체가 아니라 dict로 취급해 직렬화 로직을 돌리기 때문에 가능한 것입니다.

  2. 직접 매핑(수동 변환)

    • 개발자가 명시적으로 Pydantic 모델 인스턴스를 생성할 때는, Pydantic이 ORM 객체(예: Parent)를 직접 다루는 법을 알지 못합니다.

    • "이건 단순 dict가 아니라 SQLAlchemy 객체야"라는 사실을 Pydantic 쪽에 별도로 알려주지 않으면, 중첩된 Child 객체를 어떻게 변환해야 할지 몰라서 에러가 납니다.

Pydantic v2에서의 해결책

Pydantic v2에서는 orm_mode가 deprecated 상태가 되었고, 그 대신 아래 두 가지 요소를 도입했습니다.

  1. from_attributes = True

    • Pydantic 모델(예: ChildDto, ParentDto)에 model_config로 설정

    • “이 모델은 속성 기반(ORM 객체)에서 데이터를 가져올 수 있다”는 걸 Pydantic에 알려줍니다.

  2. model_validate()

    • Pydantic v1의 from_orm()의 대체 메서드

    • “이 ORM 객체(혹은 다른 객체)를 검증해, Pydantic 모델 인스턴스로 변환하겠다”라는 의도를 명시합니다.

    • ORM 객체를 다루려면, Pydantic 모델에 from_attributes = True가 선행되어야 합니다.

Pydantic 모델 예시 수정

# 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로 변환됩니다.

수동 매핑 (model_validate() 사용)

# 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()를 쓰면, 수동 매핑 로직이 더 간결해지고 명시적입니다.

P.S.

오랜만에 FastAPI 최신 공식문서를 들어가니 많은 것들이 추가된 것을 확인했습니다. CLI 기능을 강화한 fastapi-cli와 SQLAlchemy와 Pydantic을 통합한 SQLModel이 적극적으로 권장되고 있었습니다. 이후 포스팅에서 FastAPI의 업데이트된 최신 기능들에 대해서 다뤄보겠습니다.

참고자료

Pydantic 공식문서

🙇🏻‍ 잘못된 정보는 댓글을 통해 알려주시면 감사하겠습니다.

profile
Focus on growth rather than material

0개의 댓글