파이썬을 공부하다 보면 Entity, DTO, VO라는 용어를 자주 만나게 됩니다. 특히 웹 개발, 데이터베이스 프로그래밍, DDD(Domain Driven Design)를 학습하기 시작하면 거의 반드시 등장하는 개념입니다.
하지만 처음 접하면 이런 생각이 들 수 있습니다.
"어차피 데이터를 담는 객체 아닌가?"
"dataclass 하나만 사용하면 되는 것 아닌가?"
"Entity와 DTO는 뭐가 다른가?"
이번 글에서는 SQLite를 사용하는 간단한 쇼핑몰 프로젝트를 예로 들어 Entity, DTO, VO의 차이를 알아보겠습니다.
가상의 쇼핑몰 주문 시스템을 만든다고 가정해 보겠습니다.
데이터베이스에는 다음 두 개의 테이블이 존재합니다.
CREATE TABLE customers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL,
product_name TEXT NOT NULL,
total_amount INTEGER NOT NULL
);
예시 데이터는 다음과 같습니다.
| id | name | |
|---|---|---|
| 1 | 홍길동 | hong@test.com |
| id | customer_id | product_name | total_amount |
|---|---|---|---|
| 1 | 1 | MacBook Pro | 3500000 |
먼저 중요한 사실부터 알아야 합니다.
dataclass는 Entity, DTO, VO가 아닙니다.
dataclass는 객체를 쉽게 만들기 위한 도구입니다.
예를 들어 일반 클래스는 다음과 같이 작성해야 합니다.
class Customer:
def __init__(self, id, name):
self.id = id
self.name = name
하지만 dataclass를 사용하면
from dataclasses import dataclass
@dataclass
class Customer:
id: int
name: str
만 작성해도 자동으로 생성자와 출력 메서드가 만들어집니다.
즉,
dataclass
=
객체를 편하게 만드는 도구
입니다.
Entity는 데이터베이스 테이블과 연결되는 객체입니다.
우리 프로젝트의 CustomerEntity를 보겠습니다.
from dataclasses import dataclass
@dataclass
class CustomerEntity:
id: int | None
name: str
email: str
데이터베이스의 한 행(row)은 다음과 같습니다.
customers
id | name | email
-------------------------
1 | 홍길동 | hong@test.com
이를 파이썬 객체로 표현하면
customer = CustomerEntity(
id=1,
name="홍길동",
email="hong@test.com"
)
이 됩니다.
즉,
데이터베이스 레코드
↓
Entity
관계가 성립합니다.
Entity는 식별자(ID)를 가집니다.
예를 들어
customer1 = CustomerEntity(
id=1,
name="홍길동",
email="hong@test.com"
)
customer2 = CustomerEntity(
id=1,
name="홍길동",
email="hong@test.com"
)
두 객체는 같은 고객을 의미합니다.
왜냐하면 ID가 동일하기 때문입니다.
Entity는 "누구인가?"가 중요합니다.
Entity
=
식별자(ID)가 중요
VO는 값 자체를 표현하는 객체입니다.
우리 프로젝트에서는 Email과 Money를 VO로 만들었습니다.
from dataclasses import dataclass
@dataclass(frozen=True)
class Email:
value: str
사용 예
email1 = Email("hong@test.com")
email2 = Email("hong@test.com")
비교
print(email1 == email2)
결과
True
VO는 값이 같으면 같은 객체로 취급합니다.
@dataclass(frozen=True)
class Money:
amount: int
사용 예
price = Money(3500000)
VO는 다음과 같은 특징이 있습니다.
Money(1000)
와
Money(1000)
은 같은 값입니다.
다음 코드를 보겠습니다.
price = -1000
음수 금액이 들어갈 수 있습니다.
하지만 Money VO를 사용하면
@dataclass(frozen=True)
class Money:
amount: int
def __post_init__(self):
if self.amount < 0:
raise ValueError(
"금액은 음수가 될 수 없습니다."
)
이런 검증을 넣을 수 있습니다.
따라서
VO
=
값 + 검증 규칙
이라고 생각하면 이해하기 쉽습니다.
DTO는 Data Transfer Object의 약자입니다.
말 그대로 데이터를 전달하는 객체입니다.
주문 생성 요청을 예로 들어보겠습니다.
사용자가 주문을 생성합니다.
{
"customer_id": 1,
"product_name": "MacBook Pro",
"total_amount": 3500000
}
이를 받아주는 객체가 DTO입니다.
@dataclass
class CreateOrderDTO:
customer_id: int
product_name: str
total_amount: int
사용자 입력은 항상 불완전합니다.
예를 들어 웹 API에서는
{
"customer_id": 1,
"product_name": "MacBook Pro"
}
처럼 금액이 빠질 수도 있습니다.
DTO를 사용하면
외부 입력
↓
DTO
↓
서비스 로직
구조를 만들 수 있습니다.
많은 초급자가 가장 헷갈리는 부분입니다.
CustomerEntity
@dataclass
class CustomerEntity:
id: int
name: str
email: str
CustomerResponseDTO
@dataclass
class CustomerResponseDTO:
name: str
email: str
겉보기에는 비슷합니다.
하지만 역할이 다릅니다.
Entity는 DB용입니다.
DTO는 데이터 전달용입니다.
예를 들어 고객 비밀번호가 저장되어 있다면
@dataclass
class CustomerEntity:
id: int
name: str
email: str
password_hash: str
API 응답에서는 비밀번호를 보내면 안 됩니다.
그래서 DTO를 따로 만듭니다.
@dataclass
class CustomerResponseDTO:
name: str
email: str
우리 프로젝트의 흐름은 다음과 같습니다.
사용자 입력
↓
CreateOrderDTO
↓
OrderService
↓
OrderEntity
↓
OrderRepository
↓
SQLite
SQLite
↓
OrderRepository
↓
OrderEntity
↓
OrderResponseDTO
↓
사용자 화면
데이터베이스 레코드 표현
예:
CustomerEntity
OrderEntity
특징
값 자체를 표현
예:
Email
Money
Address
특징
데이터 전달용
예:
CreateOrderDTO
OrderResponseDTO
특징
초급자 입장에서는 다음 한 문장만 기억해도 충분합니다.
Entity는 데이터베이스를 표현한다.
VO는 의미 있는 값을 표현한다.
DTO는 데이터를 전달한다.
dataclass는 이들을 쉽게 만드는 도구이다.
처음에는 모두 비슷해 보이지만 프로젝트 규모가 커질수록 역할을 분리했을 때 코드의 가독성, 유지보수성, 확장성이 크게 향상됩니다.
실무에서 FastAPI, Django, Flask, SQLAlchemy 등을 사용할 때도 결국은 같은 개념을 다양한 형태로 적용하게 됩니다.