JPQL?
JPQL은 JPA Query Language의 줄임말로 JPA에서 사용할 수 있는 쿼리를 의미합니다. JPQL의 문법은 SQL과 매우 비슷해서 데이터베이스 쿼리에 익숙하다면 어렵지 않게 사용할 수 있습니다. SQL과 차이점은 SQL에서는 테이블이나 칼럼의 이름을 사용하지만 JPQL은 엔티티 객체를 대상으로 수행하는 쿼리이기 때문에 매핑된 엔티티의 이름과 필드의 이름을 사용합니다.
// JPQL 쿼리의 기본 구조
// Product = 엔티티 타입, p.number = 엔티티 속성
SELECT p FROM Product p WHERE p.number =?1;
쿼리 메서드
리포지토리는 JpaRepository를 상속받는 것만으로 다양한 CRUD 메서드를 제공합니다. 하지만 이러한 기본 메서드들은 식별자 기반으로 생성되기 때문에 결국 별도의 메서드를 정의해서 사용하는 경우가 많습니다. 이때 간단한 쿼리문을 작성하기 위해 사용되는 것이 쿼리 메서드입니다.
쿼리 메서드의 생성
쿼리 메서드는 크게 동작을 결정하는 주제(Subject)와 서술어(Predicate)로 구분합니다. ‘find…By’, ‘exists…By’와 같은 키워드로 쿼리의 주제를 정하며 ‘By’는 서술어의 시작을 나타내는 구분자 역할을 합니다. 서술어 부분은 검색 및 정렬 조건을 지정하는 영역입니다.
기본적으로 엔티티의 속성으로 정의할 수 있고, AND나 OR을 사용해 조건을 확장하는 것도 가능합니다.
// (리턴타입) + (주제 + 서술어(속성)) 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email);
서술어에 들어가는 엔티티의 속성 식(Expression)은 위의 예시와 같이 엔티티에서 관리하고 있는 속성(필드)만 참조할 수 있습니다.
쿼리 메서드의 주제 키워드
쿼리 메서드에 주제 부분에 사용할 수 있는 키워드는 다음과 같습니다.
- find … By
- read … By
- get … By
- query … By
- search … By
- stream … By
해당 키워드들은 조회 기능을 수행하는 키워드이며 ‘…’로 표시한 영역에는 도메인(엔티티)을 표현할 수 있습니다.. 그러나 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단하여 생략하기도 합니다. 리턴 타입으로는 Collection이나 Stream에 속한 하위 타입을 설정할 수 있습니다.
// find...By
Optional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long number);
exists … By
특정 데이터가 존재하는지 확인하는 키워드로, 리턴 타입으로는 boolean 타입을 사용합니다.
count … By
조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드의 개수를 리턴합니다.
delete … By, remove … By
삭제 쿼리를 수행하며 리턴 타입이 없거나 삭제한 횟수를 리턴합니다.
… First<number>…, …Top <number> …
쿼리를 통해 조회된 결괏값의 개수를 제한하는 키워드입니다. 두 키워드는 동일한 동작을 수행하며, 주제와 By사이에 위치합니다. 일반적으로 이 키워드는 한 번의 동작으로 여러 건을 조회할 때 사용되며, 단 건으로 조회하기 위해서는 <number>를 생략하면 됩니다.
쿼리 메서드의 조건자 키워드
JPQL의 서술어 부분에서 사용할 수 있는 조건자 키워드는 다음과 같습니다.
ls
값의 일치를 조건으로 사용하는 조건자 키워드이다. 생략되는 경우가 많으며 Equals와 동일한 기능을 수행합니다.
(ls)Not
값의 불일치를 조건으로 사용하는 조건자 키워드로 ls는 생략하고 Not 키워드만 사용할 수도 있습니다.
(ls)Null, (ls)NotNull
값이 null인지 검사하는 조건자 키워드입니다.
(ls)True, (ls)False
boolean 타입으로 지정된 칼럼값을 확인하는 키워드입니다.
And, Or
여러 조건을 묶을 때 사용합니다.
(ls)GreaterThan, (ls)LessThan, (ls)Between
숫자나 datetime 칼럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드입니다.
GreaterThan, LessThan 키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산을 수행하고, 경곗값을 포함하려면 Equal 키워드를 추가하면 됩니다.
(ls)StartingWith(==StarsWith), (ls)EndingWith(==EndsWith), (ls)Containing(==Contains), (ls)Like
컬럼값에서 일부 일치 여부를 확인하는 조건자 키워드입니다.
SQL 쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 ‘%’ 키워드와 동일한 역할을 하는 키워드입니다. 자동으로 생성되는 SQL문을 보면 Containing 키워드는 문자열의 양 끝, StartingWith 키워드는 문자열의 앞, EndingWith 키워드는 문자열의 끝에 ‘%’가 배치됩니다.. 여기서 별도로 고려해야하는 키워드는 Like 키워드인데, 이 키워드는 코드 수준에서 메서드를 호출하면서 전달하는 값에 %를 명시적으로 입력해야 합니다.
정렬과 페이징 처리
애플리케이션에서 자주 사용되는 정렬과 페이징 처리는 앞서 소개한 쿼리 메서드를 작성하는 방법을 기반으로 수행할 수 있습니다. 물론 키본 쿼리 메서드인 이름을 통한 정렬과 페이징 처리도 가능하지만 다른 방법들도 많이 쓰입니다.
정렬
정렬을 위해서 SQL문에서는 order by를 사용하는데, 쿼리 메소드도 동일합니다. 기본 쿼리 메소드를 작성한 후 OrderBy를 삽입, 정렬하고자 하는 칼럼(칼럼명)과 오름차순(Asc) 혹은 내림차순(Desc)을 설정하면 됩니다. 만일 정렬 조건을 여러 개 사용한다면, And 또는 Or이 아니라 우선순위를 기준으로 차례대로 작성하면 됩니다.
// 쿼리 메서드의 정렬 처리, Asc : 오름차순, Desc : 내림차순
List<Product> findByNameOrderByNumberAsc(String name);
List<Product> findByNameOrderByNumberDesc(String name);
이렇게 쿼리 메서드의 이름에 정렬 키워드를 삽입해서 정렬을 수행할 경우 메소드의 이름이 길어져 가독성이 떨어지는 단점이 발생할 수 있습니다. 이를 해결하기 위해 매개변수를 활용해 정렬할 수도 있습니다.
페이징
페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미합니다. 흔히 볼 수 있는 웹 페이지에서 각 페이지를 구분해서 데이터를 제공할 때 그에 맞게 데이터를 요청하는 것이라고 생각하면 됩니다.
JPA에서는 페이징 처리를 위해 Page와 Pageable을 사용합니다.
// 페이징 처리를 위한 쿼리 메서드 예시
Page<Product> findByName(String name, Pageable pageable);
위와 같이 리턴 타입으로 Page를 설정하고 매개변수에는 Pageable 타입의 객체를 정의합니다. 이런 메서드를 사용하기 위해서는 다음과 같이 호출합니다.
// 페이징 쿼리 메서드 호출
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
위 코드에서 메서드를 호출할 때 리턴 타입으로 Page 객체를 받아야 하기 때문에 Page<Product>로 타입을 선언해 객체를 리턴받았습니다. 그리고 Pageable 파라미터를 전달하기 위해 PageRequest 클래스를 사용했습니다. PageRequest는 Pageable의 구현체입니다.
일반적으로 PageRequest는 of 메서드를 통해 PageRequest 객체를 생성합니다. of 메서드는 매개변수에 따라 다양한 형태로 오버로딩 되어있는데 다음과 같은 매개변수 조합을 지원합니다.
@Query 어노테이션
데이터베이스에서 값을 가져올 때는 메서드의 이름만으로 쿼리 메서드를 생성할 수도 있고, @Query 어노테이션을 사용해 직접 JPQL을 작성할 수도 있습니다.
JPQL을 사용하면 JPA 구현체에서 자동으로 쿼리 문장을 해석하고 실행하게 됩니다. 만약 데이터베이스를 다른 데이터베이스로 변경할 일이 없다면 직접 해당 데이터베이스에 특화된 SQL을 작성할 수 있으며, 주로 튜닝된 쿼리를 사용하고자 할 때 직접 SQL을 작성합니다.
// @Query 어노테이션을 사용하는 메서드
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
두번째 줄과 같이 @Query 어노테이션을 사용해 JPQL 형식의 쿼리문을 작성합니다. (쿼리문에서 SQL 예약어는 소문자로 작성해도 됩니다.). FROM 뒤에 엔티티 타입을 지정하고 별칭을 생성합니다. (AS 생략 가능). WHERE문에서는 SQL과 마찬가지로 조건을 지정합니다.. 조건문에서 사용한 ‘?1’은 파라미터를 전달받기 위한 인자에 해당한다. 1은 첫 번째 파라미터를 의미합니다.
이 같은 방식을 사용할 경우 파라미터의 순서가 바뀌면 오류가 발생할 가능성이 있어 @Param 어노테이션을 사용하는 것이 좋습니다.
// @Query 어노테이션과 @Param 어노테이션을 사용한 메서드
@Query("SELECT p FROM Product p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);
QueryDSL 적용하기
메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통해 대부분 해소할 수 있지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있습니다.
쿼리의 문자열이 잘못된 경우에는 애플리케이션이 실행된 후 로직이 실행되고 나서야 오류를 발견할 수 있습니다. 이런 이유로 개발 환경에서는 문제가 없는 것처럼 보이다가 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견되는 리스크를 유발합니다.
이런 문제를 해결하기 위해 사용되는 것이 QueryDSL이다. QueryDSL은 문자열이 아니라 코드로 쿼리를 작성할 수 있도록 도와줍니다.
QueryDSL이란?
QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크입니다. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 플루언트(Fluent) API를 활용해 쿼리를 생성할 수 있습니다.
QueryDSL을 사용하면 다음과 같은 장점이 있습니다.
- IDE가 제공하는 코드 자동 완성 기능을 사용할 수 있습니다.
- 문법적으로 잘못된 쿼리를 허용하지 않는다. 따라서 정상적으로 QueryDSL을 활용한다면 문법 오류를 발생시키지 않습니다.
- 고정된 SQL 쿼리를 작성하지 않기 때문에 동적으로 쿼리를 생성할 수 있습니다.
- 코드로 작성하므로 가독성 및 생산성이 향상됩니다.
- 도메인 타입과 프로퍼티를 안전하게 참조할 수 있습니다.
QueryDSL을 사용하기 위한 프로젝트 설정
QueryDSL을 사용하려면 몇 가지 설정이 필요합니다. 먼저 pom.xml 파일에 의존성을 추가합니다.
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
그리고 <plugins> 태그에 APT 플러그인을 추가해야 합니다.
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<procecessor>com.querydsl.apt.jpa.JPAAnotationProcessor</processor>
<options>
<querydsl.entityAcessors>true</querydsl.entityAcessors>
</options>
</configuration>
</execution>
</executions>
</plugin>
JPAAAnotationProcessor는 @Entity 어노테이션으로 정의된 엔티티 클래스를 찾아서 쿼리 타입을 생성합니다.
QuerydslPredicateExecutor, QuerydslRepositorySupport 활용
스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스를 제공합니다.
QuerydslPredicateExecutor 인터페이스
QuerydslPredicateExecutor는 JpaRepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스를 제공합니다.
// QuerydslPredicateExecutor를 사용하는 리포지토리 생성
public interface QProductRepository extends JpaRepository<Product, Long>,
QuerydslPredicateExecutor<Product> {
}
QuerydslRepositorySupport 추상 클래스 사용하기
QuerydslRepositorySupport 클래스 역시 QueryDSL 라이브러리를 사용할 수 있도록 하는 여러 방식을 제공합니다. 그 중 가장 일반적으로 사용하는 것은 CustomRepository를 활용해 리포지토리를 구현하는 방식입니다.
// ProductRepositoryCustom.java
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
// ProductRepositoryCustomImpl.java
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport
implements ProductRepositoryCustom {
ProductRepositoryCustom {
public ProductRepositoryCustomImpl() {
super(Product.class);
}
@Override
public List<Product> findByName(String name) {
QProduct product = QProduct.product;
List<Product> productList = from(product)
.where(product.name.eq(name))
.select(product)
.fetch();
return productList;
}
}
}
JpaRepository를 상속받는 ProductRepository를 생성합니다. 직접 구현한 쿼리를 사용하기 위해서는 JpaRepository를 상속받지 않는 리포지토리 인터페이스인 ProductRepositoryCustom을 생성합니다. ProductRepository에서 해당 메소드들을 이용하기 위해서 ProductRepository는 ProductRepositoryCustom도 상속받습니다.
JPA Auditing 적용
JPA에서 ‘Audit”이란 ‘감시하다’라는 뜻으로, 각 데이터마다 ‘누가’, ‘언제’ 데이터를 생성했고 변경했는지 감시한다는 의미로 사용됩니다. 엔티티 클래스에는 공통적으로 들어가는 필드가 있습니다.. ‘생성 일자’와 ‘변경 일자’같은 코드를 의미한다.
대표적으로 많이 사용되는 필드는 다음과 같습니다.
- 생성 주체
- 생성 일자
- 변경 주체
- 변경 일자
이러한 필드들은 매번 엔티티를 생성하거나 변경할 때마다 값을 주입해야 하는 번거로움이 있습니다.. 이런 번거로움을 해소하기 위해 Spring Data JPA에서는 이러한 값을 자동으로 넣어주는 기능을 제공합니다.
JPA Auditing을 사용하기 위해서는 main() 메소드가 있는 클래스에 @EnableJpaAuditing 어노테이션을 추가하거나, 아래와 같이 Configuration 클래스를 따로 생성하여 적용하면 됩니다.
// JpaAuditingConfiguration.java
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}
BaseEntity 만들기
생성 주체, 생성 일자, 변경 주체, 변경 일자와 같은 필드들은 많은 엔티티에 공통으로 들어가기 때문에 따로 클래스로 빼두어 만들기도 하는데, 이를 BaseEntity 클래스라고 일반적으로 명명합니다. 각 엔티티 클래스는 해당 클래스를 상속하는 방식으로 구성합니다.
// BaseEntity.java
@Getter
@Setter
@ToString
@MappedSuperClass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
여기서 사용한 주요 어노테이션은 다음과 같습니다.
- @MappedSuperClass : JPA의 엔티티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달합니다.
- @EntityListeners : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션입니다.
- @AuditingEntityListener : 엔티티의 Auditing 정보를 주입하는 JPA 엔티티 리스너 클래스입니다.
- @CreatedDate : 데이터 생성 날짜를 자동으로 주입해주는 어노테이션입니다.
- @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입해주는 어노테이션입니다.
'Backend > Spring Boot' 카테고리의 다른 글
Spring Boot - 연관관계 매핑 (0) | 2023.07.30 |
---|---|
Spring Boot - MVC 모델 (0) | 2023.07.30 |
Spring Boot - API를 작성하는 다양한 방법 (0) | 2023.07.23 |
Spring boot - VScode로 시작하기 (0) | 2023.07.16 |
Spring boot - 데이터베이스 연동 (0) | 2023.07.15 |