표현 영역과 응용 영역
사용자에게 기능을 잘 제공하려면 도메인과 사용자를 연결시킬 표현영역과 응용영역이 필요하다.
- 표현영역은 응용서비스가 요구하는 형식으로 사용자 요청을 변환한다.
- 응용서비스가 요구하는 객체를 생성한뒤 응용서비스의 메서드를 호출한다.
public ModelAndView join(HttpServletRequest request) {
String email = request.getParameter("email");
String password = request.getParameter("password");
//사용자 요청을 응용서비스에 맞게 변환
JoinRequest joinReq = new JoinRequest(email, password);
joinService.join(joinReq);
}
- 응용서비스를 실행한뒤에 표현영역은 실행결과를 사용자에 알맞은 형식으로 응답한다.
- 응용서비스는 표현영역이 뭘하던 의존하지 않는다. 사용자가 무엇을 사용하던지 알 필요가 없다.
응용 서비스의 역할
- 응용서비스는 사용자의 요청을 처리하기 위해 리포지터리로 부터 도메인 객체를 구하고 도메인 객체를 사용한다.
- 응용서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는것이므로 표현영역에서 봤을때 응용 서비스는 도메인 영역과 표현영역을 연결해주는 창구인 파사드 역할을 한다.
public Result doSomeFunc(SomeReq req) {
//1. 리포지터리에서 애그리것을 구한다
SomeAgg agg = someAggRepository.findByid(req.getId());
checkNull(agg);
//2. 애그리것의 도메인 기능을 실행한다.
agg.doFunc(req.getValue());
// 3. 결과를 리턴한다.
return createSuccessResult(agg);
}
- 응용서비스가 복잡하다면 응용서비스에서 도메인 로직의 일부를 구현하고 있는지 확인해보자.
- 트랜잭션은 응용서비스에서 처리한다.
도메인 로직 넣지 않기
- 도메인로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드품질에 문제가 발생한다.
- 코드의 응집도가 떨어진다.
- 여러 응용서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
- 보조클래스를 만들어서 사용할수 있지만 애초에 도메인 영역에 구현하면 간단하다.
응용서비스의 구현
- 응용서비스는 표현 영역과 도메인 영역을 연결하는 매개채 역할을 하는데 디자인 패턴에서 파사드 패턴과 같은 역할을 한다.
- 응용 서비스는 복잡한 로직이 없기 때문에 구현이 쉽다.
응용 서비스의 크기
- 보통 응용서비스의 크기는 다음 두가지 방법중 한가지 방식으로 구현한다.
- 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기.
- 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일로직에 대한 코드 중복을 제거할수 있다.
- 하지만 단점으로는 서비스 클래스의 크기가 커진다.
- 코드가 커진다는 것은 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아짐을 의미하는데 이는 결과적으로 관련없는 코드가 뒤섞여서 코드를 이해하는데 방해가 될 수 있다.
- 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기.
- 구분되는 기능별로 서비스 클래스를 구현하는 방식은 서비스 클래스에서 한개 내지 2~3개의 기능을 구현한다.
- 이방식을 사용하면 클래스 갯수는 많아지지만 한 클래스에 관련 기능을 모두 구현하는것과 비교해서 코드 품질을 일정수준으로 유지하는데 도움이 된다.
- 각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드를 구현할 가능성이 있다.
- 이런 경우 별도 클래스에 로직을 구현해서 코드가 중복되는것을 방지할수 있다.
public final class MemberServiceHelper { //구현 }
- 범균이 형님은 한 클래스가 여러 역할을 갖는 것보다 각 클래스마다 구분되는 역할을 갖는것을 선호하신다. ( 2번 선호 )
응용 서비스의 인터페이스와 클래스(흥미로운 주제)
- 응용 서비스를 구현할때 논쟁이 될만한 것이 인터페이스가 필요한지 여부이다
- 인터페이스가 필요한 경우는 의존성을 분리할 때 또는 구현 클래스가 여러개일때 등등 여러가지 이유가 있지만 구현 클래스가 다수일때 인터페이스가 유용하게 쓰인다.
- 그런데 응용서비스는 보통 런타임에 이를 교체하는 경우가 거의 없을뿐만 아니라 한 응용 서비스의 구현 클래스가 두 개인 경우도 매우 드물다.
- 이런 이유로 인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고 구현 클래스에 대한 간접참조가 증가해서 전체 구조만 복잡해지는 문제가 발생한다.
- 따라서 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는것이 좋은 설계는 아니다.
- 만약 TDD를 하는 경우에는 미리 응용서비스를 구현할수 없으므로 응용서비스의 인터페이스를 작성하게 될것이다.
- 하지만 mockito와 같은 테스트 도구를 사용하면 테스트용 가짜 객체를 만들기 때문에 인터페이스가 없어도 표현영역을 테스트 할수 있다.
- 결과적으로 응용서비스에 대한 인터페이스 필요성을 약화시킨다.
메서드 파라미터와 값 리턴
파라미터
- 값 전달을 파라미터로 받을수도 별도의 데이터 클래스 dto를 만들어 전달받을 수도 있다.
- 스프링 MVC는 웹 요청 파라미터를 자바 객체로 변환해주는 기능 ( Converter ) 를 제공해주므로 파라미터가 두개 이상 존재하면 데이터 전달을 위한 별도의 클래스를 사용하는것이 편리하다.
값 리턴
- 값리턴시 편하게 애그리것을 return할수도 있지만 애그리것을 리턴할 경우 의존성이 깨진다.
- 도메인의 로직이 응용서비스에서 소비되는것이 아닌 표현영역까지 가서 소비되면 응집도를 낮추는 원인이 된다.
- 응용서비스는 표현영역에서 필요한 데이터만 return 하도록 하자.
표현영역에 의존하지 않기.
- 응용서비스의 파라미터 타입을 결정할때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다.
- 예를 들어 HttpServletRequest 나 HttpSession을 응용서비스에 파라미터로 전달하면 안된다.
- 응용서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워진다.
- 만약 httpsession을 응용 서비스까지 가져와 변경해버린다면 표현 영역에서만 사용해야되는 httpsession이 응용서비스까지 내려온것이므로 표현 영역에 대한 응집도가 꺠진다.
트랜잭션 처리
- 트랜잭션을 관리하는것은 응용 서비스의 중요한 역할이다
- 스프링에서 제공하는 @Transaction 어노테이션을 사용하자. 매우 편하다
- 이 어노테이션을 적용하면 RuntimeException이 발생하면 트랜잭션을 롤백하고 그렇지 않으면 커밋한다.
도메인 이벤트 처리
- 응용서비스는 또한 도메인 영역에서 발생시킨 이벤트를 처리하는것이 주요 역할중 하나다.
- 도메인 이벤트란 도메인에서 발생한 상태변경을 의미하며 "암호 변경됨", "주문 취소함"과 같은것이 이벤트가 될 수 있다.
public void changePassword(String oldPw, String newPw) {
if (!password.match(oldPw)) {
throw new IdPasswordNotMatchingException();
}
this.password = new Password(newPw);
Events.raise(new PasswordChangedEvent(id.getId(), newPw));
}
- 도메인에서 이벤트를 발생시키면 그 이벤트를 받아서 처리할 코드가 필요한데 그 역할을 하는것이 바로 응용 서비스다.
- 암호가 변경되면 암호가 변경됬다는 알림을 보내는 이벤트 핸들러를 등록할수 있을것이다.
- 이벤트를 사용하면 코드가 다소 복잡해지는 대신 도메인 간의 의존성이나 외부시스템에 대한 의존을 낮춰주는 장점을 얻을 수 있다.
- 또한 시스템을 확장하는데에 이벤트가 핵심 역할을 수행하게 된다.
표현 영역
- 표현영역의 책임은 크게 다음과 같다.
- 사용자가 시스템을 사용할 수 있는 (화면) 흐름을 제공하고 제어한다.
- 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
- 사용자의 세션을 관리한다. ( 권한 검사와도 연결이 된다. )
값 검증
- 값 검증은 표현과 응용 두곳에서 모두 할수 있다.
- 원칙적으로는 응용서비스에서 처리한다.
- 표현계층에서 값 검증을 할 경우
- 스프링 MVC는 에러 메시지를 보여주기 위한 용도로 Errors나 BindingResult를 사용한다.
- 그렇기에 폼에 에러메시지를 보여주기 위해 다소 번잡한 코드를 작성해야 한다.
- 응용계층에서 값 검증을 할 경우
- 각 값이 올바른지 확인할 목적으로 익셉션을 사용할때의 문제점은 사용자에게 좋지않은 경험을 제공한다는 것이다.
- 사용자는 잘못입력했을경우 어느부분에서 잘못됬는지 알고싶을것이다.
- 하지만 응용계층에서 값을 검사하는시점에 첫번째값이 잘못되어 익셉션을 발생시키면 나머지 항목에 대해서는 값을 검사하지 않게된다.
- 그래서 첫번째값의 검증만 알게되고 나머지 항목은 값이 올바른지 여부를 알수 없게되어 사용자가 여러번 입력하게 만든다.
- 결국 표현계층에서 검증해야 하는데 Spring은 Validator인터페이스를 별도로 제공하므로 위 코드를 사용하면 간결하게 줄일수 있다.
- 이렇게 표현영역에서 필수값과 값의 형식을 검사하면 실질적으로 응용서비스는 아이디 중복여부와 같은 논리적 오류만 검사하면 된다.
- 표현영역
- 필수값, 값의 형식, 범위등을 검증
- 응용 서비스
- 데이터의 존재 유무와 같은 논리적 오류를 검증한다.
- 범균이형님은 두곳에서 다 값검사를 하는것이 아닌 위처럼 구분지어서 하시는 편이라고 하신다.
- 하지만 응용서비스를 사용하는 주체가 다양하면 응용 서비스에서 반드시 파라미터로 전달받은 값이 올바른지 검사를 해야 된다고 하신다.
권한 검사
- 보안 프레임워크의 복잡도를 떠나 보통 다음의 세곳에서 권한 검사를 수행할 수 있다.
- 표현 영역
- 응용 서비스
- 도메인 영역
- 이 URL을 처리하는 컨트롤러에 웹 요청을 전달하기 전에 인증 여부를 검사해서 인증된 사용자의 웹 요청만 컨트롤러에 전달.
- 인증된 사용자가 아닐경우 로그인 화면으로 리다이렉트
- 위와 같은 접근제어를 하기 좋은 위치가 서블릿 필터다.
- 만약 URL만으로도 불가능한 경우에는 스프링 시큐리티는 AOP를 이용한 어노테이션을 이용해 검사를 할 수 있다.
@PreAuthorize("hasRole('ADMIN')")
@Transactional
public void block(String memberId) {
Member member = memberRepository.findById(new MemberId(memberId));
if (member == null) throw new NoMemberException();
member.block();
}
- 개별 도메인 단위로 검사해야하는 경우는 구현이 아래와 같이 복잡해진다. ( 게시글 삭제는 본인이 또는 관리자 역할을 가진 사용자만 할수 있는 경우의 검증 )
public void delete(String userId, Long articleId) {
Article article = articleRepository.findById(articleId);
checkArticleExistence(article);
permissionService.checkDeletePermission(userId, article);
article.markDeleted();
}
- 만약 프레임워크 이해도가 낮아 프레임 워크 확장을 원하는 수준으로 할 수 없다면 프레임워크를 사용하는 대신 도메인에 맞는 권한 검사 기능을 직접 구현하는 것이 코드 유지보수에 유리할 수 있다.
조회 전용 기능과 응용 서비스
public class OrderListService {
public List<OrderView> getOrderList(String ordererId) {
return orderViewDao.selectByOrderer(ordererId);
}
}
- 위와 같이 아주 간단한 조회기능인 경우 표현영역에서 바로 조회 전용 기능을 사용해도 된다.
@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";
}
- 범균이형님은 굳이 응용 서비스를 만들 필요성을 못느낀다면 안만들어도 된다고 생각하신다.
- 조회를 위한 응용 서비스가 단지 조회 전용 기능을 실행하는 코드밖에 없다면 응용 서비스를 생략한다.
'book > DDD start' 카테고리의 다른 글
9장 도메인 모델과 바운디드 컨텍스트 1부 (0) | 2021.06.30 |
---|---|
7장 도메인 서비스 (0) | 2021.06.22 |
5장 리포지토리 조회기능 ( JPA 중심 ) (0) | 2021.06.05 |
4장 리포지터리와 모델구현 ( JPA 중심 ) (0) | 2021.06.04 |
DDD start 3장 - 애그리것 (0) | 2021.05.22 |
Uploaded by Notion2Tistory v1.1.0