발생한 문제(v1)

기존에는 위와 같이 공연장의 좌석을 하나의 테이블로 두었고, 좌석 하나당 하나의 행이 생기도록 하였다.
공연장 하나당 좌석이 1만개를 넘는 일도 흔한데, 이 방식에선 Seat 테이블에 행이 수십~수백만 개나 쌓일 수도 있었다. 그러면서도 서로 간에 중복된 데이터 비중이 많아 비효율적이라 생각했다.
그리고 각 좌석이 어디에 위치한 것인지 나타내기 위해 좌표 데이터를 같이 갖고 있게 했는데, 이 역시 굳이 다 저장을 해야 할까 싶은 비효율을 느꼈다.
우리가 규칙을 정해 저장하면 이런 식의 명시적인 데이터 저장을 동반하지 않고도 좌석 위치를 특정할 수 있겠다고 생각했다.
해결(v2)
압축식으로 개선한 스키마:

중복되는 데이터 저장을 줄이고, join 횟수를 줄이는 스키마를 위와 같이 재설계했다.
주요 변경 사항은:
- 좌석이 하나의 테이블로 관리되지 않고 구역(Section) 엔티티 내에 boolean 배열로 들어간다. 이를 통해 저장되는 데이터를 압축했다.
- 좌석의 실제 위치는 배열 상의 위치를 통해 계산할 수 있다. 따라서 배열 상의 순서가 중요한 의미를 갖는데, Place와 Section의 배열이 가진 순서와 Event의 배열이 가진 순서를 통일하는 규칙을 정의했다. 이를 통해 검색 소요를 줄이고 빠른 참조/수정 연산이 가능해진다.
자세한 설계 내용:
UX 컨셉:

멜론 티켓 서비스의 좌석 표현 방식을 일부 따라한다.
우측 전체 오버뷰를 보면 실제 물리적 구역은 곡선으로 휘어 있다. 그러나 멜론은 이를 모두 직사각형으로 근사하여 사용자에게 보여준다.
이는 디테일은 다소 떨어질 수 있어도 데이터를 크게 압축할 수 있게 해준다.

위와 같이 모든 좌석이 공연장을 둥글게 둘러 쌌을 때에도, 사진과 같이 구역을 나누면 또한 직사각형으로 근사할 수 있게 된다.
최악의 경우는 둥글게 둘러쌌으면서 그 규모가 매우 작을 때다. 이 때에는 구역을 나눠 직사각형으로 근사하기가 힘들어진다.
그러나 그런 경우는 정말 찾아볼 수 없을 정도다.
만약 그런 경우가 있다 하더라도, 구역 당 좌석이 적어서 불편할 순 있어도 결국 구현이 가능한 시스템이다.
좌석 저장 방식:

- Place
- Place는 공연장 전체 오버뷰(배경)를 표현하는 SVG 정보(FE 요구사항에 맞춰 가공해 저장)를 갖는다.
- Place는 Section의 리스트(=id 리스트)를 갖는다. 그리고 서로 다른 Place는 부분적으로 같은 Section을 참조할 수도 있다.
- 이는 실제로 같은 물리적 Place 내에서도 Section의 배치가 틀릴 수 있음을 반영한다.
- 또한 Section 정보를 비롯한 그 안의 좌석 배치 정보가 중복으로 저장되는 비효율을 방지한다.
- Section
- Section은 구역의 뷰를 표현하는 SVG 정보를 갖는다.
- Section은 UX 제공을 위해 구역 이름도 갖는다.(ex: ‘A구역’, ‘다구역’, ‘뭐시기존’)
- Section은 좌석 배치 정보를 스스로 갖는다.
- Event
- 좌석 현황을 2차원 배열로 스스로 갖는다.
- 가장 바깥 배열은 Section의 배열이다.
- Place의 sections 배열과 같은 순서로(index를 공유하도록) 구역들을 저장한다.
- 값은 Place의 sections 배열과 다르게, Section의 id가 아닌 좌석의 배열이다.
- 그 안쪽 배열은 좌석의 배열이다.
- Section의 seats 배열과 같은 순서로(index를 공유하도록) 좌석들을 저장한다.
- 값은 Section의 seats 배열과 다르게, BOOL이 아닌 TINYINT(1)로 ‘0(빈 칸)’, ‘1(예약 없음)’, ‘2(예약됨)’ 상태를 갖는다.
- TINYINT(1) 대신 ENUM도 가능하다. 하지만 MySQL DB 안팎으로 데이터가 오갈 때마다 변환이 필요해지기에, Redis와 Node.js 등 통합 시스템에서 통용할 수 있는 숫자 코드로 일단 정했다.
- Reservation
- 좌석의 이름(ex: A구역 B열 3번), 좌석의 좌표(Event의 seats 배열 내에서의 두 index)를 구매 매수만큼 배열로 갖는다.
- 좌석 렌더링 방식:
- Place에서 오버뷰 SVG와 섹션 리스트를 얻는다.
- 오버뷰 SVG, 섹션 각각의 SVG를 이용해 좌석 요약도를 그린다.
- 하나의 섹션이 클릭되면, 해당 섹션의 좌석 배열을 따라 좌석을 그린다.
- 좌석이 선택되면, 해당 섹션 이름(서버 제공)과 행/열 정보(
클라이언트 계산 응답에서 서버가 계산해 넘겨줌)를 나타낸다.
- 좌석 예약/취소 방식:
- 좌석이 선택되면,
섹션 index
, 좌석 index
를 포함한 요청을 보낸다.
- 서버는
섹션 index
를 seats의 1차원 인덱스로, 좌석 index
를 seats의 2차원 인덱스로 하여, Event의 seats를 수정한다.
- 좌석 현황 업데이트 방식:
- 서버가 Event의 seats 배열을 boolean 배열로 변환하여(
애초에 빈 칸인 좌석을 제거함. 클라이언트가 이 공백을 추론해야함. 애초에 빈 칸을 포함하여 예약 가능/불가능 여부로 bool값이 설정됨.) SSE를 발행한다.
- boolean 배열로 굳이 변환하는 이유:
- 성능적으로 가장 중요한 SSE 브로드캐스트 부분을 최대한 최적화하기 위함.
- 좌석의 개수가 일반적으로 많기 때문에 이 부분에서 생기는 차이가 유의미하리라 예상함.
- 클라이언트는 이미 가지고 있는 좌석 배치 정보를 기반으로, 수신한 seats 배열을 해석해 좌석 현황을 재렌더링한다.
- 예약 확정 방식:
- 유저가 선택한
섹션 index
를 통해 Section의 ‘구역 이름’을 얻는다.
- 유저가 선택한
좌석 index
를 통해 ‘좌석 이름’(ex: ‘B열 3번’)을 계산한다.
- 위 값들로 Reservation 엔티티를 생성하여 저장한다.
- 예약 확정 후 취소 방식:
- Reservation의 seats 배열이 가진 (
섹션 index
, 좌석 index
) 좌표로 Event의 seats에 접근하여 예약 상태를 변경한다.
추가 개선(v3)

주요 변경 사항: