elevne's Study Note
Spring Data JPA 본문
Spring Data JPA 는 스프링에서 JPA 를 편리하게 사용할 수 있도록 지원하는 프로젝트다. 이는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 해결해준다. CRUD 를 처리하기 위한 공통 인터페이스를 제공하며, 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 Spring Data JPA 가 구현 객체를 동적으로 생성해서 주입해준다. 데이터 접근 계층 개발 시 구현 클래스 없이 인터페이스만 작성해도 개발을 할 수 있는 것이다.
아래 Dependency 를 추가해 사용한다.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.8.0.RELEASE</version>
</dependency>
그 다음 Spring 설정에 <jpa:repositories> 를 사용하여 base-packages 를 지정한다. (만약 Java Config 를 사용한다면 @EnableJpaRepositories 애노테이션을 사용하면 된다)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://springframework.org/schema/data/jpa http://springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="com.example.jpastudy.repository" />
</beans>
Spring Data JPA 는 간단한 CRUD 기능을 공통으로 처리하는 JpaRepository 인터페이스를 제공한다. Spring Data JPA 를 사용하는 가장 단순한 방법이 해당 인터페이스를 상속받아 사용하는 것이다. 그리고 제네릭에 엔티티 클래스, 엔티티 클래스가 사용하는 식별자 타입을 지정한다.
@Entity
public class Member {
@Id @Column(name = "MEMBER_ID") @GeneratedValue
private Long id;
private String name;
private Integer age;
...
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
...
사용되는 JpaRepository 는 아래와 같은 계층구조를 가진다.
org.springframework.data.repository 의 Repository, CrudRepository, PagingAndSortingRepository 를 확인할 수 있다. 이들은 Spring Data 프로젝트가 공통으로 사용하는 인터페이스이다. Spring Data JPA 가 제공하는 JpaRepository 인터페이스는 추가로 JPA 에 특화된 기능들을 제공하는 것이다. 다음과 같은 주요 메소드들이 있다.
- save(S): 새로운 엔티티를 저장하고 이미 있는 엔티티라면 수정
- delete(T): 엔티티 하나를 삭제 (내부에서 EntityManager.remove() 호출)
- findOne(ID): 엔티티 하나를 조회 (내부에서 EntityManager.find() 호출)
- getOne(ID): 엔티티를 프록시로 조회 (내부에서 EntityManager.getReference() 호출)
- findAll(): 모든 엔티티를 조회 (정렬이나 페이징 조건을 파라미터로 제공 가능)
Spring Data JPA 는 메소드 이름만으로 쿼리를 생성할 수 있는 쿼리 메소드 기능을 제공한다. 메소드 이름만으로 JPA Named Query 를 호출할 수도 있으며, @Query 애노테이션을 사용해 레포지토리 인터페이스에 쿼리를 직접 정의할 수도 있다.
예를 들어, 이름과 나이로 회원을 조회하려고 하면 아래와 같은 메소드를 선언한다.
List<Member> findByNameAndAge(String name, Integer age);
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = 1? |
Between | findByStartDateBetween | … where x.startDate between 1? and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> age) | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
위와 같은 규칙으로 인터페이스에 정의한 이름으로 쿼리를 생성한다. 이 기능을 사용할 때, 엔티티의 필드명이 변경되면 인터페이스에 정의된 메소드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
아래와 같은 Named Query 를 사용하는 경우도 있다.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.name = :name"
)
public class Member {
...
Spring Data JPA 를 사용하면 메소드 이름만으로 네임드 쿼리를 호출할 수 있다.
List<Member> findByUsername(@Param("name") String name);
Spring Data JPA 는 선언한 "도메인 클래스 + . + 메소드 이름" 으로 네임드쿼리를 찾아서 실행한다. 예제는 Member.findByUsername 이라는 네임드쿼리를 찾아 실행하는 것이다. 만약 실행할 네임드 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다. 또 @Param 애노테이션이 사용되었는데, 이는 이름 기반 파라미터를 바인딩 할 때 사용하는 애노테이션이다.
@Query 애노테이션으로 직접 쿼리를 작성할 수 있다. 이 방법은 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 네임드 쿼리라고 할 수 있다. JPA Named Query 처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다. (Native SQL 을 사용하려면 nativeQuery=true 를 넣어주면 된다. 이 때, JPQL 은 위치 기반 파라미터를 1 부터 시작하지만 Native SQL 은 0 부터 시작함에 유의해야 한다. 물론 위치 기반 파라미터를 사용하지 않고 이름 기반 파라미터와 @Param 을 사용하는 것도 가능하다)
// JPQL
@Query("SELECT m FROM Member m WHERE m.name = ?1")
Member findByUsername1(String username);
// NATIVE SQL
@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
Member findByUsername2(String username);
Bulk 성 수정 쿼리는 아래와 같은 형식으로 작성한다.
@Modifying
@Query("UPDATE Member m SET m.age = m.age + 1 WHERE m.name = :name")
int bulkAgeUpdate(@Param("name") String name);
@Modifying 을 통해 벌크성 수정, 삭제 쿼리로 지정할 수 있다. 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying 애노테이션 내에 clearAutomatically=true 를 넣어주면 된다. (기본값은 false)
Spring Data JPA 는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2 가지 특별한 파라미터를 제공한다. org.springframework.data.domain.Sort, org.springframework.data.domain.Pageable 이다. 파라미터에 Pageable 을 사용하면 반환 타입으로 List 나 org.springframework.domain.Page 를 사용할 수 있다. 반환 타입으로 Page 를 사용하면 Spring Data JPA 는 페이징 기능을 제공하기 위해 검색된 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
Page<Member> findByName(String name, Pageable pageable);
Page<Member> findByName(String name, Sort sort);
// List<Member> findByName(String name, Pageable pageable);
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "name"));
Page<Member> result = repository.findByName("andy", pageRequest);
List<Member> members = result.getContent();
int totalPages = result.getTotalPages();
boolean hasNextPage = result.hasNext();
Pageable 은 인터페이스이기에 실제 사용할 때는 위와 같이 PageRequest 객체를 사용한다.
이 외에도 JPA 쿼리 힌트를 사용하기 위해 @QueryHints 애노테이션을 사용할 수 있으며 (SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트), @Lock 애노테이션을 통해 쿼리 시 락을 걸 수도 있다.
@QueryHints(value = {@QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
@Lock(LockModeType.PESSIMISTIC_WRITE)
Page<Member> findByAge(Integer age);
org.springframework.data.jpa.domain.Specification 을 사용하여 다양한 검색조건으로 검색을 진행할 수 있다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor<Member> {
...
private void findAllTest() {
List<Member> result = repository.findAll(
Specifications.where(memberName("andy"))
);
}
private Specification<Member> memberName(final String name) {
return new Specification<Member>() {
@Override
public Predicate toPredicate(Root<Member> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
if (StringUtils.isEmpty(name)) return null;
return criteriaBuilder.equal(root.get("name"), name);
}
};
}
Specification 은 Composite 패턴으로 구성되어 있어서 여러 Specification 을 조합할 수 있다. (검색 조건 정의 시 toPredicate 메소드만 구현하면 되는데, JPA Criteria 의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어지기에 이들을 활용하여 적절한 검색조건을 만들면 된다)
Spring Data JPA 로 레포지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않는다. 하지만 다양한 이유로 메소드를 직접 구현해야 할 때도 있다. 그렇다고 레포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. Spring Data JPA 는 이런 문제를 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공한다. 사용자 정의 인터페이스를 작성한다.
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
그 다음으로 작성한 사용자 정의 인터페이스를 구현한 클래스를 작성한다. 이 때 클래스 이름은 항상 "리포지토리 인터페이스 이름 + Impl" 로 지어야 한다. 이렇게 해야 Spring Data JPA 가 사용자 정의 구현 클래스로 인식한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
return null;
}
}
마지막으로 레포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 이를 사용 가능하다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor<Member>, MemberRepositoryCustom {
...
QueryDSL 등을 사용할 때 사용자 정의 인터페이스를 사용하면 좀 더 편리하게 사용할 수 있을 것이다. (그 외에도 물론 QueryDslPredicateExecutor, QueryDslRepositorySupport 를 사용할 수 있긴 하다)
Reference:
자바 ORM 표준 JPA 프로그래밍
'Backend > JPA 프로그래밍' 카테고리의 다른 글
JPA 컬렉션과 부가기능 (0) | 2023.07.26 |
---|---|
JPA N+1 & OSIV (0) | 2023.07.20 |
Spring Maven 설정 (0) | 2023.07.17 |
JPA - NativeSQL (0) | 2023.07.16 |
JPA - QueryDSL (0) | 2023.07.15 |