들어가며
DDD를 적용하여 프로젝트를 구성하고 개발하게 되면 대부분의 업무상 중요한 코드들을 도메인 계층에 작성하게 된다. 인터페이스와 계층과 응용 계층은 만들어 놓은 도메인 계층을 기반으로 구현된다. 인터페이스 계층이야 클라이언트와 통신을 하기 위해 필요하다고 이해할 수 있는데, 응용 계층은 반드시 필요한 것인가?라는 생각이 들었다. 이번 글에서는 DDD START! 책에서 나오는 인터페이스 계층과 응용 계층의 역할을 좀 더 알아보도록 한다. 만약 도메인 주도 설계의 계층에 대해서 생소하다면 이전 글을 참고하기 바란다.
인터페이스 계층
인터페이스 계층의 역할
- 사용자(클라이언트)의 요청을 해석하여 관련 기능으로 라우팅 시킨다.
분기를 판단할 수 있는 요청 내용에는 URL, 요청 파라미터, 쿠키, 헤더 등이 포함되어 있다. - 관련 기능이란 응용 서비스의 기능을 말한다.
인터페이스 계층의 구현체 예시
예시 1 (책 예시)
@RequestMapping(value = "/member/join")
public ModelAndView join(HttpServletRequest request) {
String email = requset.getParameter("email");
String password = request.getParameter("password");
// 사용자의 요청을 통해 응용 계층 요청으로 변환
JoinRequest joinReq = newJoinRequest(email, password);
// 변환한 객체를 이용해서 응용 서비스 실행
joinService.join(joinReq);
...
}
예시 2 (실제 프로젝트에서 사용한 예시)
아래의 예시는 노드라는 것을 등록하는 간단한 컨트롤러의 구현 부이다.
public class NodeApiController {
//응용 계층의 서비스 선언 (파사드 형태로 Node 라는 도메인의 모든 메소드들이 묶여있음)
private final NodeFacade nodeFacade;
@PostMapping
public CommonResponse registerNode(@RequestBody @Valid NodeDto.NodeRequest request) {
//클라이언트의 요청을 DTO로 받고난 후 응용 계층 서비스의 입력 객체로 변환 (커맨드)
NodeCommand command = request.toCommand();
//응용 계층 서비스 호출 및 결과값(객체) 획득
NodeInfo nodeInfo = nodeFacade.registerNode(command);
//결과값을 DTO를 이용하여 응답 DTO 포맷으로 변경
var response = new NodeDto.NodeResponse(nodeInfo);
//클라이언트로 응답 전송
return CommonResponse.success(response);
}
...
}
위의 코드를 통해 알 수 있듯이 컨트롤러는 클라이언트-응용계층 사이에서, 요청의 라우팅 및 변환 작업을 수행한다.
응용 계층
응용 계층의 역할
응용 서비스는 사용자(클라이언트)가 요청한 기능을 실행하는 역할을 수행한다. 이 역할을 수행할 때 주로 도메인 영역을 사용한다. (푸시 알림 전송 기능 및 이메일 발송 등과 같이 도메인 영역에 의존적이지 않은 기능을 수행하기도 한다.)
예시 1 (책 예시)
public Result doSomeFunc(SomeReq req) {
// 1. 리포지터리에서 애그리거트를 구한다.
SomeAggregate aggregate = someAggregateRepository.findById(req.getId());
// 2. 애그리거트의 도메인 기능을 실행한다.
aggregate.doFuncion(req.getValue());
// 3. 결과를 반환한다.
return createSuccessResult();
}
예시 2 (실제 프로젝트에서 사용한 예시)
아래의 예시는 노드라는 애그리거트의 도메인 계층에서 제공하는 매서드들을 모아 하나의 Facade로 만든 것이다.
@Slf4j
@Service
@RequiredArgsConstructor
public class NodeFacade {
private final NodeService nodeService;
public NodeInfo registerNode(NodeCommand command) {
NodeInfo nodeInfo = nodeService.registerNode(command);
return nodeInfo;
}
public NodeInfo modifyNode(Long nodeId, NodeCommand command) {
NodeInfo nodeInfo = nodeService.modifyNode(nodeId, command);
return nodeInfo;
}
public NodeInfo searchNodeInfo(Long nodeId) {
NodeInfo nodeInfo = nodeService.searchNodeInfo(nodeId);
return nodeInfo;
}
public List<NodeInfo> searchAllNodeInfo() {
List<NodeInfo> nodeInfoList = nodeService.searchAllNodeInfo();
return nodeInfoList;
}
public void removeNode(Long nodeId) { nodeService.removeNode(nodeId);}
}
예시 1과 2의 차이점은 크게 다음과 같다.
- 응용 서비스의 크기
예시 1의 경우 기능에 따라 응용 계층 클래스를 만든 것이고, 예시 2는 하나의 응용 계층에 파사드 형태로 노드 도메인의 모든 기능을 구현한 것이기 때문에 예시 2의 덩치가 더 크다. 이 방법의 장점은 도메인 기능을 이용할 때 공통으로 사용되는 로직을 이 클래스에 구현할 수 있다는 점과, 도메인 기능을 보다 사용하기 쉽게 모을 수 있다는 점이다. 하지만 덩치가 커질 경우 코드 해독에 방해가 될 수 있다. - 리포지터리 호출 방식
예시 1의 경우 도메인 계층 안에 있는 리포지터리를 직접 호출하는 형태이고, 예시 2의 경우 도메인 계층의 서비스에 객체를 전달하고 응답받는 형태로 구현되어 있다. 필자는 도메인 계층과 응용계층의 분리를 위해 예시 2와 같이 사용하는 편이다. (책에서는 간단한 검색 로직의 경우 응용 계층을 생략하고 인터페이스 계층에서 직접 DAO를 이용하여 호출하기도 한다.)
예시 3 (여러 도메인 기능 및 기타 기능을 Transaction 처리하는 예시)
public class InitPasswordService {
@Transactional
public void initializePassword(String memberId) {
Member member = memberReopsitory.findById(memberId);
checkMemberExists(member);
member.initializePassword();
//이메일 전송 요청 이벤트
sendNewPasswordMailToMember(member);
}
}
마치며
이번 글에서는 인터페이스 계층과 응용 계층의 역할에 대해 알아보았다. 응용 계층은 도메인 계층의 기능을 묶어서 인터페이스 계층에서 사용하기 편하도록 하거나, 도메인 계층과 도메인 계층에 포함되지 않는 기능의 트랜잭션 처리가 가능하도록 한다는 것을 확인할 수 있었다. 또한 응용 계층이 매우 단순하게 구현될 경우 생략하는 방법도 있다는 것을 알게 되었다.
참고자료
[1] DDD START! | 2016 | 최범균 | 링크
'MSA 설계 & 도메인주도 설계' 카테고리의 다른 글
[MSA] 이벤트 스토밍(Event storming) (0) | 2022.02.15 |
---|---|
[DDD] 도메인 계층의 Structure (0) | 2022.02.08 |
[DDD] 애그리거트란? (0) | 2022.02.06 |
[DDD] 도메인 주도 설계의 계층 분리에 관하여 (0) | 2022.02.03 |
[MSA] MSA 기반 프로젝트 입문 후기 (Devlos feat. @Todo) (1) | 2022.02.02 |