요구사항 분석 - leonroars/slam GitHub Wiki
제공 기능
- 사용자 대기열 토큰 기능
- 예약 가능 날짜 조회 API
- 대기 상태 확인 API (요구 사항 분석 중 추가 도출)
- 지정 날짜 예약 가능 좌석 조회 API
- 좌석 예약 요청 API
- 결제(포인트 차감) API
- 포인트 충전 API
- 포인트 잔액 조회 API
서비스 운용 시나리오 설정
- 해당 서비스는 분산 환경에서의 운용을 전제합니다.
- 동시에 전달된 복수의 요청이 '순서대로' 처리될 것을 보장해야 합니다.
- 서비스 운영 주체는 '공연장'이라고 가정합니다.
즉, 정해진 좌석(1~50번)을 가지고 있고, 같은 공연장에서 여러 공연이 이루어지는 시나리오입니다.
기술적인 문제 해결 경험과 역량 강화에 집중하기 위해 '하루에 공연 한 개가 개최되는' 것으로 시나리오를 간략하게 설정했습니다.- 좌석 별 가격은 모두 동일하며, 별도의 할인 프로모션은 존재하지 않는다고 상정합니다.
- 하나의 좌석에 대해선 하나의 예약만 가능합니다.
- 1인 최대 한 개의 좌석에 대해서만 예약이 가능합니다.
- 결제가 이루어지지 않더라도, 현재 사용자가 '조회 중인 좌석'에 대해서는 5분 간 다른 사용자의 예약이 체결되지 않도록 합니다.
- 대기열은 '콘서트 일정' 단위로 관리됩니다. 즉, 공연이 다르거나 개최되는 날짜가 다른 두 공연의 대기열은 서로와 분리되어 관리됩니다.
서비스를 이용할 수 있는 권리를 표현하는 토큰 을 생성하여 발급합니다.
- 생성 시점 : 지정 날짜 예약 가능 좌석 조회 API 로의 요청이 애플리케이션에 진입하여 토큰 생성 모듈에 도달한 시점.
- 소멸(만료) 시점
- a. 결제 완료 시점
- b. 좌석에 대한 배타적 권리 유지 기간(좌석 예약 요청 API 요청 시점으로부터 5분) 만료 시.
- 서비스 사용 가능 여부를 판별케하는 정보
- 토큰을 통해 해당 토큰 보유한 사용자의 권한(서비스 진입 가능 / 불가) 판단 가능해야함.
- 해당 정보는 '본인의 진입 순번'일 수도 있고 단순히 '진입 가능 / 불가 판단케하는 정보'일 수 있다.
- 토큰 식별자 : 식별자를 이용해 해당 토큰의 서비스 진입 가능 여부를 효율적으로 판별 가능하도록 하기 위해 이러한 정보를 포함합니다.
이때 토큰 내에 담긴 정보가 '순번'일 경우 다음의 두 가지 문제가 발생 가능합니다.
- 해당 사용자의 차례 결정 책임이 서비스 로직과 토큰 자체의 내용 두 가지로 분할됩니다. 따라서 누군가가 본인의 토큰 상 순번을 변경할 경우 이를 판별하기 위한 복잡하고 불필요한 대책이 요구됩니다.
- 전체 n 개의 토큰 중 k 개의 토큰 보유 사용자를 진입시킬 경우, n-k 개의 토큰에 담긴 '순번'이 모두 수정되어야 합니다.
그렇기 때문에 토큰에 순번에 대한 정보를 담지 않고 오직 토큰의 식별과 관련한 정보만 포함하기로 합니다.
또한 해당 토큰의 상태(서비스 진입 가능 여부)는 별도의 저장소에 저장하고 서버에서 이를 판별하도록 함으로써,
오직 서버에 의해서만 사용자의 진입 가능 여부가 판별되도록 하여 서비스 무결성을 유지합니다.
토큰은 비즈니스 관점에서 다음의 두 가지를 보장함으로써 사용자가 체감하는 서비스 무결성의 중요한 요인으로서 존재합니다.
- 공정한 예매 순서 보장
- 부정한 사용자의 서비스 이용 제한을 제공함으로써 신뢰 가능한 서비스 제공
따라서 토큰 전송 방식은 두 가지 조건을 만족해야 합니다.
- 외부 노출 가능성 최소화
- 임의 조작 가능성 차단
이러한 위협 요소를 최소화하기 위한 방안으로 다음의 두 가지를 생각해볼 수 있습니다.
- 토큰을 요청 헤더가 아닌
httpOnly
쿠키에 담아 전송 - 배포 후 서비스 운영이
HTTPS
프로토콜 기반으로 이루어질 것을 고려해, 배포 후엔secure cookie
로 설정. - Cookie의 만료 시간을 설정하지 않음으로써 Session Cookie로서 전달되도록 구현.
- 사용자 브라우저 종료 상황을 대기열 이탈로 상정하고 이를 해제함으로써 즉, 토큰 이 외부에 무방비하게 노출되는 것을 방지하여 탈취로 인해 발생 가능한 문제에 대한 최소한의 방비책을 마련합니다.
토큰 은 다음 API에 대한 요청 시 함께 서버로 전달되어야 하며, 매번 검증이 이루어집니다.
- 지정 날짜 예약 가능 좌석 조회 API
- 좌석 예약 요청 API
- 요청 좌석 결제 API
- 기본적으로 사용자(클라이언트)는 '폴링' 방식으로 본인의 순번을 확인합니다.
따라서 이 폴링 요청에도 토큰이 포함되어야 합니다.
위에서 기술한 토큰 필요 API는 모두 '좌석 예약'이라는 작업 집합 원소에 대한 진입지점으로 생각해볼 수 있습니다.
토큰의 유효성 판별은 다음의 API에서 모두 시행됩니다.
- 지정 날짜 예약 가능 좌석 조회 API
- 좌석 예약 요청 API
- 요청 좌석 결제 API
- 대기 상태 확인 API
토큰의 유효성은 두 가지 기준으로 판별됩니다.
- 토큰이 저장소 내에 존재하는가?
- 서비스 이용 시 제출하는 토큰이 저장소 확인 결과 "활성화" 상태인가?
아래의 API는 두 가지 조건을 모두 만족했을 때 토큰이 유효하다고 판단합니다.
- 지정 날짜 예약 가능 좌석 조회 API
- 좌석 예약 요청 API
- 요청 좌석 결제 API
아래의 API는 '토큰의 저장소 내 존재 여부'만으로 유효성을 판단합니다.
- 대기 상태 확인 API
대기열의 세부 구현 방안은 별도의 문서로 작성합니다.
현 문서에서는 대기열의 비즈니스 로직 상 위치와 역할에 대한 분석만 진행합니다.
대기열은 '좌석에 대한 배타적 권리 확립 행위(예약)'가 어떤 규칙에 의해 이루어질 수 있도록 하기 위한 개념입니다.
현재 과제는 동시 다발적 사용자의 예약 요청이 '선착순'으로 처리될 것을 요구합니다.
이를 달성하기 위해 다음과 같은 정책 설정이 필요합니다:
- 선착순 결정 방식: 요청의 선후관계를 무엇을 기준으로 결정할 것인가?
현 과제에서는 요청의 선후관계를 토큰 생성 시점을 기준으로 판별하도록 설계 합니다.
-
서비스 진입 순서 반영 가능:
- 사용자가 서버 애플리케이션에 요청을 전송하면, '토큰 생성 서비스'를 통해 토큰이 생성됩니다.
- 이때 토큰 생성 시점이 사용자의 서비스 진입 순서를 결정하는 기준이 됩니다.
-
원자적 처리 보장:
- 토큰의 생성과 저장은 원자적으로 처리되므로, 저장소에 저장된 순서는 토큰의 생성 순서와 일치합니다.
- 이는 동시 요청 시에도 정확한 선후관계를 유지할 수 있게 합니다.
-
프레임워크의 스레드 관리 활용:
- Java Spring Framework는 요청을 처리하기 위해 스레드 풀을 사용하며, 스레드 풀의 크기 이상의 요청은 '선입선출 큐'에 적재됩니다.
- 이로 인해 요청의 처리 순서는 토큰 생성 시점과 일치하게 됩니다.
위의 이유들을 종합하면, 토큰의 생성 시점은 사용자의 서비스 진입 순서 결정 기준으로서 적합하다는 결론을 내릴 수 있습니다.
이와 같은 설계를 통해 동시 다발적 예약 요청의 공정한 선착순 처리를 보장함으로써, 서비스의 신뢰성을 확보할 수 있습니다.
-
토큰 생성과 검증은 비즈니스 로직과 직결되어 있으나 핵심이라고는 할 수 없습니다.
토큰 생성과 검증 부분을 제외한다고 하더라도 핵심 비즈니스 로직인 "콘서트 티켓(좌석) 예약" 이라는 서비스의 진행 자체가 불가능해지는 것이 아니기 때문입니다.- 따라서 대기열 서비스 로직 내에 구현하는 것이 아닌 별도의 분리된 클래스 형태로 구현 및 관리하는 방안이 적절할 것으로 사료됩니다.
- 또한, 토큰 생성 및 검증 로직과 이를 활용하는 비즈니스 로직 간 강결합을 지양하기 위해 인터페이스를 통한 의존이 되도록 구현함으로써 느슨한 결합 상태 를 만드는 것이 좋을 것으로 사료됩니다.
-
'최소한 독립된 패키지' 단위로 구현 및 관리하여 분할 필요 시 분할 작업이 효율적으로 진행될 수 있도록 합니다. 이유는 다음과 같습니다.
-
대기열은 DB로 구현할 예정입니다.
하지만 현재 기술 활용 추세를 보았을 때 Redis 혹은 Kafka 등의 독립적인 컴포넌트로 분할해 관리할 가능성이 높을 것으로 추정됩니다. - 비즈니스 로직을 위한 수단으로서 존재합니다. 즉, 도메인과 별도의 관심사를 가진다고 생각할 수 있습니다.
-
대기열은 DB로 구현할 예정입니다.
-
상기한 이유로, 구현 시엔 대기열 기능과 서비스의 다른 부분이 '인터페이스를 통한 느슨한 결합' 구조를 이루도록 구현합니다.
-
빠른 Production 을 위해 '일정한 단위의 사용자를 한 번에 서비스로 들여보내고, 한 번에 만료'시키는 설계로 진행하도록 합니다.
이는 작동 방식을 비교적 단순하게 설계함으로써 고객에게 빠르게 제품을 전달하고,
시장에 출시한 후 다수의 개선이 빠르고 간결하게 이루어질 수 있도록 함으로써 시장의 변화와 실 사용 환경에서 제품이 유연하게 대응할 수 있도록 하기 위함입니다.
- Cookie : None
- Body : 사용자 식별자, 공연 식별자
- Body : 특정 공연의 예약 가능 날짜 목록(yyyy-mm-dd UTC HH:MM:ss).
화면 상에서 예매하고자하는 공연을 선택할 경우, 해당 공연이 열리는 날짜가 사용자에게 표시됩니다.
- 이때 해당 날짜의 잔여 좌석이 없거나, 예매 시작 시점 이전인 경우 사용자가 선택할 수 없습니다.
- 잔여 좌석이 있는 경우 해당 날짜 선택이 가능합니다.
예약 가능 날짜를 선택할 경우, 클라이언트는 별도의 웹 브라우저 팝업 페이지를 생성하여 출력합니다.
이 페이지로부터 대기 상태 관리 API로 해당 날짜 정보와 사용자, 공연 식별자를 포함한 polling 요청이 전송됩니다.
- Cookie : None / 대기열 토큰 ID
- Query Param : 날짜(yyyy-mm-dd UTC HH:MM:ss)
- Body : 사용자 식별자, 공연 식별자
- 요청의 Cookie 필드가 비어있는 경우, 대기열 토큰을 생성하여 응답의 쿠키 필드에 담아 전송합니다.
- 차례인 경우 : 지정 날짜 예약 가능 좌석 조회 API 로 해당 요청 정보와 함께 Redirect.
- 차례가 아닌 경우 : 차례까지 남은 요청 수와, 아직 차례가 아님을 알리는 응답 전달.
클라이언트로부터 Polling 방식으로 시도되는 요청을 처리하는 API 입니다. 크게 두 가지 시나리오로 동작이 구분됩니다.
- A. 차례가 아닌 경우 : 서버에서는 예외가 발생하고 이는 실패 메세지 전달로 Handling 됩니다.
- B. 차례인 경우 : 대기열 토큰 ID와 사용자/공연 식별자, 날짜를 요청에 담아 지정 날짜 예약 가능 좌석 조회 API로 Redirect.
- 사용자의 재진입(브라우저 종료 후 다시 접속) 시 대처 방안
- 토큰을 생성하여 Client에게 응답 쿠키로 전달 시 expiration time 설정하지 않음으로써 Session Cookie 가 되도록 설계
- 브라우저 종료 시 해당 토큰은 브라우저 저장소에서 삭제되므로, 재진입시 새로운 토큰을 발급받아야 한다.
- 기존의 토큰은 이후 만료 스케쥴링 시 만료 처리되어 삭제된다.
- 사용자의 지나친 반복 요청으로 인한 서버 장애 예방 방안
-
Filter
Interface를 활용한 Rate Limiting 알고리즘을 구현하여,
요청이 Spring Container 도달 이전에 저지되도록 구현하는 것을 고려하고 있습니다.
-
- Cookie : 대기열 토큰 ID
- Query Parameter : 날짜
- Body : 사용자 식별자, 공연 식별자
- Body : 해당 날짜의 공연 좌석 중 예약 가능한 좌석 좌석 목록
해당 API 요청에는 '토큰'이 포함되어 있어야 합니다.
토큰이 유효하지 않을 경우 예외를 발생시키고 클라이언트는 다시 날짜 조회 페이지로 사용자를 Redirect 시킵니다.
예약 가능한 좌석을 선택한 경우, 클라이언트는 해당 좌석의 식별자를 포함하는 요청을 좌석 예약 요청 API로 전송합니다.
- Cookie : 대기열 토큰 식별자
- Request Body : 사용자 식별자, 콘서트 식별자, 요청 좌석 식별자
- 비즈니스 로직 상의 문제(일치 사용자 없음, 일치 콘서트 없음, 일치 좌석 없음, 토큰 유효하지 않음) 없을 경우 결제(포인트 차감) API 로 Redirect 됩니다.
요청 좌석에 대한 실제 예약 프로세스를 실행합니다.
이때 중요한 점은, 해당 좌석에 대한 예약 요청이 정상적인 것으로 판단될 경우, 해당 시점부터 5분 간 다른 사용자의 예약이 불가한 상태가 된다는 것입니다.
이를 구현할 수 있는 방안으로 '예약 요청이 들어온 좌석을 별도의 DB 테이블로 관리'하는 방안을 선택했습니다.
선택의 이유는 크게 두 가지 입니다.
- 이를 '가예약 좌석 관리'라는 별도의 관심으로 정의할 수 있기 때문입니다. 이렇게 할 경우, 기존의 좌석 데이터를 담은 테이블의 책임이 복잡해지는 것을 방지할 수 있습니다. 이를 바탕으로 차후 복수의 공연과 공연장에 대한 예약 기능 제공 시나리오로 서비스 고도화/확장 시 효율성 측면에서 유리할 것으로 판단했습니다.
- 또한 실제 서비스 고도화 시 이 관심사를 별도로 수평 확장(scale out)하기 용이할 것으로 판단했습니다.
이렇게 '가예약 상태'가 된 좌석은 다음의 상황에 의해 다시 '예약 가능 상태'가 됩니다.
- 해당 좌석 결제 완료 : 결제 완료 프로세스의 마지막에 해당 좌석에 대한 상태 변경도 함께 원자적으로 이루어지도록 구현합니다.
- '가예약 상태' 유지 시간 만료 : Spring Scheduler와 같은 애플리케이션 수준의 스케쥴러가 해당 테이블을 1분 단위로 순회하며 탐색 시점 기준 만료한 좌석의 상태를 '예약 가능'으로 바꾸어주고 해당 테이블에서 삭제합니다.
- Cookie : 대기열 토큰 식별자
- Request Body : 사용자 식별자, 공연 식별자, 공연 날짜, 좌석 식별자
- 잔액 >= 좌석 가격 : 예약 완료 처리 후 200 OK
- 잔액 < 좌석 가격 : 예약 실패를 알리는 응답 반환.
결제를 통해 해당 좌석에 대한 예약을 완료합니다. 이때 예약에 성공할 경우 다음의 연산이 원자적으로 이루어지도록 보장해야 합니다
- 결제 반영한 잔액 차감 프로세스
- 예약 좌석 상태 변경
- 예약 내역 생성
- 포인트 사용 내역 생성
잔액이 부족할 경우, 클라이언트 단의 설계/구현에 따라 해당 응답 수령 시 포인트 충전 페이지로 이동하거나 결제 실패 처리 후 다시 좌석 선택 페이지로 돌아갈 수 있습니다.
- Body : 사용자 식별자, 충전 희망 금액
- Body
- 충전 성공 시 : 사용자 식별자, 현재 잔액
- 충전 실패 시 : 이를 알리는 메세지 반환
- a. 일치 사용자 없음 : 실패 메세지
- b. 희망 금액 충전 시 최대 한도 초과할 경우 : 현 잔액 + 실패 메세지
사용자의 포인트를 충전합니다. 사용자의 보유 포인트 한도는 최대 1,000,000만점 입니다. 이를 초과한 충전 시도는 실패합니다. 이때 해당 사용자의 현재 잔액과 함께 실패 메시지를 반환합니다.
또한 해당 사용자가 존재하지 않을 경우에도 실패합니다.
- Body : 사용자 식별자
- Body : 사용자 식별자, 사용자 포인트 잔액.
포인트 잔액 조회 API입니다. 해당 사용자 존재하지 않는 경우 이를 알리는 메세지와 함께 실패 응답을 반환합니다.