상태 전이마다 테스트가 없으면, 운영 가이드가 빌드되지 않습니다

flex 매출 플랫폼 팀은 구독, 청구, 결제, 환불, 매출 인식을 다룹니다. 얼마 전 이 팀이 만든 운영 가이드를 실제 업무에 그대로 대 보는 검토를 했습니다. 검토에서 어떤 문제가 나왔고, 가이드와 테스트를 하나의 선언으로 묶어 어떻게 풀었는지 소개합니다.
운영 가이드 검토
검토자에게 운영자, 신규 개발자, 데이터팀의 입장이 되어 가이드만 보고 실제 업무 25개를 처리해 달라고 했습니다. 다운그레이드 후 환급이 없다는 고객 문의에 답하는 일도 있었고, 이중 청구의 원인을 찾거나 환불을 승인한 뒤 매출 인식까지 확인하는 일도 있었습니다.
가이드만으로 끝낸 업무는 25개 중 6개였습니다.
막힌 곳을 하나씩 따라가 보니 가이드와 실제 동작이 맞지 않는 데가 있었습니다. 가이드대로 계약을 만들고 활성화하려 하면 버튼이 눌리지 않았습니다. 가이드에 없는 "구독예정 만들기" 단계를 먼저 거쳐야 했습니다. 같은 절차를 검증하는 E2E 테스트도 바로 그 단계에서 실패하고 있었는데, CI 실행 대상에서 빠져 있어서 팀은 실패하는 줄 몰랐습니다. 화면의 팝업에는 닫기 버튼이 없었고, 테스트 코드는 JavaScript로 팝업을 닫아 이 문제를 우회하고 있었습니다.
가이드에는 필요한 단계가 빠져 있었고, 테스트에는 화면에 없는 동작을 우회하는 코드가 들어 있었습니다. 둘을 서로 맞춰 보는 장치가 없으니, 두 문제 모두 누구의 눈에도 띄지 않았습니다.
가이드가 틀리면 생기는 일
이 서비스에서는 상태 하나를 잘못 처리하면 요금이 두 번 청구되고, 고객이 받아야 할 크레딧이 사라집니다. 운영자는 가이드를 보고 고객 문의에 답하고, 재무 담당자는 가이드를 참고해 마감합니다. 가이드가 틀리면 운영자도 고객에게 틀리게 안내하게 됩니다.
당시 저희는 이 영역의 정책도 다시 정리하고 있었습니다. 조건을 판단하는 코드는 여러 곳에 흩어져 있었고, 실행은 통과해도 상태나 금액을 제대로 검증하지 않는 테스트가 많았습니다. 리팩터링을 시작하기 전에 기존 동작을 붙잡아 둘 테스트가 필요했고, 그 테스트가 어떤 동작을 지키는지 사람이 읽을 수 있어야 했습니다.
변경은 매일 여러 건씩 머지되고 있었습니다. 위키 문서를 최신으로 유지하려면 변경이 있을 때마다 누군가 직접 고쳐야 했습니다. 그래서 위키를 늘리지 않고, CI에서 검증한 정보로 운영 가이드를 만들기로 했습니다.
테스트 결과로 만든 사용설명서
먼저 테스트 결과를 모아 시스템 사용설명서를 만들었습니다. 시나리오 테스트와 불변식 검사 결과, JUnit 결과, 화면 흐름 기록, 결정표를 모아 HTML 문서 하나로 만들고, CI가 돌 때마다 게시했습니다. 알려진 결함은 지금의 동작을 그대로 재현하는 테스트로 남겼고, 사용설명서에는 "현재 이렇게 동작하며 아직 고치지 않았다"는 목록에 따로 모아 보여 줬습니다.
그런데 첫 검토에서 이 사용설명서에도 문제가 있다는 것을 알게 됐습니다. 결함을 재현하는 테스트의 출처가 모두 잘못 표시돼 있었고, 검토자는 그 표시를 그대로 믿고 업무를 끝냈다고 판단했습니다. 테스트 결과로 문서를 만들었어도 문서와 테스트 사이의 참조를 아무도 검사하지 않고 있었던 것입니다. 저희는 운영자가 가장 자주 묻는 질문부터 손대기로 했습니다. "이 요청은 지금 어떤 상태이고, 다음에는 어떤 상태로 바뀔 수 있나요?"라는 질문입니다.
상태 전이를 선언하는 YAML
선언의 구조
계약, 청구서, 결제, 환불 요청처럼 상태를 관리하는 엔티티마다 YAML 파일을 하나씩 만들고, 그 안에 상태와 전이, 금지 전이를 적었습니다. 예를 들어 환불 요청의 승인 전이는 다음과 같이 선언합니다.
transitions:
- id: approve
from: PENDING_APPROVAL
to: APPROVED
trigger: operator
action: 고객 상세 > 환불 탭 > 승인 버튼
code: RefundRequestService.approve()
tests:
- ...RefundRequestServiceTest#approve 는 승인된 환불요청으로 콜백을 호출한다()전이 하나에 운영자가 볼 정보와 개발자가 볼 정보를 함께 적습니다. 운영자가 화면에서 누를 버튼은 action에 적고, 개발자가 열어 볼 메서드는 code에, 이 전이를 검증하는 테스트는 tests에 적습니다. 생성기는 이 선언을 읽어 운영 가이드의 상태 안내와 상태 다이어그램을 만듭니다.
테스트 없는 전이를 거부하는 생성기
전이에 연결된 테스트가 하나도 없으면 생성기는 오류를 내고 멈춥니다. 금지 전이에도 같은 검사를 합니다. 테스트 참조가 비어 있는지 확인하는 코드는 다음과 같습니다.
val tests = value.testReferences("$slug.yaml transition tests")
require(tests.isNotEmpty()) { "$slug.yaml: every transition must link at least one test" }선언에 적은 테스트 클래스와 메서드가 소스 코드에 실제로 있는지도 검사합니다. 찾지 못하면 생성기는 "Test reference does not exist in source"라는 오류를 내고 멈춥니다.
이 생성 작업은 백엔드 PR의 CI에서 운영 가이드를 만들 때마다 실행됩니다. 누군가 선언에서 참조하는 테스트의 이름을 바꾸거나 테스트를 지우면 CI가 실패해 PR을 머지할 수 없습니다. 테스트를 고친 개발자는 같은 PR에서 상태 선언도 함께 고쳐야 합니다.
아래 그림은 YAML 선언 하나로 운영 가이드와 다이어그램을 만들고, 검사를 통과하지 못하면 PR이 실패하는 과정입니다.

선언을 쓰면서 추가한 테스트
YAML 파일을 추가할 때는 관련 테스트도 항상 같은 커밋에 넣었습니다. 전이마다 연결할 테스트를 찾다 보니, 코드에는 있지만 테스트가 하나도 없는 전이가 계속 나왔습니다. 구독의 활성화 대기와 만료, 청구서의 상태 이동, 견적의 가드 조건은 이때 처음 테스트를 추가했습니다.
금지 전이는 허용되지 않는 상태에서 시도하면 정말 실패하는지까지 테스트했습니다. 예를 들어 "승인은 승인 대기 상태에서만 할 수 있다"는 규칙에는 이미 승인됐거나 반려, 취소된 요청을 다시 승인할 수 없는지 확인하는 테스트를 붙였습니다. 결제와 환불의 금지 전이 테스트는 상태 검사 코드를 지웠을 때 테스트가 실패하는 것까지 확인하고 그 결과를 남겼습니다. 실행만 통과하고 아무것도 검증하지 않는 테스트를 다시 만들지 않으려고 이렇게 했습니다.
아직 고치지 못한 결함도 선언에 적었습니다. 실제 환불은 끝났는데 요청이 승인 상태에 남을 수 있는 결함은 환불 요청 YAML의 defects에 기록했고, 운영 가이드에는 "환불 요청과 결제 취소 내역을 함께 확인해 주세요"라는 안내로 나갑니다. 결함을 고치기 전이라도 운영자는 이 안내를 보고 두 내역을 함께 확인할 수 있습니다.
아래 그림은 환불 요청의 상태 전이를 정상 전이, 금지 전이, 알려진 결함으로 나누고 각각에 연결된 테스트를 표시한 것입니다.

재검토 결과
첫 검토 당시의 코드와 지금 코드로 각각 가이드를 만들고, 같은 조건에서 다시 검토했습니다. 검토자는 두 가이드로 같은 업무 목록을 같은 절차에 따라 두 번씩 처리했습니다. 화면에서 직접 확인해야 하는 업무는 집계에서 뺐습니다.

첫 검토 때 두 번 다 막혔던 업무가 이제는 한 번도 막히지 않습니다. 수동 정지 뒤 연체 건이 다시 생기는 이유를 찾는 업무가 그런 예입니다. 예전 가이드에는 테스트 스위트 이름 한 줄만 나와 있었지만, 지금 가이드에는 이 결함을 고쳤는지, 어떤 테스트로 확인하는지, 지금은 어떻게 동작하는지가 한 행에 적혀 있습니다. 다운그레이드 차액을 약정 조건별로 설명하는 업무도 같은 행을 보고 풀 수 있었습니다. 이 행들은 첫 검토 직후 가이드에 추가한 결함 및 규칙 색인입니다.
재검토에서는 이 색인의 약점도 드러났습니다. 색인은 사람이 표 파일에 테스트 이름을 직접 적는 방식이라, 빌드할 때 그 이름이 맞는지 검사하지 않습니다. 미납 청구서를 VOID하면 크레딧이 돌아오는지 확인해 보니 색인에는 돌아온다고 적혀 있었고, 결정표에는 부분 상계 건은 돌아오지 않는다고 적혀 있었습니다. 두 설명이 달라서 검토자는 어느 쪽이 맞는지 판단하지 못했습니다. 첫 검토 때 가이드와 테스트가 서로 달랐던 것과 같은 일이, 빌드가 검사하지 않는 색인에서 다시 일어난 셈입니다. 상태 전이를 선언에 적고 빌드에서 검사하게 만든 이상, 상태 안내에서는 이런 일이 생길 수 없습니다.
독자에 따라 나눈 출력
운영자 가이드에는 테스트 이름을 넣지 않습니다. 운영자에게는 상태 이름과 버튼 경로, 막혔을 때 확인할 곳, 화면 캡처만 보여 줍니다. 테스트 참조는 생성기가 검사할 때만 씁니다. 운영자용 정보와 개발자가 관리하는 테스트 참조를 같은 YAML에 적되, 가이드를 만들 때는 운영자에게 필요한 항목만 꺼냅니다.
다이어그램도 손으로 그리지 않고 Mermaid와 ELK로 생성합니다. 청구서처럼 전이가 많은 엔티티는 정상 전이와 나머지 전이를 나눠 그립니다. 상태 선언을 고치면 다음 빌드에서 다이어그램도 다시 그려집니다.
생성기 코드는 다른 서비스에서도 쓸 수 있게 별도 모듈로 분리했습니다. 이 모듈에서 청구 도메인 패키지를 import하면 테스트가 실패하도록 막아 두었습니다. 상태를 가진 서비스라면 어디든 YAML과 테스트만으로 같은 가이드를 얻습니다.
마치며
첫 검토에서 가이드와 테스트는 서로 다른 내용을 담고 있었지만, 그 차이를 잡아낼 검사가 없었습니다. 가이드와 테스트를 따로 관리하면 둘은 반드시 다시 어긋납니다. 그래서 가이드에 필요한 정보와 테스트 참조를 하나의 YAML 선언에 적고, 빌드할 때마다 둘이 맞는지 검사하도록 했습니다.
지금은 빌드할 때마다 테스트 참조가 들어 있는 선언으로 환불 요청의 상태 안내를 새로 만듭니다. 전이를 바꾸고 테스트를 추가하지 않았거나, 테스트를 지우고 선언을 고치지 않았다면 CI가 실패합니다. 운영자가 펼치는 가이드는 언제나 이 검사를 통과한 결과입니다. 선언을 작성하면서 그동안 아무도 검증하지 않던 전이를 찾아 테스트도 추가했습니다.
저희는 문서가 틀릴 수 없는 시스템을 만들고 있습니다. 이 일을 함께 밀고 나갈 엔지니어를 찾습니다.
🚀플렉스팀 채용페이지 바로가기
☕flex Private Talk 신청하기