관리 메뉴

효습

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

책/도메인 주도 개발 시작하기

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

효효효효 2024. 3. 25. 22:41

6.1 표현 영역과 응용 영역

  • 도메인 영역을 잘 구현하지 않으면 사용자의 요구를 충족하는 제대로 된 소프트웨어를 만들지 못한다.
  • 도메인이 제 기능을 하려면 사용자와 도메인을 연결해 주는 매개체가 필요하다.
    • 표현 영역과 응용 영역이 이에 해당한다.
  • 표현 영역은 사용자의 요청을 해석한다.
    • 요청을 받은 표현 영역은 URI , 요청 파라미터,쿠키,헤더 등을 이용해서 사용자가 실행하고 싶은 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.
  • 실제 사용자가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스다.
    • 응용 서비스는 기능을 실행하는 데 필요한 값을 메서드 인자로 받고 실행 결과를 리턴한다.
  • 응용 서비스의 메서드가 요구하는 파라미터와 표현 영역이 사용자로부터 전달받은 데이터는 형식이 일치하지 않기 때문에 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
  • 응용 서비스를 실행한 뒤에 표현 영역은 실행 결과를 사용자에게 알맞는 형식(HTML,JSON)으로 응답한다.
  • 사용자와 상호작용은 표현 영역이 처리하기 때문에, 응용 서비스는 표현 영역에 의존하지 않는다.
    • 사용자가 웹 브라우저를 사용하는지 , REST API를 호출하는지 등등 알 필요 없다.

 

 

6.2 응용 서비스의 역할

  • 응용 서비스는 사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다.
  • 응용 서비스의 주요 역할은 도메인 객체를 사용해서 사용자의 요청을 처리하는 것이므로 표현(사용자) 입장에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해 주는 창구 역할을 한다.

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 데이터 중복 둥 데이터가 유효한지 검사한다. 
    checkValid(req);

    // 2. 애그리거트를 생성 한다.
    SomeAgg newAgg = createSome(req);

    // 3. 리포지터리에 애그리거트를 저장한다. 
    someAggRepository.save(newAgg);

    // 4. 결과를 리턴한다.
    return createSuccessResult(newAgg);
}

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

  • 응용 서비스는 트랜잭션 처리도 담당한다.
    • 도메인의 상태 변경을 트랜잭션으로 처리해야 한다. ← 안그러면 일관성이 깨짐

 

6.2.1 도메인 로직 넣지 않기

 

도메인의 핵심 로직을 서비스에 구현한다면 코드 품질에 문제가 발생한다.

  • 코드의 응집성이 떨어진다.
    • 도메인 데이터와 그 데이터를 조작하는 도메인 로직이 한 영역에 위치하지 않고 서로 다른 영역에 위치한다는 것은 도메인 로직을 파악하기 위해 여러 영역을 분석해야한다.
  • 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
    • 도메인 클래스에서 기능을 구현하고 서비스에서 도메인이 제공하는 기능을 사용하면 코드 중복을 막을 수 있다.

일부 도메인 로직이 응용 서비스에 출현하면서 발생하는 문제는 결과적으로 코드 변경을 어렵게 만든다

→ 도메인 로직을 도메인 영역에 모아서 코드 중복을 줄이고 응집도를 높여야한다.

 

 

6.3 응용 서비스의 구현

6.3.1 응용 서비스의 크기

  • 응용 서비스를 구현할 때 응용 서비스의 크기를 생각해야한다.
  • 응용 서비스는 보통 두 가지 방식 중 한 가지 방식으로 구현된다.
    • 한 응용 서비스 클래스에 도메인의 모든 기능 구현하기
    • 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
  • 한 응용 서비스 클래스에 도메인의 기능을 모두 구현한 경우
    • 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다.
    • 하지만 한 서비스 클래스의 크기(코드 줄 수)가 커진다는 것이 단점이다.
    • 코드 크기가 커지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지게 되는데 결과적으로 관련 없는 코드가 뒤섞여 코드를 이해하는 데 방해가 될 수 있다.
    • 한 클래스에 코드가 모이기 시작하면 분리하는 게 더 좋은 상황임에도 한 클래스에 억지로 끼워넣게 된다.
  • 구분되는 기능별로 서비스 클래스를 구현하는 경우
    • 한 응용 서비스에서 1~3개의 기능을 구현한다.
    • 클래스의 개수는 많아지지만 이전과 비교하서 코드 품질이 높아지고 일정 수준으로 유지하는 데 도움이 된다.
    • 각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않는다.
    • 하지만 각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드가 생길 수 있다.
      • 별도의 클래스에 로직을 구현해서 코드가 중복되는 걸 방지할 수 있다.

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

응용 서비스를 구현할 때 인터페이스가 필요한 지는 논쟁거리가 될 수 있다.

인터페이스가 필요한 상황

  • 구현 클래스가 여러 개인 경우
    • 구현 클래스가 여러 개이거나 런타임에 구현 객체를 교체해야하는 경우 인터페이스를 유용하게 사용할 수 있다. → 응용 서비스는 런타임에 교체하는 경우가 거의 없고 한 응용 서비스의 구현 클래스가 2개인 경우도 드물다

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

6.3.3 메서드 파라미터와 값 리턴

응용 서비스가 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는 데 필요한 값을 파라미터로 전달받아야 한다.

  • 각 값을 개별 파라미터로 받을 수 있고
  • 값 전달을 위한 별도 데이터 클래스를 만들어 전달받을 수 있다.
  • 스프링 MVC와 같은 웹 프레임워크는 웹 요청 파라미터를 자바 객체로 변환하는 기능을 제공하므로 전달할 요청 파라미터가 2개 이상이라면 별도 클래스를 사용하는 것이 편리하다.

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

  • 대표적으로 식별자를 리턴함
  • 애그리거트 객체를 그대로 리턴할 수도 있음
    • 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 됨 → 코드 응집도가 낮아짐
    • 응용 서비스에서 표현 영역이 필요한 데이터만 리턴하는 게 응집도를 높이는 확실한 방법이다.

6.3.4 표현 영역에 의존하지 않기

응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다는 점이다.

@Controller
@RequestMapping("/member/changePassword")
public class MemberPasswordController {

    @RequestMapping(method = RequestMethod.POST)
    public String submit(HttpServletRequest request) {
        try { // 응용 서비스가 표힌 영역에 대한 의존이 발생하면 안 됨!
            changePasswordService.changePassword(request);
        } catch (NoMemberException ex) {
            // 적절한 exception 처리 및 응답  
        }
    }

    ...
}
  • 표현 영역에 해당하는 HttpServletRequest나 HttpSession을 응용 서비스에 파라미터로 전달하면 안된다.

왜?

  • 응용 서비스만 단독으로 테스트하기가 어려워짐
  • 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야함
  • 표현 영역의 응집도가 낮아짐
  • 더 심각한 건 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있음

6.3.5 트랜잭션 처리

스프링 같은 프레임 워크가 제공하는 트랜잭션 관리 기능을 이용하면 쉽게 트랜잭션을 처리할 수 있다.

 

6.4 표현 영역

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

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

MVC 프레임 워크는 HTTP 요청 파라미터로부터 자바 객체를 생성하는 기능을 지원하므로 응용 서비스에 전달할 자바 객체를 쉽게 생성할 수 있다.

try{
   changePasswordService.changePassword(chPwdReq);
   return successView;
} catch(BadPasswordException || NoMemberException ex){
   //응용 서비스의 처리 결과를 알맞는 응답으로 변환
   errors.reject("idPasswordNotMatch");
   return formView;
   }
  • 응용 서비스의 실행 결과를 사용자에게 알맞는 형식으로 제공하는 것은 표현 영역의 몫이다.
  • 이 코드는 응용 서비스에서 예외가 발생하면 에러 코드를 설정하는데 표현 영역 뷰는 이에 맞는 알맞는 처리를 한다.

표현 영역의 다른 주된 역할을 사용자의 연결 상태인 세션을 관리하는 것이다.

웹은 쿠키나 서버 세션을 이용해서 사용자의 연결 상태를 관리한다.

 

6.5 값 검증

  • 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.
    • 원칙적으로 모든 값에 대한 검증은 응용 서비스에 처리한다.
    • 응용 서비스에서 값을 검샇는 시점에 첫 번째 값이 올바르지 않아 예외가 발생하면 나머지 항목에 대한 검사를 하지 않기 때문에 사용자에게 좋지 않은 경험을 제공한다.
    • 이런 경우, 응용 서비스에서 에러 코드를 모아 하나의 익셉션으로 발생시키는 방법도 있다.
  • 표현 영역은 응용 서비스가 ValidationErrorException을 발생시키면 익셉션에서 에러 목록을 가져와 표현 영역에서 사용할 형태로 변환한다.
  • 스프링과 같은 프레임 워크는 Validator 인터페이스를 별도로 제공한다.

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

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

필자는 응용 서비스의 완성도를 높이기 위해 응용 서비스에서 값의 검증도 하는 편이다.

 

6.6 권한 검사

개발하는 시스템마다 권한의 복잡도가 다르다.

보안 프레임 워크의 복잡도를 떠나 표현 영역 , 응용 서비스 , 도메인에서 권한 검사를 할 수 있다.

 

  • 표현 영역
    • 접근 제어를 하기 좋은 위치가 서블릿 필터(Servlet Filter)이다.
      • 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사한다.
      • 인증된 사용자면 다음 과정을 진행하고 그렇지 않으면 로그인 화면이나 에러
  • 기본적인 검사로 인증된 사용자인지 검사한다.
  • 예를 들어 인증된 사용자만 특정 URL에 접근할 수 있고 아닐 경우 로그인 화면으로 리다이렉트 한다.
  • 접근 제어를 하기 좋은 위치가 서블릿 필터(Servlet Filter)이다.
  • 권한에 대해서 동일한 방식으로 필터를 사용해서 URL별 권한 검사를 할 수 있다.
    • 스프링 시큐리티는 이와 유사한 방식으로 필터를 이용해서 인증 정보를 생성하고 웹 접근을 제어한다.

 

URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야한다.

  • 예를 들어 스프링 시큐리티는 AOP를 활용해서 애너테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다.

 

개별 도메인 객체 단위로 권한 검사를 해야할 경우 구현이 복잡해진다.

  • 관련 도메인 애그리거트를 로딩해야하므로 직접 권한 검사 로직을 구현해야하기 때문
  • 스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권란 검사 기능을 프레임 워크에 통합할 수 있다.
  • 본인이 프레임 워크에 대한 이해도가 높지 않다면 각 도메인에 맞는 권한 검사 기능을 구현하는 것이 코드 유지 보수에 유리하다.

 

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

서비스에서 단일 쿼리만 실행하고 다른 추가적인 쿼리가 없을 경우 , 응용 서비스를 구현할 필요가 없다.

어색하게 느껴질 수 있으나 응용 서비스가 사용자 요청 기능을 실행하는 데 별다른 기여를 하지 않는다면 굳이 서비스를 만들지 않아도 된다.