본문 바로가기

programming/DDD

[DDD] chapter 6

반응형

6.1 표현 영역과 응용 영역

도메인이 제 기능을 하려면 사용자와 도메인을 연결해 주는 매개체가 필요하다. 응용 영역과 표현 영역이 사용자와 도메인을 연결해 주는 매개체 역할을 한다.

 

실제 사용자가 원하는 기능을 제공하는 것은 응용 역역에 위치한 서비스다. 사용자가 회원가입을 요청했다면, 실제 그 요청을 위한 기능을 제공하는 주체는 응용 서비스에 위치한다. 

 

사용자와 상호작용은 표현 영역이 처리하기 때문에, 응용 서비스는 표현 영역에 의존하지 않는다. 응용 영역은 사용자가 웹 브라우저를 사용하는지 REST API를 호출하는지, TCP 소켓을 사용하는지를 알 필요가 없다.

6.2 응용 서비스의 역할

응용 서비스의 역할

  • 사용자가 요청한 기능을 실행한다.
  • 사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다.
  • 응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 비교적 단순한 형태를 가지게 된다.

1. repository에서 애그리거트를 구한다.

2. 애그리거트의 도메인 기능을 실행한다.

3. 결과를 리턴한다.


새로운 애그리거트를 생성하는 응용서비스 역시 다음과 같이 간단하다.


1. 데이터 중복 등 데이터가 유효한지 검사한다.

2. 애그리거트를 생성한다.

3. repository aggregate를 저장한다.

4. 결과를 리턴한다.


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

 

응용 서비스는 트랜잭션 처리도 담당한다. 응용 서비스는 도메인의 상태 변경을 트랜잭션으로 처리해야한다.

 

6.2.1 도메인 로직은 넣지 않기

도메인 로직은 도메인 영역에 위치하고 응용 서비스는 도메인 로직을 구현하지 않는다. 아래는 도메인 로직과 서비스 로직이 분리된 예이다.

 

- 응용

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

 

- 도메인 계층

public class Member {
    private Password password;
    public void changePassword(String oldPw, String newPw){
        if(!matchPassword(oldPw)){
            throw new BadPasswordException();
        }
        setPassword(newPw);
    }
    public boolean matchPassword(String pwd){
        return passwordEncoder.matches(pwd);
    }
    
    private void setPassword(String newPw){
        if(isEmpty(newPw)) throw new IllegalArgumentException("no new password");
        this.password = newPw;
    }
}

 

위의 예시처럼 응용계층은 도메인을 불러오고 도메인 로직을 실행시키는 용도로만 사용해야 할 것 같다. (응용 서비스에서 암호를 잘 입력했는지에 대한 검사 로직을 구현하면 절대 안 된다.)

 

도메인 로직을 도메인 영역과 응용 서비스에 분산하여 구현하면 발생되는 문제점.

  • 코드의 응집성이 떨어짐.
    • 도메인 로직이 한 영역에 위치하지 않고 서로 다른 영역에 위치한다는 것은, 도메인 로직을 파악하기 위해 여러 영역을 분석해야 한다는 것을 의미한다.
  • 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
    • 코드 중복을 막기 위해 응용 서비스 영역에 별도의 보조 클래스를 만들 수 있지만, 애초에 도메인 영역에 암호 확인 기능을 구현한다면 응용 서비스는 해당 기능을 사용하기만 하면 된다.

일부 도메인 로직이 응용 서비스에 출현하면서 발생하는 두 가지 문제는 결과적으로 코드 변경을 어렵게 만든다. 소프트웨어가 가져야 할 중요한 경쟁 요소 중 하나는 변경 용이성인데, 변경이 어렵다는 것은 그만큼 소프트웨어의 가치가 떨어진다는 것을 의미한다.

6.3 응용 서비스의 구현

응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 한다.

6.3.1 응용 서비스의 크기

응용 서비스 자체의 구현은 어렵지 않지만, 몇가지 생각할 거리가 있다.

 

만약 회원 도메인을 가져오는 응용 서비스를 구현한다고 가정했을 때 다음과 같은 기능이 있을 것이다.


  • 회원 가입하기
  • 회원 탈퇴하기
  • 회원 암호 변경하기
  • 비밀번호 초기화하기

위의 기능을 구현하기 위해 도메인 모델을 사용하게 된다. 이 경우 응용 서비스는 보통 다음의 두 가지 방법 중 한 가지 방식으로 구현한다.


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

다음은 각각의 방법의 장단점을 정리한 것이다.

 

한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기

  • 한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로, 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다.
    • 회원지 존재하지 않는 NoMemberException 발생 코드를 한 메소드로 작성하고 클래스 내의 모든 메소드가 해당 매소드를 가져와 쓸 수 있도록 한다.
  • 한 서비스 클래스의 크기가 커진다.
    • 코드 양이 많아지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지게 되는데 결과적으로 관련 없는 코드가 뒤섞여 코드를 이해하는 데 방해가 된다.
    • 하나의 기능에만 필요한 필드를 사용할 때 추후, 해당 필드의 용도를 확인하기 위해 오랜 시간이 걸린다.
  • 한 클래스에 코드가 모이기 시작하면, 엄연히 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 억지로 끼워 넣게 된다. 이것은 코드를 점점 얽히게 만들어 코드 품질을 낮추는 결과를 초래한다.

구분되는 기능별로 응용 클래스를 따로 구현하기

  • 기능별로 서비스 클래스를 구현하는 방식은 한 응용 서비스 클래스에서 한 개 내지 2~3개의 기능을 구현하는 것이다.
    • 회원 도메인을 전부 가져오는 것이 아닌, 암호 변경 기능만을 위한 응용 클래스를 구현하는 것이 한 예이다.
  • 해당 방식은 클래스 개수는 많아지지만 한 클래스에 관련 기능을 모두 구현하는 것과 비교해서 코드 품질을 일정 수준으로 유지하는데 도움이 된다.
  • 각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드를 구현할 가능성이 있다.
    • 이 경우 여러 클래스에 중복되는 기능을 별로 클래스에 구현하고, 해당 클래스를 각 서비스마다 끌어와 사용하는 방식으로 문제를 해결할 수 있다.

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

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

 

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

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

응용 서비스가 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는 데 필요한 값을 파라미터로 전달 받아야 하는데, 이 때 별도의 도메인을 생성하면 편리하다. (이건 원래 하던 방법)

 

public class ChangePasswordRequest {
    private String memberId;
    private String currentPassword;
    private String newPassword;
}

 

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

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

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

 

응용 서비스에서 표현 영역에 대한 의존이 발생시 생기는 문제점

  • 응용 서비스만 단독으로 테스트하기가 어려워진다.
  • 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 된다.

6.3.5 트랜잭션 처리

아래의 두 가지 예시는 트랜잭션과 관련된 문제이다.

  • 회원 가입에 성공했다고 하면서 실제로 회원 정보를 DB에 삽입하지 않으면 고객은 로그인을 할 수 없다.
  • 배송지 주소를 변경하는 데 실패했다는 안내 화면을 보여줬는데 실제로는 DB에 변경된 배송지 주소가 반영되어 있다면 고객은 물건을 제대로 받지 못하게 된다.

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

public class ChangePasswordService {
    @Transactional
    public void changePassword(ChangePasswordRequest req){
        Member member = findExistiongMember(req.getMemberId());
        checkMemberExists(member);
        member.changePassword(req.getCurrentPassword(), req.getNewPassword());
    }
}

 

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

6.4 표현 영역

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

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

표현 영역의 첫 번재 책임은 사용자가 시스템을 사용할 수 있도록 알맞은 흐름을 제공하는 것이다.


 


표현 영역의 두 번째 책임은 사용자의 요청에 맞게 응용 서비스에 기능 실행을 요청하는 것이다.

 

화면을 보여주는데 필요한 데이터를 읽거나 도메인의 상태를 변경해야 할 때 응용 서비스를 사용한다. 이 과정에서 표현 영역은 사용자의 요청 데이터를 응용 서비스가 요구하는 형식으로 변환하고 응용 서비스의 결과를 사용자에게 응답할 수 있는 형식으로 변환하고 응용 서비스의 결과를 사용자에게 응답할 수 있는 형식으로 변환한다. (데이터 값 변환은 표현 서비스에서 하는구나...)

 

6.5 값 검증

원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.

 

그러나 표현 영역은 잘못된 값이 존재하면 이를 사용자에게 알려주고 값을 다시 입력받아야 한다. 사용자는 폼에 값을 입력하고 전송했는데 입력한 값이 잘못되어 다시 폼에 입력해야 할 때 한 개 항목이 아닌 입력한 모든 항목에 대해 잘못된 값이 존재하는지 알고 싶을 것이다. 그래야 한 번ㄴ에 잘못된 값을 제대로 입력할 수 있기 때문이다.

 

그러나 응용 서비스에서 값을 검사하는 시점에 첫 번째 값이 올바르지 않아 익셈션을 발생시키면 나머지 항목에 대해서는 값을 검사하지 않게 된다. 사용자는 값에 대한 에러메시지만 보게 되고 항목에 대해서는 알 수 없게 된다.

 

이런 사용자 불편을 해소하기 위해 응용 서비스에서 에러 코드를 모아 하나의 익셉션으로 발생시키는 방법도 있다.

 

	@Transactional
    public OrderNo placeOrder(OrderRequest orderRequest){
        List<ValidationError> errors = new ArrayList<>();
        if(orderRequest == null){
            errors.add(ValidationError.of("empty"));
        }else{
            if(orderRequest.getOrderMemberId() == null)
                errors.add(ValidationError.of("ordererMemberId", "empty"));
            if(orderRequest.getOrderProducts() == null)
                errors.add(ValidationError.of("orderProducts", "empty"));
        }
        if(!errors.isEmpty()) throw new ValidationErrorException(errors);
    }

 

표현 영역에서 필수 값을 검증하는 방법도 있다.

 

표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 ID 중복 여부와 같은 논리적 오류만 검사하면 된다. 즉 표현 영역과 응용 서비스가 값 검사를 나눠서 수행하는 것이다. 응용 서비스를 사용하는 표현 영역 코드가 한 곳이면 구현의 편리함을 위해 다음과 같이 역할을 나눌 수 있다. (아.. 응용 영역을 여러 표현 영역에서 사용할 수 있구나...)


 

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

6.6 권한 검사

보안 프레임워크의 종류 혹은 복잡도를 떠나 다음 세 곳에서 권한 검사를 수행할 수 있다.


  • 표현 영역
  • 응용 서비스
  • 도메인

표현 영역의 권한 검사

  • 인증된 사용자인지 아닌지 검사하는 것.
    • 회원 정보 변경 기능이 큰 예시이다.
    • 회원 정보 변경과 관련된 URl은 인증된 사용자만 점근해야 한다.
    • 회원 정보 변경을 처리하는 URL에 대한 표현 영역은 다음과 같ㅌ이 접근 제어를 할 수 있다.
      • URL을 처리하는 컨트롤러에 웹 요청을 전달하기 전에 인증 부여를 검사해서 인증된 사용자의 웹 요청만 컨트롤러에 전달한다.
      • 인증된 사용자가 아니 ㄹ경우 로그인 화면으로 리다이렉트 시킨다.
  • 접근 제어를 하기에 좋은 위치는 서블릿 필터이다. (그렇다면,,, 서블릿 필터는 표현 계층 이라는 것인가...?)
    • 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사할 수 있다.
    • 인증된 사용자면 다음 과정을 진행하고 그렇지 않으면 로그인 화면이나 에러 화면을 보여주면 된다.

응용 영역의 권한 검사

  • URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행한다.
  • 이것이 꼭 응용 서비스의 코드에서 직접 권한 검사를 해야 한다는 것을 의미하는 것은 아니다.
    • Spring security는 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();
    }
}

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

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

 

응용 서비스 없이 조회 전용 기능에 접근하는 것이 이상할 수 있지만, 응용 서비스가 사용자 요청 기능을 실행하는 데 별다른 기여를 하지 못한다면 굳이 서비스를 만들지 않아도 된다.

반응형

'programming > DDD' 카테고리의 다른 글

[DDD] chapter 8  (0) 2024.02.21
[DDD] chapter 7  (0) 2024.02.16
[DDD] chapter 5  (1) 2024.02.07
[DDD] chapter 4  (0) 2024.02.03
[DDD] chapter 3  (0) 2024.01.18