Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
00e0f91
Merge branch 'hanghae-skillup:main' into dev
dbdb1114 Jan 11, 2025
6f13ff4
[ feat ] readme 작성
dbdb1114 Jan 11, 2025
7570bf0
[ refactor ] 불필요 interface 제거
dbdb1114 Jan 23, 2025
91bb9f7
[ refactor ] 불필요 넘버링 제거
dbdb1114 Jan 23, 2025
ee57d70
[ refactor ] 400 매직넘버 제거
dbdb1114 Jan 23, 2025
bce708c
Merge pull request #3 from dbdb1114/feature/caching
dbdb1114 Jan 23, 2025
32ecc2f
[ refactor ] 프로젝트 프로퍼티 통합
dbdb1114 Jan 23, 2025
9be6370
[ test ] test 프로퍼티 Import 설정 추가
dbdb1114 Jan 23, 2025
1b6cf7c
[ test ] 캐싱 evict 메소드 기능 테스트
dbdb1114 Jan 23, 2025
f5f5571
Merge pull request #4 from dbdb1114/feature/caching
dbdb1114 Jan 23, 2025
c3163a8
[ refactor ] showingCache 관련 로직 제거
dbdb1114 Jan 23, 2025
698bba6
Revert "[ refactor ] showingCache 관련 로직 제거"
dbdb1114 Jan 23, 2025
bc33f87
[ refactor ] ShowingCacheService 생성 및 테스트 코드 수정
dbdb1114 Jan 23, 2025
523dfbd
[ refactor ] core moduel ShowingResponse stTime,edTime 칼럼명 수정
dbdb1114 Jan 24, 2025
8b126ff
[ refactor ] stTime, edTime => showStTime,showEdTime 칼럼명 변경
dbdb1114 Jan 25, 2025
a1a4441
[ fix ] redis LocalDateTime 직렬화,역직렬화 설정 수정
dbdb1114 Jan 25, 2025
8482053
[ fix ] 전반적인 실행환경 수정
dbdb1114 Jan 25, 2025
1240b3a
[ fix ] 로컬에서 사용하는 클래스 ignore 설정 추가
dbdb1114 Jan 25, 2025
510ab7e
[ feat ] rds-repo module - 사용자 엔티티 및 레파지토리 작성
dbdb1114 Jan 26, 2025
84667c7
[ feat ] rds-repo module 티켓 레파지토리 및 엔티티 수정
dbdb1114 Jan 26, 2025
e6d8ef8
[ feat ] rds-repo module - 좌석 엔티티 및 티켓 좌석 조회 레파지토리 작성
dbdb1114 Jan 26, 2025
06a3570
[ feat ] rds-repo module - 판매 엔티티 및 사용자별 티켓 판매 내역 정보 조회 레파지토리 작성
dbdb1114 Jan 26, 2025
32ed9fd
[ feat ] rds-repo module - rating age column 추가
dbdb1114 Jan 26, 2025
dc85e29
[ fix ] rds-repo module - Movie 엔티티 getter 추가
dbdb1114 Jan 26, 2025
029dcca
[ test ] rds-repo module 사용자별 티켓 구매 이력 조회 테스트 작성
dbdb1114 Jan 26, 2025
0a13318
[ feat ] module-external 모듈 추가
dbdb1114 Jan 26, 2025
b76e4c7
[ feat ] external module - fcm 앱 푸시 서비스 작성
dbdb1114 Jan 26, 2025
cf3586a
[ feat ] module-core - 티켓 예매 서비스에 대한 커스텀 예외 정의
dbdb1114 Jan 26, 2025
b99c4f9
[ feat ] service module - 티켓 조회 및 예매 기능 구현 및 테스트 코드 작성
dbdb1114 Jan 26, 2025
1e116b3
[ feat ] core module - ticket및 seates 관련 DTO 작성
dbdb1114 Jan 26, 2025
b535eae
[ fix ] core module - spring validation 의존성 추가
dbdb1114 Jan 26, 2025
2468321
[ feat ] app module 예외처리 어드바이스 작성
dbdb1114 Jan 26, 2025
c27d11a
[ feat ] app module 티켓 예매 컨트롤러 및 테스트 코드 작성
dbdb1114 Jan 26, 2025
8caa762
[ feat ] .http 작성
dbdb1114 Jan 26, 2025
db2379e
[ fix ] app module - 유저별 티켓 조회 수정 및 예매 시스템 기능 구현 완료
dbdb1114 Jan 27, 2025
6351b56
[ feat ] pessimisticLock 설정 및 테스트
dbdb1114 Jan 27, 2025
2f032ac
[ feat ] OptimisticLock 설정 및 테스트
dbdb1114 Jan 27, 2025
7db5e3c
Merge remote-tracking branch 'forked-origin/dev' into dev
dbdb1114 Jan 27, 2025
138359a
Merge branch 'dbdb1114' into dev
dbdb1114 Jan 29, 2025
b8875b8
[ Feat ] 설정 정보 변경
dbdb1114 Feb 1, 2025
e963c98
[ feat ] redisson 기반 분산락 aop 구현
dbdb1114 Feb 1, 2025
19c3228
[ feat ] service module aop 기반 분산락 적용
dbdb1114 Feb 1, 2025
4860358
[ feat ] rds-repo module 낙관적락 설정 해제
dbdb1114 Feb 1, 2025
fe569d7
[ feat ] rds-repo module @Version 제거
dbdb1114 Feb 2, 2025
19b2851
[ feat ] 락 점유 실패 exception 정의
dbdb1114 Feb 2, 2025
3330668
[ feat ] service module AOP 기반 분산락 작성
dbdb1114 Feb 2, 2025
1a3aaa6
[ feat ] service module 함수형 분산락 정의
dbdb1114 Feb 2, 2025
a1d22ba
[ feat ] service module ticketService 분산락별 메소드 분리
dbdb1114 Feb 2, 2025
d6b82c5
[ feat ] service module waitTime및 leaseTime 설정
dbdb1114 Feb 2, 2025
28871da
[ feat ] readme 분산락 관련 내용 정리
dbdb1114 Feb 2, 2025
2b5c4b2
Merge remote-tracking branch 'forked-origin/dev' into dev
dbdb1114 Feb 2, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ bin/

### Mac OS ###
.DS_Store

### 로컬에서만 사용 클래스
CreateShowing.java
ScheduleTest.java
240 changes: 237 additions & 3 deletions README.md

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,67 @@ app은 전반적으로 클라이언트와 요청을 주고받는 모듈입니다
![스크린샷 2025-01-08 오전 11.26.11.png](https://github.com/user-attachments/assets/036e8c14-4300-4f0b-adc3-603580e30928)

네이밍은 조금 더 신경 쓰겠지만 아무튼 이렇게 두 개로 나눠질 수 있지 않을까 싶습니다. 그땐 조금 더 세부적인 책임과 역할이 생긴 상태겠지요


## Redisson 기반 분산락 구현 관련 및 성능 테스트

### wait_time과 lease_time
[ feat ] service module waitTime및 leaseTime 설정
PEAK 타임을 고려한 최대 동시간대 요청 건수 계산

----------------------------------------------------------
Junghyun's hanghaeho

인기영화: 100편 <br>
비인기 영화: 489편 <br>
상영정보: 모든 영화 1일 1회씩 상영, d+2 상영정보까지 전시

----------------------------------------------------------

인기 영화 100편의 경우<br>
상영정보 오픈과 동시에 반 정도의 티켓이 바로 판매된다고 가정<br>
100 * 13 = 1300건

비인기 영화 489편의 경우<br>
상영정보 오픈과 동시에 약 3매 티켓 정도 바로 판매된다고 가정<br>
489 * 3 = 1467건

피크시간 전체 판매 티켓 수 : 2767건

----------------------------------------------------------

예매는 한 사람이 여러좌석 예매 가능하므로<br>
피크시간 예상 요청건수 = 922.33333 건 (전체 판매 티켓 수/3)<br>
상영정보 조회 및 선택 - 좌석 선정 프로세스 소요시간 : 최대 30초 추정

----------------------------------------------------------

테스트상 30초까지 점진적으로 1000건의 요청시 최대 요청 처리시간 : 483.43ms


waitTime 동안 Lock을 기다릴 이유는 무엇인가? <br>
먼저 좌석을 선정한 고객이 불가피한 이유로 결제 과정까지 넘어가지 못 했을 때

>**결제 과정까지 넘어가지 못하는 상황**
>1. 로그인, 연령 부적합 등 애초에 좌석 선정 자체가 불가한 경우
>2. 결제 중 이탈
>3. 단순 변심
>4. ...

사실상 1번 말고는 기다렸다가 재시도 해야하고, 1번의 경우 매우 적은 수일 것으로 예상되어
실제로 Lock 점유를 기다리는 시간이 그다지 길 필요가 없다는 판단됨.
최악의 경우를 생각하여 앞의 요청 처리 두 건에서 lock점유 후
다음 프로세스로 진행이 못 했을 때 까지만 보장하기로 함
결과적으로 wait_time은 최대 요청 처리 시간의 두 배, lease_time은 wait_time의 2배로 선정<br>

**wait_time: 1초<br>
lease_time: 2초**


### 성능 테스트 비교
#### AOP
![스크린샷 2025-02-02 오후 4 23 36](https://github.com/user-attachments/assets/e189898a-bb6e-47dc-bde3-e75b1823e66a)
#### 함수형
![스크린샷 2025-02-02 오후 4 32 20](https://github.com/user-attachments/assets/f795a4ca-b8f0-458f-9d65-30a7d0178375)
#### 테스트 비교
<img width="554" alt="스크린샷 2025-02-02 오후 9 46 33" src="https://github.com/user-attachments/assets/90207370-bce6-445f-a017-06faaf98ac0a" />
23 changes: 18 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
version: "3.7"

services:
db:
image: mysql
hanghaeho-redis:
image: redis
restart: always
ports:
- "6300:6379"
hanghaeho-mysql:
image: mysql:8
restart: always
ports:
- "3300:3306"
environment:
MYSQL_ROOT_PASSWORD: Tlmm3PjdJ*

volumes:
- ./mysql.conf:/etc/mysql/conf.d # conf 파일이 위치한 디렉토리를 마운트
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
app:
command:
# - --skip-character-set-client-handshake
# - --character-set-server=utf8mb4
# - --collation-server=utf8mb4_unicode_ci
hanghaeho-application:
restart: always
build:
context: . # Dockerfile이 있는 디렉토리
dockerfile: Dockerfile # 사용할 Dockerfile 지정 (옵션)
ports:
- "8000:8080"
environment:
SPRING_PROFILES_ACTIVE: dev
depends_on:
- hanghaeho-mysql
- hanghaeho-redis
2 changes: 2 additions & 0 deletions http/TicketListAllByShowing.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
###
GET http://localhost:8080/api/v1/ticket/all?showingId=7770
1 change: 1 addition & 0 deletions http/TicketListAllByUser.http
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GET http://localhost:8080/api/v1/ticket/reserved?username=dbdb1114
19 changes: 19 additions & 0 deletions http/TicketReservation.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
POST http://localhost:8080/api/v1/ticket/reservation
Content-Type: application/json

{
"username": "dbdb1114",
"showingId": 9176,
"ticketList": [
{
"ticketId" : 35426
},
{
"ticketId" : 35427
},
{
"ticketId" : 35428
}
]
}

2,266 changes: 1,942 additions & 324 deletions init.sql

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions module-app/src/main/java/module/controller/TicketController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package module.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import dto.ticket.TicketDTO;
import dto.ticket.TicketReservationRequest;
import dto.ticket.TicketResponse;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import module.service.ticket.TicketService;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/ticket")
public class TicketController {

private final TicketService ticketService;

@GetMapping(value = "/all")
public ResponseEntity<List<TicketResponse>> getAllTicket(
@NotNull @RequestParam Long showingId
) {
return ResponseEntity.ok(ticketService.getAllTicket(showingId));
}

@GetMapping(value = "/reserved")
public ResponseEntity<List<TicketResponse>> getUserTicket(
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "사용자 아이디는 영문 및 숫자로 이루어져 있습니다.")
@NotNull @RequestParam String username
){
return ResponseEntity.ok(ticketService.getUserTicket(username));
}

@PostMapping(value = "/reservation")
public ResponseEntity<String> reservation(
@RequestBody TicketReservationRequest request
) {
Long showingId = request.getShowingId();
String username = request.getUsername();
List<TicketDTO> ticketList = request.getTicketList();
return ResponseEntity.ok(ticketService.reservationWithFunctional(showingId, username, ticketList));
}
}
71 changes: 66 additions & 5 deletions module-app/src/main/java/module/exception/ControllerAdvice.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;

import exception.common.TryLockFailedException;
import exception.showing.ShowingNotFoundException;
import exception.ticket.InvalidAgeForMovieException;
import exception.ticket.InvalidSeatConditionException;
import exception.ticket.InvalidTicketException;
import exception.ticket.NotOnSaleTicketException;
import exception.ticket.TooManyReservationException;
import exception.user.UserNotFoundException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
Expand All @@ -15,11 +23,64 @@ public class ControllerAdvice {

@ExceptionHandler(HandlerMethodValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<ErrorResponse> notValidParameter(HandlerMethodValidationException e){
protected ResponseEntity<String> notValidParameter(HandlerMethodValidationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse.builder()
.errorCode(400)
.message(e.getParameterValidationResults().get(0).getResolvableErrors().get(0).getDefaultMessage())
.build());
.body(e.getParameterValidationResults().get(0).getResolvableErrors().get(0).getDefaultMessage());
}

@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<String> userNotFound(UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}

@ExceptionHandler(InvalidAgeForMovieException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<String> invalidReservation(InvalidAgeForMovieException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}

@ExceptionHandler(InvalidSeatConditionException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<String> invalidReservation(InvalidSeatConditionException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}

@ExceptionHandler(InvalidTicketException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<String> invalidReservation(InvalidTicketException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}

@ExceptionHandler(TooManyReservationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<String> invalidReservation(TooManyReservationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}

@ExceptionHandler(NotOnSaleTicketException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<String> invalidReservation(NotOnSaleTicketException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}

@ExceptionHandler(ShowingNotFoundException.class)
@ResponseStatus(HttpStatus.NO_CONTENT)
protected ResponseEntity<String> showingNotFoundException(ShowingNotFoundException e){
return ResponseEntity.status(HttpStatus.NO_CONTENT)
.body(e.getMessage());
}

@ExceptionHandler(TryLockFailedException.class)
@ResponseStatus(HttpStatus.NO_CONTENT)
protected ResponseEntity<String> tryLockFailedException(TryLockFailedException e){
return ResponseEntity.status(HttpStatus.NO_CONTENT)
.body(e.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ spring:
hibernate:
format_sql: true
show_sql: true
data:
redis:
host: host.docker.internal
port: 6300


logging:
level:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ spring:
hibernate:
format_sql: true
show_sql: true
data:
redis:
host: localhost
port: 6379


logging:
level:
Expand Down
13 changes: 7 additions & 6 deletions module-app/src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,24 @@ <h2 class="text-2xl font-bold mb-6">박스오피스</h2>

<script>
async function fetchMovies() {
console.log(`${window.location.origin}/api/v1/showings/all`)
const response = await fetch(`${window.location.origin}/api/v1/showings/all`);
const data = await response.json();

const movieGrid = document.getElementById('movie-grid');
movieGrid.innerHTML = '';

data.data.forEach(item => {
data.forEach(item => {
const { movie, showings } = item;

const card = document.createElement('div');
card.className = 'bg-gray-800 rounded-lg shadow-lg movie-card';

const showingsHTML = showings.map(showing => `
<tr>
<td class="px-4 py-2 text-gray-400">${new Date(showing.stTime).toLocaleDateString()}</td>
<td class="px-4 py-2 text-gray-400">${new Date(showing.stTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</td>
<td class="px-4 py-2 text-gray-400">${showing.screenDTO.name}</td>
<td class="px-4 py-2 text-gray-400">${new Date(showing.showStTime).toLocaleDateString()}</td>
<td class="px-4 py-2 text-gray-400">${new Date(showing.showStTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</td>
<td class="px-4 py-2 text-gray-400">${showing.screen.screenName}</td>
</tr>
`).join('');

Expand All @@ -120,10 +121,10 @@ <h2 class="text-2xl font-bold mb-6">박스오피스</h2>
<img src="${movie.rating.img}" alt="등급 이미지" class="w-8 h-8 mr-2">
<h3 class="text-lg font-bold">${movie.title}</h3>
</div>
<p class="text-gray-400">${movie.rating.name}</p>
<p class="text-gray-400">${movie.rating.ratingName}</p>
<p class="text-gray-400">개봉일: ${movie.openDay}</p>
<p class="text-gray-400">런닝타임: ${movie.runningTimeAsMinute}분</p>
<p class="text-gray-400">장르: ${movie.genre.name}</p>
<p class="text-gray-400">장르: ${movie.genre.genreName}</p>
<table class="mt-4 w-full">
<thead>
<tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package module.controller;

import static org.assertj.core.api.Assertions.*;

import java.util.List;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.Rollback;

import dto.ticket.TicketResponse;

@SpringBootTest
@Rollback
public class TicketControllerTest {

private final TicketController ticketController;

@Autowired
public TicketControllerTest(TicketController ticketController) {
this.ticketController = ticketController;
}

@Test
@DisplayName("[조회] - 기능")
public void getAllTicketTest(){
//given
Long showingId = 7760L;

//when
ResponseEntity<List<TicketResponse>> allTicket = ticketController.getAllTicket(showingId);

//then
assertThat(allTicket.getStatusCode().value()).isEqualTo(200);
assertThat(allTicket.getBody().size()).isEqualTo(25);
}

}
Loading