일단 씻고 나가자

[Spring] 거리 순 조회 서비스 로직 -> JPA @Query (native query) + paging 본문

Study/고난과 역경 (trouble shooting)

[Spring] 거리 순 조회 서비스 로직 -> JPA @Query (native query) + paging

일단 씻고 나가자 2023. 11. 27. 00:22

문제 상황

모든 데이터를 거리 순으로 조회하는 과정을

[모든 데이터 불러오기 -> 서비스 로직 내에서 정렬 (Collections.sort())] 방식을 사용하고 있었다.

 

해당 방식은 [전체 데이터 DB에서 조회 -> 서비스 로직 내에서 또 한 번 전체 조회 후 정렬] 두 번 로직을 타므로

 

1. 데이터가 많아지면 속도가 기하급수적으로 증가

2. 이후 페이지네이션 구현에서 비효율 발생

3. DB의 빠른 검색 이점을 활용하지 못함

 

등의 문제가 발생할 수 있다고 판단, JPA를 이용한 로직으로 수정했다.
(관련 질문 링크 https://okky.kr/questions/1475504#answer-684912


 

 

해결책 (선요약)

JPA의 @Query, nativeQuery를 이용하여 성공적으로 로직을 수정하였다.
 
 
 


 
 
 

사건 개요

앞선 이유로, 기존의 로직 -> JPA로의 리팩토링 과정에서 발생한 시행착오를 적어보려 한다.
 

 

기존의 코드는 이와 같이 먼저 전체 데이터를 담아오고,

해당 데이터들을 내부 로직을 통해 정렬하는 방식을 택했다.

 

이런 방법은 비효율 및 이후 페이지네이션을 고려한 개발에서 문제가 발생할 수 있으므로, 

두렵지만 JPA와 @Query를 이용하여 로직을 수정하기로 결정했다.

 

1. 수정 방식 선택

먼저 위도, 경도값만 저장돼 있는 데이터를 현재 위치를 기준하여 거리를 계산하고 정렬해야 하므로

JPA 만으로는 계산 부에 어려움이 있을 것이라 판단, @Query와 native query의 이용을 선택했다.

JPQL와 QueryDSL의 선택지도 있었지만, 현재는 JPA에도 익숙하지 않은 상황이라

우선 native한 로직을 작성하여 성공해보고, 기회가 된다면 다른 방식도 채택하여 장단점을 파악하기로 했다.

 

 

2. 로직(native query) 작성

처음엔 mySql의 문법인 ST_DISTANCE_SPHERE()를 사용하여 간단히 거리를 계산하려 했으나,

내가 현재 프로젝트에 적용한 mariaDB와 mySql이 일부 상이하다는 걸 알지 못하고 계속 sql 구문 에러를 냈다.

현재 mariaDB는 해당 함수를 지원하지 않는다는 것을 알고, 구글링하여 거리 계산 식을 수기로 작성했다.

 

 

 

3. Projection 적용

native query를 적용했기 때문에 DTO 객체 형태로 return을 받을 수 없었다.

구글링 도중 projection 이란 기능이 있는 것을 알게 되었고, (아직 해당 기능을 완벽히 이해하진 못했다)

이는 인터페이스 내에 필드 별로 get필드명(); 메서드를 선언해주면 해당 필드 값을 return 해주는 DTO 역할을 대신해주는 기능이라는 것을 알게 됐다.

하지만 이 방법은 필드 명을 스네이크 케이스에서 카멜 케이스로 자동으로 변환해주는 도움을 받지 못하기 때문에

native query 내에서 DB에서 조회된 스네이크 케이스의 필드 명을 카멜 케이스로 바꿔주어야 했다. (sql의 AS 문법 사용)

 

 

나의 경우 일부 필드만 받는 것이 아닌 전체 필드를 받아와야 했기에,

이렇게 일일이 query를 작성하고 메서드를 선언하는 것이 비효율적이었고 분명히 다른 좋은 대안이 있을 것이라 생각하여 끈질기게 고민하고 검색해보았지만 다른 대안이 나타나질 않았다..

하지만 분명 다른 좋은 대안이 있을 것이다. 추후에라도 찾아서 리팩토링 할 것이다.

 

 

4. Up/Down Casting

내 로직의 경우 거리 순일 때는 거리 차이를 함께 보내주고, 다른 순서일 때에는 거리 차이 필드를 response에 담지 않았다.

처음엔 두 방식을 각 Response 클래스로 나누고 controller의 반환 값을 ResponseEntity<?>인 와일드 카드를 사용했는데,

와일드 카드를 제거하기 위해 두 클래스를 합칠 필요성이 있어 보였다.

여기서 두 클래스를 상속 관계로 얽어서 보내겠다는 아이디어를 생각했는데, 업/다운 캐스팅의 이론 및 적용 부족으로 한참을 헤맸다. 진짜 오만하고 부족하구나를 다시 느끼는 부분이었다.

결국 response를 부모 클래스 형태로 선언하고, 거리 순일 경우에만 상속된 객체를 담아두는 형태로 간단히 구현했다.

 

 

5. Paging

페이징의 경우 단순히 JPA 메서드 파라미터에 Pagable 객체만 담아주면 되는 간단한 일이었다.

거리 순을 제외한 다른 정렬 방법에서는 해당 방식으로 해결됐으나,

native query를 사용한 JPA 메서드의 경우 Pagable을 담아주는 것만으로는 구현될 수 없다는 정보를 알게 되었다.

따라서 @Query의 옵션 중 하나인 countQuery를 추가하여 전체 데이터 개수를 선언해주고 paging을 구현하였다.

(이때 많은 에러를 만났는데, 다음 글에서 후술)

 

 

 

 

native query의 첫 적용과 pagable의 두려움으로 미루던 리팩토링이었는데,

많은 걸 느끼고 알게 된 좋은 경험이었다.

나의 무지함은 어디까지일까..?