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 쿼리를 최적화하기 위해서 쿼리를 분리할 수 있다.
    • 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개 관리하지 않아도 된다. 
@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]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA 강의 | 김영한 - 인프런

김영한 | 스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제

www.inflearn.com