Slice

2023. 4. 17. 11:51Spring Boot/Spring Data JPA

Spring Data JPA의 Slice는 페이징 처리에서 효율성을 높이기 위한 대안으로, Page 객체와 비교해 일부 간략화된 정보를 제공하는 페이징 결과 객체입니다. Slice는 전체 데이터 수를 계산하지 않고도 페이징을 처리할 수 있어 성능 면에서 이점을 제공합니다. 더 자세히 설명하자면, Slice는 현재 페이지의 데이터다음 페이지로 이동할 수 있는지 여부만 제공하며, 전체 데이터의 수를 포함하지 않는다는 점이 특징입니다.

Slice와 Page의 비교

PageSlice는 모두 페이징 처리를 위해 사용되지만, 두 클래스는 제공하는 정보가 다릅니다.

  • Page
    • 전체 데이터 개수 (getTotalElements())
    • 총 페이지 수 (getTotalPages())
    • 현재 페이지에 해당하는 데이터 (getContent())
    • 페이지 번호, 페이지 크기
    • 전체 페이징 정보를 계산합니다. 이는 성능에 부담이 될 수 있으며, 전체 데이터의 개수를 알기 위해 추가적인 쿼리가 필요할 수 있습니다.
  • Slice
    • 현재 페이지에 해당하는 데이터 (getContent())
    • 다음 페이지가 존재하는지 여부 (hasNext())
    • Slice는 전체 데이터 개수를 계산하지 않고 데이터의 일부를 가져옵니다. 전체 데이터 개수를 몰라도, 현재 페이지가 끝났는지 여부와 다음 페이지로 이동할 수 있는지 여부만을 알 수 있습니다.

Slice의 주요 메서드

Slice는 다음과 같은 메서드를 제공합니다:

  • getContent(): 현재 페이지의 데이터 리스트를 리턴합니다.
  • getNumber(): 현재 페이지의 인덱스를 리턴합니다 (0부터 시작).
  • getSize(): 요청한 페이지 크기를 리턴합니다.
  • hasNext(): 다음 페이지가 존재하는지 여부를 확인합니다.
  • hasPrevious(): 이전 페이지가 존재하는지 여부를 확인합니다.
  • isFirst(): 현재 페이지가 첫 페이지인지 여부를 확인합니다.
  • isLast(): 현재 페이지가 마지막 페이지인지 여부를 확인합니다.

성능 면에서의 이점

  1. 전체 데이터 개수 조회 생략: Page 객체는 전체 데이터의 개수(SELECT COUNT)를 조회하여 총 페이지 수를 계산합니다. 이는 데이터 양이 많을 때 성능에 영향을 미칠 수 있습니다. 반면, Slice는 다음 페이지가 있는지 여부만 확인하기 때문에 추가적인 데이터 카운팅 작업을 하지 않아 성능상 이점을 가집니다.
  2. 다음 페이지 유무만 확인: Slice는 기본적으로 "현재 페이지"와 "다음 페이지로 넘어갈 수 있는지 여부"만을 알면 충분한 상황에서 유용합니다. 예를 들어, 무한 스크롤과 같은 기능을 구현할 때 전체 데이터 개수나 총 페이지 수가 필요 없으므로 Slice를 사용하는 것이 적합합니다.

동작 방식

Slice는 기본적으로 요청한 페이지 크기보다 하나 더 많은 데이터를 조회한 후, 추가 데이터가 존재하는지 확인하여 hasNext() 값을 설정합니다. 예를 들어, 페이지 크기를 10으로 설정한 경우 실제로는 11개의 데이터를 조회하여 11번째 데이터가 존재하면 hasNext()true가 됩니다. 이 방식은 전체 데이터를 카운팅하지 않고도 다음 페이지가 존재하는지 확인할 수 있게 해 줍니다.

Slice를 사용하는 상황

  1. 무한 스크롤: 사용자가 페이지 하단으로 스크롤할 때마다 데이터를 불러오는 방식의 UI에서, 전체 데이터 개수를 알 필요 없이 다음 페이지로 넘어가는 기능이 중요합니다. 이 경우, Slice를 사용하면 성능상 이점을 얻을 수 있습니다.
  2. 데이터가 많을 때: 대량의 데이터를 다룰 때, 전체 데이터 개수를 카운트하는 작업이 비용이 많이 들 수 있습니다. Slice는 이러한 상황에서 불필요한 데이터 카운트를 생략하고 성능을 최적화할 수 있습니다.

예시 코드

1. Repository 인터페이스 정의

Slice를 사용하려면 Repository 메서드에서 리턴 타입을 Slice로 정의해야 합니다. 아래는 예시입니다:

public interface UserRepository extends JpaRepository<User, Long> {
    Slice<User> findByLastname(String lastname, Pageable pageable);
}

이 메서드는 주어진 lastname을 가진 사용자를 페이지 단위로 조회합니다. 리턴 타입이 Slice이므로 전체 데이터 개수를 계산하지 않고 결과를 리합니다.

2. 서비스에서 Slice 사용

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public Slice<UserDTO> processUsers(String lastname, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Slice<User> userSlice = userRepository.findByLastname(lastname, pageable);

        // User -> UserDTO로 변환
        List<UserDTO> userDTOs = userSlice.getContent().stream()
                .map(user -> new UserDTO(user.getFirstName(), user.getLastName()))
                .toList();

        // Slice로 변환해서 반환
        return new SliceImpl<>(userDTOs, pageable, userSlice.hasNext());
    }
}

위 코드에서 processUsers 메서드는 주어진 성씨를 가진 사용자를 페이징 처리하여 Slice 객체로 리턴받습니다. getContent()로 현재 페이지의 데이터를 얻고, hasNext()로 다음 페이지가 있는지 확인할 수 있습니다.

3. Controller에서 Slice 사용

package com.intheeast.demo.controller;

import com.intheeast.demo.dto.UserDTO;
import com.intheeast.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 특정 성을 기준으로 사용자를 페이징 처리하여 조회
     * 
     * @param lastname 성 필터
     * @param page 조회할 페이지 번호
     * @param size 페이지 크기
     * @return 페이징된 사용자 정보(Slice)
     */
    @GetMapping("/api/users/process")
    public Slice<UserDTO> processUsers(@RequestParam String lastname, 
                                       @RequestParam int page, 
                                       @RequestParam int size) {
        return userService.processUsers(lastname, page, size);
    }
}

processUsers 메서드에서 반환값이 정의되지 않았으므로, 현재는 단순히 콘솔 출력만을 하고 있습니다. 만약 processUsers 메서드가 컨트롤러에서 호출되는 경우, API는 HTTP 응답으로 데이터를 반환해야 하기 때문에 반환값을 정의해줘야 합니다.

다음은 processUsers 메서드를 수정하여 Slice<UserDTO>를 반환하고, 그 결과를 컨트롤러에서 JSON 형태로 응답하는 예시입니다.

수정된 UserService:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public Slice<UserDTO> processUsers(String lastname, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Slice<User> userSlice = userRepository.findByLastname(lastname, pageable);

        // User -> UserDTO로 변환
        List<UserDTO> userDTOs = userSlice.getContent().stream()
                .map(user -> new UserDTO(user.getFirstName(), user.getLastName()))
                .toList();

        // Slice로 변환해서 반환
        return new SliceImpl<>(userDTOs, pageable, userSlice.hasNext());
    }
}

설명:

  1. processUsers 메서드는 이제 Slice<UserDTO>를 반환합니다.
  2. User 객체를 UserDTO로 변환하여 리스트로 담고, 이를 다시 SliceImpl로 변환해 반환합니다.
  3. hasNext()를 사용하여 다음 페이지가 있는지 여부도 설정합니다.

UserController:

package com.intheeast.demo.controller;

import com.intheeast.demo.dto.UserDTO;
import com.intheeast.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Slice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 특정 성을 기준으로 사용자를 페이징 처리하여 조회
     * 
     * @param lastname 성 필터
     * @param page 조회할 페이지 번호
     * @param size 페이지 크기
     * @return 페이징된 사용자 정보(Slice)
     */
    @GetMapping("/api/users/process")
    public Slice<UserDTO> processUsers(@RequestParam String lastname, 
                                       @RequestParam int page, 
                                       @RequestParam int size) {
        return userService.processUsers(lastname, page, size);
    }
}

API 설명:

  • URL: /api/users/process
  • HTTP 메서드: GET
  • 파라미터:
    • lastname (String) - 필터링할 성
    • page (int) - 조회할 페이지 번호
    • size (int) - 페이지 당 데이터 크기
  • 응답: Slice<UserDTO> (페이징된 사용자 정보)

예시 요청:

GET /api/users/process?lastname=Doe&page=0&size=10

동작 방식:

  1. 주어진 lastname으로 페이징된 사용자 데이터를 반환합니다.
  2. 각 페이지의 사용자 정보(firstName, lastName)를 UserDTO로 반환합니다.
  3. 다음 페이지가 있는지 여부도 응답에 포함됩니다.

이제 processUsers는 실제 데이터를 반환하는 형태로 동작하며, API 호출 시 해당 데이터를 클라이언트가 받을 수 있습니다.

요약

  • Slice는 페이징 처리 시 전체 데이터 개수를 필요로 하지 않는 상황에서 성능을 최적화할 수 있는 객체입니다.
  • Slice는 현재 페이지 데이터다음 페이지로 이동 가능한지 여부만 제공합니다.
  • 무한 스크롤이나 전체 데이터 개수가 불필요한 상황에서 Slice를 사용하는 것이 적합합니다.
  • Slice를 통해 성능을 향상시키면서도 필요한 페이징 기능을 제공받을 수 있습니다.

Slice는 이런 효율성 덕분에, 데이터 양이 많거나 전체 개수를 불필요하게 조회하고 싶지 않을 때 특히 유용합니다.

'Spring Boot > Spring Data JPA' 카테고리의 다른 글

Core concepts  (0) 2024.10.19
Getting Started  (0) 2024.10.19
Query Hint  (0) 2023.06.06
Database Index  (0) 2023.06.04
Aggregate Root  (0) 2023.06.04