Refactoring : 동적쿼리(패턴 적용)

심규환·2022년 6월 29일
0

Refactoring

목록 보기
1/1

간단한 게시판 구현입니다. 기존에는 QueryDsl을 사용해서 간단하게 만들 수 있었습니다. 하지만 너무 QueryDsl에만 의존하는 건 좋지 않아보여서 사용하지 않고 JPA에서 지원하는 Specification 을 사용해서 구현해 봤습니다.
지금 있는 구성에서 좀 더 확장성있게 설계하기 위해 빌더 패턴, 스테이트 패턴을 사용했습니다.
패턴에 대한 이해와 내용이 부족할 수도 있습니다.

화면

간단하게 글제목, 내용, 작성자에 따라 검색 결과를 화면에 표현하는 웹입니다.

Before - PostController

먼저 PostController 입니다. PostSearchDto는 String 타입인 SearchBy와 SearchQuery가 들어있습니다.
SearchBy는 title, content, writer 중 하나가 들어오게 됩니다.
SearchQuery는 검색어가 들어가게 됩니다.

    @GetMapping(value = {"", "/{page}"})
    public String list(Model model, @PathVariable("page") Optional<Integer> page, PostSearchDto postSearchDto){
        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0,
                5, Sort.by(Sort.Direction.DESC,
                        "createdDate"));

        model.addAttribute("postSearchDto", new PostSearchDto());
        model.addAttribute("posts", service.findSearchPage(postSearchDto, pageable));
        model.addAttribute("maxPage", 5);
        return "board/board";
    }

Before - PostService

기존 PostService에서의 page 처리 메서드입니다. searchRepository의 findAllBySearchDto를 사용하여 Page<Posts> 를 전달 받고 이를 PostDto로 매핑 시켜줍니다.
좀 더 자세히 알기위해 searchRepository를 보겠습니다.

    // 기존 Page 처리
    @Transactional(readOnly = true)
    public Page<PostDto> findPage(PostSearchDto postSearchDto, Pageable pageable){
        Page<PostDto> pages = searchRepository.findAllBySearchDto(postSearchDto, pageable).map(m -> PostDto.of(m));
        return pages;
    }

Before - PostRepository

전체 코드입니다. 언뜻보기에 그렇게 가독성이 좋아보이지 않습니다. 완성된 코드도 아니었고 만족스럽지 않습니다.

@Repository
@RequiredArgsConstructor
public class PostSearchRepository {

    private final PostRepository postRepository;

    @PersistenceContext
    EntityManager em;

    public Page<Posts> findAllBySearchDto(PostSearchDto postSearchDto, Pageable pageable){
    
        if(StringUtils.hasText(postSearchDto.getSearchQuery())){
            String jpql = "select p From Posts p where "+postSearchDto.addWhereQuery() + " like :query";
           
           TypedQuery<Posts> query = getQuery(postSearchDto, pageable, jpql);
           
           Integer total = query.getResultList().size();
           
           return new PageImpl<>(query.setMaxResults(pageable.getPageSize()).getResultList(), pageable, total);
        }
        return postRepository.findAll(pageable);
    }

    private TypedQuery<Posts> getQuery(PostSearchDto postSearchDto, Pageable pageable, String jpql) {
        return em.createQuery(jpql, Posts.class)
                .setParameter("query", "%" + postSearchDto.getSearchQuery() + "%")
                .setFirstResult((int) pageable.getOffset());
    }
}

After

먼저 Specification 을 사용하기 위해 Repository에 상속을 추가했습니다.

그리고 검색 조건으로 title, content, writer 가 있는데. 만약 조건이 늘어나면 어떻게 해야할까? 는 고민을 많이 했습니다.
등록날짜 같은 것들이 추가될 수도 있지만 단순하게 글 번호 같은 것만 추가 된다고 가정했습니다.

그러면 각 조건에 맞는 Specification을 생성 해야하는데. 이러한 책임을 어디다 두는게 좋을까. 어댑터패턴을 사용하는게 좋을지 스테이트 패턴이 좋을지 고민해 봤을 때, 스테이트 패턴을 사용하는게 한 곳에 관리하기 더 좋아보여서 스테이트 패턴을 사용했습니다.

Enum 을 생성하여 각 타입에 맞게 Specification을 생성하게 했습니다.

State Pattern

public enum PostSearchType {
    TITLE{
        @Override
        public Specification<Posts> equalSearchBy(String title) {
            return ((root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("title"), "%" + title + "%"));
        }
    }, CONTENT{
        @Override
        public Specification<Posts> equalSearchBy(String content) {
            return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("content"), "%" + content+ "%");
        }
    }, WRITER{
        @Override
        public Specification<Posts> equalSearchBy(String writer) {
            return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("writer"), "%" + writer + "%");
        }
    };

    public abstract Specification<Posts> equalSearchBy(String title);
}

검색 조건이 추가됨에 따라 ENUM에 추가하여 equalSearchBy를 구현하기만 하면 됩니다.

Builder Pattern

다음은 Specification을 생성하기 위해 Builer 패턴을 적용해 봤습니다.
검색 조건에 다양성이 늘어남에 따라 이곳에 로직을 추가하면 될 것 같습니다.

주요하게 봐야할 곳은 searchBy 메서드입니다.
위에 만들었던 Enum.values 를 Stream으로 받은 뒤 전달받은 값과 동일한 '검색 조건'을 찾고 타입에 맞는 Specification을 생성합니다. 그리고 Specification 을 담은 List에 넣어줍니다.

마지막 build()를 하면 조건이 담긴 Specification<Posts> 가 반환됩니다.

public class PostSpecificationBuilder {

    public static PostSpecificationBuilder.Builder builder(){
        return new PostSpecificationBuilder.Builder();
    }

    public static class Builder{

        public static List<Specification> spec = new ArrayList<>();

        public Builder searchBy(String searchBy, String query){
            spec.add(Arrays.stream(PostSearchType.values())
                    .filter(n -> n.name().equals(searchBy.toUpperCase()))
                    .map(n -> n.equalSearchBy(query))
                    .findFirst()
                    .get());
            return this;
        }

        public Specification<Posts> build(){
            return Specification.where(spec.get(0));
        }
    }
}

Service

다음은 리팩터링 여지가 많아보이는 Service 입니다. 단순 return 한 줄로 구현되게 할 수 있을 것 같은데. 오늘은 여기까지 하고 싶어서 둘로 나눴습니다.

if문에는 검색어가 없을 때 진행되는 로직입니다. 단순히 Pageable을 repository에 넘겨줍니다.

    public Page<PostDto> findSearchPage(PostSearchDto postSearchDto, Pageable pageable){
        if(!StringUtils.hasText(postSearchDto.getSearchQuery())){
            return repository.findAll(pageable).map(m -> PostDto.of(m));
        }

        return repository.findAll(PostSpecificationBuilder
                .builder()
                .searchBy(postSearchDto.getSearchBy(),
                        postSearchDto.getSearchQuery())
                .build(), pageable)
                .map(m -> PostDto.of(m));
    }
profile
장생농씬가?

0개의 댓글