Spring Data JPA는 메소드 이름을 분석하여 JPQL을 생성하고 실행한다.
동작 방식을 살펴보기 위해 순수 JPA코드와 Spring Data 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를 사용하여 username, age를 조건으로 구성한 JPQL이다.
//Spring Data JPA- 메소드 이름으로 쿼리 생성
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
//Spring Data JPA- 메소드 이름으로 쿼리 생성
List<Member> findTop3TestBy();
같은 동작을 하는 코드를 Spring Data JPA로 작성한 코드이다.
findByUsernameAndAgeGreaterThan이름의 메소드를 선언하게 되면 Spring Data JPA는
...where m.username=:username and m.age>:age의 JPQL를 생성하게 된다.!
공식문서에 나와있는 쿼리 메소드 기능중 조회를 간단히 살펴보자면,
find...By = read...By 등으로 메소드를 설계가 가능하다.
...의 부분에는 메소드를 식별하기 위한 설명을 자유롭게 추가 가능하고 By 뒷부분부터는 조건문이 위치하게 된다.
만약 By절 뒤에 아무것도 적지 않는다면 아무런 조건을 사용하지 않는것이다.
이밖에도,
많은 쿼리 메소드 기능을 제공한다. 아래 Spring 공식문서를 통해 자세히 볼수 있다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
이처럼 Spring Data JPA는 메소드 이름을 통해 정적 쿼리를 미리 정의할 수 있기 때문에 컴파일 시점에서 문법적인 오류를 조기에 발견할 수 있는 큰 장점이 있다!!!
하지만 조건으로 사용하는 필드가 많아지면, 메소드의 이름이 장황해지는 단점이 발생한다.
이를 해결할 수 있는 방식을 뒤에 소개해보겠다.
참고💡
조건문에 넣을 필드가 2이하까지는 "메소드 이름으로 쿼리 생성 방식"을 사용하고 초과할때부터는 뒤에 나오는 "@Query, 레포지토리 메소드 쿼리정의" 방식을 사용하는것이 좋다고 한다.
이 방식은 @NamedQuery 어노테이션을 사용하여 쿼리를 미리 정의해논후 이름을 할당해 해당 이름을 호출하여 사용하는 방식이다.
마찬가지로 순수 JPA 코드와 Spring Data JPA 코드를 비교하며 공부해보겠다.
...
@NamedQuery( //Named 쿼리
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)
public class Member {
...
}
Entity에 @NamedQuery를 사용하여 쿼리의 이름과 JPQL을 미리 선언하게 된다.
//순수 JPA - Named 쿼리 호출
public List<Member> findByUsername(String username){
return em.createNamedQuery("Member.findByUsername")
.setParameter("username", username)
.getResultList();
}
createNamedQuery를 사용하여 미리 정의해논 쿼리이름을 호출한 후,
필요한 파라미터를 세팅해주는 방식으로 사용할 수 있다.
//Spring Data JPA - Named 쿼리 호출
@Query(name = "Member.findByUsername") //생략 가능
List<Member> findByUsername(@Param("username")String username);
@Query 어노테이션을 사용하여 쿼리문 이름을 호출할수 있게 된다.
또한 필요한 파라미터들은 @Param 어노테이션을 통해 매핑 시켜준다.
참고로
@Param("username")) -> "username"이름과
select ... =:username" -> ":"username"이름이 같아야지 정상적으로 파라미터 매핑이 된다.!
또한 위의 메소드에서는 @Query 어노테이션이 없어도 정상동작하게 된다.
왜냐하면,
Spring Data JPA는 정의한 메소드의 JPQL문을 생성할때 아래와 같은 순서가 있기 때문이다.
@NamedQuery를 먼저 찾는다. 메소드 이름으로 쿼리 생성을 통해 JPQL을 생성한다.이 방식은 메소드 이름으로 쿼리 생성과 마찬가지로 @NameQuery 를 통해 정적쿼리를 미리 정의하기 때문에 컴파일 시점에서 문법 오류를 발견할수 있는 장점이 있다.!!
하지만 "엔티티에 JPQL과 쿼리 이름을 미리 정의하고 정의한 이름을 호출"와 같이 번거로움이 많이 발생한다.
이러한 @NameQuery의 장점과 단점을 보안한 방식이 다음 나올 방식이다.
이 방식은 @NamedQuery 방식처럼 엔티티에 미리 쿼리문을 생성하는것이 아니라,
레포지토리 메소드에 @Query 사용하여 바로 JPQL문을 작성할수 있는 방식이다.
//Spring Data JPA - 리포지토리 메소드에 쿼리 정의하기
@Query("select m from Member m where m.username= :username and m.age= :age")
List<Member> findMember(@Param("username") String username, @Param("age") int age);
보다시피 @Query 어노테이션에 JPQL문을 바로 작성할 수 있다.
실행할 메서드에 정적쿼리를 바로 작성하므로 이름 없는 Named 쿼리라고 할 수 있다.
실무에서 가장 많이 사용하는 방식이라고 한다.
정적 쿼리에 대해서 작성할때 파라미터가 소수일때는 "메소드 이름으로 쿼리생성" 방식을 사용하고 파라미터가 증가할때는 "@Query,레포지토리 메소드에 쿼리 정의"을 사용하면 될거 같다!
그 밖에도 DTO로 직접 조회하기, 파라미터 바인딩, 다양한 반환타입 등도 있다.
DTO 직접 조회 같은 경우에는 QueryDSL방식이 더 편한거 같다.
(쿼리 결과를 DTO로 생성할때 생성자를 쓰는 방식이 QueryDSL이 더 편하기 때문에)
Spring Data JPA가 내부적으로 try~ catch로 처리하여 null을 대신 반환해준다.