강의를 수강한지 1년이 되가는 Spring Data JPA에 대해 정리해보도록 하겠습니다.
Spring Data JPA
스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
CRUD 처리를 위한 공통 인터페이스를 제공하며, 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 인터페이스를 구현한 객체를 동적으로 생성해서 주입한다.
이 밖에도 많은 JPA의 기능을 편리하게 사용할 수 있도록 지원한다.
본격적으로 살펴보자.
공통 인터페이스
스프링 데이터 JPA에서 제공하는 공통 인터페이스 JpaRepository에 대해 살펴보자.
//JpaRepository 공통 기능 인터페이스
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
....
}
//JpaRepository를 사용하는 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {
}
- JpaRepository 인터페이스 : 공통 CRUD와 페이징 기능을 제공한다.
- 제네릭은 <엔티티 타입, 식별자 타입>으로 설정해야 한다.
다음은 JpaRepository 상속 계층이다.
- 스프링 데이터 JPA가 구현 클래스를 대신 생성한다.
getClass 메서드를 사용해 클래스를 확인해보면 프록시 클래스가 출력되는 것을 확인할 수 있다. - org.springframework.data.repository.Repository를 구현한 클래스는 컴포넌트 스캔 대상이다.
그러므로 @Component 애노테이션을 작성하지 않아도 컴포넌트 스캔을 통해 빈으로 등록된다. - @Repository 애노테이션 생략이 가능하다. 즉, JPA 예외를 스프링 예외로 변환하는 과정도 자동적으로 처리한다.
- 주요 메서드로는 save(), delete(), findById(() 등이 있으며, 대부분의 공통 메서드를 제공한다.
쿼리 메서드 기능
쿼리 메서드 기능으로는 3가지가 있다.
- 메서드 이름으로 쿼리 생성
- 메서드 이름으로 JPA NamedQuery 호출
- @Query 애노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의. 실무에서는 이 방식을 많이 쓴다고 한다.
- 첫 번째 방식과 세 번째 방식을 살펴보겠다.
- DTO로 직접 조회하는 방식도 살펴보겠다.
public interface MemberRepository extends JpaRepository<Member, Long> {
// 스프링 데이터 JPA는 메서드 이름을 분석해서 JPQL을 생성하고 실행
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
//실행 시점에 문법 오류를 발견할 수 있다.
@Query("select m from Member m where m.username= :username and m.age = :age")
List<Member> findUser(@Param("username") String username, @Param("age") int age);
//DTO로 직접 조회. new를 사용해야 한다.
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
//컬렉션 파라미터 바인딩
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
//Optional을 반환
@Query("select m from Member m where m.username = :username")
Optional<Member> findByUsername(String username)
}
위에서 사용한 파라미터 바인딩은 이름 기반 바인딩이다. 위치 기반 바인딩도 있는데 이름 기반 바인딩을 사용하도록 하자.
페이징
스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
또한 페이징을 사용할 수 있는 특별한 반환 타입도 존재한다.
- org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
- List(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환한다.
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
벌크성 수정 쿼리
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리는 아래와 같다. 아래는 예시 코드다.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- 벌크성 수정, 삭제 쿼리는 @Modifying 애노테이션을 사용한다.
- 벌크성 쿼리를 실행하고 나서 영속셍 컨텍스트를 초기화 해야 한다.
- 벌크성 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB의 엔티티 상태가 달라질 수 있기 때문이다.
- @Modifying(clearAutomatically = true)로 설정하면 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화 한다.
@EntityGraph
- 연관된 엔티티들을 SQL 한 번에 조회하는 방법이다.
- 즉 @EntityGraph를 사용하면 JPQL 페치 조인을 사용하지 않아도 된다.
- 페치 조인의 간편 보전으로 LEFT OUTER JOIN을 사용한다.
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
확장 기능
사용자 정의 리포지토리 구현
- 스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 생성해준다.
- 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
- 인터페이스의 메서드를 직접 구현하고 싶다면 어떻게 해야할까?
//사용자 정의 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
//사용자 정의 인터페이스 구현 클래스
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}}
//사용자 인터페이스 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
사용자 정희 구현 클래스에 규칙이 있다.
- 원래 규칙은 리포지토리 인터페이스 이름 + Impl 형식을 지켜야한다.
- 스프링 데이터 2.x 부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl 을 적용하는
- 대신에 사용자 정의 인터페이스 명 + Impl 방식도 지원한다.
- 예를 들어서 위 예제의 MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl 같이 구현해도 된다.
이 방식이 더 직관적이라 추천하는 방식이라고 한다.
Auditing
엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶으면 어떻게 해야 할까?
스프링 데이터 JPA는 매우 편리하게 해당 기능을 제공한다.
참고로 JPA의 주요 이벤트 애노테이션은 다음과 같다.
@PrePersist, @PostPersist, @PreUpdate, @PostUPdate
스프링 데이터 JPA에서는 먼저 2가지 설정이 필요하다.
- @EnableJpaAuditing: 스프링 부트 설정 클래스에 적용해야 한다.
- @EntityListeners(AuditingEntityListener.class) -> 엔티티에 적용해야 한다.
사용 애노테이션은 크게 @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy가 있다.
이상으로 포스팅을 마칩니다.
더 자세한 내용은 책과 강의를 직접 보시면 큰 도움이 될 것이라고 생각합니다.
도메인 클래스 컨버터와 페이징, 정렬
- HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾는다.
- 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다. (트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.
- HTTP 요청에서 파라미터로 Pageable로 받을 수 있다.
- 만들어진 구현체는 org.springframework.data.domain.PageRequest 다.
참고
스프링 데이터는 Page를 0부터 시작한다. 만약 1부터 시작하고 싶다면, 아래와 같이 설정해야 한다.
spring.data.web.pageable.one-indexed-parameters=true
스프링 데이터 JPA 분석
스프링 데이터 JPA 구현체 분석
- 스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체는 아래 클래스다.
- org.springframework.data.jpa.repository.support.SimpleJpaRepository
- @Repository와 @Transactional를 적용했다.
- 매우 중요한 부분이다. save 메서드에서 새로운 엔티티면 저장을 진행하고 아니라면 병합을 진행한다.
새로운 엔티티를 판단하는 기본 전략은 다음과 같다.
- 식별자가 객체일 때 null로 판단.
- 식별자가 자바 기본 타입일 때 0으로 판단.
- Persistable 인터페이스를 구현해서 판단 로직 변경 가능.
만약 식별자 생성 전략이 @GeneratedValue를 사용하는 것이 아니라 직접 할당이면 식별자 값이 있는 상태로 save()를 호출한다.
그러므로 여기서 병합이 진행되는데, 이는 성능에 매우 비효율적이다.
그러므로 Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.
참고로 등록시간(@CreatedDate)을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.
그 외의 기능
- DDD에서 Specifications(명세)라는 개념을 소개하는데 스프링 데이터 JPA는 JPA Criteria를 확용해서 이 개념을 사용할 수 있도록 지원한다.
- 결론을 말하자면 실무는 JPA Criteria를 거의 안 쓰고 QueryDsl을 사용한다고 한다.
- 또한 QueryByExample 기능도 살펴보았는데 실무에서 잘 안쓰인다고 한다.
- 프로젝션도 등장하는데 프로젝션 대상이 root 엔티티면 유용하다고 한다. root 엔티티를 넘어가면 최적화가 힘들다.
- 그러므로 정말 단순할 때만 Spring Data JPA를 사용한 프로젝션을 사용하고, 좀만 어려워지면 Querydsl을 사용하자.
- 정말 모두 안된다 싶으면 native Query +프로젝션을 사용하자.
이상으로 포스팅을 마칩니다. 더 깊고 자세한 내용은 아래 참고 자료를 보시면 좋을 것 같습니다.
참고 자료
자바 ORM 표준 JPA 프로그래밍 - 김영한
실전! 스프링 데이터 JPA - https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard
댓글