호스팅 콘솔을 만들 때 가장 먼저 부딪힌 벽은 서버 생성이 느리다는 사실이었습니다. 결제는 몇 초면 끝나는데 EC2 인스턴스는 요청을 넣고 실행 상태가 되기까지 1분에서 2분이 걸립니다. 이 긴 작업을 결제를 처리하는 요청 안에서 그대로 기다리면 서버리스 함수든 일반 라우트 핸들러든 타임아웃에 걸립니다. 그래서 서버 생성은 결제와 떼어 내 백그라운드로 돌리는 것이 출발점이었습니다. 이 글은 자체 서비스인 호스팅 콘솔을 만들며 프로비저닝을 어떻게 워커로 옮겼는지, 그리고 중간에 실패했을 때 어떻게 리소스를 정리했는지를 정리합니다.
왜 결제 요청 안에서 서버를 만들면 안 되는가
결제가 끝나는 순간 서버를 만들고 싶은 마음은 자연스럽습니다. 사용자 입장에서는 결제하면 곧바로 서버를 받는 흐름이 가장 깔끔하니까요. 하지만 EC2 인스턴스를 띄우는 일은 한 번의 호출로 끝나지 않습니다. 이미지를 조회하고 키페어를 만들고 보안 그룹을 열고 인스턴스를 실행한 뒤 그 인스턴스가 정말 켜졌는지 기다렸다가 고정 IP를 붙여야 합니다. 이 연쇄를 다 합치면 수십 초에서 몇 분입니다.
그 시간을 결제 응답이 붙잡고 있으면 두 가지가 무너집니다. 하나는 요청 자체가 시간제한에 걸려 실패하는 것이고 다른 하나는 사용자가 결제 화면에서 하염없이 기다리는 것입니다. 그래서 결제가 성공하면 구독 기록만 먼저 원자적으로 만들고 서버 생성은 작업 큐에 던진 뒤 화면은 즉시 응답하도록 나눴습니다. 무거운 일은 뒤에서 워커가 이어받습니다.
프로비저닝 워커가 하는 일
작업 큐에는 BullMQ를 썼습니다. 결제가 끝나면 구독 아이디와 서버 기록 아이디, 인스턴스 타입, 사용자 정보를 담은 작업이 프로비저닝 큐에 들어가고 워커가 이를 하나씩 집어 실제 AWS 리소스를 만듭니다. 워커는 동시에 두 건까지 처리하도록 제한을 두어 한꺼번에 많은 주문이 몰려도 AWS 호출이 폭주하지 않게 했습니다.
프로비저닝 자체는 순서가 정해진 여덟 단계의 시퀀스입니다. 먼저 최신 Amazon Linux 2023 이미지 아이디를 조회하고 Ed25519 키페어를 만들며 돌려받은 개인 키를 곧바로 암호화합니다. 그다음 보안 그룹을 만들어 SSH 포트를 열고 인스턴스를 실행한 뒤 실행 상태가 될 때까지 최대 5분을 기다립니다. 마지막으로 고정 IP를 할당해 인스턴스에 붙입니다. 이 모든 단계가 끝나면 워커는 서버 기록에 인스턴스 아이디와 공인 IP, 각종 리소스 식별자를 채우고 상태를 실행 중으로 바꿉니다.
작업이 성공적으로 끝나면 서버 준비 완료 메일을 보냅니다. 다만 이 메일 발송은 프로비저닝의 성패와 분리해 두었습니다. 메일이 실패해도 서버는 이미 만들어졌으니 작업 자체를 실패로 되돌리면 안 되기 때문입니다. 그래서 메일 발송은 별도의 예외 처리로 감싸 실패하면 로그만 남기고 넘어갑니다.
중간에 실패하면 리소스가 새어 나간다
프로비저닝에서 가장 신경 쓴 지점은 성공이 아니라 실패였습니다. 여덟 단계 중 뒤쪽에서 실패하면 앞서 만든 리소스가 그대로 남습니다. 키페어와 보안 그룹은 남아 있어도 큰 비용이 아니지만 실행된 인스턴스와 할당된 고정 IP는 방치하면 계속 요금이 붙습니다. 결제는 됐는데 서버는 못 받고 우리 쪽 AWS 요금만 새는 최악의 조합이 될 수 있습니다.
그래서 워커의 실패 처리기에 리소스 정리를 붙였습니다. BullMQ는 작업을 정해진 횟수만큼 재시도하는데, 모든 재시도가 소진된 최종 실패에서만 정리를 실행합니다. 재시도가 남아 있는 중간 실패에서 리소스를 지워 버리면 다음 시도가 아무것도 없는 상태에서 처음부터 다시 만들어야 하니까요. 최종 실패라고 판단되면 그때 서버 기록에 저장된 인스턴스 아이디와 키페어 이름, 보안 그룹 아이디, 고정 IP 할당 아이디를 읽어 하나씩 정리합니다.
정리 함수는 최선 노력 방식으로 짰습니다. 인스턴스를 종료하고 고정 IP를 반납하고 보안 그룹과 키페어를 지우는 각 단계를 서로 독립적으로 감쌌습니다. 한 단계가 실패해도 예외를 던지지 않고 로그만 남긴 뒤 다음 단계를 계속합니다. 인스턴스 종료가 완전히 반영되기 전에는 그 인스턴스가 물고 있던 보안 그룹을 지울 수 없어 잠깐 기다렸다가 나머지를 정리하도록 순서도 맞췄습니다. 모든 정리가 끝나면 서버 기록을 실패 상태로 표시해 운영자가 무엇이 잘못됐는지 알 수 있게 남깁니다.
이 설계가 준 것
프로비저닝을 워커로 옮기고 나니 결제 흐름이 단순해졌습니다. 결제 요청은 자기 일만 하고 끝나며 서버 생성이라는 느리고 실패 가능한 작업은 재시도와 정리 로직을 갖춘 별도 공간으로 격리됐습니다. 결제와 인프라를 한 요청에 묶었다면 부분 실패를 다루기가 훨씬 까다로웠을 것입니다. 지금은 결제는 트랜잭션으로, 프로비저닝은 재시도 가능한 작업으로 각자의 방식대로 실패를 다룹니다.
이 프로젝트의 전체 그림과 결제, 정기결제, 생명주기까지 이어지는 구조는 SaaS 운영 콘솔 구축기에 정리해 두었습니다. 매달 도는 정기결제를 어떻게 무인으로 만들었는지는 스케줄러 없는 결제사로 정기결제 만들기에서 이어집니다.
자주 묻는 질문
왜 서버 생성을 결제와 분리했나요?
EC2 인스턴스가 실행 상태에 도달하기까지 1분에서 2분이 걸리는데 이 시간을 결제 요청이 기다리면 타임아웃이 나기 때문입니다. 결제가 성공하면 구독만 먼저 만들고 서버 생성은 작업 큐로 넘겨 백그라운드 워커가 처리하도록 나누면, 화면은 즉시 응답하고 무거운 작업은 재시도와 정리 로직을 갖춘 곳에서 안전하게 돌릴 수 있습니다.
프로비저닝 도중 실패하면 만들던 서버는 어떻게 되나요?
BullMQ가 작업을 재시도하고 모든 재시도가 소진된 최종 실패에서만 정리를 실행합니다. 이때 서버 기록에 저장해 둔 인스턴스, 고정 IP, 보안 그룹, 키페어를 되짚어 지우고 서버 상태를 실패로 표시합니다. 각 정리 단계는 서로 독립적이라 하나가 실패해도 나머지는 계속 진행돼 리소스가 방치되는 일을 줄입니다.
동시에 여러 주문이 들어오면 감당이 되나요?
워커의 동시 처리 수를 두 건으로 제한해 한꺼번에 많은 주문이 몰려도 AWS API 호출이 폭주하지 않게 했습니다. 큐에 쌓인 나머지 작업은 순서대로 처리되므로 주문이 몰려도 유실되지 않고 차례를 기다립니다.