검색

김도현·2023년 4월 26일

검색 기능

SBB는 질문과 답변에 대한 데이터가 계속 쌓여가는 게시판이므로 검색기능은 필수라고 할 수 있다. 검색의 대상은 질문의 제목, 질문의 내용, 질문 작성자, 답변의 내용, 답변 작성자 정도로 하면 적당할 것이다. 즉, "스프링"이라고 검색을 하면 "스프링" 이라는 문자열이 제목, 내용, 질문 작성자, 답변, 답변 작성자에 존재하는지 찾아보고 그 결과를 화면에 보여주어야 한다.

이런 조건으로 검색하려면 다음과 같은 SQL 쿼리가 실행되어야 한다.


select
    distinct q.id,
    q.author_id,
    q.content,
    q.create_date,
    q.modify_date,
    q.subject 
from question q 
left outer join site_user u1 on q.author_id=u1.id 
left outer join answer a on q.id=a.question_id 
left outer join site_user u2 on a.author_id=u2.id 
where
    q.subject like '%스프링%' 
    or q.content like '%스프링%' 
    or u1.username like '%스프링%' 
    or a.content like '%스프링%' 
    or u2.username like '%스프링%' 

위 쿼리는 "스프링" 이라는 문자열이 포함된 데이터를 question, answer, site_user 테이블을 대상으로 검색하는 쿼리이다. 그리고 question 테이블을 기준으로 answer, site_user 테이블을 아우터 조인하여 "스프링" 이라는 문자열을 검색한다. 아우터(outer) 조인 대신 이너(inner) 조인을 사용하면 합집합이 아닌 교집합으로 검색되어 결과가 누락될 수 있어 주의해야 한다. 그리고 총 3개의 테이블을 대상으로 아우터 조인하여 검색하면 중복된 결과가 나올수 있기 때문에 select 문에 distinct를 주어 중복을 제거했다.

JPA를 사용하면 위의 쿼리를 자바코드로 만들수 있다. 다음을 따라해 보자.

Specification

위의 쿼리에서 본 것과 같이 여러 테이블에서 데이터를 검색해야 할 경우에는 JPA가 제공하는 Specification 인터페이스를 사용하는 것이 편리하다. Specification을 어떻게 사용할 수 있는지 예제를 통해서 알아보자.


(... 생략 ...)
import com.mysite.sbb.answer.Answer;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
(... 생략 ...)
public class QuestionService {

    private final QuestionRepository questionRepository;

    private Specification search(String kw) {
        return new Specification<>() {
            private static final long serialVersionUID = 1L;
            @Override
            public Predicate toPredicate(Root q, CriteriaQuery query, CriteriaBuilder cb) {
                query.distinct(true);  // 중복을 제거 
                Join u1 = q.join("author", JoinType.LEFT);
                Join a = q.join("answerList", JoinType.LEFT);
                Join u2 = a.join("author", JoinType.LEFT);
                return cb.or(cb.like(q.get("subject"), "%" + kw + "%"), // 제목 
                        cb.like(q.get("content"), "%" + kw + "%"),      // 내용 
                        cb.like(u1.get("username"), "%" + kw + "%"),    // 질문 작성자 
                        cb.like(a.get("content"), "%" + kw + "%"),      // 답변 내용 
                        cb.like(u2.get("username"), "%" + kw + "%"));   // 답변 작성자 
            }
        };
    }

    (... 생략 ...)
}

추가한 search 메서드는 검색어(kw)를 입력받아 쿼리의 조인문과 where문을 생성하여 리턴하는 메서드이다. 코드를 자세히 보면 위에서 알아본 쿼리를 자바 코드로 그대로 재현한 것임을 알수 있다.

위 코드에서 사용한 변수들에 대해서 자세히 살펴보자.

  • q - Root, 즉 기준을 의미하는 Question 엔티티의 객체 (질문 제목과 내용을 검색하기 위해 필요)
  • u1 - Question 엔티티와 SiteUser 엔티티를 아우터 조인(JoinType.LEFT)하여 만든 SiteUser 엔티티의 객체. Question 엔티티와 SiteUser 엔티티는 author 속성으로 연결되어 있기 때문에 q.join("author")와 같이 조인해야 한다. (질문 작성자를 검색하기 위해 필요)
  • a - Question 엔티티와 Answer 엔티티를 아우터 조인하여 만든 Answer 엔티티의 객체. Question 엔티티와 Answer 엔티티는 answerList 속성으로 연결되어 있기 때문에 q.join("answerList")와 같이 조인해야 한다. (답변 내용을 검색하기 위해 필요)
  • u2 - 바로 위에서 작성한 a 객체와 다시 한번 SiteUser 엔티티와 아우터 조인하여 만든 SiteUser 엔티티의 객체 (답변 작성자를 검색하기 위해서 필요)

그리고 검색어(kw)가 포함되어 있는지를 like로 검색하기 위해 제목, 내용, 질문 작성자, 답변 내용, 답변 작성자 각각에 cb.like를 사용하고 최종적으로 cb.or로 OR 검색되게 하였다. 위에서 예시로 든 쿼리와 비교해 보면 코드가 어떻게 구성되었는지 쉽게 이해될 것이다.

profile
Just do it

0개의 댓글