Skip to content

✨ Feat: 사용자 정보 수정, 조회 기능 구현 + 사용자 정보 캐시 무효화#46

Merged
imjuyongp merged 3 commits intodevelopfrom
refactor/user
Feb 4, 2026
Merged

✨ Feat: 사용자 정보 수정, 조회 기능 구현 + 사용자 정보 캐시 무효화#46
imjuyongp merged 3 commits intodevelopfrom
refactor/user

Conversation

@imjuyongp
Copy link
Copy Markdown
Member

@imjuyongp imjuyongp commented Feb 3, 2026

#️⃣ Issue Number

📝 요약(Summary)

생성된 파일

  • UpdateProfileRequest.java : 프로필 수정 요청 DTO
  • UpdateProfileResponse.java : 프로필 수정 응답 DTO
  • UserInfoResponse.java : 사용자 정보 조회 응답 DTO
  • UserAuthInfo 필드 추가
    • 추가됨
      profileImage, introduce, gender, phoneNumber, interestType

수정된 파일

  • User.java : updateProfile() 메서드 추가
  • UserService.java : updateProfile() 인터페이스 추가
  • UserServiceImpl.java : 프로필 수정 로직 + 캐시 무효화
  • UserController.java : PUT /api/users/profile 엔드포인트 추가
  • UserService.java : getUserInfo() 인터페이스 추가
  • UserServiceImpl.java : getUserInfo() 구현
  • UserController.java : GET /api/users/me 엔드포인트 추가

API 스펙

  • PUT /api/users/profile
    Authorization: Bearer {accessToken}

Request Body:
{
"nickname": "새로운닉네임",
"introduce": "안녕하세요! 축구를 좋아합니다."
}

Response:
{
"success": true,
"message": "프로필 수정 성공",
"data": {
"userId": 1,
"nickname": "새로운닉네임",
"introduce": "안녕하세요! 축구를 좋아합니다."
}
}

  • GET /api/users/me
    Authorization: Bearer {accessToken}

    Response:
    {
    "success": true,
    "message": "사용자 정보 조회 성공",
    "data": {
    "userId": 1,
    "username": "test@example.com",
    "nickname": "축구왕",
    "introduce": "안녕하세요!",
    "profileImage": "https://s3.../profile.jpg",
    "gender": "MALE",
    "phoneNumber": "010-1234-5678",
    "interestType": ["SOCCER", "BASKETBALL"]
    }
    }


캐시 처리

  • 닉네임 변경 시 userCacheService.evictUserAuthInfo(userId) 호출하여 캐시 무효화 → 다음 요청에서 새 정보로 갱신
  • 프로필 사진 수정 시 캐시 무효화
  • 내 정보 조회 캐시

🛠️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 코드 리팩토링
  • 주석 추가 및 수정

📸스크린샷 (선택)

💬 공유사항 to 리뷰어

  • erd 수정 사항 있습니다.
  • 사용자 정보 관련 캐시 키 값 -> ''' userAuthInfo '''

✅ PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

Summary by CodeRabbit

새로운 기능

  • 프로필 수정: 닉네임과 자기소개를 언제든지 수정하여 프로필을 최신 상태로 유지할 수 있습니다.
  • 내 정보 조회: 로그인 후 자신의 전체 프로필 정보를 한곳에서 확인할 수 있습니다.
  • 회원가입 확대: 가입 시 성별과 휴대폰 번호를 추가로 입력하여 더 완전한 프로필을 구성할 수 있습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 3, 2026

안내

Walkthrough

사용자 프로필 조회 및 수정 기능을 추가하고, 해당 기능에 캐시 무효화 처리를 적용했습니다. Gender 열거형과 새로운 DTOs를 도입하며, User 엔티티와 UserAuthInfo에 필드를 확장했습니다.

Changes

Cohort / File(s) Summary
User Controller 엔드포인트
src/main/java/com/be/sportizebe/domain/user/controller/UserController.java
사용자 프로필 수정(PUT /profile)과 마이정보 조회(GET /me) 두 개의 새로운 엔드포인트 추가
Request/Response DTO
src/main/java/com/be/sportizebe/domain/user/dto/request/UpdateProfileRequest.java, src/main/java/com/be/sportizebe/domain/user/dto/response/UpdateProfileResponse.java, src/main/java/com/be/sportizebe/domain/user/dto/response/UserInfoResponse.java
프로필 수정과 사용자 정보 조회를 위한 새로운 DTO 클래스 및 팩토리 메서드 추가
User 엔티티 확장
src/main/java/com/be/sportizebe/domain/user/entity/User.java, src/main/java/com/be/sportizebe/domain/user/entity/Gender.java, src/main/java/com/be/sportizebe/domain/user/dto/request/SignUpRequest.java
Gender 열거형 신규 생성, User 엔티티에 introduce, gender, phoneNumber 필드 추가 및 updateProfile 메서드 구현. SignUpRequest에 gender와 phoneNumber 필드 추가
Service Layer
src/main/java/com/be/sportizebe/domain/user/service/UserService.java, src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java
updateProfile과 getUserInfo 메서드 추가. UserCacheService 의존성 주입 및 프로필 수정 시 캐시 무효화 처리 적용
Cache 통합
src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java
@builder 애너테이션 추가 및 profileImage, introduce, gender, phoneNumber, interestType 필드 확장. 팩토리 메서드에서 새로운 필드들 매핑

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant CacheService
    participant Database
    
    rect rgba(100, 150, 200, 0.5)
    Note over Client,Database: 사용자 프로필 수정 흐름
    Client->>Controller: PUT /profile (UpdateProfileRequest)
    Controller->>Service: updateProfile(userId, request)
    Service->>Database: findById(userId)
    Database-->>Service: User entity
    Service->>Service: updateProfile(nickname, introduce)
    Service->>Database: save(user)
    Database-->>Service: updated User
    Service->>CacheService: evictUserAuthInfo(userId)
    CacheService-->>Service: cache cleared
    Service-->>Controller: UpdateProfileResponse
    Controller-->>Client: BaseResponse<UpdateProfileResponse>
    end
    
    rect rgba(200, 100, 100, 0.5)
    Note over Client,Database: 사용자 정보 조회 흐름
    Client->>Controller: GET /me (authenticated)
    Controller->>Service: getUserInfo(userId)
    Service->>CacheService: getUserAuthInfo(userId)
    CacheService-->>Service: UserAuthInfo (from cache)
    Service-->>Controller: UserInfoResponse
    Controller-->>Client: BaseResponse<UserInfoResponse>
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #44: 사용자 인증 정보를 UserAuthInfo로 리팩토링하고 UserCacheService/Redis 설정을 추가한 PR로, 이 PR의 캐시 통합 및 UserAuthInfo 확장과 직접 연관됩니다.
  • PR #37: 프로필 이미지 업로드와 프로필 정보 조회 기능을 추가한 PR로, User 엔티티와 컨트롤러에서의 프로필 관련 기능 확장이 겹칩니다.
  • PR #11: 초기 회원가입 및 인증 관련 구현 PR로, SignUpRequest 확장 및 User 엔티티 수정이 겹치는 영역입니다.

Poem

🐰 사용자 정보 수정 기능이 쏘옥, 캐시도 깔끔하게 무효화
프로필을 꾸미고, 마이페이지에서 확인해,
Gender와 phoneNumber로 더 완성된 서비스,
코더의 손길 하나하나가 모여 빛나는 기능이야!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 사용자 정보 수정, 조회 기능 구현과 캐시 무효화라는 핵심 변경사항을 명확하게 요약하고 있으며, 이모지는 있지만 changeset의 주요 변경사항을 정확하게 반영합니다.
Linked Issues check ✅ Passed PR은 연결된 이슈 #41의 모든 요구사항을 충족합니다: 사용자 정보 조회 API 구현(UserInfoResponse, GET /api/users/me), 사용자 정보 수정 기능 구현(UpdateProfileRequest/Response, PUT /api/users/profile), 캐시 무효화 로직 적용(UserCacheService.evictUserAuthInfo 호출).
Out of Scope Changes check ✅ Passed 모든 변경사항이 issue #41의 범위 내에 있습니다. 사용자 정보 수정/조회 API와 캐시 무효화 구현에 필요한 DTO, 엔티티, 서비스, 컨트롤러 변경만 포함되어 있습니다.

✏️ 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 refactor/user

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


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
Copy Markdown

@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/be/sportizebe/domain/user/dto/request/SignUpRequest.java`:
- Around line 24-29: The SignUpRequest DTO's Gender gender field lacks null
validation which can violate the DB nullable=false constraint; add a validation
annotation (e.g., `@NotNull`) to the gender field in SignUpRequest and provide a
clear message (e.g., "성별을 입력해주세요"), and ensure the corresponding import for the
validation annotation is added so incoming requests with null gender are
rejected before persisting.

In `@src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java`:
- Around line 96-100: The nickname-comparison can throw NPE and allows null
nicknames despite the DB column being non-nullable; in UserServiceImpl validate
request.nickname() is not null/blank before comparing, throw an appropriate
exception if it is null, and use a null-safe comparison (e.g., Objects.equals or
compare user.getNickname() to request.nickname() only after null-check) prior to
calling userRepository.existsByNickname(request.nickname()) to check duplicates
and potentially throw CustomException(UserErrorCode.DUPLICATE_NICKNAME.

In `@src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java`:
- Around line 29-33: UserAuthInfo currently stores phoneNumber in plaintext and
is serialized by Jackson2JsonRedisSerializer; either remove the phoneNumber
field from the DTO (UserAuthInfo) if it's not required in cache, or implement
encryption: add field-level encryption/decryption by creating a custom Redis
serializer or Jackson (de)serializer for the phoneNumber property and wire it
into RedisCacheConfig (replace Jackson2JsonRedisSerializer for cached
UserAuthInfo or add `@JsonSerialize/`@JsonDeserialize on phoneNumber) so values
are encrypted before persisting and decrypted on read; as an
alternative/complement, ensure Redis server access is hardened (requirepass,
network isolation) in deployment.
🧹 Nitpick comments (3)
src/main/java/com/be/sportizebe/domain/user/entity/User.java (2)

73-76: updateProfile 메서드에 null 체크 누락

nickname이나 introduce가 null로 전달될 경우, 기존 값이 null로 덮어씌워질 수 있습니다. 선택적 업데이트를 지원하거나, null이 아닌 값만 업데이트하도록 방어적 코드를 추가하는 것이 좋습니다.

♻️ null 체크를 추가한 개선안
 public void updateProfile(String nickname, String introduce) {
-    this.nickname = nickname;
-    this.introduce = introduce;
+    if (nickname != null) {
+        this.nickname = nickname;
+    }
+    if (introduce != null) {
+        this.introduce = introduce;
+    }
 }

58-59: 주석 처리된 코드 정리 필요

주석 처리된 address 필드가 남아 있습니다. 향후 구현 예정이라면 TODO 주석과 함께 이슈 번호를 기록하거나, 필요 없다면 삭제하는 것이 좋습니다.

src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java (1)

104-105: 캐시 무효화 타이밍 관련 고려사항

트랜잭션 커밋 전에 캐시를 무효화하면, 롤백 시 캐시는 비어있지만 DB에는 이전 데이터가 남아있는 불일치 상태가 발생할 수 있습니다. 현재 구현에서는 큰 문제가 아니지만, @CacheEvict 어노테이션을 사용하거나 TransactionSynchronizationManager를 활용하여 커밋 후 무효화하는 방식을 고려해볼 수 있습니다.

Comment on lines +24 to +29
@Schema(description = "성별", example = "MALE")
Gender gender,

@Schema(description = "전화번호", example = "010-xxxx-xxxx")
@NotBlank(message = "전화번호를 입력해주세요")
String phoneNumber
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

성별 필드에 null 검증이 필요합니다.

gender가 null이면 DB의 nullable=false 제약과 충돌해 런타임 예외가 발생할 수 있습니다. 입력 단계에서 차단해 주세요.

✅ 수정 제안
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
@@
     `@Schema`(description = "성별", example = "MALE")
+    `@NotNull`(message = "성별을 선택해주세요")
     Gender gender,
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/user/dto/request/SignUpRequest.java`
around lines 24 - 29, The SignUpRequest DTO's Gender gender field lacks null
validation which can violate the DB nullable=false constraint; add a validation
annotation (e.g., `@NotNull`) to the gender field in SignUpRequest and provide a
clear message (e.g., "성별을 입력해주세요"), and ensure the corresponding import for the
validation annotation is added so incoming requests with null gender are
rejected before persisting.

Comment on lines +96 to +100
// 닉네임 중복 체크 (자신의 닉네임이 아닌 경우만)
if (!user.getNickname().equals(request.nickname())
&& userRepository.existsByNickname(request.nickname())) {
throw new CustomException(UserErrorCode.DUPLICATE_NICKNAME);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

닉네임 비교 시 NPE 발생 가능성

request.nickname()이 null인 경우 user.getNickname().equals(request.nickname())에서 문제가 발생하지 않지만, 순서를 바꾸면 request.nickname()이 null일 때 NPE가 발생합니다. 또한 현재 로직은 null 닉네임을 허용하는데, 닉네임이 @Column(nullable = false)로 정의되어 있으므로 null 검증이 필요합니다.

🛡️ null 검증 추가 제안
+    // 닉네임 필수 검증
+    if (request.nickname() == null || request.nickname().isBlank()) {
+        throw new CustomException(UserErrorCode.INVALID_NICKNAME);
+    }
+
     // 닉네임 중복 체크 (자신의 닉네임이 아닌 경우만)
     if (!user.getNickname().equals(request.nickname())
         && userRepository.existsByNickname(request.nickname())) {
         throw new CustomException(UserErrorCode.DUPLICATE_NICKNAME);
     }
🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/domain/user/service/UserServiceImpl.java`
around lines 96 - 100, The nickname-comparison can throw NPE and allows null
nicknames despite the DB column being non-nullable; in UserServiceImpl validate
request.nickname() is not null/blank before comparing, throw an appropriate
exception if it is null, and use a null-safe comparison (e.g., Objects.equals or
compare user.getNickname() to request.nickname() only after null-check) prior to
calling userRepository.existsByNickname(request.nickname()) to check duplicates
and potentially throw CustomException(UserErrorCode.DUPLICATE_NICKNAME.

Comment on lines +29 to +33
private String profileImage;
private String introduce;
private Gender gender;
private String phoneNumber;
private List<SportType> interestType;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the UserAuthInfo.java file and examine it
fd -name "UserAuthInfo.java" -type f

Repository: Sportize/be

Length of output: 285


🏁 Script executed:

# Find cache and redis configuration files
fd -e yml -e yaml -e properties -type f | head -20

Repository: Sportize/be

Length of output: 225


🏁 Script executed:

# Search for CacheConfig, RedisConfig, or cache-related configuration classes
rg -l 'CacheConfig|RedisConfig|@EnableCaching|@Configuration.*Cache' --type java

Repository: Sportize/be

Length of output: 197


🏁 Script executed:

# Search for how UserAuthInfo is used in caching context
rg 'UserAuthInfo' --type java -B 2 -A 2 | head -50

Repository: Sportize/be

Length of output: 5291


🏁 Script executed:

# Find SportType class to verify if it's Serializable
fd -name "SportType.java" -type f

Repository: Sportize/be

Length of output: 285


🏁 Script executed:

# Find and read UserAuthInfo.java
fd "UserAuthInfo.java"

Repository: Sportize/be

Length of output: 121


🏁 Script executed:

# Find RedisCacheConfig.java and read it
cat src/main/java/com/be/sportizebe/global/config/RedisCacheConfig.java

Repository: Sportize/be

Length of output: 6172


🏁 Script executed:

# Find UserCacheService.java and read it
cat src/main/java/com/be/sportizebe/global/cache/service/UserCacheService.java

Repository: Sportize/be

Length of output: 1339


🏁 Script executed:

# Find SportType.java
fd "SportType.java"

Repository: Sportize/be

Length of output: 120


🏁 Script executed:

# Search for application configuration files
fd "\.(yml|yaml|properties)$" | head -10

Repository: Sportize/be

Length of output: 153


🏁 Script executed:

# Search for any encryption or security configuration related to cache/redis
rg -i "serializer|encryption|password|ssl|secure" --type java | grep -i "redis\|cache"

Repository: Sportize/be

Length of output: 4104


🏁 Script executed:

# Read UserAuthInfo.java
cat src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java

Repository: Sportize/be

Length of output: 1501


🏁 Script executed:

# Read SportType.java
cat src/main/java/com/be/sportizebe/domain/user/entity/SportType.java

Repository: Sportize/be

Length of output: 154


🏁 Script executed:

# Check application configuration files for Redis security settings
cat docker/docker-compose.yml | grep -A 20 redis

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Search for any Spring Security or encryption configuration
rg "CommandLineEventPublisher|sslEnabled|requirepass|masterauth" --type java

Repository: Sportize/be

Length of output: 37


🏁 Script executed:

# Check for any mention of encryption in cache/redis context
rg -i "encrypt|cipher|secret.*redis|redis.*secret" --type java

Repository: Sportize/be

Length of output: 37


phoneNumber PII 캐싱 시 암호화 전략 필요

phoneNumber는 민감정보로 현재 Redis에 평문(JSON)으로 저장되고 있습니다. RedisCacheConfig에서 Jackson2JsonRedisSerializer를 사용하면서 암호화가 적용되지 않고 있습니다. 다음 중 하나의 조치가 필요합니다:

  1. UserAuthInfo에서 phoneNumber 필드 제거 (필수 아닌 경우)
  2. Redis 저장소에 대한 암호화 전략 구현 (예: 필드 레벨 암호화)
  3. Redis 접근 제어 강화 (requirepass 설정, 네트워크 격리)

현재 TTL은 5분으로 적절히 설정되어 있습니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/be/sportizebe/global/cache/dto/UserAuthInfo.java` around
lines 29 - 33, UserAuthInfo currently stores phoneNumber in plaintext and is
serialized by Jackson2JsonRedisSerializer; either remove the phoneNumber field
from the DTO (UserAuthInfo) if it's not required in cache, or implement
encryption: add field-level encryption/decryption by creating a custom Redis
serializer or Jackson (de)serializer for the phoneNumber property and wire it
into RedisCacheConfig (replace Jackson2JsonRedisSerializer for cached
UserAuthInfo or add `@JsonSerialize/`@JsonDeserialize on phoneNumber) so values
are encrypted before persisting and decrypted on read; as an
alternative/complement, ensure Redis server access is hardened (requirepass,
network isolation) in deployment.

@imjuyongp imjuyongp merged commit 3b47c29 into develop Feb 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨Feat: 사용자 정보 수정 기능 추가 및 @CacheEvict 적용시켜주기

2 participants