기록

[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA 기능/쿼리 메서드, 페이징과 정렬, 벌크 연산 (12장-1) 본문

교육/책

[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA 기능/쿼리 메서드, 페이징과 정렬, 벌크 연산 (12장-1)

zyin 2024. 12. 7. 09:00

시작하면서

아래 내용은 "자바 ORM 표준 JPA 프로그래밍" 책을 읽고 정리한 내용입니다. 스프링 데이터 JPA는 Java Persistence API(JPA)를 더 쉽게 사용할 수 있도록 도와주는 모듈로, 데이터베이스와의 상호작용을 효율적으로 처리하는 데 큰 역할을 합니다. 이 모듈은 CRUD(생성, 조회, 수정, 삭제) 작업을 위한 공통 인터페이스를 제공하며, 개발자가 리포지토리를 구현할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 해당 구현체를 동적으로 생성하고 주입해줍니다. 이를 통해 데이터 접근 계층을 간결하게 유지할 수 있고, 생산성을 크게 높일 수 있습니다.

공통 인터페이스 기능

스프링 데이터 JPA는 CRUD 작업을 위한 공통 메서드를 제공합니다. 대표적인 메서드는 다음과 같습니다:

  • save(): 엔티티 저장 또는 업데이트
  • delete(): 엔티티 삭제
  • findOne() / findById(): 엔티티 단건 조회
  • findAll(): 모든 엔티티 조회

이 기본적인 기능들만으로도 대부분의 데이터 조작 작업을 쉽게 처리할 수 있습니다. 반복적인 SQL 작성의 부담에서 벗어나 비즈니스 로직에 집중할 수 있게 해줍니다.

쿼리 메소드 기능

스프링 데이터 JPA의 가장 매력적인 기능 중 하나는 쿼리 메소드입니다. 메서드 이름만으로 자동으로 쿼리를 생성할 수 있는 기능인데요, 예를 들어 findByUsername(String username)이라는 메서드를 작성하면 username 값을 조건으로 조회하는 쿼리가 자동 생성됩니다.

복잡한 쿼리가 필요할 때는 @Query 어노테이션을 사용해 직접 정의할 수 있습니다.

@Query("SELECT m FROM Member m WHERE m.age = :age")
List<Member> findMembersByAge(@Param("age") int age);

쿼리 메소드에는 다양한 키워드를 사용할 수 있습니다:

  • 조회: find...
  • 존재 확인: exists...
  • 삭제: delete...
  • 개수 확인: count...
  • 제한: limit...

이러한 키워드를 사용하면 다양한 방식으로 데이터를 손쉽게 조회하고 관리할 수 있습니다.

네임드 쿼리와 파라미터 바인딩

네임드 쿼리

@NamedQuery를 이용하면 엔티티 클래스에 쿼리를 미리 정의해 둘 수 있습니다. 이를 통해 애플리케이션 실행 시점에 문법 오류를 잡을 수 있으며, 코드 재사용성도 높일 수 있습니다.

@Entity
@NamedQuery(name = "Member.findByUsername", query = "SELECT m FROM Member m WHERE m.username = :username")
public class Member {
    // 필드 및 메서드
}

리포지토리에서는 간단하게 해당 쿼리를 호출할 수 있습니다.

List<Member> members = memberRepository.findByUsername("john");

파라미터 바인딩

스프링 데이터 JPA는 위치 기반이름 기반 파라미터 바인딩을 모두 지원합니다. 이름 기반 바인딩을 사용하면 가독성이 더 좋고 유지보수에도 유리합니다.

@Query("SELECT m FROM Member m WHERE m.age = :age")
List<Member> findMembersByAge(@Param("age") int age);

페이징과 정렬

특별한 반환 타입

페이징과 정렬 기능도 매우 간단하게 사용할 수 있습니다. Page, Slice, List는 각각의 특성과 사용 목적이 다릅니다:

  • Page: 추가적으로 전체 데이터의 개수를 조회하는 count 쿼리를 실행하여 총 페이지 수, 현재 페이지, 전체 요소 수 등의 정보를 제공합니다. 페이징 처리와 함께 전체 결과 개수를 알고 싶을 때 사용합니다.
  • Slice: count 쿼리 없이 다음 페이지 여부만 확인할 수 있습니다. 내부적으로 limit + 1을 조회하여 다음 페이지가 있는지 확인합니다. 전체 개수는 필요 없고, 다음 페이지 존재 여부만 필요한 경우에 사용합니다.
  • List (자바 컬렉션): 단순히 페이징 없이 결과만 반환합니다. 추가적인 페이징 정보가 필요 없을 때 사용합니다.

페이징과 정렬 사용 예제

Page<Member> findByUsername(String username, Pageable pageable); // count 쿼리 사용
Slice<Member> findByUsername(String username, Pageable pageable); // count 쿼리 사용 안함, 다음 페이지 존재 여부만 확인
List<Member> findByUsername(String username, Pageable pageable); // count 쿼리 사용 안함, 결과만 반환
List<Member> findByUsername(String username, Sort sort); // 정렬 조건만 사용

페이징 구성 요소

페이징의 구성 요소는 다음과 같습니다: Pageable 인터페이스를 활용해 페이징을 적용할 수 있으며, @PageableDefault 어노테이션으로 기본 설정을 지정할 수 있습니다.

  1. Pageable 인터페이스: 페이지 번호와 페이지 크기 등을 지정하기 위해 사용합니다.
  2. Pageable pageable = PageRequest.of(0, 10); // 첫 번째 페이지(0부터 시작), 페이지 크기 10
  3. Page 객체: 조회된 결과를 포함하며, 총 페이지 수, 현재 페이지, 전체 요소 수 등의 메타 정보를 제공합니다.
  4. Page<Member> resultPage = memberRepository.findByUsername("john", pageable); System.out.println("Total pages: " + resultPage.getTotalPages()); System.out.println("Total elements: " + resultPage.getTotalElements()); System.out.println("Current page content: " + resultPage.getContent());
  5. 정렬(Sort): 특정 필드를 기준으로 정렬할 수 있습니다. Sort 객체를 사용하여 정렬 방향과 정렬 필드를 지정할 수 있습니다.
  6. Sort sort = Sort.by(Sort.Direction.DESC, "age"); Pageable sortedPageable = PageRequest.of(0, 10, sort);

페이징에서 주의해야 할 것은 페이지 번호가 0부터 시작한다는 점입니다.

Page 객체는 DTO로 안전하게 변환하기 위해 map() 메서드를 사용할 수 있습니다. 이를 통해 엔티티가 직접 외부에 노출되는 것을 방지하고, 보안을 강화할 수 있습니다.

벌크성 수정 쿼리

벌크 연산과 영속성 컨텍스트 초기화의 필요성

대량의 데이터 수정이나 삭제 작업에는 @Modifying 어노테이션을 사용합니다. 벌크 연산 후에는 영속성 컨텍스트와 DB의 상태가 다를 수 있으므로, 영속성 컨텍스트를 초기화하는 것이 중요합니다.

// 벌크 업데이트 수행
@Modifying(clearAutomatically = true)
@Query("UPDATE Member m SET m.age = m.age + 1 WHERE m.age >= 15")
int bulkUpdateAge();

// 벌크 업데이트 후 캐시 초기화
entityManager.clear();

// 변경된 값 반영 확인을 위한 조회
Member updatedMember = memberRepository.findByUsername("member2");
System.out.println("Updated Member Age: " + updatedMember.getAge()); // 출력: Updated Member Age: 21

벌크 연산 후 발생하는 불일치 문제

벌크 연산으로 데이터베이스(DB)의 값을 직접 수정한 경우, 영속성 컨텍스트에는 여전히 이전 값이 남아 있을 수 있습니다. 이로 인해 DB의 값과 캐시(영속성 컨텍스트)의 값이 일치하지 않는 상황이 발생할 수 있습니다. 벌크 업데이트는 DB에 즉시 반영되지만, 캐시에는 해당 업데이트 정보가 전달되지 않기 때문에, DB의 최신 상태와 캐시된 상태가 불일치하게 됩니다.

벌크 연산 과정 설명

이 과정을 순서대로 설명하면 다음과 같습니다:

  1. 벌크 업데이트 수행 직전
    • 캐시: (member1, 10), (member2, 20)
    • DB: (member1, 10), (member2, 20)
  2. 벌크 업데이트 수행 (DB 즉시 반영)
    • 캐시: (member1, 10), (member2, 20)
    • DB: (member1, 11), (member2, 21)
  3. 데이터 조회 (캐시로부터 조회)
    • 캐시에서 값을 가져오므로, DB의 실제 값이 아닌, 여전히 (member1, 10), (member2, 20) 상태로 조회됩니다. 이로 인해 실제 DB의 값과 차이가 발생하게 됩니다. 따라서 캐시를 초기화할 필요가 있습니다.
  4. 캐시 초기화
    • entityManager.clear()를 통해 캐시를 초기화합니다.
  5. 데이터 조회 (캐시에 데이터가 없으므로 DB에서 조회 후 캐시에 업로드)
    • 캐시에 데이터가 없기 때문에 DB에서 조회하게 되며, 이후 캐시에 최신 상태로 저장됩니다.
    • 캐시 및 DB: (member1, 11), (member2, 21)

JPA Hint & Lock

스프링 데이터 JPA는 성능 최적화를 위한 JPA 힌트를 제공합니다. 예를 들어, 읽기 전용 쿼리로 지정하여 변경 추적을 방지할 수 있습니다.

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);

이 힌트는 엔티티를 변경하지 않는 읽기 전용 작업에서 불필요한 비용을 줄이는 데 유용합니다.

마무리

스프링 데이터 JPA는 JPA를 쉽게 사용할 수 있도록 도와주는 강력한 도구입니다. 공통 인터페이스와 쿼리 메서드, 페이징, 벌크 연산, JPA 힌트 등 다양한 기능을 활용하여 데이터를 더욱 쉽게 관리하고 비즈니스 로직에 집중할 수 있습니다.

Comments