반응형
표현 영역과 응용 영역은 사용자와 도메인을 연결해 주는 매개체 역활을 한다.
응용(Application) 영역
- 도메인 객체를 사용해 사용자가 요청한 기능을 실행한다.
- 표현 영역에서 보았을때 응용 서비스는 도메인 영역과 표현 영역을 연결해 주는 창구인 파사드(facade) 역활을 한다.
- 도메인 로직의 일부를 구현하지 않도록 주의해야 한다.
- 주된 역활 중 하나는 트랜잭션 처리이다.
응용 서비스의 구현
- 응용 서비스 자체는 복잡한 로직을 수행하지 않기 때문에 구현은 어렵지 않다.
- 구현은 어렵지 않지만 응용 서비스의 크기는 생각해보아야 한다.
- 보통 다음의 두 가지 방법 중 한 가지 방식으로 구현한다.
한 응용 서비스 클래스에 한 도메인의 모든 기능 구현하기
- 예) MemberService, OrderService
- 장점 : 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다.
- 단점 : 클래스의 크기(코드 줄 수)가 커져 이해력을 방해할 수 있다.
구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
- 예) MemberJoinService, OrderPaymentService
- 장점 : 코드 품질을 유지하는데 도움이 되고, 다른 기능을 구현한 코드에 영향을 받지 않는다.
- 단점 : 클래스 갯수가 늘어난다.
응용 서비스에 도메인 로직 넣지 않기
- 응용 영역에 도메인 로직을 넣지 않도록 주의해야한다.
- 응용 영역에 도메인 로직을 구현하게 되면
- 서로 다른 응용 영역에 위치하게 되므로 중복 코드가 생기게 되고 코드의 응집성이 떨어진다.
- 도메인 로직을 파악하기 여러 영역을 분석해야한다.
- 이러한 문제가 발생했을때 결과적으로 코드 변경을 어렵게 만들어 용이성을 떨어트린다.
/* 응용 영역에 도메인 로직이 구현된 예시 */
// 서비스
public class ChangePasswordService {
public void changePassword(String memberId, String oldPw, String newPw) {
Member member = memberRepository.findById(memberId);
checkMember(member);
if (!passwordEncoder.matches(oldPw, member.getPassword()) {
throw new BadPasswordException();
}
member.setPassword(newPW);
}
}
// 엔티티
public class Member {
public void setPassword(String newPw) {
if (isEmpty(newPw)) throw new IllegalArgumentException("no new password");
this.password = newPw;
}
}
/* 응용 영역에 도메인 로직이 구현되지 않은 예시 */
// 서비스
public class ChangePasswordService {
public void changePassword(String memberId, String oldPw, String newPw) {
Member member = memberRepository.findById(memberId);
checkMember(member);
member.changePassword(oldPw, newPW);
}
}
// 엔티티
public class Member {
public void changePassword(String oldPw, String newPw) {
if (!matchPassword(oldPw)) throw new BadPasswordException();
setPassword(newPw);
}
public void boolean matchPassword(String pwd) {
return passwordEncoder.matches(pwd);
}
public void setPassword(String newPw) {
if (isEmpty(newPw)) throw new IllegalArgumentException("no new password");
this.password = newPw;
}
}
응용 서비스에서 애그리거트를 반환하지 않도록 주의
- 응용 서비스에서 애그리거트 자체를 리턴하면 코딩은 편할 수 있지만 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 된다.
- 이는 도메인 로직을 분산시켜 코드의 응집도를 낮추는 원인이 된다.
표현 영역에 의존하지 않기
- 응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안 된다는 점이다.
- 예를 들어, 표현 영역에 해당하는 HttpServeltRequest나 HttpSession을 응용 서비스에 파라미터로 전달하면 안 된다.
- 응용 서비스에 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트 하기가 어려워진다.
- 게다가 표현 영역이 변경되면 응용 영역도 변경되어야 하는 문제가 발생한다.
- 하지만 더 나쁜 문제는 응용 서비스가 표현 영역의 역활까지 대신하는 상황이 벌어져 표현 영역의 응집도가 깨지는 결과를 초례한다.
도메인 이벤트 처리
- 응용 서비스의 역활 중 하나는 도메인 영역에서 발생시킨 이벤트를 처리하는 것이다.
- 도메인 영역에서 상태를 변경하고 이를 알리기 위해, 또는 상태변경 후 다른 메서드를 발생시키기 위해 이벤트를 발생시킬 수 있다.
- 예를 들어, 쇼핑몰에서 사용자가 주문을 하였을때 회원의 마일리지가 적립한다고 했을때
/* 주문 시 마일리지 적립 이벤트 발생 예시 */
public class OrderService {
public void order(...) {
Order order = new Order(...);
orderRepository.save(order);
// 마일리지 적립 이벤트 발생
Events.raise(new MileageSaveEvent(order.getOrderer, order.getPayMoney));
}
}
public class MileageService {
public void mileageSave() {
Events.handle((MileageSaveEvent event) -> {
// 마일리지 적립하는 기능 구현
});
}
}
- 이벤트를 사용하면 코드가 다소 복잡해지는 대신 도메인 간의 의존성이나 외부 시스템에 대한 의존을 낮춰주는 장점이 있다.
- 또한 시스템을 확장하는 데에 이벤트가 핵심 역활을 수행하게 된다.
응용 서비스의 인터페이스와 클래스
- 응용 서비스를 구현할 때 논쟁이 될 만한 것은 인터페이스가 필요한지 여부이다.
- 인터페이스가 필요한 몇 가지 상황이 있는데 그중 하나는 구현 클래스가 여러개인 경우와
- 구현 클래스가 여러개인 경우
- 런타임에 구현 객체를 교체해야할 경우
- 그런데 응용 서비스는 보통 런타임에 이를 교체하는 경우가 거의 없을 뿐만 아니라 한 응용 서비스의 구현 클래스가 두 개인 경우도 매우 드물다.
/* 응용 서비스에 인터페이스를 구현한 예시 */
public interface ChangePasswordService {
public void changePassword(String memberId, String curPw, String newPw);
}
public class ChangePasswordServiceImpl implements ChangePasswordService {
...
}
- 하지만 파일이 많아지고 간접 참조가 많아져 전체 구조를 복잡하게 만들기 때문에 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것이 좋은 설계라고는 볼 수 없다.
트랜잭션 처리
- 어떠한 기능이 실행 되고 DB에 저장된 데이터 값이 변경된 후 오류가 생겼을때 DB의 변경된 값을 다시 되돌려야 한다.
- 이때 트랜잭션으로 관리하는데 이는 응용 서비스의 중요한 역활이다.
- 프레임워크가 제공하는 트랜잭션 기능을 적극 사용하는 것이 좋다.
- 트랜잭션을 시작하고 커밋하고 익셉션이 발생하면 롤백할 수 있다.
@Transactional
public void changePassword(ChangePasswordRequest request) {
Member member = memberRepository.findById(request.getMemberId());
member.changePassword(request.getCurrentPwd(), request.getNewPwd());
}
표현(UI) 영역
- 사용자의 요청을 해석하고 그 기능을 제공하는 응용 서비스를 실행한다.
- 응용 서비스에서 기능 실행 후 반환 값을 사용자가 원하는 형식으로 변환해서 반환한다.
표현 영역의 책임
- 사용자가 시스템을 사용할 수 있는 흐름을 제공하고 제어한다.
- 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
- 사용자의 세션을 관리한다.
값 검증 (Validation)
- 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.
- 응용 서비스에서 값 검증을 할 경우 사용자가 불편을 느낄 수 있다.
- 예를 들어 회원가입을 하려고 할때
- 폼에 값을 입력하고 전송하였는데 첫번째 항목부터 차례대로 하나씩 익셉션을 발행시키면 여러번 시도 후 회원가입을 해야한다.
- 이러한 불편함을 해소하려면 응용 서비스에 값을 전달하기 전에 표현 영역에서 검사하면 된다.
- 스프링과 같은 프레임워크는 값 검증을 위한 Validator 인터페이스를 별도로 제공한다.
- 응용 서비스에서는 아이디 중복 여부와 같은 논리적 오류만 검사하면 된다.
- 하지만 응용 서비스를 실행하는 표현 영역이 다양할땐 응용 서비스에서 반드시 파라미터로 전달받은 값이 올바른지 검사해야 한다.
- 표현 영역 : 필수 값, 값의 형식, 범위 등을 검증
- 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류 검증
권한 검사
- 권한 검사를 하기위해 스프링 시큐리티나 아파치 Shiro 같은 프레임워크는 유연하고 확장 가능하지만 유연한 만큼 복잡하다는것을 의미한다.
- 보안 프레임워크에 대한 이해가 부족하면 프레임워크를 무턱대고 도입하는 것 보다 개발할 시스템에 맞는 권한 검사를 구현하는 것이 시스템 유지보수에 유리할 수 있다.
- 보통 다음의 세 곳에서 권한 검사를 수행할 수 있다.
- 표현 영역
- 응용 서비스
- 도메인
- 표현 영역에서 할 수 있는 기본적인 검사는 인증된 사용자인지 아닌지 여부를 검사하는 것이다.
- 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사한다.
- 스프링 시큐리티는 이와 유사한 방식으로 필터를 이용해서 인증 정보를 생성하고 웹 접근을 제어한다.
- URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야 한다.
- 스프링 시큐리티는 AOP를 활용해서 다음과 같이 어노테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다.
public class BlockMemberService {
private final MemberRepository memberRepository;
@PreAuthorize("hasRole('ADMIN')")
public void block(String memberId) {
Member member = memberRepository.findById(memberId);
if (member == null) throw new NoMemberException();
member.block();
}
}
꼭 응용 서비스를 거처야 하는것은 아니다
- 별도의 조회 기능(QueryDsl)을 사용할 때, 단순히 조회 기능을 사용하는 것이고 트랜잭션 또한 필요하지 않기 때문에 굳이 응용 서비스를 거치지 않고 바로 전용 모델을 사용 할 수도 있다.
public class OrderController {
private final OrderRepositoryImpl orderRepositoryImpl;
@RequestMapping("/myorders")
public List<OrderDTO> list() {
String ordererId = SecurityContext.getAuthentication().getId();
List<OrderView> orders = orderRepositoryImpl.selectByOrderer(ordererId);
...
}
}
- 응용 서비스가 사용자 요청 기능을 실행하는데 별 다은 기여를 하지 못한다면 굳이 서비스를 만들지 않아도 된다고 생각한다.(최범균씨의 생각)
반응형
'DDD' 카테고리의 다른 글
JPA를 위한 스펙 구현 (0) | 2021.09.15 |
---|---|
도메인 영역의 주요 구성요소 (0) | 2021.09.15 |
도메인(Domain) 모델에 지켜야할것! (0) | 2021.09.15 |
도메인(Domain) 서비스란? (0) | 2021.09.15 |
애그리거트(Aggregate) 트랜잭션 관리 (0) | 2021.09.15 |