Skip to content

Commit bb01315

Browse files
committed
feat: 캐싱, 재시도 처리 도입
1 parent a80614d commit bb01315

6 files changed

Lines changed: 74 additions & 25 deletions

File tree

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

Lines changed: 6 additions & 3 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,14 +45,14 @@ public class KakaoApiController {
4245
@Operation(
4346
summary = "주변 음식점 조회",
4447
description = "좌표(서울 시청 근처)로 카카오 로컬에서 주변 음식점을 조회합니다, 좌표는 입력하면 됩니다." +
45-
"예시 : http://localhost:8080/api/v1/location/kakao/restaurant?x=126.9780&y=37.5665&"
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(x, y);
55+
return kakaoLocalService.searchNearbyRestaurantsCached(y, x);
5356
}
5457

5558

@@ -63,7 +66,7 @@ public List<KakaoLocalResponse.Document> KakaoCafes(
6366
@RequestParam double x,
6467
@RequestParam double y
6568
) {
66-
return kakaoLocalService.searchNearbyCafes(x, y);
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: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
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;
1214
import org.springframework.retry.annotation.Backoff;
15+
import org.springframework.retry.annotation.Recover;
1316
import org.springframework.retry.annotation.Retryable;
1417
import org.springframework.stereotype.Service;
1518
import org.springframework.web.client.HttpServerErrorException;
@@ -19,30 +22,40 @@
1922
import java.util.List;
2023

2124
@Service
25+
@Slf4j
2226
@RequiredArgsConstructor
2327
public class KakaoLocalService {
2428

2529
private final RestClient kakaoRestClient;
2630
private final RestClient kakaoMobilityClient;
2731

32+
33+
public double round4(double num) { //좌표의 소수점 숫자가 다르면 매번 다른 캐싱을 해야하니, 통일시켜줌
34+
35+
return Math.round(num * 10000) / 10000.0;
36+
}
37+
2838
// 해당 좌표의 1km 근방에 존재하는 음식점를 거리순으로 나타냄
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+
)
2943
@Retryable( //최초 1번, 재시도 2번 시도
3044
retryFor = {HttpServerErrorException.class, ResourceAccessException.class}, //외부 서버의 문제, 네트워크, 타임아웃 문제인 경우에 재시도
45+
maxAttempts = 3,
3146
backoff = @Backoff(delay = 200, multiplier = 2.0) //0.2초, 0.4초, 0.8초 순으로 재시도
3247
)
33-
public List<KakaoLocalResponse.Document> searchNearbyRestaurants(double lat, double lng) {
34-
//좌표의 소수점 숫자가 다르면 매번 다른 캐싱을 해야하니, 통일시켜줌
35-
lat = Math.round(lat * 10000) / 10000.0;
36-
lng = Math.round(lng * 10000) / 10000.0;
37-
final double y = lat;
38-
final double x = lng;
48+
public List<KakaoLocalResponse.Document> searchNearbyRestaurantsCached(double lat, double lng) {
49+
double nLat = round4(lat);
50+
double nLng = round4(lng);
51+
3952
return kakaoRestClient.get()
4053
.uri(uriBuilder -> uriBuilder
4154
.path("/v2/local/search/keyword.json")
4255
.queryParam("query", "음식점")
4356
.queryParam("category_group_code", "FD6")
44-
.queryParam("x", x)
45-
.queryParam("y", y)
57+
.queryParam("x", nLng)
58+
.queryParam("y", nLat)
4659
.queryParam("radius", 1000) // 반경 1km
4760
.queryParam("sort", "distance")
4861
.build()
@@ -53,24 +66,25 @@ public List<KakaoLocalResponse.Document> searchNearbyRestaurants(double lat, dou
5366
}
5467

5568
// 해당 좌표의 1km 근방에 존재하는 카페를 거리순으로 나타냄
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+
)
5673
@Retryable( //최초 1번, 재시도 2번 시도
5774
retryFor = {HttpServerErrorException.class, ResourceAccessException.class}, //외부 서버의 문제, 네트워크, 타임아웃 문제인 경우에 재시도
75+
maxAttempts = 3,
5876
backoff = @Backoff(delay = 200, multiplier = 2.0) //0.2초, 0.4초, 0.8초 순으로 재시도
5977
)
60-
public List<KakaoLocalResponse.Document> searchNearbyCafes(double lat, double lng) {
61-
62-
//좌표의 소수점 숫자가 다르면 매번 다른 캐싱을 해야하니, 통일시켜줌
63-
lat = Math.round(lat * 10000) / 10000.0;
64-
lng = Math.round(lng * 10000) / 10000.0;
65-
final double y = lat;
66-
final double x = lng;
78+
public List<KakaoLocalResponse.Document> searchNearbyCafesCached(double lat, double lng) {
79+
double nLat = round4(lat);
80+
double nLng = round4(lng);
6781
return kakaoRestClient.get()
6882
.uri(uriBuilder -> uriBuilder
6983
.path("/v2/local/search/keyword.json")
7084
.queryParam("query", "카페")
7185
.queryParam("category_group_code", "CE7")
72-
.queryParam("x", x)
73-
.queryParam("y", y)
86+
.queryParam("x", nLng)
87+
.queryParam("y",nLat)
7488
.queryParam("radius", 1000) // 반경 1km
7589
.queryParam("sort", "distance")
7690
.build()
@@ -182,4 +196,12 @@ public KakaoRouteTransitResponse NaviSearchTransit(KakaoRouteTransitRequest tran
182196
.body(KakaoRouteTransitResponse.class); //KakaoRouteTransitResponse로 카카오 자동차 api에서 주는 응답값
183197
return response;
184198
}
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+
}
185207
}

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}")
@@ -51,4 +57,21 @@ public RedisTemplate<String, Object> redisTemplate(ObjectMapper objectMapper) {
5157
template.setConnectionFactory(redisConnectionFactory());
5258
return template;
5359
}
60+
61+
62+
@Bean
63+
public RedisCacheManager cacheManager(RedisConnectionFactory cf) {
64+
65+
RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
66+
.entryTtl(Duration.ofMinutes(30)); // TTL 30분
67+
68+
Map<String, RedisCacheConfiguration> configs = new HashMap<>(); // 캐시 이름마다 다른 TTL 설정
69+
configs.put("nearByCafes", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));
70+
configs.put("nearByRestaurants", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)));
71+
72+
return RedisCacheManager.builder(cf)
73+
.cacheDefaults(redisCacheConfig)
74+
.withInitialCacheConfigurations(configs)
75+
.build();
76+
}
5477
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ public RestTemplate restTemplate() {
7272
public RestClient kakaoRestClient(){
7373

7474
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
75-
factory.setConnectionRequestTimeout(3_000);
76-
factory.setReadTimeout(5_000);
75+
factory.setConnectionRequestTimeout(3000);
76+
factory.setReadTimeout(5000);
7777

7878
return RestClient.builder()
7979
.requestFactory(factory)

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)