Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
3365700
[chore] project setup
devops3199 Jan 7, 2025
05e045c
[chore] update readme
devops3199 Jan 8, 2025
cf57e7e
[refactor] change module name. api -> movie-api
devops3199 Jan 8, 2025
73f4657
[chore] change architecture image
devops3199 Jan 8, 2025
182bb6c
[chore] change erd image
devops3199 Jan 8, 2025
ec6816c
[chore] change erd image 2
devops3199 Jan 8, 2025
91ebafe
[fix] join table
devops3199 Jan 9, 2025
4bb0c93
[chore] remove unused import
devops3199 Jan 9, 2025
ee32bb4
[feat] docker-compose database
devops3199 Jan 9, 2025
31dbfd5
[chore] change directory for readme imgs
devops3199 Jan 9, 2025
11f31b3
[chore] update readme
devops3199 Jan 9, 2025
aec1f8b
[fix] movie status mock data
devops3199 Jan 9, 2025
56ae71b
[feat] sorting movies
devops3199 Jan 9, 2025
a5e8c91
[fix] change to LessThanEqual
devops3199 Jan 9, 2025
114abc3
[feat] add error advice controller
devops3199 Jan 9, 2025
64cc0b8
[chore] update readme
devops3199 Jan 9, 2025
c1bbe56
[fix] add movie status condition when fetch data
devops3199 Jan 9, 2025
3db3353
[refactor] search dto
devops3199 Jan 10, 2025
5633132
[refactor] create root build.gradle & add common used library
devops3199 Jan 10, 2025
d2313e7
[chore] add more mocked data
devops3199 Jan 10, 2025
c817a13
[fix] apply pr review
devops3199 Jan 11, 2025
e89180b
[fix] change to @Repository
devops3199 Jan 11, 2025
f610fbd
[refactor] advance clean architecture & consists of 4 modules 2
devops3199 Jan 13, 2025
607b68a
[refactor] change module name. adapter -> infrastructure
devops3199 Jan 14, 2025
2cfc25f
[chore] update readme
devops3199 Jan 14, 2025
7cfd130
[feat] add TheaterSeat enum
devops3199 Jan 14, 2025
4e0495d
[refactor] port names 2
devops3199 Jan 14, 2025
a39f67d
[fix] db_movie_showtime table start,end column type
devops3199 Jan 15, 2025
92a415d
[feat] add redis docker
devops3199 Jan 15, 2025
2e37685
[feat] add enum validator annotation
devops3199 Jan 15, 2025
83c2d2d
[feat] apply Cacheable feature
devops3199 Jan 15, 2025
7486542
[refactor] dependencies between modules
devops3199 Jan 17, 2025
ae8fa2c
[chore] undo application.yml changes
devops3199 Jan 17, 2025
4c82c1c
[refactor] querydsl dependency
devops3199 Jan 18, 2025
ec4c4e6
[chore] refactor non-code files
devops3199 Jan 18, 2025
3d5dc75
[fix] npe error from request dto
devops3199 Jan 18, 2025
2aa1649
[test] add http file
devops3199 Jan 18, 2025
74f6bcf
[test] add k6 script
devops3199 Jan 18, 2025
5c796ae
[feat] add redisson
devops3199 Jan 19, 2025
00d8bf4
[chore] add http
devops3199 Jan 19, 2025
b700933
[chore] update readme
devops3199 Jan 19, 2025
2f7743a
[refactor] movie-api request dto convert logic
devops3199 Jan 19, 2025
1915964
[refactor] apply code review
devops3199 Jan 21, 2025
6f8b81e
[chore] add index to ddl script
devops3199 Jan 21, 2025
0a8fea6
[test] add 5 stages. stress test!
devops3199 Jan 21, 2025
ab3bcbb
[chore] add endpoint to .http file
devops3199 Jan 21, 2025
1b11e97
[fix] complex cache key
devops3199 Jan 21, 2025
fc78548
[test] apply sample test code
devops3199 Jan 22, 2025
5e94eb3
[feat] 예약 API 구현
devops3199 Jan 25, 2025
f1548af
[test] pessimistic lock 구현 & 테스트코드 환경 세팅 & 테스트코드 작성
devops3199 Jan 26, 2025
ebade15
[test] optimistic lock 구현
devops3199 Jan 26, 2025
9edcaab
[feat] send message 구현
devops3199 Jan 26, 2025
d994a71
[test] AOP 기반 distributed lock 구현
devops3199 Jan 26, 2025
4780336
[test] 함수형 기반 distributed lock 구현
devops3199 Jan 26, 2025
12de380
[test] 분산락 성능 테스트 및 README 업데이트
devops3199 Jan 26, 2025
7a62b2c
[fix] 낙관적락 & 분산락 함께 적용
devops3199 Jan 27, 2025
eb840f9
[fix] PR 리뷰 적용
devops3199 Jan 27, 2025
14ce595
[feat] 단일 서버 RateLimit 구현
devops3199 Jan 29, 2025
74f84fa
[feat] 분산 환경 RateLimit 구현
devops3199 Jan 30, 2025
620e443
[fix] 예약 성공 시 rate limit 적용
devops3199 Feb 1, 2025
44841c1
[test] 테스트 코드 작성
devops3199 Feb 1, 2025
0103907
[test] @DataJpaTest로 변경
devops3199 Feb 2, 2025
2fd597c
[chore] update readme
devops3199 Feb 2, 2025
ed9e3f8
[fix] SeatRepositoryTest 테스트 조건 수정
devops3199 Feb 2, 2025
3aba743
[refactor] key generator 유틸 생성
devops3199 Feb 2, 2025
e5c4f91
[refactor] PR 리뷰 적용
devops3199 Feb 2, 2025
f6068e8
[refactor] 테스트 코드 메소드 이름 및 변수명
devops3199 Feb 4, 2025
bf10709
[refactor] use-case 로직 -> domain 로직으로 이동
devops3199 Feb 4, 2025
47f4f76
[test] domain 테스트 작성
devops3199 Feb 4, 2025
11a7b32
[chore] readme 업데이트
devops3199 May 17, 2025
4aec07b
[chore] readme 업데이트
devops3199 May 17, 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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Spring Boot
*.log
*.class

# Gradle
.gradle/

# IntelliJ IDEA
.idea/
81 changes: 78 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,78 @@
## [본 과정] 이커머스 핵심 프로세스 구현
[단기 스킬업 Redis 교육 과정](https://hh-skillup.oopy.io/) 을 통해 상품 조회 및 주문 과정을 구현하며 현업에서 발생하는 문제를 Redis의 핵심 기술을 통해 해결합니다.
> Indexing, Caching을 통한 성능 개선 / 단계별 락 구현을 통한 동시성 이슈 해결 (낙관적/비관적 락, 분산락 등)
# [본 과정] 이커머스 핵심 프로세스 구현
- 분산 캐시를 적용한 영화 조회 API 구현
- 분산 락을 적용한 영화 자리 예매 API 구현
- 분산 Rate Limit을 적용한 API 요청 속도 및 횟수 제어 구현

## How to use

```bash
docker compose up -d
```
```bash
curl -X GET http://localhost:8080/api/v1/movies
```

## Multi Module

### 1-1. movie-api
> 영화 도메인 presentation 담당합니다.

### 1-2. booking-api
> 예약 도메인 presentation 담당합니다.

### 2. application
> Use Case 생성을 담당합니다.

### 3. infrastructure
> DB 연결과 Entity 관리를 담당합니다.

### 4. domain
> 도메인 로직을 포함합니다.

## Architecture
> 클린 아키텍처를 지향합니다.

![arc](etc/readme/arc3.png)

## Table Design
![erd_db](etc/readme/erd2.png)

## 성능 테스트
### 캐싱할 데이터

> API 응답을 캐싱하였습니다.

```json
// 응답 예시
[
{
"id": 0,
"title": "나 홀로 집에",
"description": "...",
"rating": "전체관람가",
"genre": "코미디",
"thumbnail": "https://...",
"runningTime": 103,
"releaseDate": "1991-07-06",
"theaters": ["강남점", "안양점"],
"showtimes": ["08:00 ~ 09:45", "10:00 ~ 11:45"]
}
]
```

### 분산락
> Lease Time 길게 잡을수록 한 스레드가 lock을 오래 잡고 있어, 동시성 성능 테스트할때 fail 요청률이 90% 이상 나왔습니다.
>
> Wait Time 경우 좌석 예매 특성상 오래 기다린다고 예약이 되는건 아니라 Lease Time보다 낮게 잡았습니다.
- Lease Time - 2초
- Wait Time - 1초

### 보고서
- [캐싱 성능 테스트 보고서](https://gusty-football-62b.notion.site/17f81b29f03680718163fe0b7798383e)
- [분산락 테스트 보고서](https://gusty-football-62b.notion.site/18781b29f03680049de7db34240a6733)

### jacoco 리포트

| movie-api | booking-api | application | infrastructure | domain |
|----------------------------| ----------- |----------------------------|----------------------------|----------------------------|
| ![j_m](etc/readme/j_m.png) | ![j_b](etc/readme/j_b.png) | ![j_a](etc/readme/j_a.png) | ![j_i](etc/readme/j_i.png) | ![j_d](etc/readme/j_d.png) |
36 changes: 36 additions & 0 deletions application/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
../.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
22 changes: 22 additions & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
dependencies {
implementation project(':domain')

testImplementation project(':infrastructure')
}

bootJar {
enabled = false
}

test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}

jacocoTestReport {
dependsOn test

reports {
html.required = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.example.app.booking.dto;

import com.example.app.booking.domain.Booking;
import com.example.app.movie.type.TheaterSeat;
import lombok.Builder;

import java.time.LocalDate;
import java.util.Set;

@Builder
public record CreateBookingCommand(
Long userId,
Long movieId,
Long showtimeId,
Long theaterId,
LocalDate bookingDate,
Set<TheaterSeat> seats
){
public SearchSeatCommand toSearchSeatCommand() {
return SearchSeatCommand.builder()
.movieId(movieId)
.showtimeId(showtimeId)
.theaterId(theaterId)
.bookingDate(bookingDate)
.seats(seats)
.build();
}

public SearchBookingCommand toSearchBookingCommand() {
return SearchBookingCommand.builder()
.userId(userId)
.movieId(movieId)
.showtimeId(showtimeId)
.theaterId(theaterId)
.bookingDate(bookingDate)
.build();
}

public Booking toBooking() {
return Booking.builder()
.userId(userId)
.movieId(movieId)
.showtimeId(showtimeId)
.theaterId(theaterId)
.bookingDate(bookingDate)
.totalSeats(seats.size())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.app.booking.dto;

import lombok.Builder;

import java.time.LocalDate;

@Builder
public record SearchBookingCommand(
Long userId,
Long movieId,
Long showtimeId,
Long theaterId,
LocalDate bookingDate
){
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.app.booking.dto;

import com.example.app.movie.type.TheaterSeat;
import lombok.Builder;

import java.time.LocalDate;
import java.util.Set;

@Builder
public record SearchSeatCommand(
Long bookingId,
Long movieId,
Long showtimeId,
Long theaterId,
LocalDate bookingDate,
Set<TheaterSeat> seats
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.app.booking.port;

import com.example.app.booking.domain.Booking;
import com.example.app.booking.dto.CreateBookingCommand;

public interface CreateBookingPort {
Booking saveBooking(CreateBookingCommand createBookingCommand);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.app.booking.port;

import com.example.app.booking.domain.Booking;
import com.example.app.booking.dto.SearchBookingCommand;

import java.util.List;

public interface LoadBookingPort {
List<Booking> loadAllBookings(SearchBookingCommand searchBookingCommand);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.app.booking.port;

import com.example.app.booking.domain.Seat;
import com.example.app.booking.dto.SearchSeatCommand;

import java.util.List;

public interface LoadSeatPort {

List<Seat> loadAllSeats(SearchSeatCommand searchSeatCommand);

List<Seat> loadAllSeatsByBookingIds(List<Long> bookingIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.app.booking.port;

import com.example.app.booking.domain.Seat;

import java.util.List;

public interface UpdateSeatPort {

List<Seat> updateAllSeats(List<Long> seatIds, Long bookingId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.example.app.booking.service;

import com.example.app.booking.domain.Booking;
import com.example.app.booking.domain.Seat;
import com.example.app.booking.dto.CreateBookingCommand;
import com.example.app.booking.port.CreateBookingPort;
import com.example.app.booking.port.LoadBookingPort;
import com.example.app.booking.port.LoadSeatPort;
import com.example.app.booking.port.UpdateSeatPort;
import com.example.app.common.exception.APIException;
import com.example.app.common.function.DistributedLockService;
import com.example.app.movie.type.TheaterSeat;
import com.example.app.booking.usecase.CreateBookingUseCase;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import static com.example.app.booking.exception.BookingErrorMessage.*;

@Slf4j
@Service
@RequiredArgsConstructor
public class CreateBookingService implements CreateBookingUseCase {

private final Integer MAX_SEATS = 5;

private final LoadSeatPort loadSeatPort;
private final UpdateSeatPort updateSeatPort;
private final LoadBookingPort loadBookingPort;
private final CreateBookingPort createBookingPort;

private final DistributedLockService distributedLockService;

@Override
@Transactional
public Booking createBooking(String lockKey, CreateBookingCommand createBookingCommand) {
// 유저 확인
checkValidUser(createBookingCommand.userId());

// 연속된 row 체크
TheaterSeat.checkSeatsInSequence(createBookingCommand.seats());

// 기존 예약 조회
var existingBookingIds = loadBookingPort.loadAllBookings(createBookingCommand.toSearchBookingCommand())
.stream()
.map(Booking::id)
.toList();

// 기존 예약의 자리 조회
var existingSeats = loadSeatPort.loadAllSeatsByBookingIds(existingBookingIds);

// 요청한 자리 + 이미 예약한 자리 = 5개 넘는지 체크
checkLimitMaxSeats(createBookingCommand.seats().size() + existingSeats.size());

// booking 생성
var booking = createBookingPort.saveBooking(createBookingCommand);

return distributedLockService.executeWithLockAndReturn(() -> {
var requestSeats = loadSeatPort.loadAllSeats(createBookingCommand.toSearchSeatCommand());

// 요청한 자리 예약 가능 여부 체크
Seat.checkSeatsAvailable(requestSeats);

// 요청한 자리들 업데이트
var requestSeatIds = requestSeats.stream().map(Seat::id).toList();
updateSeatPort.updateAllSeats(requestSeatIds, booking.id());

return booking;
}, lockKey, 1L, 3L);
}

private void checkLimitMaxSeats(final int totalSeat) {
if (totalSeat > MAX_SEATS) {
throw new APIException(OVER_MAX_LIMIT_SEATS);
}
}

private void checkValidUser(final long userId) {
log.info(">>>>>> Checking userId : {}", userId);
/* pseudo code
* try {
* var user = userApi.getUser(userId);
* if (user == null) { throw new APIException(NOT_VALID_USER); }
* } catch (Exception e) {
* throw new APIException(SERVICE_NETWORK_ERROR);
* }
* */
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.app.booking.service;

import com.example.app.booking.usecase.SendMessageUseCase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class SendMessageService implements SendMessageUseCase {

public void sendMessage(String message) throws InterruptedException {
log.info(">>>>> Sending message: {}", message);
Thread.sleep(500);
log.info(">>>>> Sent");
}
}
Loading