Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e2b747f
[ feature ] application module 추가/facade 분리 및 api에 컴포넌트 스캔에 추가
futuremaker019 Feb 7, 2025
c1e2f03
[ dependency ] jacoco 의존성 추가 및 설정
futuremaker019 Feb 8, 2025
5f8df24
[ fix ] api/jacoco 테스트의 exclude 처리를 위한 파일위치 변경
futuremaker019 Feb 8, 2025
d34ff0a
[ fix ] api/scheduleControllerTest 실행되지 않는 장애 수정
futuremaker019 Feb 8, 2025
b6e422a
[ fix ] api/불필요한 인터셉터 삭제
futuremaker019 Feb 9, 2025
faea228
[ fix ] api/controller에서 request body missing 장애 처리
futuremaker019 Feb 9, 2025
3476b87
[ fix ] api/예약 관련 인터셉터 장애 처리/redis 결과값 형변환 시, null 처리
futuremaker019 Feb 9, 2025
585d058
[ feature ] .http/reservation.http 추가
futuremaker019 Feb 9, 2025
8707f89
[ test ] domain/예약 기능 테스트 코드 추가
futuremaker019 Feb 9, 2025
c87d4ce
[ test ] domain/Reservation/isAlreadyReserved 메서드 테스트 코드 추가
futuremaker019 Feb 9, 2025
689b9a0
[ test ] domain/ScheduleService/각 스케줄 목록 조회 기능에 대한 테스트 코드 작성
futuremaker019 Feb 9, 2025
556b1aa
[ test ] domain/ReservationService/테스트 코드 클래스명 오타 수정
futuremaker019 Feb 9, 2025
b69f779
[ test ] domain/UserAccountService/테스트 코드 클래스명 오타 수정
futuremaker019 Feb 9, 2025
bc8a291
[ fix ] storage/repository 테스트 코드 작성을 위한 전처리 작업
futuremaker019 Feb 9, 2025
af08f29
[ test ] storage/repository 테스트 코드 작성
futuremaker019 Feb 9, 2025
bb2cd95
[ test ] api/ScheduleController 테스트 코드 추가
futuremaker019 Feb 9, 2025
13d89dd
[ READMD ] 분산락, jacoco 보고서 추가
futuremaker019 Feb 9, 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
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,4 +455,100 @@ create index idx_title_genre_released_at on movie(title, genre, released_at);

</details>

</details>
</details>

<details>
<summary style="font-weight: bold; font-size: 17px;">분산락 적용 및 설정 (AOP를 활용한 분산락, 함수형 분산락)</summary>

### AOP 룰 설정한 분산락 성능 테스트
- 5번의 테스트를 진행하여 평균 약 `920ms`의 성능을 보임

<img src="./docs/images/AOP-distributed-lock.png" width="550">


### 함수형 분산락 성능 테스트
- 5번의 테스트를 진행하여 평균 약 `870ms`의 성능을 보임

<img src="./docs/images/functional-distributed-lock.png" width="550">

> 함수형 분산락의 성능이 대략 `50ms` 의 빠른 성능을 보임

### AOP 분산락과 함수형 기반 분산락의 속도 차이가 발생하는 이유는 무엇일까?

실행흐름의 차이
```
// AOP 분산락
AOP 프록시가 메서드 호출을 가로챔 -> 릭 적용 -> 메서드 실행

// 함수형 기반 분산락
메서드 내부에서 직접 함수형 분산락 실행 -> 락 적용 후 실행
```
AOP 기반은 메서드 호출을 감싸는 프록시가 만들어져 있고, 함수형 기반은 메서드 내부에서 직접 락을 적용하한 후 실행하기 때문에 속도의 차이가 발생한다.

### waitTime, leaseTime

하나의 예약 API 호출시 많게는 900ms 적게는 800ms의 속도를 보였다. 락을 획득하기 위해 대기하는 `waitTime`은 1초로도 충분하지만
5초로 주어 락을 순차적으로 가져갈 수 있도록 하였으며, leaseTime 또한 5초를 주어 획득한 락 내에 충분히 API가 처리될 수 있도록 보장함

###

</details>

<details>
<summary style="font-weight: bold; font-size: 17px;">Jacoco 테스트 결과</summary>

> jacoco 테스트를 진행한 모듈은 movie-api, movie-domain. movie-storage 이다.

### Api

<img src="./docs/images/jacoco-api.png">

- 통합 테스트를 진행하였으며 작성된 테스트 코드 클래스는 아래와 같다.
- ScheduleControllerTest
- ReservationControllerTest

>interfaces 에 위치하는 controller 의 테스트 커버리지를 80% 이상 향상 시키도록 테스트 케이스를 작성했으며, 100%으로 테스트 통과율을 보임

### Domain

<img src="./docs/images/jacoco-domain.png">

- Mock 테스트를 진행하였으며, 테스트를 작성한 클래스는 아래와 같다.
- SeatTest
- 5자리 이상 예약 시도 유효성 테스트
- 연속된 자리 예약 시도 유효성 테스트
- ReservationTest
- 이미 점유된 좌석 확인 테스트
- 점유되지 않은 좌석이라면 예약이 가능한지 테스트
- ScheduleServiceTest
- 영화관 id를 이용한 스케줄 목록조회 테스트
- 영화명, 장르를 이용한 스케줄 목록조회 테스트
- 영화관, 장르를 이용하여, 인메모리 캐시에서 스케줄 목록조회 테스트
- 영화관, 장르를 이용하여, Redis 캐시에서 스케줄 목록조회 테스트
- ReservationServiceTest
- 스케줄 id와 좌석 ids를 이용하여 예약 목록조회 테스트
- 스케줄 id, 좌석 ids, 사용자 id로 좌석 예약 테스트
- UserAccountServiceTest
- token을 이용한 단일 사용자 조회 테스트

> 도메인 또한 80% 이상의 커버리지를 확인함

### Storage 테스트

<img src="./docs/images/jacoco-storage.png">

- 통합 테스트로 진행했으며, 테스트 코드를 작성한 클래스는 아래와 같다.
- ScheduleRepositoryTest
- 영화관 id를 이용한 스케줄 목록조회
- 영화명, 장르를 이용한 스케줄 목록조회
- ReservationRepositoryTest
- 스케줄 id, 좌석 ids를 이용한 예약정보 목록조회
- 스케줄 id, 좌석 ids, 사용자 id를 이용한 좌석 예약
- UserAccountRepositoryTest
- token을 이용한 단일 사용자 조회

> instruction & Branch Coverage 를 50% 이상 보임


</details>

105 changes: 100 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ subprojects {
apply plugin: boot
apply plugin: "io.spring.dependency-management"
apply plugin: "idea"
// apply plugin: "java-test-fixtures" // java-test-fixture 플러그인
// apply plugin: "java-library" // java-test-fixture 플러그인
// apply plugin: "maven-publish" // java-test-fixture 플러그인
apply plugin: "jacoco" // Jacoco 플러그인 추가

java {
toolchain {
Expand Down Expand Up @@ -52,15 +56,106 @@ subprojects {
testImplementation "$boot:spring-boot-starter-test"
}

tasks.named("bootJar") {
mainClass = 'com.movie.movieapi.MovieApiApplication'
}

tasks.named('test') {
useJUnitPlatform()
}

// test {
// exclude '**/*'
// }
// jacoco 설정
jacoco {
toolVersion = "0.8.12" // JaCoCo 버전 설정
}
test {
exclude '**/com/movie/storage/facade/**', '**/com/movie/movieapi/config/interceptor/**'
jacoco {
enabled = true
excludes = [
'**/com/movie/movieapi/config/**',
'**/com/movie/movieapi/interfaces/movie/dto/**',

'**/com/movie/domain/config/**',
'**/com/movie/domain/common/**',
'**/com/movie/domain/movie/dto/**',
'**/com/movie/domain/movie/message/**',
'**/com/movie/domain/movie/domain/Movie.class',
'**/com/movie/domain/movie/domain/Schedule.class',
'**/com/movie/domain/movie/domain/Screen.class',
'**/com/movie/domain/movie/domain/Theater.class',
'**/com/movie/domain/movie/domain/TimeTable.class',

tasks.named("bootJar") {
mainClass = 'com.movie.movieapi.MovieApiApplication'
'**/com/movie/storage/movie/entity/**',
'**/com/movie/storage/movie/bulkInsert/**',
'**/com/movie/storage/movie/dto/**',
'**/com/movie/storage/mapper/**',


]
}
finalizedBy 'jacocoTestReport' // 테스트 완료 후 JaCoCo 리포트 생성
finalizedBy 'jacocoTestCoverageVerification' // 테스트 완료 후 커버리지 검증 실행
}
tasks.named('jacocoTestReport') { // 기존 jacocoTestReport 태스크 재정의
reports {
html.required.set(true)
xml.required.set(false)
csv.required.set(false)
}
classDirectories.setFrom(
files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
'**/com/movie/movieapi/config/**',
'**/com/movie/movieapi/interfaces/movie/dto/**',

'**/com/movie/domain/config/**',
'**/com/movie/domain/common/**',
'**/com/movie/domain/movie/dto/**',
'**/com/movie/domain/movie/message/**',
'**/com/movie/domain/movie/domain/Movie.class',
'**/com/movie/domain/movie/domain/Schedule.class',
'**/com/movie/domain/movie/domain/Screen.class',
'**/com/movie/domain/movie/domain/Theater.class',
'**/com/movie/domain/movie/domain/TimeTable.class',

'**/com/movie/storage/movie/entity/**',
'**/com/movie/storage/movie/bulkInsert/**',
'**/com/movie/storage/movie/dto/**',
'**/com/movie/storage/mapper/**',
])
})
)
}
jacocoTestCoverageVerification { // 커버리지 기준 설정
violationRules {
rule {
element = 'CLASS'
excludes = [
'**/com/movie/movieapi/config/**',
'**/com/movie/movieapi/interfaces/movie/dto/**',

'**/com/movie/domain/config/**',
'**/com/movie/domain/common/**',
'**/com/movie/domain/movie/dto/**',
'**/com/movie/domain/movie/message/**',
'**/com/movie/domain/movie/domain/Movie.class',
'**/com/movie/domain/movie/domain/Schedule.class',
'**/com/movie/domain/movie/domain/Screen.class',
'**/com/movie/domain/movie/domain/Theater.class',
'**/com/movie/domain/movie/domain/TimeTable.class',

'**/com/movie/storage/movie/entity/**',
'**/com/movie/storage/movie/bulkInsert/**',
'**/com/movie/storage/movie/dto/**',
'**/com/movie/storage/mapper/**',
]
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.60 // 최소 라인 커버리지 80%
}
}
}
}
}
Binary file added docs/images/AOP-distributed-lock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/functional-distributed-lock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/jacoco-api.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/jacoco-domain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/jacoco-storage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions http/reservation.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### 예약 API 추가
POST http://localhost:8080/api/reservations
Content-Type: application/json
Authorization: 1f504eb92b17

{
"scheduleId": 1,
"seatIds": [4, 5]
}
10 changes: 0 additions & 10 deletions http/schedule.http
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,4 @@ Content-Type: application/json
{
"title": null,
"genre": "ACTION"
}

### 예약 API 추가
POST http://localhost:8080/api/reservations
Content-Type: application/json
Authorization: 1f504eb92b17

{
"scheduleId": 1,
"seatIds": [1, 2, 3]
}
1 change: 1 addition & 0 deletions movie-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies {
// rate limit - guava
implementation 'com.google.guava:guava:32.1.2-jre'

implementation project(':movie-application:application')
implementation project(':movie-domain:domain')
implementation project(':movie-domain:common')
implementation project(':movie-infrastructures:storage')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
@ComponentScan(basePackages = {
"com.movie.domain", "com.movie.storage",
"com.movie.movieapi", "com.movie.redis",
"com.movie.application"
})
public class MovieApiApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.movie.movieapi.config;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
HttpServletRequest 에서 @RequestBody를 받아올 수 있도록 하는 Wrapper class
*/
public class RequestWrapper extends HttpServletRequestWrapper {

private final byte[] body;

public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = getBody(request).getBytes(StandardCharsets.UTF_8);
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}

@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return byteArrayInputStream.read();
}

@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
};
}

private String getBody(HttpServletRequest request) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}

public String getBodyContent() {
return new String(body, StandardCharsets.UTF_8);
}

}
21 changes: 14 additions & 7 deletions movie-api/src/main/java/com/movie/movieapi/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.movie.movieapi.config;

import com.movie.movieapi.config.interceptor.DataFetchRateLimitInterceptor;
import com.movie.movieapi.config.interceptor.DataFetchRateLimiterRedisInterceptor;
import com.movie.movieapi.config.interceptor.ReservationRateLimitInterceptor;
import com.movie.movieapi.config.interceptor.ReservationRateLimitRedisInterceptor;
import com.movie.movieapi.filter.RequestWrapperFilter;
import com.movie.movieapi.interceptor.DataFetchRateLimiterRedisInterceptor;
import com.movie.movieapi.interceptor.ReservationRateLimitRedisInterceptor;
import jakarta.servlet.Filter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
Expand All @@ -13,9 +15,6 @@
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

private final DataFetchRateLimitInterceptor dataFetchRateLimitInterceptor;
private final ReservationRateLimitInterceptor reservationRateLimitInterceptor;

private final DataFetchRateLimiterRedisInterceptor dataFetchRateLimiterRedisInterceptor;
private final ReservationRateLimitRedisInterceptor reservationRateLimitRedisInterceptor;

Expand All @@ -27,4 +26,12 @@ public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(reservationRateLimitRedisInterceptor)
.addPathPatterns("/api/reservations/**");
}

@Bean
public FilterRegistrationBean<Filter> requestWrapperFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new RequestWrapperFilter());
filterRegistrationBean.addUrlPatterns("/api/reservations/*");
return filterRegistrationBean;
}
}
Loading