기존 JPA의 JPQL
- 기존에 JPA만 사용했을 때 JPQL을 작성하려면 많은 작업을 거쳐야 한다.
문자열 사이에 공백이 빠지지 않았는지 체크하고, 파라미터 바인딩이 잘못되지 않게 조심해야 하는 등 여러가지
- 만약 조건절에 파라미터로 회원명과 나이를 넘긴다고 가정해보자.
- 만약 내가 넘긴 회원명 파리미터와 값이 같고 나이 파리미터보다 나이가 많은 회원을 구하려면 아래와 같은 메소드가 생성될 것이다.
- 이런 코드가 1,2건만 있으면 잠깐 귀찮고 말지 싶지만 현실은 그렇게 쉽지 않다.
- 하지만 그런 귀찮음을 덜어주기 위해 Spring Data Jpa는
쿼리 메소드
라는 기능을 제공한다.
쿼리 메소드
- 우선 기본적으로 해당 기능은 JpaRepository에서 동작하는 아름다운 기능이다.
우리가 추가적으로 작성한 메소드의 반환형, 인자, 메소드명을 보고 그걸 토대로 그런 기능을 자동으로 만들어 준다.
- 만약 위에서 사용한 예제를 JpaRepository의 코드로 변경하면 아래와 같이 바뀔 것이다.
메소드 명명 규칙
기능 | 키워드 | 예시 | JPQL snippet |
---|---|---|---|
중복 제거 | Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
AND 연산 | And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
OR 연산 | Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
동등 비교 | Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
BETWEEN A AND B | Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
XX 미만 | LessThan | findByAgeLessThan | … where x.age < ?1 |
XX 이하 | LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
XX 초과 | GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
XX 이상 | GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
이후 | After | findByStartDateAfter | … where x.startDate > ?1 |
이전 | Before | findByStartDateBefore | … where x.startDate < ?1 |
NULL 체크 | IsNull, Null | findByAge(Is)Null | … where x.age is null |
NOT NULL 체크 | IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
LIKE ‘%문자열%’ | Like | findByFirstnameLike | … where x.firstname like ?1 |
NOT LIKE ‘%문자열%’ | NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
LIKE ‘문자열%’ | StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
LIKE ‘%문자열’ | EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
LIKE ‘%문자열%’ | Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
정렬 | OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
NOT 연산 | Not | findByLastnameNot | … where x.lastname <> ?1 |
IN (A, B, C, …) | In | findByAgeIn(Collection | … where x.age in ?1 |
NOT IN (A, B, C, …) | NotIn | findByAgeNotIn(Collection | … where x.age not in ?1 |
true인지 확인 | True | findByActiveTrue() | … where x.active = true |
false인지 확인 | False | findByActiveFalse() | … where x.active = false |
대·소문자 구분 무시 | IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
- Like와 Containing의 차이
- Like : 위치 지정자 사용 가능
- Containing : 위치 지정자도 단순한 문자로 인식한다.
NamedQuery
- Spring Data Jpa에서의 NamedQuery는 기존의 JPA만 사용하는 방식보다 작성하는 코드 수가 매우 적다.
기존의 JPA만 사용하는 방식
Spring Data Jpa를 사용하는 방식
JpaRepository에 제네릭으로 선언한 엔티티 클래스를 통해 @Query 어노테이션도 생략할 수 있다.
엔티티 클래스
+. 연산자
+메소드명
으로 사용하면 된다.
Spring Data Jpa를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
대신 @Query 를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
JpaRepository의 메소드에 JPQL 쿼리 작성하기
- 실무에서는 JpaRepository를 상속받은 인터페이스에 @Query(실행할 쿼리) 어노테이션을 추가한 메소드를 사용하는 방법을 많이 사용한다.
- 메소드명으로 쿼리를 생성하는 기능은 파라미터의 수가 증가할 수록 메소드명이 점점 더러워지기 때문이다.
@org.springframework.data.jpa.repository.Query
어노테이션을 사용한다.- 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.
- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
JpaRepository의 메소드에서 DTO로 조회하기
- new 명령어, 패키지 주소, 클래스명을 명시함으로써 해당 DTO로 직접 조회할 수 있다.
파라미터 바인딩
위치 기반과 이름 기반
- 위치 기반은 0부터 시작하는 인덱스를 기준으로 파라미터가 바인딩된다.
- 이름 기반은 동일한 이름을 가진 문자열을 기준으로 파라미터가 바인딩된다.
코드 가독성과 유지보수를 위해서는 이름 기반 파라미터 바인딩을 사용해야 한다.
만약 위치 기반으로 했다가 순서가 잘못된다면 돌이킬 수 없는 일이 발생할 수도 있다.
컬렉션 파라미터 바인딩
- 넘기는 파라미터를 컬렉션 타입으로 넘기면 해당 컬렉션에 있던 값들이 자동으로 IN절에 포함된다.
페이징과 정렬
기존의 JPA만 사용하는 방식
- 페이징 데이터와 총 데이터 개수를 따로 구해야 한다.
- 페이징 관련 공식을 일일이 직접 적용해야 한다. (총 페이지 수, 다음 페이지 존재 여부 등등)
Spring Data Jpa를 사용하는 방식
- Spring Data Jpa가 제공하는 특별한 클래스들을 사용해서 편리하게 처리한다.
- 페이징과 정렬 파라미터
org.springframework.data.domain.Sort
- 정렬 기능 제공
org.springframework.data.domain.Pageable
- 페이징 기능 제공 (내부에 Sort가 포함되어 있다.)
- 특별한 반환 타입
- org.springframework.data.domain.Page
- 추가 count 쿼리 결과를 포함하는 페이징을 조회한다.
- org.springframework.data.domain.Slice
- 추가 count 쿼리 없이 다음 페이지만 확인 가능하다.
- 내부적으로 limit + 1을 조회한다.
- java.util.List
- 자바 컬렉션
- 추가 count 쿼리 없이 결과만 반환한다.
- org.springframework.data.domain.Page
- Page<T>에 대해서 getContent()를 실행하면 페이징된 데이터 목록인 List<T>를 반환한다.
Page<T>에 대해서 getTotalElements()를 실행하면 총 데이터 개수인 long을 반환한다.
- 간단한 예시
- 페이징 사용 예시
NamedQuery에도 페이징을 사용할 수 있는가?
- 테스트 해보니 가능하다.
count 쿼리는 분리가 가능하다.
- 단순히 데이터의 개수를 세는 count 쿼리에 불필요한 조인때문에 속도가 느려질 수도 있다.
- 상황과 성능을 고려해서 필요하면 countQuery를 따로 선언한다.
벌크성 수정 쿼리
- 벌크 연산도 Spring Data Jpa가 생산성을 증가시켜준다.
기존의 JPA만 사용하는 방식
Spring Data Jpa를 사용하는 방식
@Modifying
어노테이션을 추가해야 한다.@Modifying
어노테이션이 있어야지 JPA에서 excuteUpdate()를 실행한다.- 만약,
@Modifying
어노테이션이 없다면 getResultList()나 getSingleResult()를 호출한다. clearAutomatically = true
를 설정하면 자동으로 flush랑 clear를 진행한다.
@EntityGraph
- fetch 전략이 LAZY인 엔티티에 대해서 연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.
- Spring Data Jpa가 @EntityGraph를 통해서 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용할 수 있게 도와준다.
- 이 기능을 사용하면 JPQL 없이 페치 조인을 할 수 있다.
- 장점
- 쿼리 수 줄이기
- 한 번의 쿼리로 여러 엔터티 객체 조회 가능
- 성능 향상
- 불필요한 쿼리 왕복 감소
- 코드 간결화
- 복잡한 쿼리 코드 간소화
- 쿼리 수 줄이기
- 주의 사항
- 엔터티 그래프 이름 충돌 방지
- 엔터티 그래프 이름은 프로젝트 내에서 유일해야 한다.
- 엔터티 관계 순환 참조 방지
- 엔터티 관계 순환 참조는 무한 루프 발생 가능
- 성능 최적화
- 엔터티 그래프 사용 시 불필요한 엔터티까지 조회하지 않도록 주의
- 엔터티 그래프 이름 충돌 방지
결국은 @EntityGraph는 페치 조인을 다르게 사용하는 방법이다.
실무에서 사용할 때는 마주하는 상황에 맞게 레포지토리에서 @Query로 직접 작성하거나, @EntityGraph를 사용해야 한다.
JPA Hint
- JPA 구현체에게 제공하는 힌트
- JPQL, Criteria API, 쿼리 메소드 등 다양한 쿼리 방식에서 사용할 수 있다.
용도
- 성능 최적화
- 쿼리 실행 계획을 변경하여 성능을 향상시킬 수 있다.
- 메모리 사용량 감소
- 쿼리 결과를 캐싱하거나 영속성 컨텍스트에 저장되는 엔티티 수를 줄여 메모리 사용량을 줄일 수 있다.
- 결과 제한
- 쿼리 결과의 행 수를 제한하여 불필요한 데이터 처리를 줄일 수 있다.
- 잠금 설정
- 쿼리 실행 중에 데이터 변경을 방지하여 일관성을 유지할 수 있다.
주의 사항
- JPA 구현체에 따라 힌트 지원 여부 다를 수 있다.
- 힌트 사용 시 쿼리 성능의 변화를 주의 깊게 관찰해야 한다.
- 잘못된 힌트 사용 시 성능 저하 또는 예상치 못한 결과 발생할 수 있다.
예시
org.hibernate.readOnly
- 엔티티를 읽기 전용 모드로 설정하여 성능을 향상시킨다.
org.hibernate.cacheable
- 쿼리 결과를 캐싱하여 성능을 향상시킨다.
org.hibernate.fetchSize
- 쿼리 실행 시 가져올 데이터의 양을 제한한다.
forCounting
@QueryHints
어노테이션에forCounting = true
를 설정하면 동작하는 기능이 존재한다.- 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트가 적용된다.
- 기본값은 true다.
JPA Lock
- JPA에서 제공하는 제공하는 동시성 제어 기능이다.
- 여러 트랜잭션이 동시에 데이터를 변경하려는 경우 데이터 무결성을 유지하기 위해 사용된다.
@org.springframework.data.jpa.repository.Lock
어노테이션을 사용한다.
종류
- 낙관적 Lock
- 트랜잭션이 데이터를 커밋하기 전에 충돌을 감지하는 방식
- 버전 번호를 사용하여 충돌을 감지한다.
- 성능 오버헤드가 적다.
- 비관적 Lock
- 트랜잭션이 데이터를 읽는 즉시 Lock을 걸어 다른 트랜잭션의 접근을 차단하는 방식
- 충돌을 미연에 방지할 수 있다.
- 성능 오버헤드가 발생한다.
낙관적 Lock
- (방법 1) 엔티티 클래스에 버전 필드를 지정하고
@Version
어노테이션을 사용한다. - (방법 2) 쿼리 메소드에
@Lock
어노테이션을 사용하고LockModeType.OPTIMISTIC
을 지정한다.
비관적 Lock
- (방법 1) 쿼리 메소드에
@Lock
어노테이션을 사용하고LockModeType.PESSIMISTIC_WRITE
또는LockModeType.PESSIMISTIC_READ
를 지정한다. - (방법 2) 모든 쿼리에 비관적 Lock을 걸기 위해
PessimisticLockInterceptor
인터셉터를 사용한다.
하이버네이트 6의 left join 최적화
- 스프링 부트 3 이상을 사용하면 하이버네이트 6이 적용된다.
- 하이버네이트 6에서
의미없는 left join
을 최적화 해버린다.- 실제 실행되는 쿼리에서 left join이 빠진다.
- 여기서
의미없는 left join
이란 left join을 명시하긴 했으나 조인한 테이블을 select절에서 사용하지 않는 경우를 의미한다.