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

xyzw·2024년 3월 24일
0

DDD

목록 보기
6/11
post-thumbnail

6.1 표현 영역과 응용 영역

클라이언트가 요청 파라미터를 포함한 HTTP 요청을 표현 영역에 전달하면, URL, 요청 파라미터, 쿠키, 헤더 등을 이용해서 클라이언트가 원하는 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.

클라이언트가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스이다. 응용 서비스는 기능을 실행하는 데 필요한 입력 값을 메서드 인자로 받고 실행 결과를 리턴한다.

응용 서비스의 메서드가 요구하는 파라미터와 표현 영역이 사용자로부터 전달받은 데이터는 형식이 일치하지 않기 때문에 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.

사용자와 상호작용은 표현 영역이 처리하기 때문에 응용 서비스는 표현 영역에 의존하지 않는다.

6.2 응용 서비스의 역할

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

응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 다음과 같이 단순한 형태를 갖는다.

public Result doSomeFunc(SomeReq req) {
	// 1. 리포지터리에서 애그리거트를 구한다.
    SomeAgg agg = someAggRepository.findById(req.getId());
    
    // 2. 애그리거트의 도메인 기능을 실행한다.
    agg.doFunc(req.getValue());
    
    // 3. 결과를 리턴한다.
    return createSuccessResult(agg);
}

응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높다. 응용 서비스가 도메인 로직을 일부 구현하면 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있다.

응용 서비스는 트랜잭션 처리도 담당하는데, 데이터 일관성을 유지하려면 트랜잭션 범위에서 응용 서비스를 실행해야 한다.

도메인 로직 넣지 않기

도메인 로직은 도메인 영역에 위치하고, 응용 서비스는 도메인 로직을 구현하지 않는다.

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

  1. 코드의 응집성이 떨어진다.
  2. 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.

이 두 가지 문제는 결과적으로 코드 변경을 어렵게 만든다.

소프트웨어의 가치를 높이려면 도메인 로직을 도메인 영역에 모아서 코드 중복을 줄이고 응집도를 높여야 한다.

6.3 응용 서비스의 구현

응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 하는데 이는 디자인 패턴에서 파사드와 같은 역할을 한다.

응용 서비스를 구현할 때 몇가지 고려할 사항과 트랜잭션과 같은 구현 기술의 연동에 대해 알아보자.

응용 서비스의 크기

응용 서비스는 보통 다음 두가지 방법 중 하나로 구현한다.

  1. 한 응용 서비스 클래스에 한 도메인의 모든 기능 구현하기
  2. 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기

1. 한 응용 서비스 클래스에 한 도메인의 모든 기능 구현

  • 장점

    • 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다.
  • 단점

    • 한 서비스 클래스의 크기(코드 줄 수)가 커진다.
    • 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아진다.

2. 구분되는 기능별로 서비스 클래스를 구현

한 응용 서비스 클래스에서 1~3개의 기능을 구현한다.

  • 장점

    • 코드 품질을 일정 수준으로 유지하는 데 도움이 된다.
    • 각 클래스별로 필요한 의존 객체만 포함하여 다른 기능을 구현한 코드에 영향을 받지 않는다.
  • 단점

    • 클래스 개수가 많아진다.

➡️ 구분되는 기능을 별도의 서비스 클래스로 구현하는 방식이 더 좋다.

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

구현 클래스가 다수 존재하거나 런타임에 구현 객체를 교체해야 할 때 인터페이스를 유용하게 사용할 수 있다.
그런데 응용 서비스는 런타입에 교체하는 경우가 거의 없고 한 응용 서비스의 구현 클래스가 두 개인 경우도 드물다.

그래서 인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고 구현 클래스에 대한 간접 참조가 증가해서 전체 구조가 복잡해진다.
따라서 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것이 좋은 선택이라고 볼 수 없다.

메서드 파라미터와 값 리턴

응용 서비스가 제공하는 메서드는 필요한 값을 파라미터로 전달받아야 한다.

  • 개별 파라미터로 전달받기
public void changePassword(String memberId, String currentPw, String newPw) {}
  • 값 전달을 위한 별도 데이터 클래스를 만들기
public class ChangePasswordRequest {
	private String memberId;
    private String currentPassword;
    private String newPassword;
}

응용 서비스는 파라미터로 전달받은 데이터를 사용해서 필요한 기능을 구현하면 된다.

스프링 MVC와 같은 웹 프레임워크는 웹 요청 파라미터를 자바 객체로 변환하는 기능을 제공하므로 응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는 것이 편리하다.


응용 서비스의 메서드의 결과로 필요한 데이터를 표현 영역에 리턴한다.

애그리거트 자체를 리턴하면 코딩은 편할 수 있지만 도메인의 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 된다. 이것은 기능 실행 로직을 분산시켜 코드의 응집도를 낮추는 원인이 된다.

응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다.

표현 영역에 의존하지 않기

응용 서비스의 파라미터로 표현 영역과 관련된 타입(ex. HttpServletRequest, HttpSession)을 사용하면 안 된다.

응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워지고, 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제가 생긴다. 그리고 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있다.

이러한 문제가 발생하지 않도록 하려면 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않아야 한다.

트랜잭션 처리

트랜잭션을 관리하는 것은 응용 서비스의 중요한 역할이다.

스프링은 @Transactional이 적용된 메서드가 RuntimeException을 발생시키면 트랜잭션을 롤백하고 그렇지 않으면 커밋하므로
이 규칙에 따라 코드를 작성하면 트랜잭션 처리 코드를 간결하게 유지할 수 있다.

6.4 표현 영역

표현 영역의 책임은 다음과 같다.

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

6.5 값 검증

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

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

응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 할 수도 있는데, 응용 서비스에서 필요한 값을 모두 처리하면 프레임워크가 제공하는 검증 기능을 사용할 때보다 작성할 코드가 늘어나는 불편함이 있지만 반대로 응용 서비스의 완성도가 높아지는 이점이 있다.

6.6 권한 검사

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

표현 영역

표현 영역에서 할 수 있는 기본적인 검사는 인증된 사용자인지 아닌지 검사하는 것이다. 이런 접근 제어를 하기에 좋은 위치가 서블릿 필터이다. 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사한다.
인증 여부뿐만 아니라 권한에 대해서 동일한 방식으로 필터를 사용해서 URL별 권한 검사를 할 수 있다. 스프링 시큐리티는 이와 유사한 방식으로 필터를 이용해서 인증 정보를 생성하고 웹 접근을 제어한다.

응용 서비스

URL만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야 한다.
스프링 시큐리티는 AOP를 활용해서 애너테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다.

@PreAuthorize("hasRole('ADMIN')")
public void block(String memberId) {}

도메인

개별 도메인 객체 단위로 권한 검사를 해야 하는 경우는 구현이 복잡해진다. 응용 서비스의 메서드 수준에서 권한 검사를 할 수 없기 때문에 직접 권한 검사 로직을 구현해야 한다.
스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권한 검사 기능을 프레임워크에 통합할 수도 있다.
도메인 객체 수준의 권한 검사 로직은 도메인별로 다르므로 도메인에 맞게 보안 프레임워크를 확장하려면 프레임워크에 대한 높은 이해가 필요하다.
이해도가 높지 않다면 프레임워크를 사용하는 대신 도메인에 맞는 권한 검사 기능을 직접 구현하는 것이 코드 유지 보수에 유리하다.

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

서비스에서 조회 전용 모델과 DAO 등 조회 전용 기능을 사용하면 서비스 코드가 단순히 조회 전용 기능을 호출하는 형태로 끝날 수 있다.
이 경우라면 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 전용 기능을 사용해도 문제가 없다.

0개의 댓글