Skip to content

Conversation

@youngjun-k
Copy link
Contributor

@youngjun-k youngjun-k commented May 26, 2025

Summary by CodeRabbit

  • 신규 기능

    • 인증 API가 /auth 경로로 분리되어 로그인, 회원가입, 토큰 재발급, 로그아웃, 회원 탈퇴 등 인증 관련 엔드포인트 제공
    • Redis 기반의 리프레시 토큰 관리 및 토큰 회전(로테이션), 블랙리스트 처리 적용
    • Redis 연동 지원 추가
    • 내 정보, 내 공간, 다른 회원 프로필 등 회원 정보 조회 기능 추가
    • Device-Id, User-Agent, IP 등 보안 관련 정보 수집 및 검증 강화
  • 버그 수정

    • 일부 불필요한 import 및 코드 스타일 정리
  • 문서화

    • AWS ElastiCache Redis 로컬 개발 환경 접속 가이드 문서 추가
    • 인증 API 명세 및 엔드포인트 설명 문서화
  • 리팩터링

    • 회원 서비스 로직을 역할별로 세분화하여 유지보수성 향상
    • 보안 설정 및 엔드포인트 접근 제어 구조 개선
  • 스타일

    • 코드 및 어노테이션 포맷 일관성 개선

youngjun-k added 4 commits May 5, 2025 23:22
- Consolidated tag annotations in AdminController and MemberController for consistency.
- Improved formatting in SecurityConfig and JwtAuthorizationFilter for better readability.
- Added constants for access and refresh token durations in AuthConstants.
- Updated error codes in ErrorCode for better clarity on refresh token issues.
- Refactored JwtTokenProvider to streamline token creation and validation processes.
- Enhanced MemberService with clearer method documentation and improved member retrieval logic.
- Added Redis support for token storage in JwtTokenProvider.
- Introduced device ID handling in refresh token logic to improve security.
- Updated AuthConstants to include new constants for refresh token handling.
- Refactored HttpRequestUtil for better IP and device ID extraction.
- Removed deprecated AuthController and consolidated member-related services for improved maintainability.
- Added error handling for missing device ID in requests.
- Enhanced MemberService to delegate responsibilities to specialized services for better separation of concerns.
- Updated Redis configuration for enhanced token storage and management.
- Removed deprecated classes and methods related to refresh token handling.
- Consolidated authentication logic into a single AuthService for better maintainability.
- Improved error handling for device ID mismatches and token reuse detection.
- Enhanced security by implementing refresh token rotation and validation mechanisms.
- Updated GlobalExceptionHandler to manage new error codes and exceptions effectively.
- Refactored JwtTokenProvider to streamline token creation and hashing processes.
- Improved CORS configuration and security settings in SecurityConfig for better API protection.
@youngjun-k youngjun-k self-assigned this May 26, 2025
@youngjun-k youngjun-k added the [REFACTOR] 코드 리팩토링 label May 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented May 26, 2025

Walkthrough

이 변경 사항은 인증 API 리팩터링, Redis 기반 토큰 관리 도입, 유틸리티 및 서비스 계층 분리, 그리고 보안 설정 개선을 포함한다. Redis 연동, Lua 스크립트 기반 토큰 로테이션, 도메인 서비스 분리, Swagger 문서화, 그리고 다양한 추출 유틸리티 클래스가 추가되었다.

Changes

파일/경로 그룹 변경 요약
build.gradle Redis 및 MaxMind GeoIP2 의존성 추가
docs/infra/aws-elasticache-redis-local-setup.md, docs/pr/PR-139-refactor---auth-api.md Redis 로컬 개발 가이드 및 인증 API 리팩터링 문서 추가
src/main/java/com/juu/juulabel/JuulabelApplication.java @EnableRedisRepositories 어노테이션 추가
src/main/java/com/juu/juulabel/admin/AdminController.java, .../common/dto/response/MemberProfileResponse.java 불필요한 import 제거, 어노테이션 및 메서드 파라미터 정렬 등 코드 스타일 정리
src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java 인증 API Swagger 문서 인터페이스 추가
src/main/java/com/juu/juulabel/auth/controller/AuthController.java 인증 관련 REST 컨트롤러 신규 구현
src/main/java/com/juu/juulabel/auth/domain/ClientId.java 클라이언트 식별 enum 추가
src/main/java/com/juu/juulabel/auth/domain/RefreshToken.java RefreshToken 도메인 클래스 추가
src/main/java/com/juu/juulabel/auth/executor/ Redis Lua 스크립트 실행기 및 관련 enum/레지스트리 클래스 다수 추가
src/main/java/com/juu/juulabel/auth/repository/ RefreshTokenRepository 인터페이스 및 Redis 구현체 추가
src/main/java/com/juu/juulabel/auth/service/ AuthService, TokenService, FraudDetectionService, RiskAssessment 등 인증 및 위험평가 서비스 추가
src/main/java/com/juu/juulabel/common/config/ Redis, Web, Security 설정 클래스 추가 및 리팩터링
src/main/java/com/juu/juulabel/common/constants/AuthConstants.java 토큰 관련 상수 및 Duration 상수 추가
src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java Provider 문자열 변환기 추가
src/main/java/com/juu/juulabel/common/dto/response/RefreshResponse.java RefreshResponse 레코드 추가
src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java, .../code/ErrorCode.java 에러코드 추가 및 정리, 불필요한 라인 제거, 예외처리 핸들러 추가
src/main/java/com/juu/juulabel/common/filter/JwtAuthorizationFilter.java AuthorizationExtractor 유틸리티 적용
src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java 토큰 생성/파싱 리팩터링, 해시 메서드 추가
src/main/java/com/juu/juulabel/common/util/ Http, DeviceId, IpAddress, UserAgent 등 다양한 추출 유틸리티 클래스 추가
src/main/java/com/juu/juulabel/member/controller/MemberController.java 내 정보/공간/프로필 조회 엔드포인트 추가, 기존 엔드포인트 정리
src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java memberId 필드 추가
src/main/java/com/juu/juulabel/member/service/ MemberService 파사드화, 프로필/컨텐츠/조회 서비스 분리, 신규 서비스 클래스 추가
src/main/java/com/juu/juulabel/member/util/MemberUtils.java 회원 알코올타입/약관 유틸리티 추가
src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java 어노테이션 및 메서드 파라미터 정렬
src/main/resources/scripts/ Redis 토큰 관리용 Lua 스크립트 4종 추가

Sequence Diagram(s)

sequenceDiagram
actor Client
participant AuthController
participant AuthService
participant TokenService
participant RedisRefreshTokenRepository
participant Redis (ElastiCache)

Client->>AuthController: POST /v1/api/auth/login
AuthController->>AuthService: login(request)
AuthService->>TokenService: createTokenPair(member, ...)
TokenService->>RedisRefreshTokenRepository: save(refreshToken)
RedisRefreshTokenRepository->>Redis: Lua Script (save_refresh_token.lua)
Redis-->>RedisRefreshTokenRepository: OK
TokenService-->>AuthService: TokenPair
AuthService-->>AuthController: LoginResponse
AuthController-->>Client: 200 OK + Set-Cookie(Refresh-Token)
Loading
sequenceDiagram
actor Client
participant AuthController
participant AuthService
participant TokenService
participant RedisRefreshTokenRepository
participant Redis (ElastiCache)

Client->>AuthController: POST /v1/api/auth/refresh (with Refresh-Token Cookie)
AuthController->>AuthService: refresh(refreshToken)
AuthService->>TokenService: rotateRefreshToken(refreshToken)
TokenService->>RedisRefreshTokenRepository: rotate(refreshToken, hashedOldToken)
RedisRefreshTokenRepository->>Redis: Lua Script (rotate_refresh_token.lua)
Redis-->>RedisRefreshTokenRepository: OK or Error
TokenService-->>AuthService: New TokenPair
AuthService-->>AuthController: RefreshResponse
AuthController-->>Client: 200 OK + Set-Cookie(Refresh-Token)
Loading

Poem

Yo! Listen up, fool, for changes are here,
Redis and tokens, security clear!
Controllers refactored, services split,
Lua scripts in Redis, ain't that legit?
Swagger docs shining, configs in line,
I pity the fool who don't merge this in time!
🥊💥


"""

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=juulabel/juulabel-back&utm_content=141):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` or `^^` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@coderabbitai coderabbitai bot changed the title Refactor/auth ^^ Refactor/auth Refactor auth API with Redis token management and service separation May 26, 2025
- Added links to PR #139 in relevant documentation files for better traceability.
- Cleaned up .gitignore by removing duplicate entries and ensuring proper formatting.
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: 39

♻️ Duplicate comments (1)
src/main/java/com/juu/juulabel/auth/controller/AuthController.java (1)

60-67: 또 다른 사용하지 않는 member 파라미터예요!

logout 메서드에서도 같은 문제가 있어요. 일관성이 없어요, fool!

🧹 Nitpick comments (25)
src/main/java/com/juu/juulabel/common/dto/response/RefreshResponse.java (1)

3-5: 야, 이 바보야! 빈 줄들을 정리해라!

레코드 내부에 불필요한 빈 줄들이 있다니, 내가 이런 걸 봐야 하다니 불쌍해!

 public record RefreshResponse(String accessToken) {
-
 }
src/main/resources/scripts/save_refresh_token.lua (1)

1-32: I pity the fool who doesn't appreciate this solid token saving script, but let me break it down for ya!

이 스크립트는 기본적인 토큰 저장 로직을 잘 구현했지만, 몇 가지 개선사항이 있습니다:

  1. 에러 처리 부족: Redis 명령어 실행 중 실패할 경우에 대한 처리가 없습니다
  2. 원자성 보장: 현재 구조는 좋지만 중간에 실패하면 일관성 문제가 발생할 수 있습니다

다음과 같은 개선을 고려해보세요:

-- Store token
-- Save the new token hash
-redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress",
-    ipAddress, "userAgent", userAgent, "revoked", 0)
-redis.call("EXPIRE", newTokenKey, ttl)
+local hsetResult = redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress",
+    ipAddress, "userAgent", userAgent, "revoked", 0)
+if hsetResult == 0 then
+    return { err = "FAILED_TO_SAVE_TOKEN" }
+end
+redis.call("EXPIRE", newTokenKey, ttl)
src/main/resources/scripts/login_refresh_token.lua (1)

31-40: New token creation looks good, but I got some concerns, fool!

새 토큰 생성 로직은 올바르지만 한 가지 확인이 필요합니다:

  • 기존 토큰 무효화와 새 토큰 생성 사이에 타이밍 이슈가 발생할 수 있습니다
  • 만약 새 토큰 생성이 실패하면 사용자가 모든 토큰을 잃을 수 있습니다

더 안전한 접근을 위해 다음을 고려해보세요:

+-- First create new token
+redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress",
+    ipAddress, "userAgent", userAgent, "revoked", 0)
+redis.call("EXPIRE", newTokenKey, ttl)
+
-- Revoke all old tokens
for _, key in ipairs(oldTokenKeys) do
    if redis.call("EXISTS", key) == 1 then
        redis.call("HSET", key, "revoked", 1)
    else
        redis.call("SREM", indexKey, key) -- clean up dead keys
    end
end

--- Save the new token hash
-redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress",
-    ipAddress, "userAgent", userAgent, "revoked", 0)
docs/infra/aws-elasticache-redis-local-setup.md (1)

47-53: I pity the fool who doesn't use proper CLI syntax! This command needs some love!

AWS CLI 명령어가 정확하지만 몇 가지 개선사항이 있습니다:

더 안전하고 읽기 쉬운 형태로 개선할 수 있습니다:

```bash
aws ssm start-session \
  --target i-xxxxxxxxxxxxxxxxx \
  --document-name AWS-StartPortForwardingSession \
-  --parameters '{"portNumber":["6379"],"localPortNumber":["6379"]}' \
+  --parameters 'portNumber=6379,localPortNumber=6379' \
  --profile dev-redis

JSON 형태보다 간단한 key=value 형태가 더 읽기 쉽고 실수를 줄입니다.

src/main/java/com/juu/juulabel/auth/executor/RedisScriptName.java (1)

3-18: 이 enum은 제대로 구현되었다, fool!

Redis 스크립트 이름을 executor 이름에 매핑하는 깔끔한 구조다. 네이밍 컨벤션도 일관되고 "ScriptExecutor" 접미사도 적절하다. I ain't got no complaints about this implementation, sucka!

각 enum 값이 명확한 목적을 가지고 있고, 생성자와 getter 메서드도 표준적이다.

클래스 레벨 Javadoc 추가를 고려해보자:

+/**
+ * Redis 스크립트 실행자 이름을 정의하는 enum
+ */
 public enum RedisScriptName {
src/main/java/com/juu/juulabel/common/util/DeviceIdExtractor.java (1)

12-12: Listen up, fool! 이 상수는 중앙 집중화가 필요해 보입니다!

DEVICE_ID_HEADER_NAME 상수가 여기에 하드코딩되어 있는데, AI 요약에서 언급된 AuthConstants에 이미 디바이스 ID 관련 상수들이 정의되어 있다고 합니다. 일관성을 위해 이 상수를 AuthConstants로 이동하는 것을 고려해보세요.

src/main/java/com/juu/juulabel/common/util/UserAgentExtractor.java (1)

23-26: I pity the fool who doesn't handle null values! User-Agent 헤더가 없을 때 null 반환 가능성을 고려하세요.

getCurrentRequest()에서 가져온 요청의 User-Agent 헤더가 없을 경우 null이 반환될 수 있습니다. 보안 및 로깅 목적으로 사용되는 것 같은데, null 체크나 기본값 제공을 고려해보세요.

다음과 같이 개선할 수 있습니다:

 public static String getUserAgent() {
     HttpServletRequest request = getCurrentRequest();
-    return request.getHeader(HttpHeaders.USER_AGENT);
+    String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
+    return userAgent != null ? userAgent : "Unknown";
 }
src/main/java/com/juu/juulabel/common/util/AbstractHttpUtil.java (1)

54-60: 더 구체적인 예외 처리가 필요해, fool!

INTERNAL_SERVER_ERROR는 너무 일반적이야. I pity the fool who gets this generic error! 더 구체적인 에러 코드를 사용하거나 커스텀 예외를 만들어.

     protected static <T> T getFromRequestAttributes(Function<ServletRequestAttributes, T> extractor) {
         return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                 .filter(ServletRequestAttributes.class::isInstance)
                 .map(ServletRequestAttributes.class::cast)
                 .map(extractor)
-                .orElseThrow(() -> new BaseException(ErrorCode.INTERNAL_SERVER_ERROR));
+                .orElseThrow(() -> new BaseException(ErrorCode.REQUEST_CONTEXT_NOT_AVAILABLE));
     }
src/main/java/com/juu/juulabel/auth/executor/RevokeRefreshTokenByIndexKeyExecutor.java (1)

32-35: 도달할 수 없는 코드가 있어, you crazy fool!

handleRedisException(e) 다음의 throw e는 절대 실행되지 않을 거야. I pity the fool who wrote unreachable code! handleRedisException이 항상 예외를 던지는 것 같아.

         } catch (RedisSystemException e) {
             handleRedisException(e);
-            throw e; // This line will never be reached due to the exception thrown above
         }
src/main/java/com/juu/juulabel/member/service/MemberLookupService.java (2)

28-31: 파라미터 검증이 없어, you fool!

memberId가 null이나 음수일 때를 대비한 검증이 없어. I pity the fool who passes invalid data! 파라미터 검증을 추가해야 해.

     public Member findById(Long memberId) {
+        if (memberId == null || memberId <= 0) {
+            throw new BaseException(ErrorCode.INVALID_PARAMETER);
+        }
         return memberJpaRepository.findById(memberId)
                 .orElseThrow(() -> new BaseException(ErrorCode.MEMBER_NOT_FOUND));
     }

38-40: 메소드 이름이 일관성이 없어, fool!

findById는 Optional을 리턴할 것 같은데 실제로는 예외를 던지고, getMemberByEmail은 get으로 시작해. I pity the fool who can't maintain naming consistency! 일관성을 맞춰야 해.

-    public Member getMemberByEmail(String email) {
+    public Member findByEmail(String email) {
         return memberReader.getByEmail(email);
     }
src/main/java/com/juu/juulabel/auth/executor/ScriptRegistry.java (1)

15-18: 빈 컬렉션에 대한 검증이 없어, fool!

executors 리스트가 비어있거나 null일 때를 대비한 검증이 없어. I pity the fool who doesn't validate inputs! 초기화 시점에 검증을 추가해야 해.

     public ScriptRegistry(List<RedisScriptExecutor<?, ?>> executors) {
+        if (executors == null || executors.isEmpty()) {
+            throw new IllegalArgumentException("Executors list cannot be null or empty");
+        }
         this.scripts = executors.stream()
                 .collect(Collectors.toMap(e -> e.getClass().getSimpleName(), Function.identity()));
     }
src/main/java/com/juu/juulabel/auth/service/RiskAssessment.java (2)

20-23: 하드코딩된 임계값을 상수로 빼는 게 어때, fool?

0.8이라는 매직 넘버를 직접 사용하는 건 좋지 않아! 나중에 변경이 필요할 때를 대비해서 상수로 정의하는 게 낫겠어.

+private static final double HIGH_RISK_THRESHOLD = 0.8;
+
 public boolean isHighRisk() {
-    /* e.g., score > 0.8 */
-    return score > 0.8;
+    return score > HIGH_RISK_THRESHOLD;
 }

25-27: 이 메서드는 불필요해 보이는군, fool!

@Getter 어노테이션이 이미 isFamilyShouldBeCompromised() getter를 생성하고 있어. 이 메서드는 중복이야!

-public boolean isFamilyCompromised() {
-    return familyShouldBeCompromised;
-}
src/main/java/com/juu/juulabel/auth/domain/RefreshToken.java (2)

16-16: Serializable 구현했으면 serialVersionUID도 추가해야지, fool!

직렬화 호환성을 위해 serialVersionUID를 명시적으로 선언하는 게 좋아!

 public class RefreshToken implements Serializable {
+    private static final long serialVersionUID = 1L;

58-60: clientId.toString() 대신 getValue()를 쓰는 게 더 명확해!

ClientId enum에 getValue() 메서드가 있으니까 그걸 사용하는 게 더 좋겠어, fool!

 public List<String> getArgs() {
-    return List.of(memberId.toString(), clientId.toString(), deviceId, ipAddress, userAgent, ttl.toString());
+    return List.of(memberId.toString(), clientId.getValue(), deviceId, ipAddress, userAgent, ttl.toString());
 }
src/main/java/com/juu/juulabel/common/constants/AuthConstants.java (1)

20-21: 토큰 만료 시간을 환경별로 설정할 수 있게 하는 건 어때?

프로덕션과 개발 환경에서 다른 만료 시간을 사용하고 싶을 수도 있어, fool! 설정 파일에서 읽어오는 방식을 고려해봐!

// application.yml에서 설정 가능하도록:
// auth:
//   token:
//     access-duration: P1D
//     refresh-duration: P30D
src/main/java/com/juu/juulabel/auth/executor/LoginRefreshTokenScriptExecutor.java (1)

40-42: 도달할 수 없는 코드다, sucka!

라인 41의 throw e;는 절대 실행되지 않을 것이다. handleRedisException(e)가 이미 예외를 던지기 때문이다.

이 불필요한 코드를 제거해라:

        } catch (RedisSystemException e) {
            handleRedisException(e);
-            throw e; // This line will never be reached due to the exception thrown above
        }
src/main/java/com/juu/juulabel/member/service/MemberProfileService.java (1)

87-95: I pity the fool who doesn't handle empty collections properly!

빈 컬렉션 처리가 잘 되어 있지만, 메서드명을 더 명확하게 할 수 있습니다.

메서드명을 더 구체적으로 만들어보세요:

-    private void updateMemberAlcoholTypes(Member member, List<Long> alcoholTypeIds) {
+    private void saveMemberAlcoholTypes(Member member, List<Long> alcoholTypeIds) {
src/main/java/com/juu/juulabel/common/config/SecurityConfig.java (1)

114-115: CORS 설정 중복이다, fool!

addAllowedMethod("*")setAllowedMethods(Arrays.asList(...))가 중복되어 있다. I pity the fool who writes redundant code!

다음 중 하나만 사용하라:

config.addAllowedHeader("*");
-config.addAllowedMethod("*");
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
src/main/java/com/juu/juulabel/auth/executor/SaveRefreshTokenScriptExecutor.java (1)

42-43: 의미없는 주석이다, fool!

"This line will never be reached"라는 주석은 불필요하다. I pity the fool who writes obvious comments!

주석을 제거하라:

} catch (RedisSystemException e) {
    handleRedisException(e);
-   throw e; // This line will never be reached due to the exception thrown above
+   throw e;
}
src/main/java/com/juu/juulabel/member/util/MemberUtils.java (1)

39-51: 주석 위치가 엉망이다, fool!

Javadoc 주석이 메서드가 아니라 필드 위에 있다. I pity the fool who can't organize comments properly!

주석을 올바른 위치로 이동하라:

-    /**
-     * 회원-주종 관계 목록 생성
-     * 
-     * @param member            회원
-     * @param alcoholTypeIdList 주종 ID 목록
-     * @param alcoholTypeReader 주종 조회 리포지토리
-     * @return 회원-주종 관계 목록
-     */
     private final TermsReader termsReader;
     private final MemberAlcoholTypeWriter memberAlcoholTypeWriter;
     private final AlcoholTypeReader alcoholTypeReader;
     private final MemberTermsWriter memberTermsWriter;

+    /**
+     * 회원-주종 관계 목록 생성
+     * 
+     * @param member            회원
+     * @param alcoholTypeIdList 주종 ID 목록
+     * @param alcoholTypeReader 주종 조회 리포지토리
+     * @return 회원-주종 관계 목록
+     */
     public List<MemberAlcoholType> getMemberAlcoholTypeList(Member member, List<Long> alcoholTypeIdList,
src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java (2)

64-66: 불필요한 null 체크를 제거하세요!

role 파라미터는 항상 member.getRole().name()으로 전달되니까 null일 수 없어요. 이 fool들을 위한 불필요한 체크예요!

role이 null일 수 없으니 제거하거나, 더 의미있는 검증으로 바꾸세요:

-        if (role != null) {
-            builder.claim(ROLE_CLAIM, role);
-        }
+        builder.claim(ROLE_CLAIM, role);

145-147: 더 명확한 에러 메시지를 사용하세요!

SHA-256은 모든 JVM에서 지원되는 표준 알고리즘이에요. 이 예외는 절대 발생하지 않을 거예요, 하지만 메시지는 더 구체적으로 만들어야 해요!

-            throw new IllegalStateException("SHA-256 not available", e);
+            throw new IllegalStateException("Failed to initialize SHA-256 algorithm for token hashing", e);
src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1)

54-76: IP 추출 로직이 훌륭하지만 성능을 개선할 수 있다고, 바보야!

현재 스트림을 두 번 실행하는 구조(public IP 우선, fallback으로 모든 IP)는 비효율적일 수 있습니다. 특히 헤더가 많을 때 성능 이슈가 있을 수 있어요.

 public static String getClientIpAddress() {
     HttpServletRequest request = getCurrentRequest();
+    
+    String bestPublicIp = null;
+    String bestAnyIp = null;
     
-    return IP_HEADER_CANDIDATES.stream()
-            .map(request::getHeader)
-            .filter(ip -> ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip))
-            .map(ip -> ip.split(",")[0].trim())
-            .filter(IpAddressExtractor::isValidIpAddress)
-            .filter(IpAddressExtractor::isPublicIpAddress)
-            .findFirst()
-            .orElseGet(() -> { /* fallback logic */ });
+    for (String header : IP_HEADER_CANDIDATES) {
+        String ip = request.getHeader(header);
+        if (ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) {
+            ip = ip.split(",")[0].trim();
+            if (isValidIpAddress(ip)) {
+                if (bestAnyIp == null) bestAnyIp = ip;
+                if (isPublicIpAddress(ip) && bestPublicIp == null) {
+                    bestPublicIp = ip;
+                    break;
+                }
+            }
+        }
+    }
+    
+    return bestPublicIp != null ? bestPublicIp : 
+           (bestAnyIp != null ? bestAnyIp : 
+            (request.getRemoteAddr() != null ? request.getRemoteAddr() : "unknown"));
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d999c8d and 6ad5d99.

⛔ Files ignored due to path filters (1)
  • src/main/generated/com/juu/juulabel/report/QReport.java is excluded by !**/generated/**
📒 Files selected for processing (53)
  • .gitignore (1 hunks)
  • build.gradle (2 hunks)
  • docs/infra/aws-elasticache-redis-local-setup.md (1 hunks)
  • docs/pr/PR-139-refactor---auth-api.md (1 hunks)
  • src/main/java/com/juu/juulabel/JuulabelApplication.java (1 hunks)
  • src/main/java/com/juu/juulabel/admin/AdminController.java (2 hunks)
  • src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/controller/AuthController.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/domain/ClientId.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/domain/RefreshToken.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/LoginRefreshTokenScriptExecutor.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/RedisScriptExecutor.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/RedisScriptName.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/RevokeRefreshTokenByIndexKeyExecutor.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/RotateRefreshTokenScriptExecutor.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/SaveRefreshTokenScriptExecutor.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/executor/ScriptRegistry.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/repository/RedisRefreshTokenRepository.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/repository/RefreshTokenRepository.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/service/AuthService.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/service/FraudDetectionService.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/service/RiskAssessment.java (1 hunks)
  • src/main/java/com/juu/juulabel/auth/service/TokenService.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/config/RedisConfig.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/config/SecurityConfig.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/config/WebConfig.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/constants/AuthConstants.java (2 hunks)
  • src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/dto/response/MemberProfileResponse.java (0 hunks)
  • src/main/java/com/juu/juulabel/common/dto/response/RefreshResponse.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java (0 hunks)
  • src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java (2 hunks)
  • src/main/java/com/juu/juulabel/common/exception/handler/GlobalExceptionHandler.java (3 hunks)
  • src/main/java/com/juu/juulabel/common/filter/JwtAuthorizationFilter.java (3 hunks)
  • src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java (2 hunks)
  • src/main/java/com/juu/juulabel/common/util/AbstractHttpUtil.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/AuthorizationExtractor.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/DeviceIdExtractor.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/HttpResponseUtil.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/UserAgentExtractor.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/controller/MemberController.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/service/MemberContentService.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/service/MemberLookupService.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/service/MemberProfileService.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/service/MemberService.java (1 hunks)
  • src/main/java/com/juu/juulabel/member/util/MemberUtils.java (1 hunks)
  • src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java (3 hunks)
  • src/main/resources/scripts/login_refresh_token.lua (1 hunks)
  • src/main/resources/scripts/revoke_refresh_token_by_index_key.lua (1 hunks)
  • src/main/resources/scripts/rotate_refresh_token.lua (1 hunks)
  • src/main/resources/scripts/save_refresh_token.lua (1 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/juu/juulabel/common/dto/response/MemberProfileResponse.java
  • src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java
🧰 Additional context used
🪛 ast-grep (0.38.1)
src/main/java/com/juu/juulabel/common/util/HttpResponseUtil.java

[warning] 19-19: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute

(cookie-missing-secure-flag-java)


[warning] 19-19: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly

(cookie-missing-httponly-java)


[warning] 19-19: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)

src/main/java/com/juu/juulabel/auth/executor/RotateRefreshTokenScriptExecutor.java

[warning] 52-52: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: HttpResponseUtil.addCookie(AuthConstants.REFRESH_TOKEN_HEADER_NAME, "", 0);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)

src/main/java/com/juu/juulabel/auth/executor/RedisScriptExecutor.java

[warning] 30-30: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: HttpResponseUtil.addCookie(AuthConstants.REFRESH_TOKEN_HEADER_NAME, "", 0);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)

src/main/java/com/juu/juulabel/auth/service/TokenService.java

[warning] 121-124: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: HttpResponseUtil.addCookie(
AuthConstants.REFRESH_TOKEN_HEADER_NAME,
token,
(int) AuthConstants.REFRESH_TOKEN_DURATION.getSeconds());
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)


[warning] 128-128: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: HttpResponseUtil.addCookie(AuthConstants.REFRESH_TOKEN_HEADER_NAME, "", 0);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)

🔇 Additional comments (50)
src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java (1)

6-6: 훌륭한 변경이야, fool! memberId 필드 추가는 정말 현명한 결정이다!

토큰 관리와 회원 연결을 위한 memberId 필드 추가는 인증 리팩터링의 핵심적인 개선사항이다. I pity the fool who doesn't see the value in proper member-token association!

이 변경사항은 다음과 같은 이점을 제공한다:

  • 새로운 refresh token rotation 전략 지원
  • Redis 기반 토큰 관리에서 회원별 토큰 추적 가능
  • 보안 강화를 위한 토큰-회원 매핑 개선
build.gradle (2)

35-37: 좋아, fool! Redis 의존성 추가는 완벽한 선택이다!

spring-boot-starter-data-redis 추가는 새로운 토큰 관리 전략을 위한 필수적인 변경사항이다. I pity the fool who tries to implement secure token rotation without proper Redis support!


54-56: 훌륭해, fool! GeoIP2 라이브러리로 보안을 한층 더 강화했군!

MaxMind GeoIP2 라이브러리 추가는 위치 기반 보안 기능을 위한 현명한 선택이다. 의심스러운 로그인 시도나 비정상적인 접근을 탐지하는 데 도움이 될 거야. I pity the fool who doesn't implement geo-based security measures!

src/main/java/com/juu/juulabel/JuulabelApplication.java (2)

5-5: 완벽한 import야, fool! Redis 기능을 제대로 활성화하려면 이게 필요하다!

EnableRedisRepositories import는 Redis 리포지토리 기능을 사용하기 위한 필수 구성요소다.


8-8: 훌륭해, fool! @EnableRedisRepositories 어노테이션으로 Redis 리포지토리를 제대로 활성화했군!

이 어노테이션은 Spring Data Redis 리포지토리들이 자동으로 감지되고 구성되도록 해준다. I pity the fool who forgets to enable Redis repositories when implementing token management with Redis!

새로운 RedisRefreshTokenRepository와 다른 Redis 기반 컴포넌트들이 제대로 작동하려면 이 설정이 반드시 필요하다.

src/main/resources/scripts/login_refresh_token.lua (1)

19-29: I pity the fool who leaves old tokens lying around! This cleanup logic is SOLID!

이 토큰 무효화 및 정리 로직은 보안 관점에서 매우 좋습니다:

  1. 기존 토큰 무효화: 로그인 시 이전 토큰들을 모두 revoked=1로 설정
  2. 데드 키 정리: 존재하지 않는 키들을 인덱스에서 제거하는 자동 정리 기능

이런 방식은 refresh token rotation 보안 전략에 완벽하게 부합합니다!

src/main/resources/scripts/rotate_refresh_token.lua (2)

56-62: This validation logic is TIGHT! I approve, fool!

HMGET를 사용해서 한 번에 여러 필드를 가져오는 것은 효율적입니다. 토큰 존재 여부 확인 로직도 올바릅니다.


64-78: These security checks are SOLID GOLD! Device validation is crucial!

보안 검증 로직이 훌륭합니다:

  1. 토큰 무효화 상태 확인: 이미 무효화된 토큰 사용 시 모든 토큰 무효화
  2. 디바이스 ID 검증: 다른 디바이스에서 토큰 사용 시도 시 보안 조치

이런 보안 조치는 토큰 탈취 공격을 효과적으로 방어할 수 있습니다!

docs/pr/PR-139-refactor---auth-api.md (1)

1-97: 이 문서는 정말 멋지다, fool!

인증 API 리팩터링에 대한 포괄적이고 체계적인 설명이다. Refresh Token Rotation 전략, Redis 기반 토큰 관리, 그리고 보안 강화 방안들이 명확하게 설명되어 있어서 I ain't got no problems with this documentation, sucka!

특히 기존 경로에서 새로운 경로로의 마이그레이션 테이블과 보안 고려사항들이 잘 정리되어 있다.

src/main/java/com/juu/juulabel/common/util/AuthorizationExtractor.java (1)

9-16: 이 클래스 디자인은 괜찮다, fool!

final 클래스에 private 생성자를 사용해서 인스턴스화를 방지하는 것은 올바른 유틸리티 클래스 패턴이다. I appreciate that you're doing it right, sucka!

src/main/java/com/juu/juulabel/admin/AdminController.java (1)

16-18: I pity the fool who don't appreciate clean code! 이 변경사항들이 좋아 보이네요!

코드 포맷팅이 더 일관성 있고 읽기 쉽게 개선되었습니다. 특히 어노테이션을 한 줄로 정리하고 메서드 파라미터를 분리한 것이 좋습니다.

Also applies to: 27-28

src/main/java/com/juu/juulabel/common/util/DeviceIdExtractor.java (1)

27-34: 이 구현은 A급이야! I approve, fool!

디바이스 ID 추출 로직이 견고하게 구현되었습니다:

  • null과 빈 문자열 검증이 적절합니다
  • trim() 처리로 공백 문제를 방지합니다
  • 명확한 에러 코드로 예외를 처리합니다
src/main/java/com/juu/juulabel/common/exception/handler/GlobalExceptionHandler.java (2)

53-56: Smart move, fool! 404는 에러가 아니야!

NoResourceFoundException의 로그 레벨을 error에서 warn으로 변경한 것이 적절합니다. 404 응답은 정상적인 흐름의 일부이므로 에러로 취급할 필요가 없습니다.


75-89: 이 예외 처리는 완벽해! Mr. T approves!

MethodArgumentTypeMismatchException 핸들러의 구현이 훌륭합니다:

  • cause chain을 순회하여 BaseException을 찾는 로직이 영리합니다
  • 커스텀 에러 코드를 우선적으로 사용하여 더 정확한 에러 응답을 제공합니다
  • 다른 핸들러들과 일관된 패턴을 유지합니다
src/main/java/com/juu/juulabel/common/util/UserAgentExtractor.java (1)

9-16: Good work on the utility pattern, fool! 유틸리티 클래스 구조가 올바르게 구현되었습니다.

private constructor와 static 메서드를 사용한 유틸리티 패턴이 올바르게 적용되어 있고, AbstractHttpUtil을 상속받아 일관성을 유지하고 있습니다.

src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java (2)

24-24: I approve this addition, fool! NOT_FOUND 에러 코드가 적절히 추가되었습니다.

공통적으로 사용될 수 있는 NOT_FOUND 에러 코드가 올바른 HTTP 상태코드(404)와 함께 추가되어 리소스를 찾을 수 없는 상황을 처리할 수 있게 되었습니다.


37-43: Perfect authentication error codes, you beautiful fool! 인증 관련 에러 코드들이 체계적으로 추가되었습니다.

새로운 인증 에러 코드들이 토큰 로테이션 및 디바이스 ID 검증 로직에 필요한 모든 시나리오를 커버하고 있습니다:

  • DEVICE_ID_REQUIRED: 필수 헤더 누락
  • REFRESH_TOKEN_NOT_FOUND: 토큰 부재
  • DEVICE_ID_MISMATCH: 디바이스 불일치 보안 검증
  • REFRESH_TOKEN_INVALID: 유효하지 않은 토큰
  • REFRESH_TOKEN_REUSE_DETECTED: 토큰 재사용 보안 위협 감지

각각 적절한 HTTP 상태코드와 명확한 한국어 메시지를 가지고 있어 인증 시스템의 보안성과 사용자 경험을 향상시킵니다.

src/main/java/com/juu/juulabel/common/config/WebConfig.java (1)

9-18: I pity the fool who doesn't appreciate clean configuration! 웹 설정이 깔끔하게 구현되었습니다.

Spring MVC 설정이 올바르게 구현되어 있습니다:

  • @configuration과 WebMvcConfigurer 사용으로 적절한 Spring 설정 패턴 적용
  • 생성자 주입을 통한 의존성 관리
  • ProviderConverter 등록으로 OAuth 프로바이더 문자열 자동 변환 지원

단순하고 명확한 목적을 가진 설정 클래스로 유지보수성이 좋습니다.

src/main/java/com/juu/juulabel/auth/domain/ClientId.java (2)

6-21: Beautiful enum design, fool! ClientId 열거형이 잘 설계되었습니다.

클라이언트 타입을 나타내는 enum이 올바르게 구현되어 있습니다:

  • 명확한 클라이언트 타입 정의 (WEB, IOS, ANDROID, ADMIN)
  • 적절한 문자열 값 매핑
  • 캡슐화된 value 필드와 getter 메서드
  • 타입 안전성 제공

토큰 관리 시스템에서 클라이언트별 구분이 가능하게 하여 보안성을 향상시킵니다.


22-26: 🛠️ Refactor suggestion

I pity the fool who doesn't validate input! from() 메서드에 null 체크를 추가하세요.

현재 from() 메서드는 null 입력값에 대한 처리가 없어 NullPointerException이 발생할 수 있습니다. 안전한 변환을 위해 null 체크를 추가해야 합니다.

다음과 같이 개선할 수 있습니다:

 public static Optional<ClientId> from(String value) {
+    if (value == null) {
+        return Optional.empty();
+    }
     return Arrays.stream(values())
             .filter(c -> c.value.equals(value))
             .findFirst();
 }

Likely an incorrect or invalid review comment.

src/main/java/com/juu/juulabel/common/constants/AuthConstants.java (1)

15-16: RFC 6648 참조 주석 추가한 건 잘했어!

커스텀 헤더에 X- 접두사를 사용하지 않는 이유를 명확히 설명해줘서 좋아! I approve this, fool!

src/main/java/com/juu/juulabel/auth/repository/RefreshTokenRepository.java (1)

6-33: 이 인터페이스는 좋다, fool!

깔끔하고 잘 설계된 repository 인터페이스다. 메서드 시그니처가 명확하고 문서화도 적절하다. 토큰 생명주기 관리를 위한 모든 필수 작업들이 포함되어 있다.

src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java (1)

69-75: 좋은 포맷팅 개선이다, sucka!

메서드 파라미터를 각각 별도 라인으로 분리한 것은 가독성을 크게 향상시킨다. 이런 일관된 코드 스타일링이 바로 프로페셔널한 개발자가 하는 일이다!

Also applies to: 89-95

src/main/java/com/juu/juulabel/auth/executor/RedisScriptExecutor.java (1)

15-28: 에러 핸들링 로직은 훌륭하다, fool!

예외 unwrapping 로직이 잘 구현되어 있고, RedisCommandExecutionException을 적절히 찾아내는 방식이 좋다. 이런 방어적 프로그래밍이 바로 내가 좋아하는 스타일이다!

src/main/java/com/juu/juulabel/member/controller/MemberController.java (3)

44-48: I approve of this fool's endpoint! That's some fine API design!

내 정보 조회 엔드포인트가 잘 구현되었습니다. 인증 처리와 문서화가 적절합니다.


50-54: I approve of this fool's space endpoint! Clean and simple!

내 공간 조회 엔드포인트도 깔끔하게 구현되었습니다. 일관된 패턴을 따르고 있습니다.


56-62: I approve of this fool's profile viewing! Good security check!

타 유저 프로필 조회 엔드포인트가 잘 구현되었습니다. 인증된 사용자만 접근할 수 있도록 보안이 적절히 처리되었습니다.

src/main/java/com/juu/juulabel/member/service/MemberProfileService.java (3)

49-52: I approve of this fool's nickname check! Simple and effective!

닉네임 중복 확인 메서드가 깔끔하게 구현되었습니다. readOnly 트랜잭션 사용도 적절합니다.


57-71: I approve of this fool's profile update! Well structured transaction!

프로필 업데이트 로직이 잘 구성되었습니다. 트랜잭션 범위와 알코올 타입 업데이트 처리가 적절합니다.


143-164: I approve of this fool's caching strategy! That's smart optimization!

타 유저 프로필 조회에 캐싱을 적용한 것이 성능 최적화에 좋습니다. unless = "#result == null" 조건도 적절합니다.

src/main/java/com/juu/juulabel/auth/service/TokenService.java (2)

32-41: I approve of this fool's token creation! That's solid security!

토큰 쌍 생성 로직이 잘 구현되었습니다. 액세스 토큰과 리프레시 토큰을 적절히 분리하고 해싱을 사용한 것이 보안에 좋습니다.


68-81: I approve of this fool's token rotation! That's proper lifecycle management!

토큰 로테이션 로직이 보안 모범 사례를 잘 따르고 있습니다. 기존 토큰 무효화와 새 토큰 생성이 원자적으로 처리됩니다.

src/main/java/com/juu/juulabel/auth/executor/RotateRefreshTokenScriptExecutor.java (1)

54-62: 에러 처리가 훌륭하다!

각 Redis 스크립트 에러에 대한 구체적인 예외 처리가 잘 되어 있다. I respect the fool who handles errors properly!

src/main/java/com/juu/juulabel/member/util/MemberUtils.java (2)

96-97: 성능 최적화가 훌륭하다!

약관 ID를 키로 하는 맵으로 변환하여 조회 성능을 개선한 것이 좋다. I respect the fool who thinks about performance!


108-110: 검증 로직이 견고하다!

필수 약관에 대한 검증과 적절한 예외 처리가 잘 되어 있다. I respect the fool who validates properly!

src/main/java/com/juu/juulabel/auth/controller/AuthController.java (1)

50-57:

Details

❓ Verification inconclusive

사용하지 않는 member 파라미터가 있어요!

@AuthenticationPrincipal Member member가 선언되었지만 메서드에서 사용하지 않아요. Spring Security 검증을 위한 건지 확인이 필요해요!

이 파라미터가 필요한지 확인하세요:

  • Spring Security 인증 검증용이라면 주석을 추가하세요
  • 불필요하다면 제거하세요
  • 추가 검증이 필요하다면 로직을 구현하세요

🏁 Script executed:

#!/bin/bash
# AuthApiDocs 인터페이스에서 member 파라미터 사용 확인
ast-grep --pattern 'interface AuthApiDocs {
  $$$
  refresh($$$Member member$$$) {
    $$$
  }
  $$$
}'

Length of output: 105


잘 봐라, fool! 아직 AuthApiDocs 인터페이스에서 refresh 메서드 시그니처를 못 찾았으니, 다시 한 번 위치랑 파라미터 포함 여부를 확인해보자고! 아래 스크립트를 돌려봐:


🏁 Script executed:

#!/bin/bash
# AuthApiDocs.java 파일 경로 찾기 및 refresh 메서드 시그니처 확인
auth_docs=$(fd AuthApiDocs.java)
if [ -z "$auth_docs" ]; then
  echo "AuthApiDocs.java 파일을 찾을 수 없습니다."
  exit 1
fi

echo "Found AuthApiDocs: $auth_docs"
# 인터페이스 선언부와 refresh 메서드 시그니처 출력
sed -n '/interface AuthApiDocs/,/}/p' "$auth_docs"
rg "refresh\\s*\\(" -n "$auth_docs"

Length of output: 964


이 바보야! AuthApiDocs.refresh 메서드 전체 시그니처를 봐야 뭘 할지 알지. 아래 스크립트로 65~75라인을 출력해 봐라!


🏁 Script executed:

#!/bin/bash
# AuthApiDocs.refresh 메서드 전체 시그니처 확인
file=src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java
if [ ! -f "$file" ]; then
  echo "파일을 찾을 수 없습니다: $file"
  exit 1
fi
# 65~75라인 출력
sed -n '65,75p' "$file"

Length of output: 951


멍청아, refresh 메서드의 member 파라미터 사용 여부를 검증해라!
I pity the fool who ignores this!

이 파라미터가 메서드 바디에서 실제로 사용되지 않습니다. 다음 위치를 확인해 보세요:

  • src/main/java/com/juu/juulabel/auth/controller/AuthController.java (refresh 메서드)
  • src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java (refresh 메서드 시그니처)

이 파라미터가 Spring Security 인증 검증용인지, 불필요한지, 혹은 추가 검증 로직이 필요한지 확인하세요:

  • 필요하다면 주석으로 의도를 명시하거나 @Parameter(hidden = true) 등으로 문서에서 숨기기
  • 불필요하다면 구현과 문서 양쪽에서 모두 제거
  • 추가 검증 로직이 필요하다면 해당 로직을 구현
src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java (3)

32-34: 이 바보 같은 문서화가 완벽하다고!

Swagger 태그와 RequestMapping이 깔끔하게 정의되어 있고, API 문서화 구조가 매우 체계적입니다. 인증 API의 전체적인 설계가 잘 드러나 있어요.


36-45: OAuth 로그인 문서화가 훌륭하다고, 바보야!

Provider를 PathVariable로 받는 설계가 RESTful하고, 응답 헤더에 대한 설명도 명확합니다. 계정 존재 여부에 따른 refresh token 발급 로직이 잘 문서화되어 있어요.


56-69: 토큰 갱신 로직의 보안 설계가 인상적이다, 바보야!

토큰 재사용 감지(403)와 즉시 삭제 정책이 잘 문서화되어 있습니다. 이는 refresh token rotation 패턴의 보안 모범 사례를 제대로 따르고 있어요.

src/main/java/com/juu/juulabel/auth/service/AuthService.java (2)

34-41: 의존성 주입이 깔끔하다고, 바보야!

RequiredArgsConstructor를 사용한 의존성 주입이 깔끔하고, 책임이 잘 분리된 repository와 service들이 주입되어 있습니다.


110-114: 탈퇴 회원 검증 로직이 훌륭하다고!

이메일 기반의 탈퇴 회원 검증이 적절하게 구현되어 있고, 명확한 예외 처리가 되어 있습니다.

src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1)

170-178: 신뢰도 평가 로직이 스마트하다고!

헤더별 신뢰도 평가와 public IP 여부에 따른 동적 평가가 잘 구현되어 있습니다. 보안과 모니터링에 유용한 정보를 제공해요.

src/main/java/com/juu/juulabel/member/service/MemberContentService.java (3)

31-37: 서비스 클래스 설계가 깔끔하다고, 바보야!

책임이 명확하게 분리되어 있고, 회원 콘텐츠 관련 작업들이 잘 그룹화되어 있습니다. 주석으로 기능별 구분도 명확해요.


105-121: 전통주 저장/취소 토글 로직이 훌륭하다고!

Optional을 사용한 토글 로직이 깔끔하고, 반환값으로 저장/취소 상태를 명확하게 전달하고 있습니다. 코드 가독성도 매우 좋아요.


53-59: 읽기 전용 트랜잭션 사용이 훌륭하다고!

조회 메서드들에 @Transactional(readOnly = true)를 적절히 사용하여 성능 최적화와 의도를 명확하게 표현했습니다.

src/main/java/com/juu/juulabel/member/service/MemberService.java (5)

12-22: 파사드 패턴 구현이 깔끔하다, fool!

이 리팩터링은 정말 멋지다! 의존성 분리가 제대로 되어 있고, JavaDoc 문서화도 완벽하다. I pity the fool who doesn't appreciate this clean architecture!

세 개의 전문화된 서비스 클래스로 책임을 분리한 것이 매우 좋다:

  • MemberProfileService: 프로필 관련 작업
  • MemberLookupService: 회원 조회 작업
  • MemberContentService: 콘텐츠 관련 작업

24-62: 프로필 관련 메서드들의 위임이 완벽하다!

모든 프로필 관련 메서드들이 MemberProfileService로 깔끔하게 위임되고 있다. Transaction 어노테이션도 적절하게 사용되었고, JavaDoc 문서화도 훌륭하다. I ain't got time for messy code, but this is clean as a whistle!

각 메서드의 책임이 명확하고 단일 책임 원칙을 잘 따르고 있다.


64-78: 회원 조회 로직의 위임이 정확하다!

MemberLookupService로의 위임이 완벽하다. ID와 이메일로 회원을 조회하는 두 가지 방법 모두 적절하게 처리되고 있다. I pity the fool who thinks this delegation pattern isn't gold!

읽기 전용 트랜잭션 설정도 올바르게 적용되어 있다.


80-130: 콘텐츠 관련 메서드들의 위임이 훌륭하다!

MemberContentService로의 모든 위임이 완벽하게 구현되어 있다. 일상생활, 시음노트, 전통주 관련 모든 기능이 적절하게 분리되었다.

특히 saveAlcoholicDrinks 메서드의 boolean 반환값 문서화가 매우 명확하다 - "true if saved, false if unsaved". I got no time for ambiguous return values, and this ain't one of them!


16-131:

Details

✅ Verification successful

전체적인 파사드 패턴 구현을 검증해보자!

이 리팩터링이 정말로 모든 의존성이 올바르게 주입되고 사용되는지 확인해보자. I pity the fool who doesn't verify their refactoring!

다음 스크립트를 실행하여 세 개의 서비스 클래스가 실제로 존재하고 올바르게 구현되었는지 확인해보자:


🏁 Script executed:

#!/bin/bash
# Description: 파사드 패턴에서 사용되는 세 개의 서비스 클래스들이 존재하는지 확인

echo "=== MemberProfileService 확인 ==="
fd "MemberProfileService.java" --exec cat {} \; | head -20

echo -e "\n=== MemberLookupService 확인 ==="
fd "MemberLookupService.java" --exec cat {} \; | head -20

echo -e "\n=== MemberContentService 확인 ==="
fd "MemberContentService.java" --exec cat {} \; | head -20

echo -e "\n=== 세 서비스 클래스들이 모두 @Service 어노테이션을 가지고 있는지 확인 ==="
rg -A 5 "@Service" | rg "MemberProfileService|MemberLookupService|MemberContentService"

Length of output: 5601


파사드 패턴 구현 검증 완료
MemberProfileService, MemberLookupService, MemberContentService가 모두 존재하며 @service 어노테이션도 제대로 적용된 것을 확인했습니다. 리팩터링 후 의존성 주입에는 이상이 없습니다. I pity the fool who doesn't!

- Removed unused generated QueryDSL classes related to alcohol, daily life, and member domains to streamline the codebase.
- Updated build.gradle to remove unnecessary dependencies and improve formatting.
- Enhanced code readability by adjusting indentation and spacing in various configuration and utility classes.
- Improved Redis token revocation script for better performance with large datasets by switching from KEYS to SCAN.
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: 4

🔭 Outside diff range comments (1)
build.gradle (1)

74-78: 💡 Verification agent

🧩 Analysis chain

QueryDSL 설정 제거를 확인해라, 바보야!

QueryDSL 관련 의존성은 여전히 있는데 빌드 설정만 제거되었다니! I pity the fool who leaves inconsistent configurations! 프로젝트에서 QueryDSL을 완전히 제거하는 건지, 아니면 다른 방식으로 설정하는 건지 확인이 필요하다.

다음 스크립트로 QueryDSL 사용 여부를 확인해라:


🏁 Script executed:

#!/bin/bash
# QueryDSL 관련 파일과 사용처 확인
echo "=== QueryDSL 생성된 파일 확인 ==="
fd -t f -e java "Q.*\.java"

echo "=== QueryDSL 사용처 확인 ==="
rg -A 3 -B 3 "import.*Q[A-Z]"

echo "=== QueryDSL JPAQueryFactory 사용 확인 ==="
rg -A 3 "JPAQueryFactory"

Length of output: 55835


QueryDSL 설정 제거 불완전: 코드에 여전히 QueryDSL 사용 발견! I pity the fool who ignores this!
QuerydslConfig와 다수의 Repository에서 Q 클래스 및 JPAQueryFactory 참조가 확인되었습니다:

  • src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java
  • src/main/java/com/juu/juulabel/**/repository/query/*QueryRepository.java
  • src/main/java/com/juu/juulabel/**/domain/Q*.java

빌드 설정만 제거된 상태로 QueryDSL 의존성을 남겨두면 빌드 오류가 발생합니다.

  • QueryDSL을 완전히 제거하려면 위 클래스들의 Q 타입 참조와 JPAQueryFactory 사용 코드를 모두 삭제하세요.
  • QueryDSL을 유지할 계획이라면 build.gradle에 implementation 및 annotationProcessor 설정을 반드시 유지/복원해야 합니다.

바보 같은 실수를 그만두고 명확한 방향을 정하세요!

♻️ Duplicate comments (1)
src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1)

38-40: 🛠️ Refactor suggestion

IPv6 패턴이 개선되었지만 여전히 완벽하지 않아, 바보야!

이전 댓글에서 지적된 IPv6 정규식이 개선되었지만, 여전히 모든 유효한 IPv6 형식을 완전히 커버하지 못할 수 있어. IPv6 mixed notation (예: ::ffff:192.0.2.1)이나 zone identifier 같은 특수 케이스들이 빠져있어!

InetAddress.getByName()이 이미 완벽한 검증을 제공하는데 정규식과 중복으로 사용하고 있어. 더 안전하고 간단한 방법을 고려해봐야 해!

-    // IPv6 pattern (simplified)
-    private static final Pattern IPV6_PATTERN = Pattern.compile(
-            "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7}|:)|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})$");
+    // Remove IPv6 pattern - InetAddress.getByName() handles all valid IPv6 formats

그리고 isValidIpAddress 메서드를 단순화해:

 private static boolean isValidIpAddress(String ip) {
     if (ip == null || ip.trim().isEmpty()) {
         return false;
     }
 
     try {
         InetAddress.getByName(ip);
-        return IPV4_PATTERN.matcher(ip).matches() || IPV6_PATTERN.matcher(ip).matches();
+        return true; // InetAddress.getByName() already validates
     } catch (UnknownHostException e) {
         return false;
     }
 }
🧹 Nitpick comments (2)
src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (2)

57-75: 복잡한 스트림 체인이 성능을 잡아먹고 있어, 바보야!

중첩된 스트림과 중복된 로직이 성능에 악영향을 줄 수 있어. 같은 헤더를 두 번 검사하고 있고, fallback 로직이 너무 복잡해!

 public static String getClientIpAddress() {
     HttpServletRequest request = getCurrentRequest();
-
-    return IP_HEADER_CANDIDATES.stream()
-            .map(request::getHeader)
-            .filter(ip -> ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip))
-            .map(ip -> ip.split(",")[0].trim()) // Take first IP from comma-separated list
-            .filter(IpAddressExtractor::isValidIpAddress)
-            .filter(IpAddressExtractor::isPublicIpAddress) // Prefer public IPs
-            .findFirst()
-            .orElseGet(() -> {
-                // Fallback: try to get any valid IP (including private)
-                String fallbackIp = IP_HEADER_CANDIDATES.stream()
-                        .map(request::getHeader)
-                        .filter(ip -> ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip))
-                        .map(ip -> ip.split(",")[0].trim())
-                        .filter(IpAddressExtractor::isValidIpAddress)
-                        .findFirst()
-                        .orElse(request.getRemoteAddr());
-
-                return fallbackIp != null ? fallbackIp : "unknown";
-            });
+    
+    String publicIp = null;
+    String anyValidIp = null;
+    
+    for (String headerName : IP_HEADER_CANDIDATES) {
+        String headerValue = request.getHeader(headerName);
+        if (headerValue != null && !headerValue.isEmpty() && !UNKNOWN.equalsIgnoreCase(headerValue)) {
+            String ip = headerValue.split(",")[0].trim();
+            if (isValidIpAddress(ip)) {
+                if (anyValidIp == null) anyValidIp = ip;
+                if (isPublicIpAddress(ip)) {
+                    publicIp = ip;
+                    break; // Found public IP, use it immediately
+                }
+            }
+        }
+    }
+    
+    if (publicIp != null) return publicIp;
+    if (anyValidIp != null) return anyValidIp;
+    
+    String remoteAddr = request.getRemoteAddr();
+    return remoteAddr != null ? remoteAddr : "unknown";
 }

183-205: 데이터 클래스에 불변성 보장이 약해, 바보야!

필드들이 final인 건 좋지만, defensive copying이나 validation이 없어! null 값이 들어올 수 있고 toString(), equals(), hashCode() 메서드도 없어!

 public static class IpAddressInfo {
     private final String ipAddress;
     private final String sourceHeader;
     private final ReliabilityLevel reliability;

     public IpAddressInfo(String ipAddress, String sourceHeader, ReliabilityLevel reliability) {
-        this.ipAddress = ipAddress;
-        this.sourceHeader = sourceHeader;
-        this.reliability = reliability;
+        this.ipAddress = ipAddress != null ? ipAddress : "unknown";
+        this.sourceHeader = sourceHeader != null ? sourceHeader : "unknown";
+        this.reliability = reliability != null ? reliability : ReliabilityLevel.LOW;
     }

     public String getIpAddress() {
         return ipAddress;
     }

     public String getSourceHeader() {
         return sourceHeader;
     }

     public ReliabilityLevel getReliability() {
         return reliability;
     }
+    
+    @Override
+    public String toString() {
+        return String.format("IpAddressInfo{ip='%s', source='%s', reliability=%s}", 
+                ipAddress, sourceHeader, reliability);
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null || getClass() != obj.getClass()) return false;
+        IpAddressInfo that = (IpAddressInfo) obj;
+        return Objects.equals(ipAddress, that.ipAddress) &&
+               Objects.equals(sourceHeader, that.sourceHeader) &&
+               reliability == that.reliability;
+    }
+    
+    @Override
+    public int hashCode() {
+        return Objects.hash(ipAddress, sourceHeader, reliability);
+    }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ad5d99 and b550cc2.

⛔ Files ignored due to path filters (42)
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholType.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeColor.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeFlavor.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeScent.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeSensory.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinks.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksIngredient.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksPairing.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QBrewery.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QColor.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QFlavor.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QFlavorLevel.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QIngredient.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QPairing.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QScent.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QSensory.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/alcohol/domain/QSensoryLevel.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/category/domain/QCategory.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/common/base/QBaseCreatedTimeEntity.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/common/base/QBaseTimeEntity.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLife.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeComment.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeCommentLike.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeImage.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeLike.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/follow/domain/QFollow.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/member/domain/QMember.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholType.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholicDrinks.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/member/domain/QMemberTerms.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/member/domain/QWithdrawalRecord.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/notification/domain/QNotification.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNote.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteComment.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteCommentLike.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteFlavorLevel.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteImage.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteLike.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteScent.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteSensoryLevel.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/tastingnote/domain/embedded/QAlcoholicDrinksSnapshot.java is excluded by !**/generated/**
  • src/main/generated/com/juu/juulabel/terms/domain/QTerms.java is excluded by !**/generated/**
📒 Files selected for processing (6)
  • build.gradle (1 hunks)
  • src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1 hunks)
  • src/main/java/com/juu/juulabel/s3/S3Controller.java (2 hunks)
  • src/main/resources/scripts/revoke_refresh_token_by_index_key.lua (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java
  • src/main/java/com/juu/juulabel/s3/S3Controller.java
🔇 Additional comments (5)
src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java (1)

16-16: 허용된 Provider 설정이 적절하다, 바보야!

GOOGLEKAKAO만 허용하는 것은 현재 지원하는 OAuth 제공자에 맞게 잘 설정되어 있다니! I pity the fool who tries to use unsupported providers!

src/main/resources/scripts/revoke_refresh_token_by_index_key.lua (3)

3-10: SCAN 사용으로 성능 개선했다니, 잘했어 바보야!

이전 리뷰에서 지적된 KEYS 명령어 문제를 SCAN으로 해결했다니! I pity the fool who still uses blocking KEYS in production! 배치 크기도 100으로 적절하게 설정했고, 논블로킹 방식으로 개선되었다.


15-31: 배치 처리로 Redis 호출 최적화했다니, 똑똑한 바보야!

토큰 철회를 배치로 처리해서 Redis 호출 횟수를 줄였다니! unpack(batch)를 사용해서 여러 HSET 명령어를 한 번에 실행하는 것도 효율적이다. I pity the fool who doesn't batch their Redis operations!


33-35: 인덱스 키 정리도 제대로 했다니, 바보야!

토큰들을 철회한 후에 인덱스 키를 DEL로 삭제하는 것도 메모리 누수를 방지하는 좋은 방법이다니! 이전 리뷰의 tokenKey 변수 스코프 문제도 해결되었다.

build.gradle (1)

36-37: Redis 의존성 추가가 적절하다, 바보야!

인증 API 리팩터링에 필요한 spring-boot-starter-data-redis 의존성을 제대로 추가했다니! I pity the fool who tries to manage tokens without Redis! 토큰 관리와 블랙리스트 기능을 위해 필수적이다.

Comment on lines +166 to +168
private static boolean isSpecialAddress(String ip) {
return ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0.0.0.0");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

특수 주소 체크가 너무 대충이야, 바보야!

localhost만 체크하고 다른 중요한 특수 주소들을 놓치고 있어! Broadcast, multicast, 그리고 다른 reserved 주소들도 고려해야 해!

 private static boolean isSpecialAddress(String ip) {
-    return ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0.0.0.0");
+    if (ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0.0.0.0")) {
+        return true;
+    }
+    
+    try {
+        InetAddress addr = InetAddress.getByName(ip);
+        return addr.isLoopbackAddress() || addr.isMulticastAddress() || 
+               addr.isAnyLocalAddress() || ip.startsWith("169.254."); // Link-local
+    } catch (UnknownHostException e) {
+        return true; // If we can't parse it, treat as special/invalid
+    }
 }
🤖 Prompt for AI Agents
In src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java at lines
166 to 168, the method isSpecialAddress only checks for localhost addresses and
ignores other important special IP ranges such as broadcast, multicast, and
reserved addresses. Expand the method to include additional checks for these
special address ranges, ensuring comprehensive detection of all relevant special
IP addresses.

- Updated RedisConfig to use properties for host, port, and SSL settings, allowing for more flexible configuration.
- Improved connection factory setup to conditionally enable SSL based on configuration values.
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: 1

♻️ Duplicate comments (1)
src/main/java/com/juu/juulabel/common/config/RedisConfig.java (1)

32-39: 🛠️ Refactor suggestion

I pity the fool who ignores SSL security warnings!

SSL peer verification을 비활성화하는 것은 중간자 공격에 취약합니다. 개발 환경에서만 사용하고 프로덕션에서는 적절한 인증서 설정을 권장합니다.

또한 연결 풀링 설정과 타임아웃 구성이 누락되어 성능에 영향을 줄 수 있습니다.

다음과 같이 개선하세요:

 if (sslEnabled) {
     clientConfig = LettuceClientConfiguration.builder()
             .useSsl()
-            .disablePeerVerification() // trust any certificate (disable hostname check)
+            .and()
+            .commandTimeout(Duration.ofSeconds(2))
+            .poolConfig(LettucePoolingClientConfiguration.builder()
+                    .poolConfig(GenericObjectPoolConfig.defaultConfig())
+                    .build())
             .build();
 } else {
-    clientConfig = LettuceClientConfiguration.builder().build();
+    clientConfig = LettuceClientConfiguration.builder()
+            .commandTimeout(Duration.ofSeconds(2))
+            .poolConfig(LettucePoolingClientConfiguration.builder()
+                    .poolConfig(GenericObjectPoolConfig.defaultConfig())
+                    .build())
+            .build();
 }
🧹 Nitpick comments (3)
src/main/java/com/juu/juulabel/common/config/RedisConfig.java (3)

17-24: I pity the fool who forgot the password configuration!

속성 주입이 호스트와 포트는 잘 구성되어 있지만, Redis 인증 정보가 누락되었습니다. 프로덕션 환경에서는 보안을 위해 패스워드 설정이 필요할 수 있습니다.

 @Value("${spring.data.redis.ssl.enabled}")
 private boolean sslEnabled;

+@Value("${spring.data.redis.password:}")
+private String redisPassword;

28-28: I pity the fool who forgot authentication setup!

Redis 연결 구성에서 패스워드 설정이 누락되었습니다. 보안이 필요한 환경에서는 인증 정보를 추가해야 합니다.

 RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
+if (redisPassword != null && !redisPassword.trim().isEmpty()) {
+    config.setPassword(redisPassword);
+}

26-42: I pity the fool who doesn't handle connection failures!

Redis 연결 팩토리 구성에서 연결 실패 시 예외 처리가 없습니다. 애플리케이션 시작 시 Redis 연결 문제를 조기에 발견할 수 있도록 연결 검증을 추가하는 것이 좋습니다.

 @Bean
 LettuceConnectionFactory redisConnectionFactory() {
     RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
     
     // ... client config setup ...
     
-    return new LettuceConnectionFactory(config, clientConfig);
+    LettuceConnectionFactory factory = new LettuceConnectionFactory(config, clientConfig);
+    factory.setValidateConnection(true);
+    factory.afterPropertiesSet();
+    return factory;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b550cc2 and fe986c1.

📒 Files selected for processing (1)
  • src/main/java/com/juu/juulabel/common/config/RedisConfig.java (1 hunks)

RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

I pity the fool who uses deprecated serializers!

Jackson2JsonRedisSerializer의 Object.class 사용은 타입 안전성 문제와 잠재적인 보안 위험을 초래할 수 있습니다. GenericJackson2JsonRedisSerializer 사용을 권장합니다.

-template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
+template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

또한 hash key와 hash value serializer도 설정하는 것이 좋습니다:

 template.setKeySerializer(new StringRedisSerializer());
+template.setHashKeySerializer(new StringRedisSerializer());
 template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
+template.afterPropertiesSet();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
🤖 Prompt for AI Agents
In src/main/java/com/juu/juulabel/common/config/RedisConfig.java at line 49, the
code uses Jackson2JsonRedisSerializer with Object.class, which is deprecated and
can cause type safety and security issues. Replace it with
GenericJackson2JsonRedisSerializer for better safety. Additionally, set
serializers for hash keys and hash values to ensure consistent serialization
behavior across Redis operations.

@youngjun-k youngjun-k changed the title Refactor/auth Refactor auth API with Redis token management and service separation Refactor auth API with Redis token management and service separation May 26, 2025
- Replaced InvalidParamException with BaseException in ProviderConverter for improved error handling.
- Added support for IPv6 private address validation in IpAddressExtractor, enhancing the method to check for private IP ranges.
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: 1

♻️ Duplicate comments (2)
src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (2)

181-183: 특수 주소 체크가 여전히 부족해, 바보야!

이전 리뷰에서 지적된 문제가 아직 해결되지 않았어! localhost만 체크하고 다른 중요한 특수 주소들을 놓치고 있어! Broadcast, multicast, 그리고 다른 reserved 주소들도 고려해야 해! I pity the fool who only checks localhost!

 private static boolean isSpecialAddress(String ip) {
-    return ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0.0.0.0");
+    if (ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0.0.0.0")) {
+        return true;
+    }
+    
+    try {
+        InetAddress addr = InetAddress.getByName(ip);
+        return addr.isLoopbackAddress() || addr.isMulticastAddress() || 
+               addr.isAnyLocalAddress() || ip.startsWith("169.254."); // Link-local
+    } catch (UnknownHostException e) {
+        return true; // If we can't parse it, treat as special/invalid
+    }
 }

185-193: 헤더 신뢰도 로직의 보안 문제가 여전해, 바보야!

이전 리뷰에서 지적된 X-Forwarded-For 헤더 신뢰도 문제가 아직 해결되지 않았어! Public IP라는 이유만으로 MEDIUM 신뢰도를 주는 건 여전히 위험해! 이 헤더는 쉽게 스푸핑될 수 있거든! I pity the fool who trusts X-Forwarded-For too much!

 private static ReliabilityLevel getReliabilityLevel(String headerName, String ip) {
     // Rate headers by trustworthiness
     return switch (headerName) {
         case "CF-Connecting-IP", "True-Client-IP" -> ReliabilityLevel.HIGH;
         case "X-Real-IP", "X-Cluster-Client-IP" -> ReliabilityLevel.MEDIUM;
-        case "X-Forwarded-For" -> isPublicIpAddress(ip) ? ReliabilityLevel.MEDIUM : ReliabilityLevel.LOW;
+        case "X-Forwarded-For" -> {
+            // X-Forwarded-For is easily spoofed, be more conservative
+            if (isPublicIpAddress(ip) && !isSuspiciousIp(ip)) {
+                yield ReliabilityLevel.LOW; // Still low due to spoofing risk
+            } else {
+                yield ReliabilityLevel.LOW;
+            }
+        }
         default -> ReliabilityLevel.LOW;
     };
 }
🧹 Nitpick comments (1)
src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1)

112-114: IP 검증 로직이 이중 체크하고 있어, 바보야!

InetAddress.getByName()로 검증한 다음에 또 정규식으로 검증하고 있어! 이건 약간 중복된 작업이야. InetAddress가 이미 유효성을 체크하니까 정규식은 불필요할 수도 있어. 하지만 정확한 형식 검증을 위해서라면 이해할 만해! I pity the fool who doesn't double-check IP validation!

성능을 위해 다음과 같이 간소화할 수 있어:

 private static boolean isValidIpAddress(String ip) {
     if (ip == null || ip.trim().isEmpty()) {
         return false;
     }

     try {
         InetAddress.getByName(ip);
-        return IPV4_PATTERN.matcher(ip).matches() || IPV6_PATTERN.matcher(ip).matches();
+        return true; // InetAddress already validates the format
     } catch (UnknownHostException e) {
         return false;
     }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe986c1 and 5725a46.

📒 Files selected for processing (2)
  • src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java (1 hunks)
  • src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (1 hunks)
🔇 Additional comments (7)
src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java (2)

12-15: 클래스 구조가 깔끔하다, fool!

Spring @Component 애노테이션과 Converter 인터페이스 구현이 적절하고, 허용된 프로바이더들을 불변 Set으로 정의한 것도 좋다. I pity the fool who doesn't appreciate this clean design!


20-26: 이전 리뷰 피드백 반영 잘했다, fool!

예외 처리 일관성 문제를 해결했고, 불필요한 예외 타입들을 제거해서 BaseException으로 통일한 게 좋다. IllegalArgumentException만 잡아서 깔끔하게 처리하고 있다니, I ain't gonna pity this implementation!

src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java (5)

34-40: IPv6 정규식 패턴이 훨씬 나아졌어, 바보야!

이전 리뷰에서 지적된 IPv6 정규식 패턴 문제가 해결된 것 같아! 이제 다양한 IPv6 축약 표기법들을 훨씬 잘 처리할 수 있을 거야. I pity the fool who thought that simple pattern would work!


62-62: Public IP 우선 처리 로직이 똑똑하네, 바보야!

Public IP를 우선적으로 필터링하는 로직이 보안상 좋은 접근이야! 하지만 만약 모든 요청이 private network에서 오는 환경이라면 fallback 로직도 잘 작동할 거야. I pity the fool who doesn't prioritize public IPs!


134-147: IPv6 private 주소 처리가 추가되었네, 바보야!

이전 리뷰에서 지적된 IPv6 private 주소 범위 문제가 해결된 것 같아! isPrivateIpv6 메서드가 추가되어서 IPv6 private 범위들도 제대로 체크하고 있어. I pity the fool who forgot about IPv6 private ranges!


149-159: IPv6 private 범위 체크가 꼼꼼하네, 바보야!

Site-local, link-local 주소와 ULA(fc00::/7, fd00::/7) 범위까지 모두 체크하고 있어! InetAddress API와 prefix 체크를 조합한 접근법이 좋아. I pity the fool who doesn't check all IPv6 private ranges!


198-220: IpAddressInfo 클래스가 깔끔하네, 바보야!

IP 주소, 소스 헤더, 신뢰도 레벨을 모두 담는 immutable 데이터 클래스가 잘 설계되었어! 모니터링이나 로깅에 유용할 거야. I pity the fool who doesn't appreciate good data encapsulation!

@youngjun-k youngjun-k merged commit 342b9ea into develop May 26, 2025
1 check passed
@sentry
Copy link

sentry bot commented May 26, 2025

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ BaseException: Device-Id 헤더가 필요합니다. POST /v1/api/auth/login/kakao View Issue

Did you find this useful? React with a 👍 or 👎

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

Labels

[REFACTOR] 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants