

실전 스프링 데이터 JPA
Series
공통 인터페이스 설정
@SpringBootApplication위치에 @EnableJpaRepositories설정
(스프링 부트 사용시 생략 가능)
JpaRepository를 상속받아 인터페이스 JpaRepository내 구현된 메소드 사용 필요 시 확장
-> Generic - <엔티티 타입, 식별자 타입>
public interface TeamRepository extends JpaRepository<Team, Long> {
}
쿼리 메소드
메소드 이름으로 쿼리 생성
스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
- 필드명이 바뀐 경우 메서드 이름도 함께 변경
- 짧은 쿼리를 사용하는 경우 주로 사용
- 식별하기 위한 내용(설명) 추가 가능 ex) findSliceMemberByAge
NamedQuery
쿼리에 이름을 부여하고 호출하는 기능
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
엔티티에 @NamedQuery로 작성한 SQL 가져와 사용
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
- 스프링 데이터 JPA는 선언한 “도메인 클래스 + .(점) + 메서드 이름”으로 Named 쿼리를 찾아서 실행
- 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략 사용
@Query - 리파지토리 메소드에 쿼리 정의 파라미터 바인딩
NamedQuery 보단 @Query 사용해 리파지토리 메소드에 쿼리 직접 정의
@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로 직접 조회
@Query(
"select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견 가능
- DTO로 직접 조회 시 new 명령어 사용
- 파라미터 바인딩, 컬렉션 파라미터 바인딩 가능
반환 타입
유연한 반환 타입 지원
Spring Data JPA return type 문서
//컬렉션
List<Member> findByUsername(String name);
//단건
Member findByUsername(String name);
//단건 Optional
Optional<Member> findByUsername(String name);
컬렉션
- 결과 없음: 빈 컬렉션 반환
단건 조회
- 결과 없음: null 반환 -> 예외 발생 시 무시하고 null 반환
- 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
페이징과 정렬
페이징 객체
- org.springframework.data.domain.Pageable : 패이징 기능 (내부에 Sort객체 포함)
반환 타입
- Page : count 쿼리 결과를 포함하는 페이징
- Slice : count 쿼리 없이 다음 페이지만 확인 (limit + 1 조회)
- List(자바 컬렉션) : count없이 결과만 반환
// repository
Page<Member> findPageByUsername(String name, Pageable pageable);
Slice<Member> findSliceByUsername(String name, Pageable pageable);
List<Member> findListByUsername(String name, Pageable pageable);
count를 포함하는 경우 성능상 이슈 발생 가능 ex) join이 많을 경우
-> @Query를 사용해 count 쿼리 분리 가능
// repository
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
벌크성 수정 쿼리
@Modifying 어노테이션 추가, 영속성 컨텍스트 초기화(clearAutomatically true 옵션, em.clear())
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkUpdateAgePlus(@Param("age") int age);
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산 먼저 실행
- 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트 초기화
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법 -> 패치 조인 사용 JPQL 없이 패치 조인
// JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
// 메서드 이름에서 사용
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
JPA Hint
@QueryHints 어노테이션 사용해 readOnly 속성 사용
JPA도 @Lock 어노테이션으로 트랜잭션 처리의 순차성을 보장하기 위해 DB Lock 지원
관련된 내용은 추가로 정리 예정
사용자 정의 인터페이스
- Custom interface 생성
- Custom interface 을 상속받은 사용자 정의 구현 클래스 생성
- 기존 Repository Interface에 Custom interface를 상속 받는다.
사용자 정의 구현 클래스
- 규칙: 리포지토리 인터페이스 이름 + Impl ex) MemberRepositoryImpl
- 2.x 버전부터는 사용자 정의 인터페이스 명 + Impl 지원 ex) MemberRepositoryCustomImpl
- 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
(따로 Bean 관련 어노테이션 붙이지 않아도 된다)
다른 이름으로 적용하고 싶으면 설정 변경
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
repositoryImplementationPostfix = "Impl")
Custom 기능은 확장하는 기능으로 관리의 복잡도가 높아질 수 있음
-> 레포지토리를 기능에 따라 분리해 작성
핵심은
- 쿼리와 커맨드 분리
- 핵심 비즈니스 로직 / 아닌 부분 분리
Auditing
엔티티 생성, 변경할 때 변경한 사람과 시간을 추적하는데 사용 보통 1) 등록일, 2)수정일 -기본 / 추가로 3)등록자, 4)수정자(로그인 한 세션정보 기반)
순수 JPA 사용, Spring JPA 사용 2가지 방법 존재
Spring JPA 사용 방법
- @EnableJpaAuditing 어노테이션 스프링 부트 설정 클래스에 적용
- @EntityListeners(AuditingEntityListener.class) 어노테이션 엔티티에 적용
- 공통 매핑 정보 사용을 위한 @MappedSuperclass 어노테이션 엔티티에 적용
관련 어노테이션
@CreatedDate
@LastModifiedDate
@CreatedBy
@LastModifiedBy
Domain Class Converter
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
도메인 클래스 컨버터로 엔티티를 파라미터로 받으면 단순 조회용으로 사용
-> 트랜잭션이 없는 범위에서 엔티티를 조회하기 때문 (권장x)
페이징, 정렬
Pageable객체를 파라미터로 받아 페이징, 정렬 기능 사용 가능
- org.springframework.data.domain.PageRequest tkdyd
- 사용 파라미터 : page(0부터 시작), size, sort
설정 방법
글로벌 설정 - 스프링 부트 설정
# Application.yml
data:
web:
pageable:
default-page-size: 10 # 기본 페이지 사이즈
max-page-size: 2000 # 최대 페이지 사이즈
개별 설정 - @PageableDefault 어노테이션 사용
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
멀티 페이징 시
페이징 정보가 둘이상이면 접두사로 구분 -> @Qualifier에 접두사 추가
ex) /members?member_page=0&order_page=1
public String list (
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable,
)
Page 1부터 시작 시
- 직접 클래스 만들어 변환 후 PageRequest 생성 -> 레포지토리에서 사용
- 응답도 Page 대신 직접 만들어 제공
스프링 데이터 JPA 구현체
org.springframework.data.jpa.repository.support.SimpleJpaRepository
- 구현체를 살펴보면 class레벨에 @Repository, @Transactional(readOnly = true) 적용
- save() 같은 경우 readOnly false (default)
readOnly
기본값은 false 이며 true 로 세팅하는 경우 트랜잭션을 읽기 전용으로 변경
- 읽기 전용 트랜잭션 내에서 INSERT, UPDATE, DELETE 작업을 해도 반영x
- 성능 향상을 위해 사용하거나 읽기 외 다른 동작 방지위해 사용
Dirty Checking 무시
개발자가 임의로 UPDATE 쿼리를 사용하지 않아도 트랜잭션 커밋 시에 1차 캐시에 저장되어 있는 Entity 와 스냅샷을 비교해서 변경된 부분이 있으면 UPDATE 쿼리를 날려주는 기능
-> readOnly=true 설정 시 스프링 프레임워크가 Hibernate의 FlushMode를 MANUAL로 설정
-> 플러시 할 때 일어나는 스냅샷 비교와 같은 무거운 로직 수행x
새로운 엔티티 구분
save() 메서드는
- 새로운 엔티티면 저장 - em.persist()
- 새로운 엔티티가 아니면 병합 - em.merge()
객체일 때 null로 판단
자바 기본 타입일 때 0으로 판단
-> 식별자 생성 전략이 @Id만 사용해 직접 할당할 때 문제 발생
Persistable 인터페이스 구현해 새로운 엔티티 판단
-> 새로운 엔티티인지 확인하는 isNew 함수 구현
-> @CreatedDate 조합하면 편리하게 확인 가능
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public Object getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
Tip!!
- gradle 의존관계 확인 command
./gradlew dependencies --configuration compileClasspath
@Inheritance -> 다형성을 위한 상속 (상속 관계 매핑)
@Embedded, @Embeddable -> 합성 (복합 값 타입 사용 시)
@MappedSuperclass -> 객체의 입장에서 공통 매핑 정보가 필요할 때 사용