본문 바로가기

Spring

Spring Data JPA

Spring Data JPA 공식 사이트: https://docs.spring.io/spring-data/jpa/reference/jpa.html

 

1 JPQL (JPA Query Language)

JPQL은 JPA에서 사용할 수 있는 쿼리를 의미한다. 문법은 SQL과 비슷하나, 차이점이 있다. SQL에서는 테이블이나 칼럼의 이름을 사용하는 것과 달리, JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용한다.

JPQL 쿼리의 기본 구조

 

 

2 쿼리 메서드

리포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드를 제공한다. 하지만 이러한 기본 메서드는 식별자 기반으로 생성되기 때문에 결국 별도의 메서드를 정의해서 사용하는 경우가 많다. 이때 간단한 쿼리문을 작성하기 위해 사용되는 것이 '쿼리 메서드'이다.

 

2.1 쿼리 메서드 생성

쿼리 메서드는 크게 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분한다. 'find~By', 'exists~By'와 같은 키워드로 쿼리의 주제를 정하며, 'By'는 서술어의 시작을 나타내는 구분자 역할을 한다.

서술어 부분은 검색 및 정렬 조건을 지정하는 영역이다. 기본적으로 엔티티의 속성으로 정의할 수 있고, AND나 OR를 사용해 조건을 확장하는 것도 가능하다.

// 쿼리 메서드 생성 예시
List<Student> findByLastnameAndEmail(String lastName, String email);
// 리턴 타입 + {주제 + 서술어(속성)} 구조의 메서드

 

서술어에 들어가는 엔티티의 속성식은 엔티티에서 관리하고 있는 속성(필드)만 참조할 수 있다.

 

2.2 쿼리 메서드의 주제 키워드

아래는 조회 기능을 수행하는 키워드이다.

  • find~By: 지정된 기준과 일치하는 엔티티를 검색한다. 가장 일반적으로 사용한다.
  • read~By: 'find~By'와 유사하게 레코드를 읽는 쿼리를 수행하지만, 일반적으로 데이터 '읽기'에 중점을 둘 때 사용 (수정 없음을 의미)
  • get~By: 주어진 기준에 따라 단일 엔티티를 가져온다. 'find~By'와 유사하지만 정확히 하나의 결과가 예상되는 의미를 포함한다.
  • query~By: 주어진 기준에 따라 쿼리를 실행. 'find~By'와 유사하지만, 더 복잡하거나 구체적인 쿼리 실행을 암시한다.
  • search~By: 보다 광범위한 기준에 따라 엔티티를 검색한다. 주로 free-text search와 함께 사용한다.
  • stream~By: 기준과 일치하는 엔티티의 '스트림'을 반환한다. 대규모 데이터 셋을 lazy하게 처리할 때 유용하다.

'~'으로 표시한 곳에는 도메인(엔티티)을 표현할 수 있다. 그러나 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단해 생략하기도 한다.

반환 타입은 Collection이나 Stream에 속한 하위 타입을 설정할 수 있다.

  • exists~By: 특정 데이터가 존재하는지 확인하는 키워드이다. boolean 타입을 반환한다.
  • count~By: 조회 쿼리를 수행한 후, 쿼리 결과로 나온 레코드의 개수를 반환한다.
  • delete~By, remove~By: 삭제 쿼리를 수행한다. 반환 타입이 없거나 삭제한 횟수를 반환한다.
  • ~First<number>~, ~Top<number>~: 쿼리를 통해 조회된 결괏값의 개수를 제한하는 키워드이다. 두 키워드는 동일한 동작을 수행하며, 주제와 By 사이에 위치한다. 일반적으로 이 키워드는 한 번의 동작으로 여러 건을 조회할 때 사용하며, 단 건으로 조회할 땐 <number>를 생략한다.

 

2.3 쿼리 메서드의 조건자 키워드

  • Is: 값의 일치를 조건으로 사용하는 키워드이다. 생략되는 경우가 많고, Equals와 동일한 기능을 수행한다.
  • (Is)Not: 값의 불잋리를 조건으로 사용하는 키워드이다. Is는 생략하고 Not만 사용할 수도 있다.
  • (Is)Null, (Is)NotNull: 값이 null인지 검사하는 조건자 키워드
  • (Is)True, (Is)False: boolean 타입으로 지정된 칼럼값을 확인하는 키워드
  • And, Or: 여러 조건을 묶을 때 사용
  • (Is)GreaterThan, (Is)LessThan, (Is)Between: 숫자나 datetiem 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드이다. GreaterThan, LessThan 키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산을 수행하고, 경곗값을 포함하려면 Equal 키워드를 추가하면 된다.
  • (Is)StartingWith(==StartsWith), (Is)EndingWith(==EndsWith), (Is)Containing(==Contains), (Is)Like : 칼럼값에서 일치 여부를 확인하는 키워드이다. SQL 쿼리문의 '%' 키워드와 동일한 역할을 한다. Like 키워드는 코드 수준에서 메서드를 호출하면서 전달하는 값에 %를 명시적으로 작성해야 한다.

 

 

3 정렬

쿼리 메서드를 작성한 후 OrderBy 키워드를 작성하고 정렬하고자 하는 칼럼과 차순을 설정하면 정렬이 수행된다.

// Asc : 오름차순, Desc : 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);

 

정렬 구문은 And나 Or 키워드를 사용하지 않고 우선순위를 기준으로 차례대로 작성하면 된다.

// And를 붙이지 않음
List<Product> findByNameOrderByPriceAscStockDesc(String name);

 

이렇게 쿼리 메서드의 이름에 키워드를 삽입하면 메서드의 이름이 길어지게 되고, 길어질수록 가독성이 떨어지는 문제가 생긴다. 이를 해결하기 위해 매개변수를 활용하여 정렬할 수도 있다.

List<Product> findByName(String name, Sort sort);

 

이 메서드는 키워드를 넣지 않고 Sort 객체를 활용해 매개변수로 받아들인 정렬 기준을 가지고 쿼리문을 작성하게 된다.

 

매개변수를 활용한 쿼리 메서드는 메서드 정의 단계에서 코드가 줄어드는 장점이 있지만, 호출하는 위치에서는 여전히 정렬 기준이 길어져 가독성이 떨어진다. 해당 코드는 정렬 기준을 설정하기 위한 필수 구문이기 때문에 코드를 줄이기는 어렵다. 하지만 아래 예시와 같이 Sort 부분을 하나의 메서드로 분리하여 쿼리 메서드를 호출하는 코드를 작성하는 방법도 가능하다.

@SpringBootTest
class ProductRepositoryTest
{
    
    @Autowired
    ProductRepository productRepository;
    
    @Test
    void sortingAndPagingTest() {
        .. 상단 코드 생략 ..
        System.out.println(productRepository.findByName("펜", getSort()));
    }
    
    private Sort GetSort() {
        return Sort.by(
            Order.asc("price"),
            Order.desc("stock")
        );
    }
}

 

 

4 페이징 처리

페이징 처리란 데이터베이스의 레코드를 개수로 나누어 페이지를 구분하는 것을 의미한다. JPA에서는 페이징 처리를 위해 Page와 Pageable을 사용한다.

Page<Product> findByName(String name, Pageable pageable);

위 예시와 같이 반환 타입으로 Page를 설정하고 매개변수에는 Pageable 타입의 객체를 정의한다.

메서드 매개변수 설명 비고
of(int page, int size) 페이지 번호(0부터 시작), 페이지당 데이터 개수 데이터를 정렬하지 않음
of(int page, int size, Sort) 페이지 번호, 페이지당 데이터 개수, 정렬 sort에 의해 정렬
of(int page, int size, Direction, String... properties) 페이지 번호, 페이지당 데이터 개수, 정렬 방향, 속성(칼럼) Sort.by(direction, properties)에 의해 정렬

 

 

5 @Query 어노테이션

데이터베이스에서 값을 가져올 때 앞 절에서 소개한 것처럼 메서드의 이름만으로 쿼리 메서드를 생성할 수도 있지만, @Query 어노테이션을 사용하여 직접 JPQL을 작성할 수도 있다.

만약 데이터베이스를 다른 데이터베이스로 변경할 일이 없다면 직접 해당 데이터베이스에 특화된 SQL을 작성할 수 있고, 주로 튜닝된 쿼리를 사용하고자 할 때 직접 SQL을 작성한다.

@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);

 

예시처럼 @Query 어노테이션을 사용하여 JPQL 형식의 쿼리문을 작성한다.

'?1'은 첫 번째 파라미터를 의미한다. 하지만, 이 같은 방식은 파라미터의 순서가 바뀌면 오류가 발생할 가능성이 있어 @Param  어노테이션을 사용하는 것이 좋다.

@Query("SELECT p FROM Product AS p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);

 

두 예시는 동일한 쿼리를 실행한다.

또한 @Query를 사용하면 엔티티 타입이 아니라 원하는 칼럼의 값만 추출할 수도 있다.

@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
List<Obejct[]> findByNameParam2(@Param("name") String name);

 

 

이때 메서드는 Object 배열의 리스트 형태로 반환 타입을 지정해야 한다.

 

 

6 QueryDSL (Query Domain-Specific Languag)

메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통해 해소할 수 있지만, 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있다. 이러한 이유로 개발 환경에서는 문제가 없는 것처럼 보이다가도, 만약 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견된다면 엄청난 리스크를 유발하게 된다.

이 같은 문제를 해결하기 위해 사용되는 것이 QueryDSL이다. QueryDSL은 문자열이 아닌, 코드로 쿼리로 작성할 수 있도록 해준다.

 

6.1 QueryDSL이란?

QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크이다. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있다.

 

6.2 QueryDSL의 장점

  • IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있다.
  • 문법적으로 잘못된 쿼리를 허용하지 않는다.
  • 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있다.
  • 코드로 작성하므로 가독성 및 생산성이 향상된다.
  • 도메인 타입과 프로퍼티를 안전하게 참조할 수 있다.

스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 추상 클래스를 제공한다.

 

6.3 QuerydslPredicateExecutor 인터페이스 

QuerydslPredicateExecutor<T>는 Spring Data JPA에서 제공하는 인터페이스로, QueryDSL의 predicate API를 이용하여 안전한 쿼리를 실행할 수 있도록 한다. 이 인터페이스를 리포지토리 인터페이스에 상속하여 QueryDSL predicate API를 사용할 수 있다.

QuerydslPredicateExecutor 인터페이스의 메서드는 대부분 Predicate 타입을 매개변수로 받는다.

Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스이다.

특징

  • Type-Safe Predicate: QueryDSL predicate를 사용하여 타입에 안전하게 쿼리를 작성할 수 있다.
  • Convenience Method: predicate를 기반으로 findAll, count, exists 등과 같은 쿼리를 실행하기 위한 기본적인 메서드를 제공한다. 이 메서드는 대부분 predicate 타입을 매개변수로 받는다.

사용 예

'Employee' 엔티티가 있고, QueryDSL를 리퍼지토리에 상속

public interface EmployeeRepository 
    extends JpaRepository<Employee, Long>, QuerydslPredicateExecutor<Employee> {
}

 

이제 QueryDSL Predicate와 findAll 또는 count와 같은 QuerydslPredicateExecutor에서 제공하는 메서드를 사용할 수 있다.

QEmployee employee = QEmployee.employee;
Predicate predicate = employee.salary.gt(50000).and(employee.department.eq("IT"));
Iterable<Employee> results = employeeRepository.findAll(predicate);

 

첫 번째 코드: QueryDSL이 자동으로 생성한 QEmployee 클래스를 사용하여 employee 객체를 생성한다.

 

두 번째 코드: 'employee.salary.gt(50000)'은 salary가 50,000보다 큰 Employee 엔티티를 선택하는 조건

'employee.department.eq("IT")'는 department가 'IT'인 Employee 엔티티를 선택하는 조건

'and()' 메서드는 두 가지 조건을 AND로 결합하여, 두 조건을 모두 만족하는 Employee 데이터를 찾는다.

즉, 이 Predicate는 "급여가 50,000 이상이고, 부서가 'IT'인 직원"을 필터링하는 조건을 나타낸다.

 

세 번째 코드: 'employeeRepository.findAll(predicate)'는 QuerydslPredicateExecutor 인터페이스를 통해 제공되는 메서드로, Predicate 조건에 맞는 Employee 엔티티 목록을 조회하는 역할을 한다. 이 코드는 Predicate에 정의된 조건을 만족하는 Employee 엔티티들을 데이터베이스에서 조회하여 Iterable<Employee> 타입으로 반환한다.

 

 

6.4 QuerydslRepositorySupport 추상 클래스

QuerydslRepositorySupport는 QueryDSL을 사용하여 사용자 정의 리퍼지토리 구현을 단순화하기 위해 Spring Data JPA에서 제공하는 추상 클래스이다. 이 클래스는 Spring Data 환경에서 QueryDSL로 작업할 수 있는 편리한 방법을 제공하며, 특히 복잡한 사용자 정의 쿼리를 정의할 때 유용하다.

특징

  • EntityManager Injection: 엔티티 매니저를 자동으로 주입하고 QueryDSL 쿼리의 핵심인 'JPAQueryFactory'를 빌드하는 기능을 제공한다.
  • Helper Method: QueryDSL 쿼리 생성 및 실행 프로세스를 단순화하는 메서드를 제공한다.
  • Reusable Infrastructure: 이 클래스를 상속할 때, QueryDSL 및 엔티티 매니저의 설정을 처리하지 않고도 QueryDSL 쿼리를 생성하고 실행하기 위한 유틸리티에 쉽게 액세스할 수 있다.

사용 예

'Employee'에 대한 복잡한 쿼리를 처리하기 위해 사용자 정의 리퍼지토리를 생성한다고 가정

먼저, 사용자 정의 리퍼지토리 인터페이스 생성

public interface EmployeeCustomRepository {
    List<Employee> findHighlyPaidEmployeesInDepartment(String department, double minSalary);
}

 

 

QuerydslRepositorySupport를 사용하여 구현을 제공

public class EmployeeRepositoryImpl extends QuerydslRepositorySupport implements EmployeeCustomRepository {

    public EmployeeRepositoryImpl() {
        super(Employee.class);
    }

    @Override
    public List<Employee> findHighlyPaidEmployeesInDepartment(String department, double minSalary) {
        QEmployee employee = QEmployee.employee;
        
        // JPAQuery: QueryDSL의 쿼리 객체로, 이 객체를 통해 쿼리를 빌드하고 실행
        JPAQuery<Employee> query = from(employee) // from: employee를 기준으로 쿼리 시작
            .where(employee.salary.gt(minSalary)
                   .and(employee.department.eq(department)));
        return query.fetch();
    }
}

 

 

생성자: super(Employee.class)를 호출하여 Employee 엔티티를 기반으로 동작하는 QuerydslRepositorySupport를 초기화한다. 이 클래스는 Employee 엔티티와 관련된 QueryDSL 쿼리 작업을 쉽게 할 수 있도록 기본 설정을 한다.

 

메서드: 'employee.salary.gt(minSalary)'는 직원의 급여가 minSalary보다 큰지 확인하는 조건

'employee.department.eq(department)'는 직원의 부서가 지정된 department와 같은지 확인하는 조건

'and()' 메서드를 사용하여 두 조건을 모두 만족하는 데이터를 필터

즉, "지정된 부서에서 최소 급여보다 더 높은 급여를 받는 직원들"을 찾는 조건을 의미

'query.fetch()' 메서드는 쿼리를 실행하여 결과를 리스트 형태로 반환한다. 여기서 반환되는 List<Employee>는 조건을 만족하는 직원들로 구성된다.

 

 

 

 

 

출처: 이 글의 출처는 책 '스프링 부트 핵심 가이드'를 참고하여 작성하였습니다.

https://www.yes24.com/Product/Goods/110142898

'Spring' 카테고리의 다른 글

유효성 검사와 예외 처리  (1) 2024.10.24
연관관계 매핑  (2) 2024.10.17
스프링 부트와 ORM  (5) 2024.10.03
API를 작성하는 다양한 방법  (1) 2024.09.27
스프링 부트 애플리케이션 개발하기  (0) 2024.09.27