티스토리 뷰
이전 글: https://gojs.tistory.com/64
도메인 주도 개발(DDD) 알아보기
도메인 주도 개발을 해야하는 이유?도메인 주도 개발은 OOP를 깔끔하게 적용할 수 있는 하나의 방법이다.유지보수성이 높은 코드를 생산하기 위해 OOP 기반의 도메인을 설계하고, 쿼리 매퍼 대신
gojs.tistory.com
도메인이 많아지는 경우
만약 100개 이상의 도메인을 표현하는 다이어그램을 보고 시스템을 이해하려한다면 한 번에 이해가 되지 않을 것이다.
특정 기능을 수정하는 경우 이 기능과 인접한 몇 개의 도메인만을 파악하여 수정해서 전체적인 시스템의 일관성이 틀어지게 될 수도 있다.
이처럼 복잡한 도메인을 이해하기 위해 애그리거트를 사용한다.
도메인 영역의 여러 객체를 애그리거트 단위로 묶어서 상위 단위의 다이어그램으로 시스템을 보다 간단하게 파악할 수 있다.
위와 같은 여러 객체의 관계를 애그리거트로 묶어서 간단하게 파악할 수 있다.
애그리거트는 관련된 모델을 하나로 묶었기 때문에 동일한 라이프사이클을 가진다. 따라서 Account를 생성할 때는 AccountType과 RatePolicy를 같이 생성한다. 라이프사이클이 일치하지 않는 경우에는 애그리거트 단위가 제대로 설계된 것인지 확인할 필요가 있다.
애그리거트 루트
애그리거트 내의 객체들은 동일한 라이프사이클을 가지며 데이터가 변경되는 경우에도 일관성을 유지해야한다.
예를 들면 이체 애그리거트에 아래와 같은 요구사항이 있다고 가정해보자.
- 이체가 완료되면 ReceiveInfo의 receiveState가 COMPLETE로 저장되어야 한다.
- 이체가 완료되면 SendInfo의 sendState가 COMPLETE로 저장되어야 한다.
이체 완료 처리를 하는 경우 요구사항을 일관적으로 지키기위해 애그리거트를 관리할 주체가 필요한데, 이 책임을 지는 것이 애그리거트 루트 엔티티이다.
이체 애그리거트의 루트 엔티티는 Transfer이다. 따라서 이체 완료에 대한 처리를 Transfer가 수행하기 때문에 상태를 바꾸는 역할도 Transfer가 수행한다.
public class Transfer {
...
private SendInfo sendInfo;
private ReceiveInfo receiveInfo;
...
public void complete() {
...
sendInfo.complete();
receiveInfo.complete();
}
}
위 코드를 보면 Transfer의 complete() 메서드는 도메인 규칙을 구현한 기능을 응용 계층에 public하게 제공한다.
그러나 sendInfo의 complete()나 receiveInfo의 complete() 메서드는 도메인 계층 외부에 열려있으면 일관성이 깨지는 수단이 될 수 있다.
따라서 애그리거트 루트에만 제공할 수 있도록 protected 접근 제한자로 선언할 수 있다.
애그리거트 트랜잭션 단위
트랜잭션 범위는 작을수록 좋다.
한 트랜잭션에서 여러 개의 테이블을 조회하고 변경하는 경우ㅡ 연관된 데이터의 락을 선점하기 때문에 멀티 스레드 환경에서 동시성이 저하할 수 있다. (성능 저하)
따라서 일반적으로 하나의 트랙잭션에서 하나의 애그리거트만 변경하는 것이 좋다.
예를 들어 개명한 고객이 계좌를 새로만드는 경우 고객 애그리거트의 이름 정보를 수정하는 기능이 있다고 가정해보자.
public class Account {
...
private Member owner;
...
public Account(AccountCreateInfo createInfo) {
...
String createMemberName = createInfo.getMemberName();
if (!owner.getName().equals(createMemberName)) {
owner.changeName(createMemberName);
}
}
}
위의 코드는 고객 애그리거트의 정보를 계좌 애그리거트에서 수정하고 있기 때문에 애그리거트 간의 결합도가 생긴다.
또한 하나의 트랜잭션에서 여러 애그리거트를 수정함으로써 트랜잭션이 길어지고 동시성이 떨어지게 된다.
public class CreateAccountService {
...
@Transactional
public void createAccount(AccountCreateInfo accountCreateInfo) {
Account account = new Account(accountCreateInfo);
accountRepository.save(account);
Member owner = account.getOwner();
String newName = accountCreateInfo.getName();
if (!owner.getName().equals(newName)) {
owner.changeName(newName);
}
}
}
애그리거트 간의 결합도를 줄이기 위해서라면 위와 같이 응용 계층에서 두 개의 애그리거트를 수정하는 방법으로 어느정도 해소할 수 있다.
트랜잭션을 분리하고자 한다면 응용 계층에서 비동기 이벤트를 발행하는 방식으로 개발하는 것이 더 좋은 방법이 될 것이다. (추후 포스팅)
애그리거트 단위의 영속성 구현
애그리거트에 포함된 엔티티 모델들 전체가 완전한 한 개의 도메인을 표현하기 때문에 레포지토리는 애그리거트 단위로 존재한다.
예를 들어 이체에 관련된 테이블이 3개라면 Transfer 애그리거트를 저장할 때 모든 테이블의 영속성을 한 번에 관리해야한다.
...
// 이체 레포지토리는 완전한 이체 정보를 조회한다
Transfer transfer = transferRepository.findById(transferId);
// transfer가 온전하지 않으면 receiveInfo에 대해 NPE가 발생할 수도 있다
transfer.receive();
...
애그리거트 간 참조
JPA를 사용하는 경우 엔티티 연관관계를 이용하면 손쉽게 애그리거트를 참조할 수 있다.
account.getOwner().changeName(newName);
위와 같이 계좌 애그리거트에서 손쉽게 고객 애그리거트를 참조하여 이름을 변경하는데, 이 코드는 여러 문제를 내포하고 있다.
우선 애그리거트가 관리하는 범위를 침범하기 때문에 애그리거트간 강한 결합이 발생할 수 있고, 애그리거트 간 연관관계 로딩 전략을 디테일하게 결정해야한다.
또한 애그리거트의 분리나 확장이 어려울 수 있다. 각 업무에 맞는 구현 기술을 사용한다던지 인프라를 확장하는 과정에서 JPA라는 단일 기술만을 사용해야한다.
이러한 문제를 해결하기 위한 방법은 애그리거트 간 참조 시 ID를 이용하는 방법이다.
Member owner = memberRepository.findById(account.getOwnerId());
owner.changeName(newName);
위와 같이 응용 계층에서 ID를 이용하여 참조하기 때문에 지연 로딩을 하는 것과 동일한 결과를 얻을 수 있다.
또한 애그리거트에서 직접 다른 애그리거트를 수정하는 문제를 방지할 수 있으며, 각 애그리거트 간 다른 기술로 구현하더라도 문제가 없다.
애그리거트 팩토리 메서드
신규 계좌를 생성하는 경우 신용점수에 따라 생성이 안되게 막는 로직이 존재한다고 가정해보자.
public class CreateAccountService {
public void createAccount(CreateAccountInfo createAccountInfo, int creditScore) {
if (creditScore < 500) {
throw new ShortfallCreditScoreException();
}
Account account = new Account(createAccountInfo);
accountRepository.save(account);
}
...
}
위 코드는 Account가 생성 가능한지 판단하는 코드와 생성하는 코드가 분리되어있다.
미달기준 신용점수가 500점이라는 것과 미달의 경우 생성에 실패한다는 비즈니스 로직이 도메인 계층이 아닌 응용 계층에 노출된다.
이 경우 아래와 같이 팩토리 메서드를 제공하는 방식으로 구현할 수도 있다.
public class CreateAccountService {
public void createAccount(CreateAccountInfo createAccountInfo, int creditScore) {
Account account = Account.create(createAccountInfo, creditScore);
accountRepository.save(account);
}
...
}
public class Account {
...
public static Account(CreateAccountInfo createAccountInfo, int creditScore) {
...
if (creditScore < 500) {
throw new ShortfallCreditScoreException();
}
...
}
}
위와 같이 팩토리 메서드를 구현하게 되면 Account의 생성 검증로직이 도메인 계층에서 수행되기 때문에 생성에 대한 응집도가 더욱 높아지게 된다.
다음 글: https://gojs.tistory.com/66
JPA를 활용한 DDD 적용
JPA를 사용하지 않고 MyBatis나 JDBC를 활용하더라도 의존성을 잘 관리한다면 충분히 DDD를 적용할 수 있다.그러나 JPA와 같은 ORM을 사용한다면 도메인 모델과 데이터 모델을 매핑하는 다양한 기능을
gojs.tistory.com
'공부 > DDD' 카테고리의 다른 글
이벤트를 활용한 도메인 의존성 관리하기 (0) | 2025.04.06 |
---|---|
애그리거트 단위의 트랜잭션 관리하기 (0) | 2025.02.28 |
DDD에서 표현 계층과 응용 계층 구성하기 (0) | 2025.02.02 |
JPA를 활용하여 DDD 구현하기 (0) | 2025.01.11 |
도메인 주도 개발(DDD) 알아보기 (0) | 2024.12.30 |