Query Lookup Strategies

2024. 10. 20. 16:22Spring Boot/Spring Data JPA

JPA 모듈은 두 가지 방식으로 쿼리를 정의할 수 있습니다. 첫 번째는 쿼리를 문자열로 수동으로 정의하는 방식이고, 두 번째는 메서드 이름을 기반으로 파생된 쿼리를 사용하는 방식입니다.

1. 파생된 쿼리(Derived Queries)

Spring Data JPA에서는 메서드 이름을 기반으로 자동으로 쿼리를 생성하는 기능을 제공합니다. 예를 들어, findByFirstNameStartingWith 같은 메서드 이름을 정의하면 JPA는 자동으로 LIKE 연산자를 사용해 "FirstName"이 특정 문자열로 시작하는 레코드를 검색하는 쿼리를 생성합니다. 이런 파생된 쿼리에서 사용할 수 있는 여러 가지 술어(predicate)가 존재합니다. 이 술어들은 다음과 같습니다:

  • IsStartingWith, StartingWith, StartsWith: 메서드 이름의 마지막에 붙여서 해당 필드가 특정 문자열로 시작하는지를 확인하는 쿼리를 생성합니다.
  • IsEndingWith, EndingWith, EndsWith: 필드가 특정 문자열로 끝나는지를 확인하는 쿼리 생성.
  • IsNotContaining, NotContaining, NotContains: 필드가 특정 문자열을 포함하지 않는지를 확인하는 쿼리 생성.
  • IsContaining, Containing, Contains: 필드가 특정 문자열을 포함하는지를 확인하는 쿼리 생성.

이러한 파생된 쿼리에서 사용하는 아규먼트들은 쿼리의 와일드카드 문자를 포함하고 있을 경우 자동으로 이스케이프 처리됩니다. 즉, LIKE 구문에서 사용되는 _%와 같은 와일드카드 문자가 포함된 아규먼트는 해당 문자가 실제로 문자열의 일부로 취급되도록 이스케이프됩니다. 예를 들어, 검색 값에 %가 들어가 있다면, 이를 일반 문자로 인식하도록 처리하는 것입니다. 이때 이스케이프 문자는 기본적으로 \이지만, @EnableJpaRepositoriesescapeCharacter 속성을 설정하여 다른 이스케이프 문자를 지정할 수 있습니다.

2. 선언된 쿼리(Declared Queries)

메서드 이름을 기반으로 쿼리를 파생하는 것은 매우 편리한 방식이지만, 특정한 경우에는 메서드 이름이 너무 길어지거나, 사용하고자 하는 키워드를 지원하지 않는 문제가 발생할 수 있습니다. 이런 경우에는 다음과 같은 두 가지 대안이 있습니다.

  1. JPA Named Queries: JPA의 네이밍 규칙을 따르는 Named Queries를 사용하는 방법입니다. Named Queries는 엔티티 클래스에 쿼리를 미리 정의해두고 메서드에서 이를 호출하는 방식입니다. 자세한 내용은 Using JPA Named Queries 부분에서 다룰 수 있습니다.
  2. @Query 애너테이션: 메서드에 직접 @Query 애너테이션을 붙여서 SQL 또는 JPQL 쿼리를 직접 정의할 수 있습니다. 이 방식은 복잡한 쿼리나 메서드 이름으로는 표현하기 어려운 쿼리를 사용할 때 유용합니다. 예를 들어, @Query("SELECT p FROM Post p WHERE p.title LIKE %:title%")와 같은 방식으로 직접 쿼리를 작성할 수 있습니다. 이는 메서드 이름 기반의 자동 생성보다 더 세밀한 제어가 가능합니다.

이렇게 JPA에서는 파생된 쿼리선언된 쿼리 두 가지 방식을 통해 유연하게 데이터베이스 쿼리를 처리할 수 있습니다.

예시

1. 파생된 쿼리(Derived Queries)

User 테이블

CREATE TABLE User (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    email VARCHAR(255),
    status VARCHAR(50)
);

 

User 엔티티 클래스

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;
    private String status;

    // 기본 생성자와 Getter/Setter 생략
}

 

Repository 인터페이스 및 파생된 쿼리 예시

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {

    // 파생된 쿼리 메서드
    List<User> findByFirstNameStartingWith(String prefix);

    List<User> findByLastNameContaining(String substring);

    List<User> findByEmailNotContaining(String substring);
}
Hibernate가 생성하는 SQL

1. findByFirstNameStartingWith

  • findByFirstNameStartingWith: 주어진 접두사로 시작하는 firstName을 가진 유저를 찾습니다.
SELECT u.id, u.first_name, u.last_name, u.email, u.status 
FROM User u 
WHERE u.first_name LIKE ? || '%';
  • 여기서 ? || '%'는 쿼리 아규먼트 prefix로 시작하는 문자열을 의미합니다.
  • %는 SQL에서 와일드카드로 사용됩니다. 문자열 검색을 위한 LIKE 조건에서 사용되며, 이는 "아무 문자나 0개 이상"을 의미합니다.
  • 예를 들어, SQL 쿼리에서 LIKE 'prefix%'"prefix로 시작하고, 그 뒤에 어떤 문자열이 와도 상관없다"라는 의미입니다. 즉,
    prefix 로 시작하는 모든 문자열을 찾습니다.
  • 따라서 ? || '%'에서 %는 와일드카드로서 "어떤 문자열이든 뒤따를 수 있다"는 의미를 나타내고, ?는 메서드에서 전달된 아규먼트가 들어가
    는 자리입니다. prefix || '%'는 prefix로 시작하는 문자열을 찾기 위해 사용되며, 여기서 prefix는 메서드 호출 시 전달된 값입니다.
  • 예를 들어, findByFirstNameStartingWith("John")이라는 메서드가 실행되면, 이 쿼리는 LIKE 'John%'로 변환되며, "John"으로 시작하는 모
    든 first_name 값을 가진 유저를 검색합니다.

2. findByLastNameContaining

  • findByLastNameContaining: lastName에 특정 문자열을 포함하는 유저를 찾습니다.
SELECT u.id, u.first_name, u.last_name, u.email, u.status 
FROM User u 
WHERE u.last_name LIKE '%' || ? || '%';
  • '%' || ? || '%'는 SQL의 LIKE 절에서 사용되는 패턴으로, 특정 문자열을 부분적으로 포함하는 레코드를 찾기 위한 조건입니다.
  • %는 SQL에서 와일드카드로 사용되며, "임의의 문자 0개 이상"을 의미합니다.
  • ?는 Spring Data JPA에서 메서드에 전달되는 아규먼트 값을 바인딩할 자리입니다.
  • 따라서 '%' || ? || '%'의 의미는: 앞에 %, 뒤에 %가 붙어 있기 때문에, 어떤 문자열이든 그 중간에 ? 값이 포함된 문자열을 찾습니다.

findByLastNameContaining 메서드를 호출할 때 substring으로 "Smith"라는 값을 전달하면, 이 쿼리는 Hibernate에 의해 다음과 같은 SQL로 변환됩니다:

SELECT u.id, u.first_name, u.last_name, u.email, u.status 
FROM User u 
WHERE u.last_name LIKE '%' || 'Smith' || '%'

실제로는 다음과 같은 SQL로 실행됩니다:

SELECT u.id, u.first_name, u.last_name, u.email, u.status 
FROM User u 
WHERE u.last_name LIKE '%Smith%'

이 쿼리는 last_name"Smith"라는 문자열을 포함하는 모든 레코드를 반환합니다. '%' || ? || '%'는 메서드 아규먼트로 주어진 문자열을 포함하는 모든 값을 찾기 위한 패턴입니다.

요약

  • '%' || ? || '%'"임의의 위치에 주어진 문자열을 포함하는" 레코드를 찾는 조건입니다.
  • 앞과 뒤에 %가 있으므로, 해당 문자열이 문자열의 처음, 중간, 끝 어디에 있든 상관없이 검색됩니다.
  • findByEmailNotContaining: email에 특정 문자열을 포함하지 않는 유저를 찾습니다.

이처럼 파생된 쿼리는 메서드 이름을 기반으로 적절한 SQL을 자동으로 생성하고 실행합니다. 이를 통해 개발자는 SQL을 직접 작성할 필요 없이 간단하게 데이터를 조회할 수 있습니다.

 

2. 선언된 쿼리(Declared Queries)

때로는 메서드 이름으로 쿼리를 자동 생성하기 어려운 상황이 발생하거나, 복잡한 쿼리를 명시적으로 작성해야 할 경우가 있습니다. 이때는 @Query 애너테이션이나 Named Query를 사용하여 직접 쿼리를 정의할 수 있습니다.

 

@Query 애너테이션 사용 예시

import org.springframework.data.jpa.repository.Query;

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.firstName = ?1 AND u.lastName = ?2")
    List<User> findByFullName(String firstName, String lastName);
}
Hibernate가 생성하는 SQL
  • findByFullName: 주어진 firstNamelastName이 모두 일치하는 유저를 찾습니다.
SELECT u.id, u.first_name, u.last_name, u.email, u.status 
FROM User u 
WHERE u.first_name = ? AND u.last_name = ?;

이 방식에서는 JPQL을 직접 정의할 수 있으며, Hibernate가 이를 SQL로 변환하여 실행합니다.

 

Named Query 사용 예시

import jakarta.persistence.NamedQuery;

@Entity
@NamedQuery(name = "User.findByStatus", query = "SELECT u FROM User u WHERE u.status = ?1")
public class User {
    // 엔티티 필드 및 메서드 생략
}
// 리포지토리에서 네임드 쿼리 사용
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByStatus(String status);
}
Hibernate가 생성하는 SQL
  • findByStatus: 주어진 status에 해당하는 유저를 찾습니다.
SELECT u.id, u.first_name, u.last_name, u.email, u.status 
FROM User u 
WHERE u.status = ?;

        Named Query는 미리 정의된 JPQL 쿼리를 통해 SQL을 실행하는 방식으로, 복잡한 쿼리나 재사용이 필요한 쿼리에서

        유용하게 사용할 수 있습니다.

 

Spring Data JPA는 쿼리 정의의 유연성을 제공합니다. 파생된 쿼리는 메서드 이름을 기반으로 자동으로 SQL을 생성하고, 선언된 쿼리는 개발자가 직접 정의한 JPQL을 기반으로 SQL을 생성합니다. 이 과정은 Hibernate가 담당하며, 복잡한 SQL 쿼리를 자동으로 처리하여 개발자가 비즈니스 로직에 더 집중할 수 있도록 돕습니다.

  • 파생된 쿼리는 메서드 이름을 기반으로 간단한 데이터 조회를 빠르게 구현할 수 있습니다.
  • 선언된 쿼리는 복잡한 비즈니스 로직에 맞춘 세밀한 데이터 조회가 필요할 때 유용합니다.

이를 통해 Spring Data JPA와 Hibernate는 데이터베이스와 상호작용하는 과정을 자동화하고, 개발자의 생산성을 높이는 데 크게 기여합니다.

 

참고

||는 SQL에서 문자열을 연결(concatenate)하는 연산자입니다. 이 연산자는 두 개의 문자열을 하나로 결합하는 데 사용됩니다.

||의 역할

  • || 연산자는 왼쪽과 오른쪽에 있는 문자열을 연결하여 하나의 긴 문자열로 만듭니다.
  • 예를 들어, 'Hello' || ' ' || 'World''Hello World'라는 문자열을 생성합니다.

예시에서의 사용

SQL 조건에서 '%' || ? || '%'와일드카드 %와 메서드 아규먼트(?)를 연결하여, 검색 패턴을 생성하는 데 사용됩니다.

  • '%' || ? || '%':
    • 왼쪽에 % 와일드카드를 추가하고,
    • ?는 메서드 아규먼트로 전달된 값(예: substring),
    • 오른쪽에 % 와일드카드를 추가하여,

이들을 하나의 문자열로 결합합니다. 즉, '%substring%'와 같은 결과를 만들어냅니다.

SELECT u.id, u.first_name, u.last_name
FROM User u
WHERE u.last_name LIKE '%' || 'Smith' || '%'

실제로는 다음과 같이 실행됩니다:

WHERE u.last_name LIKE '%Smith%'
  • 여기서 || 연산자는 '%', 'Smith' (전달된 값), 그리고 '%'를 하나의 문자열로 결합하여 '%Smith%' 패턴을 만들어냅니다.

요약

  • ||는 SQL에서 문자열을 연결하는 연산자입니다.
  • '%' || ? || '%'는 와일드카드 %와 전달된 아규먼트 ?를 연결하여 해당 아규먼트가 포함된 문자열을 찾는 패턴을 만듭니다.

 

Query Lookup Strategies

Spring Data JPA에서 Query Lookup Strategies는 리포지토리 인터페이스에서 메서드 이름 기반으로 자동으로 쿼리를 생성하는 방식과 명시적으로 선언된 쿼리를 사용하는 방식을 결정하는 전략입니다. 이러한 전략은 XML 설정이나 EnableJpaRepositories 어노테이션을 통해 설정할 수 있습니다. 각 전략은 리포지토리 인프라에서 쿼리를 어떻게 해결할지 결정합니다.

1. CREATE 전략

CREATE 전략은 메서드 이름을 분석하여 데이터베이스에 맞는 쿼리를 자동으로 생성하는 방식입니다. 이 방법은 Spring Data JPA에서 가장 기본적인 방식으로, 메서드 이름에 포함된 키워드를 바탕으로 쿼리를 동적으로 구성합니다.

작동 방식:

  • Spring Data JPA는 find, By, And, Or와 같은 잘 알려진 접두사를 제거하고 나머지 부분을 해석하여 SQL 쿼리로 변환합니다.
  • 예를 들어, 메서드 이름이 findByLastName이라면, 이는 SQL에서 SELECT * FROM user WHERE last_name = ?와 같은 쿼리로 변환됩니다.

장점:

  • 코드를 간결하게 유지할 수 있고, 쿼리를 자동으로 생성해줍니다.

단점:

  • 복잡한 쿼리의 경우, 메서드 이름만으로는 처리하기 어렵고 쿼리의 세부 조정이 불가능할 수 있습니다.

2. USE_DECLARED_QUERY 전략

USE_DECLARED_QUERY 전략은 명시적으로 선언된 쿼리를 사용합니다. 선언된 쿼리는 어노테이션이나 XML, 혹은 네이티브 SQL 등 다양한 방식으로 정의할 수 있습니다. 이 전략은 메서드 이름을 바탕으로 쿼리를 생성하지 않고, 미리 정의된 쿼리를 찾습니다. 만약 선언된 쿼리를 찾지 못하면 예외를 발생시킵니다.

작동 방식:

  • Spring Data JPA는 메서드에 연결된 @Query 어노테이션이나 네이티브 SQL 파일에서 쿼리를 찾습니다.
  • 예를 들어, findByEmail 메서드에 대해 @Query("SELECT u FROM User u WHERE u.email = :email")가 있으면, 해당 쿼리를 사용하여 실행합니다.

장점:

  • 복잡한 쿼리나 성능 튜닝이 필요한 쿼리의 경우, 쿼리를 직접 제어할 수 있습니다.

단점:

  • 모든 쿼리를 명시적으로 작성해야 하기 때문에 코드가 다소 복잡해질 수 있습니다.

3. CREATE_IF_NOT_FOUND 전략 (기본값)

CREATE_IF_NOT_FOUND 전략은 CREATEUSE_DECLARED_QUERY를 결합한 전략입니다. Spring Data JPA는 먼저 선언된 쿼리가 있는지 확인하고, 만약 없으면 메서드 이름을 바탕으로 쿼리를 자동 생성합니다. 이는 Spring Data JPA의 기본 전략으로 설정되어 있습니다.

작동 방식:

  • 먼저, 명시된 쿼리를 찾습니다. 만약 @Query 어노테이션으로 정의된 쿼리가 있으면 그 쿼리를 사용합니다.
  • 명시된 쿼리가 없으면, 메서드 이름을 해석하여 자동으로 쿼리를 생성합니다.
  • 예를 들어, findByFirstNameAndLastName이라는 메서드가 있으면, SELECT * FROM user WHERE first_name = ? AND last_name = ?라는 쿼리를 자동으로 생성합니다.

장점:

  • 개발 초기 단계에서는 메서드 이름 기반으로 빠르게 쿼리를 생성하고, 필요에 따라 명시적인 쿼리를 추가할 수 있어 유연하게 쿼리를 관리할 수 있습니다.

단점:

  • 쿼리가 복잡해지면 메서드 이름만으로 관리하기 어려울 수 있으며, 명시된 쿼리가 없을 경우 디버깅이 어려울 수 있습니다.

XML 설정 예시

<jpa:repositories base-package="com.example.repository" query-lookup-strategy="create_if_not_found"/>

 

Java 설정 예시

@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository", queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
public class JpaConfig {
    // 추가 설정
}

요약

  • CREATE: 메서드 이름을 바탕으로 쿼리를 자동 생성.
  • USE_DECLARED_QUERY: 명시된 쿼리를 사용하고, 없으면 예외 발생.
  • CREATE_IF_NOT_FOUND: 명시된 쿼리를 먼저 찾고, 없으면 메서드 이름 기반으로 쿼리 자동 생성 (기본값).

이를 통해 Spring Data JPA는 개발자에게 유연한 쿼리 작성 방식을 제공하며, 프로젝트 요구사항에 맞게 적절한 전략을 선택할 수 있습니다.