[DDD-START] Ch06. 응용서비스와 표현영역
in Dev-Study on Architecture
공부하는 내용을 정리하는 목적으로 작성하고 있습니다. 잘못 작성된 내용을 지적해주시면 좀더깊이 공부해서 내용을 수정하겠습니다.
표현 영역과 응용서비스 영역
표현 영역은 사용자와의 상호작용을 수행한다.
사용자의 요청을 받고, 서비스로부터 받은 결과를 통해 사용자에게 적절한 응답을 전달한다.
응용서비스 영역은 실제 사용자가 원하는 기능을 수행한다.
사용자가 요구한 기능을 수행하기 위해 필요한 도메인 객체를 사용한다.
비즈니스 로직은 도메인에 구성되어있으므로 서비스 영역은 복잡한 로직없이 도메인 객체 간 흐름 제어를 수행한다.
표현 영역 프로세스
사용자의 요청을 해석한다.
- 사용자가 브라우저에서 폼에 입력하여 서버로 전달하면 표현영역은 요청온 URL, 파라미터, 쿠키, 헤더 등 정보를 해석한다.
사용자가 요구한 기능을 서비스에 요청한다. - 사용자가 원하는 기능을 실행하기 위해 해석한 입력값을 서비스가 해석할 수 있는 값으로 적절히 변환하여 서비스를 실행한다.
- 각 영역은 독립적으로 구성되어야 하므로 Servlet, Session 등의 값을 서비스로 보내지않고 서비스가 처리할 수 있는 객체로 변환하여 보낸다.
응용 영역 프로세스
사용자의 요청을 처리한다.
- 리포지터리로부터 도메인 객체를 얻고, 도메인 객체를 사용하여 사용자가 요구한 기능을 실행한다.
도메인 객체 간 실행 흐름을 제어한다. - 기능을 실행하기 위해 필요한 도메인들을 순서대로 실행한다.
트랙잭션을 처리한다. - 도메인의 상태는 반드시 트랜잭션으로 처리해야한다. 그래야 처리 도중 예외가 발생하더라도 무결성을 보장할 수 있다.
응용 서비스의 구현
응용 서비스의 크기
보통 서비스를 구현할 때 2가지 중 하나를 선택하게 된다.
- 한 서비스 클래스에서 처리해야 할 기능(도메인)을 모두 구현한다.
- 처리해야 할 기능별로 서비스 클래스를 분리한다.
전자의 경우 각 도메인 기능이 한 서비스 안에 구현되어있으므로, 코드 중복을 피할 수 있다.
하지만 서비스가 책임져야 할 기능이 많아질수록 서비스의 크기가 커지고 유지보수에 어려움이 생길 수 있다.
또한 구조 유지를 위해 한 서비스 클래스를 억지로 끼워맞추다보면 책임이 애매한 기능도 서비스에 추가될 수 있다.
후자의 경우 각 기능별로 서비스가 구현되어있으므로, 책임이 명료하여 유지보수에 이득이 될 수 있다.
하지만 기능이 많아질수록 서비스 클래스의 개수가 많아지는 문제가 발생하며, 각 서비스에서 공유될 기능은 코드가 중복될 수도 있다.
물론 정적 공통 클래스를 통해 중복 기능은 별도로 구현할 수 있는데 이렇게 코드 중복을 피할수도 있다.
응용서비스의 인터페이스와 클래스
과거 레거시 코드를 보면 SpringMVC에서 다음과 같은 구조를 볼 수 있다.
// package: ptl.counsel.controller (Controller Class)
CounselController.java
// package: ptl.counsel.service (Service Interface)
CounselService.java
// package: ptl.counsel.service.impl (Service implements Class)
CounselServiceImpl.java
실제 응용서비스는 CounselServiceImpl 클래스에 구현되어있지만 기능을 나타내는 인터페이스 CounselService가 존재한다.
CounselService는 특별한 기능이 없는데 이런 인터페이스를 유지하는게 옳은걸까?
구현 클래스가 2개 이상 존재하거나, 런타임 시점에 구현 객체의 교체가 일어날 수 있는경우는 인터페이스를 사용하여 의존을 낮추는 등
유용한 방법이 될 수 있다.
하지만 대부분의 서비스는 한번 구현되고 교체되는 일이 거의 없으며, 한 응용 서비스(하나의 기능 수행)를 2개 이상으로 나누어
구현하는 경우도 거의 없다. 따라서 위와 같은 구조에서 인터페이스는 불필요한 파일에 불과하게 된다.
파라미터
응용서비스는 기능을 수행하기위해 표현영역으로부터 1개 이상의 파라미터(기능 수행을 위한 조건값)가 필요할 수 있다.
파라미터가 여러 개인 경우 서비스 메서드에 일일이 파라미터로 전달하지 않고 객체로 전달하면 유지보수에 용이할 수 있다.
예를들어 문의내용을 수정할경우 Id(String), Title(String), RequestUser(String), RequestDate(TimeStamp) 등등 많은 파라미터가 필요할 수 있는데, 이 값들을 일일히 메서드 파라미터로 전달하지 않고 객체로 묶는다.
// 안좋은 예
public CounselService {
public String modify(String id, String title, String requestUserId, Long requestDate) { ... }
}
// 좋은 예
public CounselService {
public String modify(CounselRequestDtO dto) { ... }
}
결과 반환
응용서비스는 기능을 수행한 뒤 결과를 표현영역으로 반환할 책임이 있다.
표현영역에서 응용서비스로 파라미터를 그대로 보내지않고 응용서비스가 해석할 수 있는 객체로 변환해서 전달한것과 같이
응용서비스도 표현영역으로 결과를 반환할 때는 사용한 도메인 객체를 그대로 전달하는게 아니라 요구사항에 필요한 값만 전달해야 한다.
예를들어 문의 상세화면을 호출하기 위해 문의 엔티티를 그대로 표현영역으로 전달해버리면 표현영역과 도메인간 결합이 발생하는 문제가 있다.
따라서 표현영역이 사용자에게 전달하기 위한 값만 정제하여 반환해야 한다.
// 안좋은 예
public CounselService {
// Counsel Domain이 표현영역에 노출됨으로써 결합도가 증가한다.
public Counsel detail(CounselRequestDto dto) { ... }
}
// 좋은 예
public CounselService {
// View에 전달할 값만 추출함으로써 표현영역은 사용자에게 응답할 값에 대해 고민하지 않아도 된다.
public Map<String,Object> detail(CounselRequestDto dto) { ... }
}
트랜잭션 처리
응용 서비스에서 사용되는 도메인들은 모두 하나의 트랜잭션 내에서 처리되어야 한다.
예를들어 계좌이체 기능을 요청받은 서비스가 출금자의 계좌에서 출금기능(도메인)은 수행하였는데, 입금자의 계좌에서 입금기능(도메인)이 실패할경우 한 트랜잭션에서 이루어지지 않는다면 이체할 금액이 증발하는 문제가 발생할 수 있다.
따라서 서비스에서 처리될 기능은 한 트랜잭션으로 묶어 모두 커밋되거나, 모두 롤백되는 무결성이 보장되야 한다.
값 검증
표현영역과 응용서비스 영역은 각각 검증하는 영역이 구분되어야 한다.
표현영역은 사용자의 요청을 파라미터로 전달받으므로 필수 파라미터가 누락되진 않았는지, 잘못된 값이 들어오진 않았는지 등을 검증하고
응용서비스 영역은 논리적오류(데이터의 존재유무, 도메인결과에 따른 유효성 등등)을 검증할 수 있도록 한다.
Spring을 사용할경우 @Validator를 사용하여 검증 기능을 사용할 수 있다.
권한
관리자 기능은 관리자에게만 노출하고 일반 권한에게는 노출하지 않아야 한다.
권한정보는 보통 세션에 저장되어있으므로 표현영역(Controller)에서 처리할 수 있다고 생각되지만
이 경우 많은 요청을 처리하는 Controller가 있는경우 각 메서드마다 권한체크를 한다면 너무많은 중복로직이 들어가게 된다.
따라서 ServletFilter 혹은 Interceptor를 구현하여 요청이 Controller로 넘어가기 전에 권한체크를 하도록 한다
이 경우 Controller에서 요청을 받기 전에 이미 권한 검증이 끝나게되므로 뒷단에서는 권한 검증이 불필요하게 된다.
DDD-START (최범균님 저) 도서 참조