Contents
postgresql에서는 소수를 표현하기 위해서, 여러 타입을 제공해준다.
- NUMERIC(), DECIMAL()
- REAL 부동소수
- DOUBLE PRECISION 부동소수
- MONEY 고정 소수
보통의 경우에서는 사람들은 NUMERIC()을 선택하지 않을 까 싶다. 데이터를 저장하는데 있어서 무결성을 지키고 싶어서 정확한 소수를 저장하고 싶을 것이다. 물론 비즈니스 요구사항에 따라 다르다.
아무튼 위와 같이 NUMERIC()을 DB에서 사용한다고 가정하겠다.
문제는 Raw SQL 모듈과 Pydantic 2.0을 이용한 직렬화에서 발생한다.
구성 환경은 다음과 같다.
- postgresql 15 (DB)
- pydantic 2.0 (JSON 직렬화)
- psycopg 3.0 (Raw SQL 모듈)
우선 DB에서 NUMERIC type으로 저장된 값은 python에서 decimal.Decimal type으로 표시된다.
여기에서 Pydantic 2.0의 예상치 못한 동작을 보여주는데, JSON 직렬화를 진행 할때, decimal.Decimal type을 문자열로 변환한다.
즉, 숫자 type(int, float 등)으로 직렬화 될 것이라고 예상하는 것과는 다르게, str로 직렬화 된다.
왜 str로 될 것이라고 예상하지 못했냐 라고 한다면, pydantic 1.0은 decimal.Decimal type을 자동으로 float로 변경해준다.
아래 링크에서 pydantic 2.0이 decimal.Decimal을 str으로 변환 한다는 내용이 적혀 있다.
https://docs.pydantic.dev/2.0/usage/types/number_types/
Number Types - Pydantic
Number Types Pydantic supports the following numeric types from the Python standard library: int float enum.IntEnum decimal.Decimal Validation of numeric types int Pydantic uses int(v) to coerce types to an int; see Data conversion for details on loss of i
docs.pydantic.dev
왜 str로 변환 했을까? 라고 한다면, JSON 직렬화에서 decimal.Decimal 즉, 정밀한 숫자에 대한 표시 방법이 없기 때문에, pydantic 2.0부터는 str으로 제공한다고 짐작해본다.
해결
나와 같은 케이스라면 float로 자동 변경해주도록 바꿀 수 있을 것이다.
케이스) 기획이 된 바가 없으며, 혹시나 다른 이해 관계자나 서비스가 정밀한 값을 요구 할 수 있는 경우를 대비하여, NUMERIC으로 저장했으나, 사용자에게 데이터를 제공하는 경우에는 부동 소수여도 상관 없는 경우
위와 같은 케이스라면, decimal.Decimal type을 자동으로 직렬화 하는 것이 많은 도움이 될 것이다.
Raw SQL 모듈의 결과를 validation하기 위해서, 단일 값인 경우, list인 경우, dict인 경우에 대해서 data 필드 안에 들어온 decimal.Decimal 값을 float로 변경해준다.
_convert()함수를 재귀적으로 호출해서, pydantic이 type check이나 parsing을 하기 전에 validator를 호출해서, 모든 decimal.Decimal type의 값을 float로 변경 시킨다.
class ResultResponse(BaseModel):
data = None
@field_validator("data", mode="before")
def convert_decimals_to_float(cls, value):
return cls._convert(value)
@classmethod
def _convert(cls, item: Any) -> Any:
if isinstance(item, Decimal):
return float(item)
elif isinstance(item, list):
return [cls._convert(sub_item) for sub_item in item]
elif isinstance(item, dict):
return {key: cls._convert(sub_value) for key, sub_value in item.items()}
return item
이제 아래와 같이 사용해주면 된다.
data = get_users() # Raw data from postgresql
return ResultResponse(data=data)
Pydantic을 통해서 자동으로 직렬화를 하는 FastAPI와 같이 사용하기 좋은 것 같다.
그런데 웬만하면, Model class를 사용하여, 내부적으로 typing을 해주는 ORM을 사용하는게 확실히 편하긴 하다.
그래도 복잡한 분석 쿼리나, 속도 요구사항에 따라서 Raw query를 사용하게 된다면, 여전히 유용하게 사용 가능하다.
'Python' 카테고리의 다른 글
[Python] Refresh Token Rotation 간헐적 비정상 동작 문제 (0) | 2025.01.28 |
---|---|
[FastAPI] 왜 FastAPI는 동기 router도 비동기 "처럼" 보일까? (0) | 2024.12.22 |
[Python] Wrapper에 대해서(@wraps) (0) | 2024.12.21 |
[python fastapi] Depends에 동적으로 parameter 사용하기 (1) | 2024.09.01 |
[python] shallow copy, deep copy, pass by assignment (0) | 2024.08.31 |