6장: 응용 서비스와 표현 영역

응용 서비스 역할

  • 응용 영역의 서비스는 사용자가 원하는 기능을 제공하고, 표현 영역은 사용자의 요청을 해석해 응용 서비스가 요구하는 형식으로 변환한다.

  • 응용 서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는 것이다.

  • 주로 도메인 객체 간의 흐름을 제어하기 때문에 다음과 같이 애그리거트를 조회해 원하는 기능을 수행하거나, 새로운 애그리거트를 생성하는 단순한 형태를 갖는다.

public Result doSomeFunc(SomeReq req) {
	// 1. 리포지터리에서 애그리거트를 구한다.
	SomeAgg agg = someAggRepository.findById(req.getId());
	checkNull(agg);
	
	// 2. 애그리거트의 도메인 기능을 실행한다.
	agg.doFunc(req.getValue());
	
	// 3. 결과를 리턴한다.
	return createSuccessResult(agg);
}
public Result doSomeCreation(CreateSomeReq req) {
	// 1. 데이터 중복 등 데이터가 유효한지 검사한다.
	validate(req);
	
	// 2. 애그리거트를 생성한다.
	SomeAgg newAgg = createSome(req);
	
	// 3. 리포지터리에 애그리거트를 저장한다.
	someAggRepository.save(newAgg);
	
	// 4. 결과를 리턴한다.
	return createSuccessResult(newAgg);
}
  • 트랜잭션 범위 내에서 로직을 처리하도록 하여 데이터의 일관성을 유지하면서 원하는 기능을 수행할 수 있도록 한다.

  • 응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높다.

  • 도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드의 응집성이 떨어지고, 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.

    • 예를 들어 비밀번호 변경을 위한 응용 서비스에서 입력된 비밀번호와 실제 비밀번호가 일치하는지 검증하는 메서드를 구현했다고 가정한다. 사용자 비활성화를 위한 응용 서비스에서도 입력된 비밀번호와 실제 비밀번호가 일치하는지 검증하는 메서드가 필요해진다면 이를 직접 다시 구현해 사용해야 할 것이다. 이러한 현상은 도메인 영역에 암호 확인 기능을 구현하면 해결된다.

public class Member {
	public void changePassword(String oldPw, String newPw) {
		if (!matchPassword(oldPw)) throw new BadPasswordException();
		setPassword(newPw);
	}
}

응용 서비스 구현

  • 여러 기능을 구현할 때 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현할 수도 있고, 구분되는 기능별로 응용 서비스 클래스를 따로 구현할 수도 있다.

  • 한 응용 서비스 클래스에 여러 기능을 구현한다면 코드 중복을 제거하기 쉽지만 코드 라인이 길어져 클래스가 거대해진다. 그리고 분리하는 것이 좋은 상황에도 기존 클래스에 계속해서 기능을 모으게 된다.

  • 구분된 기능별로 서비스 클래스를 구현하는 방식은 한 응용 서비스 클래스에서 1~3개의 기능을 구현한다. 이렇게 하면 클래스 개수는 많아지지만 코드 품질을 일정 수준으로 유지할 수 있고 반드시 필요한 객체만 의존해 사용할 수 있다.

  • 응용 서비스를 구현할 때 인터페이스를 만들고 상속하는 클래스를 구현하는 방식은 구현 클래스가 여러 개 존재하여 런타임에 교체해야 하지 않는 이상, 전체 구조를 복잡하게 할 뿐 장점이 없다.

  • 응용 서비스는 표현 영역에서 필요한 데이터만 반환하여 기능 실행 로직의 응집도를 높일 수 있다. 이 때 표현 영역과 관련된 구체적인 타입을 반환해서는 안된다. 표현 영역에 대한 의존이 생겨 테스트 수행과 코드 변경이 어려워지고 표현 영역의 응집도가 깨지기 때문이다.

표현 영역의 역할

  • 표현 영역에서는 사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공 및 제어하고, 사용자의 요청을 알맞은 응용 서비스에 전달해 사용자에게 결과를 제공해야 하고, 사용자의 세션을 관리해야 한다.

값 검증

  • 원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리하는 것이 좋다. 응용 서비스의 완성도가 높아지기 때문이다.

  • 응용 서비스에서 값을 검사하는 시점에 첫 번째 값이 올바르지 않아 익셉션을 발생시키면 나머지 항목에 대해서는 값을 검사하지 않게 된다. 이렇게 되면 사용자는 여러 값을 입력했을 때 하나의 항목에 대한 에러 메시지만 볼 수 있어 나머지 항목이 올바른지 알 수 없게 된다.

  • 이를 해결하기 위해 응용 서비스에서 에러 코드를 모아 하나의 예외로 발생시킬 수 있다. 잘못된 값이 존재하면 errors 목록에 담고 모든 검증이 끝나면 errors 목록을 담은 예외를 발생시킬 수 있다.

  • 스프링에서는 값 검증을 위한 Validator 인터페이스를 제공하기 때문에 표현 계층(컨트롤러)에서 필수 값과 타입, 범위 등을 검사할 수도 있다. 응용 서비스에서는 논리적으로 오류가 없는지 검사하면 된다.

권한 검사

  • 권한 검사는 표현 영역, 응용 서비스, 도메인에서 수행할 수 있다.

  • 표현 영역에서 할 수 있는 기본적인 검사는 인증된 사용자인지 아닌지 검사하는 것이다. 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사해 웹 요청을 응용 서비스로 전달하거나, 로그인/에러 화면으로 리다이렉트시키면 된다.

  • 응용 서비스 메서드 단위로 권한 검사를 수행하려면 Spring Security의 AOP를 활용해 권한을 검사할 수 있다.

@PreAuthorize(“hasRole(‘ADMIN’)”)
public void block(String memberId) {
	Member member = memberRepository.findById(memberId);
	if (member == null) throw new NoMemberException();
	member.block();
}
  • 게시글 삭제의 경우 글쓴이나 관리자만이 수행할 수 있는데, 이러한 경우 게시글 애그리거트를 먼저 로딩하여 현재 사용자가 게시글을 삭제할 수 있는지 직접 권한 검사 로직을 구현해야 한다.

조회 전용 기능과 응용 서비스

  • 응용 서비스에서 조회 전용 기능만을 호출하여 기능을 구현할 수 있다.

  • 이런 경우 서비스에서 수행하는 추가적인 로직이 없을뿐더러 단일 쿼리만 실행하는 조회 전용 기능이어서 트랜잭션이 필요하지도 않다. 따라서 서비스를 만들지 않고 표현 영역에서 바로 조회 전용 기능을 사용해도 된다.

public class OrderListService {

	public List<OrderView> getOrderList(String ordererId) {
		return orderViewRepository.selectByOrderer(ordererId);
	}
	// ...
}

Last updated