간단한 게시판 구현입니다. 기존에는 QueryDsl을 사용해서 간단하게 만들 수 있었습니다. 하지만 너무 QueryDsl에만 의존하는 건 좋지 않아보여서 사용하지 않고 JPA에서 지원하는 Specification 을 사용해서 구현해 봤습니다.
지금 있는 구성에서 좀 더 확장성있게 설계하기 위해 빌더 패턴, 스테이트 패턴을 사용했습니다.
패턴에 대한 이해와 내용이 부족할 수도 있습니다.
간단하게 글제목, 내용, 작성자에 따라 검색 결과를 화면에 표현하는 웹입니다.
먼저 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";
}
기존 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;
}
전체 코드입니다. 언뜻보기에 그렇게 가독성이 좋아보이지 않습니다. 완성된 코드도 아니었고 만족스럽지 않습니다.
@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());
}
}
먼저 Specification 을 사용하기 위해 Repository에 상속을 추가했습니다.
그리고 검색 조건으로 title, content, writer 가 있는데. 만약 조건이 늘어나면 어떻게 해야할까? 는 고민을 많이 했습니다.
등록날짜 같은 것들이 추가될 수도 있지만 단순하게 글 번호 같은 것만 추가 된다고 가정했습니다.
그러면 각 조건에 맞는 Specification을 생성 해야하는데. 이러한 책임을 어디다 두는게 좋을까. 어댑터패턴을 사용하는게 좋을지 스테이트 패턴이 좋을지 고민해 봤을 때, 스테이트 패턴을 사용하는게 한 곳에 관리하기 더 좋아보여서 스테이트 패턴을 사용했습니다.
Enum 을 생성하여 각 타입에 맞게 Specification을 생성하게 했습니다.
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를 구현하기만 하면 됩니다.
다음은 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 입니다. 단순 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));
}