stream + Repository에서 발생한 쿼리 반복호출 문제

섭정이·2025년 4월 23일

[Java Stream + Repository] 리팩터링기: 창고 공간 계산에서 발생한 쿼리 반복호출 문제 해결기

1. 상황

WMS(창고관리시스템)를 개발하면서, "각 창고별 남은 공간(remainSpace)을 계산해서 DTO에 담아 반환"하는 기능을 구현하게 되었다.

창고(warehouse_table)와 공간(area_table)은 1:N 관계다. 각 창고에 등록된 공간의 총합(sum(area_space))을 빼서 남은 공간을 계산해야 했다.


2. 문제 발생

처음 내가 작성한 코드:

처음엔 그냥 남은 공간 계산하는 메소드를 하나 만들고, 창고 리스트에서 루프 돌면서 호출하면 되겠지? 하고 생각했다.

// AdminWarehouseService.java

private Optional<Integer> getRemainingSpace(Integer warehouseId) {
    return adminAreaRepository.getAllAdminWarehouseSpaceUsage().stream()
            .filter(adminRemainSpace ->
                    adminRemainSpace.getWarehouseId().equals(warehouseId))
            .map(AdminWarehouseSpaceRemainVo::getRemainSpace)
            .findFirst();
}

@Override
public List<AdminWarehouseDto> getAllWarehouses() {
    return adminWareHouseRepository.adminGetAllWarehouses().stream()
            .map(adminWarehouseVo -> {
                Integer remainSpace = getRemainingSpace(adminWarehouseVo.getWarehouseId())
                        .orElse(0); // 못 찾으면 0
                return AdminWarehouseDto.builder()
                        .warehouseId(adminWarehouseVo.getWarehouseId())
                        .warehouseName(adminWarehouseVo.getWarehouseName())
                        .warehouseSpace(remainSpace)
                        .warehouseAddress(adminWarehouseVo.getWarehouseAddress())
                        .warehouseAmount(adminWarehouseVo.getWarehouseAmount())
                        .build();
            })
            .collect(Collectors.toList());
}

문제는 여기에 있었다

  • getRemainingSpace()가 내부적으로 adminAreaRepository.getAllAdminWarehouseSpaceUsage()를 매번 호출 하게되고 이는 창고 수만큼 쿼리를 반복 실행하게 된다. 이 경우 창고가 100개면 area_table 전체를 100번 훑는 셈이 되었다.

3. 문제 해결:

해결방법: Map으로 캐싱해서 해결

한 번만 쿼리로 전체 사용공간 데이터를 불러오고, Map으로 보관하자

@Override
public List<AdminWarehouseDto> getAllWarehouses() {
    // 1. 남은 공간 정보 한번에 조회 (창고 ID → 남은 공간)
    Map<Integer, Integer> remainSpaceMap = adminAreaRepository.getAllAdminWarehouseSpaceUsage().stream()
            .collect(Collectors.toMap(
                    AdminWarehouseSpaceRemainVo::getWarehouseId,
                    AdminWarehouseSpaceRemainVo::getRemainSpace
            ));

    // 2. 창고 리스트 조회 및 DTO 매핑
    return adminWareHouseRepository.adminGetAllWarehouses().stream()
            .map(adminWarehouseVo -> {
                Integer remainSpace = remainSpaceMap.getOrDefault(adminWarehouseVo.getWarehouseId(), 0);
                return AdminWarehouseDto.builder()
                        .warehouseId(adminWarehouseVo.getWarehouseId())
                        .warehouseName(adminWarehouseVo.getWarehouseName())
                        .warehouseSpace(remainSpace) // ✅ 여기 변경됨
                        .warehouseAddress(adminWarehouseVo.getWarehouseAddress())
                        .warehouseAmount(adminWarehouseVo.getWarehouseAmount())
                        .build();
            })
            .collect(Collectors.toList());
}

4. 정리

  • 내가 지금 루프 안에서 repository 메소드를 호출하고 있는지
  • 이 repository가 내부적으로 쿼리를 다시 날리고 있는 건 아닌지
  • 이걸 한 번에 조회해서 Map으로 처리할 수 없을지

3가지를 고려해야겠다.

특히 다음번 기능으로 구역에 들어간 제품들의 크기들을 합한 것을 가져와서 구역 최대공간에서 차를 구하여 남은 공간을 계산하는 기능도 개발해야하는데 같은 방법으로 하면 쿼리 반복 호출 문제.. 해결 할 수 있을 것이다.

💡 Stream은 깔끔하지만, 쿼리와 함께 쓰일 땐 쿼리 수를 꼭 고려하자!

profile
우직하게

0개의 댓글