Conversation
Walkthrough로그인한 사용자의 마이페이지 조회 API가 추가되었습니다. User → MyPageResponse 매핑, Redis 기반 캐시 설정(30분 TTL), Changes
Sequence DiagramsequenceDiagram
actor Client
participant Controller as UserController
participant JwtFilter as JwtAuthenticationFilter
participant Service as UserService
participant Cache as Redis Cache
participant DB as Database
Client->>Controller: GET /api/users/my (Bearer token)
Controller->>JwtFilter: 인증 흐름 (필터가 토큰 파싱)
JwtFilter-->>Controller: 인증된 Principal (userId, provider)
Controller->>Service: getMyPage(userId, provider)
Service->>Cache: GET user:profile:{userId}:{provider}
alt Cache Hit
Cache-->>Service: MyPageResponse
else Cache Miss
Service->>DB: findById(userId)
DB-->>Service: User 엔티티
Service->>Service: UserConverter.toMyPageResponse(user, provider)
Service->>Cache: PUT user:profile:{userId}:{provider} (TTL 30m)
end
Service-->>Controller: MyPageResponse
Controller-->>Client: 200 DataResponse<MyPageResponse>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
검토 포인트(짧게)
잘 정리된 변경입니다 — 특히 Redis 캐시 설정과 서비스 레벨의 Cacheable 적용은 깔끔합니다. 한편 Jwt/Authentication 흐름에서 provider를 재구성하는 부분은 null·예외 경로(토큰 변조 등)를 추가로 검증해 주세요. 🚥 Pre-merge checks | ✅ 4 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
No actionable comments were generated in the recent review. 🎉 Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`:
- Around line 97-103: The Redis cache for getMyPage ("user:profile") isn't
evicted after profile changes done in CustomOAuth2UserService.loadUser (where
User.updateProfile(...) is called), causing stale data; fix by evicting the
cache for that user after updating the profile—either annotate loadUser with
`@CacheEvict`(cacheNames="user:profile", key="#result.id") (or otherwise call
cacheEvict for the user's id) immediately after User.updateProfile, or move
profile mutation into a new UserService.updateUserProfile(Long userId, String
name) method annotated with `@CacheEvict`(cacheNames="user:profile",
key="#userId") and call that from CustomOAuth2UserService.loadUser.
- Around line 97-103: The API security currently allows all "/api/users/**" via
SecurityConfig.requestMatchers(...).permitAll(), which exposes the protected
"/api/users/my" endpoint and can cause a null `@AuthenticationPrincipal` (userId)
leading to cache/key errors in UserService.getMyPage (annotated with `@Cacheable`
and using userId); update SecurityConfig to only permit public endpoints
(signup, email-send, email-verify, sms-send, sms-verify, password-reset/**) in
requestMatchers and ensure .anyRequest().authenticated() is applied so that
"/api/users/my" requires authentication and userId is always present for
UserService.getMyPage.
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java`:
- Around line 87-93: The endpoint /api/users/my is currently exposed due to
SecurityConfig permitting /api/users/** and that lets UserController.getMyPage
receive a null userId (and contaminate cache); fix it by updating SecurityConfig
to require authentication for "/api/users/my" (e.g., add a
.requestMatchers("/api/users/my").authenticated() rule and keep other
/api/users/** or /api/auth/** as permitAll as intended), and also harden the
caching/service path by preventing null keys: in the method annotated with
`@Cacheable` (the service method backing userService.getMyPage) add a cache
condition to skip caching when userId is null or add an explicit null check that
returns 401/throws before calling the cache-backed logic so null userId is never
stored under a shared cache key.
🧹 Nitpick comments (1)
src/main/java/com/whereyouad/WhereYouAd/global/config/RedisConfig.java (1)
15-34: 캐시 기본 설정이 깔끔하고 예측 가능해서 좋습니다.TTL/직렬화/NULL 비캐시가 명확해 운영 시 동작을 추적하기 쉬워요. 다만
GenericJackson2JsonRedisSerializer는@class메타데이터를 저장하므로, 패키지/클래스명이 바뀌는 배포가 있으면 캐시 무효화(키 버전업 또는 캐시 삭제) 전략을 함께 두는 게 안전합니다.
| @Cacheable(value = "user:profile", key = "#userId", unless = "#result == null") | ||
| @Transactional(readOnly = true) | ||
| public MyPageResponse getMyPage(Long userId) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); | ||
|
|
||
| return UserConverter.toMyPageResponse(user); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# UserService.java 파일 확인
find . -type f -name "UserService.java" | head -1 | xargs wc -lRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 163
🏁 Script executed:
# 사용자 프로필 업데이트/수정 흐름이 있는지 확인
rg -n "updateProfile|updatePhone|updateImage|updateUser|@CacheEvict" src/main/java --type java -iRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 362
🏁 Script executed:
# UserService.java 전체 내용 확인
find . -type f -name "UserService.java" | head -1 | xargs cat -nRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 5117
🏁 Script executed:
# CustomOAuth2UserService.java 파일 확인 및 updateProfile 주변 코드 보기
find . -type f -name "CustomOAuth2UserService.java" | xargs wc -lRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 178
🏁 Script executed:
# CustomOAuth2UserService.java 전체 내용 확인
find . -type f -name "CustomOAuth2UserService.java" | xargs cat -nRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 5357
🏁 Script executed:
# 프로필 업데이트 관련 컨트롤러 또는 엔드포인트 검색
rg -n "updateProfile|updateUser|updatePhone|updateImage" src/main/java --type java -B 3 -A 5Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 2341
🏁 Script executed:
# User 엔티티에서 프로필 관련 메서드 확인
find . -type f -name "User.java" -path "*/domains/user/*" | xargs cat -nRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 1988
소셜 로그인 시 프로필 변경 후 캐시 무효화 처리 필수입니다.
현재 CustomOAuth2UserService의 loadUser() 메서드에서 기존 소셜 계정으로 로그인 시 user.updateProfile()로 프로필을 변경하고 있는데(95라인), getMyPage()의 Redis 캐시가 무효화되지 않고 있습니다. 이로 인해 사용자가 소셜 로그인으로 프로필을 업데이트한 후 마이페이지를 조회하면 변경되기 전의 캐시된 정보를 받게 되는 데이터 불일치 문제가 발생합니다.
해결 방법
CustomOAuth2UserService에서 프로필 변경 후 다음과 같이 캐시를 무효화해주세요:
`@Override`
`@Transactional`
`@CacheEvict`(cacheNames = "user:profile", key = "#result.id") // 로그인 후 해당 사용자 캐시 제거
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// ... 기존 코드 ...
user.updateProfile(oAuth2Response.getName()); // 95라인
// ... 나머지 코드 ...
}또는 UserService에 프로필 변경 전담 메서드를 추가하고 거기서 캐시를 처리하는 방식도 좋습니다:
`@Transactional`
`@CacheEvict`(cacheNames = "user:profile", key = "#userId")
public void updateUserProfile(Long userId, String name) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));
user.updateProfile(name);
}🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`
around lines 97 - 103, The Redis cache for getMyPage ("user:profile") isn't
evicted after profile changes done in CustomOAuth2UserService.loadUser (where
User.updateProfile(...) is called), causing stale data; fix by evicting the
cache for that user after updating the profile—either annotate loadUser with
`@CacheEvict`(cacheNames="user:profile", key="#result.id") (or otherwise call
cacheEvict for the user's id) immediately after User.updateProfile, or move
profile mutation into a new UserService.updateUserProfile(Long userId, String
name) method annotated with `@CacheEvict`(cacheNames="user:profile",
key="#userId") and call that from CustomOAuth2UserService.loadUser.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "UserService.java" | grep -E "src/main/java"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 159
🏁 Script executed:
fd "UserService.java" src/main/javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 257
🏁 Script executed:
fd "UserService" src/main/java --type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 257
🏁 Script executed:
git ls-files src/main/java | grep -i userserviceRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 257
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java | head -120Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5117
🏁 Script executed:
wc -l src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 161
🏁 Script executed:
fd ".*Controller.*" src/main/java --type f | grep -i userRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 434
🏁 Script executed:
git ls-files src/main/java | grep -i controller | grep -i userRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 434
🏁 Script executed:
rg "getMyPage" src/main/java --type javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 797
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 5127
🏁 Script executed:
wc -l src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 161
🏁 Script executed:
rg "@AuthenticationPrincipal|@PreAuthorize|@Secured|@RolesAllowed" src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java -A 2Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 267
🏁 Script executed:
fd "SecurityConfig|SecurityConfiguration" src/main/java --type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 148
🏁 Script executed:
git ls-files src/main/java | grep -i "security" | grep -i "config"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 148
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java | head -150Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 5187
🏁 Script executed:
wc -l src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 151
🏁 Script executed:
fd "JwtAuthenticationFilter" src/main/java --type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 161
🏁 Script executed:
rg "userId" src/main/java/com/whereyouad/WhereYouAd/global/security --type java -B 2 -A 2Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 55
🏁 Script executed:
git ls-files src/main/java | grep -i "principal\|userdetails\|authentication" | head -20Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 538
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetails.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 2393
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java | head -80Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 4008
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java | tail -30Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 1401
🏁 Script executed:
rg "GlobalExceptionHandler|@ExceptionHandler|ControllerAdvice" src/main/java --type java -lRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 157
🏁 Script executed:
git ls-files src/main/java | grep -i "exceptionhandler\|exception"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 1010
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/global/exception/GlobalExceptionHandler.javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 5191
🏁 Script executed:
web_search
Spring `@AuthenticationPrincipal` null when unauthenticated Spring Security 6Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 168
🏁 Script executed:
web_search
Spring `@Cacheable` null key IllegalArgumentException Spring Boot 3Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 168
🌐 Web query:
Spring @AuthenticationPrincipal null when unauthenticated Spring Security 6
💡 Result:
In Spring Security 6, @AuthenticationPrincipal can be null for unauthenticated/anonymous requests because Spring MVC resolves it from HttpServletRequest#getPrincipal(), and for anonymous requests that servlet principal is null—even though Spring Security may still have an AnonymousAuthenticationToken in the SecurityContext. [1]
Also, the @AuthenticationPrincipal resolver returns null when Authentication or Authentication.getPrincipal() is null. [2]
What to do instead (if you need “anonymous vs authenticated”)
Use @CurrentSecurityContext to read from Spring Security’s SecurityContext instead of the servlet principal:
`@GetMapping`("/me")
public Object me(`@CurrentSecurityContext` SecurityContext context) {
Authentication auth = context.getAuthentication(); // can be AnonymousAuthenticationToken
return (auth == null) ? null : auth.getPrincipal();
}@CurrentSecurityContext is specifically meant for resolving the SecurityContext (and supports SpEL via expression). [3]
If you only want the user when logged in
Keep @AuthenticationPrincipal and accept that it will be null when not logged in:
`@GetMapping`("/me")
public Object me(`@AuthenticationPrincipal` MyUserDetails user) {
return user; // null when unauthenticated
}Sources: Spring Security reference on anonymous + MVC principal resolution [1], Spring Security API behavior of principal resolution [2], @CurrentSecurityContext annotation API [3].
🌐 Web query:
Spring @Cacheable null key IllegalArgumentException Spring Boot 3 cache
💡 Result:
In Spring Boot 3.x (Spring Framework 6.1+), @Cacheable(key = "#paramName") can throw:
IllegalArgumentException: Null key returned for cache operation ... If you are using named parameters, ensure that the compiler uses the '-parameters' flag
because Spring no longer tries to infer Java parameter names from bytecode debug info; if your code isn’t compiled with -parameters, SpEL can’t resolve #username (etc.), so the key expression evaluates to null and caching fails. (github.com)
Fix options
- Compile with parameter-name retention (
-parameters) (recommended)
- Maven
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <parameters>true</parameters> </configuration> </plugin>
- Gradle
tasks.withType(JavaCompile).configureEach { options.compilerArgs.add("-parameters") }
This restores #paramName support. (github.com)
- Use index-based arguments in SpEL (works even without
-parameters)
`@Cacheable`(cacheNames="user", key="#p0") // or `#a0`Spring explicitly documents #a0/#p0 as the fallback when parameter names aren’t available. (docs.enterprise.spring.io)
- If the actual argument value can be null, guard it
`@Cacheable`(cacheNames="user", key="#p0", condition="#p0 != null")Note: unless is evaluated after invocation; it won’t prevent a failure caused by a null key computed up front. (docs.enterprise.spring.io)
Citations:
- 1: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes
- 2: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes
- 3: https://docs.enterprise.spring.io/spring-framework/reference/integration/cache/annotations.html?utm_source=openai
- 4: https://docs.enterprise.spring.io/spring-framework/reference/integration/cache/annotations.html?utm_source=openai
🏁 Script executed:
rg "getMyPage|updateMyPage|updateProfile" src/main/java --type java -B 2 -A 5Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 4733
🏁 Script executed:
rg "@CacheEvict|@CachePut|evict" src/main/java/com/whereyouad/WhereYouAd/domains/user --type javaRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 55
🏁 Script executed:
find . -name "pom.xml" -o -name "build.gradle" | head -5Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 87
🏁 Script executed:
cat pom.xml | grep -A 10 "maven-compiler-plugin"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 112
🏁 Script executed:
cat build.gradleRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 1960
🏁 Script executed:
rg "CACHEABLE_CONFIG|cache:" src/main/java --type java -A 3Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 55
🏁 Script executed:
grep -r "parameters.*true\|compilerArgs.*parameters" . --include="*.gradle" --include="*.xml"Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 55
🏁 Script executed:
cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java | sed -n '48,52p'Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 484
🏁 Script executed:
python3 << 'EOF'
# Analyze Spring Security pattern matching for /api/users/my
patterns = [
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html",
"/api/users/**",
"/api/auth/**"
]
endpoints = [
"/api/users/my",
"/api/users/signup",
"/api/auth/login"
]
from fnmatch import fnmatch
print("Pattern Matching Analysis:")
print("=" * 60)
for endpoint in endpoints:
matches = [p for p in patterns if fnmatch(endpoint, p)]
if matches:
print(f"✓ {endpoint} matches permitAll patterns: {matches}")
else:
print(f"✗ {endpoint} requires authentication")
EOFRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 351
/api/users/my 엔드포인트가 permitAll()로 노출되어 있어서 인증 없이 접근 가능합니다.
SecurityConfig의 .requestMatchers("/api/users/**").permitAll()로 인해 마이페이지 엔드포인트가 누구나 접근할 수 있게 열려 있습니다. 이 경우 @AuthenticationPrincipal(expression = "userId") Long userId는 null이 되고, null 키로 @Cacheable을 실행하면 예외가 발생할 수 있습니다.
해결책: 보호된 엔드포인트로 변경하세요.
.requestMatchers("/api/users/signup", "/api/users/email-send", "/api/users/email-verify",
"/api/users/sms-send", "/api/users/sms-verify",
"/api/users/password-reset/**").permitAll()
.anyRequest().authenticated()그리고 /api/users/my는 인증된 요청만 도달하도록 보호되어야 합니다. 현재 JWT 필터와 인증 메커니즘은 잘 설계되어 있으니, 설정만 바로잡으면 userId는 항상 유효한 값이 됩니다.
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`
around lines 97 - 103, The API security currently allows all "/api/users/**" via
SecurityConfig.requestMatchers(...).permitAll(), which exposes the protected
"/api/users/my" endpoint and can cause a null `@AuthenticationPrincipal` (userId)
leading to cache/key errors in UserService.getMyPage (annotated with `@Cacheable`
and using userId); update SecurityConfig to only permit public endpoints
(signup, email-send, email-verify, sms-send, sms-verify, password-reset/**) in
requestMatchers and ensure .anyRequest().authenticated() is applied so that
"/api/users/my" requires authentication and userId is always present for
UserService.getMyPage.
src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java
Show resolved
Hide resolved
|
프론트 측에서 로그인 유형을 마이페이지 API 응답에 추가하는걸 요청하셨는데 이 부분 처리하려면 JWT 랑 소셜 로그인 쪽을 추가 수정해야 할 것 같아 좀 시간이 걸릴 것 같습니다. 이 PR 은 해당 수정 사항 진행한 뒤에 리뷰해주시는게 좋을 듯 해요! |
넵 이 부분은 민규님이랑 함께 담당해도 괜찮을 것 같습니다! |
kingmingyu
left a comment
There was a problem hiding this comment.
P4: 고생하셨습니다! 찾아보니까 마이페이지에는 redis를 많이 적용하는 것 같아서 적용하는 것도 좋은 것 같습니다!
...ain/java/com/whereyouad/WhereYouAd/domains/user/application/dto/response/MyPageResponse.java
Outdated
Show resolved
Hide resolved
이 부분은 제가 소셜 로그인하면 password 부분을 아마 null로 만들고 있어서 유저 정보 조회할 때 password 부분이 null인지만 확인해서 프론트에서는 보기 좋게 isSocial같은 값을 true 나 false로 같이 반환해주면 될 것 같아요!! (UserConverter 부분 toSocialUser 부분 참고) |
사실 저도 이 방법으로 소셜 로그인 인지 / 이메일 로그인인지 확인 가능하게 하면 되지 않을까 싶었는데 프론트 쪽에서 각 소셜 로그인 플랫폼 구분을 원할 수도 있다고 생각해서요...ㅠㅠ 일단 프론트 측에 이러한 방식으로 소셜로그인 유무만 반환 해줘도 괜찮은지 물어보고 나서 진행하겠습니다! |
|
오늘은 주말이기도 해서 팀 휴식기간 이후에 제경님께 디스코드로
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java (1)
86-93:⚠️ Potential issue | 🟠 Major재발급 로직에서 OAuth2 사용자의
provider정보가 변경되는 버그가 있습니다.RefreshToken에
provider클레임이 없는 것은 의도된 설계라 괜찮지만(사용자 이메일만 필요), 재발급 시 다음 문제가 발생합니다:
OAuth2 사용자(GOOGLE, GITHUB)가 토큰을 재발급받을 때:
AuthService.reIssue()에서customUserDetailsService.loadUserByUsername(email)호출CustomUserDetailsService라인 26에서 항상Provider.EMAIL로 고정 설정- 원래 GOOGLE/GITHUB 사용자의
provider가 EMAIL로 변경됨AccessToken 재생성 시:
- 변경된
provider정보로 새 AccessToken이 발급되어 사용자 인증 정보가 틀어짐해결방안:
CustomUserDetailsService.loadUserByUsername()메서드에서 OAuth2 사용자의 원래 provider 정보를 DB에서 조회하여 설정하거나, 재발급 로직에서 AccessToken의provider클레임을 파싱하여 복원해야 합니다.
🤖 Fix all issues with AI agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`:
- Around line 91-106: The cache key currently includes provider in getMyPage
(Cacheable key = "#userId + ':' + `#provider`"), which creates separate cache
entries per provider and forces complex invalidation; either stop including
provider in the cache key and cache only by userId (change `@Cacheable` key to use
only userId and return/stash only user fields, then add provider via
UserConverter.toMyPageResponse or assemble in the controller), or keep provider
in the key but implement eviction that removes all provider variants when user
data changes; locate getMyPage and UserConverter.toMyPageResponse to implement
the chosen approach and ensure the cached value does not embed provider-specific
state so profile updates only require a single eviction.
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java`:
- Around line 88-100: Remove the debug System.out.println in
UserController.getMyPage, replace it with SLF4J logging (use the controller's
logger, e.g., log.info(...)) and add a defensive null-check for the
AuthenticationPrincipal userDetails at the start of getMyPage (return an
appropriate error ResponseEntity or throw a domain-specific exception handled by
GlobalExceptionHandler) before calling
userService.getMyPage(userDetails.getUserId(),
userDetails.getProvider().name()); ensure you reference the existing getMyPage
method and userDetails variable when making the change.
🧹 Nitpick comments (3)
src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetailsService.java (1)
26-26: 동작은 정확하지만,Provider.EMAIL하드코딩에 대한 맥락 주석이 있으면 좋겠습니다.
loadUserByUsername은 이메일/비밀번호 로그인 flow뿐만 아니라JwtAuthenticationFilter에서도 호출됩니다. 필터에서는 이후 JWT의 provider 값으로 새CustomUserDetails를 재생성하므로 동작에 문제는 없지만, 코드만 보면 "이 유저는 항상 EMAIL provider인가?"라는 오해를 줄 수 있습니다.간단한 주석을 추가하면 다른 개발자가 flow를 이해하는 데 도움이 됩니다:
💡 주석 추가 제안
- return new CustomUserDetails(user, Provider.EMAIL); + // 기본값 EMAIL 설정. JWT 기반 인증 시 JwtAuthenticationFilter에서 토큰의 provider로 재할당됨. + return new CustomUserDetails(user, Provider.EMAIL);src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java (1)
99-106:provider를String으로 받으면 캐시 키 불일치 위험이 있습니다.
provider파라미터가String타입이라서 호출 측에서"EMAIL","email","Email"등 다양한 형태로 전달할 수 있습니다. 이 경우 같은 유저+같은 provider인데도 캐시가 따로 생성되어 캐시 히트율이 떨어집니다.
Providerenum을 직접 받으면 타입 안전성과 캐시 키 일관성을 동시에 확보할 수 있습니다:💡 Provider enum 타입으로 변경 제안
- `@Cacheable`(value = "user:profile", key = "#userId + ':' + `#provider`", unless = "#result == null") + `@Cacheable`(value = "user:profile", key = "#userId + ':' + `#provider.name`()", unless = "#result == null") `@Transactional`(readOnly = true) - public MyPageResponse getMyPage(Long userId, String provider) { + public MyPageResponse getMyPage(Long userId, Provider provider) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); - return UserConverter.toMyPageResponse(user, provider); + return UserConverter.toMyPageResponse(user, provider.name()); }src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java (1)
52-68: Java 17instanceof패턴 매칭을 활용하면 더 깔끔해져요! 👍현재 코드는
instanceof체크 후 별도 캐스팅을 하고 있는데, Java 17에서는 패턴 변수를 사용해서 한 줄로 줄일 수 있어요. 예를 들어:if (principal instanceof CustomOAuth2User oAuth2User) { // 바로 oAuth2User 사용 가능 }이렇게 하면 불필요한 캐스팅 라인이 사라지고 가독성이 올라갑니다.
✨ Java 17 패턴 매칭 적용 예시
if (principal instanceof CustomOAuth2User) { - CustomOAuth2User oAuth2User = (CustomOAuth2User) principal; - // 소셜 로그인: CustomOAuth2User에서 이메일 추출 - email = oAuth2User.getEmail(); - - provider = oAuth2User.getProvider().name(); - } else if (principal instanceof CustomUserDetails) { - CustomUserDetails userDetails = (CustomUserDetails) principal; - // 일반 로그인: UserDetails의 username(email) 추출 - email = userDetails.getUsername(); - - provider = userDetails.getProvider().name(); + if (principal instanceof CustomOAuth2User oAuth2User) { + email = oAuth2User.getEmail(); + provider = oAuth2User.getProvider().name(); + } else if (principal instanceof CustomUserDetails userDetails) { + email = userDetails.getUsername(); + provider = userDetails.getProvider().name(); } else {As per coding guidelines, "Java 17의 최신 문법(Record, Switch Expression, Text Block 등)을 적절히 사용하는지 확인하라."
| /** | ||
| * 마이페이지 조회 | ||
| * 파라미터 추가: userId 외에 'provider'(로그인 유형) 도 받기 | ||
| * 캐시 키 수정: key = "#userId + ':' + #provider" | ||
| * 같은 유저(userId=1)라도 '구글'로 로그인했을 때와 '이메일'로 로그인했을 때 | ||
| * 응답 데이터(MyPageResponse의 provider 필드)가 다르므로 캐시를 구분해야 합니다. | ||
| * 예) user:profile::1:GOOGLE / user:profile::1:EMAIL 로 따로 저장됨. | ||
| */ | ||
| @Cacheable(value = "user:profile", key = "#userId + ':' + #provider", unless = "#result == null") | ||
| @Transactional(readOnly = true) | ||
| public MyPageResponse getMyPage(Long userId, String provider) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); | ||
|
|
||
| return UserConverter.toMyPageResponse(user, provider); | ||
| } |
There was a problem hiding this comment.
같은 유저인데 provider별로 캐시를 분리하는 것이 정말 필요한지 검토해 주세요.
현재 캐시 키가 userId + ':' + provider이므로 같은 유저(userId=1)가 이메일 로그인, 구글 로그인할 때마다 별도 캐시 엔티리가 생깁니다. 하지만 MyPageResponse에서 provider 외의 필드(email, name, phoneNumber 등)는 모두 DB의 동일한 User 엔티티에서 가져오므로, 프로필 업데이트 시 모든 provider 변형의 캐시를 각각 evict해야 데이터 불일치를 방지할 수 있습니다.
예를 들어 유저가 이름을 변경하면 user:profile::1:EMAIL, user:profile::1:GOOGLE 등을 모두 무효화해야 하는데, 이는 관리가 복잡해집니다.
대안으로, provider 정보는 캐시에 포함하지 않고 캐시 키를 userId로만 설정한 뒤, 응답 조립 시 provider를 동적으로 추가하는 방식을 고려해 보세요:
💡 캐시 키 단순화 예시
// UserService
`@Cacheable`(value = "user:profile", key = "#userId", unless = "#result == null")
`@Transactional`(readOnly = true)
public MyPageResponse getMyPage(Long userId, Provider provider) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));
return UserConverter.toMyPageResponse(user, provider.name());
}단, 이 경우에도 provider가 응답에 포함되면 캐시된 값과 현재 로그인 provider가 다를 수 있으므로, provider를 캐시 대상에서 분리하는 구조가 더 적합합니다:
// 캐시는 User 정보만, provider는 컨트롤러에서 조립
`@Cacheable`(value = "user:profile", key = "#userId")
public UserProfileCache getUserProfile(Long userId) { ... }
// 컨트롤러에서
UserProfileCache cached = userService.getUserProfile(userId);
MyPageResponse response = MyPageResponse.of(cached, provider);🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`
around lines 91 - 106, The cache key currently includes provider in getMyPage
(Cacheable key = "#userId + ':' + `#provider`"), which creates separate cache
entries per provider and forces complex invalidation; either stop including
provider in the cache key and cache only by userId (change `@Cacheable` key to use
only userId and return/stash only user fields, then add provider via
UserConverter.toMyPageResponse or assemble in the controller), or keep provider
in the key but implement eviction that removes all provider variants when user
data changes; locate getMyPage and UserConverter.toMyPageResponse to implement
the chosen approach and ensure the cached value does not embed provider-specific
state so profile updates only require a single eviction.




📌 관련 이슈
🚀 개요
프론트 요청에 따라 로그인한 회원의 마이페이지 API 추가
📄 작업 내용
📸 스크린샷 / 테스트 결과 (선택)
✅ 체크리스트
🔍 리뷰 포인트 (Review Points)
Summary by CodeRabbit