Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e407e9c
refactor: 영화 검색 조건을 DTO로 캡슐화해 확장 가능하도록 개선
zhdiddl Jan 21, 2025
d34a68b
refactor: 요청 값에 대한 validation 조건 개선
zhdiddl Jan 21, 2025
82ce364
feat: 인덱스 생성 쿼리를 DDL에 추가
zhdiddl Jan 23, 2025
b6c91f5
refactor: 동적 정렬 로직에 순서 유지 및 필드 제한 적용
zhdiddl Jan 23, 2025
2e43f50
chore: 복합 인덱스 적용 후 성능 테스트 결과 README 반영
zhdiddl Jan 23, 2025
7afcc1d
chore: 주석 및 설정 수정으로 가독성 개선
zhdiddl Jan 23, 2025
df23690
chore: README 수정
zhdiddl Jan 24, 2025
c441a56
feat: 영화 예약 API에 필요한 엔티티 추가
zhdiddl Jan 24, 2025
56b1505
refactor: 좌석 번호 값 객체 리팩토링
zhdiddl Jan 24, 2025
78ec165
feat: 예약 생성 시 검증에 필요한 Validation 추가
zhdiddl Jan 25, 2025
2e50fc0
feat: 저장소 포트 및 어댑터 추가
zhdiddl Jan 25, 2025
ad7edce
feat: 예약 생성 요청 및 응답 DTO 추가
zhdiddl Jan 25, 2025
54be6e6
refactor: 예약 생성 Validation 로직 개선
zhdiddl Jan 25, 2025
660f36b
refactor: Validation 포트 및 어댑터 추가로 서비스 간 참고 제거
zhdiddl Jan 25, 2025
4f7ef63
feat: 예약 생성 서비스 포트 및 구현체 추가
zhdiddl Jan 25, 2025
51f364c
fix: 예약 리포지토리 인터페이스 및 어댑터 수정
zhdiddl Jan 25, 2025
90f9eec
style: 코드 포맷팅 수정
zhdiddl Jan 25, 2025
7991a01
feat: 엔티티 추가에 따른 DDL 업데이트
zhdiddl Jan 25, 2025
3d95f66
refactor: 클라이언트 요청 값을 컨트롤러에서 검증하도록 책임 분리
zhdiddl Jan 25, 2025
b1d4dfe
fix: CustomException 메시지 처리 로직 수정
zhdiddl Jan 25, 2025
9556ab8
test: 회원 더미 데이터 생성 및 테스트 파일 추가
zhdiddl Jan 25, 2025
8f803bb
feat: 예약 완료 시 메시지 발송 기능 추가
zhdiddl Jan 25, 2025
8019bcd
refactor: 불필요한 필드 및 클래스 제거
zhdiddl Jan 25, 2025
b36f91c
test: 동시성 테스트 작성 및 관련 메서드 추가
zhdiddl Jan 25, 2025
7b3029f
refactor: 불필요한 코드 제거 및 가독성 개선
zhdiddl Jan 26, 2025
db53d5e
feat: 예약 서비스에 Pessimistic Lock 적용으로 동시성 해결
zhdiddl Jan 26, 2025
106e92b
refactor: 예약 생성 메서드 책임을 세부 메서드로 분리
zhdiddl Jan 26, 2025
ee48288
refactor: 데이터베이스 구조 리팩토링
zhdiddl Jan 27, 2025
93c14b7
refactor: 변경된 구조 기반으로 리포지토리 및 DTO 수정
zhdiddl Jan 27, 2025
5351541
refactor: 변경된 구조 기반으로 DDL 파일 수정
zhdiddl Jan 27, 2025
b9672af
feat: 예약 서비스에 Optimistic Lock 적용으로 동시성 해결
zhdiddl Jan 27, 2025
5a29de5
test: 동시성 테스트 작성 및 메서드명 수정
zhdiddl Jan 27, 2025
b77c6fb
feat: AOP 기반 분산 락 적용하여 동시성 제어
zhdiddl Jan 27, 2025
3382fb3
feat: 함수형 분산 락 적용하여 동시성 제어
zhdiddl Jan 27, 2025
cb2d0f2
fix: 트랜잭션 처리 및 분산 락 동작 개선
zhdiddl Jan 30, 2025
66b4601
refactor: 코드 가독성 개선 및 불필요한 메서드 제거
zhdiddl Jan 30, 2025
f82330d
feat: 예외 핸들러 및 예외 코드 추가
zhdiddl Jan 30, 2025
37b606f
refactor: 분산 락 예외 처리 및 해제 로직 개선
zhdiddl Jan 30, 2025
f35814a
refactor: 불필요한 메서드 호출 제거
zhdiddl Jan 30, 2025
e4e03cf
chore: Redis 타임아웃 및 재시도 설정 추가
zhdiddl Jan 30, 2025
be09082
chore: 빌드 설정 및 주석 수정
zhdiddl Jan 31, 2025
06aff06
chore: Redis 설정 추가
zhdiddl Feb 1, 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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,12 +458,14 @@ Hibernate:

# 예약 생성 API 추가
## 1. AOP 분산 락 적용 후
![img_aop.png](doc/img_aop.png)
![img.png](doc/img_aop.png)

- 트랜잭션 실행 시간이 500ms ~ 750ms 으로 관찰됨
- 이를 기반으로 leaseTime(잠금 유지 시간)을 실행 시간의 약 2배
- waitTime(대기 시간)은 잠금 유지 시간의 2배로 설정
- ⚠️ leaseTime 1.5초 + waitTime 3초 설정 시: 평균 응답 시간 9.68초
- ✅ leaseTime 1초 + waitTime 2초 설정으로 변경: 평균 응답시간 `7.79초`로 감소

## 2. 함수형 분산 락 적용 후
## 2. 함수형 분산 락 적용 후
![img.png](doc/img_functional.png)
- 함수형 적용 후 평균 응답 시간(http_req_duration)이 AOP보다 약 20% 개선
Binary file added doc/img_functional.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.application.lock;

import java.util.function.Supplier;

public interface DistributedLockExecutor {
<T> T executeWithLock(String key, long waitTime, long leaseTime, Supplier<T> task);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.application.lock;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
public class RedissonDistributedLockExecutor implements DistributedLockExecutor {

private final RedissonClient redissonClient;

public RedissonDistributedLockExecutor(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}

@Override
public <T> T executeWithLock(String key, long waitTime, long leaseTime, Supplier<T> task) {
Copy link

Choose a reason for hiding this comment

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

적절하게 잘 구현해주셨습니다 👍

RLock lock = redissonClient.getLock(key);
boolean acquired = false;

try {
acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new IllegalStateException("[ERROR] Unable to acquire lock for key: " + key);
}
return task.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("[ERROR] Lock acquisition interrupted", e);
} finally {
if (acquired) {
lock.unlock();
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.example.application.dto.request.ReservationRequestDto;
import com.example.application.dto.response.ReservationResponseDto;
import com.example.application.lock.DistributedLock;
import com.example.application.lock.DistributedLockExecutor;
import com.example.application.port.in.MessageServicePort;
import com.example.application.port.in.ReservationServicePort;
import com.example.application.port.out.MemberRepositoryPort;
Expand All @@ -20,11 +20,15 @@
import com.example.domain.model.entity.ScreeningSeat;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.LockAcquisitionException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@RequiredArgsConstructor
@Service
public class ReservationService implements ReservationServicePort {
Expand All @@ -38,8 +42,8 @@ public class ReservationService implements ReservationServicePort {
private final ReservationValidationPort reservationValidationPort;

private final MessageServicePort messageServicePort;
private final DistributedLockExecutor distributedLockExecutor;

@DistributedLock(key = "'seat_reservation:' + #request.screeningId + ':' + #request.seatIds.toString()")
@Transactional
@Override
public ReservationResponseDto create(ReservationRequestDto request) {
Expand All @@ -48,10 +52,21 @@ public ReservationResponseDto create(ReservationRequestDto request) {

List<ScreeningSeat> requestedSeats = validateReservationConstraints(screening, member, request.seatIds());

Reservation reservation = saveReservationAndSeats(screening, member, requestedSeats);
sendReservationConfirmation(member, requestedSeats, screening);

return ReservationResponseDto.fromEntity(reservation);
// 함수형 분산 락을 특정 메서드에 적용
String lockKey = "seat_reservation:" + request.screeningId() + ":" + request.seatIds();
long waitTime = 2000;
long leaseTime = 1000;
Copy link

Choose a reason for hiding this comment

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

평균 작업시간(500ms) �기준 고려해서 2배로 설정하신 것 같은데, GC가 발생하는 상황이라던지 긴 시간 대기가 발생할 수 있어 3초~5초 정도로 하는건 어떨지 의견드려봅니다.

Copy link
Author

Choose a reason for hiding this comment

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

해당 부분 반영해 leaseTime을 5초로 수정했습니다! waitTime은 leaseTime의 2배로 잡으면 10초가 되어 다소 길어지는 데, leaseTime과 동일하게 5초로 설정하는 것도 괜찮을까요?

Copy link

Choose a reason for hiding this comment

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

leaseTime 이 5초이긴 해서 보다 여유롭게 7초 정도로 해둬도 좋을 것 같습니다.


try {
return distributedLockExecutor.executeWithLock(lockKey, waitTime, leaseTime,
() -> {
Reservation reservation = saveReservationAndSeats(screening, member, requestedSeats);
return ReservationResponseDto.fromEntity(reservation);
});
} catch (LockAcquisitionException e) {
log.warn("[락 실패] 좌석 예약에 대한 락을 획득하지 못함 - Key: {}", lockKey);
throw new CustomException(ErrorCode.LOCK_ACQUISITION_FAILED, "좌석 예약 요청이 많아 처리가 지연되었습니다. 다시 시도해주세요.");
}
}

/** 상영 정보 조회 */
Expand Down Expand Up @@ -112,4 +127,5 @@ private void sendReservationConfirmation(Member member, List<ScreeningSeat> requ
requestedSeats.stream().map(ss -> ss.getSeat().getSeatNumber().toString()).collect(Collectors.joining(", ")),
screening.getMovie().getTitle(), screening.getTheater().getName(), screening.getStartTime()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public enum ErrorCode {
SEATS_NOT_CONSECUTIVE("Seats must be consecutive in the same row.", 400),
DUPLICATE_SEAT_REQUEST("Duplicate seat reservations are not allowed.", 400),
INVALID_REQUEST("Invalid request.", 400),

LOCK_ACQUISITION_FAILED("Lock acquisition failed.", 400),
;

private final String message;
Expand Down
Loading