프로그래머스/Spring

[Spring] JPA Specification

annovation 2025. 2. 19. 08:51

JPA Specification

JPA Specification은 Spring Data JPA에서 제공하는 도구로, 복잡한 데이터베이스 쿼리를 SQL 문자열로 작성하는 대신 Java 객체와 메서드로 표현하여 객체지향적으로 작성할 수 있게 도와줍니다. 이를 통해 코드의 가독성과 재사용성이 높아지고, 유지보수가 쉬워집니다.


특징

1. 객체지향적 쿼리 작성

JPA Specification은 SQL 대신 Java 객체를 사용하여 쿼리를 작성합니다.

  • SQL: SELECT * FROM customers WHERE name = 'John' AND age > 30;
  • 객체지향적 표현: Specification.where(nameEquals("John")).and(ageGreaterThan(30));

2. 재사용 가능한 조건

각 조건을 별도의 메서드로 정의하여 여러 곳에서 재사용할 수 있습니다. ex. "활성 고객"이라는 조건을 정의하고 필요할 때마다 쉽게 조합하여 사용

 

3. 동적 쿼리 생성

사용자 입력이나 상황에 따라 조건을 동적으로 추가하거나 제거할 수 있습니다. 이는 검색 기능이나 필터링 기능이 자주 사용되는 애플리케이션에서 매우 유용합니다.

public Specification<Question> search(String keyword, String author) {
    return (root, query, cb) -> {
        Predicate p1 = cb.like(root.get("title"), "%" + keyword + "%");
        Predicate p2 = cb.equal(root.get("author"), author);
        return cb.and(p1, p2);
    };
}

 

  • 사용자가 제목(keyword)과 작성자(author) 조건을 입력했을 때만 해당 조건으로 검색합니다.
  • 조건이 없다면 기본 데이터를 반환하거나 조건 없이 전체를 조회할 수 있습니다.

동작원리

1. Specification 인터페이스

Specification<T>는 JPA 쿼리를 작성하기 위한 인터페이스로, toPredicate 메서드를 통해 조건을 정의합니다. 이 메서드는 다음과 같은 매개변수를 사용합니다.

 

  • Root<T> : 쿼리의 루트 엔티티
  • CriteriaQuery<?> : JPA Criteria API의 쿼리 객체
  • CriteriaBuilder : 조건을 생성하는 빌더

 

2. Predicate

Predicate는 SQL 조건과 비슷한 역할을 하며, 필드 값 비교나 논리 연산(AND, OR)을 수행하는 데 사용됩니다.


예시

private Specification<Question> search(String kw) {
    return new Specification<>() {
        private static final long serialVersionUID = 1L;

        @Override
        public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb) {
            query.distinct(true);  // 중복을 제거 
            Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);
            Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);
            Join<Answer, SiteUser> 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 + "%"));   // 답변 작성자 
        }
    };
}

 

1. private Specification<Question> search(String kw)

  • 검색 키워드(kw)를 사용하여 Question 엔티티를 대상으로 동적 검색을 수행하는 Specification을 생성합니다.
  • Specification<Question> : Spring Data JPA에서 제공하는 인터페이스로, 엔티티를 대상으로 조건 기반 검색을 정의합니다.

2. new Specification<>()

  • 익명 클래스를 사용하여 Specification 인터페이스를 구현합니다.
  • toPredicate 메서드를 오버라이드하여 검색 조건을 정의합니다.

3. private static final long serialVersionUID = 1L;

  • 직렬화(Serialization)와 관련된 필드입니다. 익명 클래스가 Serializable 인터페이스를 간접적으로 구현할 때 필요합니다.
  • 직렬화는 객체를 저장하거나 전송할 때 사용되므로, 여기서는 코드 안정성을 위한 관례입니다.

4. public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb)

  • toPredicate 메서드 : Specification의 핵심 메서드로, 조건을 생성합니다.
  • Root<Question> q : 검색의 루트 엔티티를 나타냅니다. 여기서는 Question 엔티티입니다.
  • CriteriaQuery<?> query : JPA Criteria API의 쿼리 객체입니다. select, where, distinct 등의 쿼리 동작을 정의할 수 있습니다.
  • CriteriaBuilder cb : 조건(Predicate)을 생성하는 데 사용됩니다. and, or, like 등의 메서드를 제공합니다.

5. query.distinct(true);

  • 중복 제거를 위해 쿼리 결과를 distinct 처리합니다.
  • JPA에서 SQL의 SELECT DISTINCT와 동일하게 동작합니다.

6. Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);

  • Join 객체 : 엔티티 간의 관계를 정의합니다.
  • q.join("author", JoinType.LEFT) : Question 엔티티의 author 필드를 기준으로 SiteUser와 LEFT JOIN합니다.
  • LEFT JOIN은 author 필드가 없는 경우에도 결과에 포함되도록 보장합니다.

7. Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);

  • Question 엔티티와 answerList(질문에 달린 답변 리스트)를 LEFT JOIN하여 답변 데이터를 가져옵니다.

8. Join<Answer, SiteUser> u2 = a.join("author", JoinType.LEFT);

  • 답변(Answer)의 작성자(author) 필드를 기준으로 SiteUser와 LEFT JOIN합니다.

9. cb.or(...)

  • 조건을 OR 연산으로 결합합니다.
  • ex. (조건1 OR 조건2 OR 조건3)의 SQL 쿼리를 생성합니다.

10. cb.like(...)

  • SQL의 LIKE 연산자를 사용합니다.
  • ex. cb.like(q.get("subject"), "%" + kw + "%")는 제목(subject) 필드에 키워드(kw)가 포함된 데이터를 검색합니다.

11. q.get("subject"), u1.get("username") 

  • Root와 Join 객체를 통해 필드를 지정합니다.
  • ex. q.get("subject")는 Question 엔티티의 subject 필드를 나타냅니다.

출처

1. OpenAI ChatGPT (https://openai.com)

2. 점프 투 스프링 부트 책 3-13 검색 기능 추가하기 : https://wikidocs.net/162799