Skip to content

Commit b25b098

Browse files
authored
[BACKEND] 카카오 로그인 수정 및 ProductService 캐싱 적용 (#70)
## 📝작업 내용 > 개발 환경을 위해 카카오 로그인 시 redirectUri 지정 가능하도록 변경 > redirectUri 지정하지 않으면 env file의 redirectUri로 사용됨 > ProductService 상품 API 호출에 캐싱 적용 (Caffeine Local Cache) > 6시간 후 만료, maximumSize 5000 > CacheMonitorService로 캐싱 로그 출력 ### 스크린샷 (선택) <img width="1642" height="275" alt="image" src="https://github.com/user-attachments/assets/c37d8c8f-91e1-4e51-8c7b-cb56893f917b" />
1 parent 596e1c5 commit b25b098

File tree

11 files changed

+164
-56
lines changed

11 files changed

+164
-56
lines changed

backend/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ dependencies {
4141
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
4242

4343
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.12'
44+
45+
implementation 'org.springframework.boot:spring-boot-starter-cache'
46+
implementation 'com.github.ben-manes.caffeine:caffeine'
4447
}
4548

4649
tasks.named('test') {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.cmg.comtogether.cache;
2+
3+
import com.github.benmanes.caffeine.cache.stats.CacheStats;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.cache.caffeine.CaffeineCache;
7+
import org.springframework.cache.CacheManager;
8+
import org.springframework.stereotype.Service;
9+
10+
@Slf4j
11+
@Service
12+
@RequiredArgsConstructor
13+
public class CacheMonitorService {
14+
15+
private final CacheManager cacheManager;
16+
17+
public void printCacheStats(String cacheName) {
18+
CaffeineCache cache = (CaffeineCache) cacheManager.getCache(cacheName);
19+
if (cache != null) {
20+
CacheStats stats = cache.getNativeCache().stats();
21+
log.info("""
22+
[CACHE STATS: {}]
23+
hits: {}
24+
misses: {}
25+
hitRate: {}
26+
loadSuccess: {}
27+
loadFailure: {}
28+
evictions: {}
29+
avgLoadPenalty: {} ns
30+
""",
31+
cacheName,
32+
stats.hitCount(),
33+
stats.missCount(),
34+
String.format("%.2f%%", stats.hitRate() * 100),
35+
stats.loadSuccessCount(),
36+
stats.loadFailureCount(),
37+
stats.evictionCount(),
38+
stats.averageLoadPenalty()
39+
);
40+
} else {
41+
log.warn("Cache '{}' not found!", cacheName);
42+
}
43+
}
44+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.cmg.comtogether.common.config;
2+
3+
import com.github.benmanes.caffeine.cache.Caffeine;
4+
import org.springframework.cache.CacheManager;
5+
import org.springframework.cache.annotation.EnableCaching;
6+
import org.springframework.cache.caffeine.CaffeineCacheManager;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
import java.util.concurrent.TimeUnit;
11+
12+
@Configuration
13+
@EnableCaching
14+
public class CacheConfig {
15+
16+
@Bean
17+
public Caffeine<Object, Object> caffeineConfig() {
18+
return Caffeine.newBuilder()
19+
.expireAfterWrite(6, TimeUnit.HOURS)
20+
.maximumSize(5000)
21+
.recordStats();
22+
}
23+
24+
@Bean
25+
public CacheManager cacheManager(Caffeine<Object, Object> caffeineConfig) {
26+
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
27+
cacheManager.setCaffeine(caffeineConfig);
28+
return cacheManager;
29+
}
30+
}

backend/src/main/java/com/cmg/comtogether/oauth/controller/OauthController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public class OauthController {
2424
@PostMapping("/kakao")
2525
public ResponseEntity<ApiResponse<TokenDto>> kakaoLogin(@Valid @RequestBody OauthLoginRequestDto requestDto) {
2626
String code = requestDto.getCode();
27-
TokenDto tokenDto = oauthService.kakaoLogin(code);
27+
String redirect_uri = requestDto.getRedirect_uri();
28+
TokenDto tokenDto = oauthService.kakaoLogin(code, redirect_uri);
2829
return ResponseEntity.ok(ApiResponse.success(tokenDto));
2930
}
3031
}

backend/src/main/java/com/cmg/comtogether/oauth/dto/OauthLoginRequestDto.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ public class OauthLoginRequestDto {
1010

1111
@NotBlank(message = "인가 코드는 필수 값입니다.")
1212
private String code;
13+
14+
private String redirect_uri;
1315
}

backend/src/main/java/com/cmg/comtogether/oauth/service/KakaoService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.cmg.comtogether.common.exception.ErrorCode;
55
import com.cmg.comtogether.jwt.dto.TokenDto;
66
import com.cmg.comtogether.oauth.dto.KakaoProfileDto;
7+
import jakarta.annotation.Nullable;
78
import lombok.RequiredArgsConstructor;
89
import org.springframework.beans.factory.annotation.Value;
910
import org.springframework.http.HttpHeaders;
@@ -24,19 +25,19 @@ public class KakaoService {
2425
private String clientId;
2526

2627
@Value("${oauth.kakao.redirect-uri}")
27-
private String redirectUri;
28+
private String defaultRedirectUri;
2829

2930
@Value("${oauth.kakao.user-info-uri}")
3031
private String userInfoUri;
3132

3233
@Value("${oauth.kakao.token-uri}")
3334
private String tokenUri;
3435

35-
public TokenDto getToken(String code) {
36+
public TokenDto getToken(String code, @Nullable String redirectUri) {
3637
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
3738
params.add("grant_type", "authorization_code");
3839
params.add("client_id", clientId);
39-
params.add("redirect_uri", redirectUri);
40+
params.add("redirect_uri", redirectUri != null ? redirectUri : defaultRedirectUri);
4041
params.add("code", code);
4142

4243
return restClient.post()

backend/src/main/java/com/cmg/comtogether/oauth/service/OauthService.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.cmg.comtogether.user.entity.SocialType;
99
import com.cmg.comtogether.user.entity.User;
1010
import com.cmg.comtogether.user.repository.UserRepository;
11+
import io.micrometer.common.lang.Nullable;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.stereotype.Service;
1314

@@ -19,8 +20,8 @@ public class OauthService {
1920
private final JwtService jwtService;
2021
private final UserRepository userRepository;
2122

22-
public TokenDto kakaoLogin(String code) {
23-
TokenDto kakaoToken = kakaoService.getToken(code);
23+
public TokenDto kakaoLogin(String code, @Nullable String redirectUri) {
24+
TokenDto kakaoToken = kakaoService.getToken(code, redirectUri);
2425
KakaoProfileDto profile = kakaoService.getKakaoProfile(kakaoToken.getAccessToken());
2526

2627
User user = userRepository.findBySocialId(String.valueOf(profile.getId()))
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.cmg.comtogether.product.service;
2+
3+
import com.cmg.comtogether.common.exception.BusinessException;
4+
import com.cmg.comtogether.common.exception.ErrorCode;
5+
import com.cmg.comtogether.product.dto.NaverProductResponseDto;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.cache.annotation.Cacheable;
9+
import org.springframework.http.HttpStatusCode;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.web.client.RestClient;
12+
import org.springframework.web.util.UriComponentsBuilder;
13+
14+
import java.util.Optional;
15+
16+
@Service
17+
@RequiredArgsConstructor
18+
public class NaverProductService {
19+
20+
@Value("${naver.shopping.client-id}")
21+
private String clientId;
22+
23+
@Value("${naver.shopping.client-secret}")
24+
private String clientSecret;
25+
26+
@Value("${naver.shopping.base-url}")
27+
private String baseUrl;
28+
29+
private final RestClient restClient;
30+
31+
@Cacheable(
32+
value = "naverProducts",
33+
key = "T(java.util.Objects).hash(#searchQuery, #display, #start, #sort, #exclude)"
34+
)
35+
public NaverProductResponseDto getNaverProducts(String searchQuery, int display, int start, String sort, String exclude) {
36+
return restClient.get()
37+
.uri(UriComponentsBuilder
38+
.fromUriString(baseUrl)
39+
.queryParam("query", searchQuery)
40+
.queryParam("display", display)
41+
.queryParam("start", start)
42+
.queryParam("sort", sort)
43+
.queryParamIfPresent("exclude", Optional.ofNullable(exclude))
44+
.build()
45+
.toUri())
46+
.header("X-Naver-Client-Id", clientId)
47+
.header("X-Naver-Client-Secret", clientSecret)
48+
.retrieve()
49+
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
50+
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
51+
})
52+
.onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
53+
throw new BusinessException(ErrorCode.NAVER_API_ERROR);
54+
})
55+
.body(NaverProductResponseDto.class);
56+
}
57+
}
Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.cmg.comtogether.product.service;
22

3+
import com.cmg.comtogether.cache.CacheMonitorService;
34
import com.cmg.comtogether.common.exception.BusinessException;
45
import com.cmg.comtogether.common.exception.ErrorCode;
56
import com.cmg.comtogether.interest.entity.Interest;
@@ -8,34 +9,22 @@
89
import com.cmg.comtogether.user.entity.UserInterest;
910
import com.cmg.comtogether.user.repository.UserRepository;
1011
import lombok.RequiredArgsConstructor;
11-
import org.springframework.beans.factory.annotation.Value;
12-
import org.springframework.http.HttpStatusCode;
1312
import org.springframework.stereotype.Service;
14-
import org.springframework.web.client.RestClient;
15-
import org.springframework.web.util.UriComponentsBuilder;
16-
17-
import java.util.Optional;
1813
import java.util.stream.Collectors;
1914

2015
@Service
2116
@RequiredArgsConstructor
2217
public class ProductService {
2318

24-
private final RestClient restClient;
2519
private final UserRepository userRepository;
26-
27-
@Value("${naver.shopping.client-id}")
28-
private String clientId;
29-
30-
@Value("${naver.shopping.client-secret}")
31-
private String clientSecret;
32-
33-
@Value("${naver.shopping.base-url}")
34-
private String baseUrl;
20+
private final CacheMonitorService cacheMonitorService;
21+
private final NaverProductService naverProductService;
3522

3623
public NaverProductResponseDto searchProducts(String category, String query, int display, int start, String sort, String exclude) {
3724
String searchQuery = category + " " + query;
38-
return getNaverProducts(searchQuery, display, start, sort, exclude);
25+
NaverProductResponseDto result = naverProductService.getNaverProducts(searchQuery, display, start, sort, exclude);
26+
cacheMonitorService.printCacheStats("naverProducts");
27+
return result;
3928
}
4029

4130
public NaverProductResponseDto recommendProducts(Long userId, String category, String query, int display, int start, String sort, String exclude) {
@@ -47,29 +36,8 @@ public NaverProductResponseDto recommendProducts(Long userId, String category, S
4736
.map(Interest::getName)
4837
.collect(Collectors.joining(" "));
4938
String searchQuery = category + " " + interestString + query;
50-
return getNaverProducts(searchQuery, display, start, sort, exclude);
51-
}
52-
53-
private NaverProductResponseDto getNaverProducts(String searchQuery, int display, int start, String sort, String exclude) {
54-
return restClient.get()
55-
.uri(UriComponentsBuilder
56-
.fromUriString(baseUrl)
57-
.queryParam("query", searchQuery)
58-
.queryParam("display", display)
59-
.queryParam("start", start)
60-
.queryParam("sort", sort)
61-
.queryParamIfPresent("exclude", Optional.ofNullable(exclude))
62-
.build()
63-
.toUri())
64-
.header("X-Naver-Client-Id", clientId)
65-
.header("X-Naver-Client-Secret", clientSecret)
66-
.retrieve()
67-
.onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
68-
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR);
69-
})
70-
.onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
71-
throw new BusinessException(ErrorCode.NAVER_API_ERROR);
72-
})
73-
.body(NaverProductResponseDto.class);
39+
NaverProductResponseDto result = naverProductService.getNaverProducts(searchQuery, display, start, sort, exclude);
40+
cacheMonitorService.printCacheStats("naverProducts");
41+
return result;
7442
}
7543
}

backend/src/test/java/com/cmg/comtogether/oauth/service/KakaoServiceTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ void getToken_success() {
6868
.andRespond(withSuccess(responseJson, MediaType.APPLICATION_JSON));
6969

7070
// when
71-
TokenDto tokenDto = kakaoService.getToken(code);
71+
TokenDto tokenDto = kakaoService.getToken(code, null);
7272

7373
// then
7474
assertThat(tokenDto.getAccessToken()).isEqualTo("access-token");
@@ -81,7 +81,7 @@ public void getToken_fail_invalidCode() {
8181
server.expect(requestTo("https://kauth.kakao.com/oauth/token"))
8282
.andRespond(withStatus(HttpStatus.BAD_REQUEST));
8383

84-
assertThatThrownBy(() -> kakaoService.getToken("invalid-code"))
84+
assertThatThrownBy(() -> kakaoService.getToken("invalid-code", null))
8585
.isInstanceOf(BusinessException.class)
8686
.hasMessageContaining(ErrorCode.OAUTH_INVALID_CODE.getMessage());
8787
}
@@ -91,7 +91,7 @@ public void getToken_fail_invalidCode() {
9191
public void getToken_fail_serverError() {
9292
server.expect(requestTo("https://kauth.kakao.com/oauth/token"))
9393
.andRespond(withServerError());
94-
assertThatThrownBy(() -> kakaoService.getToken("any-code"))
94+
assertThatThrownBy(() -> kakaoService.getToken("any-code", null))
9595
.isInstanceOf(BusinessException.class)
9696
.hasMessageContaining(ErrorCode.OAUTH_PROVIDER_ERROR.getMessage());
9797
}

0 commit comments

Comments
 (0)