Spring/[인프런 김영한 실전 스프링 데이터 JPA]
[인프런 김영한 실전 스프링 데이터 JPA] 쿼리 메소드 기능
h2boom
2024. 9. 11. 20:30
쿼리 메소드 기능
- 스프링 데이터 JPA에서 제공하는 쿼리 메소드 기능
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
- 관례에 맞게 메소드 이름을 기반으로 쿼리를 생성한다.
ex) findByNameAndAge => select ... ~ where name = :name and age = :age - 이 방식은 조건이 늘어날수록 메소드 이름이 길어진다는 단점이 존재한다.
- 조회 - find...By, read...By, query...By, get...By로 메소드 명을 작성 가능
- ...에는 식별하기 위한 내용이 포함되어도 된다. ex) findMemberBy~
- By는 SQL의 where절과 같은 역할이다.
- COUNT - count...By 반환타입(long)
- EXISTS - exists...By 반환타입(boolean)
- 삭제 - delete...By, remove...By 반환타입(long)
- DISTINCT - findDistinct, findMemberDistinctBy ...
- LIMIT - findFirst3, findFirst, findTop, findTop3 ...
- 엔티티의 필드명을 변경 시 인터페이스에 정의한 메소드 명도 꼭 변경해줘야 한다.
JPA NamedQuery
- JPA NamedQuery는 실무에서 잘 사용하지 않는 방법
// Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username"
)
public class Member {...}
// JPA의 경우 Repository
public List<Member> findByUsername(String username) {
return em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
// Spring Data Jpa의 경우 Repository
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
- Entity에서 @NamedQuery로 이름과 쿼리를 지정해서 만든다.
- JPA의 경우 createNamedQuery() 메소드로 만들어진 NamedQuery를 호출할 수 있다.
- Spring Data JPA의 경우 @Query의 name 속성으로 인터페이스 메소드에 지정해줄 수 있다.
- 메소드 명은 임의로 작성해도 상관없다.
- 엔티티에서 JPQL을 사용해 쿼리를 작성해서 파라미터를 받았기 때문에 @Param으로 파라미터를 받아줘야한다.
- 기본적으로 Spring Data JPA는 NamedQuery를 먼저 찾고 없으면 메소드 명으로 쿼리를 작성해서 실행한다.
- NamedQuery는 오타와 같이 문법에 오류가 있으면 애플리케이션 로딩 시점에 알려준다는 장점이 있다.
레포지토리에 쿼리 직접 정의
//Spring Data JPA Repsoitory
@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로 직접 쿼리를 작성할 수 있다.
- 메소드 명을 임의로 작성할 수 있다.
- 파라미터는 @Param 어노테이션을 통해서 전달해야 한다.
- 쿼리 문법에 오타와 같이 오류가 있으면 애플리케이션 로딩 시점에 알려준다.
- 주로 실무에서 메소드 명으로 쿼리 생성하는 방식은 간단한 경우에 사용하고 복잡해지는 경우 @Query로 쿼리를 직접 정의해서 사용하는 방식으로 한다.
@Query DTO 조회
//DTO
public class MemberDto {
private Long id;
private String username;
private String teamName;
public MemberDto(Long id, String username, String teamName) {
this.id = id;
this.username = username;
this.teamName = teamName;
}
}
//Spring Data JPA Repository
@Query("select new study.data_jpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
- Spring Data JPA도 JPA와 마찬가지로 DTO를 조회하기 위해서 생성자와 new 연산자를 사용해야한다.
- DTO 클래스명을 적을 때는 전체 패키지를 모두 작성해야한다.
파라미터 바인딩
- 파라미터 바인딩에는 위치 기반 방식과 이름 기반 방식이 있다.
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반
- 코드 가독성과 유지 보수를 위해 이름 기반 파라미터 바인딩을 사용해야 한다.
//컬렉션 파라미터 바인딩
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
- Collection 타입으로 in절을 지원한다.
Spring Data JPA 반환 타입
- 반환 타입으로 컬렉션, 단건, Optional 등 유연하게 제공한다.
List<Member> findListByUsername(String username); //컬렉션
Member findMemberByUsername(String username); //단건
Optional<Member> findOptionalByUsername(String username); //단건 Optional
- Spring Data JPA로 컬렉션과 단건으로 조회하는 경우 데이터가 없으면 예외가 발생하지 않는다.
- 컬렉션의 경우에는 데이터가 없으면 빈 컬렉션이 들어간다.
- 단건의 경우 데이터가 없으면 Null이 들어간다.
Spring Data JPA 페이징, 정렬
- 페이징, 정렬 파라미터
- 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);
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
//페이징, 정렬 Test
@Test
public void paging() throws Exception {
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));
int age = 10;
//offset = 0 limit = 3으로 데이터를 가져오고 username을 기준으로 내림차순으로 정렬
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
//Page 내부의 데이터를 content로 가져온다.
List<Member> content = page.getContent();
long totalElements = page.getTotalElements(); //totalCount
assertThat(page.getNumber()).isEqualTo(0);
assertThat(page.getTotalPages()).isEqualTo(2);
assertThat(page.isFirst()).isTrue();
assertThat(page.hasNext()).isTrue();
}
- Page는 0부터 시작
- 파라미터 Pagable 구현체로 PageRequest를 많이 사용한다.
- 파라미터 Pagable은 쿼리에 limit, offset을 위한 용도이고 totalCount는 반환 타입으로 결정된다.
- 페이징 쿼리를 잘 사용하지 않는 이유는 totalCount 쿼리가 DB의 모든 데이터를 카운트해야 하기 때문에 성능이 느리다.
- 조인을 하는 경우 totalCount 쿼리의 성능이 더 나빠질 수 있기에 최적화가 필요하다.
- 조인을 하는 경우 totalCount 쿼리의 성능이 더 나빠질 수 있기에 최적화가 필요하다.
- 쿼리가 복잡해지는 경우 totalCount 쿼리를 최적화하기 위해서 쿼리를 분리할 수 있다.
- ex) left join을 하는 경우 조인을 하기 전과 count에는 영향이 없지만 totalCount 쿼리에서도 조인을 사용하여 성능이 나빠지기에 Count 쿼리를 분리함으로 조인을 하지 않도록 할 수 있다.
@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);
- @Query value 속성에는 Contents를 가져올 쿼리, countQuery 속성에는 Count 쿼리를 작성함으로 쿼리를 분리할 수 있다.
벌크성 수정 쿼리
- 벌크 수정 연산을 JPA에서는 Update 쿼리와 executeUpdate() 메소드를 사용한다.
@Modifying
@Query("update Member m set m.age = m.age+1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- Spring Data JPA에서 벌크 수정 연산은 @Modifying 어노테이션을 메소드에 추가해줘야 한다.
@Modifying
@Query("update Member m set m.age = m.age+1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
@Test
public void bulkUpdate() throws Exception {
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
int resultCount = memberRepository.bulkAgePlus(20);
List<Member> result = memberRepository.findByUsername("member5");
Member member5 = result.get(0);
System.out.println("member5 = " + member5);
}
- 예제에서 member5 엔티티는 save() 호출 시에 영속성 컨텍스트에 나이 40으로 저장이 되어 있다.
하지만 DB상에서는 벌크 연산으로 인해 나이 41로 저장되어 있다.- member5 엔티티를 조회하면 영속성 컨텍스트에 존재하는 데이터이기에 영속성 컨텍스트에서 데이터를 찾아서 가져오는데 그 결과 영속성 컨텍스트에서는 나이가 40, DB에서는 41로 데이터가 불일치하게 된다.
- 벌크 연산 주의사항
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날린다.
=> 영속성 컨텍스트에 해당 데이터가 존재하는 경우 DB의 데이터와 불일치하게 된다. - 벌크 연산 이후에는 필수적으로 영속성 컨텍스트를 모두 초기화 시켜야한다.
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 날린다.
//영속성 컨텍스트 초기화 방법
//1번
@Modifying(clearAutomatically = true)
//2번
em.flush();
em.clear();
- 1번 방식은 Spring Data JPA에서 지원하는 방법으로 @Modifying clearAutomatically = true 속성을 사용하면 해당 쿼리가 수행된 후 영속성 컨텍스트를 초기화 시킨다.
- 2번 방식은 em.save()와 같은 기능을 수행 후 flush()를 통해 영속성 컨텍스트의 내용을 DB로 저장시키고 clear()로 영속성 컨텍스트를 초기화 시키는 방식이다.
@EntityGraph
- @EntityGraph : Spring Data JPA에서 연관된 엔티티들을 SQL 한번에 조회하는 방법이다.
- fetch join을 편리하게 할 수 있도록 돕는 기능
//직접 JPQL을 작성해서 fetch join 하는 방식
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
//@EntityGraph를 사용하는 방식
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
- N+1 문제를 해결하기 위해서 @Query로 직접 JPQL로 작성해서 fetch join을 하는 방식과 @EntityGraph를 사용하는 방식이 있다.
- 두 가지 방법 모두 내부적으로는 fetch join을 사용하는 것이다.
- @Query로 직접 JPQL을 작성하고 @EntityGraph를 사용할 수도 있다.
- fetch join은 한 번의 조회 쿼리로 연관된 엔티티 모두를 가져오는 방식
- 일반 join과 달리 fetch join은 조회 대상뿐만 아니라 조인 대상까지 영속성 컨텍스트에 저장한다.
JPA Hint & Lock
- JPA Hint : JPA 쿼리 힌트를 의미한다. (SQL 힌트가 아닌 JPA 구현체(하이버네이트)에게 제공하는 힌트)
- Hint : 기존에 실행되어야 하던 일부 로직을 개선하여 최적화를 하는 것
- JPA 변경 감지(Dirty Checking)의 단점은 원본이 있어야하기에 객체를 2개(원본, 변경본) 관리해야 한다.
- 만약 데이터를 수정하지 않고 조회용으로만 사용하려면 readOnly 옵션을 사용할 수 있다.
=> 객체를 2개 관리하지 않아도 된다.
- 만약 데이터를 수정하지 않고 조회용으로만 사용하려면 readOnly 옵션을 사용할 수 있다.
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
- @QueryHint 어노테이션을 사용해서 Hint를 사용할 수 있다.
- 조회용이기에 데이터 변경 감지가 발생하지 않는다.
- JPA Lock : Spring Data JPA에서 DB의 비관적 락(Pessimistic Lock) 기능을 제공하는 것
- @Lock 어노테이션을 사용한다.
- Pessimistic Lock (비관적 락) : 모든 트랜잭션은 충돌이 발생한다고 가정하고 Lock을 거는 방식
- select for update 구문을 사용한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
- 실시간 트래픽이 많은 서비스에서는 가급적 락을 사용해서는 안된다.
출처 : [인프런 김영한 실전 스프링 데이터 JPA]
실전! 스프링 데이터 JPA 강의 | 김영한 - 인프런
김영한 | 스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제
www.inflearn.com