구독 생명주기를 상태 머신으로 설계하기

웹 · 앱 개발5분 읽기

구독형 서비스를 만들다 보면 결제보다 까다로운 것이 그 뒤의 생명주기 관리입니다. 결제가 성공하면 활성이지만 결제가 밀리면 연체가 되고 계속 밀리면 정지되며 결국 종료되어 서버까지 정리되어야 합니다. 고객이 직접 해지를 누르면 또 다른 흐름이 시작됩니다. 이 여러 갈래를 조건문 몇 개로 흩어 두면 곧 손댈 수 없는 코드가 됩니다. 자체 호스팅 콘솔을 만들며 구독의 일생을 상태 머신으로 못 박고 그 전이를 매일 도는 워커가 실행하게 만든 과정을 정리합니다.

다섯 개의 상태와 갈 수 있는 길

구독의 상태를 다섯 가지로 정의했습니다. 활성, 연체, 정지, 해지 예정, 종료입니다. 각 상태의 의미는 이렇습니다. 활성은 정상적으로 결제되어 서버가 돌아가는 상태이고 연체는 이번 청구가 실패해 유예 기간에 들어간 상태입니다. 정지는 유예 기간이 지나도록 결제되지 않아 서버를 멈춘 상태이며 해지 예정은 고객이 해지를 신청했지만 이미 낸 기간이 남아 그때까지는 서비스를 유지하는 상태입니다. 종료는 모든 것이 정리된 마지막 상태입니다.

중요한 것은 이 상태들 사이를 아무렇게나 오갈 수 없다는 점입니다. 어떤 전이는 자연스럽지만 어떤 전이는 있어서는 안 됩니다. 예를 들어 이미 종료된 구독이 다시 활성으로 돌아가면 안 되고 정지된 구독이 곧바로 활성이 되려면 반드시 결제라는 사건을 거쳐야 합니다. 그래서 각 상태에서 갈 수 있는 다음 상태의 목록을 하나의 표로 못 박았습니다. 활성은 연체나 해지 예정으로, 연체는 다시 활성이 되거나 정지로, 정지는 활성이 되거나 종료로, 해지 예정은 종료로만 갈 수 있고 종료는 어디로도 가지 않는 마지막 상태입니다.

잘못된 전이를 코드가 막게 한다

이 표를 그냥 문서로만 두면 지켜지지 않습니다. 그래서 전이가 유효한지 확인하는 함수를 만들어 상태를 바꾸는 모든 지점이 이 함수를 거치게 했습니다. 어떤 상태에서 어떤 상태로 가려는지를 넣으면 그 전이가 허용된 목록에 있는지 검사해 참과 거짓을 돌려줍니다. 허용되지 않은 전이를 시도하면 그 자리에서 막히거나 예외를 던집니다.

이 작은 장치가 준 안정감이 컸습니다. 생명주기 로직이 여기저기서 상태를 바꾸더라도 모두 같은 관문을 통과하니, 종료된 구독이 되살아나거나 활성 구독이 갑자기 정지되는 식의 어긋난 상태 변화가 원천적으로 걸러집니다. 상태 전이 규칙이 흩어진 조건문이 아니라 한곳에 모인 표와 검증 함수로 존재하므로, 나중에 규칙을 바꿀 때도 그 표만 고치면 됩니다.

매일 도는 생명주기 워커

상태를 정의했으면 누군가는 실제로 그 전이를 일으켜야 합니다. 연체가 정지로, 정지가 종료로 넘어가는 것은 시간이 흐르며 자동으로 일어나야 하는 일입니다. 이 역할을 매일 도는 생명주기 워커에 맡겼습니다. 정기결제 점검이 아침에 청구를 처리하고 나면 한 시간 뒤 생명주기 워커가 돌면서 결제 실패의 결과를 이어받아 정리합니다.

생명주기 워커가 매일 확인하는 것은 세 가지입니다. 연체된 지 일정 기간이 지난 구독은 정지 대상으로, 정지된 지 더 오래 지난 구독은 종료 대상으로, 그리고 고객이 해지를 신청했고 이미 낸 기간이 끝난 구독도 종료 대상으로 뽑습니다. 뽑은 구독마다 개별 작업을 만들어 큐에 넣고 각 작업은 실제 전이를 실행하기 전에 앞서 만든 검증 함수로 그 전이가 유효한지 다시 한번 확인합니다. 목록을 뽑은 시점과 실제로 처리하는 시점 사이에 상태가 바뀌었을 수 있으니 마지막 순간에 다시 검사합니다.

정지와 종료는 서버까지 건드린다

상태 전이는 데이터베이스 값만 바꾸는 일이 아닙니다. 구독이 정지되면 서버 기록을 멈춤 상태로 표시하고 종료되면 실제 AWS 리소스까지 정리해야 합니다. 다만 여기에 단계를 두었습니다. 정지 시점에는 서버를 곧바로 없애지 않고 멈춤 상태로만 표시합니다. 연체 고객이 곧 결제하면 되살려야 하니 유예를 둡니다. 정지 상태가 충분히 오래 이어져 종료로 넘어갈 때 비로소 인스턴스와 고정 IP, 보안 그룹, 키페어를 실제로 정리합니다.

종료 처리에서 리소스 정리가 부분적으로 실패하더라도 데이터베이스의 상태 갱신은 진행하도록 했습니다. AWS 정리가 한두 단계 어긋났다고 구독이 계속 종료되지 못하고 남아 있으면 그것대로 문제이기 때문입니다. 정리 실패는 로그로 남겨 운영자가 뒤처리할 수 있게 하고 구독 자체는 종료로 확정합니다. 이렇게 상태의 정합성과 인프라의 정리를 분리해 인프라 쪽 잡음이 구독 상태 관리를 막지 않도록 했습니다.

정리하며

구독 생명주기를 상태 머신으로 다룬 뒤로 이 부분의 코드가 예측 가능해졌습니다. 상태와 전이 규칙이 한곳에 모여 있고 실제 전이는 검증을 거쳐야만 일어나며 시간에 따른 변화는 매일 도는 워커가 책임집니다. 조건문으로 흩어 두었다면 새 상태를 추가하거나 유예 기간을 조정할 때마다 온 코드를 뒤졌을 텐데, 지금은 표 하나와 워커 하나를 보면 됩니다.

결제 실패가 어떻게 연체를 만드는지는 스케줄러 없는 결제사로 정기결제 만들기에서, 종료 시 실제 서버 리소스를 정리하는 방법은 EC2 자동 프로비저닝을 백그라운드 워커로에서 이어집니다. 전체 구조는 SaaS 운영 콘솔 구축기에 정리해 두었습니다.

자주 묻는 질문

상태 머신을 굳이 따로 만들 필요가 있나요?

구독은 상태가 다섯 개이고 그 사이 전이가 여러 갈래인데, 이를 조건문으로 흩어 두면 종료된 구독이 되살아나는 식의 잘못된 전이를 막기 어렵습니다. 전이 규칙을 표 하나로 모으고 검증 함수를 두면 상태를 바꾸는 모든 지점이 같은 관문을 통과하므로 규칙을 지키기도 바꾸기도 쉬워집니다.

연체되면 서버가 바로 삭제되나요?

아닙니다. 단계를 둡니다. 연체된 지 일정 기간이 지나면 먼저 정지 상태로 바꾸고 서버는 멈춤으로만 표시합니다. 이때 고객이 결제하면 되살릴 수 있습니다. 정지 상태가 충분히 오래 이어져 종료로 넘어갈 때 비로소 인스턴스와 고정 IP 같은 실제 AWS 리소스를 정리합니다.

전이는 누가 실행하나요?

매일 도는 생명주기 워커가 실행합니다. 연체가 오래된 구독, 정지가 오래된 구독, 해지 예정이면서 기간이 끝난 구독을 매일 찾아 각각 정지나 종료로 넘깁니다. 실제 전이 직전에 유효성을 다시 검증해 목록을 뽑은 뒤 상태가 바뀐 경우에도 잘못된 전이가 실행되지 않도록 했습니다.

#상태머신#구독#생명주기#BullMQ#설계

업무자동화 구축이 필요하신가요?

무료 상담 신청