본문 바로가기

책리뷰/도메인 주도 개발 시작하기(DDD핵심 개념 정리부터 구현까지)

도메인 주도 설계(DDD) 스프링데이터 JPA를 이용한 조회

728x90
반응형

 

목차

    CQRS 란

    명령(Command)모델과 조회(Query) 모델을 분리하는 패턴.

    명령 모델은 상태 변경 기능 구현시 사용, 조회 모델은 데이터 조회기능 구현시 사용.

     

    ex) 명령모델 : 회원가입, 암호 변경, 주문 취소 > 상태를 변경

    조회모델 : 주문 목록, 주문상세 > 데이터 보여주는 기능

     

    도메인 모델은 명령모델로 주로 사용됨.

     


    검색을 이용한 스펙

    • 검색 조건이 고정되어 있고 단순하면 조회 기능을 만들면 된다.
    public interface OrderDataDao{
    	Optional<OrderData> findById(OrderNo id);
        List<OrderData> findByOrderer(String ordererId, Date fromDate, Date toDate);
        ...
    }
    • 다양한 검색조건을 조합해야할 때가 있음 
    • 필요한 조합마다 find메서드를 정의하는 것은 좋은 방법이 아님 
    • 다양하게 검색 조건을 조합할 때 사용하는 것이 Specification
    • Specification : 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스 

    public interface Specification<T> {
    	public boolean isSatisfiedBy(T agg);
    }
    • isSatisfiedBy() 메서드는 검사 대상 객체가 조건 충족시 true, 아니면 false 리턴 


    public class OrdererSpec implements Specification<Order> {
    	private String ordererId;
        
        public OrdererSpec(String ordererId){
        	this.ordererId = ordererId;
        }
        
        public boolean isSatisfiedBy(Order agg) {
        	return agg.getOrdererId().getMemberId().getId().equals(ordererId);
        }
    }
    • Order애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙 

     

    public class MemberOrderRepository implements OrderRepository {
        
        public List<Order> findAll(Specification<Order> spec) {
            List<Order> allOrders = findAll();
            return allOrders.stream().filter(order -> spec.isSatisfiedBy(order)).toList();
        }
    }
    • 리포지터리나 DAO는 검색 대상을 걸러내는 용도로 사용 
    • 리포지터리가 모든 애그리거트 보관 시 위와 같이 사용 가능 
    //검색 조건을 표현하는 스펙을 생성
    Specification<Order> ordererSpec = new OrdererSpec("madvirus");
    
    //리포지터리에 전달
    List<Order> orders = orderRepository.findAll(ordererSpec);
    • 특정 조건을 충족하는 애그리거트를 찾고싶을 때 사용 

     

    • 실제 스펙은 위처럼 사용하지 않음 
    • 객체를 메모리에 보관하기 어렵고, 조회성능에 심각한 문제가 발생할 수 있음 

     


    스프링 데이터 JPA를 이용한 스펙 구현 

    • 스프링 데이터 JPA에서 제공하는 인터페이스 (Specification)
    public interface Specification<T> extends Serializable {
    	@Nullable
    	Predicate toPredicate(Root<T> root, 
        	CriteriaQuery<?> query, 
        	CriteriaBuilder criteriaBuilder);
    }
    • 제네릭 타입 파라미터 T는 JPA 엔티티 타입을 의미함
    • toPredicate() 는 JPA 크리테리아 API에서 조건을 표현하는 Predicate를 생성함 

    • Entity Type이 OrderSummary
    • ordererId프로퍼티 값이 생성자로 전달받은 ordererId와 동일한지 비교하는 Predicate를 생성 
    • 24라인은 JPA 정적 메타 모델 (OrderSummary_.ordererId)

    public class OrderSummarySpecs {
        public static Specification<OrderSummary> ordererId(String ordererId) {
            return (Root<OrderSummary> root,CriteriaQuery<?> query, 
                    CriteriaBuilder cb) -> cb.equal(root.<String>get("ordererId"), ordererId);
        }
        
        public static Specification<OrderSummary> orderDataBetween(
                LocalDateTime from, LocalDateTime to) {
            return (Root<OrderSummary> root, CriteriaQuery<?> query,
                CritefiaBuilder cb) -> cb.between(root.get(OrderSummary_.orderDate), from, to);       
        }
    }
    • 스펙인터페이스는 함수형 인터페이스이므로 위와 같이 객체 생성 가능 
    Specification<OrderSummary> betweenSpec = OrderSummarySpecs.orderDateBetween(from, to);
    • 스펙 생성이 필요한 코드는 스펙 생성 기능을 제공하는 클래스를 이용해 간결하게 스펙 생성할 수 있음

     


    리포지터리/DAO에서 스펙 사용

    • findAll() : 스펙 충족하는 엔티티 검색할 때 사용
    public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    	List<OrderSummary> findAll(Specification<OrderSummary> spec);
    }
    • OrderSummary에 대한 검색 조건을 표현하는 스펙 인터페이스를 파라미터로 가짐 
    // 스펙 객체 생성
    Specification<OrderSummary> spec = new OrdererIdSpec("user1");
    // findAll() 메서드를 이용해서 검색
    List<OrderSummary> results = orderSummaryDao.findAll(spec);

    스펙조합

    • Spring Data JPA는 스펙을 조합할 수 있는 두 메서드 and, or를 제공 
    public interface Specification<T> extends Serializable {
    	 ...
         
         default Specification<T> and(@Nullable Specification<T> other) {...}
         default Specification<T> or(@Nullable Specification<T> other) {...}
         
         @Nullable
         Predicate toPredicate(Root<T> root, 
         		CriteriaQuery<?> query, 
                CreteriaBuilder criteriaBuilder);
    }
    • and() : 두 스펙을 모두 충족하는 조건
    • or() : 두 스펙 중 하나 이상 충족하는 조건
    Specification<OrderSummary> spec1 = OrderSummarySpecs.ordererId("user1");
    Specification<OrderSummary> spec2 = OrderSummarySpecs.orderDateBetween(
    	LocalDateTime.of(2022, 1, 1, 0, 0, 0),
        LocalDateTime.of(2022, 1, 2, 0, 0, 0));
    Specification<OrderSummary> spec3 = spec1.and(spec2);
    • spec1.and(spec2) 는 spec1과 spec2를 모두 충족하는 spec3을 생성 
    Specification<OrderSummary> spec = OrderSummarySpecs.ordererId("user1")
    		.and(OrderSummarySpecs.orderDateBetween(from, to));
    • 스펙마다 변수 선언하지 않고 바로 and() 사용가능 
    Specification<OrderSummary> spec = Specification.not(OrderSummarySpecs.ordererId("user1"));
    
    Specification<OrderSummary> nullableSpec = createNullableSpec(); // null일 수 있음
    Specification<OrderSummary> otherSpec = createOtherSpec();
    
    Specification<OrderSummary> spec = nullableSpec == null?otherSpec:nullableSpec.and(otherSpec);
    • not()은 조건을 반대로 적용할 때 사용
    • null여부를 검사하는 방법은 귀찮음 
    Specification<OrderSummary> spec = Specification.where(createNullableSpec()).and(createOtherSpec());
    • where()메서드는 null을 전달하면 아무조건도 생성하지 않는 스펙 개체 리턴 null이 아니면 인자로 받은 스펙 객체를 리턴

    정렬 지정하기

    • 메서드 이름에 OrderBy 사용해서 정렬기준 지정
    • Sort 인자로 전달 
    public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    	List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
    }
    • ordererId 프로퍼티값을 기준으로 검색 조건 지정
    • number 프로퍼티 값 역순으로 정렬 


    findByOrdererIdOrderByOrderDateDescNumberAsc
    • 두 개 이상 프로퍼티에 대한 정렬순서 지정시 위와 같이 사용 
    • 이름이 길어지는 단점이 있음
       
    List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
    List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);
    • Sort타입을 파라미터로 사용하면 정렬순서를 지정할 수 있다.

     

    Sort sort = Sort.by("number").ascending();
    List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);
    • "number"프로퍼티 기준 오름차순 정렬을 표현하는 sort 객체를 생성함 

    Sort sort1 = Sort.by("number").ascending();
    Sort sort2 = Sort.by("orderDate").descending();
    Sort sort = sort1.and(sort2);
    • 두 개 이상의 정렬순서 지정하고 싶으면 Sort#and() 메서드 사용 

    Sort sort = Sort.by("number").ascending().and(Sort.by("orderDate").descending());

    페이징 처리하기 

    • 스프링 데이터 JPA는 Pageable타입을 제공함
    public interface MemberDataDao extends Repository<MemberData, String> {
    	List<MemberData> findByNameLike(String name, Pageable pageable);
    }

     

    PageRequest pageReq = PageRequest.of(1, 10);
    List<MemberData> user = memberDataDao.findByNameList("사용자%",  pageReq);
    • PageRequest 첫 번째 인자는 페이지 번호, 두 번째 인자는 페이지 개수
    • 페이지 번호는 0부터 시작하므로 1은 두 번째 페이지 조회(11~20번째까지)

     

    Sort sort = Sort.by("name").descending();
    PageRequest pageReq = PageRequest.of(1, 2, sort);
    List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
    • Sort를 사용하면 정렬도 지정할 수 있음 

    • 리턴타입이 Page일 경우 JPA는 목록 쿼리와 함께 Count 쿼리도 실행해서 조건에 해당하는 데이터 개수를 구함 
    • 전체개수, 페이지 개수 등 페이지에 필요한 데이터도 함께 제공함 

    스펙 조합을 위한 스펙 빌더 클래스

    • 스펙 생성시 아래와 같이 스펙을 조합해야하는 경우 
    Specification<MemberData> spec = Specification.where(null);
    if(searchRequest.isOnlyNotBlocked()) {
    	spec = spec.and(MemberDataSpecs.nonBlocked());
    }
    if(StringUtils.hasText(searchRequest.getName())) {
    	spec = spec.and(MemberDataSpecs.nameLike(searchRequest.getName()));
    }
    List<MemberData> results = memberDataDao.findAll(spec, PageRequest.of(0, 5));
    • 스펙 빌더를 만들어 사용 할 수 있음 

    Specification<MemberData> spec = SpecBuilder.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));
    • 메서드 호출 체인으로 변수 할당을 줄임 

    public class SpecBuilder {
    	public static <T> Builder<T> build(Class<T> type) {
        	return new Builder<T>();
        }
        
        public static class Builder<T> {
        	private List<Specification<T>> specs = new ArrayList<>();
            
            public Builder<T> and(Specification<T> spec) {
            	specs.add(spec);
                return this;
            }
            
            public Builder<T> ifHasText(String str, 
            	Function<String, Specification<T>> specSupplier) {
                if(StringUtils.hasText(str)) {
                    specs.add(specSupplier.apply(str));
                }
                return this;
            }
            
            public Builder<T> ifTrue(Boolean cond, 
            	Supplier<Specification<T>> specSupplier) {
             	if(cond != null && cond.booleanValue()) {
                	specs.add(specSupplier.get());
                }
                return this;
            }
            
            public Specification<T> toSpec() {
            	Specification<T> spec = Specification.where(null);
                for(Specification<T> s : specs) {
                	spec = spec.and(s);
                }
                return spec;
            }
        }
    }

    동적 인스턴스 생성

    • JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공함

    • select절의 new 키워드 뒤에 생성할 인스턴스의 클래스이름 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정 

    • 조회 전용 모델을 만듬 > 표현영역을 통해 사용자에게 데이터를 보여주기 위해 

    하이버네이트 @Subselect 사용

    • 하이버네이트는 JPA 확장으로 @Subselect를 제공함 

    • @Immutable, @Subselect, @Synchronize는 하이버네이트 전용 애너테이션인데 이 태그를 이용해 @Entity로 매핑할 수 있음 
    • @Subselect는 조회쿼리를 값으로 가짐 > @Entity는 수정 불가능
    • @Subselect를 이용한 @Entity 매핑필드 수정시 변경내역을 반영하는 update 쿼리를 실행함 > 에러 발생(테이블이 없으므로)
    • @Immutable > 엔티티의 매핑필드/프로퍼티 변경되도 DB반영하지 않고 무시함 
    • 하이버네이트는 트랜잭션 커밋 시점에 변경 사항을 DB에 반영하므로 data변경과 조회를 한 트랜잭션에 할 경우 변경내용을 조회하지 못한다 > 이 문제 해소 하기 위해 @Synchronize 사용 (변경내역이 있으면 플러시 먼저함)
    728x90
    반응형