프론트엔드 최적화를 위한 효율적인 리스트 조회 API 설계

최근 프론트엔드에서 다중 섹션을 가진 화면을 구현하던 중, 백엔드에서 단순히 전체 데이터를 한 번에 반환하는 방식으로 인해 성능 이슈와 유연성 부족 문제가 발생했다. 요구사항은 다음과 같다:

  • 세로 방향으로 여러 섹션이 존재
  • 각 섹션은 가로 스크롤이 가능하며, 아이템 목록과 "더 보기" 기능을 포함

처음에는 편의상 모든 데이터를 메모리에서 조합해 한꺼번에 응답했지만, 이는 네트워크 전송량 증가와 프론트엔드 처리 부담을 초래했다. 이를 해결하기 위해 두 가지 차원에서 개선이 필요하다.

수평적 분할: 페이징 기반 데이터 제공

각 섹션 내 아이템 목록은 페이지 기반으로 나누어 제공하는 것이 바람직하다. 프론트엔드에서 pageSizepageIndex를 요청 파라미터로 전달하면, 백엔드는 해당 범위의 데이터만 반환한다. 이를 통해 무분별한 데이터 전송을 방지하고, 필요 시점에 데이터를 로드하는 레이지 로딩 또는 무한 스크롤 구현도 용이해진다.

수직적 분할: 타입 기반 동적 쿼리

여러 섹션이 존재할 경우, 각 섹션을 식별할 수 있는 tabType 또는 sectionId와 같은 파라미터를 도입해야 한다. 프론트엔드는 미리 정의된 타입 값을 기반으로 개별 섹션 정보를 요청할 수 있으며, 백엔드는 해당 타입에 맞는 데이터만 처리하여 응답한다.

개선된 API 설계 예시

@RequestMapping("/section/data")
public ResultVO<SectionResponse> getSectionData(
    @RequestHeader("language-type") int langType,
    @RequestParam("type") int sectionType,
    @RequestParam("page") int pageIndex,
    @RequestParam("size") int pageSize) {

    SectionResponse response = shopService.fetchSectionData(sectionType, pageIndex, pageSize, langType);
    return ResultVO.success(response);
}

서비스 로직 개선

public SectionResponse fetchSectionData(int sectionType, int pageIndex, int pageSize, int langType) {
    // 타입에 해당하는 섹션 정보 조회
    Optional<ShopSection> targetSection = ItemManager.getSections().stream()
        .filter(section -> section.getId() == sectionType)
        .findFirst();

    if (targetSection.isEmpty()) {
        return null;
    }

    ShopSection section = targetSection.get();
    SectionResponse response = new SectionResponse();

    // 다국어 처리
    LanguageInfo langInfo = LanguageManager.get(Module.SHOP, KEY_SECTION_TITLE, 0, section.getId(), langType);
    response.setTitle(langInfo != null ? langInfo.getContext1() : section.getName());

    // 아이템 목록 변환
    List<ItemSummary> items = ItemManager.getItemsBySection(section.getId())
        .stream()
        .map(item -> new ItemSummary(item, langType))
        .collect(Collectors.toList());

    // 메모리 기반 페이징
    int totalItems = items.size();
    int startIdx = pageIndex * pageSize;
    int endIdx = Math.min(startIdx + pageSize, totalItems);

    if (startIdx < totalItems) {
        response.setItems(items.subList(startIdx, endIdx));
    } else {
        response.setItems(Collections.emptyList());
    }

    response.setHasMore(endIdx < totalItems);
    return response;
}

기존 데이터가 사전에 메모리에 로드되어 있으므로, subList()를 통한 뷰 생성은 추가 객체 할당 없이 경량화된 슬라이싱이 가능하다. 다만 subList()는 원본 리스트 참조를 유지하므로, 원본 리스트 수정 시 하위 리스트에도 영향을 줄 수 있음을 유의해야 한다. 필요 시 별도의 복사를 수행하거나, 불변 리스트를 사용하는 것이 안전하다.

태그: Spring Boot REST API Java Collections Memory Paging Lazy Loading

6월 21일 22:36에 게시됨