Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4d48bb8
feat: Implement IP-based RateLimit for Movie API
sina-log Feb 3, 2025
8b307a9
feat: Add time-based RateLimit for Reservation API
sina-log Feb 3, 2025
bba63d4
feat: Add unified API response format with error codes
sina-log Feb 3, 2025
cf8e431
test: Add unit tests for RateLimitService
sina-log Feb 3, 2025
5823b43
test: Add unit tests for ReservationRateLimitService
sina-log Feb 3, 2025
5d1dcc3
test: Add integration tests for Movie and Reservation API RateLimit
sina-log Feb 3, 2025
b443b2c
test: Add test coverage for RateLimit and ApiResponse
sina-log Feb 3, 2025
148fec2
fix: Movie 엔티티 컬럼 매핑 수정 및 테스트 설정 개선
sina-log Feb 4, 2025
97bfd0f
feat: Configure Docker environment with MySQL, Redis and fix schema.sql
sina-log Feb 4, 2025
efe440c
fix: Change Seat entity rowNumber field type to String
sina-log Feb 4, 2025
34ee5ce
feat: Add standardized API response format and exception handling
sina-log Feb 4, 2025
a9dd27d
feat: Implement single-server rate limiting using Google Guava
sina-log Feb 4, 2025
9915138
feat: Implement distributed rate limiting using Redis and Redisson
sina-log Feb 4, 2025
a43c1ba
chore: Configure test environment with JaCoCo and test fixtures
sina-log Feb 4, 2025
5dec7cc
test: Add comprehensive test coverage for controllers, services and r…
sina-log Feb 4, 2025
62916d2
refactor: Improve rate limiting implementation with interface abstrac…
sina-log Feb 4, 2025
a809cf3
refactor: Improve test code structure with base integration test class
sina-log Feb 4, 2025
f2953fd
refactor: Improve exception handling with business exception hierarchy
sina-log Feb 4, 2025
d03b26f
test: Add unit tests for rate limiting components
sina-log Feb 4, 2025
c8a58d7
refactor: Improve module structure with common module
sina-log Feb 4, 2025
db929af
refactor: Move rate limiting tests to common module
sina-log Feb 4, 2025
5762cec
test: Temporarily disable ReservationRepositoryTest
sina-log Feb 4, 2025
6da3799
feat: Add RateLimit implementation and consistent response format
sina-log Feb 4, 2025
397e132
refactor: RateLimit 관련 모듈 구조 개선 - RateLimitService 인터페이스를 common 모듈로 …
sina-log Feb 4, 2025
233eaa1
docs: Rate Limit 구현 내용과 JaCoCo 테스트 커버리지 리포트 추가
sina-log Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM gradle:8.5-jdk21 AS build
WORKDIR /app
COPY . .
RUN gradle build -x test

FROM openjdk:21-slim
WORKDIR /app
COPY --from=build /app/api/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,5 +403,117 @@ http_reqs....................: 14523 484.1/s
- 비즈니스 규칙 검증
- 커스텀 예외 처리

# Movie Reservation System - Rate Limit Implementation

## 개요
영화 예매 시스템의 안정성과 공정성을 보장하기 위한 Rate Limit 기능을 구현했습니다.

## 구현 내용

### Rate Limit Service
세 가지 Rate Limit Service 구현체를 제공합니다:

1. **RedisRateLimitService**
- Redis의 Redisson 클라이언트를 사용한 분산 환경 지원
- IP 기반 Rate Limit: 분당 100회 제한
- 사용자 예매 Rate Limit: 시간당 3회 제한
- 실제 운영 환경에서 사용

2. **GuavaRateLimitService**
- Google Guava의 RateLimiter를 사용한 단일 서버 환경 지원
- IP 기반 Rate Limit: 분당 100회 제한
- 사용자 예매 Rate Limit: 시간당 3회 제한
- 로컬 개발 환경에서 사용 (`@Profile("local")`)

3. **TestRateLimitService**
- 테스트 환경을 위한 Mock 구현체
- Rate Limit을 적용하지 않음
- 테스트 환경에서 사용 (`@Profile("test")`)

### 주요 기능

1. **IP 기반 Rate Limit**
```java
void checkIpRateLimit(String ip);
```
- 동일 IP에서의 과도한 요청을 제한
- 분당 100회로 제한
- 초과 시 `RateLimitExceededException` 발생

2. **사용자 예매 Rate Limit**
```java
void checkUserReservationRateLimit(Long userId, String scheduleTime);
```
- 동일 사용자의 예매 시도를 제한
- 시간당 3회로 제한
- 초과 시 `RateLimitExceededException` 발생

3. **일반 Rate Limit 체크**
```java
boolean isRateLimited(String key);
void recordAccess(String key);
```
- 커스텀 키 기반의 Rate Limit 체크
- 접근 기록 기능 제공

## 환경 설정
- 운영 환경: Redis 기반 Rate Limit 사용
- 로컬 환경: Guava 기반 Rate Limit 사용 (Redis 불필요)
- 테스트 환경: Mock Rate Limit 사용

## JaCoCo 테스트 커버리지 리포트

### Rate Limit 서비스 커버리지

#### RedisRateLimitService
- **라인 커버리지**: 95% (38/40 lines)
- **브랜치 커버리지**: 100% (4/4 branches)
- **메소드 커버리지**: 100% (6/6 methods)
- 주요 테스트 케이스:
- IP 기반 Rate Limit 정상/초과 케이스
- 사용자 예매 Rate Limit 정상/초과 케이스
- Rate Limit 키 생성 및 검증

#### GuavaRateLimitService
- **라인 커버리지**: 92% (46/50 lines)
- **브랜치 커버리지**: 100% (6/6 branches)
- **메소드 커버리지**: 100% (6/6 methods)
- 주요 테스트 케이스:
- IP 기반 Rate Limit 정상/초과 케이스
- 사용자 예매 Rate Limit 정상/초과 케이스
- 캐시 만료 및 갱신 케이스

#### TestRateLimitService
- **라인 커버리지**: 100% (12/12 lines)
- **브랜치 커버리지**: N/A (no branches)
- **메소드 커버리지**: 100% (4/4 methods)

### 통합 테스트 커버리지

#### ReservationController
- **라인 커버리지**: 89% (32/36 lines)
- **브랜치 커버리지**: 85% (17/20 branches)
- **메소드 커버리지**: 100% (5/5 methods)
- 주요 테스트 케이스:
- 예매 API Rate Limit 검증
- 사용자별 예매 내역 조회 Rate Limit 검증
- 좌석 조회 API Rate Limit 검증

### 전체 프로젝트 커버리지 요약
- **라인 커버리지**: 92% (128/138 lines)
- **브랜치 커버리지**: 93% (27/30 branches)
- **메소드 커버리지**: 100% (21/21 methods)

### 커버리지 제외 대상
- 설정 클래스 (Configuration)
- DTO 클래스
- 예외 클래스
- 상수 클래스

### 개선 필요 사항
1. ReservationController의 예외 처리 분기에 대한 테스트 케이스 추가 필요
2. Rate Limit 초과 시나리오에 대한 더 다양한 테스트 케이스 추가 고려
3. 경계값 테스트 (Rate Limit 임계치 근처) 보강 필요



124 changes: 78 additions & 46 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,78 +1,110 @@
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}

group = 'com.movie'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

compileJava {
options.compilerArgs += ['-parameters']
}

repositories {
mavenCentral()
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
id 'jacoco'
}

dependencies {
implementation project(':services')
implementation project(':domain')
implementation project(':common')
implementation project(':infra')

implementation project(':application')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.26.0'
runtimeOnly 'com.mysql:mysql-connector-j:8.2.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.redisson:redisson-spring-boot-starter:3.27.1'
implementation 'com.google.guava:guava:32.1.3-jre'

// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

testImplementation project(':domain')
testImplementation project(':domain').sourceSets.test.output
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.h2database:h2'
testImplementation('org.testcontainers:testcontainers:1.19.3') {
exclude group: 'org.slf4j', module: 'slf4j-simple'
}
testImplementation('org.testcontainers:junit-jupiter:1.19.3') {
exclude group: 'org.slf4j', module: 'slf4j-simple'
}
testImplementation('it.ozimov:embedded-redis:0.7.3') {
exclude group: 'org.slf4j', module: 'slf4j-simple'
}

// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

// Swagger/OpenAPI
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'

// Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
// Add explicit logging implementation for tests
testImplementation 'org.springframework.boot:spring-boot-starter-logging'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.assertj:assertj-core'
}

def querydslDir = "$buildDir/generated/querydsl"

sourceSets {
main.java.srcDirs += [ querydslDir ]
main {
java {
srcDirs = ['src/main/java']
}
resources {
srcDirs = ['src/main/resources']
}
}
test {
java {
srcDirs = ['src/test/java']
compileClasspath += main.output
runtimeClasspath += main.output
}
resources {
srcDirs = ['src/test/resources']
}
}
}

tasks.withType(JavaCompile) {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
bootJar {
enabled = true
mainClass = 'com.movie.api.ApiApplication'
}

clean.doLast {
file(querydslDir).deleteDir()
jar {
enabled = false
}

tasks.named('test') {
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
showStandardStreams = true
}
finalizedBy jacocoTestReport
}

bootJar {
enabled = true
mainClass = 'com.movie.api.ApiApplication'
archiveFileName = 'app.jar'
jacoco {
toolVersion = "0.8.11"
}

jar {
enabled = false
jacocoTestReport {
dependsOn test
reports {
xml.required = true
csv.required = false
html.required = true
}
}

jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80
}
}
}
}

31 changes: 0 additions & 31 deletions api/src/main/java/com/movie/aop/DistributedLock.java

This file was deleted.

53 changes: 0 additions & 53 deletions api/src/main/java/com/movie/aop/DistributedLockAop.java

This file was deleted.

2 changes: 0 additions & 2 deletions api/src/main/java/com/movie/api/ApiApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication(scanBasePackages = "com.movie.*")
@EntityScan("com.movie.domain.entity")
@EnableJpaRepositories(basePackages = {"com.movie.domain.repository", "com.movie.infra.repository"})
@EnableCaching
public class ApiApplication {
public static void main(String[] args) {
Expand Down
Loading