본문 바로가기

SpringFramework/JPA

JPA - 스프링 데이터 JPA

스프링 데이터 JPA란?

스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트. 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 더 깔끔한 방법으로 해결한다. CRUD를 처리하기 위한 공통 인터페이스를 제공하고 리포지토리를 개발할 때 인터페이스만 작성하면, 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성하여 주입한다.

데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성하여 사용이 가능하다.

구현 클래스 생성

 

- 스프링 데이터 JPA 라이브러리 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // ...
}

공통 인터페이스

- 스프링 데이터 JPA 적용

package com.ys.test.lab.api.repository;

import com.ys.test.lab.api.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;

// JPA Repository 준비끝!
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    UserEntity findByNname(String name);
}

JpaRepository 부분을 보면 제네릭에 UserEntity, 식별자 타입을 지정했다. JpaRepository 인터페이스가 제공하는 다양한 기능을 이용할 수 있다. 아래의 그림은 JpaRepository 인터페이스의 계층구조이다.

윗부분에 스프링 데이터 모듈이 있고 CrudRepository, PagingAndSortingRepository가 있는데, 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스다. 여기에 JpaRepository 인터페이스는 추가로 JPA에 특화된 기능을 더 제공해준다.

 

- JpaRepository의 주요메소드

메소드 설명
save([Entity or ChildEntity]) 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정
(엔티티에 식별자 값이 없으면 새로운 엔티티로 인식하여 EntityManager.persist() / 식별자가 있으면 EntityManager.merge())
delete(Entity) 엔티티 하나를 삭제. 내부에서 EntityManager.remove()가 호출됨
findOne(ID) 엔티티 하나를 조회. 내부에서 EntityManager.find()가 호출됨
getOne(ID) 엔티티를 프록시로 조회. 내부에서 EntityManager.getReference()가 호출됨
findAll() 모든 엔티티를 조회. 정렬(Sort)이나 페이징(Pagable) 조건을 파라미터로 제공이 가능

=> JpaRepository 인터페이스는 일반적인 CRUD를 해결해준다.

쿼리 메소드

- 메소드 이름으로 쿼리 생성

: 위의 예제에서 인터페이스에 findByName() 메소드를 정의하였다. 메소드를 실행하면 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다. 이 방법은 이름을 마음대로 지어서 쿼리가 만들어지는게 아니라, 일정한 정해진 규칙이 있다.

스프링 데이터 JPA 쿼리 생성 기능 공식문서 (SpringBoot-Data-JPA 2.5.5 ver 기준)

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

- 메소드 이름으로 JPA NamedQuery 호출

@Entity
@NamedQuery(
    name = "UserEntity.findByName",
    query = "SELECT u FROM User u WHERE u.name = :name"
)
@Table(name = "user")
public class UserEntity {
    // ...
}

// ---------------------------------------

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    // 정의해둔 NamedQuery를 호출
    List<UserEntity> findByName(@Param("name") String name);
}

    - JPA는 [ 도메인 클래스 + . + 메소드 이름 ] 으로 Named쿼리를 찾아서 실행한다. (이것도 또한 NamedQuery를 사용하기 위한 규칙이다) 만약 Named쿼리가 없으면 메소드 이름으로 쿼리 생성전략을 사용한다 (전략을 바꾸는 것도 가능)

 

- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    // JPQL Query
    @Query("SELECT u FROM User u WHERE u.name = ?1")
    UserEntity findByName(String name);
    
    // Native Query
    @Query("SELECT * FROM User WHERE name = ?0", nativeQuery = true)
    UserEntity findByName2(String name);
}

네이티브 SQL도 작성할 수 있도록 지원한다. 이때는 nativeQuery 옵션을 true로 한다. JPQL 문법과 다른부분은 위치기반 파라미터의 시작이 0이라는 점이다. (가급적이면 이름 기반 파라미터를 사용하는게 가독성에 좋을듯)

 

- 벌크성 수정 쿼리

@Modifying(clearAutomatically = true)
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);

=> 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용하면 된다. 벌크성 쿼리를 실행하고나서 영속성 컨텍스트를 초기화하고 싶으면 clearAutomatically옵션을 true로 설정한다. (default: false)

반환타입

SELECT 결과가 단건이면 객체를, 여러건이라면 컬렉션 인터페이스를 사용하는 등 유연하게 반환해준다.

- List<UserEntity> findByName(String name); // 여러건. 결과데이터가 없는경우 빈컬렉션 리턴

- UserEntity findById(Long id); // 단건. 결과데이터가 없는경우 null 리턴

페이징과 정렬

스프링 데이터JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 파라미터를 제공한다.

1] Sort: 정렬 기능

2] Pageable: 페이징 기능(Sort 포함)

// count 쿼리 사용
Page<UserEntity> findByName(String name, Pageable pageable);

// count 쿼리 사용안함
List<UserEntity> findByName(String name, Pageable pageable);
List<UserEntity> findByName(String name, Sort sort);

- Page 사용 예제 코드

public interface UserRepository extends JpaRepository<UserEntity, Long> {
    Page<UserEntity> findByNameStartingWith(String name, Pageable pageable);
}


public class UserService {
    private final UserRepository userRepository;
    
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void test() {
        // Pagable 인터페이스의 구현체인 PageRequest를 사용 (현재패이지, 한페이지내 조회수, 정렬정보)
        // 페이지는 0부터 시작함.
        PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
        
        Page<UserEntity> result = userRepository.findByNameStartingWith("장", pageRequest);
        
        List<UserEntity> users = result.getContent(); // 조회된 데이터
        int totalPages = result.getTotalPages(); // 전체 페이지 수
        boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부
    }
}

- Page 인터페이스

: 이 인터페이스를 사용하여 JPA 조회 데이터에 페이징, 정렬기능을 손쉽게 사용할 수 있다.

public interface Page<T> extends Iterable<T> {
    int getNumber();             // 현재 페이지
    int getSize();               // 페이지 크기
    int getTotalPages();         // 전체 페이지 수
    int getNumberOfElements();   // 현재 페이지에 나올 데이터 수
    long getTotalElements();     // 전체 데이터 수
    boolean hasPreviousPage();   // 이전 페이지 여부
    boolean isFirstPage();       // 현재 페이지가 첫 페이지 인지 여부
    boolean hasNextPage();       // 다음 페이지 여부
    boolean isLastPage();        // 현재 페이지가 마지막 페이지 인지 여부
    Pageable nextPageable();     // 다음 페이지 객체, 다음 페이지가 없으면 null 반환
    Pageable previousPageable(); // 다음 페이지 객체, 이전 페이지가 없으면 null 반환
    List<T> getContent();        // 조회된 데이터
    boolean hasContent();        // 조회된 데이터 존재 여부
    Sort getSort();              // 정렬 정보
}

 

- 페이징과 정렬 기능 - 스프링 MVC

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공.

    - 페이징 기능: PageableHandlerMethodArgumentResolver

    - 정렬 기능: SortHandlerMethodArgumentResolver

 

- Pageable 객체는 Request시 page, size, sort 요청 파라미터를받아서 처리할 수 있다.

// ex) http://localhost:8080/users?page=0&size=20&sort=name,desc&sort=address.city

@RestController
public clsss UserController {
    private final UserService userService;
    
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping(value = "/users")
    public List<UserEntity> userList(Pageable pageable) {
        Page<UserEntity> page = userService.findUsers(pageable);
        return page.getContent();
    }
}

 

(!) 페이지를 1부터 시작하고 싶으면 두가지 방법으로 설정을 변경해준다.

1] yml, yaml 파일에 외부설정 추가

data.web.pageable.one-indexed-parameters = true

2] implements WebMvcConfigurer 하여 Configuration 어노테이션으로 설정값 Bean에 등록

설정방법 링크: http://devstory.ibksplatform.com/2020/03/spring-boot-pageable.html

 

- 접두사

: 사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다. @Qualifier 사용 {접두사명}_ 로 구분한다.

public String list {
    @Qualifier("user") Pageable userPageable,
    @Qualifier("order") Pageable orderPageable
}

// ex) /users?user_page=0&order_page=1

 

- @PageableDefault

PageableDefault어노테이션을 사용하여 기본값 설정 변경이 가능하다. (default는 page=1, size=20)

    - 송성: page, size, sort, direction

명세(Specification)

: 명세를 이해하기 위한 핵심 단어는 술어(Predicate)인데 이것은 단순히 참과 거짓으로 평가된다. 그리고 이것은 AND, OR 같은 연산자로 조합할 수 있다. Specification은 컬포지트 패턴으로 구성되어 있어서 여런 Specification을 조합할 수 있으므로 다양한 검색조건을 조립해서 새로운 검색조건을 만들 수 있다. 아래의 참고 외부링크로 예시를 확인해보자.

예시 참고링크: https://groti.tistory.com/49

사용자 정의 리포지토리 구현

JPA 리포지토리 외에 직접 메소드를 구현할 수 있다.

- 사용방법

1] 사용자 정의 인터페이스(interface) 생성

public interface UserRepositoryCustom {
    public List<UserEntity> findUserCustom();
}

2] 사용자 정의 구현 클래스 생성

- 클래스명 생성 규칙이 존재한다. [ 리포지토리인터페이스명(UserRepository) + "Impl" ] 이어야 한다. 이렇게 이름이 지어져야 JPA가 사용자 정의 구현 클래스로 인식한다

public class UserRepositoryImpl implements UserRepositoryCustom {
    @Override
    public List<UserEntity> findUserCoutom() {
        // ... 사용자정의
    }
}

// 사용자 정의 인터페이스 상속
public interface UserRepository extends JpaRepository<UserEntity, Long>, UserRepositoryCustom {

}

스프링 데이터 JPA와 QueryDSL 통합

- https://data-make.tistory.com/626

참고자료

- 서적 - 자바 ORM 표준 JPA 프로그래밍 제 12장 - 김영한 지음

- https://groti.tistory.com/49 - 명세(Specification)

- http://devstory.ibksplatform.com/2020/03/spring-boot-pageable.html - Pageable 설정관련

- https://data-make.tistory.com/626 - 스프링 데이터 JPA + QueryDSL

'SpringFramework > JPA' 카테고리의 다른 글

JPA - 트랜잭션과 락 (+격리수준)  (0) 2021.11.29
JPA - N+1 문제  (0) 2021.11.29
JPA - QueryDSL  (0) 2021.11.19
JPA - Criteria  (0) 2021.11.19
JPA - 객체지향 쿼리 언어, JPQL  (0) 2021.11.17