기록이 힘이다.

[도메인 주도 개발 시작하기] 6. 응용 서비스와 표현 영역 본문

IT서적/도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] 6. 응용 서비스와 표현 영역

dev22 2023. 7. 29. 19:57
728x90

표현 영역과 응용 영역

표현 영역의 코드는 다음과 같이 폼에 입력한 요청 파라미터 값을 사용해서 응용 서비스가 요구하는 객체를 생성한 뒤, 응용 서비스의 메서드를 호출한다. 

@PostMapping("/member/join")
public ModelAndView join(HttpServletRequest request){
	String email = request.getParameter("email");
    String password = request.getParameter("password");
    //사용자 요청을 응용 서비스에 맞게 변환
    JoinRequest joinReq = new JoinReqeust(email, password);
    //변환한 객체(데이터)를 이용해서 응용 서비스 실행
    joinService.join(joinReq);
    ...
}

 

사용자와의 상호작용은 표현 영역이 처리하기 때문에 응용 서비스는 표현 영역에 의존하지 않는다. 응용 영역은 사용자가 웹 브라우저를 사용하는지, REST API를 호출하는지, TCP 소켓을 사용하는지 여부를 알 필요가 없다. 단지, 응용 영역은 기능 실행에 필요한 입력 값을 전달받고 실행 결과만 리턴하면 될 뿐이다.

 

응용 서비스의 역할

응용 서비스는 사용자(클라이언트)가 요청한 기능을 실행한다. 응용 서비스는 사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다. 

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 ChangePasswordService{

	public void chagePassword(string memberId, String, oldPw, String newPw){
    	Member member = memberRepository.findById(memberId);
        checkMemberExists(member);
        member.changePassword(oldPw, newPw);
    }
}

도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다.

  • 코드의 응집성이 떨어진다는 것이다. -> 여러 영역을 분석해야 한다
  • 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.

 

응용 서비스의 구현

응용 서비스의 크기
응용 서비스는 보통 다음의 두 가지 방법 중 한 가지 방식으로 구현한다.
  • 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
    • 중복 로직이 있을경우 private method를 사용하여 중복 로직을 제거할 수 있는 장점이 있다.
  • 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기 (필자는 이 방법을 더 선호함)
    • 클래스 개수는 많아지지만 이전과 비교해서 코드 품질을 일정 수준으로 유지하는 데 도움이 된다.
    • 클래스의 기능이 분산되어 중복해서 동일한 코드를 구현할 가능성이 있다.

응용 서비스의 인터페이스와 클래스

응용 서비스를 구현할 때 논쟁이 될 만한 것이 인터페이스가 필요한지 여부이다. 인터페이스가 필요한 상황은 어느 경우 일까?
  • 구현 클래스가 다수 존재하거나 런타임에 구현 객체를 교체해야 할 경우이다.
  • 표현 영역에서 단위 테스트를 위해 응용 서비스 클래스의 가짜 객체가 필요할 경우 (Mockito를 사용할 경우엔 필요없음)

보통 런타임에 이를 교체하는 경우가 거의 없을 뿐만 아니라 한 응용 서비스의 구현 클래스가 두 개인 경우도 매우 드물다. 이런 이유로 인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고 전체 구조만 복잡해지는 문제가 발생한다. 따라서, 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것이 좋은 설계라고는 볼 수 없다

 

표현 영역에 의존하지 않기

응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안 된다는 점이다.
  • 응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워진다. 
  • 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제도 발생한다.
  • 더 나쁜 문제는 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있다는 것이다.
@Controller
@RequestMapping("/member/changePassword")
public class MemberPasswordController{
	
    @PostMapping
    public String submit(HttpServletRequest request){
    	try{
        	//응용 서비스가 표현 영역을 의존하면 안 됨!
            changePasswordService.changePassword(request);
        }catch(NoMemberException ex){
        	//알맞은 익셉션 처리 및 응답
        }
    }
}

트랜잭션 처리 

스프링과 같은 프레임 워크가 제공하는 기능을 적극 사용하는 것이 좋다. (@Transactional)

 

표현 영역

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

값 검증

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

응용 서비스에서 각 값이 존재하는지 형식이 올바른지 확인할 목적으로 익셉션을 사용할 때의 문제점은 사용자에게 좋지 않은 경험을 제공한다는 것이다. (순차적으로 필드를 검사하기 때문에 사용자 입력 값에 대한 전반적인 형식 오류 결과를 내어줄 수 없다) 이를 해결하기 위해 표현 영역에서 값을 검사하면 된다.

 

응용 서비스를 사용하는 표현 영역 코드가 한 곳이면 구현의 편리함을 위해 다음과 같은 역할을 나누어 검증을 수행할 수도 있다.

  • 표현 영역: 필수 값, 값의 형식, 범위 등을 검증한다.
  • 응용 서비스: 데이터의 존재 유무와 같은 논리적 오류를 검증한다.

필자는 응용서비스의 완성도가 높아지는 이점이 있어 응용 서비스에서 값 오류를 검증하는 편이다. 

 

권한 검사

개발할 시스템마다 권한의 복잡도가 달라진다. 다양한 상황을 충족하기 위해 스프링 시큐리티 같은 프레임워크는 유연하고 확장 가능한 구조를 갖고 있다. 이는 유연한 만큼 복잡하다는 것을 의미하기도 한다. 보안 프레임워크에 대한 이해가 부족하면 프레임워크를 무턱대고 도입하는 것보다 개발할 시스템에 맞는 권한 검사 기능을 구현하는 것이 시스템 유지보수에 유리할 수 있다.
권한 검사는 다음의 영역에서 수행할 수 있다.
  • 표현 영역
    • 인증된 사용자 여부 검사
    • 접근 제어를 하기에 좋은 위치가 서블릿 필터이다.
  • 응용 서비스
    • URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야 한다.
    • 스프링 시큐리티는 AOP를 활용해서 다음과 같이 권한 검사를 수행할 수 있다.
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 DeleteArticleService {
	public void delete(String userId, Long articleId) {
		Article article = articleRepository.findById(articleId);
		checkArticleExistence(article);
		permissionService.checkDeletePermission(userId, article);
		article.markDeleted();
	}
	...

스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권한 검사 기능을 프레임워크에 통합할 수도 있다.