-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#12] 좌석 선택 #13
[#12] 좌석 선택 #13
Conversation
- 예매 번호(PurchaseSerialNumber)는 임시 번호로 저장 - 티켓 수령 방법(ReceivingType) 타입 추가
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확인하였습니다. 아래 리뷰를 확인해주시고 동시성 제어 방식에 대해 어떻게 접근할지 코멘트로 남겨주세요~
- 테스트코드 작성 부탁드립니다.
동시성과 관련되어 종시성 이슈가 발생하지 않음을 보장하는 통합테스트를 작성해주세요. 동시성 테스트는 동시성을 제어하는 역할이 어디에 있느냐에 따라 테스트 작성법이 달라집니다.
하나의 어플리케이션 내부에서 동시성을 제어한다면 단위테스트로 작성이 가능하겠지만, DB나 외부 자원으로 동시성을 제어한다면 통합테스트를 진행해주세요.
- 동시성을 제어하는 로직이 없습니다.
트랜잭션의 격리 레벨을 낮춘다 해도 여러 스레드가 하나의 자원에 접근하여 여러가지 일을 한다면 동시성 이슈가 발생할 여지가 여전히 존재합니다. JPA + DB를 사용하여 제어한다면 아래 Lock을 사용할 수 있습니다.
JPA Pessimistic Locking
JPA Optimistic Locking
DB를 사용한다면 쉽게 제어가 가능하지만 그만큼 여러 단점과 리스크가 있습니다. DB 말고 다른 락이나 동시성 제어 방법도 한번 고려해보고 찾아보시겠어요? DB는 유저가 많아질 경우 데이터의 읽기/쓰기 리소스도 부족해질 여지가 있습니다.
ExceptionResponse exceptionResponse = ExceptionResponse.builder() | ||
.message(exception.getMessage()) | ||
.build(); | ||
return ResponseEntity.badRequest().body(exceptionResponse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UnavailablePurchaseException
, 예외에 실패했을 때 400번대 에러를 주는게 적절할까요? 400 에러는 어떤 상황을 뜻할까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
400번 에러는 클라이언트 요청에 문제가 있을 때 사용되는 코드입니다.
나머지 400번 대 에러도 클라이언트 측에 문제(권한이 없거나 URI가 틀렸을 때 등)가 있을 때 반환 됩니다.
이 경우는 요청은 정상적으로 처리 되었으나 해당하는 데이터가 없는 경우이므로 400번 대는 적절하지 않은 것 같습니다!
이 경우에는 클라이언트나 서버에 문제가 있는 상황이 아니라 비즈니스 상에서 필요한 예외 처리니까 200번으로 변경하려고 하는데 맞을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTTP Status code에 대한 문서를 보면 좋을것 같네요.
200은 요청이 성공적으로 수행되었음을 의미하기 때문에 예외상황에 적절하지 않아보입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아! 해당 예외는 예매 기간이 아니거나 예매 가능 매수가 초과했을 때 발생하는 예외이니, 403 Forbidden이 적절할 것 같습니다.
if (purchaseSeatRepository.existsBySeat(seat)) { | ||
throw new UnavailablePurchaseException("이미 선택된 좌석입니다.", SELECTED_SEAT, seat.getPerformanceDetail().getId()); | ||
} | ||
purchaseSeatRepository.save(PurchaseSeat.selectSeat(user, seat)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
동시성 이슈가 발생할 여지가 있네요.
동시성 이슈는 동일한 자원에 두가지 이상의 액션이 있고, 그 자원을 점유하지 못한 상황일 경우 발생할 여지가 있습니다.
이 코드에서는 아래 상황에서 동시성 이슈가 발생할 수 있습니다.
purchaseSeatRepository.existsBySeat(seat) // 이 때는 좌석이 있었다
// 아래 코드를 실행하기 전 다른 트랜잭션이 좌석을 이미 예매해 버렸다
purchaseSeatRepository.save(PurchaseSeat.selectSeat(user, seat)); // 이 코드를 실행하면 정해진 좌석보다 더 많은 좌석이 예매된다
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@AllArgsConstructor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
꼭 필요한 인자만을 이용해서 생성한다는 의미로 @RequiredArgsConstructor
를 사용하는건 어떠신가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아아 넵! 그게 더 명확한 것 같습니다. 수정했습니다:) 6c169e2
…ture/12-select-seat
- 유효성 검증(checkValidSeat) - 좌석 선택(SeatService.selectSeat(24f1127)
좌석 선택 기능 전체 Flow동시성 제어를 위해 분산 락을 적용하였습니다.좌석 점유 실패 케이스
|
} finally { | ||
validLocks.forEach(FencedLock::unlock); | ||
} | ||
|
||
if (validLocks.size() != fencedLocks.size()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lock을 일부만 획득했을 때, 해제해주는 로직 필요
- destory ➡️ unlock, - Map.value(userId ➡️ userId, thread id를 갖는 객체)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lock 해제 방식을 변경하였습니다.(7c13941)
변경 전
- 좌석 점유 시 좌석 PK와 유저 PK 쌍 저장
- 점유 좌석 데이터 만료 or 삭제 이벤트 발생 시 Lock 객체
destroy
변경 후
- 좌석 점유 시 좌석 PK와 점유 정보 객체(유저 PK, Lock을 획득한 Thread ID) 쌍 저장
- 점유 좌석 데이터 만료 or 삭제 이벤트 발생 시 Thread ID를 세팅하고
unlock
@@ -38,7 +37,6 @@ public class PurchaseFacade { | |||
* @throws UnavailablePurchaseException | |||
* @see SeatService#occupySeats(OccupySeatInfo) | |||
*/ | |||
@Transactional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좌석 점유 메서드(occupySeats
)는 DB 처리를 하지 않아 트랜잭션 설정을 제거했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다! 리뷰확인해주시고 간단히 수정해서 머지해주세요~
boolean isSuccessfullyOccupied = fencedLock.tryLock(time, unit); | ||
if (!isSuccessfullyOccupied) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
딱 한번만 사용하는 변수는 선언하지 않는 것이 좋습니다.
변수를 선언한다는 것은 이 변수의 생명주기 종료까지 코드를 읽을 때 변수를 염두해두어야 하기 때문에 변수 선언이 많아지면 가독성이 떨어질 수 있거든요.
예외적으로 변수가 "반드시 이름이 필요한 경우" 선언하는게 좋지만, 이 코드(fencedLock.tryLock(time, unit);
와 같이 메서드로 행위를 유추할 수 있는 경우에는 변수선언 없이 인라인 메서드로 처리하는게 좋지 않을까요?
boolean isSuccessfullyOccupied = fencedLock.tryLock(time, unit); | |
if (!isSuccessfullyOccupied) { | |
if (!fencedLock.tryLock(time, unit)) { |
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@AllArgsConstructor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
꼭 필요한 생성자 (final 변수)만으로 생성자를 생성한다는 의미로 @RequiredArgsConstructor
를 명시적으로 사용해주세요~
@DisplayName("요청 수가 100000일 때 무결성이 보장된다.") | ||
@Test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 대규모 테스트의 경우 실제 CI마다 테스트를 돌리면 락 서버에 부하가 많이 가기떄문에, 로컬에서만 테스트로 돌려보는 경우가 있습니다. 이 경우, disable
처리를 해주시면 전체 일괄테스트 구동시에 해당 테스트가 구동되지 않게 만들 수 있습니다,.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
꼼꼼한 테스트케이스 좋습니다~ 테스트코드도 많이 좋아졌네요
Quality Gate failedFailed conditions See analysis details on SonarCloud Catch issues before they fail your Quality Gate with our IDE extension SonarLint |
Summary
Issue Number
Describe changes
PURCHASE_SEAT
)에 저장된 데이터가 있는 지 체크하였습니다.PURCHASE_SEAT
과PURCHASE_INFO
를 생성하여 저장하였습니다.PURCHASE_SEAT
이 저장되었다면, commit 전이어도 선택된 좌석으로 판단되어야 해서 트랜잭션 격리 수준을READ_UNCOMMITTED
으로 설정했습니다.UnavailablePurchaseException
)는 사용되는 상황이 여러 가지로 나뉠 수 있어서 구체적으로 명시해주기 위해 필드를 추가하였습니다.(7964004)