티스토리 뷰

 Spring boot 3.0.0 + Webflux + Spring data(JPA)로 프로젝트를 한 지 어느덧 두 달 째.. 

QueryDsl을 안 쓰고서 프로젝트를 끝나겠다는 미친 일념으로 계속 달려오다가 결국 한계를 만나고야 말았다. API에서 Parameter를 받아서 조건에 따라 Where절을 고쳐주면 되는데 이게 조건문이 많아지다보니 도저히 findBy로 조건문을 다 적어서 Case에 따라 분기치기에는 너무 비효율적이었습니다. 이걸 위해서 각 조건마다 맞는 findBy를 다 만들어주면 거의 30~40개에 가까운 인터페이스를 작성해야 할텐데 일단 이 미친짓을 할 수는 없었기에 Specification을 사용하기로 했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.Predicate;
 
public class ReviewSpec {
        
    public static Specification<Review> getBookReviewList(int bookSeq, String searchType, String searchText) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
 
            predicates.add(cb.equal(root.get("bookSeq"), challengeSeq));            
            
            if ("NICKNAME".equals(searchType)) {
                predicates.add(cb.like(root.get("user").get("nickname"), "%" + searchText + "%"));
            } else if ("EMAIL".equals(searchType)) {
                predicates.add(cb.equal(root.get("user").get("email"), searchText));
            } else if ("USERSEQ".equals(searchType)) {
                predicates.add(cb.equal(root.get("userSeq"), searchText));
            } 
            
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }
}
 
cs

 

 그래서 이렇게 해두고 Repository에 JpaSpecificationExecutor를 상속받고 findAll에 Spec을 넣어주었는데... 문제가 Return타입이 순수 테이블이 아니라 Dto라 projection을 해줘야 하는데 이 멍청한 JPA가 똑똑하게 변경을 해주지 않았습니다. 그냥 JPA Repository에선 Return 타입을 Dto로 해줬을 때는 잘 됐는데 조인되는 table에 있는 컬럼들을 제대로 들고 오지 못했습니다.

 

1
2
3
4
5
6
7
8
9
10
11
    public interface ReviewItem {
       Integer getDisplayNo();
       Integer getReviewSeq();
       String getUser_Email();
       String getUser_Nickname();
       Integer getUserSeq();
      Date getCreateDatetime();
       String getReviewContents(); 
       Integer getScore(); 
    }
cs

 

 저렇게 User_Email로 선언을 해주면 명령어로 Interface에 작성을 해줬을 때는 Join되는 테이블의 컬럼이라고 잘 인식했는데, Specification을 썼을 때는 인식을 못하더군요.  그래서 Specification에서 컬럼을 어떻게 변경할 수 없나해서 찾아보니 Specification은 컨셉 자체가 Where절만 변경을 하는거라 Join되는 테이블의 컬럼을 들고 오게 할 수 없었습니다. 그래서 이에 대해서 계속 찾아봤는데 Spring 팀에서 해당 부분에 대한 업데이트가 없다는 얘기만 계속 보이더군요. 그래서 결국에는 QueryDsl을 써야하나?라고 포기할 뻔 하다가 결국 찾아낸 게 FetchableFluentQuery 입니다.

 

JpaSpecificationExecutor 클래스를 까보면 여러 명세들이 작성되어 있는데 그 중에 맨 밑에 보면(Spring 3.0.0 기준)

 

1
2
3
4
5
6
7
8
9
10
    /**
     * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query
     * and its result type.
     *
     * @param spec – must not be null.
     * @param queryFunction – the query function defining projection, sorting, and the result type
     * @return all entities matching the given Example.
     * @since 3.0
     */
    <extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
cs

 

 findBy에 Spec + FetchableFluentQuery를 넣을 수 있는 명세가 하나 있음을 볼 수 있습니다. 거기서 queryFunction에 대한 설명을 보면 projection, sorting, result type을 지정할 수 있음을 볼 수 있고 또 paging 기능까지 추가적으로 제공하고 있습니다.

 

1
2
3
4
5
6
7
8
    public Page<ReviewItem> getBookReviewList(int bookSeq, int pageNo, int pageSize, String searchType, String searchText) {
        Page<ReviewItem> page = null;
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize);
        page = reviewRepository.findBy(ReviewSpec.getBookReviewList(bookSeq, searchType, searchText)
                , q -> q.project("user").as(ReviewItem.class).page(pageable));
        
        return page;
    }
cs

 

 그래서 이렇게 람다식으로 queryFunction 선언 후에 project로 join 테이블 컬럼을 가져오게 한 후에 Result Type을 맞춰주고 page를 통해 paging처리까지 완료 시켰습니다. 다만 저렇게 하다보니 기존 JPA에서 썼던 Join Table 컬럼을 들고오는 방식으론 Mapping이 안되어서 아래와 같이 Dto 및 Entity를 수정해줬습니다. 

 

1
2
3
4
5
6
7
8
9
10
11
   public interface ReviewItem {
       Integer getDisplayNo();
       Integer getReviewSeq();
       String getEmail(); // getUser_email -> getEmail
       String getNickname(); // getUser_nickname -> getNickname
       Integer getUserSeq();
       Date getCreateDatetime();
       String getReviewContents(); 
       Integer getScore(); 
    }
 
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    /**
     * optional false가 inner join
     */
    @OneToOne(optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "user_seq", referencedColumnName = "user_seq", insertable = false, updatable = false)
    private User user;
    
    public String getEmail() {
        return this.user.getEmail();
    }
    
    public String getNickname() {
        return this.user.getNickname();
    }
    
cs

 

 이렇게 수정을 하니 전부 다 제대로 작동했습니다. FetchableFluentQuery 같은 경우에는 예제가 구글신님한테도 없어서 혼자 상상력을 가지고 이것 저것 변경하면서 진행했는데 잘 되어서 다행이고 저와 같이 QueryDsl을 하지 않기로 한 미련한 개발자 분들이 또 있으시다면 참고하시면 좋을 것 같습니다. 

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday