[Fluent Python] classmethod를 이용한 팩토리 메서드 (260626)

WonTerry·2026년 6월 26일

Python

목록 보기
17/28

파이썬에서 일반클래스와 dataclass의 차이를 이해하기 위한 코드를 작성해줘. 코드에서 UserRequest class는 사용자의 간단한 정보 (username, password, age 등)을 담고 있고, 초기화 하는 과정까지 담고 있다.

일반 클래스dataclass의 차이를 가장 쉽게 이해하는 방법은 동일한 기능을 구현한 코드를 비교하는 것입니다.

예를 들어, UserRequest라는 클래스가 사용자 정보를 저장한다고 가정해보겠습니다.


1. 일반 클래스 (Class)

class UserRequest:
    def __init__(self, username: str, password: str, age: int):
        self.username = username
        self.password = password
        self.age = age


# 객체 생성
user = UserRequest(
    username="terry",
    password="1234",
    age=30
)

print(user.username)
print(user.password)
print(user.age)

출력

terry
1234
30

객체를 출력하면?

print(user)

출력

<__main__.UserRequest object at 0x104c5c670>

별로 보기 좋지 않습니다.


값을 비교하면?

user1 = UserRequest("terry", "1234", 30)
user2 = UserRequest("terry", "1234", 30)

print(user1 == user2)

출력

False

왜냐하면 일반 클래스는 값(value) 이 아니라 메모리 주소(identity) 를 비교하기 때문입니다.


일반 클래스에서 편의 기능을 직접 구현해야 한다.

class UserRequest:
    def __init__(self, username, password, age):
        self.username = username
        self.password = password
        self.age = age

    def __repr__(self):
        return (
            f"UserRequest(username='{self.username}', "
            f"password='{self.password}', age={self.age})"
        )

    def __eq__(self, other):
        if not isinstance(other, UserRequest):
            return False

        return (
            self.username == other.username
            and self.password == other.password
            and self.age == other.age
        )

2. dataclass 사용

from dataclasses import dataclass


@dataclass
class UserRequest:
    username: str
    password: str
    age: int


# 객체 생성
user = UserRequest(
    username="terry",
    password="1234",
    age=30
)

print(user)

출력

UserRequest(username='terry', password='1234', age=30)

객체 비교

user1 = UserRequest("terry", "1234", 30)
user2 = UserRequest("terry", "1234", 30)

print(user1 == user2)

출력

True

dataclass가 자동으로 __eq__()를 만들어주기 때문입니다.


dataclass가 자동으로 생성해주는 것

위 코드 하나만 작성해도 내부적으로는 다음과 비슷한 코드가 생성됩니다.

class UserRequest:
    def __init__(self, username, password, age):
        self.username = username
        self.password = password
        self.age = age

    def __repr__(self):
        return (
            f"UserRequest(username='{self.username}', "
            f"password='{self.password}', age={self.age})"
        )

    def __eq__(self, other):
        ...

즉,

@dataclass
class UserRequest:
    username: str
    password: str
    age: int

은 사실상 수십 줄의 보일러플레이트 코드를 대신 작성해주는 것입니다.


실제 웹 API 개발에서 사용하는 예제

예를 들어 로그인 요청을 받는다고 해보겠습니다.

일반 클래스

class UserRequest:
    def __init__(self, username, password, age):
        self.username = username
        self.password = password
        self.age = age


request = UserRequest(
    username="terry",
    password="1234",
    age=30
)

dataclass

from dataclasses import dataclass


@dataclass
class UserRequest:
    username: str
    password: str
    age: int


request = UserRequest(
    username="terry",
    password="1234",
    age=30
)

코드가 훨씬 간결해집니다.


하지만 dataclass의 중요한 특징

request = UserRequest(
    username="terry",
    password="1234",
    age="30"
)

에러가 발생할까요?

print(type(request.age))

출력

<class 'str'>

에러가 발생하지 않습니다.

왜냐하면 dataclass타입 힌트만 제공할 뿐 런타임 검사를 하지 않기 때문입니다.


타입 검사를 하고 싶다면?

실무에서는 보통 다음을 사용합니다.

  • 일반 Class
  • dataclass
  • typing
  • beartype
  • typeguard
  • pydantic.BaseModel

특히 API 서버에서는 많이 사용합니다.

from pydantic import BaseModel


class UserRequest(BaseModel):
    username: str
    password: str
    age: int
request = UserRequest(
    username="terry",
    password="1234",
    age="30"
)

print(request)

출력

username='terry' password='1234' age=30

자동으로 형 변환까지 수행합니다.


한눈에 비교

항목일반 클래스dataclass
__init__ 작성직접자동
__repr__ 작성직접자동
__eq__ 작성직접자동
코드 길이짧음
값 비교기본적으로 불가능가능
타입 검사없음없음
데이터 저장 객체(DTO) 용도가능매우 적합

실무 규칙

  • 행동(메서드)이 많은 객체 → 일반 클래스
  • 데이터를 담는 객체(DTO, Request, Response)dataclass
  • API 입력 검증까지 필요pydantic.BaseModel

따라서 UserRequest처럼 단순히 사용자 요청 데이터를 담는 객체라면, 실무에서는 dataclass 또는 Pydantic의 BaseModel을 가장 많이 사용합니다.


pydantic.BaseModel, pydantic.field_validation을 이용하여 classmethod를 정의하여 초기화할때 검증하는 과정까지 코드를 확장해줘.

dataclass는 데이터를 저장하기에는 편리하지만, 실제 웹 서비스에서는 입력값 검증(Validation) 이 매우 중요합니다. 그래서 실무에서는 DTO(Request 객체)를 만들 때 Pydantic의 BaseModelfield_validator를 많이 사용합니다.

예를 들어 다음과 같은 요구사항이 있다고 가정해봅시다.

  • username

    • 3글자 이상
    • 영문자, 숫자, _만 허용
  • password

    • 8글자 이상
    • 숫자 1개 이상 포함
    • 특수문자 1개 이상 포함
  • age

    • 0~150 사이

1. field_validator를 이용한 검증

from pydantic import BaseModel, field_validator
import re


class UserRequest(BaseModel):
    username: str
    password: str
    age: int

    @field_validator("username")
    @classmethod
    def validate_username(cls, value: str) -> str:
        if len(value) < 3:
            raise ValueError(
                "username은 3글자 이상이어야 합니다."
            )

        if not re.fullmatch(r"[a-zA-Z0-9_]+", value):
            raise ValueError(
                "username은 영문자, 숫자, '_'만 가능합니다."
            )

        return value

    @field_validator("password")
    @classmethod
    def validate_password(cls, value: str) -> str:
        if len(value) < 8:
            raise ValueError(
                "password는 8글자 이상이어야 합니다."
            )

        if not re.search(r"\d", value):
            raise ValueError(
                "password는 숫자를 포함해야 합니다."
            )

        if not re.search(r"[!@#$%^&*()_+=\-]", value):
            raise ValueError(
                "password는 특수문자를 포함해야 합니다."
            )

        return value

    @field_validator("age")
    @classmethod
    def validate_age(cls, value: int) -> int:
        if not (0 <= value <= 150):
            raise ValueError(
                "age는 0~150 사이여야 합니다."
            )

        return value

정상적인 초기화

user = UserRequest(
    username="terry_lee",
    password="abcd1234!",
    age=30
)

print(user)

출력

username='terry_lee' password='abcd1234!' age=30

검증 실패 예제

user = UserRequest(
    username="ab",
    password="1234",
    age=300
)

출력

ValidationError: 3 validation errors for UserRequest
username
  username은 3글자 이상이어야 합니다.

password
  password는 8글자 이상이어야 합니다.

age
  age는 0~150 사이여야 합니다.

2. classmethod를 이용한 팩토리 메서드 추가

실무에서는 외부 JSON이나 DB 데이터를 받아서 객체를 생성하는 경우가 많습니다.

class UserRequest(BaseModel):
    username: str
    password: str
    age: int

    @field_validator("username")
    @classmethod
    def validate_username(cls, value):
        if len(value) < 3:
            raise ValueError(
                "username은 3글자 이상이어야 합니다."
            )
        return value

    @field_validator("age")
    @classmethod
    def validate_age(cls, value):
        if value < 0:
            raise ValueError(
                "age는 음수가 될 수 없습니다."
            )
        return value

    @classmethod
    def from_dict(cls, data: dict):
        """
        dict 데이터를 받아 객체 생성
        """
        return cls(**data)

사용 예:

request_data = {
    "username": "terry",
    "password": "abcd1234!",
    "age": 30
}

user = UserRequest.from_dict(request_data)

print(user)

3. classmethod에서 추가 전처리하기

class UserRequest(BaseModel):
    username: str
    password: str
    age: int

    @classmethod
    def create(
        cls,
        username: str,
        password: str,
        age: int
    ):
        username = username.strip().lower()

        return cls(
            username=username,
            password=password,
            age=age
        )

사용:

user = UserRequest.create(
    username="   Terry_Lee   ",
    password="abcd1234!",
    age=30
)

print(user)

출력

username='terry_lee'
password='abcd1234!'
age=30

실무에서 가장 많이 사용하는 패턴

from pydantic import BaseModel, field_validator
import re


class UserRequest(BaseModel):
    username: str
    password: str
    age: int

    @field_validator("username")
    @classmethod
    def validate_username(cls, v):
        if len(v) < 3:
            raise ValueError(
                "username은 3글자 이상이어야 합니다."
            )
        return v

    @field_validator("password")
    @classmethod
    def validate_password(cls, v):
        if not re.search(r"\d", v):
            raise ValueError(
                "숫자가 포함되어야 합니다."
            )
        return v

    @field_validator("age")
    @classmethod
    def validate_age(cls, v):
        if not (0 <= v <= 150):
            raise ValueError(
                "잘못된 나이입니다."
            )
        return v

    @classmethod
    def create(
        cls,
        username: str,
        password: str,
        age: int
    ):
        return cls(
            username=username.strip().lower(),
            password=password,
            age=age
        )

사용:

user = UserRequest.create(
    username=" Terry ",
    password="abcd1234!",
    age=30
)

print(user)

객체 생성 흐름

create()
      ↓
입력값 전처리(strip, lower 등)
      ↓
BaseModel 생성
      ↓
field_validator 실행
      ↓
검증 통과
      ↓
UserRequest 객체 생성

이 패턴은 특히 FastAPI에서 Request DTO를 설계할 때 매우 자주 사용됩니다.

profile
Hello, I'm Terry! 👋 Enjoy every moment of your life! 🌱 My current interests are Signal processing, Machine learning, Python, Database, LLM & RAG, MCP & ADK, Multi-Agents, Physical AI, ROS2...

0개의 댓글