-
[도메인 주도 개발 시작하기] 6. 응용 서비스와 표현 영역STUDY/DDD 2022. 12. 12. 22:56
1. 표현 영역과 응용 영역
사용자에게 기능을 제공하려면 도메인과 사용자를 연결해 줄 표현 영역과 응용 영역이 필요하다.
표현 영역은 사용자의 요청을 해석한다. 사용자의 HTTP 요청은 표현 영역에 전달되고, 표현 영역은 URL, 요청 파라미터, 쿠키, 헤더 등을 이용해서 사용자가 실행하고 싶은 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.
응용 영역은 실제 사용자가 원하는 기능을 제공하는 서비스가 위치해 있다. 응용 서비스는 기능을 실행하는 데 필요한 입력 값을 메서드 인자로 받고 실행 결과를 리턴한다.
응용 서비스의 메서드 파라미터와 표현 영역이 사용자로부터 전달받은 데이터는 형식이 일치하지 않기 때문에 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
응용 서비스를 실행한 뒤에 표현 영역은 실행 결과를 사용자에게 알맞은(사용자 요청에 맞게 HTML이나 JSON) 형식으로 응답한다.
사용자와 상호작용은 표현 영역이 처리하기 때문에, 응용 서비스는 표현 영역에 의존하지 않는다. 응용 영역은 사용자가 웹 브라우저를 사용하는지 REST API를 호출하는지, TCP 소켓을 사용하느지 알 필요 없이 단지 기능 실행에 필요한 입력 값을 받고 실행 결과만 리턴하면 된다.
2. 응용 서비스의 역할
응용 서비스는 사용자(클라이언트)가 요청한 기능을 실행한다. 응용 서비스는 사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다.
응용 서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는 것이므로 표현(사용자) 영역 입장에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해 주는 창구 역할을 한다.
응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 단순한 형태를 갖는다. 응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높은데, 그러면 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있다.
public Result doSomeFunc(SomeReq req) { // 1. 리포지터리에서 애그리거트를 구한다. SomeAgg agg = someAggRepository.findById(req.getId()); checkNull(agg); // 2. 애그리거트의 도메인 기능을 실행한다. agg.doFunc(req.getValue()); // 3. 결과를 리턴한다. return createSuccessResult(agg); }
응용 서비스는 트랜잭션 처리도 담당한다. 응용 서비스는 도메인의 상태 변경을 트랜잭션으로 처리해야 한다. 상태 변경 도중 문제가 발생했을 때 데이터 일관성이 깨지지 않게 하기 위해서 트랜잭션 범위에서 응용 서비스를 실행해야 한다. 응용 서비스는 트랜잭션 외에 접근 제어와 이벤트 처리 역할도 수행한다.
도메인 로직을 응용 서비스에 구현할 경우 발생하는 문제점
- 코드의 응집성이 떨어진다. 도메인 데이터와 그 데이터를 조작하는 도메인 로직이 한 영역에 위치하지 않고 서로 다른 영역에 위치한다는 것은 도메인 로직을 파악하기 위해 여러 영역을 분석해야 한다는 것을 의미한다.
- 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
두 가지 문제는 결과적으로 코드 변경을 어렵게 만들고, 그만큼 소프트웨어의 가치는 떨어진다는 것을 의미한다. 소프트웨어의 가치를 높이려면 도메인 로직을 도메인 영역에 모아서 코드 중복을 줄이고 응집도를 높여야 한다.
3. 응용 서비스의 구현
응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 하는데 이는 디자인 패턴에서 파사드(facade)와 같은 역할을 한다. 응용 서비스 자체는 복잡한 로직을 수행하지 않기 때문에 응용 서비스의 구현은 어렵지 않다.
응용 서비스를 구현할 때 고려할 사항
- 응용 서비스의 크기
한 서비스 클래스 내에서 도메인과 관련된 여러 기능을 갖게 되면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지고, 이는 결과적으로 관련 없는 코드가 뒤섞여 코드를 이해하는 데 방해가 될 수 있다. 따라서 한 도메인과 관련된 기능을 하나의 응용 서비스 클래스에서 모두 구현하는 방식보다 구분되는 기능을 별도의 서비스 클래스로 구현하는 방식을 사용하는 것이 좋다.
- 응용 서비스의 인터페이스와 클래스
인터페이스는 구현 클래스가 다수 존재하거나 런타임에 구현 객체를 교체해야 할 때 유용하게 사용할 수 있다. 하지만 응용 서비스는 런타임에 교체하는 경우가 거의 없고 한 응용 서비스의 구현 클래스가 두 개인 경우가 드물다. 결과적으로 인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고 구현 클래스에 대한 간접 참조가 증가해서 전체 구조가 복잡해질 수 있다. 따라서 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것이 좋은 선택이라고 볼 수 없다.
- 메서드 파라미터와 값 리턴
응용 소바수거 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는데 필요한 값을 파라미터로 전달받아야 하고, 응용 서비스의 결과를 표현 영역에서 사용해야 할 경우 응용 서비스 메서드의 결과로 표현 영역에서 필요한 데이터를 리턴한다.
- 표현 영역에 의존하지 않기
응용 서비스의 파라미터 타입을 결정할 때 표현 영역과 관련된 타입을 사용하지 않도록 주의해야 한다.
@Controller @RequestMapping("/member/changePassword") public class MemberPasswordController { @PostMapping public String submit(HttpServletRequest request) { try { // 응용 서비스가 표현 영역에 의존하면 안 됨 changePasswordService.changePassword(request); } catch (NoMemberException ex) { // } } }
응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기 어려워지고, 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제가 발생한다. 또한 응용 서비스가 표현 영역의 역할까지 대신하는 문제가 발생할 수도 있다. 이러한 문제가 발생하지 않도록 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않아야 한다.
트랜잭션 처리
트랙잭션을 관리하는 것은 응용 서비스의 중요한 역할이다. 프레임워크가 제공하는 트랜잭션 기능을 사용하면 간단한 설정만으로 트랜잭션을 시작하여 커밋하고 exception이 발생하면 롤백할 수 있다. 스프링의 경우 @Transactional이 적용된 메서드가 RuntimeException을 발생시키면 트랜잭션을 롤백하고 그렇지 않으면 커밋하므로 이 규칙에 따라 코드를 작성하면 트랜잭션 토드를 간결하게 유지할 수 있다.
4. 표현 영역
- 사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공하고 제어한다.
- 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 전달한다.
- 사용자의 세션을 관리한다.
웹 서비스의 표현 영역은 사용자가 요청한 내용을 응답으로 제공한다, 응답에는 다음 화면으로 이동할 수 있는 링크나 데이터를 입력하는 데 필요한 폼 등이 포함된다. 사용자는 표현 영역이 제공한 폼에 알맞은 값을 입력하고 다시 폼을 표현 영역에 전송한다. 표현 영역은 응용 서비스를 이용해서 표현 영역의 요청을 처리하고 그 결과를 응답으로 전송한다.
또한 화면에 보여주는데 필요한 데이터를 읽거나 도메인의 상태를 변경해야할 때 응용 서비스를 사용한다. 이 과정에서 표현 영역은 사용자의 요청 데이터를 응용 서비스가 요구하는 형식으로 변환하고 응용 서비스의 결과를 사용자에게 응답할 수 있는 형식으로 변환한다.
마지막으로 사용자의 연결 상태인 세션을 관리하는데, 웹은 쿠키나 서버 세션을 이용해서 사용자의 연결 상태를 관리한다.
5. 값 검증
값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다. 원칙적으로 모든 값에 대한 검증은 응용 서비스에서 차리하지만, 표현 영역에서 필수 값과 값의 형식을 검사하면 응용 서비스는 실질적으로 ID 중복 여부와 같은 논리적 오류만 검사하면 된다. 응용 서비스를 사용하는 표현 영역 코드가 한 곳이면 구현의 편리함을 위해 역할을 나누어 검증을 수행할 수 있다.
- 표현 영역 : 필수 값, 값의 형식, 범위 등을 검증한다.
- 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류를 검증한다.
6. 권한 검사
개발하는 시스템마다 권한의 복잡도가 다른데, 복잡도를 떠나 보통 세 곳에서 권한 검사를 수행할 수 있다.
- 표현 영역
- 응용 서비스
- 도메인
표현 영역에서 할 수 있는 기본적인 검사는 인증된 사용자인지 아닌지 검사하는 것이다.
- URL을 처리하는 컨트롤러에 웹 요청을 전달하기 전에 인증 여부를 검사해서 인증된 사용자의 웹 요청만 컨트롤러에 전달한다.
- 인증된 사용자가 아닐 경우 로그인 화면으로 리다이렉트 시킨다.
이러한 접근 제어를 하기에 좋은 위치가 서블릿 필터이다. 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사한다. 인증된 사용자면 다음 과정을 진행하고 그렇지 않으면 로그인 화면이나 에러 화면을 보여준다. 인증 여부뿐만 아니라 권한에 대해서 동일한 방식으로 필터를 사용해서 URL별 권한 검사를 할 수 있다.
URL만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야 한다. 이것이 꼭 응용 서비스의 코드에서 직접 권한 검사를 해야 한다는 것을 의미하는 것은 아니다. 예를 들어 스프링 시큐리티는 AOP를 활용해서 @PreAuthorize 애너테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다.
개별 도메인 객체 단위로 권한 검사를 해야 하는 경우는 구현이 복잡해진다. 응용 서비스의 메서드 수준에서 권한 검사를 할 수 없기 때문에 직접 권한 검사 로직을 구현해야 한다. 스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권한 검사 기능을 프레임워크에 통합할 수 있다.
7. 조회 전용 기능과 응용 서비스
조회를 위한 응용 서비스가 단지 조회 전용 기능을 실행하는 코드밖에 없다면 응용 서비스를 생략해도 된다. 서비스에서 수행하는 추가적인 로직이 없을뿐더러 단일 쿼리만 실행하는 조회 전용 기능은 트랜잭션이 필요하지 않기 때문에 서비스를 만들 필요 없이 표현 영역인 컨트롤러에서 조회 전용 기능을 사용해도 문제 없다.
출처 : 최범균, 『도메인 주도 개발 시작하기 DDD 핵심 개념 정리부터 구현까지』, 한빛미디어(2022), p200-p232
'STUDY > DDD' 카테고리의 다른 글
[도메인 주도 개발 시작하기] 8. 애그리거트 트랜잭션 관리 (0) 2022.12.19 [도메인 주도 개발 시작하기] 7. 도메인 서비스 (0) 2022.12.19 [도메인 주도 개발 시작하기] 4. 리포지터리와 모델 구현 (0) 2022.12.01 [도메인 주도 개발 시작하기] 3. 애그리거트 (0) 2022.11.18 [도메인 주도 개발 시작하기] 2. 아키텍처 개요 (0) 2022.11.08