Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.annotation.Recover;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
Expand All @@ -34,6 +36,7 @@
@RestController
@RequestMapping("/api/v1/location/kakao")
@RequiredArgsConstructor
@Log4j2
public class KakaoApiController {

private final KakaoLocalService kakaoLocalService;
Expand All @@ -42,28 +45,28 @@ public class KakaoApiController {
@Operation(
summary = "주변 음식점 조회",
description = "좌표(서울 시청 근처)로 카카오 로컬에서 주변 음식점을 조회합니다, 좌표는 입력하면 됩니다." +
"예시 : http://localhost:8080/api/v1/location/kakao/restaurant?x=37.5665&y=126.9780"
"예시 : http://localhost:8080/api/v1/location/kakao/restaurant?x=126.9780&y=37.5665"
)
@PostMapping("/restaurant")
public List<KakaoLocalResponse.Document> KakaoRestaurants(
@RequestParam double x,
@RequestParam double y
) {
return kakaoLocalService.searchNearbyRestaurants(y, x);
return kakaoLocalService.searchNearbyRestaurantsCached(y, x);
}


@Operation(
summary = "주변 카페 조회",
description = "좌표(서울 시청 근처)로 카카오 로컬에서 주변 카페를 조회합니다, 좌표는 입력하면 됩니다." +
"예시 : http://localhost:8080/api/v1/location/kakao/cafes?x=37.5665&y=126.9780"
"예시 : http://localhost:8080/api/v1/location/kakao/cafes?x=126.9780&y=37.5665"
)
@PostMapping("/cafes")
public List<KakaoLocalResponse.Document> KakaoCafes(
@RequestParam double x,
@RequestParam double y
) {
return kakaoLocalService.searchNearbyCafes(y, x);
return kakaoLocalService.searchNearbyCafesCached(y, x);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,53 @@
import com.back.web7_9_codecrete_be.global.error.code.LocationErrorCode;
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class KakaoLocalService {

private final RestClient kakaoRestClient;
private final RestClient kakaoMobilityClient;


public double round4(double num) { //좌표의 소수점 숫자가 다르면 매번 다른 캐싱을 해야하니, 통일시켜줌

return Math.round(num * 10000) / 10000.0;
}

// 해당 좌표의 1km 근방에 존재하는 음식점를 거리순으로 나타냄
public List<KakaoLocalResponse.Document> searchNearbyRestaurants(double lat, double lng) {
@Cacheable(
cacheNames = "nearByRestaurants",
key = "T(String).format('lat=%s:lng=%s:r=1000', T(java.lang.Math).round(#lat*10000)/10000.0, T(java.lang.Math).round(#lng*10000)/10000.0)"
)
@Retryable( //최초 1번, 재시도 2번 시도
retryFor = {HttpServerErrorException.class, ResourceAccessException.class}, //외부 서버의 문제, 네트워크, 타임아웃 문제인 경우에 재시도
maxAttempts = 3,
backoff = @Backoff(delay = 200, multiplier = 2.0) //0.2초, 0.4초, 0.8초 순으로 재시도
)
public List<KakaoLocalResponse.Document> searchNearbyRestaurantsCached(double lat, double lng) {
double nLat = round4(lat);
double nLng = round4(lng);

return kakaoRestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v2/local/search/keyword.json")
.queryParam("query", "음식점")
.queryParam("category_group_code", "FD6")
.queryParam("x", lng)
.queryParam("y", lat)
.queryParam("x", nLng)
.queryParam("y", nLat)
.queryParam("radius", 1000) // 반경 1km
.queryParam("sort", "distance")
.build()
Expand All @@ -41,15 +66,25 @@ public List<KakaoLocalResponse.Document> searchNearbyRestaurants(double lat, dou
}

// 해당 좌표의 1km 근방에 존재하는 카페를 거리순으로 나타냄
public List<KakaoLocalResponse.Document> searchNearbyCafes(double lat, double lng) {

@Cacheable(
cacheNames = "nearByCafes",
key = "T(String).format('lat=%s:lng=%s:r=1000', T(java.lang.Math).round(#lat*10000)/10000.0, T(java.lang.Math).round(#lng*10000)/10000.0)"
)
@Retryable( //최초 1번, 재시도 2번 시도
retryFor = {HttpServerErrorException.class, ResourceAccessException.class}, //외부 서버의 문제, 네트워크, 타임아웃 문제인 경우에 재시도
maxAttempts = 3,
backoff = @Backoff(delay = 200, multiplier = 2.0) //0.2초, 0.4초, 0.8초 순으로 재시도
)
public List<KakaoLocalResponse.Document> searchNearbyCafesCached(double lat, double lng) {
double nLat = round4(lat);
double nLng = round4(lng);
return kakaoRestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v2/local/search/keyword.json")
.queryParam("query", "카페")
.queryParam("category_group_code", "CE7")
.queryParam("x", lng)
.queryParam("y", lat)
.queryParam("x", nLng)
.queryParam("y",nLat)
.queryParam("radius", 1000) // 반경 1km
.queryParam("sort", "distance")
.build()
Expand Down Expand Up @@ -161,4 +196,12 @@ public KakaoRouteTransitResponse NaviSearchTransit(KakaoRouteTransitRequest tran
.body(KakaoRouteTransitResponse.class); //KakaoRouteTransitResponse로 카카오 자동차 api에서 주는 응답값
return response;
}

@Recover
public List<KakaoLocalResponse.Document> recover(Exception e, double lat, double lng) {
// 로그 남기기
log.warn("Kakao API 실패 (재시도 소진) lat={}, lng={}, msg={}", lat, lng, e.getMessage());
// 실패하면 서비스 정책대로 처리
throw new BusinessException(LocationErrorCode.EXTERNAL_API_FAILED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
Expand All @@ -13,6 +15,10 @@

import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
Expand Down Expand Up @@ -51,4 +57,21 @@ public RedisTemplate<String, Object> redisTemplate(ObjectMapper objectMapper) {
template.setConnectionFactory(redisConnectionFactory());
return template;
}


@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory cf) {

RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)); // TTL 30분

Map<String, RedisCacheConfiguration> configs = new HashMap<>(); // 캐시 이름마다 다른 TTL 설정
configs.put("nearByCafes", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));
configs.put("nearByRestaurants", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));

return RedisCacheManager.builder(cf)
.cacheDefaults(redisCacheConfig)
.withInitialCacheConfigurations(configs)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.back.web7_9_codecrete_be.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
@Configuration
public class RetryConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
Expand Down Expand Up @@ -70,7 +71,12 @@ public RestTemplate restTemplate() {
@Bean
public RestClient kakaoRestClient(){

HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectionRequestTimeout(3000);
factory.setReadTimeout(5000);

return RestClient.builder()
.requestFactory(factory)
.baseUrl(kakaoBaseUrl)
.defaultHeader("Authorization", kakaomapApiKey)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum LocationErrorCode implements ErrorCode{
INVALID_KOREA_COORDINATE(HttpStatus.NOT_FOUND, "L-103" , "한국 좌표가 아닙니다"),
LOCATION_NOT_EXIST_IN_KAKAO(HttpStatus.NOT_FOUND, "L-104", "해당 좌표는 카카오에 등록되어있지 않습니다."),
LOCATION_NOT_HAVE(HttpStatus.NOT_FOUND, "L-105", "저장되어있는 좌표가 없어서 삭제가 불가능합니다."),
ROUTE_NOT_FOUND(HttpStatus.NOT_FOUND, "L-106", "추천 경로가 존재하지 않습니다");
ROUTE_NOT_FOUND(HttpStatus.NOT_FOUND, "L-106", "추천 경로가 존재하지 않습니다"),
EXTERNAL_API_FAILED(HttpStatus.NOT_FOUND, "L-107", "외부 api 연결에 실패했습니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ void searchNearbyRestaurantsTest() throws Exception {

//kakasLocalService에 있는 searchNearByRestaurants 함수에 위도, 경도를 넣고 WebClient가 호출해서 응답을 반환
List<KakaoLocalResponse.Document> docs =
kakaoLocalService.searchNearbyRestaurants(37.5, 127.0);
kakaoLocalService.searchNearbyRestaurantsCached(37.5, 127.0);

assertThat(docs).hasSize(1); //응답 배열의 크기가 1인지
assertThat(docs.get(0).getPlace_name()).isEqualTo("테스트식당"); // 제대로 필드가 들어갔는지 확인
Expand Down Expand Up @@ -113,7 +113,7 @@ void searchNearbyCafesTest() throws Exception {
"""));

List<KakaoLocalResponse.Document> docs =
kakaoLocalService.searchNearbyCafes(37.5665, 126.9780);
kakaoLocalService.searchNearbyCafesCached(37.5665, 126.9780);

assertThat(docs).hasSize(1);
assertThat(docs.get(0).getPlace_name()).isEqualTo("테스트카페");
Expand Down