"만들면서 배우는 클린 아키텍처"라는 책을 읽으며 고민했던 내용을 정리해 본 글입니다.
다른 의견이 있으시다면 댓글 언제나 환영합니다!
"만들면서 배우는 클린 아키텍처"를 읽으면서 다음과 같은 질문이 떠올랐다.
조회/명령 모델을 분리하여 DTO Projection을 빈번하게 사용하는 상황에서,
영속성 엔티티는 반드시 테이블의 모든 컬럼을 갖고 있어야 할까?
클린 아키텍처에서 영속성 엔티티의 역할
클린 아키텍처는 계층 간 의존성을 끊는 것을 목표로 한다.
서비스는 DB를 모르고, DB는 도메인의 비즈니스 규칙을 모르도록 설계한다. 이를 위해 두 가지 객체를 명확하게 분리한다.
- 도메인 모델
- 순수 Java 객체. JPA 모름
- 비즈니스 규칙과 행위를 담음
- 서비스 계층에서 사용
- 영속성 엔티티
- @Entity 붙은 JPA 클래스
- DB 스키마와 매핑을 담당
- 영속성 어댑터 내부에서 사용
영속성 어댑터는 mapper를 호출해서 두 객체 사이의 변환을 처리한다.
서비스로부터 도메인 모델을 받아 영속성 엔티티로 변환한 뒤 DB에 저장하고,
DB에서 꺼낸 영속성 엔티티를 다시 도메인 모델로 변환해 서비스로 돌려준다.
책에서 제공하는 Adapter 예시 코드를 보면 이 흐름이 잘 드러난다.
class AccountPersistenceAdapter implements
LoadAccountPort,
UpdateAccountStatePort {
@Override
public Account loadAccount(
AccountId accountId,
LocalDateTime baselineDate) {
AccountJpaEntity account =
accountRepository.findById(accountId.getValue())
.orElseThrow(EntityNotFoundException::new);
List<ActivityJpaEntity> activities =
activityRepository.findByOwnerSince(
accountId.getValue(),
baselineDate);
Long withdrawalBalance = orZero(activityRepository
.getWithdrawalBalanceUntil(
accountId.getValue(),
baselineDate));
Long depositBalance = orZero(activityRepository
.getDepositBalanceUntil(
accountId.getValue(),
baselineDate));
// 영속성 엔티티 → 도메인 모델 변환
return accountMapper.mapToDomainEntity(
account,
activities,
withdrawalBalance,
depositBalance);
}
}
영속성 엔티티를 테이블과 1:1로 매핑하는 게 맞을까?
DTO Projection을 사용하면 DB로부터 필요한 값들만 받아올 수 있다.
이러한 상황에서, 당장 쓰이지 않는 컬럼들을 포함해서 영속성 엔티티로 가져올 필요가 있을까?라는 고민이었다.
결론부터 말하면, 영속성 엔티티는 테이블의 모든 컬럼을 갖는 게 맞다고 생각한다.
지금 당장 쓰이지 않는 컬럼이라도 나중에 비즈니스 규칙이 바뀌면 컬럼이 필요해질 수 있다.
엔티티가 테이블의 모든 컬럼을 들고 있으면 mapper만 수정하면 되지만, 컬럼을 미리 제거했다면 영속성 엔티티부터 수정해야 한다.
따라서 데이터 저장, 수정, 단건 조회를 위해서 영속성 엔티티는 모든 컬럼을 포함하고 여러 테이블을 JOIN 하는 복잡한 조회 시에는 DTO Projection으로 처리하면 될 것이다.
결론
필자가 내린 결론은 영속성 엔티티와 DB 테이블은 1:1 매핑으로 연결하는 게 맞다고 생각한다.
하지만, 이는 모든 상황에 적용되지 않는다. 프로젝트의 상황이나 규모에 따라 고민해야 할 것이다.
모든 선택에는 TradeOff가 있으므로, 필자의 의견은 참고하고 본인의 프로젝트에 맞는 선택이 필요할 것이다.
참고
만들면서 배우는 클린 아키텍처 - 톰 홈버그