티스토리 뷰
이전 글: https://gojs.tistory.com/66
JPA를 활용한 DDD 적용
JPA를 사용하지 않고 MyBatis나 JDBC를 활용하더라도 의존성을 잘 관리한다면 충분히 DDD를 적용할 수 있다.그러나 JPA와 같은 ORM을 사용한다면 도메인 모델과 데이터 모델을 매핑하는 다양한 기능을
gojs.tistory.com
응용 계층 구성
응용 계층은 도메인 객체를 사용해서 사용자가 원하는 기능을 제공한다.
응용 계층은 표현 계층에 의존성을 가지지 않으며 어떤 통신을 이용하는지 등의 정보를 알 필요는 없다.
public Result processSomething(FunctionRequest request) {
Aggregate aggregate = aggregateRepository.findById(request.getId());
aggregate.process(request.getValue());
return createResult(aggregate);
}
일반적으로 응용 계층의 코드는 위와 같이 간단한 구조의 코드 형태를 가지게 된다.
복잡한 비즈니스 로직은 도메인 영역에서 처리되어야하며, 이를 어기게 되면 비즈니스 로직이 분산되는 등의 코드 품질을 저하시키는 영향을 줄 수 있다.
또한 응용 계층은 트랜잭션 처리에 대한 역할을 담당한다.
public void chooseWinners(LotteryRequest request) {
Event event = eventRepository.findById(request.getEventId());
event.chooseWinner();
}
public class Event {
private Set<Applicant> applicants;
private Set<Winner> winners;
private LotteryPolicy lotteryPolicy;
...
public void chooseWinner() {
int applicantsCount = applicants.size();
for (Applicant applicant : applicants) {
if (lotteryPolicy.lotteryWinner(applicant, applicantsCount, winners.size())) {
applicant.win();
winners.add(Winner.of(applicant));
}
}
}
...
}
위의 코드를 보면 이벤트의 응모자들을 추첨하여 당첨자를 선정하는 기능을 구현하고 있다.
위 코드에 따라 당첨자 정보를 저장하던 중 DB에서 문제가 생겨서 일부 사용자만 당첨자 처리가 된다면 데이터 일관성이 깨지게 될 것이다.
따라서 이런 상황을 방지하기 위해 트랜잭션 범위 내에서 수행될 수 있도록 서비스를 구성해야한다.
응용 서비스의 크기 고려하기
하나의 도메인에 대한 여러 기능을 구현하기 위해 하나의 응용 서비스에서 여러 기능을 제공할 수 있다.
public class AccountService {
private AccountRepository accountRepository;
public AccountCreateResult create(AccountCreateInfo createInfo) {
validateAccountNumber(createInfo.getAccountNumber());
...
}
public List<AccountTransactionHistory> retrieveTransactionHistory(String accountNumber) {
validateAccountNumber(accountNumber);
...
}
public void modifyPassword(String accountNumber, String oldPassword, String newPassword) {
validateAccountNumber(accountNumber);
...
}
private void validateAccountNumber(String accountNumber) {
...
}
}
위와 같이 서비스를 구현하게 되면 비슷한 기능의 메서드의 비슷한 로직을 private 메서드로 공통화해서 사용하기에 좋다.
그러나 도메인이 커지고 기능이 많아질수록 응용 서비스 하나가 너무 커져버리며 서로 연관없는 코드들이 뒤섞이게 된다.
public class AccountService {
private AccountRepository accountRepository;
private PushPublisher pushPublisher;
public AccountCreateResult create(AccountCreateInfo createInfo) {
...
}
public List<AccountTransactionHistory> retrieveTransactionHistory(String accountNumber) {
...
}
public void modifyPassword(String accountNumber, String oldPassword, String newPassword) {
...
pushPublisher.push(pushInfo);
}
}
비밀번호를 변경하는 경우 Push를 발송해야한다는 요구사항이 추가되어 위와 같이 AccountService에 PushPublisher에 대한 의존성을 추가했다.
그러나 계좌를 생성하거나 거래내역을 조회하는 기능에도 불필요한 의존성이 생기게 된다.
public class AccountCreateService {
private AccountRepository accountRepository;
public AccountCreateResult create(AccountCreateInfo createInfo) {
AccountServiceHelper.validateAccountNumber(createInfo.getAccountNumber());
...
}
...
}
public class AccountPasswordService {
private AccountRepository accountRepository;
public void modifyPassword(String accountNumber, String oldPassword, String newPassword) {
AccountServiceHelper.validateAccountNumber(accountNumber);
}
...
}
코드 품질 저하를 방지하기 위해 도메인 내의 세부적인 기능별로 응용 서비스를 분리할 수 있다.
이 경우 발생하는 중복 코드는 AccountServiceHelper와 같은 정적 클래스를 활용해 제거할 수 있다.
응용 서비스의 인터페이스 고려하기
응용 서비스는 관습적으로 인터페이스와 구현 클래스를 함께 구성하곤하지만, 실제로 인터페이스가 필요한 상황은 극히 드물다.
런타임에 여러 구현 클래스 중 하나를 의존성 주입해야하는 경우에는 인터페이스가 필요하다.
응용 서비스 개발 전 표현 계층을 개발하고 테스트할 필요가 있다면 인터페이스가 필요하다.
그 외의 경우에는 굳이 인터페이스를 만드는 것이 코드의 양만 많아질 뿐 인터페이스를 구성하는 것에 대한 특별한 효용은 없다.
응용 서비스의 파라미터와 반환 값
응용 서비스의 메서드는 사용자가 요구한 기능을 실행하는데 필요한 값만을 파라미터로 전달받아야 한다.
public class AccountService {
public void modifyPassword(String accountNumber, String oldPassword, String newPassword) {
...
}
public void modifyPassword(AccountPasswordModifyRequest modifyRequest) {
...
}
}
위 코드와 같이 여러 파라미터를 받던지 하나의 DTO 클래스를 선언해서 받을 수 있다.
파라미터가 3개 이상이 되면 가독성 차원에서 별도의 클래스를 생성하는 것이 나은 선택이라고 생각한다.
응용 서비스의 메서드는 표현 계층에서 필요한 데이터를 반환해야한다.
일종의 표현계층의 의존성이라고 볼 수도 있다고 생각되기 때문에.. 표현 계층에서는 기능에 대한 식별자를 반환받고 이를 활용해 표현에 필요한 다른 메서드로 필요한 데이터를 만드는 것이 원칙적으로는 맞다고 생각된다. (트레이드오프인 것 같다)
public class AccountService {
...
public AccountNumber modifyPassword(AccountPasswordModifyRequest modifyRequest) {
...
return accountNumber;
}
public PasswordModifyResultView retrieveModifyResultView(AccountNumber accountNumber) {
...
return PasswordModifyResultView.of(...);
}
}
public class AccountController {
private AccountService accountService;
@PutMapping("/account/password")
public ResponseEntity<PasswordModifyResultView> modifyPassword(AccountPasswordModifyRequest request) {
AccountNumber accountNumber = accountService.modifyPassword(request);
return ResponseEntity.ok(accountService.retrieveModifyResultView(accountNumber));
}
}
위 코드를 보면 modifyPassword 메서드는 오로지 비밀번호를 바꾸는 것에만 집중하고 응답은 Account의 식별자를 응답한다.
비밀번호 변경 결과 화면에 필요한 정보를 찾는 것은 retrieveModifyresultView 메서드가 그 역할을 가진다. 해당 메서드는 비밀번호 결과 화면에 필요한 데이터를 만드는 것에만 집중한다.
개발을 하다보면 응용 계층에서 애그리거트 루트 엔티티 자체를 반환하는 것이 더 편해지는 순간이 온다. 그러나 그렇게 구성하게 된다면 도메인 로직을 표현 계층에서도 실행할 수 있게 되며 이러한 것들이 코드 응집도를 낮추는 원인이 되기 때문에 별도의 클래스를 구성하는 것이 더 바람직하다.
표현 영역에 의존하지 않기
API 요청의 세션 값이나 헤더 값을 사용해야하는 경우가 생길 수도 있다.
그러나 응용 계층은 어떤 경로로 어떤 통신을 통해 요청이 들어왔는지 알 필요가 없다.
따라서 이러한 값들은 표현 계층에서 변환해서 응용 계층으로 넘겨줘야하며, HttpServletRequest나 HttpSession 등의 객체를 전달받아서는 안된다.
포현 계층 구성
표현 계층은 클라이언트의 요청을 받고 응답을 주는 역할을 수행하며 세션을 관리한다.
따라서 요청에 따라 적당한 기능을 판별하고 해당 기능을 제공하는 응용 계층을 수행한다.
@PutMapping("/api/v1/password")
public String changePassword(ChangePasswordRequest changePasswordReq) {
String memberId = SecurityContext.getAuthentication().getId();
try {
changePasswordService.change(memberId, changePasswordReq);
return successView;
} catch(InvalidPasswordException e) {
log.error("Password is invalid: {}", e);
return failureView;
} catch(RuntimeException e) {
log.error("changePassword RuntimeException ===> {}", e);
return failureView;
}
}
위와 같이 패스워드를 변경하는 컨트롤러는 클라이언트에서 받은 응답과 세션의 정보를 조합하여 적절한 응용 계층 서비스를 호출한다.
또한 각 예외에 대한 응답 처리를 수행한다.
그렇다면 사용자 요청의 값을 검증하는 로직은 어디에 들어가는 것이 더 적절할까?
이론적으로 따지게 되면 표현 계층에서는 해당 화면의 요구사항에 따라 검증되어야하는 값에 대해 검증하는 것이 맞다고 본다.
응용 계층에서는 해당 도메인 자체의 요구사항에 따라 값이 검증되는 것이 맞다.
(하지만 이 역시 트레이드오프이다)
다음 글: https://gojs.tistory.com/68
애그리거트 트랜잭션 관리
하나의 애그리거트를 대상으로 동시에 다른 두 요청이 들어왔을 때 일관성이 깨질 수 있다. 만약 위의 그림과 같이 하나의 계좌에 대해 1,000원 출금/입금 거래가 동시에 들어왔다고 가정해보자.
gojs.tistory.com
'공부 > DDD' 카테고리의 다른 글
이벤트를 활용한 도메인 의존성 관리하기 (0) | 2025.04.06 |
---|---|
애그리거트 단위의 트랜잭션 관리하기 (0) | 2025.02.28 |
JPA를 활용하여 DDD 구현하기 (0) | 2025.01.11 |
DDD를 활용하여 복잡한 도메인 관리하기 (0) | 2025.01.05 |
도메인 주도 개발(DDD) 알아보기 (0) | 2024.12.30 |