값 객체(VO)
자동차 경주 미션 중 다른 크루들이 값 객체를 사용하여 구현하는 것을 보았다.
값 객체를 알고 있었지만 사용하지 못한 부분에 대해 반성을 하면서 작성하는 반성문
도메인 주도 설계 철저 입문 책을 보고 정리하는 글입니다.
값 객체란?
시스템 특유의 값을 표현하기 위해 정의하는 객체
값의 성질
1. 변하지 않는다.
2. 주고받을 수 있다.
3. 등가성을 비교할 수 있다.
값 객체는 값의 성질을 가지고 있다.
1. 값은 변하지 않는다.
값은 변화하지 않는다. 우리가 값을 수정할 때는 새로운 값을 대입한다.
예를 들면,
String greet = "하이"
greet = "안녕"
greet의 값이 처음에는 "하이" 였지만 "안녕"으로 수정되었다. 그렇지만 이것을 값이 변화했다고 보지 않는다.
값은 "하이", "안녕"을 나타낸다. 즉 "하이"라는 값이 "안녕"이라는 값으로 변화한 것이 아니라
greet 변수 내의 값이 수정된 것이다. "하이", "안녕"이라는 값은 처음부터 끝까지 변하지 않는다.
여기서 주의해야 할 점은 값 자체를 수정할 수는 없다.
예시는 아래와 같다.
# 잘못된 코드
"하이" = "안녕"
System.out.println("하이") // 결과는 "안녕"????
변하지 않는 성질을 가지는 값은 값 자체를 수정할 수 없다.
그렇다면 값이 변하지 않는 성질의 장점은 무엇일까?
의도하지 않은 상태 변화를 막을 수 있다. 모르는 사이에 상태가 변화되어 버그가 발생하는 것을 방지할 수 있다.
값 객체 또한 변하지 않는다.
2. 값을 주고받을 수 있다.
String greet = "하이"
greet = "안녕"
위에서 보았던 코드를 다시 한번 확인해보면,
대입을 통해 값을 주고 받을 수 있다. 이를 통해 변수의 값이 수정된다.
값 객체 또한 마찬가지이다.
값 객체를 갖는 변수를 수정하기 위해서는 값 자체를 주고받아야 한다.
var name = new Name("박성우");
name = new Name("배카라");
3. 등가성을 비교할 수 있다.
다음과 같이 값은 등가성을 비교할 수 있다.
0 == 1
'a'.equals('a')
마찬가지로 값 객체 또한 등가성을 비교할 수 있다.
var name = new Name("박성우")
name.equals(new Name("박성우"))
위는 값 객체의 등가성을 비교하는 예시이다.
값 객체가 같은 값을 가지므로 위의 예시는 true를 반환해야 한다.
따라서 값 객체 클래스를 구현할 때 equals 메서드를 오버 라이딩해야 한다.
값 객체로 정의해야 하는 기준
1. 규칙이 존재하는가
2. 낱개로 다루어야 하는가
이번 자동차 경주 미션에서는
Car의 String name 필드 변수를 값 객체로 만들었으면 좋았을 것이다.
이번 미션에는 "자동차 이름은 5자 이하여야 한다"라는 조건이 있었다.
다음과 같이 Name 클래스를 작성할 수 있다.
public class Name {
String name;
public Name(String name) {
validateNameLength(name);
this.name = name;
}
private void validateNameLength(String name) {
if (name.length() > 5) {
throw new IllegalArgumentException();
}
}
}
이 값 객체를 사용한다면 다음과 같은 이점을 얻을 수 있다.
1. 무결성의 유지
이름을 생성할 때 이름의 길이가 5 이하여야 한다는 조건을 정확하게 지킬 수 있다.
값이 유효한 지에 대한 여부를 항상 확인할 수 있다.
2. 로직이 코드 이곳저곳에 흩어지는 것을 방지한다.
이번 미션에서는 다음과 같이 Car 클래스 내부에서 자동차 이름에 대한 유효성 검증을 하였다.
public Car(String name, int position) {
checkCarNamesBlank(name);
checkCarNamesLength(name);
this.name = name;
this.position = position;
}
private void checkCarNamesBlank(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException(NULL_EMPTY_CAR_NAME_ERROR_MESSAGE);
}
}
private void checkCarNamesLength(String name) {
if (name.length() > MAX_CAR_NAME_LENGTH) {
throw new IllegalArgumentException(CAR_NAME_LENGTH_ERROR_MESSAGE);
}
}
이 코드처럼 이름에 대한 검증을 Car 클래스 내부에서 했을 때, 이름에 대한 추가적인 규칙이 생긴다면
Car 클래스 내부에 그 규칙을 추가해야 한다.
따라서 Car 클래스가 지저분해지게 된다.
위의 코드보다는 값 객체를 나타내는 Name 클래스를 생성하여서 이름에 대한 로직을 모아둘 수 있게 해야한다.