(본문과는 상관없는 귀여운 시바다)
현재 개발 중인 동영상 조회 수익 통계 정산 프로그램에서 기간별 데이터를 조회하는 API가 필요한데, Spring Data JPA를 활용하면 기간별 데이터를 쉽게 찾아올 수 있다. 구현 방법과 이후 성능 개선 방법에 대해서도 정리해보았다.
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import java.io.Serializable;
import java.time.LocalDate;
import lombok;
@Entity
@Data
@IdClass(AdvertisementDailyProfitKey.class)
public class AdvertisementDailyProfit {
@Id
private Long advertisementId;
@Id
private LocalDate date;
private Double profit;
...
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
class AdvertisementDailyProfitKey implements Serializable {
private Long advertisementId;
private LocalDate date;
...
}
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface AdvertisementDailyProfitRepository extends JpaRepository<AdvertisementDailyProfit, AdvertisementDailyProfitKey> {
List<AdvertisementDailyProfit> findByDateBetween(LocalDate startDate, LocalDate endDate);
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import lombok.*;
import java.time.LocalDate;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AdvertisementProfitService {
private final AdvertisementDailyProfitRepository repository;
public List<AdvertisementDailyProfit> getProfitsByDateRange(LocalDate startDate, LocalDate endDate) {
return repository.findByDateBetween(startDate, endDate);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
import lombok.*;
@RestController
@RequiredArgsConstructor
public class AdvertisementProfitController {
private final AdvertisementService service;
@GetMapping("/profits")
public List<AdvertisementDailyProfit> getProfits(@RequestParam String startDate, @RequestParam String endDate) {
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
return service.getProfitsByDateRange(start, end);
}
}
JPA를 사용하면, 알아서 쿼리를 작성해주니 편리하지만 성능 면에서는 아쉬움이 남는다. 좀 더 성능을 개선할 방법을 찾아보자.
데이터베이스 인덱스 설정
적절한 인덱스를 설정해주면 findByDateBetween의 조회 성능을 향상시킬 수 있다. date 컬럼에 인덱스를 추가하면 date 컬럼을 기준으로 하는 조회가 훨씬 빨라진다.
CREATE INDEX idx_date ON AdvertisementDailyProfit(date);
이렇게 AdvertisementDailyProfit에 복합 키 중 하나인 날짜를 활용한 인덱스를 추가해준다. 또는, advertisement_id와 date 두 컬럼 모두를 사용하는 복합 인덱스를 추가할 수도 있다. 그렇게 하면 특정 광고 ID와 날짜 범위를 동시에 조회할 때 유리하다.
CREATE INDEX idx_advertisement_date ON AdvertisementDailyProfit(advertisement_id, date);
성능 개선을 위해서는 JPA repository에서 findByDateBetween 메서드가 SQL로 변환되는 과정이 효율적으로 작동하도록 하는 것이 중요하다. @Query 어노테이션을 활용해 네이티브 쿼리를 작성하면, JPA에 맡기는 것보다 더 효율적으로 쿼리를 날릴 수 있다.
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface AdvertisementDailyProfitRepository extends JpaRepository<AdvertisementDailyProfit, AdvertisementDailyProfitKey> {
@Query(value = "SELECT * FROM AdvertisementDailyProfit WHERE date BETWEEN :startDate AND :endDate", nativeQuery = true)
List<AdvertisementDailyProfit> findProfitsByDateRange(@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate);
}
대량의 데이터를 한 번에 조회하는 대신, '페이징'을 활용해 데이터를 나누어 조회할 수 있다. 이렇게 하면 메모리 사용량을 줄이고 성능을 향상시킬 수 있다. Spring Data JPA의 Pageable 인터페이스를 사용하면 된다.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface AdvertisementDailyProfitRepository extends JpaRepository<AdvertisementDailyProfit, AdvertisementDailyProfitKey> {
Page<AdvertisementDailyProfit> findByDateBetween(LocalDate startDate, LocalDate endDate, Pageable pageable);
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
@Service
public class AdvertisementService {
@Autowired
private AdvertisementDailyProfitRepository repository;
public Page<AdvertisementDailyProfit> getProfitsByDateRange(LocalDate startDate, LocalDate endDate, int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size);
return repository.findByDateBetween(startDate, endDate, pageRequest);
}
}
여기서 사용된 Pageable, PageRequest, Page 클래스에 대해 알아보자!
Pageable
Pageable은 페이징 정보를 제공하는 인터페이스다. 이 인터페이스는 페이지 번호, 페이지 크기, 정렬 정보를 포함하고 있다.
int getPageNumber(): 현재 페이지 번호를 반환한다.
int getPageSize(): 페이지당 항목 수를 반환한다.
Sort getSort(): 정렬 정보를 반환한다.
PageRequest
PageRequest는 Pageable 인터페이스를 구현한 구체 클래스이다. PageRequest 객체는 페이지 번호와 페이지 크기를 인수로 받아 생성할 수 있다.
of(int page, int size): 페이지 번호와 페이지 크기를 인수로 받아 PageRequest 객체를 생성한다.
of(int page, int size, Sort sort): 페이지 번호, 페이지 크기, 정렬 정보를 인수로 받아 PageRequest 객체를 생성한다.
PageRequest.of(page, size)를 사용해서 PageRequest 객체를 생성하고, 이걸 Pageable 타입으로 repository 메서드에 전달하면, 거기서 해당 Pageable 객체를 해서 페이징된 결과를 반환한다. 페이징된 결과는 Page 객체로 반환된다.
Page
Page는 Spring Data JPA에서 페이징된 데이터를 포함하는 객체로, Page 인터페이스는 java.util.List를 확장하고 있다. 페이징된 데이터 리스트와 총 페이지 수, 현재 페이지 번호, 총 항목 수 등 추가적인 페이징 정보를 제공한다.
Page 인터페이스의 주요 메서드와 구성 요소:
List<T> getContent(): 현재 페이지의 데이터를 리스트 형태로 반환한다.
boolean hasContent(): 현재 페이지에 데이터가 있는지 여부를 반환한다.
int getTotalPages(): 총 페이지 수를 반환한다.
long getTotalElements(): 전체 항목 수를 반환한다.
int getNumber(): 현재 페이지 번호를 반환한다 (0부터 시작).
int getSize(): 페이지당 항목 수를 반환한다.
int getNumberOfElements(): 현재 페이지에 포함된 항목 수를 반환한다.
boolean hasNext(): 다음 페이지가 있는지 여부를 반환한다.
boolean hasPrevious(): 이전 페이지가 있는지 여부를 반환한다.
boolean isFirst(): 현재 페이지가 첫 페이지인지 여부를 반환한다.
boolean isLast(): 현재 페이지가 마지막 페이지인지 여부를 반환한다.
Sort getSort(): 현재 페이지의 정렬 정보를 반환한다.
<U> Page<U> map(Function<? super T, ? extends U> converter): 현재 페이지의 내용을 다른 타입으로 변환하여 새로운 Page 객체를 반환한다.