본문 바로가기

spring

@FunctionalInterface 사용기

 

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이 아닌 경우에 대한 로직이 처리됩니다.

 

이 조건문은 두 가지 경우를 구분해서 처리합니다.

 

  1. 현재 페이지의 데이터 개수(content.size())가 페이지 크기(pageable.getPageSize())보다 작은 경우:
    • 이 경우는 예를 들어 한 페이지에 최대 10개의 데이터를 보여줄 수 있는데, 실제로 데이터가 7개만 있는 경우를 생각해보면 됩니다.
    • 이때는 전체 데이터 개수를 계산할 필요가 없다. 왜냐하면, 현재 페이지가 마지막 페이지임이 분명하기 때문입니다. 마지막 페이지라면 추가적인 데이터가 없으므로 offset 값 + 현재 페이지의 데이터 개수 = 전체 데이터의 갯수 처럼 작용할 수 있다.
    • 예를 들어, 페이지 크기가 10이고, 현재 2번째 페이지인데 실제로 데이터가 7개만 있다고 하면 pageable.getOffset()이 10이고 content.size()가 7이므로, 전체 데이터 개수는 10 + 7 = 17이 됩니다.
  2. 현재 페이지의 데이터 개수가 페이지 크기와 같거나 더 큰 경우:
    • 이 경우에는 아직 더 많은 데이터가 있을 수 있으므로, 전체 데이터를 정확히 알기 위해 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