[설계] 예매 시스템의 데이터 구조

y001·2025년 4월 14일
0
post-thumbnail

영화 예매 시스템의 본질은 좌석이라는 한정된 자원을 여러 사용자에게 안전하게 할당하는 것이다. 이를 위해선 정확하고 일관된 데이터 모델이 필요하다.
이번 글에서는 실제 구현한 JPA 기반의 엔티티들을 중심으로, 도메인 요구사항을 어떻게 데이터 구조로 표현했는지 설명한다.


1. 전체 구조 요약 (ERD)

USERS          - 사용자
MOVIES         - 영화
IMAGES         - 영화 이미지 (다중 이미지 대응)
THEATERS       - 상영관
SEATS          - 좌석 (행/번호 조합)
SCHEDULES      - 영화 상영 일정
RESERVATIONS   - 예매 정보

모든 테이블은 단방향 N:1 또는 1:N 관계로 설계되어 있고, 상태값 기반 흐름 제어, 좌석 중복 방지, 시간 순 정렬 등의 요구를 반영한다.


USERS – 사용자 테이블

@Entity
@Table(name = "users")
class UserEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    var nickname: String,
    var password: String
) : BaseTimeEntity()
  • ID 기준으로 예약 이력 추적
  • 향후 OAuth 기반 인증도 쉽게 확장 가능
  • BaseTimeEntity를 통해 생성/수정 시간 관리

MOVIES – 영화 정보

@Entity
@Table(name = "movies")
class MovieEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    var title: String,
    @Enumerated(EnumType.STRING)
    var genre: Genre,
    var rating: String,
    var releaseDate: LocalDate,
    var runningTimeMin: Int,
    @Enumerated(EnumType.STRING)
    var status: MovieStatus
) : BaseTimeEntity()
  • Genre, MovieStatus는 Enum으로 제한된 값을 가지도록 설계
  • 상영 여부는 status 필드를 기준으로 조회 캐싱 처리
  • 러닝타임은 runningTimeMin으로 명시해 단위를 명확히 함

IMAGES – 영화 이미지

@Entity
@Table(name = "images")
class ImageEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "movie_id")
    var movie: MovieEntity,
    var url: String
) : BaseTimeEntity()
  • 하나의 영화에 여러 이미지가 붙을 수 있도록 설계 (1:N)
  • 추후 type(썸네일, 포스터, 스틸컷) 필드를 추가하면 캐러셀 형식도 가능
  • 썸네일만 필요한 경우는 LIMIT 1 또는 type = THUMBNAIL 조건 사용 가능

THEATERS / SEATS – 극장 및 좌석

@Entity
@Table(name = "theaters")
class TheaterEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    var name: String,
    var totalSeats: Int
) : BaseTimeEntity()

@Entity
@Table(name = "seats")
class SeatEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    @ManyToOne(fetch = FetchType.LAZY)
    var theater: TheaterEntity,
    var seatRow: String,
    var seatNumber: Int
) : BaseTimeEntity()
  • 좌석은 seatRow + seatNumber 조합으로 실물 좌석을 표현
  • 예약은 좌석 ID로 연결되지만, 뷰에서는 좌석 행/번호 기준으로 출력
  • 특정 극장의 좌석 배치 구성에도 대응 가능

SCHEDULES – 상영 일정

@Entity
@Table(name = "schedules")
class ScheduleEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    @ManyToOne(fetch = FetchType.LAZY)
    var movie: MovieEntity,
    @ManyToOne(fetch = FetchType.LAZY)
    var theater: TheaterEntity,
    var startTime: LocalDateTime
) : BaseTimeEntity()
  • 영화 + 극장 조합으로 상영 일정을 생성
  • 시간 단위로 예약 가능 여부 판단 가능
  • 예약 캐시 및 스케줄별 예매 분산에 활용

RESERVATIONS – 예매 정보

@Entity
@Table(name = "reservations")
class ReservationEntity(
    @Id @GeneratedValue
    var id: Long = 0,
    @ManyToOne(fetch = FetchType.LAZY)
    var user: UserEntity,
    @ManyToOne(fetch = FetchType.LAZY)
    var schedule: ScheduleEntity,
    @ManyToOne(fetch = FetchType.LAZY)
    var seat: SeatEntity,
    @Enumerated(EnumType.STRING)
    var status: ReservationStatus,
    @Version
    var version: Long = 0
) : BaseTimeEntity()
  • 중복 예약 방지를 위해 schedule_id + seat_id 조합으로 유일성 확보
  • @Version을 통한 낙관적 락 적용 → 단일 인스턴스에서도 중복 방지 가능
  • 상태값 기반 흐름 제어 (PENDING, CONFIRMED, CANCELED)

동시성 제어와의 연결

  • 멀티 인스턴스 환경에서는 Redis 기반 DistributedLock으로 분산 락 처리
  • 단일 인스턴스에서는 @Version 기반 낙관적 락 처리
  • DB 수준에서는 UNIQUE(schedule_id, seat_id) 제약 조건도 추가 가능

정렬 / 조회 최적화 포인트

  • createdAt, startTime 등을 기준으로 정렬할 수 있도록 BaseTimeEntity를 상속
  • 조회 쿼리는 JOIN FETCH 또는 QueryDSL select new로 필요한 필드만 가져옴
  • movie_id, schedule_id, seat_id 등에는 인덱스 필수

0개의 댓글