초기 구현 구조

1) Controller와 DTO

@GetMapping("/search")
public Response<Page<GetHotelListResponse>> searchHotels(
        @ModelAttribute HotelSearchRequest request,
        @PageableDefault(size = 10) Pageable pageable
) {
    Page<GetHotelListResponse> result = hotelService.searchHotels(request, pageable);
    return Response.success(result);
}
public record HotelSearchRequest(
        String location,
        String checkInDate,
        String checkOutDate,
        Integer guestCount,
        String sortBy,
        Double lat,
        Double lon
) { }

사용자가 DTO로 체크인/체크아웃 날짜, 인원수, 정렬 기준, 위도 +경도를 넘기고,

서비스에서 실제 검색 로직을 처리하는 구조다.

2) Repository

@Query("""
    SELECT DISTINCT h
    FROM HotelEntity h
    JOIN FETCH h.rooms r
    LEFT JOIN FETCH r.reservations
    WHERE h.latitude BETWEEN :minLat AND :maxLat
      AND h.longitude BETWEEN :minLon AND :maxLon
""")
List<HotelEntity> searchByLocation(
        @Param("minLat") double minLat,
        @Param("maxLat") double maxLat,
        @Param("minLon") double minLon,
        @Param("maxLon") double maxLon
);

위도와 경도 범위를 Bounding Box로 만들어, 이 안에 들어오는 호텔 전부를 가져오도록 한다.

Hotel, Room, Reservation까지 한번에 Fetch Join하는 구조다

3) Service에서 거리 계산 + 필터링 + 수작업 페이징

@Transactional(readOnly = true)
public Page<GetHotelListResponse> searchHotels(HotelSearchRequest request, Pageable pageable) {

    double centerLat = request.lat();
    double centerLon = request.lon();

    double kmPerDegree = 111;
    double delta = 5.0 / kmPerDegree;  // 5km 반경

    double minLat = centerLat - delta;
    double maxLat = centerLat + delta;
    double minLon = centerLon - delta;
    double maxLon = centerLon + delta;

    List<HotelEntity> candidates = hotelRepository.searchByLocation(minLat, maxLat, minLon, maxLon);

    LocalDate checkIn = LocalDate.parse(request.checkInDate());
    LocalDate checkOut = LocalDate.parse(request.checkOutDate());

    // 1) 거리 계산 + 2) 예약 가능 여부 필터 + 3) 거리순 정렬
    List<HotelEntity> filtered = candidates.stream()
            .map(hotel -> Map.entry(hotel,
                    haversine(hotel.getLatitude().doubleValue(),
                              hotel.getLongitude().doubleValue(),
                              centerLat, centerLon)))
            .filter(entry -> entry.getValue() <= 5) // 5km 이내
            .filter(entry -> {
                HotelEntity hotel = entry.getKey();
                return hotel.getRooms().stream()
                        .anyMatch(room -> room.isAvailableDuring(checkIn, checkOut));
            })
            .sorted(Comparator.comparingDouble(Map.Entry::getValue))
            .map(Map.Entry::getKey)
            .toList();

    // 4) 수작업 페이징
    int start = (int) pageable.getOffset();
    int end = Math.min(start + pageable.getPageSize(), filtered.size());
    List<HotelEntity> paged = filtered.subList(start, end);

    return new PageImpl<>(paged, pageable, filtered.size())
            .map(hotel -> GetHotelListResponse.toDto(hotel, photoUrlBuilder));
}

<aside> 👉🏼

DB에서는 대략적인 위치로 걸러서 호텔, 방, 예약 전체를 다 가져오고, 이후 모든 계산을 자바 코드로 서버에서 한다. 바로 이 부분이 리팩토링이 필요했던 가장 큰 이유다.

</aside>