Skip to content

feat: 이미지 업로드 시 요청 횟수 제한 추가#535

Merged
dh2906 merged 11 commits intodevelopfrom
feat/image-upload-limit
Apr 13, 2026
Merged

feat: 이미지 업로드 시 요청 횟수 제한 추가#535
dh2906 merged 11 commits intodevelopfrom
feat/image-upload-limit

Conversation

@dh2906
Copy link
Copy Markdown
Contributor

@dh2906 dh2906 commented Apr 13, 2026

🔍 개요

  • 동아리 플랫폼에서 악의적인 사용자나 버그로 인한 대량 이미지 업로드로 서버 리소스가 과도하게 소모될 수 있습니다. 이를 방지하기 위해 사용자별 업로드 횟수 제한이 필요했습니다.

🚀 주요 변경 내용

Rate Limiting 기반 구조 추가

  • @RateLimit 어노테이션 추가: 메서드 레벨에서 Rate Limit 적용 가능
  • RateLimitAspect 구현: AOP를 통한 선언적 Rate Limit 처리
    • Lua 스크립트를 사용하여 INCR와 EXPIRE를 원자적으로 실행
    • SETNX와 INCR 사이의 레이스 컨디션 방지 (TTL 손실 문제 해결)
  • RateLimitExceededException 팩토리: 429 응답 생성
  • TOO_MANY_REQUESTS (429) 응답 코드 추가

이미지 업로드 API 적용

  • UploadController.uploadImage()에 Rate Limit 적용
  • 설정: 사용자별 1분에 20회 제한 (#userId로 사용자 식별)

테스트

  • RateLimitAspectTest 단위 테스트 추가
    • 정상 요청 통과 검증
    • Lua 스크립트 실행 검증
    • 제한 초과 예외 발생 검증
    • 기본 키 사용 검증

💬 참고 사항

  • Redis 장애 시 업로드 API가 영향을 받을 수 있음

✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

dh2906 added 8 commits April 13, 2026 11:20
- 이미지 업로드 Rate Limiting 구현을 위한 에러 코드 추가
- HTTP 429 상태 코드와 함께 '요청 횟수가 너무 많습니다' 메시지 정의
- @ratelimit 어노테이션 생성 (maxRequests, timeWindowSeconds, keyExpression 속성)
- RateLimitExceededException 팩토리 클래스 생성
- CustomException.of()를 활용한 예외 생성 패턴 적용
- UploadController.uploadImage()에 @ratelimit 어노테이션 추가
- 사용자별 10분당 50회 업로드 제한 설정
- #userId SpEL 표현식으로 사용자별 카운터 분리
- 제한 내 요청 시 정상 통과 테스트
- SET NX 패턴을 활용한 TTL 설정 검증
- Rate Limit 초과 시 예외 발생 테스트
- SpEL keyExpression 평가 테스트
- StringUtils.hasText() 적용하여 null-safe 체크 개선
- generateKey() 메서드에서 methodKey 중복 생성 제거
- 미사용 RateLimitExceededException.create() 메서드 제거
- paramNames null 체크 불필요성으로 인한 제거
- SETNX + INCR 두 개 명령에서 Lua 스크립트(INCR + EXPIRE)로 변경
- TTL 손실 레이스 컨디션 방지 (윈도우 롤오버 시 영구 락 문제 해결)
- 관련 테스트를 Lua 스크립트 실행 검증 방식으로 업데이트
- 기존: 10분에 50회
- 변경: 1분에 20회
- 정상 사용에 여유를 두면서 비정상적 대량 업로드 방지
@dh2906 dh2906 self-assigned this Apr 13, 2026
@dh2906 dh2906 added the 기능 새로운 기능을 개발합니다. label Apr 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@dh2906 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 17 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 15 minutes and 17 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b24e3460-0f41-4437-a82b-6a6a90e0d637

📥 Commits

Reviewing files that changed from the base of the PR and between 0f4c009 and 7ff3bac.

📒 Files selected for processing (1)
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
📝 Walkthrough

Walkthrough

업로드 엔드포인트에 사용자별 분산 속도 제한을 도입합니다. 새로운 @RateLimit 애노테이션과 AOP 기반 RateLimitAspect가 Redis Lua 스크립트로 원자적 카운팅을 수행하고, 초과 시 TOO_MANY_REQUESTS 응답을 발생시킵니다.

Changes

Cohort / File(s) Summary
Rate limiting core
src/main/java/gg/agit/konect/global/ratelimit/annotation/RateLimit.java, src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java, src/main/java/gg/agit/konect/global/ratelimit/exception/RateLimitExceededException.java
새로운 @RateLimit 애노테이션 추가, AOP RateLimitAspect 도입(SpEL 키 평가, Redis Lua로 원자적 INCR+TTL, 실패 시 fail-open), 제한 초과시 CustomException 생성 팩토리 추가
Endpoint 적용
src/main/java/gg/agit/konect/domain/upload/controller/UploadController.java
uploadImage(...)@RateLimit(maxRequests = 20, timeWindowSeconds = 60, keyExpression = "#userId") 적용
API 응답 코드
src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
HTTP 429 대응 TOO_MANY_REQUESTS enum 상수 추가(한국어 메시지 포함)
테스트 지원 & 테스트 케이스
src/test/java/gg/agit/konect/unit/global/ratelimit/aspect/RateLimitAspectTest.java, src/test/java/gg/agit/konect/support/EmbeddedRedisConfig.java, src/test/java/gg/agit/konect/support/IntegrationTestSupport.java, build.gradle
RateLimitAspect 단위 테스트 추가(허용/초과/키/TTL 검증), 임베디드 Redis 테스트 설정 추가 및 테스트용 Redis 의존성 등록

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as UploadController
    participant Aspect as RateLimitAspect
    participant Redis
    participant Service as UploadService

    Client->>Controller: POST /upload (userId)
    Controller->>Aspect: 메서드 호출 인터셉트 (`@RateLimit`)
    Aspect->>Aspect: SpEL로 키 생성 (예: ratelimit:...:userId)
    Aspect->>Redis: Lua 스크립트 실행 (INCR + EXPIRE)
    Redis-->>Aspect: 현재 카운트 반환

    alt 카운트 ≤ maxRequests
        Aspect->>Service: proceed() 호출 (원본 로직 실행)
        Service-->>Controller: 처리 완료
        Controller-->>Client: 200 OK
    else 카운트 > maxRequests
        Aspect->>Redis: TTL 조회
        Redis-->>Aspect: 남은 시간(초) 반환
        Aspect-->>Client: 429 TOO_MANY_REQUESTS (남은 시간 안내)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Redis에 깡충 뛰어들어 카운트를 세었네,
Lua 한 줄로 원자성 지키며 멈추지 않네.
사용자마다 공평히 몫을 나누고,
초과하면 다정히 429로 알려주네.
토끼가 깡충, 변경을 축하하노라 ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 이미지 업로드 API에 대한 요청 횟수 제한 기능 추가라는 주요 변경사항을 명확하고 간결하게 설명하고 있습니다.
Description check ✅ Passed PR 설명이 Rate Limiting 기반 구조, 이미지 업로드 API 적용, 테스트 추가 등 변경사항을 충분히 상세하게 설명하고 있습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/image-upload-limit

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.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 13, 2026

🧪 JaCoCo Coverage Report (Changed Files)

Summary

  • Overall Coverage: 93.5% ✅
  • Covered Lines: 157 / 168
  • Changed Files: 5

Coverage by File

Class Coverage Lines Status
RateLimitAspect
gg.agit.konect.global.ratelimit.aspect
76.1% 35/46
ApiResponseCode
gg.agit.konect.global.code
100.0% 121/121
UploadController
gg.agit.konect.domain.upload.controller
100.0% 1/1

📊 View Workflow Run

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/gg/agit/konect/domain/upload/controller/UploadController.java`:
- Line 20: The hardcoded RateLimit values in UploadController
(`@RateLimit`(maxRequests = 20, timeWindowSeconds = 60, keyExpression =
"#userId")) must be externalized: add a `@ConfigurationProperties` class (e.g.,
RateLimitProperties with maxRequests and timeWindowSeconds fields), load them
from application.yml/properties, and modify the rate-limiting implementation
(the RateLimit annotation handling class — e.g., RateLimitAspect or
RateLimitInterceptor) to read defaults from RateLimitProperties at runtime; then
update UploadController to remove numeric attributes and use
`@RateLimit`(keyExpression = "#userId") (or a named policy) so the operational
policy can be tuned without redeploying.

In `@src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java`:
- Around line 86-91: The current catch in RateLimitAspect swallows SpEL failures
from parser.parseExpression(keyExpression) and silently falls back to the global
key, invalidating per-user limits; update the catch to log the failed expression
and exception (include keyExpression and methodKey) using the class logger, and
change behavior so that if the rate-limit metadata indicates a sensitive rule
(e.g., a boolean like isSensitive on the annotation/metadata you have in
RateLimitAspect) you fail explicitly by rethrowing or returning an error rather
than returning the global key; for non-sensitive cases keep the existing
fallback but ensure the error is logged with the exception to aid debugging.
- Line 41: The current RateLimitAspect uses `@Around`("@annotation(rateLimit)")
which only intercepts method-level annotations and therefore ignores class-level
RateLimit; update the aspect's pointcut expression in RateLimitAspect (the
`@Around` annotation) to include class-level matches by using "@within(rateLimit)
|| `@annotation`(rateLimit)" so both class- and method-level RateLimit annotations
are honored (alternatively, you may remove ElementType.TYPE from the RateLimit
annotation if you intentionally want only method-level application).
- Around line 49-53: The redis calls in RateLimitAspect
(redisTemplate.execute(...) and getExpire(...)) must be wrapped in explicit
exception handling to avoid propagating Redis connection/timeout errors; inside
the method in RateLimitAspect that computes currentCount and expiry for the
key/timeWindowSeconds, catch Redis connection and runtime exceptions and
implement a clear policy (choose either fail-open: log.warn with context (key,
timeWindowSeconds, exception), emit a metric/tag via your metrics system, and
skip rate-limit checks as if no prior count; or fail-closed: log.error with
context, emit a metric, and convert to a 503 ServiceUnavailable by throwing a
custom exception). Ensure both redisTemplate.execute and getExpire calls are
guarded, use the same policy for both, and include descriptive log messages and
a metric increment so operators can detect Redis failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4b73b009-5577-4129-b2c5-cd11b7bfeba4

📥 Commits

Reviewing files that changed from the base of the PR and between ebe0efc and e41ddc4.

📒 Files selected for processing (6)
  • src/main/java/gg/agit/konect/domain/upload/controller/UploadController.java
  • src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
  • src/main/java/gg/agit/konect/global/ratelimit/annotation/RateLimit.java
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
  • src/main/java/gg/agit/konect/global/ratelimit/exception/RateLimitExceededException.java
  • src/test/java/gg/agit/konect/unit/global/ratelimit/aspect/RateLimitAspectTest.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: coverage
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
  • src/main/java/gg/agit/konect/global/ratelimit/exception/RateLimitExceededException.java
  • src/main/java/gg/agit/konect/domain/upload/controller/UploadController.java
  • src/main/java/gg/agit/konect/global/ratelimit/annotation/RateLimit.java
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/java/gg/agit/konect/global/code/ApiResponseCode.java
  • src/main/java/gg/agit/konect/global/ratelimit/exception/RateLimitExceededException.java
  • src/main/java/gg/agit/konect/domain/upload/controller/UploadController.java
  • src/main/java/gg/agit/konect/global/ratelimit/annotation/RateLimit.java
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
  • src/test/java/gg/agit/konect/unit/global/ratelimit/aspect/RateLimitAspectTest.java

- 클래스 레벨 @ratelimit 지원 (@Within 추가)
- Redis 장애 시 fail-open 정책 적용 (예외 발생 시 rate limit 체크 스킵)
- SpEL 평가 실패 시 에러 로깅 추가

Closes coderabbitai review comments:
- 클래스 레벨 @ratelimit 선언과 실제 AOP 적용 범위 불일치 문제
- Redis 장애 시 레이트리밋 체크 예외 전파 문제
- SpEL 표현식 평가 실패 시 로깅 없이 전역 제한으로 폴백 문제
Copy link
Copy Markdown
Contributor Author

@dh2906 dh2906 left a comment

Choose a reason for hiding this comment

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

코드리뷰 반영 완료:

[2] 클래스 레벨 @ratelimit 지원 - @Within(rateLimit) || @annotation(rateLimit)로 수정
[3] Redis 장애 처리 - try-catch로 예외 처리, fail-open 정책 적용
[4] SpEL 평가 실패 로깅 - log.error()로 keyExpression과 methodKey 기록

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java`:
- Around line 86-92: In RateLimitAspect (around the StandardEvaluationContext
creation and use of signature.getParameterNames()), add a defensive null-check
for the result of signature.getParameterNames(): if it returns null, fall back
to generating stable placeholder names (e.g., "arg0".."argN") or use an empty
array and map variables by index using joinPoint.getArgs(); then proceed to set
variables into the context using those names so the for-loop cannot NPE when
paramNames is null. This change ensures the loop that sets
context.setVariable(...) in RateLimitAspect will be safe in builds compiled
without -parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a8cb239c-b461-409c-8014-a5c26d4b7cbb

📥 Commits

Reviewing files that changed from the base of the PR and between e41ddc4 and f4926ca.

📒 Files selected for processing (4)
  • build.gradle
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
  • src/test/java/gg/agit/konect/support/EmbeddedRedisConfig.java
  • src/test/java/gg/agit/konect/support/IntegrationTestSupport.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: coverage
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • build.gradle
  • src/test/java/gg/agit/konect/support/EmbeddedRedisConfig.java
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
  • src/test/java/gg/agit/konect/support/IntegrationTestSupport.java
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
🪛 ast-grep (0.42.1)
src/test/java/gg/agit/konect/support/EmbeddedRedisConfig.java

[info] 45-45: "Detected use of a Java socket that is not encrypted. As a result, the
traffic could be read by an attacker intercepting the network traffic. Use
an SSLSocket created by 'SSLSocketFactory' or 'SSLServerSocketFactory'
instead."
Context: new ServerSocket(0)
Note: [CWE-319] Cleartext Transmission of Sensitive Information [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures

(unencrypted-socket-java)

🔇 Additional comments (5)
src/test/java/gg/agit/konect/support/EmbeddedRedisConfig.java (2)

45-48: [LEVEL: low] Static analysis 경고는 false positive입니다.

ServerSocket(0)에 대한 "unencrypted socket" 경고는 테스트용 embedded Redis 포트 탐색 목적이므로 무시해도 됩니다. 민감한 데이터 전송이 아닌 단순 포트 할당용입니다.


24-28: [LEVEL: resolved] Spring이 LettuceConnectionFactory 초기화를 자동으로 처리합니다

이 코드는 올바릅니다. LettuceConnectionFactoryInitializingBean을 구현하므로, @Bean 메서드에서 반환될 때 Spring이 자동으로 afterPropertiesSet()을 호출합니다. 명시적 호출은 불필요하며, @PostConstruct startRedis()가 실행된 후 빈이 요청될 때 초기화가 완료되므로 순서도 올바릅니다.

src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java (1)

41-75: [LEVEL: low] 이전 리뷰 피드백이 반영되었습니다.

  • 클래스 레벨 @RateLimit 지원을 위한 @within(rateLimit) 포인트컷 추가 완료
  • Redis 장애 시 fail-open 정책(lines 63-67)으로 API 가용성 보장
  • SpEL 평가 실패 시 log.error() 로깅 추가(lines 99-100)

구조적으로 잘 개선되었습니다.

build.gradle (1)

89-89: [LEVEL: low] embedded-redis 테스트 의존성 추가 확인

com.github.codemonstur:embedded-rediskstyrc/embedded-redis의 유지보수 포크로 적절한 선택입니다. Rate limiting 테스트를 위한 Redis 인프라 구성에 필요합니다.

src/test/java/gg/agit/konect/support/IntegrationTestSupport.java (1)

46-46: [LEVEL: low] 통합 테스트에 EmbeddedRedisConfig 추가 확인

Rate limiting 기능을 위한 Redis 인프라를 통합 테스트 컨텍스트에 올바르게 추가했습니다.

dh2906 added 2 commits April 13, 2026 13:37
- Lua 스크립트 객체를 매 요청마다 생성에서 생성자에서 한 번만 생성하도록 변경
- Redis getExpire 호출을 제한 초과 시에만 실행하도록 지연 로딩 적용
- @requiredargsconstructor 제거 및 커스텀 생성자 정리
- signature.getParameterNames()가 null을 반환하는 경우 방어
- -parameters 컴파일 플래그 없이 빌드된 경우 arg0, arg1... 형태의
  플레이스홀더 이름 생성하여 SpEL 평가 안전하게 수행
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.

♻️ Duplicate comments (2)
src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java (2)

69-75: ⚠️ Potential issue | 🔴 Critical

[LEVEL: high] 제한 초과 경로의 Redis 조회가 fail-open 정책에서 빠져 있습니다.

문제: Line 71의 redisTemplate.getExpire(key)는 예외 처리가 없어, 제한을 넘긴 요청에서 Redis 타임아웃/연결 오류가 나면 500이 그대로 전파됩니다. 영향: 평소에는 정상 처리되다가 임계 사용자 요청만 장애 시점에 업로드 실패로 바뀌어 운영 장애를 더 복잡하게 만듭니다. 제안: TTL 조회도 같은 try/catch로 감싸고, 실패 시 timeWindowSeconds로 대체하거나 앞선 execute()와 동일한 fail-open 정책으로 통일하세요.

최소 수정 예시
         if (currentCount > maxRequests) {
-            Long remainingSeconds = redisTemplate.getExpire(key);
+            Long remainingSeconds;
+            try {
+                remainingSeconds = redisTemplate.getExpire(key);
+            } catch (Exception e) {
+                log.warn("Rate limit TTL lookup failed for key={}. Using default window.", key, e);
+                remainingSeconds = Long.valueOf(timeWindowSeconds);
+            }
             long remaining = remainingSeconds != null && remainingSeconds > 0
                 ? remainingSeconds
                 : timeWindowSeconds;
             throw RateLimitExceededException.withRemainingTime(remaining);
         }

As per coding guidelines, '보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java`
around lines 69 - 75, The TTL lookup on limit-exceeded path uses
redisTemplate.getExpire(key) without error handling, so Redis failures can
surface as 500s for throttled requests; wrap the TTL fetch in the same
try/catch/fail-open pattern used by the earlier execute() call in
RateLimitAspect: catch any Redis exceptions around redisTemplate.getExpire(key)
and on error (or null/invalid TTL) fall back to timeWindowSeconds, then call
RateLimitExceededException.withRemainingTime(remaining) as before; reference the
redisTemplate.getExpire(key) call, the surrounding RateLimitAspect class, and
the RateLimitExceededException.withRemainingTime method when making the change.

98-105: ⚠️ Potential issue | 🟠 Major

[LEVEL: medium] SpEL 실패 시 전역 키로 폴백되어 사용자별 제한이 무너집니다.

문제: Line 102-105는 keyExpression이 설정된 경우에도 평가 실패 시 ratelimit:<method>로 폴백해 모든 사용자가 같은 카운터를 공유하게 만듭니다. 영향: #userId 오타나 파라미터명 변경만으로 한 사용자의 업로드가 전체 사용자 429로 번질 수 있습니다. 제안: 사용자별 제한이 필수인 규칙에서는 이 경로를 설정 오류로 처리해 즉시 실패시키고, 전역 폴백은 명시적으로 허용한 경우에만 사용하세요.

As per coding guidelines, '확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java`
around lines 98 - 105, The current catch in RateLimitAspect swallows SpEL
failures and returns a global key (RATE_LIMIT_KEY_PREFIX + methodKey), breaking
per-user limits; change the behavior so that if a keyExpression was provided and
parsing/evaluation via parser.parseExpression(keyExpression).getValue(context)
throws, you treat it as a configuration/error condition rather than silently
falling back: log the full exception, and rethrow a specific runtime exception
(or return an error response) so the request fails fast; if a global fallback is
explicitly allowed, gate that behavior behind a configurable flag (e.g.,
allowGlobalFallback) checked when methodKey/keyExpression are present so the
current global-key fallback only occurs when the flag is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java`:
- Around line 69-75: The TTL lookup on limit-exceeded path uses
redisTemplate.getExpire(key) without error handling, so Redis failures can
surface as 500s for throttled requests; wrap the TTL fetch in the same
try/catch/fail-open pattern used by the earlier execute() call in
RateLimitAspect: catch any Redis exceptions around redisTemplate.getExpire(key)
and on error (or null/invalid TTL) fall back to timeWindowSeconds, then call
RateLimitExceededException.withRemainingTime(remaining) as before; reference the
redisTemplate.getExpire(key) call, the surrounding RateLimitAspect class, and
the RateLimitExceededException.withRemainingTime method when making the change.
- Around line 98-105: The current catch in RateLimitAspect swallows SpEL
failures and returns a global key (RATE_LIMIT_KEY_PREFIX + methodKey), breaking
per-user limits; change the behavior so that if a keyExpression was provided and
parsing/evaluation via parser.parseExpression(keyExpression).getValue(context)
throws, you treat it as a configuration/error condition rather than silently
falling back: log the full exception, and rethrow a specific runtime exception
(or return an error response) so the request fails fast; if a global fallback is
explicitly allowed, gate that behavior behind a configurable flag (e.g.,
allowGlobalFallback) checked when methodKey/keyExpression are present so the
current global-key fallback only occurs when the flag is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76db3bff-2a44-42e6-9ddd-0b1d13b99a49

📥 Commits

Reviewing files that changed from the base of the PR and between f4926ca and 0f4c009.

📒 Files selected for processing (1)
  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: coverage
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.

  • 코멘트는 반드시 한국어로 작성한다.
  • 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
  • 각 코멘트 첫 줄에 심각도를 [LEVEL: high|medium|low] 형식으로 반드시 표기한다.
  • 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
  • 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
  • 가능하면 재현 조건 및 실패 시나리오도 포함한다.
  • 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
  • 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
  • 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
  • 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
  • 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.

Files:

  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java
**/*

⚙️ CodeRabbit configuration file

**/*: 공통 리뷰 톤 가이드:

  • 모든 코멘트는 첫 줄에 [LEVEL: ...] 태그를 포함한다.
  • 과장된 표현 없이 사실 기반으로 작성한다.
  • 한 코멘트에는 하나의 이슈만 다룬다.
  • 코드 예시가 필요하면 최소 수정 예시를 제시한다.
  • 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.

Files:

  • src/main/java/gg/agit/konect/global/ratelimit/aspect/RateLimitAspect.java

@dh2906 dh2906 merged commit 0ed5604 into develop Apr 13, 2026
5 checks passed
@dh2906 dh2906 deleted the feat/image-upload-limit branch April 13, 2026 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

기능 새로운 기능을 개발합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant