Google places API 다중 요청 문제 해결

현주·2024년 10월 15일
0

Trouble Shooting

목록 보기
31/32

프로젝트에서 장소 정보를 받아오기 위해 google places api를 사용하였다.

그리고 textSearch, nearbySearch, getDetails 메서드를 만들어 테스트를 해보던 과정에서 다중 요청이 가는 것을 인지하게 되었다.

그래서 api의 응답 속도도 현저히 느렸고,
또 Google Cloud의 경우 무료 요청 수에 제한이 있어, 이 다중 요청이 지속되면 이 제한을 초과하여 결국 유료로 전환되어 비용을 지불해야 할 위험이 있었다.


✏️ 처음 구현 코드

처음 구현한 코드는 아래와 같다.

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class GooglePlaceServiceImpl implements GooglePlaceService {
	private final GooglePlaceConfig googlePlaceConfig;
	private final RestTemplate restTemplate;
⠀ ⠀
	// (1) textSearch 메서드
	@Override
	public GooglePlaceSearchResDto searchPlaces(String contents, String nextPageToken) {
		⠀ ⠀
        // google places api 호출 후 response 받는 부분
        UriComponentsBuilder uriBuilder = UriComponentsBuilder
			.fromUriString(GooglePlaceConfig.NEARBY_SEARCH_URL)
			.queryParam("location", lat + "," + lng)
			.queryParam("radius", radius * 1000)
			.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
			.queryParam("language", "ko");
		if (nextPageToken != null) {
			uriBuilder.queryParam("pagetoken", nextPageToken);
		}
		URI uri = uriBuilder.build().toUri();
		GooglePlaceSearchResDto response = handleGooglePlacesApiException(uri, GooglePlaceSearchResDto.class);
        .
        .
⠀ ⠀⠀ ⠀
		if (response != null && response.getResults() != null) {
			setPhotoUrls(response);
			sortPlacesByPopularity(response);
		}
⠀ ⠀
		return response;
	}// (2) placeId로 장소 상세 정보 가져오는 메서드
	@Override
	public GooglePlaceDetailsResDto getPlaceDetails(String placeId) {
  		// google places api 호출 후 response 받는 부분
        UriComponentsBuilder uriBuilder = UriComponentsBuilder
			.fromUriString(GooglePlaceConfig.DETAILS_URL)
			.queryParam("placeid", placeId)
			.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
			.queryParam("language", "ko");
		URI uri = uriBuilder.build().toUri();
		GooglePlaceDetailsDto apiResponse = handleGooglePlacesApiException(uri, GooglePlaceDetailsDto.class);
			.
      		.
		List<String> photoUrls = Optional.ofNullable(result.getPhotoUrls())
			.orElseGet(() -> getPlacePhotos(placeId));return GooglePlaceDetailsResDto.builder()
			.
			.
			.
			.photoUrls(photoUrls)
			.build();
	}// (3) getPlacePhotos() 메서드를 호출해 response 안의 각 장소 정보를 하나씩 넣어 photo 정보를 가져올 수 있도록 함
    private void setPhotoUrls(GooglePlaceSearchResDto response, SearchType type) {
		response.getResults().forEach(result -> {
			List<String> photoUrls = getPlacePhotos(result.getPlaceId());switch (type) {
				case TEXT_SEARCH -> result.setPhotoUrls(photoUrls);
				case NEARBY_SEARCH -> {
					...
				}
			}// (4 - 문제의 메서드) placeId로 장소 사진 url들 가져오는 메서드
	@Override
	public List<String> getPlacePhotos(String placeId) {
		// details 가져오기
		UriComponentsBuilder detailsUriBuilder = UriComponentsBuilder
			.fromUriString(GooglePlaceConfig.DETAILS_URL)
			.queryParam("placeid", placeId)
			.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
			.queryParam("language", "ko");URI detailsUri = detailsUriBuilder.build().toUri();
		GooglePlacePhotosResDto response = handleGooglePlacesApiException(detailsUri, GooglePlacePhotosResDto.class);// 결과가 없으면 장소 정보 없다고 null 처리
		if (response == null || response.getResult() == null) {
			throw new CustomLogicException(ExceptionCode.PLACE_NONE);
		}// photo Url 만들기
		List<String> photoUrls = new ArrayList<>();
		if (response.getResult().getPhotos() != null) { // 사진 정보가 null이 아닐 때만 가져오기
			for (GooglePlacePhotosResDto.Photo photo : response.getResult().getPhotos()) {
				String photoUri = UriComponentsBuilder
					.fromUriString(GooglePlaceConfig.PHOTO_URL)
					.queryParam("maxwidth", photo.getWidth())
					.queryParam("maxheight", photo.getHeight())
					.queryParam("photo_reference", photo.getPhotoReference())
					.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
					.toUriString();
⠀ 
				photoUrls.add(photoUri);
			}
		}return photoUrls;
	}
		.
        .
        .
}

메서드의 역할과 흐름

먼저 이 메서드들의 역할과 흐름을 살펴보자면,

✔️ 각 메서드의 역할

(1) searchPlaces()
➜ google places api 호출을 통해 textSearch 기능을 하는 메서드
➜ response : 장소 list

(2) getPlaceDetails()
➜ google places api 호출을 통해 한 장소의 상세 정보를 가져오는 메서드

(3) setPhotoUrls()
➜ (1)번 메서드에서 api 호출을 통해 받은 response 안에 있는 장소 list들을 돌면서 (4)번 메서드로 연결해 장소의 photoUrls를 가져와 해당 장소의 결과에 넣어주는 역할을 하는 메서드

(4) getPlacePhotos() (문제의 메서드)
➜ placeId로 장소의 사진 url들을 가져오는 메서드

✔️ 각 메서드의 흐름

1. 검색 기능(text-search)의 경우
(1)번 메서드 호출 ➜ (3)번 ➜ (4)번 이 순서를 통해 (1)번 메서드의 response에 photoUrls가 적용됨

2. 장소 상세 정보 요청의 경우
(2)번 메서드 호출 ➜ (4)번 이 순서를 통해 (2)번 메서드의 response에 photoUrls가 적용됨


⭐ 문제점

여기서 문제는 (4)번 메서드인 getPlacePhotos() 메서드였다.

이는 1,2번 기능에 공통적으로 사용되며, photoUrls를 생성하기 위해 호출된다.

일반적으로 1,2번 기능은 (1),(2)번 메서드 내에서 먼저 Google Places API에 장소 정보에 대해 요청을 한다.

그리고 흐름의 마지막에 (4)번 메서드로 넘어가게 되는데,

지금 (4)번 메서드의 코드를 보면

@Override
	public List<String> getPlacePhotos(String placeId) {
		// details 가져오기
		UriComponentsBuilder detailsUriBuilder = UriComponentsBuilder
			.fromUriString(GooglePlaceConfig.DETAILS_URL)
			.queryParam("placeid", placeId)
			.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
			.queryParam("language", "ko");URI detailsUri = detailsUriBuilder.build().toUri();
		GooglePlacePhotosResDto response = handleGooglePlacesApiException(detailsUri, GooglePlacePhotosResDto.class);...return photoUrls;
	}

photoUrls를 만들기 위한 정보를 가져오기 위해 Google Places API에 요청을 한번 더 하고 있는 것을 볼 수 있다.

위 코드의 로직을 간단히 설명하자면
Google Places API 요청을 통해 장소의 상세 정보를 가져온 후,
google에서 받은 해당 장소의 photo 정보.
즉, width, height, photo_reference를 가지고 url을 만들어 photoUrls 변수에 넣어주는 역할이다.

즉, 1,2번 기능 모두 Google Places API에 여러번 호출을 하고 있다.


Google Places API 요청 수

그렇다면 1,2번 기능에서의 Google Places API 요청 수는 어떻게 될까?

1. 검색 기능(text-search)의 경우
(1)번 메서드에서 한 번 호출 ➜ response의 장소들이 N개라고 할 때, (4)번 메서드에서 N번 호출
👉 즉, 총 1 + N번의 요청을 한다.

2. 장소 상세 정보 요청의 경우
(2)번 메서드에서 한 번 호출 ➜ (4)번 메서드에서 한 번 호출
👉 즉, 2번 요청을 한다.

결국 이 코드는 각 장소마다 불필요한 요청을 많이 하게되는 것이다.


초기 아이디어

사실 처음에 이렇게 만든 이유는 아래와 같다.

Google Places API를 호출하면 아래와 같이 photo 정보가 포함된 response를 준다.

@Getter
	@Setter
	public static class Photo {
		private int height;
		private List<String> htmlAttributions; // 사진 출처나 저작권 정보를 HTML 형식의 문자열 배열로 제공
		private String photoReference; // 사진 요청 시 사용할 수 있는 고유 식별자
		private int width;
	}

이 정보를 받고 싶지 않다면 내가 받을 responseDto에서 해당 필드를 제외하면 되고,

그리고 내가 이 정보를 활용해 사진을 보고 싶다면 아래와 같이 url을 만들어서 볼 수 있다.
👉 https://maps.googleapis.com/maps/api/place/photo?maxwidth={{width}}&maxheight={{height}}&photo_reference={{photoReference}}&key={{api key}}

🔥 [ 초기 아이디어 ]

처음에는 (2)번 메서드에서 Google의 응답과 함께 메서드 내에서 photoUrls를 생성하여 클라이언트에 전달할 계획이었다.
그러나 이 방식은 클라이언트에게 쓸모없는 사진 정보가 노출될 수 있다는 우려가 있었다.
Ex. photo의 width, height, htmlAttributions 등

그래서 (2)번 메서드의 Google 응답에서는 사진 정보를 제외하고 받기로 하였고,
별도의 메서드((4)번 메서드)를 만들어 photo 정보가 포함된 Google 응답을 다시 받아 photoUrls를 생성한 후, 이를 (2)번 메서드의 응답에 추가하기로 했다.

그리고 photoUrls를 생성하는 (4)번 메서드를 따로 만들었으니,
(1)번 메서드에서도 이 메서드를 이용하여 각 장소의 사진 정보를 가져오기로 했다.

물론 이때는 다중 요청에 관해서는 1도 생각하지 않았고, 솔직히 잘했다고 생각하고 있었다. ㅎㅅㅎ

하지만 여기서 추가의 Google 요청으로 인해 다중 요청이 생길 줄이야..

정말 바보였다..!!


✏️ 수정 후의 코드

( (1)번 메서드는 위에서 적은 코드와 같으므로 적지 않겠다. )

			.
			.
			.
	// (2) placeId로 장소 상세 정보 가져오는 메서드
	@Override
	public GooglePlaceDetailsResDto getPlaceDetails(String placeId) {
		UriComponentsBuilder uriBuilder = UriComponentsBuilder
			.fromUriString(GooglePlaceConfig.DETAILS_URL)
			.queryParam("placeid", placeId)
			.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
			.queryParam("language", "ko");
		URI uri = uriBuilder.build().toUri();
		GooglePlaceDetailsDto apiResponse = handleGooglePlacesApiException(uri, GooglePlaceDetailsDto.class);
			.
			.
			.
		// photos를 이용하여 photoUrls 생성
		List<String> photoUrls = Optional.ofNullable(result.getPhotos())
			.orElse(Collections.emptyList()) // photos가 null일 경우 빈 리스트 반환
			.stream()
			.map(photo -> UriComponentsBuilder
				.fromUriString(GooglePlaceConfig.PHOTO_URL)
				.queryParam("maxwidth", photo.getWidth())
				.queryParam("maxheight", photo.getHeight())
				.queryParam("photo_reference", photo.getPhotoReference())
				.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
				.toUriString())
			.toList();
⠀ ⠀
		return GooglePlaceDetailsResDto.builder()
			.
  			.
  			.
			.photoUrls(photoUrls)
			.build();
	}  
⠀ ⠀
	// (3) 사진 url 만들어서 넣어주는 로직
	private void setPhotoUrls(GooglePlaceSearchResDto response) {
		response.getResults().forEach(result -> {
			// 어차피 1개 사진 정보만 들어오기 때문에 첫 번째 사진 URL만 생성
			if (result.getPhotos() != null && !result.getPhotos().isEmpty()) {
				GooglePlaceSearchResDto.Photo photo = result.getPhotos().get(0); // 첫 번째 사진만 사용
				String photoUrl = UriComponentsBuilder
					.fromUriString(GooglePlaceConfig.PHOTO_URL)
					.queryParam("maxwidth", photo.getWidth())
					.queryParam("maxheight", photo.getHeight())
					.queryParam("photo_reference", photo.getPhotoReference())
					.queryParam("key", googlePlaceConfig.getGooglePlacesApiKey())
					.toUriString();
⠀ ⠀
				result.setPhotoUrl(photoUrl);
			} else {
				result.setPhotoUrl(null); // 사진이 없으면 null 처리
			}
		});
	}

문제였던 (4)번 메서드는 없애고,

요청을 한번 더 하는 부분을 제거하여 나머지 photoUrls 생성 부분을 (2)번 메서드와 (3)번 메서드에 포함시켰다.

즉, Google Places API에 대한 요청 수를 각 기능 당 한 번으로 줄였다고 할 수 있다!

수정한 메서드의 흐름

1. 검색 기능(text-search)의 경우
(1)번 메서드에서 Google Places API에 요청하여 얻은 응답을 (3)번 메서드로 전달한다.
그리고 (3)번 메서드에서 각 장소의 사진 정보를 추출하여 photo URL을 생성하고 반환한다.
⠀ ⠀
👉 Google Places API 요청 수 : 1번

2. 장소 상세 정보 요청의 경우
(2)번 메서드에서 Google Places API에 요청하면, 응답으로 GooglePlaceDetailsDto 타입의 데이터를 받고,
이 데이터에는 장소에 대한 다양한 정보와 함께 Photo 정보가 포함되어 있다.
그리고 메서드 내에서 사진 정보를 추출하여 이를 기반으로 사진 URL을 생성한다.
이후, 클라이언트에 전달할 응답 DTO인 GooglePlaceDetailsResDto를 새로 만들어서 photoUrl을 여기에 포함시키고, 클라이언트에게 필요없는 정보인 Photo 정보는 포함시키지 않도록 하였다.
⠀ ⠀
👉 Google Places API 요청 수 : 1번


❗ api 요청 속도 비교

1. 검색 기능(text-search)의 경우

[ 수정 전 ]

[ 수정 후 ]

👉 요청 시간이 3.66s(3660ms)에서 124ms로 약 96.6% 감소한 것을 볼 수 있다.

2. 장소 상세 정보 요청의 경우

[ 수정 전 ]

[ 수정 후 ]

👉 요청 시간이 989ms에서 97ms로 약 90.0% 감소한 것을 볼 수 있다.

0개의 댓글