elevne's Study Note

Spring Data JPA 본문

Backend/JPA 프로그래밍

Spring Data JPA

elevne 2023. 7. 19. 11:38

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 는 아래와 같은 계층구조를 가진다.

 

 

JpaRepository

 

 

org.springframework.data.repositoryRepository, 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);
        }
    };
}

 

 

SpecificationComposite 패턴으로 구성되어 있어서 여러 Specification 을 조합할 수 있다. (검색 조건 정의 시 toPredicate 메소드만 구현하면 되는데, JPA CriteriaRoot, 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