표현 영역과 응용 영역

사용자에게 기능을 잘 제공하려면 도메인과 사용자를 연결시킬 표현영역과 응용영역이 필요하다.

  • 표현영역은 응용서비스가 요구하는 형식으로 사용자 요청을 변환한다.
  • 응용서비스가 요구하는 객체를 생성한뒤 응용서비스의 메서드를 호출한다.
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);
}

  • 응용서비스가 복잡하다면 응용서비스에서 도메인 로직의 일부를 구현하고 있는지 확인해보자.
  • 트랜잭션은 응용서비스에서 처리한다.

도메인 로직 넣지 않기

  • 도메인로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드품질에 문제가 발생한다.
  1. 코드의 응집도가 떨어진다.
  1. 여러 응용서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
    1. 보조클래스를 만들어서 사용할수 있지만 애초에 도메인 영역에 구현하면 간단하다.

응용서비스의 구현

  • 응용서비스는 표현 영역과 도메인 영역을 연결하는 매개채 역할을 하는데 디자인 패턴에서 파사드 패턴과 같은 역할을 한다.
  • 응용 서비스는 복잡한 로직이 없기 때문에 구현이 쉽다.

응용 서비스의 크기

  • 보통 응용서비스의 크기는 다음 두가지 방법중 한가지 방식으로 구현한다.
  1. 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기.
    • 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일로직에 대한 코드 중복을 제거할수 있다.
    • 하지만 단점으로는 서비스 클래스의 크기가 커진다.
    • 코드가 커진다는 것은 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아짐을 의미하는데 이는 결과적으로 관련없는 코드가 뒤섞여서 코드를 이해하는데 방해가 될 수 있다.
  1. 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기.
    • 구분되는 기능별로 서비스 클래스를 구현하는 방식은 서비스 클래스에서 한개 내지 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));
    }
  • 도메인에서 이벤트를 발생시키면 그 이벤트를 받아서 처리할 코드가 필요한데 그 역할을 하는것이 바로 응용 서비스다.
  • 암호가 변경되면 암호가 변경됬다는 알림을 보내는 이벤트 핸들러를 등록할수 있을것이다.
  • 이벤트를 사용하면 코드가 다소 복잡해지는 대신 도메인 간의 의존성이나 외부시스템에 대한 의존을 낮춰주는 장점을 얻을 수 있다.
  • 또한 시스템을 확장하는데에 이벤트가 핵심 역할을 수행하게 된다.

표현 영역

  • 표현영역의 책임은 크게 다음과 같다.
    1. 사용자가 시스템을 사용할 수 있는 (화면) 흐름을 제공하고 제어한다.
    1. 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
    1. 사용자의 세션을 관리한다. ( 권한 검사와도 연결이 된다. )

값 검증

  • 값 검증은 표현과 응용 두곳에서 모두 할수 있다.
  • 원칙적으로는 응용서비스에서 처리한다.

  • 표현계층에서 값 검증을 할 경우
    • 스프링 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";
	}

  • 범균이형님은 굳이 응용 서비스를 만들 필요성을 못느낀다면 안만들어도 된다고 생각하신다.
  • 조회를 위한 응용 서비스가 단지 조회 전용 기능을 실행하는 코드밖에 없다면 응용 서비스를 생략한다.

+ Recent posts