순간적으로 트래픽이 몰리는 케이스를 방어하기위해, 대기열 기능을 만들었다. (대용량 트래픽 노이로제...직업병같다..)
우선 대기열이란, 미리 정한 특정 TPS 즉 초당 사용자의 접근을 제어하여 정해진 TPS이상으로는 시스템에서는 사용자의 활동을 제한하여, 서비스의 폭주를 막는 좋은(?) 기능이다.
대용량 트래픽이 예상되는 서비스에서는 필수적인 기능이라고 해도 과언이 아니다
(이렇게 해도 죽기는 하는건.. 함정 ㅋ.. 하지만 더 안정적으로 버틴다의 관점으로 봐야한다.)
대기열을 만들기 위해서는 크게 3가지 개념을 알고있어야한다.
1) 대기열큐
2) 대기표
3) 락
이 개념만 확실히 알고있으면, 크게 어려울것은 없지만, 동시성과 락을 사용하는것이기에, 개인에 따라서, 어려운 개념일수도있다.
우선 내가 만든 대기열은 어댑티브 적응형(?) 버전이라고 해야할듯한데..
1) 1분마다 시스템의 로드(부하)를 프로메테스에서 수집하여 평균 로드를 구한다.
2) 해당 평균로드가 특정 로드이상일때, 대기열이 자동으로 활성화된다. (예: 70%이상)
3) 대기열이 활성화될때, 클라이언트에게 HttpStatus code를 429 Too Many Requests로 내려주게하고, 해당 code를 받으면, 클라이언트는 대기열 페이지를 즉시 띄우게 한다.
4) 해당 대기열 페이지에서 tryEnter api를 통해 대기열에 들어가고, 실제 입장이 가능한지 여부를 서버에게 주기적으로 폴링을 통해 응답을 받는다. (서버가 입장이 가능하다는 응답을 주면, 클라이언트는 실제 앱기능으로 진입하도록 한다)
전체 코드가 아니지만, 흐름을 알수있는 도메인서비스 코드로 설정한다.
아직 완벽한 코드는 아니고, 1차적인 코드임을 감안하자..
(실제로 테스트 돌리기가 힘들어서..쩝 - 좀더 다듬긴 해야할듯)
//대기열 등록 및 입장 처리
fun tryEnter(waitingTicketId: String, openWaitingQueueCondition:() -> Boolean) : AppWaitingQueueInfo {
//1.대기열 닫힘이면 (부하떨어지면) 바로 모두 Accept
if (!openWaitingQueueCondition()) {
return alwaysAcceptInfo
}
//2.대기표가 있지 않으면 표발급.
if (!appWaitingQueueRepository.contains(waitingTicketId)) {
appWaitingQueueRepository.enter(waitingTicketId)
return alwaysNoAcceptInfo
}
//3.만료된 대기티켓을 만료처리하고, 대기열에서 제외시킴.
var nextTicketId: String
for (i in 0..MAX_CHECK_EXPIRE_TICKETS) {
val head = appWaitingQueueRepository.next() ?: return alwaysAcceptInfo
if (appWaitingQueueRepository.isExpireTickets(head) == YesOrNoEnum.Y) {
appWaitingQueueRepository.leave(head)
continue
} else {
nextTicketId = head
break
}
}
//4.접근권한획득 (접근가능대기자인지 확인해서 맞으면, 권한획득을 시도해야함)
val accept:YesOrNoEnum = if (nextTicketId == waitingTicketId) {
if (appWaitingQueueRepository.tryLockAcquire() == YesOrNoEnum.Y) {
if (appWaitingQueueRepository.leave(waitingTicketId)) {
YesOrNoEnum.Y
} else {
appWaitingQueueRepository.releaseLockAcquire()
YesOrNoEnum.N
}
} else YesOrNoEnum.N
} else YesOrNoEnum.N
return AppWaitingQueueInfo(
accept = accept,
rank = if (accept == YesOrNoEnum.Y) 0 else appWaitingQueueRepository.getRank(waitingTicketId) ?: 1,
waitingQueueSize = appWaitingQueueRepository.size() + 1
)
}
코드 설명을 해보면,
1번 코드 블럭 설명
- 대기열 닫힘이면 (부하떨어지면) 대기열을 통해 입장을 할 필요가 없기에 바로 모두 사용자를 입장을 시켜준다.
2번 코드 블럭 설명
- 대기표를 발급받지 않은 상태(최초 호출)에는 대기표를 발급해준다.
(대기표는 사용자 이탈을 처리하기 위해, 만료시간이 존재한다 예: 10초)
- 대기표를 발급받은 사용자는 바로 입장여부(아래 코드블록)을 확인하지 않고 응답을 주고 빠져나가게 한다.
이렇게 함으로써, 폴링 주기안에 다른 사용자가 들어올수있도록하여, 대기표를 받고 바로 입장해버리는 경우를 막도록 한다.
(공정성, 락획득 분산의 이유 - 장단점이 있지만...)
3번 코드 블럭 설명
- 대기표를 받고 입장을 하지않는 경우(대기표화면에서 사용자 이탈, 앱종료등등)는 해당 코드블럭을 통해, 대기열에서 제거를 해준다.
- 사용자의 이탈을 서버는 ACTIVE하게 알수가 없다. 브라우저 이탈, 앱 종료등을 완벽히 서버에게 앱이 전달해 줄수는 없기 때문이다.
따라서 폴링 API에서 해당 만료 티켓을 종료해주는 로직을 넣어서, 처리하게 해준다.
- MAX_CHECK_EXPIRE_TICKETS 만큼만 동작하게하여, 무한루프에 빠지는 부분을 방어해준다.(while(true)보다는 안정적이다)
4번 코드 블럭 설명
- 실제 대기열에 먼저들어온순으로 대기열에 티켓정보를 내 티켓정보와 확인한다.
- 내 티켓과 대기열의 입장 가능 티켓이 동일하다면, 세마포어 입장 락을 획득하고, 해당 락을 획득하면, 이제 입장이 가능하기에, 대기열과 티켓정보를 제거하고 입장여부는 Y로 설정하여 클라이언트에게 응답을 내려준다.
내가 만든 일반적인 대기열과 조금 다른 부분이 있다.
1) ticketId를 랜덤생성하지 않고, 그앱의 유니크한 ID를 클라이언트에게 전달받는 방식을 사용하고, 티겟발급과 만료티켓처리 그리고 입장가능여부를 하나의 API로 통합하였다.
이유는 API를 분리하고, 별도로 클라이언트가 호출을 하는 복잡도를 줄이기 위해서이다.
(분리의 근거는 비즈니스 로직의 흐름이 상당히 연관성이 있기에, 하나의 API로 묶는게 이상하지 않다고 판단한것도 있다)
2) 대기열의 큐에서 입장 우선순위 티켓을 꺼낼때, pop을 사용하지 않고 peak를 사용하였다.
pop으로 큐에서 아예 꺼내버리면, 입장가능 티켓이 아닌경우, 다시 큐에 넣어야하는 불필요한 행위를 반복하게 되기때문이다.
따라서, peak를 통해 큐에서 꺼내지않고, 조회만 하여, 입장가능 티켓인 경우에만 pop을 통해, 제거를 하도록 하여, 불필요한 반복을 줄였다.
3) 코드전박적으로 입장권한 획득을 위한 세마포어를 제외하고, 동시성에 따른 락을 사용하지 않는 구조로 만들었다.
분산락,락을 사용하면 성능이 떨어지기에 최대한 락을 사용하지 않는 구조로 만들었다. 다행히 브라우저의 대기열이 아니였기에, 요청하는 사용자를 특정지을수있는 유니크한 ID를 쉽게 휙득할수있기에 해당 ID를 티켓ID로 전달받아 로직을 구현하도록 만들었다.
(웹서비스였으면, 힘들었지..ㅠ)
테스트 코드를 작성해서 실제 잘동작하는지를 확인할예정이다.
그리고 현재는 대기열 활성 기준을 cpu로드만 기준으로 하고있는데, network, memory등 전반적인 지표를 포함하여 기준을 잡도록 하면,
더 멋있는 대기열 기능이 되지않을까?...생각만...ㅋ
'MY아이디어' 카테고리의 다른 글
| [아이디어] 삼성,엘지 가전 제어, 어떻게 할까? (0) | 2026.07.09 |
|---|---|
| [아이디어] 16년된 게임개발 아이디어... (0) | 2026.05.11 |
| [아이디어] 스크래핑 내재화에 도전하다 (5) | 2026.05.05 |
| [아이디어] 테슬라 데이터 수집 - 테슬라가 빌런이다. (4) | 2026.04.25 |
| [아이디어] javafx로 OsTray영역에 텍스트 넣기 (0) | 2026.04.19 |
