Skip to content

Commit

Permalink
refactor: seatService 리팩터링 (#244)
Browse files Browse the repository at this point in the history
* fix: 좌석 잠금 만료 시간 seatService에서 설정할 수 있도록 로직 수정 및 SeatLockService -> SeatLockManager로 class명 수정

* refactor: BookingQueueRepository -> BookingQueueManager

* feat: Redis 명령어 래퍼 클래스 작성 -> RedisOperator

* refactor: bookingQueue rename Repository -> Manager, Service 리팩터링

* refactor: rename seatLock Service -> Manager, SeatService 리팩터링

* refactor: SeatLockManager RedisOperator 적용

* refactor: BookingService, EventSeatStatus 리팩터링

* chore

* fix: selectSeat 버그 수정

* feat: handleConnectException 추가

* fix: getOrderInQueue myOrder 계산식 수정
  • Loading branch information
annahxxl authored Jan 12, 2024
1 parent 1789f2c commit 7ba65fe
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 150 deletions.
2 changes: 0 additions & 2 deletions api/api-booking/http/booking.http
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,11 @@ Booking-Authorization: Bearer {{bookingToken}}

### 좌석 선택
POST http://localhost:8082/api/v1/seats/1/select
Content-Type: application/json
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

### 좌석 선택 해제
POST http://localhost:8082/api/v1/seats/1/deselect
Content-Type: application/json
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pgms.apibooking.common.exception;

import java.net.ConnectException;
import java.util.Objects;

import org.springframework.http.HttpHeaders;
Expand Down Expand Up @@ -85,4 +86,11 @@ protected ResponseEntity<ErrorResponse> handleSecurityCustomException(SecurityCu
BaseErrorCode errorCode = ex.getErrorCode();
return ResponseEntity.status(errorCode.getStatus()).body(errorCode.getErrorResponse());
}

@ExceptionHandler(ConnectException.class)
protected ResponseEntity<ErrorResponse> handleConnectException(ConnectException ex) {
log.error("ConnectException Occurred: {}", ex.getMessage());
BaseErrorCode errorCode = BookingErrorCode.SERVICE_UNAVAILABLE;
return ResponseEntity.status(errorCode.getStatus()).body(errorCode.getErrorResponse());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.pgms.apibooking.common.util;

import java.time.Duration;
import java.util.function.Supplier;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import com.pgms.apibooking.common.exception.BookingException;
import com.pgms.coredomain.domain.common.BookingErrorCode;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
@RequiredArgsConstructor
public class RedisOperator {

private final RedisTemplate<String, String> redisTemplate;

public void setIfAbsent(String key, String value, Integer expirationSeconds) {
Duration timeout = Duration.ofSeconds(expirationSeconds);
tryOperation(() -> redisTemplate.opsForValue().setIfAbsent(key, value, timeout));
}

public String get(String key) {
return tryOperation(() -> redisTemplate.opsForValue().get(key));
}

public void delete(String key) {
tryOperation(() -> redisTemplate.delete(key));
}

public void addToZSet(String key, String value, double score) {
tryOperation(() -> redisTemplate.opsForZSet().add(key, value, score));
}

public Long getRankFromZSet(String key, String value) {
return tryOperation(() -> redisTemplate.opsForZSet().rank(key, value));
}

public void removeElementFromZSet(String key, String value) {
tryOperation(() -> redisTemplate.opsForZSet().remove(key, value));
}

public void removeRangeByScoreFromZSet(String key, double minScore, double maxScore) {
tryOperation(() -> redisTemplate.opsForZSet().removeRangeByScore(key, minScore, maxScore));
}

private <T> T tryOperation(Supplier<T> operation) {
try {
return operation.get();
} catch (Exception e) {
log.error("RedisManager occurred: " + e.getMessage(), e);
throw new BookingException(BookingErrorCode.SERVICE_UNAVAILABLE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
import com.pgms.apibooking.domain.booking.dto.response.BookingsGetResponse;
import com.pgms.apibooking.domain.booking.dto.response.PageResponse;
import com.pgms.apibooking.domain.booking.repository.BookingQuerydslRepository;
import com.pgms.apibooking.domain.bookingqueue.repository.BookingQueueRepository;
import com.pgms.apibooking.domain.bookingqueue.service.BookingQueueManager;
import com.pgms.apibooking.domain.payment.dto.request.PaymentCancelRequest;
import com.pgms.apibooking.domain.payment.dto.request.RefundAccountRequest;
import com.pgms.apibooking.domain.payment.service.PaymentService;
import com.pgms.apibooking.domain.seat.service.SeatLockService;
import com.pgms.apibooking.domain.seat.service.SeatLockManager;
import com.pgms.coredomain.domain.booking.Booking;
import com.pgms.coredomain.domain.booking.BookingCancel;
import com.pgms.coredomain.domain.booking.Payment;
Expand Down Expand Up @@ -60,8 +60,8 @@ public class BookingService { //TODO: 테스트 코드 작성
private final TicketRepository ticketRepository;
private final MemberRepository memberRepository;
private final BookingQuerydslRepository bookingQuerydslRepository;
private final BookingQueueRepository bookingQueueRepository;
private final SeatLockService seatLockService;
private final BookingQueueManager bookingQueueManager;
private final SeatLockManager seatLockManager;
private final PaymentService paymentService;
private final TossPaymentConfig tossPaymentConfig;

Expand Down Expand Up @@ -174,7 +174,7 @@ public BookingGetResponse getBooking(String id, Long memberId) {

@Async
protected void removeSessionIdInBookingQueue(Long eventId, String tokenSessionId) {
bookingQueueRepository.remove(eventId, tokenSessionId);
bookingQueueManager.remove(eventId, tokenSessionId);
}

private EventTime getBookableTimeWithEvent(Long timeId) {
Expand All @@ -189,12 +189,7 @@ private EventTime getBookableTimeWithEvent(Long timeId) {
}

private List<EventSeat> getBookableSeatsWithArea(Long timeId, List<Long> seatIds, Long memberId) {
seatIds.forEach(seatId -> {
Long selectorId = seatLockService.getSelectorId(seatId);
if (selectorId == null || !selectorId.equals(memberId)) {
throw new BookingException(BookingErrorCode.UNBOOKABLE_SEAT_INCLUSION);
}
});
checkHeldSeats(seatIds, memberId);

List<EventSeat> seats = eventSeatRepository.findAllWithAreaByTimeIdAndSeatIds(timeId, seatIds);

Expand All @@ -209,6 +204,15 @@ private List<EventSeat> getBookableSeatsWithArea(Long timeId, List<Long> seatIds
return seats;
}

private void checkHeldSeats(List<Long> seatIds, Long memberId) {
seatIds.forEach(seatId -> {
Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null);
if (selectorId == null || !selectorId.equals(memberId)) {
throw new BookingException(BookingErrorCode.UNBOOKABLE_SEAT_INCLUSION);
}
});
}

private void validateDeliveryAddress(ReceiptType receiptType, Optional<DeliveryAddress> deliveryAddress) {
if (receiptType == ReceiptType.DELIVERY) {
if (deliveryAddress.isEmpty()) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.pgms.apibooking.domain.bookingqueue.service;

import java.util.Optional;

import org.springframework.stereotype.Component;

import com.pgms.apibooking.common.util.RedisOperator;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class BookingQueueManager {

private final RedisOperator redisOperator;

public void add(Long eventId, String sessionId, double currentTimeSeconds) {
redisOperator.addToZSet(String.valueOf(eventId), sessionId, currentTimeSeconds);
}

public Optional<Long> getRank(Long eventId, String sessionId) {
Long rank = redisOperator.getRankFromZSet(String.valueOf(eventId), sessionId);
return Optional.ofNullable(rank);
}

public void remove(Long eventId, String sessionId) {
redisOperator.removeElementFromZSet(String.valueOf(eventId), sessionId);
}

public void removeRangeByScore(Long eventId, double minScore, double maxScore) {
redisOperator.removeRangeByScoreFromZSet(String.valueOf(eventId), minScore, maxScore);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import com.pgms.apibooking.domain.bookingqueue.dto.response.OrderInQueueGetResponse;
import com.pgms.apibooking.domain.bookingqueue.dto.response.SessionIdIssueResponse;
import com.pgms.apibooking.domain.bookingqueue.dto.response.TokenIssueResponse;
import com.pgms.apibooking.domain.bookingqueue.repository.BookingQueueRepository;
import com.pgms.coredomain.domain.common.BookingErrorCode;

import lombok.RequiredArgsConstructor;
Expand All @@ -22,27 +21,32 @@
@RequiredArgsConstructor
public class BookingQueueService {

private final BookingQueueRepository bookingQueueRepository;
private final static double MILLISECONDS_PER_SECOND = 1000.0;
private final static double TIMEOUT_SECONDS = 7 * 60;
private final static long ENTRY_LIMIT = 5;

private final BookingQueueManager bookingQueueManager;
private final BookingJwtProvider bookingJwtProvider;

public void enterQueue(BookingQueueEnterRequest request, String sessionId) {
double currentTimeSeconds = System.currentTimeMillis() / 1000.0;
bookingQueueRepository.add(request.eventId(), sessionId, currentTimeSeconds);
double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
bookingQueueManager.add(request.eventId(), sessionId, currentTimeSeconds);
}

public OrderInQueueGetResponse getOrderInQueue(Long eventId, String sessionId) {
Long myOrder = getMyOrder(eventId, sessionId);
Boolean isMyTurn = isMyTurn(eventId, sessionId);
Long order = getOrder(eventId, sessionId);
Long myOrder = order <= ENTRY_LIMIT ? 0 : order - ENTRY_LIMIT;
Boolean isMyTurn = isReadyToEnter(eventId, sessionId);

double currentTimeSeconds = System.currentTimeMillis() / 1000.0;
double timeLimitSeconds = currentTimeSeconds - (7 * 60);
bookingQueueRepository.removeRangeByScore(eventId, 0, timeLimitSeconds);
double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS;
bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds);

return OrderInQueueGetResponse.of(myOrder, isMyTurn);
}

public TokenIssueResponse issueToken(TokenIssueRequest request, String sessionId) {
if(!isMyTurn(request.eventId(), sessionId)) {
if (!isReadyToEnter(request.eventId(), sessionId)) {
throw new BookingException(BookingErrorCode.OUT_OF_ORDER);
}

Expand All @@ -52,23 +56,23 @@ public TokenIssueResponse issueToken(TokenIssueRequest request, String sessionId
return TokenIssueResponse.from(token);
}

private Boolean isMyTurn(Long eventId, String sessionId) {
Long myOrder = getMyOrder(eventId, sessionId);
Long entryLimit = bookingQueueRepository.getEntryLimit();
private Long getOrder(Long eventId, String sessionId) {
return bookingQueueManager.getRank(eventId, sessionId)
.orElseThrow(() -> new BookingException(BookingErrorCode.NOT_IN_QUEUE));
}

private Boolean isReadyToEnter(Long eventId, String sessionId) {
Long myOrder = getOrder(eventId, sessionId);
Long entryLimit = ENTRY_LIMIT;
return myOrder <= entryLimit;
}

public void exitQueue(BookingQueueExitRequest request, String sessionId) {
bookingQueueRepository.remove(request.eventId(), sessionId);
bookingQueueManager.remove(request.eventId(), sessionId);
}

public SessionIdIssueResponse issueSessionId() {
UUID sessionId = UUID.randomUUID();
return SessionIdIssueResponse.from(sessionId.toString());
}

private Long getMyOrder(Long eventId, String sessionId) {
return bookingQueueRepository.getRank(eventId, sessionId)
.orElseThrow(() -> new BookingException(BookingErrorCode.NOT_IN_QUEUE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.pgms.apibooking.domain.seat.service;

import java.util.Optional;

import org.springframework.stereotype.Component;

import com.pgms.apibooking.common.util.RedisOperator;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class SeatLockManager {

private final static String SEAT_LOCK_CACHE_KEY_PREFIX = "seatId:";
private final static String SEAT_LOCK_CACHE_VALUE_PREFIX = "memberId:";

private final RedisOperator redisOperator;

public Optional<Long> getSelectorId(Long seatId) {
String key = generateSeatLockKey(seatId);
String value = redisOperator.get(key);
return Optional.ofNullable(value == null ? null : extractMemberId(value));
}

public void lockSeat(Long seatId, Long memberId, Integer expirationSeconds) {
String key = generateSeatLockKey(seatId);
String value = generateSeatLockValue(memberId);
redisOperator.setIfAbsent(key, value, expirationSeconds);
}

public void unlockSeat(Long seatId) {
redisOperator.delete(generateSeatLockKey(seatId));
}

private String generateSeatLockKey(Long seatId) {
return SEAT_LOCK_CACHE_KEY_PREFIX + seatId;
}

private String generateSeatLockValue(Long memberId) {
return SEAT_LOCK_CACHE_VALUE_PREFIX + memberId;
}

private Long extractMemberId(String value) {
return Long.parseLong(value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, ""));
}
}
Loading

0 comments on commit 7ba65fe

Please sign in to comment.