4 minute read

표현 영역과 응용 영역

  • 표현 영역은 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";
    }

Categories:

Updated: