영화 예매 시스템의 본질은 좌석이라는 한정된 자원을 여러 사용자에게 안전하게 할당하는 것이다. 이를 위해선 정확하고 일관된 데이터 모델이 필요하다.
이번 글에서는 실제 구현한 JPA 기반의 엔티티들을 중심으로, 도메인 요구사항을 어떻게 데이터 구조로 표현했는지 설명한다.
USERS - 사용자
MOVIES - 영화
IMAGES - 영화 이미지 (다중 이미지 대응)
THEATERS - 상영관
SEATS - 좌석 (행/번호 조합)
SCHEDULES - 영화 상영 일정
RESERVATIONS - 예매 정보
모든 테이블은 단방향 N:1 또는 1:N 관계로 설계되어 있고, 상태값 기반 흐름 제어, 좌석 중복 방지, 시간 순 정렬 등의 요구를 반영한다.
@Entity
@Table(name = "users")
class UserEntity(
@Id @GeneratedValue
var id: Long = 0,
var nickname: String,
var password: String
) : BaseTimeEntity()
BaseTimeEntity
를 통해 생성/수정 시간 관리@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
으로 명시해 단위를 명확히 함@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()
type(썸네일, 포스터, 스틸컷)
필드를 추가하면 캐러셀 형식도 가능LIMIT 1
또는 type = THUMBNAIL
조건 사용 가능@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
조합으로 실물 좌석을 표현@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()
@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
)DistributedLock
으로 분산 락 처리@Version
기반 낙관적 락 처리UNIQUE(schedule_id, seat_id)
제약 조건도 추가 가능createdAt
, startTime
등을 기준으로 정렬할 수 있도록 BaseTimeEntity
를 상속JOIN FETCH
또는 QueryDSL select new
로 필요한 필드만 가져옴movie_id
, schedule_id
, seat_id
등에는 인덱스 필수