Skip to content

Commit 2c17662

Browse files
Merge pull request #306 from prgrms-web-devcourse-final-project/feat/#296
[Location]캐싱, 재시도 처리 도입
2 parents 7546951 + bb01315 commit 2c17662

7 files changed

Lines changed: 99 additions & 14 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/location/controller/KakaoApiController.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
import io.swagger.v3.oas.annotations.media.Schema;
2424
import io.swagger.v3.oas.annotations.responses.ApiResponse;
2525
import lombok.RequiredArgsConstructor;
26+
import lombok.extern.log4j.Log4j2;
2627
import org.springframework.http.ResponseEntity;
28+
import org.springframework.retry.annotation.Recover;
2729
import org.springframework.web.bind.annotation.*;
2830

2931
import java.util.ArrayList;
@@ -34,6 +36,7 @@
3436
@RestController
3537
@RequestMapping("/api/v1/location/kakao")
3638
@RequiredArgsConstructor
39+
@Log4j2
3740
public class KakaoApiController {
3841

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

5558

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

6972

src/main/java/com/back/web7_9_codecrete_be/domain/location/service/KakaoLocalService.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,53 @@
99
import com.back.web7_9_codecrete_be.global.error.code.LocationErrorCode;
1010
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
1111
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.springframework.cache.annotation.Cacheable;
14+
import org.springframework.retry.annotation.Backoff;
15+
import org.springframework.retry.annotation.Recover;
16+
import org.springframework.retry.annotation.Retryable;
1217
import org.springframework.stereotype.Service;
18+
import org.springframework.web.client.HttpServerErrorException;
19+
import org.springframework.web.client.ResourceAccessException;
1320
import org.springframework.web.client.RestClient;
1421

1522
import java.util.List;
1623

1724
@Service
25+
@Slf4j
1826
@RequiredArgsConstructor
1927
public class KakaoLocalService {
2028

2129
private final RestClient kakaoRestClient;
2230
private final RestClient kakaoMobilityClient;
2331

32+
33+
public double round4(double num) { //좌표의 소수점 숫자가 다르면 매번 다른 캐싱을 해야하니, 통일시켜줌
34+
35+
return Math.round(num * 10000) / 10000.0;
36+
}
37+
2438
// 해당 좌표의 1km 근방에 존재하는 음식점를 거리순으로 나타냄
25-
public List<KakaoLocalResponse.Document> searchNearbyRestaurants(double lat, double lng) {
39+
@Cacheable(
40+
cacheNames = "nearByRestaurants",
41+
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)"
42+
)
43+
@Retryable( //최초 1번, 재시도 2번 시도
44+
retryFor = {HttpServerErrorException.class, ResourceAccessException.class}, //외부 서버의 문제, 네트워크, 타임아웃 문제인 경우에 재시도
45+
maxAttempts = 3,
46+
backoff = @Backoff(delay = 200, multiplier = 2.0) //0.2초, 0.4초, 0.8초 순으로 재시도
47+
)
48+
public List<KakaoLocalResponse.Document> searchNearbyRestaurantsCached(double lat, double lng) {
49+
double nLat = round4(lat);
50+
double nLng = round4(lng);
2651

2752
return kakaoRestClient.get()
2853
.uri(uriBuilder -> uriBuilder
2954
.path("/v2/local/search/keyword.json")
3055
.queryParam("query", "음식점")
3156
.queryParam("category_group_code", "FD6")
32-
.queryParam("x", lng)
33-
.queryParam("y", lat)
57+
.queryParam("x", nLng)
58+
.queryParam("y", nLat)
3459
.queryParam("radius", 1000) // 반경 1km
3560
.queryParam("sort", "distance")
3661
.build()
@@ -41,15 +66,25 @@ public List<KakaoLocalResponse.Document> searchNearbyRestaurants(double lat, dou
4166
}
4267

4368
// 해당 좌표의 1km 근방에 존재하는 카페를 거리순으로 나타냄
44-
public List<KakaoLocalResponse.Document> searchNearbyCafes(double lat, double lng) {
45-
69+
@Cacheable(
70+
cacheNames = "nearByCafes",
71+
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)"
72+
)
73+
@Retryable( //최초 1번, 재시도 2번 시도
74+
retryFor = {HttpServerErrorException.class, ResourceAccessException.class}, //외부 서버의 문제, 네트워크, 타임아웃 문제인 경우에 재시도
75+
maxAttempts = 3,
76+
backoff = @Backoff(delay = 200, multiplier = 2.0) //0.2초, 0.4초, 0.8초 순으로 재시도
77+
)
78+
public List<KakaoLocalResponse.Document> searchNearbyCafesCached(double lat, double lng) {
79+
double nLat = round4(lat);
80+
double nLng = round4(lng);
4681
return kakaoRestClient.get()
4782
.uri(uriBuilder -> uriBuilder
4883
.path("/v2/local/search/keyword.json")
4984
.queryParam("query", "카페")
5085
.queryParam("category_group_code", "CE7")
51-
.queryParam("x", lng)
52-
.queryParam("y", lat)
86+
.queryParam("x", nLng)
87+
.queryParam("y",nLat)
5388
.queryParam("radius", 1000) // 반경 1km
5489
.queryParam("sort", "distance")
5590
.build()
@@ -161,4 +196,12 @@ public KakaoRouteTransitResponse NaviSearchTransit(KakaoRouteTransitRequest tran
161196
.body(KakaoRouteTransitResponse.class); //KakaoRouteTransitResponse로 카카오 자동차 api에서 주는 응답값
162197
return response;
163198
}
199+
200+
@Recover
201+
public List<KakaoLocalResponse.Document> recover(Exception e, double lat, double lng) {
202+
// 로그 남기기
203+
log.warn("Kakao API 실패 (재시도 소진) lat={}, lng={}, msg={}", lat, lng, e.getMessage());
204+
// 실패하면 서비스 정책대로 처리
205+
throw new BusinessException(LocationErrorCode.EXTERNAL_API_FAILED);
206+
}
164207
}

src/main/java/com/back/web7_9_codecrete_be/global/config/RedisConfig.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import org.springframework.beans.factory.annotation.Value;
44
import org.springframework.context.annotation.Bean;
55
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.data.redis.cache.RedisCacheConfiguration;
7+
import org.springframework.data.redis.cache.RedisCacheManager;
68
import org.springframework.data.redis.connection.RedisConnectionFactory;
79
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
810
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
@@ -13,6 +15,10 @@
1315

1416
import com.fasterxml.jackson.databind.ObjectMapper;
1517

18+
import java.time.Duration;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
1622
@Configuration
1723
public class RedisConfig {
1824
@Value("${spring.data.redis.host}")
@@ -54,4 +60,21 @@ public RedisTemplate<String, Object> redisTemplate(ObjectMapper objectMapper) {
5460
template.setConnectionFactory(redisConnectionFactory());
5561
return template;
5662
}
63+
64+
65+
@Bean
66+
public RedisCacheManager cacheManager(RedisConnectionFactory cf) {
67+
68+
RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
69+
.entryTtl(Duration.ofMinutes(30)); // TTL 30분
70+
71+
Map<String, RedisCacheConfiguration> configs = new HashMap<>(); // 캐시 이름마다 다른 TTL 설정
72+
configs.put("nearByCafes", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));
73+
configs.put("nearByRestaurants", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));
74+
75+
return RedisCacheManager.builder(cf)
76+
.cacheDefaults(redisCacheConfig)
77+
.withInitialCacheConfigurations(configs)
78+
.build();
79+
}
5780
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.back.web7_9_codecrete_be.global.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.retry.annotation.EnableRetry;
5+
6+
@EnableRetry
7+
@Configuration
8+
public class RetryConfig {
9+
}

src/main/java/com/back/web7_9_codecrete_be/global/config/WebClientConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.springframework.http.HttpHeaders;
77
import org.springframework.http.MediaType;
88
import org.springframework.http.client.ClientHttpRequestInterceptor;
9+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
910
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
1011
import org.springframework.web.client.RestClient;
1112
import org.springframework.web.client.RestTemplate;
@@ -70,7 +71,12 @@ public RestTemplate restTemplate() {
7071
@Bean
7172
public RestClient kakaoRestClient(){
7273

74+
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
75+
factory.setConnectionRequestTimeout(3000);
76+
factory.setReadTimeout(5000);
77+
7378
return RestClient.builder()
79+
.requestFactory(factory)
7480
.baseUrl(kakaoBaseUrl)
7581
.defaultHeader("Authorization", kakaomapApiKey)
7682
.build();

src/main/java/com/back/web7_9_codecrete_be/global/error/code/LocationErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public enum LocationErrorCode implements ErrorCode{
1414
INVALID_KOREA_COORDINATE(HttpStatus.NOT_FOUND, "L-103" , "한국 좌표가 아닙니다"),
1515
LOCATION_NOT_EXIST_IN_KAKAO(HttpStatus.NOT_FOUND, "L-104", "해당 좌표는 카카오에 등록되어있지 않습니다."),
1616
LOCATION_NOT_HAVE(HttpStatus.NOT_FOUND, "L-105", "저장되어있는 좌표가 없어서 삭제가 불가능합니다."),
17-
ROUTE_NOT_FOUND(HttpStatus.NOT_FOUND, "L-106", "추천 경로가 존재하지 않습니다");
17+
ROUTE_NOT_FOUND(HttpStatus.NOT_FOUND, "L-106", "추천 경로가 존재하지 않습니다"),
18+
EXTERNAL_API_FAILED(HttpStatus.NOT_FOUND, "L-107", "외부 api 연결에 실패했습니다.");
1819

1920
private final HttpStatus status;
2021
private final String code;

src/test/java/com/back/web7_9_codecrete_be/domain/location/service/KakaoLocalServiceTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ void searchNearbyRestaurantsTest() throws Exception {
7878

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

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

115115
List<KakaoLocalResponse.Document> docs =
116-
kakaoLocalService.searchNearbyCafes(37.5665, 126.9780);
116+
kakaoLocalService.searchNearbyCafesCached(37.5665, 126.9780);
117117

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

0 commit comments

Comments
 (0)