파이썬에서 일반클래스와 dataclass의 차이를 이해하기 위한 코드를 작성해줘. 코드에서 UserRequest class는 사용자의 간단한 정보 (username, password, age 등)을 담고 있고, 초기화 하는 과정까지 담고 있다.
일반 클래스와 dataclass의 차이를 가장 쉽게 이해하는 방법은 동일한 기능을 구현한 코드를 비교하는 것입니다.
예를 들어, UserRequest라는 클래스가 사용자 정보를 저장한다고 가정해보겠습니다.
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
)
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__()를 만들어주기 때문입니다.
위 코드 하나만 작성해도 내부적으로는 다음과 비슷한 코드가 생성됩니다.
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
은 사실상 수십 줄의 보일러플레이트 코드를 대신 작성해주는 것입니다.
예를 들어 로그인 요청을 받는다고 해보겠습니다.
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
)
from dataclasses import dataclass
@dataclass
class UserRequest:
username: str
password: str
age: int
request = UserRequest(
username="terry",
password="1234",
age=30
)
코드가 훨씬 간결해집니다.
request = UserRequest(
username="terry",
password="1234",
age="30"
)
에러가 발생할까요?
print(type(request.age))
출력
<class 'str'>
에러가 발생하지 않습니다.
왜냐하면 dataclass는 타입 힌트만 제공할 뿐 런타임 검사를 하지 않기 때문입니다.
실무에서는 보통 다음을 사용합니다.
dataclasstypingbeartypetypeguardpydantic.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) 용도 | 가능 | 매우 적합 |
dataclasspydantic.BaseModel따라서 UserRequest처럼 단순히 사용자 요청 데이터를 담는 객체라면, 실무에서는 dataclass 또는 Pydantic의 BaseModel을 가장 많이 사용합니다.
pydantic.BaseModel, pydantic.field_validation을 이용하여 classmethod를 정의하여 초기화할때 검증하는 과정까지 코드를 확장해줘.
dataclass는 데이터를 저장하기에는 편리하지만, 실제 웹 서비스에서는 입력값 검증(Validation) 이 매우 중요합니다. 그래서 실무에서는 DTO(Request 객체)를 만들 때 Pydantic의 BaseModel과 field_validator를 많이 사용합니다.
예를 들어 다음과 같은 요구사항이 있다고 가정해봅시다.
username
_만 허용password
age
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 사이여야 합니다.
실무에서는 외부 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)
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를 설계할 때 매우 자주 사용됩니다.