본문 바로가기

Develop/SPRING FRAMEWORK

Spring Data JPA가 제공하는 쿼리 메소드 기능 알아보기

스프링 데이터 JPA가 제공하는 공통 메소드 이외에 특정 도메인에 특화된 기능을 구현할 때 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능을 사용한다. 이때 총 3가지 방법이 존재한다. 이번 포스팅에서 알아보자!

1. 메소드 이름으로 쿼리 생성

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();
}

예를 들어 이름과 나이를 조건으로 가지는 조회 로직을 만들기 위해서는 원래 위와 같이 JPQL문을 작성해주어야 한다.

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
	List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
    ...
}

하지만 쿼리 메소드 기능을 사용하면 MemberRepository 인터페이스 내에 해당 코드만 작성해주면 스프링 데이터 JPA가 메소드 이름을 분석하여 알아서 JPQL을 생성하고 실행한다. (정말 마법 같다..)

 

 

Spring Data JPA - Reference Documentation

Example 109. 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의 큰 장점이다!

2. JPA NamedQuery

@NamedQuery(
        name="Member.findByUsername",
        query="select m from Member m where m.username = :username"
)
public class Member {
	...
}

다음과 같이 엔티티에 직접 JPQL 쿼리를 등록할 수 있다. 이때 이름은 관례상 도메인.클래스명을 따른다. 해당 쿼리는 어플리케이션 시작 시점에 오류를 잡아낼 수 있다. 이건 NamedQuery의 장점이다!

 

// 직접 JPA로 호출하는 방법
public List<Member> findByUsername(String username) {
  return em.createNamedQuery("Member.findByUsername", Member.class)
                  .setParameter("username", username)
                  .getResultList();
}
// 스프링 데이터 JPA를 사용하여 호출하는 방법
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);

직접 JPA을 사용하는 방법과, 스프링 데이터 JPA을 사용하여 NamedQuery를 호출하는 코드를 각각 작성했다.

 

이때 아래 코드에서는 이전과 달리 @Param("username")을 사용하였다. 왜냐하면 명확하게 JPQL을 작성했을 때는 NamedParameter가 넘어가야 한다. 또한 @Query(name = "Member.findByUsername")을 통해 호출될 NamedQuery을 지정해주었는데 사실 이건 작성해주지 않아도 정상적으로 실행된다. 스프링 데이터 JPA가 관례상 도메인클래스 + . + 메소드명 (여기서는 Member.findByUsername)으로 NamedQuery을 찾아서 실행한다. 물론 실행할 NamedQuery가 없다면 앞서 설명한 메소드 이름으로 쿼리 생성 전략을 따른다.

 

여담이지만 JPA NamedQuery는 실무에서 잘 사용하지 않는다고 한다. 왜냐하면 다음에 나올 친구가 너무 강력하기 때문,,!

3. @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);

 

실행할 메소드에 정적 JPQL 쿼리를 직접 작성해준다. 마찬가지로 @Param을 통해 파라미터를 넘겨줘야 한다. 또한 어플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 매우 큰 장점이 있다!

 

@Query("select m.username from Member m")
List<String> findUsernameList();

@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();

엔티티 조회 뿐만 아니라 단순히 값 하나를 조회하거나 DTO를 조회하는 경우에도 사용할 수 있다.

 

추가적인 내용은 더보기를 눌러서 확인하면 된다!

더보기

보너스1. 파라미터 바인딩

@Query("select m from Member m where m.username = :name") // 이름 기반
Member findMembers(@Param("name") String username);

@Query("select m from Member m where m.username = ?0") // 위치 기반
Member findMembers(String username);

JPQL 쿼리의 파라미터를 바인딩할 때 이름 기반/위치 기반 2가지 방법이 있다. 하지만 웬만해서는 항상 이름 기반을 사용하자!

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);

또한 컬렉션 타입 파라미터 바인딩을 통한 IN절도 지원한다.

보너스2. 반환 타입

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types

 

Spring Data JPA - Reference Documentation

Example 109. 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는 기본적으로 유연한 반환 타입을 지원한다. (해당 공식문서 참고)

List<Member> findByUsername(String name); // 컬렉션
Member findByUsername(String name); // 단건
Optional<Member> findByUsername(String name); // 단건 Optional

만약 위와 같이 단건 또는 컬렉션을 조회했는데 값이 없거나 많으면 아래와 같은 결과를 반환한다.

  단건 조회 컬렉션 조회
결과 없음 null 반환 빈 컬렉션 반환
결과가 2건 이상 X javax.persistence.NonUniqueResultException 예외 발생

사실 단건 조회했을 때 결과가 없다면 NoResultException 예외가 발생하지만 스프링 데이터 JPA가 편의성을 위해 null로 변환한다.