728x90

문제

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;
	
	private String email;
}

 

위의 경우에는 name과 email 필드는 그저 평범한 문자열 데이터를 저장하는 역할만 수행하고

데이터의 검증이나 추가적인 처리는 포함하지 않고 있기 때문에 이를 위한 로직이 필요하다.

 

그렇다면 이러한 로직은 어디에 위치하는 것이 좋을지 생각해 보면

당장은 엔티티에 추가하는 것만 생각이 난다.

 

하지만 name과 email에 대한 로직이 다른 영역에 존재하는 것이 어울리는지 생각해 보면 그렇지 않다.

그러면 각 필드를 의미 있는 객체로 만들 수 있다면 이러한 부분을 해결할 수 있지 않을까?

 

 

VO(Value Object)란?

class Address {
    private String city;
    private String street;
    private String zipcode;
}

 

VO는 간단하게 하나 또는 그 이상의 속성들을 묶어 값 자체를 나타내는 객체라고 볼 수 있다.

 

엔티티 같은 경우야 동일성을 보장받지만 VO는 동일성을 보장받지 않기 때문에

equals와 hashCode 메서드를 오버라이딩 하여 값 자체를 비교하는 기준을 재정의 해줘야 한다.

 

이와 같은 이유 때문에 VO는 값이 변하면 동일하지도 동등하지도 못한 객체가 돼버리기 때문에

값이 변해서는 안 되는 수정자가 없는 불변 객체여야만 한다.

 

따라서 VO는 생성자를 통해 처음 생성된 이후에는 변해서는 안된다.

 

 

왜 원시 타입이 아닌 VO를 사용해야 할까?

VO를 사용하지 않고 원시 타입을 사용하는 경우에는 엔티티 자체에

해당 값들에 대한 로직들이 포함되게 되어 지나치게 복잡해지고 가독성도 떨어진다.

 

하지만 VO를 사용한다면 해당 값들에 대한 로직이 모두 엔티티에서 분리되어

있어야 할 곳으로 응집되게 되고 좀 더 객체 지향적인 프로그래밍이 가능하다.

 

 

VO를 엔티티에 매핑하는 방법

JPA는 임베디드 타입을 제공하여 VO와 같은 복합적인 값을 매핑할 수 있고

이를 이용해 기존의 name 필드를 VO로 바꿔보겠다.

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
@Getter
public class Name {

	public static final int MIN_LENGTH = 2;
	public static final int MAX_LENGTH = 10;

	@Column(name = "name", nullable = false, length = MAX_LENGTH)
	private String value;

	public Name(final String value) {
		validateNull(value);
		final String trimmedValue = value.trim();
		validateBlank(trimmedValue);
		validateLength(trimmedValue);
		this.value = trimmedValue;
	}

	private void validateNull(final String value) {
		if (value == null) {
			throw new NullPointerException("이름은 Null일 수 없습니다.");
		}
	}

	private void validateLength(final String value) {
		if (value.length() < MIN_LENGTH) {
			throw new RuntimeException("2글자 이상이여야 합니다.");
		}

		if (value.length() > MAX_LENGTH) {
			throw new RuntimeException("10글자 이하여야 합니다.");
		}
	}

	private void validateBlank(final String value) {
		if (value.isBlank()) {
			throw new RuntimeException("이름은 공백일 수 없습니다.");
		}
	}
}

 

@Embeddable 어노테이션을 클래스 레벨에 추가하면

해당 클래스를 임베디드 타입으로 사용할 수 있게 만들 수 있다.

 

생성자로 생성된 이후에는 불변 객체로 존재해야 하기 때문에

생성할 때 모든 조건을 검증한 후에 객체를 생성해 준다.

 

또한 setter를 제외한 getter와 검증 등의 로직도

모두 엔티티가 아닌 VO에 작성해 준다.

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Embedded
	private Name name;

	@Embedded
	private Email email;
}

 

엔티티에서는 @Embedded 어노테이션을 필드에 추가해

임베디드 타입을 매핑할 수 있다.

 

이로서 엔티티에는 값과 관련된 로직들이 존재하지 않게 되어

복잡하지 않고 가독성이 좋아졌다.

+ Recent posts