아래 글에서 타입, 클래스, 역할, 책임 등의 용어는 객체지향의 사실과 오해 라는 도서를 참고하자.
객체를 어떻게 분류할 수 있을까?
타입(혹은 클래스)가 역할이 동일한 객체를 모은 집합이라면, DTO, VO의 개념은 여러 타입에 부여된 역할이 얼마나 비슷한지에 따른 분류다.
어떤 객체가 DTO 인가? 라는 질문은 그 객체가 속한 타입이 DTO 라는 분류에 속하는가? 라는 질문을 간단히 표현한 것이다.
어떤 역할을 하는 객체가 DTO 일까?
DTO는 데이터 전달 객체다
DTO 는 Data Transfer Object 의 약자다. 직역하면 데이터 전달 객체이다. 즉, 데이터를 전달하는 역할을 가진 객체를 DTO 라고 부른다.
DTO 는 데이터를 전달하는 역할을 가진 객체를 말한다.
데이터를 전달하는 역할을 수행하기만 하면 그것을 DTO라고 부를 수 있다. 즉, 도메인 로직이 구현된 객체를 데이터의 전달에 사용했다면, 그 객체는 DTO라고 할 수 있다.
레이어의 경계를 넘어갈 때 사용하는 것이 DTO다?
누군가는 MVC 패턴, 레이어드 아키텍처 등의 여러 패턴에서 정의한 영역 간의 데이터를 주고 받을 때 사용하는 것이 DTO 라고 말하기도 한다. 이는 DTO의 정의를 편협한 시각에서 내린 것이다.
아키텍처는 나도 만들 수 있다. 따라서 영역이라는 것도 내가 내맘대로 정의할 수 있다.
DTO가 가지면 좋은 특성
하지만, DTO로 분류된 객체가 데이터를 효과적으로 전달하기 위해 가지면 좋은 특징은 분명히 있다.
내부 상태가 모두 공개되면 좋다.
DTO를 전달받은 객체가, DTO가 전달하고자 하는 데이터를 전달받기 위해서는 DTO 내부의 상태가 공개되어 있는 것이 좋다.
getter가 있거나 필드가 모두 public 이면 좋다.
자바에서 DTO를 만든다면 사실상 필수로 getter를 만들거나, 필드를 모두 public 으로 하는 것이 좋다. 일반적으로는 getter를 사용할 것이다.
불변 객체면 좋다.
DTO 를 통해 전달되는 데이터가 정상적인 데이터인지 보증하는 가장 좋은 방법은 DTO가 불변 객체인 것이다.
DTO를 생성하는 객체 입장에서 자신이 전달하고자 하는 데이터가 올바르게 전달되어야 할 것이다. 데이터가 올바르게 전달된다는 것은 데이터가 변조되지 않는 것을 말한다. DTO가 가변적이라면, 언제 어디서 DTO 가 가진 데이터가 어떻게 수정되는지 알 수 없다. 반면, 불변 객체라면 DTO를 생성할 때에만 올바른 데이터를 이용해 생성하도록 주의하면 된다.
데이터를 전달하는 책임만 가지는 것이 좋다.
데이터를 전달하는 것과 관련 없는 동작이 정의되어있지 않는 것이 좋다.
이유는 SRP 원칙을 참고하자.
좋은 DTO 예시
public class Card {
private final CardName name;
private final CardType cardType;
public Card(CardName name, CardType cardType) {
this.name = name;
this.cardType = cardType;
}
public CardName name() {
return name;
}
public CardType cardType() {
return cardType;
}
}
public enum CardName {
ACE(1),
/* 생략 */
JACK(11),
QUEEN(12),
KING(13);
private final int cardNumber;
CardName(int cardNumber) {
this.cardNumber = cardNumber;
}
public int getCardNumber() {
return cardNumber;
}
}
public enum CardType {
HEART, SPADE, CLOVER, DIAMOND;
}
Java
복사
Card 클래스는 포커 카드의 데이터를 감싸는 것 외에는 아무런 역할을 하지 않고, 불변이며, 외부에 내부 상태가 공개되어 있다.
어떤 역할을 하는 객체가 VO 일까?
VO 는 Value Object의 약자다. 직역하면 값 객체다. 값은 코드 상에서 항상 동일한 의미를 표현하는 것을 말한다.
동일한 의미를 표현한다는 것은 조금 설명이 필요할 것 같다.
int age = 52;
System.out.println(age);
int trumpCardCount = 52;
age = 53;
System.out.println(age);
Java
복사
위 코드에서 52는 동일하게 52 라는 정수를 표현한다. 반면, age 는 두번째 줄과 네번째 줄에서 표현하는 것이 다르다.
VO는 언제나 동일한 의미를 표현하는 역할을 가진 객체를 말한다.
VO가 가져야 하는 특성
불변 객체여야 한다.
항상 동일한 의미를 표현하기 위해선, 불변 객체여야 한다. DTO 와는 다르게 불변성이 필수 요건이다.
class Point {
public double x;
public double y;
Point(double x, double y) {
this.x = x;
this.y = y;
}
}
public class Main {
public static void main(String[] args) {
Point basePoint = new Point(0.0, 0.0);
Point pointA = new Point(1.0, 1.0);
double distance = calculateDistance(basePoint, pointA);
pointA.x = 30;
double distance = calculateDistance(basePoint, pointA);
}
}
Java
복사
위 코드는 직교 좌표계 상 한 점을 표현하는 Point 클래스와, 이를 사용하는 Main 클래스의 코드다. Point 클래스의 필드가 public 으로 정의되어있기 때문에 Point 클래스의 인스턴스는 가변 객체다.
따라서, pointA.x = 30 과 같은 방법으로 내부 값을 변경할 수 있다.
Point 의 인스턴스가 항상 동일한 의미를 표현하는가??
두 객체의 내부 상태가 같다면 동등해야 한다.
Point pointA = new Point(1.0, 1.0);
Point pointB = new Point(1.0, 1.0);
Java
복사
위 두 Point 클래스의 인스턴스가 다른가? 의미상으로 두 인스턴스는 좌표평면의 같은 지점을 표현하므로 동등하다고 생각하는 것이 당연한다. 자바에서 동등 비교는 equals 를 이용한다. 따라서, equals 를 적절히 재정의 해야 한다. equals 를 재정의하면, hashCode 도 재정의 해야 한다.
예시 코드
어떤 객체가 DTO이면서 VO일 수 있다.
어떤 역할을 하는 객체가 DTO 일까? 와 어떤 역할을 하는 객체가 VO 일까? 에서 정리한 대로, DTO와 VO는 상호 배타적인 개념이 아니다. 객체에 여러 역할이 부여되는 것의 적절성을 떠나, 객체에는 여러 역할이 부여될 수 있기 때문이다.
VO 로서의 Length
public class Length {
private int rawLength;
public Length(int rawLength) {
if (rawLength <= 0) {
throw new IllegalArgumentException("길이는 양수만 가능합니다.");
}
this.rawLength = rawLength;
}
// equals, hashCode 생략
public int getRawLength() {
return rawLength;
}
}
Java
복사
위 코드는 길이를 표현하는 VO 클래스인 Length 이다. Length 클래스는 불변이고, equals가 적절히 재정의 되었기 때문에 VO의 조건을 만족한다.
DTO 로서의 Length
public class Square {
private Length edgeLength;
private Point where;
public Square(Length edgeLength) {
this.edgeLength = edgeLength;
this.where = new Point(0.0, 0.0);
}
public int sizeOfArea() {
return edgeLength.getRawLength() * edgeLength.getRawLength();
}
public void moveX(double deltaX) {
where = new Point(where.getX() + deltaX, where.getY());
}
}
Java
복사
좌표 평면 상의 정사각형을 표현하는 Square 에서 정사각형의 넓이를 계산하기 위해선 내부에서 사용하는 Line 이라는 VO의 getter 를 통해 값 자체를 가져와야 한다.
Square에게 한 변의 길이 라는 데이터를 전달하는 것은 Length의 역할 아닌가?
Square 의 생성 시점에, 외부에서 Length 인스턴스를 통해 한 변의 길이 라는 데이터를 전달했다. 그럼 Length 는 DTO다.