✨ Feat: 사용자 정보 수정, 조회 기능 구현 + 사용자 정보 캐시 무효화#46
Conversation
안내Walkthrough사용자 프로필 조회 및 수정 기능을 추가하고, 해당 기능에 캐시 무효화 처리를 적용했습니다. Gender 열거형과 새로운 DTOs를 도입하며, User 엔티티와 UserAuthInfo에 필드를 확장했습니다. Changes
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
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. Comment |
There was a problem hiding this comment.
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를 활용하여 커밋 후 무효화하는 방식을 고려해볼 수 있습니다.
| @Schema(description = "성별", example = "MALE") | ||
| Gender gender, | ||
|
|
||
| @Schema(description = "전화번호", example = "010-xxxx-xxxx") | ||
| @NotBlank(message = "전화번호를 입력해주세요") | ||
| String phoneNumber |
There was a problem hiding this comment.
성별 필드에 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.
| // 닉네임 중복 체크 (자신의 닉네임이 아닌 경우만) | ||
| if (!user.getNickname().equals(request.nickname()) | ||
| && userRepository.existsByNickname(request.nickname())) { | ||
| throw new CustomException(UserErrorCode.DUPLICATE_NICKNAME); | ||
| } |
There was a problem hiding this comment.
닉네임 비교 시 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.
| private String profileImage; | ||
| private String introduce; | ||
| private Gender gender; | ||
| private String phoneNumber; | ||
| private List<SportType> interestType; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the UserAuthInfo.java file and examine it
fd -name "UserAuthInfo.java" -type fRepository: Sportize/be
Length of output: 285
🏁 Script executed:
# Find cache and redis configuration files
fd -e yml -e yaml -e properties -type f | head -20Repository: 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 javaRepository: 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 -50Repository: Sportize/be
Length of output: 5291
🏁 Script executed:
# Find SportType class to verify if it's Serializable
fd -name "SportType.java" -type fRepository: 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.javaRepository: 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.javaRepository: 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 -10Repository: 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.javaRepository: Sportize/be
Length of output: 1501
🏁 Script executed:
# Read SportType.java
cat src/main/java/com/be/sportizebe/domain/user/entity/SportType.javaRepository: Sportize/be
Length of output: 154
🏁 Script executed:
# Check application configuration files for Redis security settings
cat docker/docker-compose.yml | grep -A 20 redisRepository: Sportize/be
Length of output: 37
🏁 Script executed:
# Search for any Spring Security or encryption configuration
rg "CommandLineEventPublisher|sslEnabled|requirepass|masterauth" --type javaRepository: 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 javaRepository: Sportize/be
Length of output: 37
phoneNumber PII 캐싱 시 암호화 전략 필요
phoneNumber는 민감정보로 현재 Redis에 평문(JSON)으로 저장되고 있습니다. RedisCacheConfig에서 Jackson2JsonRedisSerializer를 사용하면서 암호화가 적용되지 않고 있습니다. 다음 중 하나의 조치가 필요합니다:
- UserAuthInfo에서
phoneNumber필드 제거 (필수 아닌 경우) - Redis 저장소에 대한 암호화 전략 구현 (예: 필드 레벨 암호화)
- 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.
#️⃣ Issue Number
📝 요약(Summary)
생성된 파일
profileImage, introduce, gender, phoneNumber, interestType
수정된 파일
API 스펙
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"]
}
}
캐시 처리
🛠️ PR 유형
어떤 변경 사항이 있나요?
📸스크린샷 (선택)
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit
새로운 기능