@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로 체크인/체크아웃 날짜, 인원수, 정렬 기준, 위도 +경도를 넘기고,
서비스에서 실제 검색 로직을 처리하는 구조다.
@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하는 구조다
@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>