Spring Data JPA는 페이징과 정렬 기능을 간단하게 구현할 수 있는 강력한 기능을 제공한다. 이 글에서는 페이징과 정렬을 사용하여 데이터를 효율적으로 조회하는 방법과 실무에서 유용한 팁을 살펴보겠다.
1. 페이징과 정렬을 위한 메서드 작성
Spring Data JPA에서는 리포지토리의 인터페이스에서 페이징과 정렬을 간단히 정의할 수 있다.
페이징과 정렬 사용 예제
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); //정렬만 사용
여기서 말하는 "카운트"란 무엇일까?
- JPA에서 카운트는 전체 데이터의 개수를 계산하기 위해 수행되는 Count 쿼리를 의미한다.
페이징 기능에서 Page를 사용할 때, 전체 데이터의 개수를 알아야 총 페이지 수를 계산할 수 있기 때문에 필요하다.
카운트 쿼리의 역할을 간단하게 알아보자!
페이징 처리에서 전체 데이터의 수를 알아야 몇 페이지가 필요한지 계산할 수 있다.
예를 들어 : 데이터 총 50개, 페이지당 10개 표시라고 하면 총 5페이지가 필요하다는 계산을 위해 COUNT(*) 쿼리가 자동으로 실행된다.
2. 페이징과 정렬 사용 예제
조건
- 검색 조건 : 나이가 10살
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지(0번 페이지), 페이지당 보여줄 데이터는 3건
리포지토리 정의 코드
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
3. 페이징 및 정렬 사용 예제 코드
다음은 데이터를 저장하고, 페이징 및 정렬 기능을 테스트하는 코드
Page 사용 예제 실행 코드
@Test
public void page() throws Exception {
//given
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));
//when
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//then
List<Member> content = page.getContent(); //조회된 데이터
assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}
설명
- pageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
- 첫 번째 파라미터 : 페이지 번호(0부터 시작)
- 두 번째 파라미터 : 한 페이지당 보여줄 데이터 수
- 세 번째 파라미터 : 정렬 조건.
- Page<Member>
- 조회 결과와 관련된 다양한 메타데이터를 제공
4. Page 및 Slice 인터페이스 주요 메서드
Page 인터페이스
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); // 전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
Slice 인터페이스
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
5. 실무 팁 : Count 쿼리 분리
Spring Data JPA에서 Page를 사용할 경우, 기본적으로 데이터 조회와 함께 Count 쿼리가 실행된다. 이때 엔티티 간 연관관계가 있을 경우, 불필요한 조인으로 인해 성능 저하나 N+1 문제가 발생할 수 있다
문제 상황
예를 들어, Member 엔티티가 Team과 연관관계를 맺고 있다면
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
}
Page<Member> findByAge(int age, Pageable pageable); 메서드를 호출할 때
- 데이터 조회 쿼리와 Count 쿼리에서 불필요한 조인이 발생
- Team 데이터를 조회할 필요가 없음에도 조인이 추가되어 성능 저하
- 대규모 데이터에서는 쿼리 성능이 악화되거나 N+! 문제가 발생
해결방법
Spring Data JPA에서 @Query를 사용해 Count 쿼리를 분리한다.
@Query(
value = "select m from Member m",
countQuery = "select count(m) from member m"
)
Page<Member> findMemberAllCountBy(Pageable pageable);
- 데이터 조회 쿼리와 Count 쿼리를 분리하여 조인 비용을 줄이고 성능을 최적화
6. 엔티티를 DTO로 변환하기
조회 결과를 바로 클라이언트에 반환하는 대신, 엔티티를 DTO로 변환하는 것을 권장.
이는 클라이언트에 필요한 데이터만 전달하고, 엔티티를 직접 노출함으로써 발생할 수 있는 보안 및 유지보수 문제를 방지할 수 있다.
기본 예제
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getUsername(), m.getAge()));
DTO 클래스 정의
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
DTO 변환 과정
- 변환 흐름
- 데이터 조회 : Page<Member>를 리포지토리에서 조회
- map() 사용 : Page.map() 메서드를 통해 각 Member 엔티티를 MemberDto로 변환.
- 새로운 Page 객체 반환 : 변환된 Page<MemberDto>를 서비스나 컨트롤러 계층에서 사용.
- 코드 예제
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> memberPage = memberRepository.findByAge(10, pageRequest);
// 엔티티를 DTO로 변환
Page<MemberDto> dtoPage = memberPage.map(member -> new MemberDto(member.getUsername(), member.getAge()));
// DTO 데이터 확인
List<MemberDto> dtoContent = dtoPage.getContent();
dtoContent.forEach(dto -> {
System.out.println("Username: " + dto.getUsername());
System.out.println("Age: " + dto.getAge());
});
7. 실전 예제
페이징과 DTO 변환을 컨트롤러, 서비스, 리포지토리 계층으로 나는 예제
1. DTO 클래스
클라이언트에 전달할 데이터만 포함하는 ScheduleDto 클래스
@Getter
public class ScheduleDto {
private String title;
private String content;
private int commentCount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private String username;
public ScheduleDto(String title, String content, int commentCount, LocalDateTime createdAt, LocalDateTime updatedAt, String username) {
this.title = title;
this.content = content;
this.commentCount = commentCount;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.username = username;
}
}
2. Entity 클래스
데이터베이스와 매핑되는 Schedule 엔티티
@Entity
@Getter
@Table(name = "schedule")
public class Schedule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@OneToMany(mappedBy = "schedule")
private List<Comment> comments = new ArrayList<>();
}
User엔티티
@Getter
@Entity
@Table(name="user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
}
Comment 엔티티
@Entity
@Getter
@Table(name="comment")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Schedule schedule;
}
3. 리포지토리 계층
리포지토리에서는 JPQL을 사용하여 엔티티를 DTO로 변환해 반환
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
@Query("SELECT new com.example.dto.ScheduleDto(" +
"s.title, s.content, size(s.comments), s.createdAt, s.updatedAt, u.username) " +
"FROM Schedule s " +
"JOIN s.user u " +
"WHERE s.updatedAt <= :now " +
"ORDER BY s.updatedAt DESC")
Page<ScheduleDto> findSchedules(@Param("now") LocalDateTime now, Pageable pageable);
}
4. 서비스 계층
서비스 계층은 비지니스 로직을 처리하고, 리포지토리로부터 데이터를 조회한다.
@Service
@RequiredArgment
public class ScheduleService {
private final ScheduleRepository scheduleRepository;
public Page<ScheduleDto> getSchedules(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "updatedAt"));
return scheduleRepository.findSchedules(LocalDateTime.now(), pageable);
}
}
5. 컨트롤러 계층
컨트롤러는 서비스 계층을 호출하여 클라이언트 요청을 처리하고 응답을 반환
@RestController
@RequestMapping("/schedules")
public class ScheduleController {
private final ScheduleService scheduleService;
@GetMapping
public ResponseEntity<Page<ScheduleDto>> getSchedules(
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "10") int size) {
Page<ScheduleDto> schedules = scheduleService.getSchedules(page, size);
return ResponseEntity.ok(schedules);
}
}
6. 테스트 요청
API 호출 예시
GET /schedules?page=0&size=5
응답 (JSON)
{
"content": [
{
"title": "할일1",
"content": "내용1",
"commentCount": 3,
"createdAt": "2024-12-01T10:00:00",
"updatedAt": "2024-12-02T15:30:00",
"username": "user1"
},
{
"title": "할일2",
"content": "내용2",
"commentCount": 5,
"createdAt": "2024-12-01T09:00:00",
"updatedAt": "2024-12-02T14:30:00",
"username": "user2"
}
],
"totalPages": 1,
"totalElements": 2,
"number": 0,
"size": 5,
"first": true,
"last": true
}'Back-End > JPA' 카테고리의 다른 글
| Spring Data JPA : Auditing (1) | 2024.12.18 |
|---|---|
| Spring Data JPA : 벌크성 수정 쿼리와 엔티티 그래프 (2) | 2024.12.18 |
| Spring DataJpa : 쿼리 메서드 기능 (0) | 2024.12.18 |
| 프록시와 연관관계 관리(JPA) (0) | 2024.12.17 |
| 상속관계 매핑(JPA) (0) | 2024.12.17 |