관리 메뉴

나구리의 개발공부기록

쿼리 메소드 기능, 메소드 이름으로 쿼리 생성, JPA NamedQuery, @Query - 리포지토리 메소드에 쿼리 정의, @Query - 값/DTO 조회, 파라미터바인딩, 반환타입 본문

인프런 - 스프링부트와 JPA실무 로드맵/실전! 스프링 데이터 JPA

쿼리 메소드 기능, 메소드 이름으로 쿼리 생성, JPA NamedQuery, @Query - 리포지토리 메소드에 쿼리 정의, @Query - 값/DTO 조회, 파라미터바인딩, 반환타입

소소한나구리 2024. 10. 26. 17:49

출처 : 인프런 - 실전! 스프링 데이터 JPA (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


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

1) 순수 JPA로 이름과 나이를 기준으로 회원을 조회 및 테스트

(1) MemberJpaRepository

  • 순수 JPA로 구현하려면 직접 JPQL로 쿼리를 작성해야만 가능함
// 이름과 나이를 기준으로 회원을 조회 -> 순수 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", Member.class)
            .setParameter("username", username)
            .setParameter("age", age)
            .getResultList();
}

 

(2) MemberJpaRepositoryTest

  • 테스트 코드를 작성한 후 실행해보면 작성해둔 JPQL쿼리가 정상적으로 실행되며 테스트가 성공함
@Test
public void findByUsernameAndAgeGreaterThan() {
    Member m1 = new Member("AAA", 10);
    Member m2 = new Member("AAA", 20);
    memberJpaRepository.save(m1);
    memberJpaRepository.save(m2);

    List<Member> result = memberJpaRepository.findByUsernameAndAgeGreaterThan("AAA", 15);
    assertThat(result.get(0).getUsername()).isEqualTo("AAA");
    assertThat(result.get(0).getAge()).isEqualTo(20);
}

 

2) 스프링 데이터 JPA로 동일한 기능을 구현 및 테스트

(1) MemberRepository

  • 위의 기능을 스프링 데이터 JPA로 메서드를 생성,
  • 즉 구현하는것이아니라 메소드만 정의
public interface MemberRepository extends JpaRepository<Member, Long> {

    // 이름과 나이를 기준으로 회원을 조회 -> 스프링 데이터 JPA 쿼리 메소드 기능을 활용
    // 형식에 맞춰서 메소드 이름만 규칙에 맞춰서 작성하면 쿼리를 안짜도 됨
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

 

(2) MemberRepositoryTest

  • 동일한 테스트 케이스로 MemberRepository의 메소드로 테스트를 수행하보면 테스트도 통과하며 쿼리도 통과함
  • 실제 구현을 하지 않았는데 관례대로 메소드 이름을 스프링 데이터 JPA의 리포지토리에 정의만 해두면, 메소드 이름으로 스프링 데이터 JPA가 알아서 쿼리를 만들어서 실행하는데, 이것이 쿼리 메소드 기능임
  • 규칙대로 꼭 입력해주어야 쿼리메소드 기능이 실행되며 틀리면 당연히 실행되지 않으며, 조건없이도 쿼리메소드기능을 만들 수 있음

(3) 메소드 분석

  • findBy : 조회
  • Username, Age : 필드명
  • And : and 연산
  • GreaterThan : > 연산
  • 즉 Username = 비교대상 And Age > 비교대상 처럼 조건이 완성됨,

3) 쿼리 메소드 필터 조건 및 주요 쿼리 메소드 기능 살펴보기

  • 공식 문서
  • 자세한 내용 및 다양한 기능은 공식문서를 읽어보고 한번 사용해보는 것을 권장

(1) 조회

  • find...By, read...By, query...By, get...By
  • ...에는 식별하기 위한 내용이나 설명이 들어가도되며 생략해도 됨

(2) COUNT

  • count...By
  • count 연산기능, 반환타입은 long

(3) EXISTS

  • exists...By
  • exists(조건이 만족하는지의 여부를 확인) 기능, 반환타입은 boolean

(4) DISTINCT

  • findDistinct, findMemberDistinctBy
  • 중복제거기능

(5) LIMIT

  • findFirst3, findFirst, findTop, findTop3
  • limit 기능, First, Top 뒤에 숫자를 입력하면 원하는 개수만큼 가져올 수 있으며 입력하지 않으면 1개만 가져옴
  • 역순으로 자르는 것은 없고 역순 정렬로 자르면 됨
  • ex) findFirst10ByOrderByIdDesc()

** 참고

  • 단점은 조건이 조금만 더 추가되어도 메소드의 이름이 엄청 길어지기때문에 많아도 2개까지만 쿼리메소드기능을 사용해서 해결하고, 조건이 더 많아지면 다른방법으로 해결하는것을 권장함(바로 배움)
  • 그리고 쿼리메소드의 매우 중요한 장점 중 하나는, 여러명이서 개발했을 때 누군가 엔터티를 변경하게되면 애플리케이션 실행 시점에 컴파일 에러가 발생하기 때문에 미연에 오류를 찾아낼 수 있음

2. 쿼리 메소드 기능 - JPA NamedQuery

** 참고

  • @NamedQuery 애노테이션을 사용하는 기능이며 실무에서 거의 사용할일이 없기때문에 간략히 설명

1) 사용방법

  • 엔터티나 XML문서에 @NamedQuery 애노테이션을 입력하여 쿼리를 입력

(1) Member 수정

  • Member Entity에 @NamedQuery애노테이션을 입력하여 쿼리를 추가
  • name : 쿼리의 이름
  • query : 실제 쿼리를 작성, 파라미터 바인딩도 사용할 수 있음
@NamedQuery(
        name="Member.findByUsername",
        query="select m from Member m where m.username = :username")
public class Member {
    // ... 기존 코드 생략
}

 

(2) 순수 JPA에서 네임드 쿼리를 호출하는 방법

  • createNamedQuery메소드로 네임드 쿼리의 이름 인자로 입력
  • 그 뒤는 순수 JPA 사용방법과 동일하며, 테스트를 실행하보면 정상적으로 쿼리가 실행됨(테스트 코드 생략)
// 네임드 쿼리 호출
public List<Member> findByUsername(String username) {
    return em.createNamedQuery("Member.findByUsername", Member.class)
            .setParameter("username", username)
            .getResultList();
}

 

(3) 스프링 데이터 JPA에서 네임드 쿼리 호출

  • @Query 애노테이션의 name으로 작성한 네임드 쿼리의 이름을 호출
  • 작성한 NamedQuery에 파라미터 바인딩 코드가 있으면 @Param으로 바인딩 이름을 설정해주어야 쿼리가 실행되며, 만약 NamedQuery에 파라미터 바인딩 쿼리가 없다면 @Param이 없어도 됨
  • 마찬가지로 테스트를 진행해보면 쿼리가 정상적으로 수행되는 것을 확인할 수 있음(테스트코드는 생략)

** 참고

  • 해당 코드에서 @Query 애노테이션 부분을 주석처리해도 정상 동작하는데, 그 이유는 스프링 데이터 JPA의 쿼리메소드기능은 동작 순서가 먼저 작성된 메소드의 이름과 동일한 이름으로 작성한 네임드 쿼리가 있는지 먼저 찾은 뒤에 없으면 위에서 배운 메소드이름으로 쿼리를 생성하도록 동작하기 때문임
  • 네임드 쿼리를 조회하는 엔터티는 JpaRepository를 상속 받을때 작성한 제네릭 타입의 Entity에서 찾음
public interface MemberRepository extends JpaRepository<Member, Long> {
    // ... 기존 코드 생략
    @Query(name = "Member.findByUsername")
    List<Member> findByUsername(@Param("username") String username);
}

2) 잘 사용하지 않는 이유와 네임드 쿼리만의 장점

  • 리포지토리에 바로 쿼리를 작성할 수 있는 기능이 있기 때문에 굳이 엔터티에 쿼리를 작성해서 쿼리를 분산해서 관리할 필요가 없으며 오히려 유지보수만 어려워 질 수 있음
  • 그러나 애플리케이션 로딩시점에 네임드 쿼리에 작성한 쿼리를 한번 파싱을해서 보관하기 때문에 쿼리에 오류가있으면 애플리케이션이 실행되지않고 에러를 미리 확인할 수 있는 장점을 가지고 있지만 많이 사용하지는 않음

3. 쿼리 메소드 기능 - @Query, 리포지토리 메소드에 쿼리 정의하기

1) 실무에서 자주 사용하는 @Query

(1) 스프링 데이터 JPA의 리포지토리의 메소드에 바로 쿼리를 정의

  • 메소드 위에 @Query 애노테이션으로 JPQL 쿼리를 작성할 수 있으며 파라미터 바인딩도 사용할 수 있음
  • 메소드이름으로 쿼리 생성할 때의 단점으로 메소드의 이름이 길어지는 것인데, 이 기능은 조건이 많고 복잡한 쿼리가 많아도 사용할 메소드의 위에 쿼리를 직접 작성함으로써 간편하고 쉽게 복잡한 쿼리를 작성할 수 있음
@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);

 

(2) 장점

  • @Query를 작성한 쿼리는 문자임에도 쿼리에 오타가 있거나 잘못 작성하면 애플리케이션 로딩 시점에 파싱하여 보관하고 있기 때문에 에러가 발생했는지 미리 확인할 수 있음
  • 즉, 네임드쿼리와 동일하게 동작하지만 훨씬 유지보수성 뿐만아니라 관리하기도 좋기 때문에 네임드쿼리를 사용할 일이 없어지는 이유이며 대부분의 정적쿼리에서의 문제는 @Query를 사용하여 해결이 가능함(동적 쿼리는 Querydsl로 해결)

4. @Query, 값 / DTO 조회하기

  • 엔터티가 아닌 단순 값이나, DTO를 조회하는 방법 (실무에서 자주 사용함)

1) 단순히 값 하나를 조회

  • 필드 값으로 조회할 수 있고, 임베디드 타입(값 타입)도 동일한 방식으로 조회할 수 있음
@Query("select m.username from Member m")
List<String> findUsernameList();

2) DTO로 직접 조회

(1) 조회할 Dto 생성

@Data
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;
    }
}

 

(2) 스프링 데이터 JPA에서 DTO로 직접 조회

  • DTO로 직접 조회할 때에는 순수 JPA도 마찬가지로 new 연산자를 사용하기 때문에 패키지 이름을 전부 작성해주어야함
  • DTO에서 소속된 team의 이름도 조회해야하기에 join으로 Member와 Team에서 정보를 가져오도록 Query를 작성
  • 이런 부분은 나중에 배울 Querydsl을 사용하면 매우 편리하게 작성할 수 있도록 개선할 수 있음
// dto로 조회, new 연산자를 사용하기 때문에 패키지를 전부 입력해야함
@Query("select new study.data_jpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();

 

(3) Test 

  • 테스트로 조회된 값을 출력해보면 DTO에서 정의한 값들이 출력되는 것을 확인할 수 있음(물론 실무에서는 Assertions로 검증을 하여 테스트를 작성해야 함)
@Test
public void findMemberDto() {
    Team teamA = new Team("teamA");
    teamRepository.save(teamA);

    Member m1 = new Member("AAA", 10);
    m1.setTeam(teamA);
    memberRepository.save(m1);

    List<MemberDto> memberDto = memberRepository.findMemberDto();
    for (MemberDto dto : memberDto) {
        System.out.println("dto = " + dto);
    }
}

5. 파라미터 바인딩

1) 파라미터 바인딩 종류

  • 위치 기반
  • 이름 기반
  • 코드 가독성과 유지보수를 위해서 위치 기반은 사용하지 말고 이름 기반만 사용해야 함
  • 위치 기반은 순서가 바뀌거나 엔터티가 변경되면 대참사가 일어날 수 있음
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반

2) 컬렉션 파라미터 바인딩

  • 쿼리를 in 절을 활용하여 작성하고 Collection 타입으로 파라미터 바인딩의 값을 입력할 수 있음
  • 다양한 컬렉션으로 인자를 입력할 수 있도록 조상타입인 Collection으로 받는것을 권장함
@Query("select m from Member m where m.username in :names")
List<Member> findBynames(@Param("names") Collection<String> names);

6. 반환 타입

1) 스프링 데이터 JPA의 반환타입

List<Member> findListByUsername(String username);             // 컬렉션
Member findMemberByUsername(String username);                 // 단건
Optional<Member> findOptionalByUsername(String username);     // Optional

 

(1) 컬렉션

  • 조회 결과가 많을때 사용
  • 결과가 없으면 빈 컬렉션을 반환(null이 아님)
  • 그래서 컬렉션으로 조회한 뒤에 예외처리를 한다고 != null 이런식으로 검증하는 코드를 짤 필요없이 그대로 받아도 됨

(2) 단건 조회

  • Entity, Dto로 직접 반환하거나 Optional로 감싸서 반환할 수 있음
  • 결과가 없으면 null을 반환하고 결과가 2건 이상이면 NonUniqueResultException 예외가 발생하며, Spring Framework Exception으로 변경되어 IncorrectResultSizeDataAccessException으로 반환하기 때문에 리포지토리 기술이 바뀌어도 클라이언트의 코드를 바꾸지 않아도 되는 장점이 존재함
  • 즉, 단건 조회할 때는 Optional으로 반환하여 orElse 등으로 비어있을 때와 결과가 2건일 때의 예외만 처리해주면 됨

** 참고

  • 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 getSingleResult메서드를 호출하여 조회 결과가 없으면 순수 JPA처럼 NoResultException을 반환해야하는데, 내부적으로 null로 반환되도록 동작하게 정의 되어있음
  • 예외를 반환하는것과 null을 반환하는 것중에 어떤것이 더 좋은지에 대해서는 논쟁의 여지가 있지만 개발을 할 당시에는 예외를 반환하는 것보다 null이 반환되는 것이 조금 수월한 면이 있음
  • 그러나 Java8 이후로는 Optional이라는 null에 안전한 반환타입이 있기 때문에 예외처리를 기존보다 더 간편하게 할 수 있음