Pydantic을 활용한 파이썬 타입 정의 및 직렬화 배워보기

roachroach
5 min read

오늘은 파이썬에서 타입을 다루는 방법과 Pydantic 라이브러리를 활용한 데이터 유효성 검사 및 직렬화에 대해 알아보겠습니다.

파이썬 타입 시스템의 발전

파이썬은 3.5 버전부터 typing 모듈을 도입하여 복잡한 타입을 명시적으로 정의할 수 있는 기능을 제공하기 시작했습니다. 이를 통해 코드의 가독성과 유지보수성을 향상시킬 수 있게 되었습니다.

class Fruit():
    name: str
    color: str
    weight: float
    bazam: dict[str, list[tuple[int, bool, float]]]

    def __init__(self, name: str, color: str, weight: float, bazam: dict[str, list[tuple[int, bool, float]]]):
        self.name = name
        self.color = color
        self.weight = weight
        self.bazam = bazam

위 코드와 같이 클래스 멤버 변수에 대한 타입을 선언할 수 있습니다. name은 문자열(str), color도 문자열(str), weight는 실수(float), 그리고 bazam은 키가 문자열이고 값이 (정수, 불리언, 실수) 튜플의 리스트인 딕셔너리 타입으로 정의되었습니다.

하지만 파이썬은 기본적으로 동적 타입 언어(dynamically-typed language)입니다. 이는 typing 모듈을 사용한 타입 힌트(type hint)가 런타임(runtime) 시점에 타입 검사를 강제하지는 않는다는 것을 의미합니다.

따라서 아래와 같이 타입 힌트와 다른 타입의 값을 전달해도 코드는 문제없이 실행됩니다.

Fruit(name=1, color=1, weight=1, bazam="bazam")

실행 결과는 다음과 같습니다.

<__main__.Fruit at 0x7f724dc6bb60>

이러한 유연성은 때로는 개발 과정에서 예기치 않은 오류를 발생시킬 수 있습니다. 동적언어의 장점이자 단점이라고 할 수 있죠. 바로 이러한 부분으로 인해 조금은 견고함이 필요한 부분에 Pydantic 라이브러리를 도입하면 좋습니다.

Pydantic을 이용한 데이터 유효성 검사

Pydantic은 타입 힌트를 사용하여 데이터 유효성 검사 및 설정을 관리하는 라이브러리입니다. Pydantic을 사용하는 가장 일반적인 방법은 BaseModel을 상속하는 클래스를 정의하는 것입니다.

다음은 Pydantic을 사용하여 Fruit 클래스를 정의한 예제입니다.

#!pip install pydantic
from typing import Annotated, Literal
from annotated_types import Gt # greater than x
from pydantic import BaseModel, Field

class Fruit(BaseModel):
    name: str
    color: Literal["red", "green"] # 색상은 "red" 또는 "green"만 허용
    weight: Annotated[float, Gt(0)] # 무게는 0보다 커야 함
    bazam: dict[str, list[tuple[int, bool, float]]]


Fruit(name="apple", color="red", weight=100, bazam={"a": [(1, True, 1.0)]})

위 코드에서 color 필드는 Literal 타입을 사용하여 "red" 또는 "green" 값만 허용하도록 제한했습니다. 또한 weight 필드는 AnnotatedGt(0)를 사용하여 0보다 큰 값만 유효하도록 설정했습니다.

Pydantic을 사용하면 타입 힌트에 맞지 않는 데이터가 입력될 경우, 런타임에 ValidationError가 발생하여 잘못된 데이터를 사전에 방지할 수 있습니다.

예를 들어, 다음과 같이 의도적으로 잘못된 타입의 데이터를 Fruit 모델에 전달해 보겠습니다.

Fruit(name=1, color=1, weight=1, bazam="bazam")

이 코드는 Pydantic 모델의 유효성 검사에 의해 다음과 같은 ValidationError를 발생시킵니다.

ValidationError: 3 validation errors for Fruit
name
  Input should be a valid string [type=string_type, input_value=1, input_type=int]
    For further information visit https://errors.pydantic.dev/2.11/v/string_type
color
  Input should be 'red' or 'green' [type=literal_error, input_value=1, input_type=int]
    For further information visit https://errors.pydantic.dev/2.11/v/literal_error
bazam
  Input should be a valid dictionary [type=dict_type, input_value='bazam', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/dict_type

오류 메시지를 통해 name은 문자열이어야 하지만 정수형(int) 1이 입력되었고, color는 "red" 또는 "green"이어야 하지만 정수형 1이 입력되었으며, bazam은 딕셔너리 타입이어야 하지만 문자열 "bazam"이 입력되었음을 명확히 알 수 있습니다.

만약 color 필드에 허용되지 않은 값(예: "yellow")을 입력하면 어떻게 될까요?

Fruit(name="banana", color="yellow", weight=100, bazam={"a": [(1, True, 1.0)]})

이 경우에도 Pydantic은 다음과 같이 ValidationError를 발생시켜 데이터의 무결성을 보장합니다.

ValidationError: 1 validation error for Fruit
color
  Input should be 'red' or 'green' [type=literal_error, input_value='yellow', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/literal_error

Pydantic이 어떻게 동작하는지에 대한 더 자세한 내용은 Pydantic 공식 문서에서 확인하실 수 있습니다.

직렬화 (Serialization)

직렬화(Serialization)는 객체를 바이트 스트림(byte stream)과 같이 전송하거나 저장하기 쉬운 형태로 변환하는 과정을 의미합니다. API를 개발할 때 프로그램 내부의 객체를 외부로 전달해야 하는 경우가 많은데, 이때 직렬화가 필수적입니다.

Pydantic은 객체 직렬화를 위한 세 가지 편리한 방법을 제공합니다:

  1. 파이썬 객체로 이루어진 dict로 변환

  2. JSON으로 변환 가능한 타입으로만 이루어진 dict로 변환

  3. JSON 문자열로 변환

각각의 방법에 대해 예제를 통해 자세히 살펴보겠습니다. 먼저, 사용자 정보를 나타내는 User 모델을 정의합니다.

from pydantic import BaseModel
from datetime import datetime
from typing import Optional, List

class User(BaseModel):
    id: int
    username: str
    signup_ts: Optional[datetime] = None
    friends: List[int] = []
    is_active: bool = True

user_data = {
    "id": 123,
    "username": "pydantic_lover",
    "signup_ts": datetime(2023, 4, 15, 10, 30, 0),
    "friends": [1, 2, 3]
}
user_instance = User(**user_data)

1. 파이썬 객체로 이루어진 dict로 변환 (model_dump())

model_dump() 메소드는 Pydantic 모델 인스턴스를 파이썬 딕셔너리로 변환합니다. 이 딕셔너리는 원본 객체의 필드와 값을 그대로 유지합니다.

# 방법 1: 파이썬 객체로 이루어진 dict로 변환
python_dict_objects = user_instance.model_dump()

print(f"타입: {type(python_dict_objects)}")
print(f"내용: {python_dict_objects}")

if 'signup_ts' in python_dict_objects and python_dict_objects['signup_ts'] is not None:
    print(f"signup_ts의 타입: {type(python_dict_objects['signup_ts'])}")
else:
    print("signup_ts 필드가 없거나 None입니다.")
print("-" * 30)

실행 결과:

방법 1: model_dump() 결과 (Python 객체 유지)
타입: <class 'dict'>
내용: {'id': 123, 'username': 'pydantic_lover', 'signup_ts': datetime.datetime(2023, 4, 15, 10, 30), 'friends': [1, 2, 3], 'is_active': True}
signup_ts의 타입: <class 'datetime.datetime'>
------------------------------

결과에서 볼 수 있듯이, python_dict_objectsdict 타입이며, signup_ts 필드는 여전히 파이썬의 datetime.datetime 객체로 유지됩니다.

2. JSON으로 변환 가능한 타입으로만 이루어진 dict로 변환 (model_dump(mode='json'))

model_dump(mode='json') 메소드는 모델 인스턴스를 딕셔너리로 변환하되, 모든 필드 값을 JSON으로 직접 변환 가능한 타입(Jsonable)으로 만듭니다. 예를 들어, datetime 객체는 ISO 8601 형식의 문자열로 변환됩니다.

jsonable_dict = user_instance.model_dump(mode='json')

print(f"타입: {type(jsonable_dict)}")
print(f"내용: {jsonable_dict}")

if 'signup_ts' in jsonable_dict and jsonable_dict['signup_ts'] is not None:
    print(f"signup_ts의 타입: {type(jsonable_dict['signup_ts'])}\n")
else:
    print("signup_ts 필드가 없거나 None입니다.\n")
print("-" * 30)

실행 결과:

방법 2: model_dump(mode='json') 결과 (JSON 호환 타입)
타입: <class 'dict'>
내용: {'id': 123, 'username': 'pydantic_lover', 'signup_ts': '2023-04-15T10:30:00', 'friends': [1, 2, 3], 'is_active': True}
signup_ts의 타입: <class 'str'>
------------------------------

jsonable_dictsignup_ts 필드 값이 '2023-04-15T10:30:00' 와 같이 문자열로 변환된 것을 확인할 수 있습니다. 이는 JSON이 파이썬의 datetime 객체를 직접 이해하지 못하기 때문입니다. 이렇게 변환된 딕셔너리는 json.dumps()를 사용하여 쉽게 JSON 문자열로 변환할 수 있습니다.

실제로 json.dumps()를 사용하여 변환해 보겠습니다.

import json

json_string = json.dumps(jsonable_dict)
print(f"JSON 문자열: {json_string}")
print("-" * 30)

실행 결과:

JSON 문자열: {"id": 123, "username": "pydantic_lover", "signup_ts": "2023-04-15T10:30:00", "friends": [1, 2, 3], "is_active": true}
------------------------------

is_active의 값이 파이썬의 True에서 JSON의 true로 올바르게 변환된 것도 주목할 만합니다.

3. JSON 문자열로 변환 (model_dump_json())

model_dump_json() 메소드는 Pydantic 모델 인스턴스를 직접 JSON 문자열로 변환합니다. 이는 model_dump(mode='json') 호출 후 json.dumps()를 실행하는 것과 유사한 결과를 제공하지만, 한 번의 호출로 처리할 수 있어 편리합니다.

json_string = user_instance.model_dump_json()

print(f"타입: {type(json_string)}")
print(f"내용: {json_string}")
print("-" * 30)

위 코드를 실행하면 json_string 변수에는 User 인스턴스가 직렬화된 JSON 문자열이 저장되며, 그 타입은 str이 됩니다. 내용은 이전 단계에서 json.dumps(jsonable_dict)를 실행한 결과와 동일할 것입니다.

결론

오늘은 파이썬의 타입 힌트 기능과 Pydantic 라이브러리를 활용하여 데이터의 유효성을 검사하고, 다양한 방식으로 객체를 직렬화하는 방법에 대해 알아보았습니다. Pydantic을 사용하면 타입 안정성을 높이고, 외부 데이터와의 상호작용을 더욱 견고하게 만들 수 있습니다.

0
Subscribe to my newsletter

Read articles from roach directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

roach
roach

https://www.linkedin.com/feed/update/urn:li:activity:7092144087058825216/