스프링 데이터 JPA 기반 MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {
}
공통 인터페이스 분석
public interface JpaRepository<T, ID extends Serializable>
{
...
}
JpaRepository 인터페이스: 공통 CRUD 제공
Generic
T : 엔티티 타입
ID : 식별자 타입(PK)
Generic
T : 엔티티
ID : 엔티티의 식별자 타입(PK)
S : 엔티티와 그 자식 타입
주요 메서드
- save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
- findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
- getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
- findAll(...) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.
쿼리 메소드 기능
1. 메소드 이름으로 쿼리 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
Spring Data JPA - Reference Documentation
Example 121. 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가 제공하는 쿼리 메소드 기능
- 조회: find...By ,read...By ,query...By get...By,
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
ex) findHelloBy 처럼 ...은 설명이 들어가도 된다. - COUNT: count...By 반환타입 long
- EXISTS: exists...By 반환타입 boolean
- 삭제: delete...By, remove...By 반환타입 long
- DISTINCT: findDistinct, findMemberDistinctBy
- LIMIT: findFirst3, findFirst, findTop, findTop3
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result
2. JPA NamedQuery
스프링 데이터 JPA로 NamedQuery 사용
// @Query 생략해도 됨 -> 자동으로 JpaRepository<Member> + 메서드 명(findByUsername)
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
→ 스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
3. @Query, 리포지토리 메소드에 쿼리 정의하기
메서드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
@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);
}
◎ 참고 : 메서드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 지저분해진다. 그래서 @Query 기능을 자주 사용한다.
파라미터 바인딩
이름 기반 파라미터 바인딩 쓰기!
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
반환 타입
List<Member> findByUsername(String name); //컬렉션
//결과 없음: 빈 컬렉션 반환
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
//결과 없음: null 반환
//결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
페이징과 정렬
페이징과 정렬 파라미터
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
특별한 반환 타입
- org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
- org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1조회)
- 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);
Page 사용 예제 정의 코드
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
Page 사용 예제 실행 코드
//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
//given
...
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then
assertThat...
}
☆ 주의: Page는 1부터 시작이 아니라 0부터 시작이다.
★ Count 쿼리 분리 : Count 쿼리가 단순한 경우 (굳이 조인이 필요 없을 때 - 성능향상 가능!!)
@Query(value = “select m from Member m”, countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
벌크성 수정 쿼리
// 조건에 해당하는 모든 회원의 나이 + 1
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- 벌크성 쿼리를 실행 후 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true)
(기본값은 false) - 이 옵션 없이 회원을 findById로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다.
※ 권장하는 방안
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
- 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
@EntityGraph
페치 조인(FETCH JOIN)의 간편 버전
LEFT OUTER JOIN 사용
EntitiyGraph - Member와 {"team"} 페치조인
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)
NamedEntityGraph 사용 방법 : 잘 사용하지 않는다. (엔티티에 써야함.)
@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
JPA Hint & Lock
스프링 데이터 JPA 사용시 변경감지 때문에 원복 data 저장해놓음!(메모리 낭비!)
→ 100% 조회용으로 쓴다면 JPA Hint 쓸 것! (큰 성능 개선 X - 암달의 법칙)
쿼리 힌트 사용
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
→ forCounting : Page 인터페이스를 반환타입으로 하면 추가로 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true )
Lock : 락 기능 제공. 잘 사용 하지 않음.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
사용자 정의 리포지토리 구현
사용자 정의 인터페이스
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 방식
ex) MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl 가능
Auditing
- 등록일 → @CreatedDate
- 수정일 → @LastModifiedDate
- 등록자 → @CreatedBy
- 수정자 → @LastModifiedBy
설정
@EnableJpaAuditing //스프링 부트 설정 클래스에 적용해야함
@EntityListeners(AuditingEntityListener.class) //엔티티에 적용
→ @EntityListeners(AuditingEntityLister.class) 생략하고 orm.xml 작성해도 된다.
등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
// 실제로는 세션 or 토큰에서 만든 UUID로 등록자, 수정자 체크한다.
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.of(UUID.randomUUID().toString());
}
스프링 데이터 Auditing 적용 - 등록자, 수정자
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
Web 확장 - 도메인 클래스 컨버터
도메인 클래스 컨버터 사용 후
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
}
★ 주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다. (트랜잭션 X)
Web 확장 - 페이징과 정렬
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
요청 파라미터 : ex) /members?page=0&size=3&sort=id,desc&sort=username,desc
- page: 현재 페이지, 0부터 시작한다.
- size: 한 페이지에 노출할 데이터 건수
- sort: 정렬 조건을 정의한다.
ex) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가 ( asc 생략 가능)
글로벌 설정: 스프링 부트
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.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 내용을 DTO로 변환하기
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
Page를 1부터 시작하기
1. 직접 Page 클래스를 만들어서 처리한다. 그리고 직접 PageRequest(Pageable 구현체)를 생성한다.
2. application.yml : spring.data.web.pageable.one-indexed-parameters = true
단점 : 페이지는 1부터 시작하나 아래 부가적인 내용은 바뀌지 않음. (ex. sort, offset 값은 index = 0 상태로 나옴.)
새로운 엔티티를 구별하는 방법
★ *save() 메서드*
- 새로운 엔티티면 저장( persist )
- 새로운 엔티티가 아니면 병합( merge )
※ 새로운 엔티티를 판단하는 기본 전략
- 식별자가 객체일 때 null 로 판단. ex) Long id
- 식별자가 자바 기본 타입일 때 0 으로 판단 ex) long id
- Persistable 인터페이스를 구현해서 판단 로직 변경 가능
SimpleJpaRepository
@Transactional
@Override
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
문제 : 엔티티에서 @GenerateValue 없고 @Id로 PK 값을 할당하는 경우 isNew() = false로 판단. → em.merge() 작동!
해결 방법 : persistable 인터페이스 구현해서 판단 로직 변경! (@GenerateValue 못 쓰는 상황에서 할 것.)
Specifications (명세) - JPA Criteria 활용해서 사용
Query By Example
◎ 참고 : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example
장점
- 동적 쿼리를 편리하게 처리
- 도메인 객체를 그대로 사용
단점
- 조인은 가능하지만 내부 조인(INNER JOIN)만 가능함 외부 조인(LEFT JOIN) 안됨
- 중첩 제약조건 안됨 ex) firstname = ?0 or (firstname = ?1 and lastname = ?2)
- 매칭 조건이 매우 단순함
문자는 starts/contains/ends/regex
다른속성은정확한매칭( = )만지원
엔티티 대신에 DTO를 편리하게 조회할 때 사용 : 전체 엔티티 조회 X, 필요한 값만 조회 할 때 사용.
인터페이스 기반 Projections
@Value 있으면: Open Projections
- 단점: DB에서 엔티티 정보 다 조회해온 다음에 계산 -> JPQL Select절 최적화 안됨
@Value 없으면: Close Projections
- 정확하게 매칭. DB에서 원하는 값(username)만 select절에 담아서 가져옴.
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
클래스 기반 Projections
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
동적 Projections
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
중첩 구조 처리
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
☆ 주의
- 프로젝션 대상이 root 엔티티면, JPQL SELECT 절 최적화 가능
- 프로젝션 대상이 ROOT가 아니면 LEFT OUTER JOIN 처리 → 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산
네이티브 쿼리
1. 페이징 지원
2. 반환 타입
- Object[]
- Tuple
- DTO(스프링 데이터 인터페이스 Projections 지원)
3. 제약
- Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있음(믿지 말고 직접 처리)
- JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
- 동적 쿼리 불가
JPA 네이티브 SQL 지원
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
}
Projections 활용
@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
"FROM member m left join team t ON m.team_id = t.team_id",
countQuery = "SELECT count(*) from member",
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);