반응형
개요
- 공연 티케팅 예매 프로젝트 진행 중, 공연의 좌석에 대해 중복된 예약을 제한해야하는 요구사항이 존재했습니다. 이를 Database의 Unique Constraint를 사용하여 구현하였는데, 어떻게 구현하였는지 공유하고자 합니다.
요구사항, Entity 설명
- 먼저 요구사항은 아래와 같습니다.
로그인된 사용자는 공연의 자리를 선택하여 장바구니에 추가할 수 있습니다. 이 때, 다른 사람이 이미 예약한 좌석은 예매할 수 없습니다.
- 다음으로 엔티티를 보겠습니다. 엔티티는 아래와 같습니다.
class Cart(
@Column(unique = true, updatable = false)
val uid: String,
@ManyToOne
@JoinColumn(name = "seat_id")
val seat: PerformancePlaceSeat,
@ManyToOne
@JoinColumn(name = "performance_datetime_id")
val performanceDateTime: PerformanceDateTime,
@ManyToOne
@JoinColumn(name = "user_id")
val user: User,
) : BaseEntity()
class PerformanceDateTime(
@Column(unique = true, updatable = false)
val uid: String,
val showTime: ZonedDateTime,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "performance_id")
val performance: Performance,
) : BaseEntity() {
fun checkPassed(time: ZonedDateTime = ZonedDateTime.now()) {
if (showTime.isBefore(time)) {
throw BusinessIllegalStateException(PerformanceErrorInfos.PERFORMANCE_ALREADY_PASSED)
}
}
}
@Entity
@Table(name = "performance_place_seats")
class PerformancePlaceSeat(
@Column(unique = true, updatable = false)
val uid: String,
val rowNum: Int,
val columnNum: Int,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "place_id")
val place: PerformancePlace
) : BaseEntity()
- 먼저 공연은 다수의 공연 날짜를 가질 수 있습니다. 예시는 아래와 같습니다.
- 공연 : A 뮤지컬
- 공연 날짜 : 9월 21일, 9월 22일
- 이 때 공연이 같더라도, 날짜가 다르면 같은 좌석이라도 예매가 정상적으로 수행되어야 합니다. 따라서 예약(Cart)의 동일성은 아래의 조건을 만족하면 같은 예약이라고 볼 수 있습니다.
- 같은 공연, 같은 날짜
- 같은 좌석
- 여기서 공연 날짜 엔티티는 공연 엔티티를 연관 관계를 맺고 있기 때문에 Cart 엔티티는 같은 공연 날짜, 같은 좌석이 같으면 같은 것이라고 볼 수 있습니다.
Unique Constraint로 중복 예약 막기
- 그러면 이것을 어떻게 처리할 수 있을까? 라고 고민하다 Cart가 FK로 가지고 있는 공연 날짜 PK, 좌석 PK를 Unique Constraint로 걸어 처리하면 가능하지 않을까? 하고 시도를 해보았습니다.
@Entity
@Table(
name = "carts",
uniqueConstraints = [UniqueConstraint(
name = "ux_seat_uid_date_uid",
columnNames = ["seat_id", "performance_datetime_id"]
)]
)
class Cart(
@Column(unique = true, updatable = false)
val uid: String,
@ManyToOne
@JoinColumn(name = "seat_id")
val seat: PerformancePlaceSeat,
@ManyToOne
@JoinColumn(name = "performance_datetime_id")
val performanceDateTime: PerformanceDateTime,
@ManyToOne
@JoinColumn(name = "user_id")
val user: User,
) : BaseEntity()
- 이렇게 Java단에서 Unique Constraint를 설정해 줄 수 있는데, 이러면 ddl-auto로 DDL을 생성하면 아래와 유사한 구조의 테이블이 생성될 것입니다.
CREATE TABLE carts (
uid VARCHAR(255) NOT NULL UNIQUE,
seat_id INT,
performance_datetime_id INT,
user_id INT,
PRIMARY KEY (uid),
FOREIGN KEY (seat_id) REFERENCES performance_place_seat(id),
FOREIGN KEY (performance_datetime_id) REFERENCES performance_date_time(id),
FOREIGN KEY (user_id) REFERENCES user(id),
UNIQUE KEY ux_seat_uid_date_uid (seat_id, performance_datetime_id)
);
- 이렇게 하면 중복 예약을 시도한다면 DataIntegrityViolationException를 throw할 것이고, 이를 catch해 유저에게 이미 예약된 좌석이라고 정보를 주면 될 것입니다. 실제 저장하는 로직을 보겠습니다.
fun reserve(
userUid: String,
performanceUid: String,
dateUid: String,
seatUid: String
) {
// ...
if (reservationReader.isReservationExists(seatUid, dateUid)) {
throw DuplicatedException(OrderErrorInfos.ALREADY_RESERVED)
}
// ...
saveProcess(user, performanceDateTime, seat)
}
private fun saveProcess(
user: User,
performanceDateTime: PerformanceDateTime,
seat: PerformancePlaceSeat
) {
try {
cartWriter.save(Cart(NanoIdGenerator.createNanoId(), seat, performanceDateTime, user))
} catch (e: DataIntegrityViolationException) {
throw DuplicatedException(OrderErrorInfos.ALREADY_RESERVED)
}
}
- 위 코드를 보면, 먼저 1차적으로 예약이 존재하는지 확인을 하고, 그 다음 예약(Cart) Entity를 저장하기를 시도합니다. 그리고 catch로 DataIntegrityViolationException를 잡아줘 이미 예약되었다는 Exception을 throw합니다.
반응형
'Spring > Ticketing 프로젝트' 카테고리의 다른 글
공연 정보 조회 API 쿼리 분석하고 개선하기 (0) | 2024.11.15 |
---|---|
Spring + Grafana, Loki, Prometheus로 모니터링 시스템 구축하기 (0) | 2024.11.15 |
비동기 환경에서 Request를 유지하려면 어떻게 해야할까? (0) | 2024.11.15 |
N + 1 쿼리 개선하고 확장성 있는 코드 만들기 (0) | 2024.11.15 |
테스트 코드 개선하기 (1) | 2024.11.15 |