Skip to content

Commit

Permalink
fix: 좌석 선택 시 토큰 세션 아이디 기준으로 선점하도록 수정 (#248)
Browse files Browse the repository at this point in the history
* chore: 더미데이터 추가

* refactor: 큐 클랜징 로직 리팩터링

* fix: 좌석 선 택 시 토큰 세션 아이디 기준으로 선점하도록

* chore
  • Loading branch information
annahxxl authored Jan 14, 2024
1 parent fec4735 commit e983558
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 56 deletions.
Binary file added api/.DS_Store
Binary file not shown.
14 changes: 14 additions & 0 deletions api/api-booking/http/booking.http
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ POST http://localhost:8082/api/v1/seats/1/deselect
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

### 예매 생성
POST http://localhost:8082/api/v1/bookings/create
Content-Type: application/json
Authorization: Bearer {{accessToken}}
Booking-Authorization: Bearer {{bookingToken}}

{
"timeId": 1,
"seatIds": [1],
"receiptType": "현장수령",
"buyerName": "빙봉",
"buyerPhoneNumber": "010-1234-5678"
}

### 예매 ~ 결제 승인 (브라우저에서 진행해 주세요)
GET http://localhost:8082/bookings

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public ResponseEntity<ApiResponse<BookingCreateResponse>> createBooking(
@RequestAttribute("tokenSessionId") String tokenSessionId,
HttpServletRequest httpRequest) {
BookingCreateResponse createdBooking = bookingService.createBooking(request, memberId, tokenSessionId);
ApiResponse<BookingCreateResponse> response = ApiResponse.ok(createdBooking);
ApiResponse<BookingCreateResponse> response = ApiResponse.created(createdBooking);
URI location = UriComponentsBuilder
.fromHttpUrl(httpRequest.getRequestURL().toString())
.path("/{id}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public class BookingService { //TODO: 테스트 코드 작성
public BookingCreateResponse createBooking(BookingCreateRequest request, Long memberId, String tokenSessionId) {
Member member = getMemberById(memberId);
EventTime time = getBookableTimeWithEvent(request.timeId());
List<EventSeat> seats = getBookableSeatsWithArea(request.timeId(), request.seatIds(), memberId);
List<EventSeat> seats = getBookableSeatsWithArea(request.timeId(), request.seatIds(), tokenSessionId);

ReceiptType receiptType = ReceiptType.fromDescription(request.receiptType());
validateDeliveryAddress(receiptType, request.deliveryAddress());
Expand Down Expand Up @@ -188,8 +188,8 @@ private EventTime getBookableTimeWithEvent(Long timeId) {
return time;
}

private List<EventSeat> getBookableSeatsWithArea(Long timeId, List<Long> seatIds, Long memberId) {
checkHeldSeats(seatIds, memberId);
private List<EventSeat> getBookableSeatsWithArea(Long timeId, List<Long> seatIds, String tokenSessionId) {
checkHeldSeats(seatIds, tokenSessionId);

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

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

private void checkHeldSeats(List<Long> seatIds, Long memberId) {
private void checkHeldSeats(List<Long> seatIds, String sessionId) {
seatIds.forEach(seatId -> {
Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null);
if (selectorId == null || !selectorId.equals(memberId)) {
String selectorId = seatLockManager.getSelectorId(seatId).orElse(null);
if (selectorId == null || !selectorId.equals(sessionId)) {
throw new BookingException(BookingErrorCode.UNBOOKABLE_SEAT_INCLUSION);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class BookingQueueService {

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 static long ENTRY_LIMIT = 2;

private final BookingQueueManager bookingQueueManager;
private final BookingJwtProvider bookingJwtProvider;
Expand All @@ -34,19 +34,17 @@ public void enterQueue(BookingQueueEnterRequest request, String sessionId) {
}

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

double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS;
bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds);

Boolean isMyTurn = order <= ENTRY_LIMIT;
Long myOrder = isMyTurn ? 0 : order - ENTRY_LIMIT;
return OrderInQueueGetResponse.of(myOrder, isMyTurn);
}

public TokenIssueResponse issueToken(TokenIssueRequest request, String sessionId) {
if (!isReadyToEnter(request.eventId(), sessionId)) {
Long order = getOrder(request.eventId(), sessionId);

if (order > ENTRY_LIMIT) {
throw new BookingException(BookingErrorCode.OUT_OF_ORDER);
}

Expand All @@ -61,12 +59,6 @@ private Long getOrder(Long eventId, String 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) {
bookingQueueManager.remove(request.eventId(), sessionId);
}
Expand All @@ -75,4 +67,13 @@ public SessionIdIssueResponse issueSessionId() {
UUID sessionId = UUID.randomUUID();
return SessionIdIssueResponse.from(sessionId.toString());
}

/*
* 대기열에 존재하는 세션 중 타임아웃된 세션을 제거한다.
*/
private void cleanQueue(Long eventId) {
double currentTimeSeconds = System.currentTimeMillis() / MILLISECONDS_PER_SECOND;
double timeLimitSeconds = currentTimeSeconds - TIMEOUT_SECONDS;
bookingQueueManager.removeRangeByScore(eventId, 0, timeLimitSeconds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.pgms.apibooking.domain.seat.dto.request.SeatsGetRequest;
import com.pgms.apibooking.domain.seat.dto.response.AreaResponse;
import com.pgms.apibooking.domain.seat.service.SeatService;
import com.pgms.coredomain.response.ApiResponse;
import com.pgms.coresecurity.security.resolver.CurrentAccount;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -38,15 +38,21 @@ public ResponseEntity<ApiResponse<List<AreaResponse>>> getSeats(@ModelAttribute

@Operation(summary = "좌석 선택")
@PostMapping("/{seatId}/select")
public ResponseEntity<Void> selectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) {
seatService.selectSeat(seatId, memberId);
public ResponseEntity<Void> selectSeat(
@PathVariable Long seatId,
@RequestAttribute("tokenSessionId") String tokenSessionId
) {
seatService.selectSeat(seatId, tokenSessionId);
return ResponseEntity.noContent().build();
}

@Operation(summary = "좌석 선택 해제")
@PostMapping("/{seatId}/deselect")
public ResponseEntity<Void> deselectSeat(@PathVariable Long seatId, @CurrentAccount Long memberId) {
seatService.deselectSeat(seatId, memberId);
public ResponseEntity<Void> deselectSeat(
@PathVariable Long seatId,
@RequestAttribute("tokenSessionId") String tokenSessionId
) {
seatService.deselectSeat(seatId, tokenSessionId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
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 static String SEAT_LOCK_CACHE_VALUE_PREFIX = "sessionId:";

private final RedisOperator redisOperator;

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

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

Expand All @@ -37,11 +37,11 @@ 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 String generateSeatLockValue(String tokenSessionId) {
return SEAT_LOCK_CACHE_VALUE_PREFIX + tokenSessionId;
}

private Long extractMemberId(String value) {
return Long.parseLong(value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, ""));
private String extractSessionId(String value) {
return value.replace(SEAT_LOCK_CACHE_VALUE_PREFIX, "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ public List<AreaResponse> getSeats(SeatsGetRequest request) {
.toList();
}

public void selectSeat(Long seatId, Long memberId) {
Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null);
public void selectSeat(Long seatId, String tokenSessionId) {
String selectorId = seatLockManager.getSelectorId(seatId).orElse(null);

if (selectorId != null) {
if (selectorId.equals(memberId)) {
if (selectorId.equals(tokenSessionId)) {
return;
}
throw new BookingException(BookingErrorCode.SEAT_HELD_BY_ANOTHER_MEMBER);
Expand All @@ -52,18 +52,18 @@ public void selectSeat(Long seatId, Long memberId) {
}

seat.updateStatus(EventSeatStatus.HOLDING);
seatLockManager.lockSeat(seatId, memberId, SEAT_LOCK_CACHE_EXPIRE_SECONDS);
seatLockManager.lockSeat(seatId, tokenSessionId, SEAT_LOCK_CACHE_EXPIRE_SECONDS);
}

public void deselectSeat(Long seatId, Long memberId) {
Long selectorId = seatLockManager.getSelectorId(seatId).orElse(null);
public void deselectSeat(Long seatId, String tokenSessionId) {
String selectorId = seatLockManager.getSelectorId(seatId).orElse(null);

if (selectorId == null) {
updateSeatStatusToAvailable(seatId);
return;
}

if (!selectorId.equals(memberId)) {
if (!selectorId.equals(tokenSessionId)) {
throw new BookingException(BookingErrorCode.SEAT_HELD_BY_ANOTHER_MEMBER);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ void setup() {
Optional.empty()
);

given(seatLockManager.getSelectorId(any(Long.class))).willReturn(Optional.of(member.getId()));
given(seatLockManager.getSelectorId(any(Long.class))).willReturn(Optional.of(SESSION_ID));
doNothing().when(bookingQueueManager).remove(any(Long.class), any(String.class));

// when
Expand Down Expand Up @@ -243,7 +243,7 @@ void setup() {
);

given(seatLockManager.getSelectorId(seat1.getId())).willReturn(Optional.empty());
given(seatLockManager.getSelectorId(seat2.getId())).willReturn(Optional.of(member.getId() + 1));
given(seatLockManager.getSelectorId(seat2.getId())).willReturn(Optional.of(SESSION_ID));

// when & then
assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID))
Expand Down Expand Up @@ -295,7 +295,7 @@ void setup() {
Optional.empty()
);

given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId()));
given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID));

// when & then
assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID))
Expand Down Expand Up @@ -349,7 +349,7 @@ void setup() {
Optional.empty()
);

given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId()));
given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID));

// when & then
assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID))
Expand Down Expand Up @@ -401,7 +401,7 @@ void setup() {
Optional.empty()
);

given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId()));
given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID));

// when & then
assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID))
Expand Down Expand Up @@ -453,7 +453,7 @@ void setup() {
Optional.empty()
);

given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(member.getId()));
given(seatLockManager.getSelectorId(seat.getId())).willReturn(Optional.of(SESSION_ID));

// when & then
assertThatThrownBy(() -> bookingService.createBooking(request, member.getId(), SESSION_ID))
Expand Down
16 changes: 10 additions & 6 deletions core/core-infra/src/main/resources/data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ VALUES (100000, 'S', 1),

-- EventSeat
INSERT INTO event_seat (name, status, event_seat_area_id, event_time_id)
VALUES ('A1', 'AVAILABLE', 1, 1)
, ('A2', 'AVAILABLE', 1, 1)
, ('A3', 'AVAILABLE', 1, 1)
, ('E1', 'AVAILABLE', 2, 1)
, ('E2', 'AVAILABLE', 2, 1)
, ('E3', 'AVAILABLE', 2, 1);
VALUES ('A1', 'AVAILABLE', 1, 1),
('A2', 'AVAILABLE', 1, 1),
('A3', 'AVAILABLE', 1, 1),
('A4', 'AVAILABLE', 1, 1),
('A5', 'AVAILABLE', 1, 1),
('E1', 'AVAILABLE', 2, 1),
('E2', 'AVAILABLE', 2, 1),
('E3', 'AVAILABLE', 2, 1),
('E4', 'BOOKED', 2, 1),
('E5', 'BOOKED', 2, 1);

-- EventReview
INSERT INTO event_review (score, content, event_id)
Expand Down
6 changes: 5 additions & 1 deletion db/initdb.d/2-data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ INSERT INTO event_seat (name, status, event_seat_area_id, event_time_id)
VALUES ('A1', 'AVAILABLE', 1, 1),
('A2', 'AVAILABLE', 1, 1),
('A3', 'AVAILABLE', 1, 1),
('A4', 'AVAILABLE', 1, 1),
('A5', 'AVAILABLE', 1, 1),
('E1', 'AVAILABLE', 2, 1),
('E2', 'AVAILABLE', 2, 1),
('E3', 'AVAILABLE', 2, 1);
('E3', 'AVAILABLE', 2, 1),
('E4', 'BOOKED', 2, 1),
('E5', 'BOOKED', 2, 1);

-- EventReview
INSERT INTO event_review (score, content, event_id)
Expand Down

0 comments on commit e983558

Please sign in to comment.