본문은 [도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지]를 읽고 간단하게 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
6.1 표현 영역과 응용 영역
- 도메인이 제 기능을 하려면 사용자와 도메인을 연결해주는 매개체가 필요하다
- 2장에서 설명한 응용 영역과 표현 영역이 사용자와 도메인을 연결해주는 매개체 역할을 한다
- 표현 영역은 사용자의 요청을 해석하고, 사용자가 실행하고 싶은 기능을 판별하여 그 기능을 제공하는 응용 서비스를 실행한다
- 응용 영역에 위치한 서비스가 실제 사용자가 원하는 기능을 제공한다
- 표현 영역에서는 데이터 형식 변환이 이루어진다
- 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다
- 응용 서비스를 실행한 뒤에 표현 영역은 실행 결과를 사용자에게 알맞은 형식(HTML, JSON) 으로 변환한다
- 사용자와 상호작용은 표현 영역이 처리하므로 응용 서비스는 표현 영역에 의존하지 않는다
- 응용 영역은 사용자가 웹 브라우저를 사용하는지, REST API를 호출하는지, TCP 소켓을 사용하는지 알 필요가 없다
- 단지 기능 실행에 필요한 입력 값을 받고 실행 결과만 리턴하면 된다
6.2 응용 서비스의 역할
- 응용 서비스의 주요 역할은 리포지터리에서 도메인 객체를 가져온 후, 이를 이용해서 사용자의 요청을 처리하는 것이므로 표현(사용자) 영역 입장에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해주는 창구 역할을 한다
- 응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 다음과 같은 단순한 형태를 갖는다
public Result doSomeFunc(SomeReq req) {
// 1. 리포지터리에서 애그리거트를 구한다.
SomeAgg agg = someAggRepository.findById(req.getId());
checkNull(agg);
// 2. 애그리거트의 도메인 기능을 실행한다.
agg.doFunc(req.getValue());
// 3. 결과를 리턴한다.
return createSuccessResult(agg);
}
- 새로운 애그리거트를 생성하는 응용 서비스 역시 간단하다
public Result doSomeCreation(CreateSomeReq req) {
// 1. 데이터 중복 등 데이터가 유효한지 검사한다.
validate(req);
// 2. 애그리거트를 생성한다.
SomeAgg newAgg = createSome(req);
// 3. 리포지터리에 애그리거트를 저장한다.
someAggRepository.save(newAgg);
// 4. 결과를 리턴한다.
return createSuccessResult(newAgg);
}
- 응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높다
- 응용 서비스가 도메인 로직을 일부 구현하면 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있다
- 응용 서비스는 트랜잭션 처리도 담당한다
- 응용 서비스는 도메인의 상태 변경을 트랜잭션으로 처리해야 한다
- 응용 서비스의 주요 역할로 트랜잭션 외에 접근 제어와 이벤트 처리도 있는데 이는 뒤에서 살펴본다
1) 도메인 로직 넣지 않기
- 도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다
- 첫 번째 문제는 코드의 응집성이 떨어진다는 것이다
- 이는 도메인 로직을 파악하기 위해 여러 영역을 분석해야 한다는 문제점을 야기한다
- 두 번째 문제는 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다는 것이다
- 두 가지 문제(응집도가 떨어지고 코드 중복이 발생)는 결과적으로 코드 변경을 어렵게 만든다
6.3 응용 서비스의 구현
- 응용 서비스는 디자인 패턴에서 파사드(facade)와 같은 역할을 한다
- 이 절에서는 응용 서비스를 구현할 때 몇 가지 고려할 사항과 트랜잭션과 같은 구현 기술의 연동에 대해 살펴본다
1) 응용 서비스의 크기
- 응용 서비스 자체의 구현은 어렵지 않지만 응용 서비스의 크기를 나누는 것은 고민거리이다
- 응용 서비스는 보통 다음 두 가지 방법 중 한 가지 방식으로 구현한다
- 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
- 구분되는 기능별로 응용 서비스 클래스 따로 구현하기
- 1) 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
- 장점
- 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다
- 단점
- 한 서비스 클래스의 크기 (코드 줄 수)가 커진다
- 코드 크기가 커지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지게 되므로 관련 없는 코드가 뒤섞여 코드를 이해하는 데 방해가 될 수 있다
- 한 클래스에 코드가 모이기 시작하면 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 억지로 끼워 넣게 되고, 코드를 점점 얽히게 만들어 코드 품질을 낮추는 결과를 초래한다
- 장점
- 2) 구분되는 기능별로 응용 서비스 클래스 따로 구현하기
- 한 응용 서비스 클래스에서 한 개 내지 2~3개의 기능을 구현한다
- 공통 로직의 경우 별도 클래스에 로직을 구현하면 코드가 중복되는 것을 방지할 수 있다
- 장점
- 한 클래스에 관련 기능을 모두 구현하는 것보다 코드 품질을 일정 수준으로 유지하는 데 도움이 된다
- 각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않는다
- 단점
- 클래스의 개수가 많아진다
- 참고로 필자는 한 클래스가 많은 역할을 갖는 것보다 각 클래스마다 구분되는 역할을 갖는 것을 선호한다고 한다
2) 응용 서비스의 인터페이스와 클래스
- 응용 서비스의 인터페이스는 대개 필요하지 않다
- 인터페이스가 필요한 몇 가지 상황이 있는데, 그중 하나는 구현 클래스가 여러 개인 경우다
- 응용 서비스는 런타임에 교체하는 경우가 거의 없고 한 응용 서비스의 구현 클래스가 두 개 이상인 경우도 드물다
- 따라서 응용 서비스의 인터페이스와 클래스를 따로 구현하면 필요성에 비해 구조가 복잡해지는 문제가 발생한다
- 테스트 주도 개발을 즐겨하고 표현 영역부터 개발을 시작한다면 미리 응용 서비스를 구현할 수 없으므로 응용 서비스의 인터페이스부터 작성하게 된다
- 표현 영역이 아닌 도메인 영역이나 응용 영역의 개발을 먼저 시작하면 응용 서비스 클래스가 먼저 만들어진다
- Mockito와 같은 테스트 도구를 이용하면 테스트용 대역 객체를 만들 수 있으므로 응용 서비스에 대한 인터페이스가 없어도 표현 영역을 테스트할 수 있다
- 이는 결과적으로 응용 서비스에 대한 인터페이스 필요성을 약화시킨다
3) 메서드 파라미터와 값 리턴
- 응용 서비스의 메서드가 필요한 값을 개별 파라미터로 전달받거나 VO 등의 데이터 클래스를 만들어 전달받을 수 있다
- 응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전달을 위해 별도 클래스를 사용하는 것이 편리하다
- 응용 서비스의 결과를 표현 영역에서 사용해야 하면 응용 서비스 메서드의 결과로 필요한 데이터를 리턴한다
- 예를 들어 식별자 또는 애그리거트 객체를 리턴할 수 있다
- 이때, 애그리거트 자체를 리턴하면 코딩은 편해지더라도 도메인의 로직 실행을 응용 서비스와 표현 영역에 두 곳에서 할 수 있으므로 코드의 응집도를 낮추는 원인이 된다
4) 표현 영역에 의존하지 않기
- 응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안 된다는 점이다
- 예를 들어 표현 영역에 해당하는 HttpServletRequest나 HttpSession을 응용 서비스에 파라미터로 전달하면 안 된다
- 응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워진다
- 게다가 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제가 발생한다
- 더 심각한 것은 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있다는 것이다
- 예를 들어 응용 서비스에서 표현 영역과 관련된 타입을 파라미터로 받아서표현 영역의 상태를 처리한다고 생각해보자
- 이렇게 되면 표현 영역의 코드만으로 표현 영역의 상태가 어떻게 변경되는지 추적하기 어려워진다
- 결국 표현의 응집도가 깨져서 코드 유지 보수 비용이 증가하게 된다
- 위의 이유로 응용 서비스가 표현 영역의 기술을 사용하지 않도록 분리해야 한다
- 이를 지키기 위한 가장 쉬운 방법은 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않는 것이다
5) 트랜잭션 처리
- 응용 서비스의 중요한 역할은 트랜잭션을 관리하는 것이다
- 스프링과 같은 프레임워크가 제공하는 트랜잭션 관리 기능을 이용하면 쉽게 트랜잭션을 처리할 수 있으므로 적극 사용하는 것이 좋다
- 스프링은 @Transacional이 적용된 메서드가 RuntimeException을 발생시키면 트랜잭션을 롤백하고 그렇지 않으면 커밋하므로 이 규칙에 따라 코드를 작성하면 트랜잭션 처리 코드를 간결하게 유지할 수 있다
6.4 표현 영역
- 표현 영역의 책임은 크게 다음의 세가지와 같다
- 1) 사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공하고 제어한다
- 2) 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다
- 이 과정에서 표현 영역은 사용자의 요청 데이터를 응용 서비스가 요구하는 형식으로 변환하고 응용 서비스의 결과를 사용자에게 응답할 수 있는 형식으로 변환한다
- 또한 응용 서비스의 실행 결과를 사용자에게 알맞은 형식(뷰, JSON, 예외 메시지 등)으로 제공한다
- 3) 사용자의 세션을 관리한다
6.5 값 검증
- 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다
- 원칙적으로는 모든 값에 대한 검증은 응용 서비스에서 처리한다
- 표현 영역은 사용자로부터 입력받는 값에 잘못된 값이 존재하면 이를 사용자에게 알리고 값을 다시 입력받아야 한다
- 표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 ID 중복 여부와 같은 논리적인 오류만 검사하면 된다
- 즉, 표현 영역과 응용 서비스가 값 검사를 나눠서 수행하는 것이다
- 응용 서비스를 사용하는 표현 영역 코드가 한 곳이면 구현의 편리함을 위해 다음과 같이 역할을 나누어 검증을 수행할 수도 있다
- 표현 영역: 필수 값, 값의 형식, 범위 등을 검증한다
- 응용 서비스: 데이터의 존재 유무와 같은 논리적 오류를 검증한다
- 응용 서비스에서 얼마나 엄격하게 값을 검증해야 하는지에 대해서는 의견이 갈릴 수 있다
- 필자는 응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편이라고 한다
- 응용 서비스에 필요한 값 검증을 모두 처리하면 작성할 코드가 늘어나는 불편함이 있지만 반대로 응용 서비스의 완성도가 높아지는 이점이 있다고 한다
- 필자는 응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편이라고 한다
6.6 권한 검사
- 보안 프레임워크의 복잡도를 떠나 보통 다음 세 곳에서 권한 검사를 수행할 수 있다
- 표현 영역
- 응용 서비스
- 도메인
- 표현 영역에서 할 수 있는 기본적인 검사는 인증된 사용자인지 아닌지 검사하는 것이다
- 예를 들어 회원 정보 변경을 처리하는 URL에 대해 표현 영역은 다음과 같이 접근 제어를 수행한다
- 이런 접근 제어를 하기에 좋은 위치가 서블릿 필터(Servlet Filter)이다
- 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부와 권한을 검사한다(스프링 시큐리티도 이와 유사한 방식으로 동작한다)
- URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행한다
- 이것이 꼭 응용 서비스의 코드에서 직접 권한 검사를 해야 한다는 것을 의미하지는 아니다
- 예를 들어 스프링 시큐리티는 AOP를 활용해서 다음과 같이 애너테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다
public class BlockMemberService {
private MemberRepository memberRepository;
@PreAuthorize("hasRole('ADMIN')")
public void block(String memberId) {
Member member = memberRepository.findById(new MemberId(memberId))
.orElseThrow(() -> new NoMemberException());
member.block();
}
}
- 개별 도메인 객체 단위로 권한 검사를 해야 하는 경우엔 구현이 복잡해진다
- 예를 들어 게시글 삭제는 본인 또는 관리자 역할을 가진 사용자만 할 수 있으므로 게시글 작성자가 본인인지 확인하려면 게시글 애그리거트를 먼저 로딩해야 한다
- 즉, 응용 서비스의 메서드 수준에서 권한 검사를 할 수 없으므로 직접 권한 검사 로직을 구현해야 한다
- 스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권한 검사 기능을 프레임워크에 통합할 수 있다
- 도메인 객체 수준의 권한 검사 로직은 도메인별로 다르므로 도메인에 맞게 보안 프레임워크를 확장하려면 프레임워크에 대한 높은 이해가 필요하다
- 이해도가 높지 않다면 프레임워크를 사용하는 대신 도메인에 맞는 권한 검사 기능을 직접 구현하는 것이 코드 유지 보수에 유리하다
6.7 조회 전용 기능과 응용 서비스
- 5장에서는 조회 화면을 위한 조회 전용 모델과 DAO를 만드는 내용을 다뤘다
- 서비스에서 이들 조회 전용 기능을 사용하면 서비스 코드가 다음과 같이 단순히 조회 전용 기능을 호출하는 형태로 끝날 수 있다
public class OrderListService {
public List<Orderview> getOrderList(String ordererId) {
return orderViewDao.selectByOrderer(ordererId);
}
}
- 이렇게 코드를 작성하면 서비스에서 수행하는 로직이 없고 단일 쿼리만 실행하는 조회 전용 기능이어서 트랜잭션이 필요하지도 않다
- 이 경우라면 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 전용 기능을 사용해도 문제가 없다
public class OrderController {
private OrderViewDao orderViewDao;
@RequestMapping("/myorders")
public String list(ModelMap model) {
String ordererId = SecurityContext.getAuthentication().getId();
List<OrderView> orders = orderViewDao.selectByOrderer(ordererId);
model.addAttribute("orders", orders);
return "order/list";
}
...
}
- 응용 서비스를 항상 만들었던 개발자는 컨트롤러와 같은 표현 영역에서 응용 서비스 없이 조회 전용 기능에 접근하는 것이 이상하게 느껴질 수 있다
- 하지만 응용 서비스가 사용자 요청 기능을 실행하는 데 별다른 기여를 못한다면 굳이 서비스를 만들지 않아도 된다
- 조회 전용 기능에 대한 내용은 11장 CQRS에서 다시 살펴볼 것이다
'책 > 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지' 카테고리의 다른 글
[도메인 주도 개발 시작하기] 8장: 애그리거트 트랜잭션 관리 (0) | 2022.10.24 |
---|---|
[도메인 주도 개발 시작하기] 7장: 도메인 서비스 (0) | 2022.10.21 |
[도메인 주도 개발 시작하기] 5장: 스프링 데이터 JPA를 이용한 조회 기능 (0) | 2022.10.07 |
[도메인 주도 개발 시작하기] 4장: 리포지터리와 모델 구현 (0) | 2022.10.03 |
[도메인 주도 개발 시작하기] 3장: 애그리거트 (1) | 2022.09.20 |