@Data
@AllArgsConstructor
public class RestaurantDTO implements RestaurantDTOInterface {
public long id;
public String address;
public String category;
public String image_Url;
public String name;
public Double diff_Distance;
public String business_Hours;
public Double visitor_Review_Score;
public Long save_Count;
public Double booking_Review_Score;
public RestaurantType restaurant_Type;
public Double x;
public Double y;
}
기존의 DTO와는 구성이 많이 다릅니다.
Double diff_Distance
입니다. 이곳에는 사용자 좌표와 음식점 사이의 거리를 나타낼 구간입니다빠진 것이 없는지 간단한 테스트를 진행합니다
앞서 작성한 DTO에서 빌더를 생성한뒤 매핑시켜줍니다
@BeforeEach 를 통하여 모든 테스트 진행전에 데이터를 담을 수 있도록 합니다
getRestaurantData_v2
의 경우 모든 카테고리의 음식점들을 100개 스크래핑하여 DB에 저장하는 서비스 테스트 입니다
Test
@Test
@DisplayName("x,y 좌표값을 받아와 위치기반 가까운 거리의 매장정보를 반환")
public void getRestaurantDTOfromDB () throws Exception {
// given
AddressRequest request = new AddressRequest(126.9738873,37.5502692);
// when
List<RestaurantDTO> dtos = new ArrayList<RestaurantDTO>(); // 초기화진행
List<Restaurant> restaurantList = restaurantsRepository.findAll();
for (Restaurant restaurant : restaurantList) {
// repository 의 db값 -> response dto 로 변환 (거리 계산을 해야하기 때문에 엔티티 그대로 모델로 사용하지못함)
dtos.add(RestaurantDTO.builder()
.address(restaurant.getAddress())
.diffDistance(utility.distance(
restaurant.getX(),
restaurant.getY(),
request.getX(),
request.getY(),
"meter"))
.businessHours(restaurant.getBusinessHours())
.restaurantType(restaurant.getRestaurantType())
.bookingReviewScore(restaurant.getBookingReviewScore())
.name(restaurant.getName())
.saveCount(restaurant.getSaveCount())
.saveCount(restaurant.getSaveCount())
.x(restaurant.getX())
.y(restaurant.getY())
.build());
}
// then
for (RestaurantDTO dto : dtos) {
log.info(dto.toString());
}
}
거리 계산을 위해 utility 에 좌표간 거리 계산을 추가하였습니다
public double distance(double lat1, double lon1, double lat2, double lon2, String unit) {
double theta = lon1 - lon2;
double dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));
dist = Math.acos(dist);
dist = rad2deg(dist);
dist = dist * 60 * 1.1515;
if (unit == "kilometer") {
dist = dist * 1.609344;
} else if(unit == "meter"){
dist = dist * 1609.344;
}
return (dist);
}
// This function converts decimal degrees to radians
public double deg2rad(double deg) {
return (deg * Math.PI / 180.0);
}
// This function converts radians to decimal degrees
public double rad2deg(double rad) {
return (rad * 180 / Math.PI);
}
실험적이지만 거리 출력은 잘되고 있는것으로 확인하였습니다.
문제파악
본래 막연한게
findAll
을 사용하여 거리값을 계산하고 여기서 1km 반경내에 있는 음식점들을 dto에 담아주려고 했는데 이는 불필요한 프로세스를 증가시키는 꼴일 것 같아 처음에는 프로시저를 통한 Sql 내에서 좌표를 뽑아오는 형식을 생각해냈습니다다만 JPA 에서
프로시저
를 사용하였을때, 반환되는 값이 엔티티가 아닐 경우 매핑이 이루어 지지 않는다. 이에 관련하여 아래 stackoverflow를 참고
https://stackoverflow.com/questions/71177494/is-it-possible-to-use-jparepository-without-entity즉, JPA는 JAVA 객체를 DB 테이블의 항목/행에 매핑, JAVA유형을 데이터베이스 테이블에 매핑하는 규격인데 프로시저를 통해 반환되는 값이 엔티티가 아닐 경우 JPA의 목적을 무너뜨리기 때문에 쓰이지 않음. 이를 해결할 방도로는 JDBC 추상화를 사용할 수 있음 (Spring Data JDBC, native queries, JPQL, HQL, or a bare JDBC API)
이중, JPQL과 NATIVE 쿼리를 사용해 시도해보았습니다
SELECT R.ID,
R.BUSINESS_HOURS, R.BOOKING_REVIEW_SCORE,
R.NAME,R.IMAGE_URL,R.CATEGORY,
R.RESTAURANT_TYPE,R.SAVE_COUNT,
R.VISITOR_REVIEW_SCORE,
ST_Distance_Sphere(Point(:x,:y),POINT(R.X, R.Y)) AS diff_Distance,
R.X,
R.Y,
R.ADDRESS
FROM RESTAURANT AS R HAVING diff_Distance <= 1000 order by diff_Distance
경도(longitude): 126.XXXXXX -> X
위도(latitude): 37.XXXXXX -> Y
POINT
: Geometry 타입이며 좌표를 나타낼 때에 사용 POINT(경도, 위도)의 형태를 띔
ST_Distance_Sphere
: 두 포인트 파라미터를 입력값으로 받아 두 좌표간 거리를 meter 로 반환하여 줌
JPQL과 native 쿼리에는 각자의 장단점이 있습니다.
JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다. 문법은 SQL과 유사하며 간결하다. JPQL은 결국 SQL로 변환된다
우선 dto 를 반환 받을 수 있다는 점에서 가장 먼저 접근한 방식입니다.
다만 실제 ST_Distance_Sphere 의 경우 jpql 이 내장 함수로 인식하지 못하는 문제 때문에 포기하게 되었습니다.
다음은 native 쿼리입니다
이 경우에는 선택의 폭이 다소 넓을 수 있지만 DTO로 바로 매핑 받으려고 하면
No converter found capable of converting from type
에러를 마주하게 됩니다
→ 이유는 2개이상의 Object 배열을 클래스에서 뽑아올때 Mapping에 대한 정보가 없기때문
그렇기에 서칭중 찾게된 해결법은 인터페이스 기반 projection 입니다.
참고 문서 : https://www.baeldung.com/jpa-queries-custom-result-with-aggregation-functions
이를 사용하려면 속성 이름과 일치하는 getter 메서드로 구성된 인터페이스를 정의하여야 합니다
그렇기에 위에 있던 DTO 가 인터페이스를 상속받는 것이죠!
public interface RestaurantDTOInterface {
long getId();
String getAddress();
String getCategory();
String getImage_Url();
String getName();
Double getDiff_Distance();
String getBusiness_Hours();
Double getVisitor_Review_Score();
Long getSave_Count();
Double getBooking_Review_Score();
RestaurantType getRestaurant_Type();
Double getX();
Double getY();
}
DTO 에서는 @Data 어노테이션을 설정하였기 때문에 따로 getter 메서드를 만들 필요없습니다
@Test
public void 좌표값을기준으로1km반경내음식점찾기() throws Exception{
//given
AddressRequest request = new AddressRequest(126.9738873,37.5502692);
//when
List<RestaurantDTOInterface> r = restaurantsRepository.getRestaurantByLocation(request.getX(), request.getY());
//then
for (RestaurantDTOInterface restaurantDTOInterface : r) {
log.info(restaurantDTOInterface.getName());
log.info(restaurantDTOInterface.getDiff_Distance()+"");
}
}
기존 @BeforeEach 로 되어있던 저장하기를 거친 이후 request 가 주어졌을때 위도, 경도를 통해 해당 위치에서 1키로 반경내의 음식점들을 갖고옵니다.
로그를 남겨 결과를 추적합니다 (인터페이스의 경우 tostring 을 지정해줄 수 없어 get으로 필드를 호출합니다)
[2022-08-26 11:00:21:24406] INFO 24228 --- [ Test worker] RestaurantServiceImplTest : 마르페
[2022-08-26 11:00:21:24423] INFO 24228 --- [ Test worker] RestaurantServiceImplTest : 88.9926457763199
[2022-08-26 11:00:21:24423] INFO 24228 --- [ Test worker] RestaurantServiceImplTest : 모모키친
[2022-08-26 11:00:21:24423] INFO 24228 --- [ Test worker] RestaurantServiceImplTest : 91.87147253642308
...