최근, 개발 스터디원 중 한 분께서 질문을 하셨다.
여러분께서는 도메인 객체를 순수하게 유지하시는 편인가요? 아니면 특정한 의존성이 들어가더라도 허용하시는 편인가요?
JPA 엔티티를 그대로 활용하는 편이었는데 이번에 분리하려고 시도하고 있어요. 근데 순수하게 분리한다구 마냥 좋은 것만은 아닌 것 같구, 장단이 있는 것 같아요.
그래서 다른 분들께선 JPA 엔티티를 그대로 활용하시는지, 순수한 도메인 객체를 활용하시는 편인지 궁금해서 질문드려요.
필자는 이 질문의 방향을 두 갈래로 생각했다.
- 도메인 객체는 순수하게 유지되어야 하는가?
- 도메인 entity와 DB entity는 같아야 하는가?
첫 번째 질문에 대한 생각은 명확하다. 도메인 객체는 순수하게 유지되어야 한다고 생각하기 때문이다.
외부 라이브러리에 종속을 받는 도메인 객체라면 내부 로직이 아닌, 외부 라이브러리의 변경으로 인한 도메인 객체의 수정이 필요할 수 있고, 테스트하기에도 불편하기 때문이다. 이를 해결하기 위한 방법으로는 전략 패턴과 DIP를 사용했는데, 그 내용은 아래를 참고할 수 있다.
전략 패턴을 사용하여 test에 적용하기
지난 글에 전략 패턴과 DIP를 사용하여 구현하는 법을 알아보았다.오늘은 실제 전략 패턴의 사용 예시와, test를 구현하면 어떤 점이 용이한지 알아보고자 한다. 전략 패턴을 사용하여 구현하기
hyogu.tistory.com
오늘은 두 번째 질문에 대한 나의 의견과 근거를 정리해보고자 한다.
일단 그 답은,,,
“도메인 entity와 DB entity는 구분되어야 한다.” 이다. 언제나 그래야 하는 것은 아니지만, 대개 개발할 때 구분하곤 한다.
그럼 가장 먼저 도메인 entity에 대해서 알아보자
도메인 entity
DDD의 관점에서 도메인 모델은 entity와 value로 구분될 수 있다. 도메인 모델은 비즈니스(문제 해결 영역)의 key에 해당한다.
도메인 entity는 비즈니스 로직에 사용되는 도메인 중 식별자를 포함하고 있어 식별자를 기준으로 하여 타 도메인 entity와 구분되어지는 객체를 의미한다.
아래는 그 예시이다. News와 News에 좋아요를 표시하는 Like로 구분되고 있다.
@Getter
public class News {
private final String newsId;
private final String title;
private final String contents;
private final LocalDate publishedDate;
private final long createdAt;
private final List<Like> likeList;
@Builder
public News(String newsId, String title, String contents, LocalDate publishedDate, long createdAt,
List<Like> likeList) {
this.newsId = newsId;
this.title = title;
this.contents = contents;
this.publishedDate = publishedDate;
this.createdAt = createdAt;
this.likeList = likeList;
}
... 추가 로직
}
@Getter
public class Like {
private final String likeId;
private final String userId;
private final String newsId;
private final long createdAt;
@Builder
public Like(String likeId, String userId, String newsId, long createdAt) {
this.likeId = likeId;
this.userId = userId;
this.newsId = newsId;
this.createdAt = createdAt;
}
... 추가 로직
}
위 예시를 보면 도메인 entity는 불변성에 무게가 실렸으며, POJO로만 구성되어있는 것을 볼 수 있다.
도메인 entity는 타 도메인 entity와의 관계, 라이프사이클 등 비즈니스의 관점에서 로직을 결정한다.
DB entity
DB entity(persistence entity)는 데이터가 데이터베이스와 같은 저장 장소에 어떻게 저장하고 조회할지가 중심이 되는 entity이다.
@Entity
@Table(name = "news")
@Getter @Setter
public class NewsEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private long id;
@Column(name = "news_id")
private String newsId;
private String title;
@Lob
private String contents;
private LocalDate publishedDate;
private long createdAt;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "like_id")
private List<LikeEntity> likeEntityList;
...
}
이 도메인은 JPA(ORM)을 사용하여 데이터베이스와의 매핑을 설계하는 데 목적이 있다.
→ 도메인 entity와 다른 점은 비즈니스 로직에 대한 주된 관심사가 아닌, DB스키마와 연관된 매핑이 주된 관심사이며, 생명주기도 트랜잭션 단위와 연동된다.
그렇다보니 개인적으로 불변성보다도 수정이 용이하게 setter를 허용하는 등의 설계를 한다.
도메인 entity ≠ DB entity
도메인 entity와 DB entity는 필연적으로 다르다고 생각하는 이유는 책임과 관심사의 다름이다.
이 내용은 아래 블로그 저자님께서 정말 자세하게 설명을 해주었다.
도메인 엔티티와 영속성 엔티티
개요소프트웨어 아키텍처에서 도메인 엔티티와 영속성 엔티티를 분리하는 것은 중요한 설계 원칙 중 하나이다. 이 원칙은 클린 아키텍처의 핵심 개념을 기반으로 하며, 도메인 로직과 영속성
dkswnkk.tistory.com
여기서 추가적으로 이유를 기술해 보자면, 도메인 entity는 비즈니스 로직에 집중되어 사용자에 맞는 usecase를 통해 서비스를 제공하는데 초점이 있다.
또, 도메인 entity는 저장되어야 하는 persistence 레이어에 대하여 지식을 갖고 있어야 할 의무가 없다.
더 나아가 도메인 entity와 DB entity는 포함하는 정보가 다를 수 있다.
도메인 entity에서 필요로 하지만, db entity에서 영속되지 않아도 되는 정보를 갖고 있을 수 있기도 하고, 서로 저장되는 곳이 다를 수 있기 때문이다.
→ 이를 해결하기 위해서 도메인 entity와 db entity를 분리한다면 각자의 책임을 갖고 db entity는 저장을, domain entity는 비즈니스 로직을 전담할 수 있어 유리하다.
구분함으로써 생기는 단점
다만, 구분함으로써 생기는 단점도 필연적으로 존재한다.
가장 먼저 domain entity ↔ DB entity로 변환하기 위한 중첩된 코드가 존재하게 된다.
존재하는 애그리거트 마다, db entity와 매핑해줘야 하여 구현해야하는 코드의 양이 두배로 늘어나게 된다.
@Entity
@Table(name = "boards")
@Getter
@Setter
public class BoardEntity {
@Id
@Column(name = "board_id")
private String id;
private String userId;
private String labId;
private String contents;
private boolean deleted;
private long createdAt;
private long deletedAt;
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
@BatchSize(size = 100)
private List<LikeEntity> likeEntityList;
public static BoardEntity from(Board board){
BoardEntity boardEntity = new BoardEntity();
boardEntity.id = board.getId();
boardEntity.userId = board.getUserId();
boardEntity.labId = board.getLabId();
boardEntity.contents = board.getContents();
boardEntity.deleted = board.isDeleted();
boardEntity.createdAt = board.getCreatedAt();
boardEntity.deletedAt = board.getDeletedAt();
boardEntity.likeEntityList = board.getLikeList().stream().map(
like -> LikeEntity.ofBoardEntity(like, boardEntity)).collect(Collectors.toList());
return boardEntity;
}
public Board toModel(){
return Board.builder()
.id(id)
.userId(userId)
.labId(labId)
.contents(contents)
.deleted(deleted)
.createdAt(createdAt)
.deletedAt(deletedAt)
.likeList(likeEntityList.stream()
.map(LikeEntity::toModel)
.collect(Collectors.toList())
)
.build();
}
}
다만 이런 컨버팅 작업을 하다가, 휴먼 에러를 직면한 적이 많다.
특히 빌더를 패턴을 사용할 경우 어떤 필드에 대한 값이 누락되거나, 새롭게 추가한 필드에 대한 컨버팅 작업을 진행하지 않거나 하는 오류가 있어 test코드의 필요성을 느꼈다.
다만 이렇게 되면 test코드의 양도 두배로 늘어나게 되는 단점이 있다.
결론
domain entity와 DB entity를 구분하여 구현하는 이유는 domain entity를 순수하게 유지하고 이에 맞는 비즈니스 로직을 잘~ 구현하게 하기 위함이다. 이 둘을 분리함으로써 persistence 계층에 도메인 entity가 의존하지 않고, domain entity의 행동과 로직에만 집중할 수 있도록 설계할 수 있다.
이렇게 분리해두면 DB나 ORM이 변경되더라도 Domain Entity 자체는 수정할 필요가 없고, DB Entity와 Repository 구현체만 손보면 되므로 유지보수와 확장성이 크게 향상된다. 실제로 커넥티스트 개발 과정에서 MongoDB를 MySQL로 전환했을 때, DB Entity와 Repository 구현체만 변경해도 문제가 빠르게 해결되는 경험을 통해 이점이 확실하다는 것을 체감했다.
'Backend' 카테고리의 다른 글
컨테이너와 VM은 뭐가 다를까? (0) | 2025.03.30 |
---|---|
토스증권이 이중화 된 데이터센터에서 kafka를 안정적으로 사용하는 노하우 (0) | 2025.02.04 |
AWS lambda python 패키징 오류 타파 (0) | 2024.12.07 |
전략 패턴을 사용하여 test에 적용하기 (0) | 2024.11.27 |
Interface를 테스트 하라? (0) | 2024.11.25 |