[TIL] JPA 페이징, @Modifying, @EntityGraph

wannabeing·약 13시간 전
0

SPARTA-TIL

목록 보기
21/22

JPA 페이징

페이지 객체

스프링 데이터 JPA는 페이징을 쉽게 할 수 있게 도와주는 Pageable 인터페이스를 제공한다.

우리는 PageRequest 구현체를 통해 페이징 설정을 할 수 있다.
인덱스는 0부터 시작한다.

// UserService
int age = 10;
PageRequest page = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

Page<User> result = userRepository.findAllByAge(age, page);

List<User> users = result.getContent(); // 유저 리스트를 반환
long totalCount = result.getTotalElement(); // 총 개수를 반환

// UserRepository
Page<User> findAllByAge(int age, Pageable pageable);

Slice 객체

다음 페이지 여부를 반환한다.
전체 페이지 정보는 갖고 있지 않는다.

limit + 1 을 가져온다.
예제의 경우 4 (3+1)개를 가져온다!

// UserService
int age = 10;
PageRequest page = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

Slice<User> result = userRepository.findAllByAge(age, page); // Slice

List<User> users = result.getContent(); // 유저 리스트를 반환
// 해당 기능 없음. long totalCount = result.getTotalElement();

// UserRepository
Slice<User> findAllByAge(int age, Pageable pageable); // Slice

파라미터 바인딩

이름기반과 위치기반이 있다. 보통 이름기반을 많이 사용한다.

컬렉션 파라미터 바인딩 + 이름기반으로 쿼리도 사용할 수 있다.

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

반환타입

값을 찾을 수 없다면, 순수 JPA는 NoResultException 예외를 발생시킨다.
스프링 데이터 JPA는 내부적으로 try-catch로 감싸서 null로 반환한다.

자바8 이후부터는 Optional을 사용한다!

User user = userRepository.findById(id);
System.out.println("findUser = " + user);

// Spring Data JPA : findUser = null (없다면)
// 순수 JPA : Exception (없다면)

벌크성 수정 쿼리

회원의 나이를 윤석열 나이로 일괄 변경해야 한다고 가정해보자.

// userService
int bulkAgeUpdateCount = bulkUpdateAgeByYoon(1); // 1살 이상인 경우에만 윤석열 나이 적용

// userRepository
@Modifying(clearAutomatically = true) // ✅
@Query("update User u set u.age = u.age - 1 where u.age >= :age")
int bulkUpdateAgeByYoon(@Param("age") int age);

update쿼리에는 @Modifying 어노테이션을 넣어주어야 한다.

💡 주의해야할 점은

이러한 벌크 쿼리는 영속성 컨텍스트를 무시하고 직접 DB에 반영된다.
즉, 아직 flush되지 않은 영속 상태의 엔티티는 이 변경 내용이 반영되지 않는다.

이를 해결하려면 @Modifying(clearAutomatically = true) 옵션을 활용해야 한다.
이 옵션을 설정하면 쿼리 실행 이후 clear()가 자동 호출되어 영속성 컨텍스트가 초기화되고, 이후 조회 시 DB에서 다시 최신 상태의 데이터를 불러올 수 있다.

bulkUpdateAgeByYoon() 메서드 실행: DB에 직접 UPDATE 쿼리 실행
↓
JPQL 기반 쿼리이므로 flush() 자동 호출
↓
clearAutomatically = true: 영속성 컨텍스트 clear()
↓
향후 조회 시 DB에서 변경된 엔티티를 새로 로딩

@EntityGraph

N+1문제를 해결하는데 도와주는 어노테이션

❓ N+1문제

class User {
	@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name ="team_id")
    Team team = new ArrayList<>();
}
List<User> users = userRepository.findAll();
for(User user : users) {
	// N + 1 문제 발생
	System.out.println("team 이름: " + user.getTeam().getName());
}

User(N):Team(1) 관계에서 fetch = LAZY로 설정되어 있다고 가정해보자.
처음에 User를 조회하면, 1번의 select 쿼리로 N개의 결과(User)가 나타난다.
이때 team은 지연 로딩(Lazy)으로 설정되어 있어, 각 User의 team 필드는 프록시 객체로 채워진다.

그런데 user.getTeam().getName()처럼 실제 Team 정보에 접근하면,
각각의 User에 대해 별도의 쿼리가 추가로 발생하게 된다.
결국 처음 1번의 조회 쿼리 + N번의 팀 조회 쿼리가 실행된다.

이러한 문제를 N+1문제라고 할 수 있다.


💡 fetch join 으로 N+1 문제를 해결해보자

// UserRepository
@Query("select u from User u join fetch u.team")
List<User> findAll();

FetchJoin은 JPA가 지원하는 기능이다.
위 코드는 User와 관련있는 Team 객체도 한꺼번에 조회하는 쿼리이다.

❓ JOIN FETCH vs LEFT JOIN FETCH ?

  • team이 무조건 있는 경우 → JOIN FETCH
  • team이 없을 수도 있는 경우 → LEFT JOIN FETCH

💡@EntityGraph로도 N+1 문제를 해결해보자

public class UserRepository extends JpaRepository<User, Long> {
	
    ...
    
    @Override
    @EntityGraph(attributePaths = {"team"}
    List<User> findAll();
}

내부적으로 left fetch join을 사용한다.

간편한 쿼리의 경우 해당 어노테이션을 사용하면 되고,
쿼리가 복잡해지면 JPQL 내에서 fetch join을 사용하면 된다.


profile
wannabe---ing

0개의 댓글