값 객체에 대해서 29cm 기술 블로그에서 본 적 있다.
그때는 유효성 검증 등 한 곳에서 처리할 수 있구나~~~ 만 알고 넘어갔었다.
그리고 한 메서드에 대해 오버로딩을 하면서 long 타입 값을 BrandId, StoreId 등으로 래핑하면서 이게 값 객체인가? 생각했던 적이 있다.
근데 이번에 DDD를 공부하면서 값 객체가 나왔다... 내가 진짜 조금만 알고 있었구나... 싶었다.
한 번 제대로 알아보자 !
값 객체 (Value Object)
프로그래밍 언어에는 원시 데이터 타입이 있다. int, boolean 등등.. 이 원시 데이터 타입만을 사용하여 개발을 할 수 있지만, 시스템 특유의 값을 정의해야 할 때가 있다. 이때 정의하는 객체를 값 객체라고 한다.
이름으로 예를 들었을 때
String fullName = "김 덕배";
String firstName = fullName.split(" ")[0];
System.out.print(firstName); // 김
위와 같은 코드가 문제가 안 될 수도 있다고 생각할 수 있지만, 성을 뒤에 쓰는 나라라면?
String fullName = "덕배 김";
String firstName = fullName.split(" ")[0];
System.out.print(firstName); // 덕배
이렇게 문제가 된다.
그러니 값 객체를 사용해보자.
public class FullName {
private String firstName;
private String lastName;
public FullName(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
이렇게 시스템의 값을 표현하는 값 객체를 간단하게 만들 수 있다.
String firstName 과 String lastName 을 FullName 이라는 클래스로 감싼 것, 하지만 이 정도로는 부족하다.
값 객체를 알기 전에, 값의 성질부터 알아야한다.
값의 성질
개발자는 값을 자주 사용하지만, 값에 대해서 일정한 성질이 있다는 것을 이해하지 못하고 쓰는 경우가 많다.
우리는 값이 무슨 성질이 있는지 의심하면서 코드를 작성하지 않는다. 그래도 문제가 되지 않기 때문이다.
거기다 대부분의 개발이 원시타입을 잘 사용하기 위해 공부를 해왔기에 값을 더욱더 모르기도 하다.
값의 대표적인 성질은 다음 세가지다.
- 불변
- 교환 가능
- 등가성 비교 가능
불변성
값은 변하지 않는다.
이게 무슨 말일까?? 프로그래밍을 할 때 값을 수정하는 일이 잦은데, 변하지 않는다니?
String A = "아으아으아으!!!!";
System.out.println(A); // 아으아으아으!!!!
A = "우아우아우아우아!!!";
System.out.println(B); // 우아우아우아우아!!!
바꿀 수 있잖아??? 근데 값이 변하지 않는다고?
우리가 값을 수정할 때는 대입을 한다.
하지만 대입은 값을 수정하는 것이 아니다. 수정되는 것은 변수의 내용이지, 값 자체가 수정되지는 않는다.
String A = "꿀꿀 약간 배고프당";
A.change("피자 먹을까?"); // 이런 메서드는 존재하지 않지만... 예를 들어~
System.out.println(A); // 피자 먹을까?
A는 생성과 동시에 "꿀꿀 약간 배고프당" 이라는 값이 대입된다.
이후 change 라는 메서드로 "피자 먹을까?" 라는 값을 대입한다.
값이 수정이 된다고 하면 이런 불상사가 일어난다.
"나는 돼지다".change("배민 킬까?");
System.out.print("나는 돼지다"); // 배민 킬까?
어? 진짜 듣고 보도 못한 결과다.
만약에 이런 작동 방식으로 돌아간다면 개발자는 혼란에 빠질 것이다.
나는 내가 돼지라고만 말했는데.. 배민 킬까를 말해준다고..?
"나는 돼지다" 라는 값은 언제나 "나는 돼지다" 여야 한다.
값 객체를 배우려면 이런 값의 성질을 알아야한다.
맨위에 나왔던 FullName 클래스를 이용해보자.
FullName fullName = new FullName("김", "돼지");
fullName.setFirstName("이");
값의 성질을 알면 값 객체로 활용되는 클래스의 내용을 바꾼다는 것이 이상해보인다.
해당 값 객체를 값으로 생각해보자.
firstName 은 값이다. 그래서 변하면 안된다.
위와 같은 setFirstName() 메서드도 정의되서는 안된다.
교환 가능
교환 가능하다는 말은 무엇일까?
int A = 1;
A = 2; // 변수에 대입
String B = "저는 처갓집치킨을 가장 좋아합니다.";
B = "다음은 푸라닭입니다. 무조건 순살"; // 변수에 대입
10 = 1; // 이런 코드는 존재해서는 안 된다. 값을 수정한다니
값 객체는 값으로 본다고 했다. 그래서 객체에 대입하는 교환의 방식으로 표현한다.
FullName fullName = new FullName("푸라", "닭");
fullName = new FullName("처갓", "집");
값 객체는 불변이라서 대입문을 통해 표현하는 것이다.
등가성 비교 가능
// 값
System.out.println(1 == 1); // true;
System.out.println(1 == 2); // false;
System.out.println("하이".equals("하이")); // true;
System.out.println("하이".equals("바이")); // false;
// 위에서 구현한 값 객체
FullName name1 = new FullName("문명5", "재밌더라구요 하하");
FullName name2 = new FullName("문명5", "재밌더라구요 하하");
System.out.println(name1.equals(name2)); // Object의 Equals를 상속 받기에 false가 발생
// 값의 특징이라면 true가 발생해야한다.
값들은 같은 거를 비교하면 true가 뜬다.
위에서 적은 name1과 name2도 언뜻보면 같다. 하지만 같지 않다.
그런데 값이라고 생각하면 같아야한다. 속성이 같기 때문이다.
FullName name1 = new FullName("문명5", "재밌더라구요 하하");
FullName name2 = new FullName("문명5", "재밌더라구요 하하");
System.out.println(name1.getFirstName().equals(name2.getFirstName())); // true
이렇게 하면 구현되는 거 아니냐! 할 수 있지만 값이라고 생각하면 이상하다.
System.out.println(1.value() == 2.value()) // false
이런 비교과정을 본 적이 있는가??
그래서 FullName 클래스 안에 Object의 equals() 메서드를 오버라이드해서 구현해야한다.
public class FullName {
private String firstName;
private String lastName;
public FullName(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
FullName fullName = (FullName) obj;
return firstName.equals(fullName.getFirstName()) && lastName.equals(fullName.getLastName());
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
}
위와 같이 구성할 경우 속성을 추가해도 수정이 크게 발생하지 않는다.
만약에 middleName 이라는 속성을 추가한다해도, 값 내부에서만 equals 부분을 수정하면된다.
이렇게 구현을 해봤을 때, FirstName과 LastName을 또 따로 값 객체로 만들 수 있지 않은가 생각이 들 수 있다.
값 객체로 만들기 위한 기준
public class FullName {
private FirstName firstName;
private LastName lastName;
public FullName(FirstName firstName, LastName lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// 생략
}
위와 같이 나눠놓으면 더 안정성 있게 구현할 수 있지 않을까? 싶다.
어우 머리가 아파온다.. 지금 하고 있는 프로젝트만해도.. 엔티티가 엄청 많은데..
취사선택하면 된다.
규칙이 존재하는가?, 낱개로 다뤄지는가? 등으로 구별하면 된다. 어떤 규칙을 정할 건지는 자신이 정하는 거다.
규칙이 존재하는가?
FullName의 FirstName 등 규칙이 존재하는가?
public class FirstName {
private final String value;
public FirstName(String value) {
if (value == null) {
throw new IllegalArgumentException("Null 값은 들어올 수 없습니다.")
}
Pattern pattern = Pattern.compile("^[a-zA-Z]+$");
if (!pattern.matcher(value).matches()) {
throw new IllegalArgumentException("패턴과 일치하지 않습니다.")
}
this.value = value;
}
}
사실 이렇게 만들어도 FullName 생성자에 위와 같이 적어 놓으면 규칙을 강제 할 수 있다.
값 객체로 해도 무방하다.
하나로 해도 되고, 두개로 해도 된다. 이건 성과 이름에 규칙이 다를 때 두개로 하면 된다.
행동이 있는 값 객체
값 객체에서 중요한 점은 독자적인 행동을 할 수 있다는 것이다.
돈을 예시로 생각해보자
public class Money {
private final BigDecimal amount;
private final String type;
public Money(BigDecimal amout, String type) {
// 생략
}
public Money add(Money arg) {
if (arg == null) {
throw... // 생략
}
if (!type.equals(arg.getType())) {
throw new IllegalArgumentException("화폐 단위가 다릅니다.");
}
return new Money(amount + arg.amout, type)
}
}
만약 화폐 단위가 일치하지 않는다면 예외를 발생시켜 버그를 예방 할 수 있다..
Money KRW = new Money(10000, "KRW");
Money USD = new Money(100, "USD");
Money result = KRW.add(USD); // 예외 발생
객체에 행동을 정의하지 않으면 아 이 객체는 이런 행동을 하지 않는구나 하고 바로 알 수 있다.
값 객체의 장점
- 표현력이 증가한다.
- 원시타입으로 각종 특수한 값들을 표현을 하기에는 부족하다.
- 만약 위와 같이 Money 라는 것을 int 로 표현하기에는 화폐 단위를 표현할 수 없다.
- 무결성이 유지된다.
- 생성자 메서드에 위와 같이 Validation 같은 것을 넣어두면 시스템에 문제가 생길만한 상황을 예방 할 수 있다.
- 잘못된 대입을 방지한다.
- 값 객체 안에 값 객체를 넣어서 잘못된 대입을 방지할 수 있다.
- 로직이 이곳저곳 흩어지는 것을 방지할 수 있다.
- 예외 처리같은 메서드를 다른 메서드들에 흩어지게 하는 것보다, 값 객체 안에서 한번에 관리하면 흩어지지 않는다.
'TIL' 카테고리의 다른 글
TIL 2024-07-31 EC2, Ubuntu 스왑 메모리 (2) | 2024.07.31 |
---|---|
TIL 2024-07-30 Union Find 알고리즘 (1) | 2024.07.30 |
TIL 2024-07-24 도메인 주도 설계란 무엇일까 (1) | 2024.07.24 |
TIL 2024-05-09 N+1, 그리고 Fetch join (0) | 2024.05.09 |
TIL 2024-03-01 간단한 BufferedReader 와 StringTokenizer 사용법 (0) | 2024.03.01 |