본문 바로가기
독서에서 한걸음

도메인 주도 개발 Value Type

by oncerun 2023. 1. 1.
반응형

 

절반가량을 읽었다. 

 

정말로 책을 읽으면서 많은 서치를 해서 실제 프로젝트에 적용시킨 개념들도 많았고 도메인 주도 개발이라는 개념이 조금씩은 눈에 들어와 기존에 있던 패키지 구조도 변경을 하니 각각 맡고 있는 도메인들이 한눈에 더 잘 보이기 시작했다.

 

과거의 코드를 보면서 이 코드는 현재 잘못됫구나 루트 도메인의 하위 도메인에 접근하여 사용하고 있던 것들도 있었고, 

 

여러 가지 위임을 숨기면서 클라이언트가 필요한 메서드를 만들어서 하위 도메인 개념을 숨기기도 하였다. 

 

추가로 충격을 받았던 문구도 있었다.

 

"테이블이 도메인이라고 과거에는 착각을 했었다. 하지만 도메인에 대한 깊은 지식이 쌓일수록 이러한 지식이 잘못된 것을 알았다."

 

나도 착각을 했었고, 이를 바로잡기위해 더 노력하고 있는 중이다. 아마 해당 책을 읽고 조금 더 구체적인 서적을 찾아볼 것이다. 

 

현재는 리포지터리와 모델 구현이라는 챕터를 읽고 있다. 여기서 내가 얻을 수 있는 부분이 무엇이 있을까?

 

 

밸류를 이용한 ID 매핑

 

도메인 객체는 식별자라는 개념이 존재한다. 이는 도메인 객체를 구별할 수 있는 특정 ID 값이다.

 

보통 애플리케이션에서는 이를 구현하기 위해 primitive 타입이나, Wrap Class를 사용한다. 

 

리팩토링 책에서도 말한 적이 있는데,

"기본 타입으로 표현할 수 있는 것은 한계가 있다. 식별자를 강조하거나, 별도의 로직이 필요하다면 이를 클래스로 표현하라"

 

관련 문구가 있었다. 만약 식별자에 대한 버전관리나 추가적인 기능이 들어가야 한다면 프로그래밍 언어레벨에서 기본 타입으로 할 수 있는 것은 그리 많지 않고 해당 값을 설정하기 위해 setter를 노출시키는 것도 적절치 않다. 

 

이를 밸류로 표현해보자. 

 

@EmbeddedId
private OrderNo number;
@Embeddable
public class OrderNo implements Serializable {
    @Column(name = "order_number")
    private String number;

    protected OrderNo() {
    }

    public OrderNo(String number) {
        this.number = number;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderNo orderNo = (OrderNo) o;
        return Objects.equals(number, orderNo.number);
    }

    @Override
    public int hashCode() {
        return Objects.hash(number);
    }

    public static OrderNo of(String number) {
        return new OrderNo(number);
    }
}

 

 

JPA에서는 식별자 타입은 Serializable 타입이어야 하므로 식별자로 사용할 밸류 타입은 해당 인터페이스를 상속받는다. 

 

밸류 타입으로 식별자를 구현할 때 얻을 수 있는 장점은 식별자에 기능을 추가할 수 있다는 점.

2세대의 식별자를 확인하는 로직을 밸류 클래스에서 제공하도록 할 수 있다. 

 

public boolean is2ndGeneration() {
    return number.startsWith("2N");
}

 

주목해야 할 점은 밸류 클래스의 생성 팩토리 클래스이다. 값에 대해 불변 객체를 생성해서 리턴하는 것을 기억하자. 

이로 인해 밸류 객체가 공유되는 것을 막을 수 있다.

 

기본 생성자는 proteced 레벨로 생성한 이유는 JPA의 구현기술에 대해 필요한 것이다. 

 

추가적으로 해당 관련해서 정리한 글이 있는데 

 

2022.11.20 - [데이터 접근 기술/JPA] - 값 타입

 

JPA에서 값 타입을 컬렉션으로 사용함에 따라 발생할 수 있는 문제점을 다루고 있다. 

 

간단히 정리하면 값 타입은 식별자가 없어 값 타입의 변경이 발생하면 모든 값 타입을 제거한 후 insert문 발생에 따라 성능 이슈가 발생할 수 있음을 말한다.  따라서 실무에서는 이러한 경우 엔티티로 승격시켜 사용한다고 한다.

 

애그리거트에서 루트 엔티티를  뺀 나머지 구성요소는 대부분 밸류이다?

 

만약 또 다른 엔티티가 있다면 진짜 엔티티인지 의심해보라고 한다. 

단지 별도 테이블에 데이터를 저장한다고 해서 엔티티인 것은 아니기 때문이다. 

 

또한 밸류와 엔티티를 구분하는 방법은 다음과 같다.

 

1. 식별자가 필요하다.

하지만 식별자를 찾을 때 매핑되는 테이블 식별자를 애그리거트 구성요소의 식별자와 동일한 것으로 착각하면 안 된다.
이는 애그리거트 구성요소가 항상 고유 식별자를 갖는 것은 아니기 때문이다.

이를 다음과 같은 말로 이해하면 조금 도움이 되는 것 같다.

 

단지 테이블을 외래키로 연결하기 위해 존재하는 테이블의 식별자 때문에 엔티티로 인식하지 말 것.

특정 프로퍼티를 별도의 테이블로 보관한 것이라면 이는 밸류이다.

 

* 역의 경우를 상상할 수가 없다. 해당 경우에 대해 찾아볼 것이다.

 

2. 값을 지속적으로 추적해야 하는 경우

 

만약 엔티티가 맞다면 해당 엔티티가 다른 애그리거트는 아닌지 확인해야 한다.

 

* 애그리거트는 상위 수준에서 도메인 모델을 조망할 수 있는 방법으로 도메인의 대한 모델들을 애그리거트 단위로 표현할 수 있다.

 

이에 대한 확실한 예시가 하나 있는데, 상품과 리뷰라는 도메인이다.

 

이 도메인 엔티티들은 생성되는 주체도 다르고 라이프 사이클도 다르다. 또한 변경 주체도 다르다. 상품과 리뷰는 특성상 하나의 화면에 구성될 수 있지만 위와 같은 이유로 별도의 애그리거트로 구성되어야 한다.

 

 

 

밸류 컬렉션을 @Entity로 매핑하기

 

개념적으로 밸류이지만 구현 기술의 한계나 팀 표준 때문에 @Entity를 사용할 때가 있다. 

JPA는 @Embedded 타입의 클래스 상속 매핑을 지원하지 않아서 @Entity로 이용해야 할 수도 있다.

 

해당 섹션에서 팁을 얻은 부분은 다음과 같다.

 

@Entity에 대한 @OneToMany 매핑에서 clear() 메서드를 통해 컬렉션을 초기화한다면 하이버네이트를 사용하는 경우 삭제 과정이 효율적이지는 않다는 것.

 

이 경우 select로 해당 엔티티를 로딩하고, 각 개별 엔티티에 대해 delete 쿼리를 실행한다. 

따라서 1+ N의 현상이 발생한다. 변경 빈도가 낮으면 성능에 이슈는 없지만 빈도가 높으면 서비스 성능에 문제가 될 수 있다. 

 

다만 하이버네이트는 @Embeddable 타입에 대한 컬렉션의 clear() 메서드를 호출하면 컬렉션에 속한 객체를 로딩하지 않고 한 번의 delete 쿼리로 삭제를 수행한다고 한다. 따라서 이 문제를 해소하려면 상속을 포기하고 @Embeddable로 매핑된 단일 클래스로 구성해야 한다.

 

이는 다형성을 포기하고 if-else 구문으로 구현을 하겠다는 것이다. 

 

 

 

 

반응형

'독서에서 한걸음' 카테고리의 다른 글

debounce  (0) 2023.01.21
도메인 모델과 경계  (0) 2023.01.03
Comments  (0) 2022.12.28
Refused Bequest  (0) 2022.12.28
Alternative Classes with Different Interfaces  (0) 2022.12.27

댓글