[Spring Data JPA] 쿼리
쿼리 메서드
Spring Data JPA에서 지원하는 강력한 기능이다. 개발자가 작성하는 간단한 쿼리들은 단순히 interface에 메서드 이름만 규칙에 맞게 명명하면 사용할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
public List<Member> findTop3SomethingBy();
}
실제 구현은 스프링이 해주게 되고 실행 시점에서 오타 혹은 실제 property가 없는 등의 이슈들을 미리 확인해서 안전한 개발을 할 수 있다. 인자가 많아짐에 따라 메서드 명이 길어진다는 단점은 있지만 인자가 적은 경우에는 쿼리를 모두 작성할 필요가 없는 장점이 있다.
이렇게 간단한 쿼리들은 쿼리 메서드로 해결 가능하지만 비즈니스 로직에서 복잡한 쿼리를 날려야할 때 문제가 생길 수 있다. 그를 위한 방법이 JPA NamedQuery이다.
JPA NamedQuery
JPA NamedQuery는 쿼리에 이름을 부여하여 사용할 수 있도록 만들어 주는 기능이다. NamedQuery를 사용하는 방법은 크게 XML파일에 작성하거나 Entity 객체에 `@NamedQuery` 어노테이션과 함께 작성하는 방법이 있다. 예시를 보자.
@NamedQuery(name = "Member.ageFind", query = "select m from Member m where m.age >= :age")
public class Member {
...
}
JPA 기반으로 구현시 다음과 같이 작성한다.
@Repository
public class MemberJpaRepository {
...
public List<Member> ageFind(int age) {
return em.createNamedQuery("Member.ageFind", Member.class).setParameter("age", age)
.getResultList();
}
}
Spring Data JPA는 이 또한 간단하게 만들어 준다.
public interface MemberRepository extends JpaRepository<Member, Long> {
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
public List<Member> findTop3SomethingBy();
// @Query(name = "Member.ageFind") //Spring Data JPA가 생략해도 작동하게끔 도와준다.
public List<Member> ageFind(@Param("age") int age);
}
NamedQuery의 가장 큰 장점은 정적 쿼리로서 어플리케이션이 쿼리를 점검해서 오류가 있을 때 미리 알려준다는 점이다.
하지만, 실무에서는 이 방법을 잘 사용하지 않는다고 한다.
본인이 느끼기에 엔티티에 NamedQuery가 있어 무언가 역할이 분리되지 않은 것 같기도 하다. 이와 같은 기능을 하는 대체제가 있기에 이 기능을 사용할 필요가 없기도 하다.
@Query
1. 리포지토리 메소드에 쿼리 정의하기
NamedQuery의 기능을 Repository안에서 어노테이션과 더불어 사용할 수 있다. 따라서 쿼리가 분산되지 않도록 정리할 수 있으며 이 또한, 컴파일시 오류 체크를 받을 수 있다는 장점이 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@Query("select m from Member m where m.username >= :username")
public List<Member> usernameFind(@Param("username") String username);
}
2. 값, DTO 조회하기
값의 경우 JPQL 문법에 따라 객체 그래프 탐색을 하여 받아올 수 있고, DTO 객체에 담아 받아오고 싶다면 new 객체와 함께 생성자를 사용하여 객체를 만들듯이 사용하여 DTO에 담아 반환할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@Query("select m.username from Member m")
public List<String> usernameList();
@Query("select new study.datajpa.MemberDTO(m.id, m.username, t.name) from Member m join m.team t")
public List<MemberDTO> findMemberDTO();
}
파라미터 바인드
JPQL 문법에 따라 위치 기반과 이름 기반으로 나뉘나 위의 예제들처럼 이름 기반으로 하는 것이 좋다.
select m from Member m where m.username = ?0
select m from Member m where m.username = :username
다중 파라미터 검색
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@Query("select m from Member m where m.username in :names")
public List<Member> findByNames(@Param("names") Collection<String> names);
}
반환 타입
Spring Data JPA는 유연한 반환 타입을 지원함.
public List<Member> findListByUsername(String username);
public Member findMemberByUsername(String username);
public Optional<Member> findOptionalByUsername(String username);
Spring Data JPA - Reference Documentation
Example 119. 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
offset과 limit을 설정하여 List 형태로 받아올 수 있다.
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
...
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", Member.class)
.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();
}
}
자세한 설명은 다음 글을 참조하자.
[JPA이론] 10. 객체지향 쿼리 언어1 - 기본 문법
소개 JPA는 다양한 쿼리 방법을 지원하는데 그 예시로는 JPQL, JPA Criteria, QueryDSL, 네이티브 SQL 등의 방식이 있다. 앞의 3개는 자바코드로 JPQL을 짜주는 클래스의 집합이라고 이해하면 된다. 이외에
dul2.tistory.com
Spring Data JPA
페이징과 정렬을 공통화 시켰다.
public Page<Member> findByUsername(String username, Pageable pageable);
public Slice<Member> findByUsername(String username, Pageable pageable);
public List<Member> findByUsername(String username, Pageable pageable);
public List<Member> findByUsername(String username, Sort sort);
Page는 보통 우리가 Pagenation에 사용하는 객체이며 Slice는 원하는 갯수 + 1 만큼 가져와서 추후 더보기와 같은 기능을 구현할 떄 + 1의 값을 사용해 다시 쿼리를 날려 데이터를 받아오는데 사용할 수 있는 객체이다.
참고로 Page의 부모는 Slice이다. 따라서 사용시 캐스팅될 수 있음을 알고 사용하는 것이 좋다.
Count 쿼리 분리도 가능하다. Count의 경우 데이터 갯수 알면 될 수도 있어 굳이 join을 하여 성능 저하시킬 이유가 없을 수도 있다. 따라서 따로 Count 쿼리를 분리하여 성능 최적화에 도움을 줄 수 있다.
@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m) from Member m")
Page<Member> findBy(Pageable pageable);
Page Interface 내부에는 map이 있어 쉽게 DTO로 변경이 가능하다.
Page<MemberDTO> map = pageByAge.map(member -> new MemberDTO(member.getId(), member.getUsername(), null));
Spring - Paging 처리
Pagination 본인은 전통적인 서블릿 JSP 기반 프로젝트에서 페이지네이션을 하기 위에 List를 받아와 나누어 사용하는 Paging 알고리즘을 이용하여 해본 경험 뿐이 없었다. Pagination을 깔끔하고 이쁘게
dul2.tistory.com
벌크성 수정 쿼리
JPA는 벌크로 데이터를 수정하는 쿼리를 지원한다.
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
update 쿼리는 `@Modifying` 어노테이션이 필요하다. 있어야 executeUpdate() 메소드를 호출하고 없을시에는 select 쿼리처럼 getSingleResult() 혹은 getResultList() 메소드를 호출하여 예외(InvalidDataAccessApiUsageException)가 발생한다.
JPA의 벌크 연산은 영속성 컨텍스트에 반영이 되지 않고 바로 DB로 업데이트 쿼리를 쏘기 떄문에 JPA에서 처럼 EntityManager를 통해 컨텍스트를 clear해주어 영속성 컨텍스트를 DB와 동기화할 수 있도록 해주는 것이 좋다. Spring Data JPA는 그 기능을 `clearAutomatically = true` 옵션에 true로 설정함으로써 update 이 후 영속성 컨텍스트가 clear 되도록 설정할 수 있다.
@EntityGraph
Spring Data JPA는 `@EntityGraph(attributePaths = "team")`어노테이션을 통해 순수 JPA에서는 fetch 조인을 통해 채워야했던 Entity객체에 대해서 프록시가 아닌 진짜 데이터를 찾아오기 위한 기능을 제공한다.
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
||
\/
@Override
@EntityGraph(attributePaths = "team")
List<Member> findAll();
@Query("select m from Member m")
@EntityGraph(attributePaths = "team")
List<Member> findMemberEntityGraph();
위의 2개의 메서드는 직접 fetch 조인 쿼리를 작성한 메서드와 같은 기능이다. `@EntityGraph` 어노테이션을 명시하여 객체의 원하는 필드에 대해 자동으로 fetch 조인이 이루어준다.
또한, 다음과 같이 Spring Data JPA의 쿼리 메서드 기능에도 사용할 수 있다.
@EntityGraph(attributePaths = "team")
List<Member> findEntityGraphByUsername(String username);
NameQuery처럼 NamedEntityGraph를 사용할 수도 있다.
@NamedQuery(name = "Member.ageFind", query = "select m from Member m where m.age >= :age")
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
public class Member {
...
}
public interface MemberRepository extends JpaRepository<Member, Long> {
...
@EntityGraph("Member.all")
List<Member> findNamedEntityGraphByUsername(String username);
}
프록시와 `@EntityGraph`의 동작 원리를 이해하기 위해서 JPA의 프록시와 지연로딩에 대한 이해가 도움이 된다.
[JPA이론] 8. 프록시와 연관관계 관리
프록시 프록시는 가짜 엔티티 객체를 말한다. JPA를 사용하다 보면 자주 만날 수 있는 용어인데 왜 프록시 객체가 필요한 것일까? 먼저, 프록시가 무엇인지 알아보기 위해 프록시 객체를 만들어
dul2.tistory.com
JPA Hint & Lock
JPA Hint - ReadOnly
JPA Hint는 JPA 구현체(보통 Hibernate)에게 제공하는 힌트를 말한다.
JPA는 더티 체킹을 위해 원본과 수정될 정보를 저장할 객체, 스냅샷을 만들어 운영을 하게 된다. 만약 단순 조회를 위한 데이터 호출을 한다면 수정될 객체를 만드는 것은 비효율적인 자원 활용이므로 Hibernate(JPA 표준은 지원하지 않음)는 자원 활용 최적화 기능을 제공한다.
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyById(Long id);
위와 같이 `@QueryHints` 어노테이션을 사용하게 되면 JPA 구현체는 힌트를 통해 성능 최적화를 진행하게 된다. 따라서 다음과 같은 예제에서 볼 수 있듯 readOnly 옵션을 true로 주게 되면 더티 체킹을 위한 스냅샷이 생기지 않으므로 수정을 해도 영속성 컨텍스트와 DB에 업데이트가 되지 않는다.
@Test
public void queryHint() {
//given
Team teamA = Team.builder().name("teamA").build();
Team teamB = Team.builder().name("teamB").build();
teamRepository.save(teamA);
teamRepository.save(teamB);
Member member1 = Member.builder().username("member1").age(10).team(teamA).build();
memberRepository.save(member1);
em.flush();
em.clear();
//when
Member findMember = memberRepository.findReadOnlyById(member1.getId());
findMember.changeTeam(teamB);
em.flush();
em.clear();
//then
Member findMember1 = memberRepository.findReadOnlyById(member1.getId());
assertThat(findMember1.getTeam().getName()).isEqualTo("teamA"); //true
}
TeamA에서 TeamB로 바꾸려 했지만 최종적으로는 변경되지 않는다.
Hint를 사용하여 최적화를 하면 좋지만 모든 조회용 쿼리에 Hint 어노테이션을 붙여 튜닝하는 것은 튜닝 작업에 소모되는 시간 대비 성능 개선이 비효율적일 확률이 높다. 전체 어플리케이션에서 트래픽이 많고 성능 최적화가 필요하다고 판단되는 부분에 있어 먼저 성능 테스트를 진행 후 결과를 보고 의사결정을 통해 도입하는 것이 좋다.(하지만, 조회 성능이 부족할 정도가 되면 로직 앞단에 cache를 놓아 해결하게 되고 그에 따라 JPA Hint로 인한 이 점이 줄어들 수 있다.)
Update Lock
Lock은 DB에서 제공하는 Lock과 같은 기능이다. Lock은 트랜잭션 처리의 순차성을 보장하기 위한 방법인데 데이터에 동시 접근하게 되어 데이터가 오염되는 일을 방지하기 위해 쓰인다. 예를들어 은행의 송금이나 대학교의 수강 신청 시스템과 같이 중요하면서도 동시 접근되어 문제가 생길 수 있는 일을 막기 위한 곳에서 쓰일 수 있다..
Spring Data JPA는 이 Lock 기능을 쉽게 사용하기 위해 `@Lock` 어노테이션을 제공한다. Hint와 달리 LockModeType은 akarta.persistence 패키지 내부에 있으며 따라서 JPA에서 지원을 한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
Member findLockById(Long id);
실행해 보면 쿼리에 lock이 걸려 나가는 것을 볼 수 있다.
Lock은 실시간 트래픽이 많은 서비스에서는 가급적 쓰는 것이 좋지 않지만 돈과 같이 동기화가 중요한 곳에서는 DB에서 제공하는 PESSIMISTIC Lock을 사용할 수 있다.