나는 이렇게 학습한다/Library

Pydantic 옵션 하나로 ORM 을 DTO 모델로 변환하기

daco2020 2023. 8. 22. 18:06

ORM 에서 DTO 로 모델 변환하기

ORM(Object-Relational Mapping) 객체를 통해 DB의 데이터를 가져왔다면 이를 DTO(Data Transfer Object)를 통해 다른 레이어로 전달하고 싶을 것이다.
 

이 글에서는 Pydantic 의 from_attributes=True 옵션을 활용하여 손쉽게 DTO를 생성하는 방법을 설명하고자 한다.

 
본론에 앞서 우선 ORM을 DTO로 변환하는 다양한 방법들을 살펴보자. (본론으로 넘어가고 싶다면 생략해도 좋다)

예를 들어 아래처럼 ORM과 DTO 모델이 명시되어 있다고 해보자.

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel

Base = declarative_base()


# ORM 클래스 정의
class UserORM(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)


# DTO 클래스 정의
class UserDTO(BaseModel):
    id: int
    name: str

 
ORM 을 인자로 받아 DTO 로 변환하는 함수를 사용할 수 있다.

# orm -> dto 변환 함수 작성
def to_dto(user_orm: UserORM) -> UserDTO: 
    return UserDTO(id=user_orm.id, name=user_orm.name)


user_orm = UserORM(id=1, name='Daco')
user_dto = to_dto(user_orm)

print(user_dto.model_dump_json()) # {"id": 1, "name": "Daco"}

 
혹은 ORM 내에 메서드를 만들어 변환할 수 있다.

class UserORM(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)

    # orm -> dto 변환 메서드 작성
    def to_dto(self) -> UserDTO:
        return UserDTO(id=1, name="Daco")


user_orm = UserORM(id=1, name='Daco')
user_dto = user_orm.to_dto()

print(user_dto.model_dump_json()) # {"id": 1, "name": "Daco"}

 
이 방법들 모두 유효하지만 함수를 새로 만들거나 메서드를 추가하는 등 추가작업이 필요하다.

다행스럽게도 Pydantic 은 기존 방법들보다 더 쉽게 모델 객체를 생성시켜주는 옵션을 제공한다.
 

옵션을 추가하여 더 쉽게 모델 변환하기

먼저 만들고자 하는 데이터 모델에 from_attributes=True 옵션을 추가하자.

class UserDTO(BaseModel):
    id: int
    name: str

    # 새로 추가한 옵션
    model_config = ConfigDict(from_attributes=True)

끝이다. 이제 사용법을 알아보자!

user_orm = UserORM(id=1, name='Daco')
user_dto = UserDTO.model_validate(user_orm)

print(user_dto.model_dump_json()) # {"id": 1, "name": "Daco"}

 
옵션을 추가한 UserDTO는 model_validate 메서드를 통해 해당 객체의 속성을 가져와 자신에게 할당한다. 옵션 하나로 다른 함수나 메서드를 추가할 필요없이 DTO 와 같은 데이터 객체를 생성할 수 있다.
 
이 옵션은 ORM -> DTO 뿐만 아니라 DTO_1 -> DTO_2 처럼 데이터 모델 끼리도 가능하다. 또한 원본 객체가 DTO 보다 더 많은 속성을 가진다면 DTO 자신이 명시한 속성만 가져올 수 있다.
 
예를 들어 UserORM 에 password 속성이 있고 이 속성은 다른 곳에 전달되어선 안된다고 해보자. UserDTO 에는 password 속성을 명시하지 않음으로써 해당 속성을 제외하고 데이터를 가져올 수 있다.

class UserDTO(BaseModel):
    id: int
    name: str

    model_config = ConfigDict(from_attributes=True)


user_orm = UserORM(id=1, name="Daco", password="password")
print(user_orm.model_dump_json()) # {"id":1,"name":"Daco","password":"password"}

user_dto = UserDTO.model_validate(user_orm)
print(user_dto.model_dump_json()) # {"id":1,"name":"Daco"} password 속성이 없다.

 
Pydantic이 제공해주는 이런 기능들을 활용하면 코드를 보다 간결하게 작성하는 것은 물론 유효성 검사까지 한번에 처리할 수 있어 개발이 용이하다.

이 외에도 더 자세한 옵션과 활용법이 있으니 궁금하다면 아래 Pydantic 공식문서를 참고하길 바란다.
 

레퍼런스

https://docs.pydantic.dev/latest/usage/models/#arbitrary-class-instances