응용 서비스와 표현 영역
표현 영역과 응용 영역
- 표현 영역은 URL, 요청 파라미터, 쿠키, 헤더 등을 이용해서 사용자가 어떤 기능을 실행하고 싶어 하는지 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.
- 표현 영역의 코드는 다음과 같이 폼에 입력한 요청 파라미터 값을 사용해 응용 서비스가 요구하는 객체 생성 후, 응용 서비스 메서드를 호출한다.
@RequestMapping(value = "/member/join")
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);
}
도메인 로직 넣지 않기
- Member 애그리거트와 관련 리포지토리를 이용해서 다음 코드처럼 도메인 객체 간의 실행 흐름을 제어한다.
- 기존 암호를 올바르게 입력했는지는 도메인의 핵심 로직이기 때문에 응용 서비스에 해당 로직을 구현하면 안된다.
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();
}
}
}
응용 서비스의 구현
- 응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 한다.
- 파사드와 같은 역할을 한다.
public class MemberService {
private MemberRepository memberRepository;
public void join(MemberJoinRequest joinRequest){ ... }
public void changePassword(String memberId, String currentPw, String newPw)
{ ... }
public void initializePassword(String memberId) { ... }
public void leave(String memberId, String curPw) { ... }
}
- 각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드를 구현할 가능성이 있다. 이런 경우에 별도 클래스에 로직을 구현해서 코드가 중복되는것을 방지할 수 있다.
- 클래스마다 구분되는 역할을 갖는것이 좋다.
//각 응용 서비스에서 공통되는 로직은 별도 클래스로 구현
public final class MemberServiceHelper {
public static Member findExistingMember(MemberRepository repo, String memberId)
{
Member member = memberRepository.findById(memberId);
if(member == null)
throw new NoMemberException(memberId);
return member;
}
}
// 공통 로직을 제공하는 메서드를 응용 서비스에서 사용
import static com.myshop.member.application.MemberServiceHelper.*;
응용 서비스의 인터페이스와 클래스
- 인터페이스가 필요한 경우 구현 클래스가 여러개인 경우에 필요하다.
- 응용 서비스의 구현 클래스가 두 개인 경우가 드물다.
응용 서비스 인터페이스 필요성을 약화 시키는것
- Mockito 같은 테스트 도구는 클래스에 대해서도 테스트용 대역 객체를 만들 수 있다.
- 구현 클래스에 대한 간접 참조가 증가해서 전체 구조가 복잡해진다.
표현 영역에 의존하지 않기
- 응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다.
- 응용 서비스가 표현영역에 의존을 갖게 되면 테스트하기가 까다로워진다.
@Controller
@RequestMapping("/member/chagnePassword")
public class MemberPasswordController {
@RequestMapping(method = ReqeustMethod.POST)
public String submit(HttpServletReqeust request){
try{
//응용 서비스가 표현 영역에 대한 의존이 발생하면 안됨!
changePasswordService.changePassword(request);
}catch(NoMemberException ex){
//알맞은 익셉션 처리 및 응답
}
}
...
→ 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않는것이다.
메서드 파라미터와 값 리턴
- 값 전달을 위해 별도 데이터 클래스를 만들어 사용할 수 있다.
- 응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는것이 편리하다.
public class ChangePasswordReqeust {
private String memberId;
private String currentPassword;
private String newPassword;
... get 메서드 생략
}
public class ChangePasswordService {
//위에 있는 ChangePasswordRequest 를 Parameter로 넘겨준다.
public void changePassword(ChangePasswordRequest req){
Member member = findExistingMember(req.getMemberId());
member.changePassword(req.getCurrentPassword(), req.getNewPassword());
}
}
트랜잭션 처리
- 스프링 기본 동작은 @Transactional이 적용된 메서드에서 RuntimeException이 발생하면 트랜잭션을 롤백하고 그렇지 않으면 커밋한다.
어떻게 표현하는게 좋을까?
- 응용서비스에서 애그리거트 자체를 리턴하는것은 좋지 않다. 왜냐하면, 응용서비스를 받는 표현 영역에서 분산시키기 때문에 응집력을 낮추는 원인이 된다.
- 응용 서비스는 표현 영역에서 필요한 데이터만 리턴하도록 하는것이 좋다.
표현 영역에 의존하지 않기
- 다음과 같이 표현 영역에 해당하는 HttpServletRequest 혹은 HttpSession을 응용 서비스에 파라미터로 전달하면 안된다.
@Controller
@RequestMapping("/member/changePassword")
public class MemberPasswordController{
@PostMapping
public String submit(HttpServletRequest request){
try{
//응용 서비스가 표현 영역을 의존하게 해서는 안된다.
changePasswordService.changePassword(request);
}catch(NoMemberException ex){
//알맞은 익센션 처리 및 응답
}
}
}
- 응용 서비스가 표현 영역의 기술을 사용하지 않도록 해야한다.
- 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 말아야 한다.
- ???
도메인 이벤트 처리
- 도메인 영역은 상태가 변경되면 외부에 알리기 위해 이벤트를 발생 시킬 수 있다. 도메인에서 이벤트를 발생시키면 그 이벤트를 받아서 처리할 코드가 필요한데 그 역할을 하는 것이 바로 응용 서비스이다.
- 이벤트를 사용하면 장점은 도메인 간의 의존성이나 외부 시스템에 대한 의존을 낮출 수 있다.
public class InitPasswordService {
@Transafctional
public void initializePassword(String memberId){
Member member = memberRepository.findById(memberId);
checkMemberExists(member);
member.initializePassword(); //이벤트 발생하지 않음
sendNewPasswordMailToMember(member);
}
표현 영역
- 사용자가 시스템을 사용할 수 있는 화면흐름을 제공하고 제어한다.
- 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
- 사용자의 세션을 관리한다.
@RequestMapping(method=RequestMethod.POST)
public String changePassword(HttpServletRequest request, Errors errors){
//표현 영역은 사용자 요청을 응용 서비스가 요구하는 형식으로 반환한다.
String curPw = request.getParameter("curPw");
String newPw = request.getParameter("newPw");
String memberId = SecurityContext.getAuthentication().getId();
ChangePasswordReqeust chPwdReq = new ChangePasswordReqeust(memberId, curPw,
newPw);
try{
//응용 서비스를 실행
changePasswordService.changePassword(chPwdReq);
return successView;
} catch(BadPasswordException | NoMemberExceptin ex){
//응용 서비스의 처리 결과를 알맞은 응답으로 변환
errors.reject("idPasswordNotMatch");
return formView;
}
}
값 검증
- 응용 서비스에 값을 전달하기 전에 표현 영역에서 값을 검사한다.
- 같은 값 검사를 표현 영역과 응용 서비스에서 중복으로 할 필요가 없다.
- 표현 영역 : 필수 값, 값의 형식, 범위 검증
- 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류 검증
- 검증기를 사용하는 방법도 있다.
//표현 영역
@Controller
public class Controller {
@RequestMapping
public String join(JoinRequest joinReqeust, Errors erros){
new JoinReqeustValidator().validate(joinRequest, errors);
if(errors.hasErrors()) return formView;
joinService.join(joinRequest, errors);
}
권한 검사
- 다음 세곳에서 권한 검사를 수행한다.
- 표현 영역
- 인증된 사용자 인지 아닌지
- 회원 정보 변경(인증된 사용자만 접근해야한다.)
- 인증된 사용자가 아닌경우 로그인 화면으로 리다이렉트 시킨다.
- 대표적으로 사용하는것이 서블릿 필터이다.
- 만약, URL만으로 접근 제어 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야 한다.
public class BlockMemberService { private MemberRepository memberRepository; @PreAuthorize("hasRole('Admin')") public void block(String memberId){ Member member = memberRepository.findById(memberId); if(member == null) throw new NoMemberException(); member.block(); } ...
- 표현 영역
조회 전용 기능과 응용 서비스
- 서비스에서 수행하는 추가적인 로직이 없을뿐더러 조회 전용 기능이어서 트랜잭션이 필요하지 않다. 이런 경우라면 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 전용 기능을 사용해도 된다.
- 응용 서비스 사용자 요청 기능을 실행하는 데 별다른 기여를 하지 못한다면 굳이 서비스를 만들지 않아도 된다고 본다.
public class OrderController {
private OrderViewDao orderViewDao;
@RequestMapping("/myorders")
public String list(ModelMap model){
String orderId = SecurityContext.getAuthentication().getId();
List<OrderView> orders = orderViewDao.selectByOrderer(ordererId);
model.addAttribute("orders", orders);
return "order/list";
}