인프런 김영한 강사님의 [실전! 스프링 데이터 JPA] 을 수강하고 정리한 글입니다.
📌 메소드 이름으로 쿼리 생성
✅ 순수 JPA 리포지토리 코드와 스프링 데이터 JPA 코드
아래는 순수 JPA를 활용한 리포지토리 코드의 예시이다.
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
return em.createQuery("select m from Member m where m.username = :username
and m.age > :age")
.setParameter("username", username)
.setParameter("age", age)
.getResultList();
}
그리고 아래는 스프링 데이터 JPA를 활용한 리포지토리 코드의 예시이다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
딱 봐도 불필요한 부분을 생략하여 코드 양이 굉장히 줄고, 간편한 것을 볼 수 있다.
✅ 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능
- 조회
- find...By
- read...By
- query...By
- get...By
- count 쿼리
- count...By (반환타입 long)
- exists
- exists...By (반환타입 boolean)
- 삭제
- delete...By, remove...By (long)
- distinct
- findDistinct
- findMemberDistinctBy
- limit
- findFirst3
- findFirst
- findTop
- findTop3
📌 JPA NamedQuery
✅ @NamedQuery 순수 JPA vs Spring Data JPA
우선 @NamedQuery 어노테이션을 활용해서 Named 쿼리를 정의하는 방식은 아래와 같다.
엔티티 위에 어노테이션만 달면 된다.
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
위 설정을 했을 때, NamedQuery를 호출하는 방식을 순수 JPA와 Spring Data JPA에서 어떻게 달라지는지 비교할 것이다.
우선 아래 코드는 순수 JPA에서 NamedQuery를 호출하는 방식이다.
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
다음은 Spring Data JPA에서 NamedQuery를 호출하는 방식이다.
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
Spring Data JPA는 "도메인 클래스 + . + 메서드 이름" 방식으로 Named 쿼리를 알아서 찾아서 실행시킨다.
사실 @Query를 생략하더라도 메서드 이름만으로도 Named 쿼리는 호출 가능하다.
그렇게 된다면 단 한줄로도 이용이 가능하니, 정말 편리하다.
근데 사실 Spring Data JPA를 사용하면 NamedQuery를 직접 등록해서 사용할 일이 많지 않긴 하다.
파라미터가 점점 많아질 수록 이름이 복잡해지기 때문이다.
대신 대부분은 @Query 를 활용해서 레포지토리 메소드에 직접 쿼리를 정의한다.
📌 @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);
}
실행할 메서드에 정적 쿼리를 직접 작성한다.
: 이는 이름없는 Named 쿼리 라고 볼 수 있다.
따라서 JPA의 Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
📌 @Query에서 값, DTO 조회하기
✅ 일반적인 값 하나 조회하기
@Query("select m.username from Member m")
List<String> findUsernameList();
방금까지 알아본 방식 그대로다.
참고로 JPA의 값 타입인 @Embedded도 이 방식으로 조회 가능하다.
✅ DTO를 활용하여 조회하기
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();
DTO로 직접 조회하려면 new 명령어를 활용해야 한다.
그리고 실제 DTO 클래스에서도 저 파라미터에 알맞는 생성자를 보유해야 한다.
좀.. 길다.
나중에 QueryDSL이란걸 사용하면 가로로 길어 알아보기 힘든 것을
세로로 깔끔하게 표현할 수 있을 것!
📌 파라미터 바인딩
✅ 위치 기반 및 이름 기반 파라미터 바인딩
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반
?0이라는 것은 여러 파라미터 중 0번째, 즉 제일 앞에 있는 파라미터를 대입하겠다는 의미이다.
:name이라는 것은 여러 파라미터 중 이름이 name인 파라미터를 대입하겠다는 의미이다.
두 가지 방식 중, 파라미터 바인딩을 추천한다.
코드 가독성과 유지보수를 위함이다.
(파라미터가 변경되면 순서가 바뀔 수 있어 위치 기반은 위험하다. 또, 한 눈에 알아보기 힘들다.)
✅ 컬렉션 파라미터 바인딩
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);
in 절을 활용한다.
📌 반환 타입
✅ Spring Data JPA의 유연한 반환 타입 지원
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional
위 코드를 보면, 결과의 타입에 따라 반환 타입을 유연하게 지원한다.
특히 Optional은 nullpointerexception을 방지해주므로 적극 권장한다.
만약 컬렉션에서 결과가 없다면 어떤 결과를 보여줄끼?
빈 컬렉션을 반환한다. (객체는 존재하지만 내부 내용이 없는, size가 0인 상태를 의미한다.)
그러나 단건 조회에서 결과가 없으면 null을 반환한다.
그렇기 때문에 optional을 추천하는 것이다.
📌 페이징과 정렬
✅ 페이징 코드 비교 (순수 JPA vs Spring Data JPA)
아래는 순수 JPA에서의 페이징 레포지토리 코드다.
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by
m.username desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
public long totalCount(int age) {
return em.createQuery("select count(m) from Member m where m.age = :age",
Long.class)
.setParameter("age", age)
.getSingleResult();
}
그리고 아래는 Spring Data JPA에서의 페이징 레포지토리 코드다.
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
얼마나 편리한가.
✅ Spring Data JPA 페이징 및 정렬 사용 예제
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);
이렇게 상황에 따라 다르게 페이징을 활용할 수 있다.
페이징을 실제로 활용했을 때 테스트 코드의 예제를 보자.
//페이징 조건과 정렬 조건 설정
@Test
public void page() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
"username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then
List<Member> content = page.getContent(); //조회된 데이터
assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}
PageRequest 객체를 생성할 때
첫 번째 파라미터는 현재 페이지를, (페이지는 0부터 시작한다.)
두 번째 파라미터는 조회할 데이터 수를 입력한다.
세 번째 파라미터는 정렬에 관한 정보를 입력한다.
✅ 참고 : count 쿼리 분리하기
@Query(value = “select m from Member m”,
countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
이렇게 count 쿼리를 별도로 분리할 수도 있다.
✅ 참고 : 페이징에서 엔티티를 DTO로 변환하기
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
📌 벌크 수정 쿼리
✅ 벌크 수정 쿼리 순수 JPA vs Spring Data JPA
아래는 순수 JPA의 벌크성 수정 쿼리 코드다.
public int bulkAgePlus(int age) {
int resultCount = em.createQuery(
"update Member m set m.age = m.age + 1" +
"where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
그리고 다음은 Spring Data JPA를 활용한 벌크성 수정 코드이다.
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
벌크성 수정 및 삭제 쿼리에는 @Modifying 어노테이션을 활용한다.
만약 벌크성 쿼리 실행 후 영속성 컨텍스트를 초기화 하고 싶다면,
@Modifying(clearAutomatically = true)
이처럼 옵션을 활용한다. (기본값 : false)
대부분 벌크성 수정 및 삭제는 영속성 컨텍스트를 무시하고 실행하는 방식이기 때문에 영속성 컨텍스트 내부와 DB 상태에 따라 문제가 생길 수 있다.
이를 해결하기 위해 보통 벌크성 수정 및 삭제를 우선적으로 실행한 후, 영속성 컨텍스트를 초기화 하고 이후 작업을 진행한다.
그런 경우에 어울리는 옵션이다.
📌 EntityGraph
연관된 엔티티들을 쿼리 한 번에 조회하는 방법이다.
✅ LazyLoading
대부분의 경우에 연관관계에서는 지연로딩을 활용한다.
이 때 만약 연관된 엔티티를 한 번에 조회하려면 쿼리가 n+1번 나가게 되는 문제가 발생하고,
이를 해결하기 위해 fetch join이나 entitygraph를 활용한다.
✅ JPQL Fetch Join
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
위 방식처럼 fetch join을 통해 n+1 문제를 해결할 수도 있지만,
Spring Data JPA에는 JPA의 엔티티 그래프 기능을 편리하게 사용할 수 있도록 지원한다.
✅ EntityGraph
//공통 메서드 오버라이드
@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)
EntityGraph는 기본적으로 Left Outer Join을 내부적으로 사용한다.
📌 JPA Hint & Lock
✅ JPA Hint
JPA 쿼리의 실행 방식을 세밀하게 제어하기 위해 사용한다.
주로 성능 최적화나 구체적인 동작을 지정할 때 사용하는데, 예를 들어 캐시 사용방법, 쿼리 플랜 최적화, 특정 벤더의 옵션등을 설정할 수 있다.
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
위 처럼, Spring Data JPA에서는 @QueryHints 를 통해 Hint를 설정한다.
✅ JPA Lock
데이터 일관성을 유지하기 위해 특정 엔티티나 데이터에 대한 접근을 제어하는 메커니즘이다.
OPTIMISTIC, OPTIMISTIC_FORCE_INCREMENT, PESSIMISTIC_READ, PESSIMISTIC_WRITE 등의 잠금 모드가 있다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
위 처럼, Spring Data JPA에서는 @Lock 을 통해 쿼리 메서드에 잠금 모드를 지정할 수 있다.
'Backend > JPA' 카테고리의 다른 글
[실전! Querydsl] 1. 기본 문법 (1) | 2024.01.11 |
---|---|
[실전! 스프링 데이터 JPA] 3. 확장 기능 (0) | 2024.01.10 |
[실전! 스프링 데이터 JPA] 1. 공통 인터페이스 기능 (0) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 11. 객체지향 쿼리 언어 2 (1) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 10. 객체 지향 쿼리 언어 1 (0) | 2024.01.05 |
개발자가 되고 싶어요.