Easy Understanding

(3) DDD의 엔터티/값객체/애그리거트/리파지토리 - 코드에서 객체 정의하기 본문

Study

(3) DDD의 엔터티/값객체/애그리거트/리파지토리 - 코드에서 객체 정의하기

appleg1226 2022. 2. 20. 02:34

지금까지 DDD 설명에서는 도메인이라든지 바운디드 컨텍스트라든지 개념적인 것들을 다뤘다면,

이제부턴 실전 코딩이다.

 

데이터 중심의 개발에서

DB에 대응하는 Class들을 만들었던 것처럼,

Domain에 대응하는 Class들을 만드는 과정이 필요하다.

 

이제부터는 각 도메인들을 Class로 표현해보기 위한 개념과 코드를 구경할 차례다.

 


1. 엔터티(Entity)

@Entity
public class Movie {
    @Id
    private Long id;
    private String name;
    private Long playTimestamp;
    private String directorName;
    ...
    public void changeMovieName(){
      ...
    }
    
    public void fireAndHireNewDirector(){
      ...
    }
}

엔터티는 보는 것처럼,

우리가 JPA를 쓸 때 사용하는 @Entity가 달린 클래스와 취급과 사용까지 거의 비슷하다.

하지만 DDD에서 그게 그거냐라고 하면 혼이 날지도 모른다.

 

DDD에서는 엔터티에 대한 정의와 내용을 아래와 같이 늘여놔서 설명해 놓았기 때문에,

일단 여기에 해당하는지부터 확인한 다음에야 엔티티라고 말할 수 있을 것이다.

 

엔터티는 다음의 두 가지 특성을 지녀야 한다.

1. 개별성

다른 데이터들과 구별이 되어야 하는 데이터들의 묶음이어야 엔터티라고 할 수 있다.

어렵게 생각할 것 없이, 데이터베이스에서 Primary Key를 가지며,

그 키를 이용해서 무엇인가를 해야한다면 거의 Entity가 맞을 것이다.

2. 변화가능성

내부적으로 상태가 자주 바뀌는 변화가 존재하는 데이터라면 엔터티다.

다만 변화는 setter를 통해서가 아닌 반드시 메서드를 통해서 바꾸게 된다.(위의 예시의 changeMovieName 처럼)

 

하지만 이걸 이해한다고 기존의 인식이 크게 달라지지는 않을 것이다.

어차피 우리가 맨날 데이터베이스에 저장하던 것이 다 저 성질을 가지기 때문이다.

 

평소에는 설계한 데이터베이스 테이블을 그대로 클래스 내부 필드로 옮겨 적었다면,

이번엔 DDD의 도메인 설계를 보면서 그대로 클래스 내부 필드로 옮겨 적으면 된다.

 


2. 값 객체(Value Object)

@Entity
public class Movie {
    @Id
    private Long id;
    private String name;         => 그냥 값
    private PlayTime playtime;   => 값 객체
    private String directorName; => 그냥 값 
    ...
}

// Value Object
public class Playtime {
    private int hour;
    private int minute;
    private int second;
    public Long unixTimestampPlayTime(){
      ...
    }
    public LocalDateTime movieEndTime(LocalDateTime startTime){
      ...
    }
    ...
    equals()
    hashcode()
}

값 객체는 엔터티와 같은 선상의 개념은 아니다.

오히려 엔터티의 하위 개념이라고 보는 게 더 맞는 것 같다.

 

엔터티의 표현을 풍부하게 해주는 '내부 계산', '설명' 등을 위한 보조 객체라고 보면 될 것 같다.

 

저 위의 예시에서는 엔터티의 필드를 모두 기본형으로 두었으나, 

이번에는 Playtime을 객체로 만들고 내부에 상세 필드를 넣었다.(예시라서 크게 의미있는 데이터는 아니다)

 

이렇게 값 객체를 만들면 필드에 있을 때보다 더 표현력이 강해진다.

그래서 위의 'unixTimeStampPlayTime' 같이 값 객체 내부의 값을 가지고 변환을 해주는 메서드를 넣는다든지,

'movieEndTime' 메서드처럼 인자를 받아서 끝나는 시간을 계산해주는 식의 다이나믹한 처리가 가능하다. 

 

만약에 이 값을 바꿔줄 땐 조심해야 한다.

DDD에서의 값 객체를 취급하는 원칙은 '불변 객체'로 만드는 것이다.

그래서 setter나 내부 필드를 바꾸는 어떤 행위도 용납되지 않는다.

 

다만 가능한 방법은 아래처럼 새로운 객체를 생성하여 선언해주는 방법이다.

public void changeMovieTime(Playtime newPlayTime){
  this.playtime = newPlayTime;
}

movie.changeMovieTime(new Playtime(1, 1, 1));

추가로 값 객체는 내부의 값들이 모여서 하나의 값을 이루는 것이기 때문에, 

equals, hashcode 메서드는 그 모든 특성들을 반영해야 한다.

 


3. 애그리거트(Aggregate)

도메인 설계 내의 엔터티와 값객체는 이제부터 서로 묶이기 시작할 차례다.

특정 관계는 서로 부모-자식 관계로 묶이기도 하며,

이 과정에서 값 객체의 사용도 고려될 것이다.

위처럼 엔터티들과 값 객체들을 요리조리 묶어보면 애그리거트에 대한 맵이 도출이 된다.

이 묶음 자체를 애그리거트라고 부른다.

(물론 이 나누는 과정 자체가 쉽지는 않고 복잡하다)

 

위의 예시에서 사용하던 Movie를 Aggregate 차원으로 확장해보았다.

@Entity
public class Movie {
    @Id
    private Long id;
    private String name;         => 그냥 값
    private PlayTime playtime;   => 값 객체
    @OneToOne
    private Director director;   => 엔터티
    @OneToMany
    private List<Actor> actors;  => 엔터티
    ...
}

 

DDD에서는 웬만하면 이 과정에서 애그리거트는 작게 만들고, 최대한 나누는 것이 좋다고 한다.

애그리거트는 전체가 일괄적으로 수정되기 때문에, 어떤 엔터티가 수정되더라도 하나처럼 동작한다.

(아래의 Repository를 참고하면 알겠지만 인프라에 대해서는 루트 엔터티 말고는 따로 관리할 수가 없다.)

그렇게 되면 다양한 엔터티들을 수정하다가 동시성 문제가 생길 가능성이 커진다.

 

Movie만 봐도, Movie, Actor, Director라는 세 개의 테이블을 포함하는데,

괜히 Actor나 Director를 수정하고 싶어도, Movie부터 전체 락이 걸릴 수도 있다.

이러면 당연히 성능에 문제가 생기게 된다.

 

그래서 이런 경우에는 서로 ID만 갖도록 하고,

조금 더 분리하려고 노력하여 일관성 유지를 유연하게 하는 것이 좋다.

(이래서 처음부터 애그리거트를 잘 나눠야 한다....)


4. 리파지토리(Repository)

개념 자체는 어려운 것이 하나도 없다.

그냥 애그리거트를 조회/추가/수정/삭제 하는 메서드를 가진 인터페이스/클래스이다.

public interface MovieRepository extends BaseRepository<Movie, Long>{
    void save(Movie movie);
    Movie getById(Long id);
    Movie update(Movie movie);
    boolean deleteById(Long id);
}

다만 주의할 것은 '내부 엔터티들에 대한 Repository는 애초에 없다.'

무조건 루트 엔터티를 거쳐서 내부 엔터티들이 수정되어야 하는 구조다.

(이러니 루트에 락이 걸리면 내부 모든 테이블에 락이 걸리는 것이다.)

 

스프링 JPA를 사용하면 다음처럼 구현하면 된다.

뭔가 DDD스러운 코드같아 보이고 크게 문제는 없어 보인다.

public interface MovieRepository extends CrudRepository<Movie, Long>{
}

다만 이러면 도메인 쪽에서 JPA에 대한 의존성을 갖게 되기 때문에, 

도메인이 인프라를 알아버리는 상황이 발생해버린다.

그러므로 웬만하면 다른 방법을 이용해서 그 의존성을 제거해주는 것이 권장된다. 

(https://stackoverflow.com/questions/49934939/how-to-implement-ddd-using-spring-crud-jpa-repository)

 


엔터티든 값 객체든 Getter, Setter 는 웬만하면 없애자!

위의 코드 예시에도 조금씩 써놨지만,

각 클래스들의 내부 필드의 변화, 내부 필드를 이용한 값 반환 같은 건 모두 클래스 안에서 다룬다.

 

이렇게 안하면 또 밖에서 getter, setter 써서 로직을 구현하게 될 것이다.

테스트적인 측면만 봐도 비즈니스 로직들이 들어간 DDD 방식은 참 편리해질 것이다.