DDD 조회기능, 표현영역과 응용영역

스프링 데이터 JPA 이용한 조회 기능

CQRS

  • 명령 모델과 조회 모델을 분리하는 패턴
  • 명령 모델 : 상태를 변경하는 기능을 구현할 때 사용 ex) 회원가입, 암호변경, 주문취소
  • 조회 모델 : 데이터를 조회하는 기능을 구현할 때 사용 ex) 주문목록, 주문상세

Specification

  • 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스.
  • 무분별하게 find 메서드가 늘어나는 것을 방지위해 Specification 인터페이스를 활용하자.
Specification<MemberData> spec = Specification.builder(MemberData.class)
        .ifTrue(searchRequest.isOnlyNotBlocked(),
                () -> MemberDataSpecs.nonBlocked())
        .ifHasText(searchRequest.getName(),
                name -> MemberDataSpecs.nameLike(searchRequest.getName()))
        .toSpec();
List<MemberData> result = memberDataDao.findAll(spec, PageRequest.of(0, 5));

@Subselect

@Immutable, @Subselect, @Synchronize 를 사용하면 테이블이 아닌 쿼리 결과를 @Entity로 매핑할 수 있다.

@Entity
@Immutable
@Subselect(
        query...
)
@Synchronize({"purchase_order", "order_line", "product"})
public class OrderSummary {
    @Id
    private String number;
    private long version;
    @Column(name = "orderer_id")
    private String ordererId;
    @Column(name = "orderer_name")
    private String ordererName;
    ...
}
  • @Subselect : select 쿼리를 값으로 갖는다. 하이버네이트는 이 select 쿼리의 결과를 매핑할 테이블처럼 사용한다. DBMS가 여러 테이블을 조인해서 조회한 결과를 한 테이블처럼 보여주기 위한 용도로 뷰를 사용하는 것처럼 @Subselect를 사용하면 쿼리 실행 결과를 매핑할 테이블처럼 사용한다.
  • 뷰를 수정할 수 없듯이 @Subselect로 조회한 엔티티 역시 수정할 수 없다.
  • @Immutable : 엔티티의 필드/프로퍼티가 변경되더라도 DB에 반영하지 않고 무시한다.
  • @Synchronize : 해당 엔티티와 관련된 테이블 목록을 명시한다. 엔티티를 로딩하기 전에 지정한 테이블과 관련된 변경이 발생하면 먼저 플러시를 수행한다.
Specification<OrderSummary> spec = orderDateBetween(from, to);
Pageable pageable = PageRequest.of(1, 10);
List<OrderSummary> results = orderSummaryDao.findAll(spec, pageable);
  • @Subselect를 사용해도 일반 엔티티와 같기 때문에 EntityManager#find(), JPQL, Criteria, 스펙을 사용할 수 있다는 장점이 있다.

표현 영역과 응용 영역

표현 영역은 사용자의 요청을 해석한다. URL, 요청 파라미터, 쿠키, 헤더 등을 이용해서 사용자가 실행하고 싶은 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.

응용 영역은 실제 사용자가 원하는 기능을 제공한다. 사용자가 회원 가입을 요청했다면 실제 요청에 따른 기능을 제공하는 주체이다. 응용 서비스는 필요한 입력 값을 받고 실행 결과를 리턴한다.

응용 서비스의 역할

  • 클라이언트가 요청한 기능을 실행.
  • 표현 영역 입장에서 응용서비스는 도메인 영역과 표현 영역을 연결해 주는 창구 역할.
  • 응용 서비스에서 도메인 로직을 넣지 않도록 주의하자. 도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 응집도가 떨어지고 코드 중복이 발생한다.

응용 서비스의 구현

  • 구분되는 기능별로 서비스 클래스 구현해보자. 한 응용 서비스 클래스에서 1~3개의 기능을 구현한다.
  • 클래스 개수는 많아지지만 한 클래스에 관련 기능을 모두 구현하는 것과 비교해서 코드 품질을 일정 수준으로 유지하는 데 도움이 된다. 또한 각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향받지 않는다.
// 각 응용 서비스에서 공통되는 로직을 별도 클래스로 구현
public final class MemberServiceHelper {
    public static Member findExistingMember(MemberRepository memberRepository, String memberId) {
        return memberRepository.findById(memberId)
                .orElseThrow(() -> new NoMemberException(memberId));
    }
}

// 암호 변경 기능만을 위한 클래스를 별도로 구현
public class ChangePasswordService {
    private MemberRepository memberRepository;

    public void changePassword(String memberId, String curPw, String newPw) {
        Member member = MemberServiceHelper.findExistingMember(memberRepository, memberId);
        member.changePassword(curPw, newPw);
    }
}

표현영역의 책임

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

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

  • 서비스에서 수행하는 추가적인 로직 없이 단일 쿼리만 실행하는 조회 전용이어서 트랜잭션이 필요하지 않을 때는 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 기능을 사용해도 문제 없다.
  • 응용 서비스가 사용자 요청 기능에 별다른 기여를 하지 못한다면 굳이 서비스를 만들지 않아도 된다.
Reference
  • 도메인 주도 개발 시작하기, 최범균 지음

Categories:

Updated:

Comments