Skip to content

Feat/#31 마이페이지 추가#32

Open
ojy0903 wants to merge 16 commits intodevelopfrom
feat/#31
Open

Feat/#31 마이페이지 추가#32
ojy0903 wants to merge 16 commits intodevelopfrom
feat/#31

Conversation

@ojy0903
Copy link
Collaborator

@ojy0903 ojy0903 commented Feb 6, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.

프론트 요청에 따라 로그인한 회원의 마이페이지 API 추가

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  • 마이페이지 API 추가
  • Redis 를 통한 캐싱 적용

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

  1. id = 1 인 test 회원으로 로그인
image
  1. 해당 AccessToken 을 사용해 최초 마이페이지 조회(최초 이므로 redis 에 값이 없던 상태에서 조회 이후 값이 생김)
image image image

✅ 체크리스트

  • [✅] 브랜치 전략(GitHub Flow)을 준수했나요?
  • [✅] 메서드 단위로 코드가 잘 쪼개져 있나요?
  • [✅] 테스트 통과 확인
  • [✅] 서버 실행 확인
  • [✅] API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • 마이페이지 조회는 자주 조회가 발생하기 때문에 Redis 로 캐싱을 하면 좋다고 하여 적용해봤는데 Redis 를 기존에 써봤던게 아니라서 이상한 점 있으면 리뷰 부탁드립니다...

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • 새로운 기능
    • 내 프로필 조회 엔드포인트 추가(아이디, 이메일, 이름, 프로필 이미지, 전화번호, 이메일 인증 상태, 로그인 제공자 표시)
  • 성능 개선
    • Redis 기반 캐싱으로 프로필 조회 응답 속도 향상(30분 만료, null 값 미캐시)
  • 보안
    • 내 프로필 엔드포인트에 인증 요구 추가 및 토큰에 로그인 제공자 정보 포함
  • 문서
    • API 문서에 내 프로필 엔드포인트 명세 추가

@ojy0903 ojy0903 self-assigned this Feb 6, 2026
@ojy0903 ojy0903 added the ✨ Feature 새로운 기능 추가 label Feb 6, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Walkthrough

로그인한 사용자의 마이페이지 조회 API가 추가되었습니다. User → MyPageResponse 매핑, Redis 기반 캐시 설정(30분 TTL), @Cacheable 적용된 서비스 메서드(캐시 키에 provider 포함), 인증이 필요한 컨트롤러 엔드포인트와 Swagger 문서가 함께 추가되었습니다.

Changes

Cohort / File(s) Summary
DTO & Mapper
src/main/java/.../domains/user/application/dto/response/MyPageResponse.java, src/main/java/.../domains/user/application/mapper/UserConverter.java
MyPageResponse Java record 추가 및 UserMyPageResponse 변환 메서드 toMyPageResponse(User, String) 추가.
서비스
src/main/java/.../domains/user/domain/service/UserService.java
getMyPage(Long userId, String provider) 추가 (@Transactional(readOnly=true) + @Cacheable(value="user:profile", key = "#userId + ':' + #provider", unless = "#result == null")).
컨트롤러
src/main/java/.../domains/user/presentation/UserController.java
GET /api/users/my 엔드포인트 추가; @AuthenticationPrincipal CustomUserDetails로 userId·provider를 받아 DataResponse<MyPageResponse> 반환.
API 문서
src/main/java/.../domains/user/presentation/docs/UserControllerDocs.java
마이페이지 엔드포인트용 Swagger/OpenAPI 문서 추가.
캐시 설정
src/main/java/.../global/config/RedisConfig.java
RedisCacheManager 빈 추가 — 키: String 직렬화, 값: JSON 직렬화, TTL 30분, null 값 캐싱 비활성화.
보안·인증·토큰
src/main/java/.../global/security/...
JWT에 provider 클레임 추가 및 읽기(PROVIDER_KEY), CustomUserDetails에 provider 필드 추가, JwtAuthenticationFilter에서 토큰의 provider로 CustomUserDetails 재구성, JwtTokenProvider.getProvider() 추가, CustomOAuth2User/ServiceCustomUserDetailsService 변경.
도메인 상수
src/main/java/.../domains/user/domain/constant/Provider.java
EMAIL("email", "이메일") enum 상수 추가.
기타
여러 파일
관련 import/생성자/생성자 호출 시그니처 변경 및 인증 흐름에서 provider 전파 관련 조정.

Sequence Diagram

sequenceDiagram
    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>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jinnieusLab
  • kingmingyu

검토 포인트(짧게)

  • AuthenticationPrincipal로 전달되는 CustomUserDetails에 userId·provider가 항상 존재하는지(Null 안전) 확인하세요.
  • 캐시 키에 provider를 포함한 설계는 적절하나, 개인정보 저장 정책과 네임스페이스(prefix) 규칙을 재검토하세요.
  • JwtTokenProvider.getProvider/claim 처리에서 토큰에 provider가 없거나 잘못된 값일 때의 기본 동작(예: EMAIL 디폴트)과 에러 처리를 명확히 유지하세요.

잘 정리된 변경입니다 — 특히 Redis 캐시 설정과 서비스 레벨의 Cacheable 적용은 깔끔합니다. 한편 Jwt/Authentication 흐름에서 provider를 재구성하는 부분은 null·예외 경로(토큰 변조 등)를 추가로 검증해 주세요.

🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Feat/#31 마이페이지 추가'는 변경 사항의 핵심(마이페이지 API 추가)을 명확하고 간결하게 설명합니다.
Description check ✅ Passed PR 설명은 필수 섹션(관련 이슈, 개요, 작업 내용, 테스트 결과, 체크리스트)이 모두 포함되고 세부 내용이 충실하며 검증 스크린샷도 구체적입니다.
Linked Issues check ✅ Passed PR 변경 사항이 #31의 요구사항(로그인 회원의 마이페이지 API 추가 [#31], Redis 캐싱 적용 [#31])을 모두 충족하고 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 마이페이지 API 기능 및 Redis 캐싱 구현에 포함되어 있으며, 범위를 벗어나는 변경은 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#31
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/#31
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 메타데이터를 저장하므로, 패키지/클래스명이 바뀌는 배포가 있으면 캐시 무효화(키 버전업 또는 캐시 삭제) 전략을 함께 두는 게 안전합니다.

Comment on lines 97 to 103
@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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# UserService.java 파일 확인
find . -type f -name "UserService.java" | head -1 | xargs wc -l

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 163


🏁 Script executed:

# 사용자 프로필 업데이트/수정 흐름이 있는지 확인
rg -n "updateProfile|updatePhone|updateImage|updateUser|@CacheEvict" src/main/java --type java -i

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 362


🏁 Script executed:

# UserService.java 전체 내용 확인
find . -type f -name "UserService.java" | head -1 | xargs cat -n

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5117


🏁 Script executed:

# CustomOAuth2UserService.java 파일 확인 및 updateProfile 주변 코드 보기
find . -type f -name "CustomOAuth2UserService.java" | xargs wc -l

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 178


🏁 Script executed:

# CustomOAuth2UserService.java 전체 내용 확인
find . -type f -name "CustomOAuth2UserService.java" | xargs cat -n

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5357


🏁 Script executed:

# 프로필 업데이트 관련 컨트롤러 또는 엔드포인트 검색
rg -n "updateProfile|updateUser|updatePhone|updateImage" src/main/java --type java -B 3 -A 5

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2341


🏁 Script executed:

# User 엔티티에서 프로필 관련 메서드 확인
find . -type f -name "User.java" -path "*/domains/user/*" | xargs cat -n

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1988


소셜 로그인 시 프로필 변경 후 캐시 무효화 처리 필수입니다.

현재 CustomOAuth2UserServiceloadUser() 메서드에서 기존 소셜 계정으로 로그인 시 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.

⚠️ Potential issue | 🟠 Major

🧩 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/java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 257


🏁 Script executed:

fd "UserService" src/main/java --type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 257


🏁 Script executed:

git ls-files src/main/java | grep -i userservice

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 257


🏁 Script executed:

cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java | head -120

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5117


🏁 Script executed:

wc -l src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 161


🏁 Script executed:

fd ".*Controller.*" src/main/java --type f | grep -i user

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 434


🏁 Script executed:

git ls-files src/main/java | grep -i controller | grep -i user

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 434


🏁 Script executed:

rg "getMyPage" src/main/java --type java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 797


🏁 Script executed:

cat -n src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5127


🏁 Script executed:

wc -l src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java

Repository: 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 2

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 267


🏁 Script executed:

fd "SecurityConfig|SecurityConfiguration" src/main/java --type f

Repository: 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 -150

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5187


🏁 Script executed:

wc -l src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 151


🏁 Script executed:

fd "JwtAuthenticationFilter" src/main/java --type f

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 161


🏁 Script executed:

rg "userId" src/main/java/com/whereyouad/WhereYouAd/global/security --type java -B 2 -A 2

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

git ls-files src/main/java | grep -i "principal\|userdetails\|authentication" | head -20

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 538


🏁 Script executed:

cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetails.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2393


🏁 Script executed:

cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java | head -80

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 4008


🏁 Script executed:

cat -n src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java | tail -30

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1401


🏁 Script executed:

rg "GlobalExceptionHandler|@ExceptionHandler|ControllerAdvice" src/main/java --type java -l

Repository: 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.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 5191


🏁 Script executed:

web_search
Spring `@AuthenticationPrincipal` null when unauthenticated Spring Security 6

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 168


🏁 Script executed:

web_search
Spring `@Cacheable` null key IllegalArgumentException Spring Boot 3

Repository: 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

  1. 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)

  1. 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)

  1. 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:


🏁 Script executed:

rg "getMyPage|updateMyPage|updateProfile" src/main/java --type java -B 2 -A 5

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 4733


🏁 Script executed:

rg "@CacheEvict|@CachePut|evict" src/main/java/com/whereyouad/WhereYouAd/domains/user --type java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

find . -name "pom.xml" -o -name "build.gradle" | head -5

Repository: 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.gradle

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1960


🏁 Script executed:

rg "CACHEABLE_CONFIG|cache:" src/main/java --type java -A 3

Repository: 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")
EOF

Repository: 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.

@ojy0903
Copy link
Collaborator Author

ojy0903 commented Feb 6, 2026

프론트 측에서 로그인 유형을 마이페이지 API 응답에 추가하는걸 요청하셨는데 이 부분 처리하려면 JWT 랑 소셜 로그인 쪽을 추가 수정해야 할 것 같아 좀 시간이 걸릴 것 같습니다. 이 PR 은 해당 수정 사항 진행한 뒤에 리뷰해주시는게 좋을 듯 해요!

@jinnieusLab
Copy link
Collaborator

프론트 측에서 로그인 유형을 마이페이지 API 응답에 추가하는걸 요청하셨는데 이 부분 처리하려면 JWT 랑 소셜 로그인 쪽을 추가 수정해야 할 것 같아 좀 시간이 걸릴 것 같습니다. 이 PR 은 해당 수정 사항 진행한 뒤에 리뷰해주시는게 좋을 듯 해요!

넵 이 부분은 민규님이랑 함께 담당해도 괜찮을 것 같습니다!

Copy link
Collaborator

@kingmingyu kingmingyu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4: 고생하셨습니다! 찾아보니까 마이페이지에는 redis를 많이 적용하는 것 같아서 적용하는 것도 좋은 것 같습니다!

@kingmingyu
Copy link
Collaborator

프론트 측에서 로그인 유형을 마이페이지 API 응답에 추가하는걸 요청하셨는데 이 부분 처리하려면 JWT 랑 소셜 로그인 쪽을 추가 수정해야 할 것 같아 좀 시간이 걸릴 것 같습니다. 이 PR 은 해당 수정 사항 진행한 뒤에 리뷰해주시는게 좋을 듯 해요!

이 부분은 제가 소셜 로그인하면 password 부분을 아마 null로 만들고 있어서 유저 정보 조회할 때 password 부분이 null인지만 확인해서 프론트에서는 보기 좋게 isSocial같은 값을 true 나 false로 같이 반환해주면 될 것 같아요!! (UserConverter 부분 toSocialUser 부분 참고)

@ojy0903
Copy link
Collaborator Author

ojy0903 commented Feb 8, 2026

이 부분은 제가 소셜 로그인하면 password 부분을 아마 null로 만들고 있어서 유저 정보 조회할 때 password 부분이 null인지만 확인해서 프론트에서는 보기 좋게 isSocial같은 값을 true 나 false로 같이 반환해주면 될 것 같아요!! (UserConverter 부분 toSocialUser 부분 참고)

사실 저도 이 방법으로 소셜 로그인 인지 / 이메일 로그인인지 확인 가능하게 하면 되지 않을까 싶었는데 프론트 쪽에서 각 소셜 로그인 플랫폼 구분을 원할 수도 있다고 생각해서요...ㅠㅠ 일단 프론트 측에 이러한 방식으로 소셜로그인 유무만 반환 해줘도 괜찮은지 물어보고 나서 진행하겠습니다!

@ojy0903
Copy link
Collaborator Author

ojy0903 commented Feb 8, 2026

오늘은 주말이기도 해서 팀 휴식기간 이후에 제경님께 디스코드로

  1. 소셜 로그인 여부를 단순히 소셜로그인 인지 이메일 로그인인지 (isSocialLogin 같은 boolean 형식) 으로 반환해도 괜찮은지,
  2. 해당 회원의 조직에 대한 정보는 마이페이지 API 응답값에 필요없는지
    물어보겠습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 클레임이 없는 것은 의도된 설계라 괜찮지만(사용자 이메일만 필요), 재발급 시 다음 문제가 발생합니다:

  1. OAuth2 사용자(GOOGLE, GITHUB)가 토큰을 재발급받을 때:

    • AuthService.reIssue()에서 customUserDetailsService.loadUserByUsername(email) 호출
    • CustomUserDetailsService 라인 26에서 항상 Provider.EMAIL로 고정 설정
    • 원래 GOOGLE/GITHUB 사용자의 provider가 EMAIL로 변경됨
  2. 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: providerString으로 받으면 캐시 키 불일치 위험이 있습니다.

provider 파라미터가 String 타입이라서 호출 측에서 "EMAIL", "email", "Email" 등 다양한 형태로 전달할 수 있습니다. 이 경우 같은 유저+같은 provider인데도 캐시가 따로 생성되어 캐시 히트율이 떨어집니다.

Provider enum을 직접 받으면 타입 안전성과 캐시 키 일관성을 동시에 확보할 수 있습니다:

💡 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 17 instanceof 패턴 매칭을 활용하면 더 깔끔해져요! 👍

현재 코드는 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 등)을 적절히 사용하는지 확인하라."

Comment on lines +91 to +106
/**
* 마이페이지 조회
* 파라미터 추가: 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);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

같은 유저인데 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.

@ojy0903
Copy link
Collaborator Author

ojy0903 commented Feb 14, 2026

프론트 측에서 로그인 유형이 구체적으로 반환되면 추후 UI 처리에 유용할 것 같다고 해서 로그인 유형을 반환하는 방식으로 진행했습니다! 아래 테스트 캡쳐 사진 보고 확인하시면 될 것 같고 네이버 소셜로그인의 경우 따로 설정해주셔야 하는 부분이 있는거 같아 일단 제외하고 테스팅 진행했습니다. 회원의 조직 정보의 경우에는 일단 반환하지 않는 상태로 진행하고, 조직 관련 작업이 어느정도 마무리 된 후에 추가 수정으로 붙이는게 좋을 것 같아 제외하고 진행했습니다!

  1. 일반 이메일 로그인 마이페이지 응답
일반 이메일 로그인 마이페이지
  1. 구글 로그인 마이페이지 응답
구글 로그인
  1. 카카오 로그인 마이페이지 응답
카카오 로그인
  1. 네이버 로그인 불가(추가 설정 필요한듯 하네요. 디스코드에서 제경님이 언급하신 소셜로그인 이슈 관련인 듯 합니다!)
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 마이페이지 - 로그인 회원의 마이페이지 추가

3 participants