
page 와 size 옵션에 따라 count 쿼리가 실행될때도 있고 아닐때도 있어서 PageableExecutionUtils 추상 클래스를 더 자세히 살펴보게 되었습니다.
package org.springframework.data.support;
public abstract class PageableExecutionUtils {
private PageableExecutionUtils() {
}
public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {
Assert.notNull(content, "Content must not be null");
Assert.notNull(pageable, "Pageable must not be null");
Assert.notNull(totalSupplier, "TotalSupplier must not be null");
if (!pageable.isUnpaged() && pageable.getOffset() != 0L) {
return content.size() != 0 && pageable.getPageSize() > content.size()
? new PageImpl(content, pageable, pageable.getOffset() + (long)content.size())
: new PageImpl(content, pageable, totalSupplier.getAsLong());
} else {
return !pageable.isUnpaged() && pageable.getPageSize() <= content.size()
? new PageImpl(content, pageable, totalSupplier.getAsLong())
: new PageImpl(content, pageable, (long)content.size());
}
}
}
첫 번째 조건문 if (!pageable.isUnpaged() && pageable.getOffset() != 0L) 에서는
pageable.isUnpaged()가 false이고, pageable.getOffset()이 0이 아닌 경우 ->
즉 실제로 페이징이 필요하고, 페이지의 시작 위치가 0이 아닌 경우에 대한 로직이 처리됩니다.
이 조건문은 두 가지 경우를 구분해서 처리합니다.
- 현재 페이지의 데이터 개수(content.size())가 페이지 크기(pageable.getPageSize())보다 작은 경우:
- 이 경우는 예를 들어 한 페이지에 최대 10개의 데이터를 보여줄 수 있는데, 실제로 데이터가 7개만 있는 경우를 생각해보면 됩니다.
- 이때는 전체 데이터 개수를 계산할 필요가 없다. 왜냐하면, 현재 페이지가 마지막 페이지임이 분명하기 때문입니다. 마지막 페이지라면 추가적인 데이터가 없으므로 offset 값 + 현재 페이지의 데이터 개수 = 전체 데이터의 갯수 처럼 작용할 수 있다.
- 예를 들어, 페이지 크기가 10이고, 현재 2번째 페이지인데 실제로 데이터가 7개만 있다고 하면 pageable.getOffset()이 10이고 content.size()가 7이므로, 전체 데이터 개수는 10 + 7 = 17이 됩니다.
- 현재 페이지의 데이터 개수가 페이지 크기와 같거나 더 큰 경우:
- 이 경우에는 아직 더 많은 데이터가 있을 수 있으므로, 전체 데이터를 정확히 알기 위해 totalSupplier.getAsLong()을 호출해서 전체 데이터 갯수를 계산하게 됩니다.
여기서 항상 long total 을 인자로 받는 new PageImpl<> 대신 LongSupplier 함수형 인터페이스로 받는
PageableExecutionUtils 의 이점은 무엇일까요 ?


기존 new PageImpl() 와 getPage() 의 차이는 세 번째 인자만 LongSupplier totalSupplier 로 변경되어서
long total 인 primitive 인자를 LongSupplier totalSupplier 함수형 인터페이스가 대신합니다.
count 쿼리의 호출 시점 지연 (지연 계산) 이 가능해졌습니다.
기존 pageImpl()에선 count 값을 세 번째 인자로 넣기 위해 count 쿼리를 필수로 호출했지만 PageableExecutionUtils.getPage() 에선 count 쿼리를 실행하기 전인 함수형 인터페이스를 인자로 받고 있습니다.
- Primitive Type long 을 사용하는 경우: long 타입의 값을 인자로 전달받을 때는, 그 값이 미리 계산되어 있어야 합니다. 즉, PageImpl 객체를 생성하기 전에 이미 count 쿼리가 실행되어야 하고, 그 결과를 받아서 사용해야 하기 때문에 비효율적입니다.
- LongSupplier 를 사용하는 경우: LongSupplier는 함수형 인터페이스로, 전체 데이터를 계산하는 로직을 나중에 호출할 수 있습니다. 즉, 이 값이 실제로 필요할 때까지 계산을 미루는 것이 가능합니다. 만약 전체 데이터를 미리 계산하지 않아도 되는 경우라면, count 쿼리를 실행하지 않고도 PageImpl 객체를 만들 수 있습니다.
LongSupplier를 사용하면, 필요한 시점에만 전체 데이터를 계산할 수 있고, 필요하지 않은 경우 불필요한 데이터베이스 쿼리를 줄여 성능을 최적화할 수 있습니다. 함수형 인터페이스를 사용함으로써 얻는 주요 이점인 것 같습니다.
그러다 문득 조회수 동시성 증가에 대한 고민으로 여러 Lock 성능 테스트를 해보던 중 작성한 test code 에서 유용하게 사용했던 FunctionInterface 가 생각이 났습니다.
@SpringBootTest
class ItemViewLockTest {
@Autowired
ItemService itemService;
@Autowired
ItemRepository itemRepository;
@BeforeEach
void setUp() {
itemRepository.save(
Item.builder().id(1L).views(0).brand("Aesop").build());
}
private void executeMultiThread(int numberOfExecutions, RunnableWithException action, int threadPoolSize) throws InterruptedException {
AtomicInteger successCount = new AtomicInteger();
ExecutorService es = Executors.newFixedThreadPool(threadPoolSize);
CountDownLatch latch = new CountDownLatch(numberOfExecutions);
var startTime = System.currentTimeMillis();
for (int i = 0; i < numberOfExecutions; i++) {
es.execute(() -> {
try {
action.run(); // 예외(InterruptedException) 를 처리할 수 있는 커스텀 함수형 인터페이스 사용
successCount.getAndIncrement();
printState(es);
} catch (Exception e) {
System.out.println(e);
} finally {
latch.countDown();
}
});
}
latch.await();
var duration = (System.currentTimeMillis() - startTime);
double seconds = duration / 1000.0;
assertThat(successCount.get()).isEqualTo(numberOfExecutions);
System.out.printf("Time Taken %.2f sec", seconds);
}
@FunctionalInterface
interface RunnableWithException {
void run() throws Exception;
}
@Test
void increaseViewWithPessimisticLockForMultiThreadTest() throws InterruptedException {
executeMultiThread(100, () -> itemService.increaseViewsWithLock(1L), 10);
}
@Test
void increaseViewWithUpdateForMultiThreadTest() throws InterruptedException {
executeMultiThread(100, () -> itemService.increaseViewsUpdating(1L), 10);
}
@Test
void increaseViewsRequestLockFacade() throws InterruptedException {
executeMultiThread(100, () -> itemService.increaseViewsRequestLock(1L), 32);
}
}
executeMultiThread 라는 로직을 하나의 메서드로 공통화하고 싶어서 해당 메서드의 두번째 인자로 받는 RunnableWithException 이라는 FunctionalInterface 를 생성해줬습니다.

함수형 인터페이스를 통해 코드의 유연성과 재사용성이 여러 방면으로 도움이 되는 점을 느낄 수 있었습니다.
'spring' 카테고리의 다른 글
| 양방향 연관관계, 연관관계의 주인 (1) | 2022.09.25 |
|---|