토스의 속도와 품질, 상용 도구로 충분한가 — 토션(Tossion)
안녕하세요. 토스 QA Platform 팀 리더 이정성입니다.
지난 글에서 저희가 누구이고 무엇을 만들어 왔는지 짧게 소개해 드렸는데요. 오늘은 그 첫 번째 이야기, 저희만의 플랫폼 토션(Tossion)을 소개합니다.
"이 기능, 지난번에 테스트 결과 어땠었죠?"
QA라면 릴리즈 직전에 이 질문을 한 번쯤 받아 보셨을 겁니다. 저희도 받았고, 답을 드리는 데 시간이 걸리곤 했습니다. 자동화 테스트(Automation Test) 결과, 매뉴얼 테스트(Manual Test) 결과와 테스트 케이스(Test Case) 그리고 각 테스트의 판단 근거가 저마다 다른 곳에 흩어져 있었기 때문입니다.
아쉬움은 또 있었습니다. 쓰고 있던 상용 TCM(Test Case Management System)은 기능을 추가하거나 더 나은 방식으로 개선하고 싶어도, 쌓여 있는 QA 데이터를 활용하고 싶어도 저희가 직접 손댈 수 없는 도구였습니다. 개발을 요청하면 오래 걸리거나 아예 반영되지 않는 경우가 많았고, 기다리다 포기하곤 했습니다.
그래서 이 문제들을 해결하기 위해 토션(Tossion)을 만들었습니다.
지난 테스트 결과는 그대로 남고, 필요한 건 바로 올립니다
가장 먼저 만든 건 아주 평범한 것이었습니다.
자동화 테스트 결과, 매뉴얼 테스트 결과와 테스트 케이스, 그리고 그렇게 판정한 근거. 흩어져 있던 이 세 가지를 한 행에 모으는 일이었습니다. 셋이 각각 다른 곳에 있으면 "지난번엔 어땠나요"라는 질문이 나올 때마다 세 군데를 확인해야 했습니다.

토션의 구조는 프로젝트(Project) → 스위트(Suite) → 섹션(Section) → 테스트 케이스(Test Case)입니다. 섹션이 트리로 이어지고 테스트 케이스가 그 끝에 놓입니다. 여기까지는 상용 TCM과 다르지 않습니다.
테스트 케이스는 제품을 따라 계속 바뀝니다. 기능이 바뀌면 고치고, 없어진 화면의 것은 지웁니다. 반면 테스트 런(Test Run)은 그 옆에 따로 쌓입니다. 테스트 케이스가 어떻게 바뀌든 지난달에 무엇으로 검증했는지는 남아야 하니까요.
그래서 테스트 런을 만들 때 테스트 케이스를 참조만 하지 않습니다. Assignee와 Test Step, Description까지 그 시점의 값을 복사해 테스트 런 안에 한 행으로 새로 만듭니다. 그 행은 상태가 바뀔 때마다 History에 한 줄씩 쌓입니다. 누가 언제 어느 Version에서 그렇게 판단했는지가 테스트 케이스 옆이 아니라 그 테스트 런의 그 행에 남습니다.

테스트 런에 담을 때는 트리에서 섹션을 고릅니다. Type이나 Platform으로 한 번 추려 담을 수도 있는데, 규칙을 하나 뒀습니다. 사람이 직접 지정한 테스트 케이스에는 필터를 적용하지 않습니다. 명시적인 선택이 조건보다 우선해야 하니까요.
테스트 런은 Active → Completed → Closed로 갑니다. Closed하면 그 시점의 테스트 케이스와 코멘트, 자동화 결과를 스냅샷으로 저장해 고정합니다. 원본 테스트 케이스를 나중에 지워도 Close된 테스트 런의 화면과 리포트는 그대로 열립니다.
여기까지가 뼈대입니다. 직접 만들어서 달라진 건 그다음이었습니다. 사용자가 "이거 매번 하기 번거로운데요", "이런 것도 되면 좋겠는데요", "너무 느려서 사용성이 떨어져요"라고 하면 그걸 바로 만들어 반영할 수 있게 됐습니다. 상용 도구를 쓸 때는 요청을 넣고 기다리다 잊어버리던 일입니다.

Assignee별 진척도 차트가 그렇게 나왔습니다. 테스트가 한창일 때는 "지금 누가 얼마나 남았죠"를 서로 물어보게 되는데, 그때마다 표를 Assignee로 나눠 세고 있었습니다. 지금은 테스트 런 한 곳에서 사람별로 몇 건 중 몇 건이 남았는지가 한눈에 보입니다.
표에 들어가는 항목도 마찬가지입니다. Status, Type, Assignee, Version, Platform, RNR, History는 필요하다는 말이 나오면 그때 넣고, 안 쓰이면 뺐습니다. 덕분에 안 쓰는 칸이 쌓이지 않고, 실제로 쓰는 것만 남은 상태로 유지됩니다.
여러 명이 같은 테스트 런을 동시에 채우는 일도 흔합니다. 그래서 지금 이 화면을 누가 같이 보고 있는지가 위쪽에 아바타로 표시되고, 사람마다 색이 하나씩 붙습니다. 누가 테스트 케이스나 Description을 편집하기 시작하면 그 항목이 잠기면서 편집 중이라는 표시가 나타나고, 그 사람이 창을 닫거나 연결이 끊기면 잠금은 자동으로 풀립니다. 다른 사람이 바꾼 Status는 새로고침 없이 바로 적용됩니다.
하나하나 놓고 보면 큰 기능은 아닙니다. 중요한 건 요청에서 반영까지가 짧다는 겁니다. 로드맵에 올려 두고 기다리는 게 아니라, 바로 개발해서 적용하고 활용할 수 있게 되었습니다.
토스의 속도와 품질, 상용 TCM으로 충분한가
토스는 속도감이 남다른 조직입니다. 품질을 지키는 방식도 그 속도를 따라가야 하고, 품질이 속도의 트레이드오프가 되어서는 안 됩니다.
이 문제를 해결하기 위한 결정적인 역할을 할 수 있는 건 AI라고 판단했습니다. 저희는 테스트 케이스 생성부터 PR 분석, 회귀 테스트 자동화 작성부터 수행까지 QA의 수많은 일을 AI로 효율화할 계획이었고, 이건 기능 하나를 기다리는 문제가 아니라 일하는 방식 자체를 계속 실험하고 바꿔 나가는 일이었습니다.
기능이 아쉬운 거라면 더 좋은 도구로 교체하면 됩니다. 하지만 도구는 만든 사람이 정한 것만 할 수 있습니다. 비슷한 기능이 어느 도구의 로드맵에 있더라도 그건 그 회사가 정한 모양이고, 저희가 원하는 모양과 속도는 저희 손으로 만들 때 나옵니다. 저희에게 필요했던 건 새로운 요구를 그때그때 바로 추가할 수 있는 플랫폼이었고, 그런 건 사서 쓸 수 있는 것이 아니었습니다.
토션을 만들고 직접 사용하다 보니, 더 하고 싶은 일들이 보이기 시작했습니다.
이번에는 누군가에게 요청하고 기다리지 않고 네 가지 전부 저희 손으로 토션 위에 추가했습니다.
모두 토션을 처음 만들 때는 목록에 없던 것들이고, 앞으로도 다섯 번째, 여섯 번째 요구가 계속 나올 겁니다. 그때마다 새로 만드는 게 아니라 이미 있는 구조 위에 추가할 수 있어야 했습니다.

이번 릴리즈, 무엇부터 봐야 할까요?
시작은 RC 빌드가 배포된 직후입니다. QA 라벨이 붙은 PR은 어차피 봅니다. 정말 위험한 건 개발자가 "이건 QA 안 봐도 됩니다"라고 분류해 둔 나머지입니다. 대부분은 실제로 안 봐도 되지만, 가끔 한 건이 섞여 있고 그 한 건이 해당 Sprint를 흔듭니다. 저희가 만든 PR 분석기의 존재 이유는 새 기능을 요약하는 게 아니라 그 분류를 한 번 더 감사하는 것에 있습니다.
릴리즈 마일스톤(Milestone)이나 RC(Release Candidate) 태그를 선택하면 그 릴리즈에 들어간 PR을 전량 수집해, QA 라벨 유무로 두 갈래로 나눠 분석합니다. 라벨이 붙은 쪽은 검증 관점을 정리하고, 안 붙은 쪽은 "정말 안 봐도 되는가"를 따집니다. 마지막에 릴리즈 전체를 한 장으로 요약한 뒤 "이번에 꼭 봐야 할 것"만 따로 추려 올립니다.
그런데 분석하는 주체가 토션이 아닙니다.
QA 서버에서 동작하는 작은 프로그램이 하나 있습니다. 저희는 이걸 에이전트(Agent)라고 부르는데, 하는 일은 단순합니다. 토션에 자기를 등록해 두고 몇 초마다 "제가 할 일 있나요"를 물어보다가, 작업을 받으면 그 서버에 이미 로그인돼 있는 AI를 직접 실행합니다.
여기서 공들인 건 정확도보다 빠뜨리지 않는 것입니다.
한 릴리즈에 PR이 400건이면 한 번에 다 읽힐 리가 없어서, 분석을 한 덩어리로 수행하지 않습니다. 수집한 PR을 한 묶음에 적절한 수만큼 나눠 담은 뒤, 묶음마다 AI를 따로 실행해 동시에 진행합니다. 많은 PR을 얇게 보는 것보다 적은 PR을 깊게 보는 쪽이 낫다고 판단했습니다.
문제는 그다음입니다. AI에게 수백 건을 시키면 그럴듯하지만 내용이 없는 문장이 섞입니다. "관련 화면이 영향받아요" 같은 것들이요. 그래서 어떤 문장이 부적합인지를 규칙으로 정해 두고, 결과가 그 규칙에 어긋나는지를 사람이 보기 전에 먼저 걸러냅니다. 화면명도 조건도 없는 모호한 문장, PR 제목을 그대로 옮긴 요약, 함수명이 그대로 남은 설명, 그리고 테스트 케이스가 재현 단계 · 기대 결과 · 실패 증상 · 판단 근거 네 칸을 채우지 못한 것. 이 가운데 하나라도 해당하면 부적합입니다. 부적합으로 판정된 PR은 AI에게 다시 분석하게 합니다.
마지막은 숫자 대조입니다. 병합할 때 들어간 PR 수와 나온 분석 수를 맞춰 보고, 빠진 게 있으면 그 PR만 다시 분석해 채웁니다.

여기에 과거 장애 이력이 함께 들어갑니다.
핫픽스가 나면 그걸 고친 PR이 어떤 파일을 건드렸는지 기록해 둡니다. 다음 분석 때 그 목록을 같이 넣어 주고, 이번 PR이 같은 파일을 건드린 경우에만 "여기 예전에 장애가 있었습니다" 표시가 붙고 위험도가 한 단계 올라갑니다. 겹치지 않는데 붙은 표시는 뒤따르는 검사가 다시 제거합니다.

이 분석은 두 시간이 걸리기도 해서, 다 끝난 뒤에 저장하지 않고 묶음 하나가 끝날 때마다 Agent가 그때까지 분석한 것을 토션에 올립니다. 덕분에 Agent가 중간에 멈춰도 이미 올라간 분석은 토션에 남습니다. 토션은 그 실행을 실패로 정리해 두었다가, 나중에 같은 릴리즈를 다시 실행하면 남아 있던 결과를 이어받아 이미 분석한 PR은 건너뛰고 아직 보지 않은 것만 분석합니다.
이렇게 나온 ‘이번에 꼭 봐야 할 것’ 목록이, 해당 Sprint 테스트 런의 범위 근거가 됩니다.
테스트 케이스를 만드는 일이 사람 손을 떠날 때
이번엔 Sprint 밖으로 나가보겠습니다.
새 기능이 개발되면 QA에게 테스트 요청이 들어옵니다. 그때마다 테스트 케이스를 새로 써야 하는데, 기능이 쏟아지는 속도만큼 사람이 다 쓸 수는 없습니다. 그래서 AI가 테스트 케이스를 만들어 토션에 올리게 했습니다. 여기서 지켜야 할 것은 두 가지, 테스트 케이스 트리의 구조와 결과에 대한 신뢰였습니다.
구조는 AI에게 맡기지 않았습니다. AI는 ‘자산 > 계좌 연결 > 은행 선택’ 같은 평평한 경로 문자열만 내놓습니다. 그걸 실제 트리로 펼치고 중간에 없는 단계를 만들어 붙이는 건 토션입니다. 덕분에 이미 테스트 케이스가 많은 스위트에도 알맞은 자리로 들어갑니다. 같은 이름의 섹션이 이미 있으면 그 안에 넣고, 없으면 새로 만듭니다.

신뢰는 세 겹으로 걸었습니다.
첫째는 AI가 자기 결과를 다시 보게하는 겁니다. 빠진 분기, 놓친 에러 상황, 경계값 같은 걸 항목별로 살펴 더 나올 게 없을 때까지 반복합니다.
둘째는 미리 정해 둔 규칙에 맞는지 보는 겁니다. 문장 표기부터 테스트 케이스 번호, 화면이 하나도 안 빠졌는지, 요구사항이 전부 테스트 케이스로 옮겨졌는지까지 스크립트가 확인합니다. 여기서 어긋나면 토션에 올리는 것 자체가 막힙니다.
셋째는 다른 AI에게 테스트 케이스가 아니라 테스트 계획을 세우게 하는 겁니다. 같은 기획서를 주되 순서를 반대로 갑니다. 테스트 케이스부터 쓰는 대신 이번 검증 범위가 어디까지인지, 무엇이 위험한지, 어떤 기법으로 테스트 케이스를 도출해야 하는지를 ISTQB·ISO/IEC 29119 기준으로 먼저 정리하게 합니다. 이때 앞에서 만든 테스트 케이스는 보여 주지 않습니다.
그리고 마지막에 양쪽을 맞춰 봅니다. 계획에는 있는데 테스트 케이스에 없으면 누락, 테스트 케이스에는 있는데 계획에 없으면 범위 밖입니다. 이 겹을 둔 진짜 이유는 따로 있습니다. 테스트 케이스는 보통 화면을 따라 만들어집니다. 그런데 어떤 기능은 화면이 아니라 상태로 갈립니다. 같은 화면이어도 직전에 무엇을 했느냐에 따라 다르게 동작하죠. 화면만 보고 도출하면 이 축이 통째로 비는데, 개수는 오히려 많아서 비었다는 것이 드러나지 않습니다.
계획을 먼저 세우는 쪽은 "이 기능은 상태 전이로 도출해야 한다"를 앞에서 정해 둡니다. 무엇을 어디까지 볼지, 그걸 어떤 기법으로 도출할지가 테스트 케이스보다 먼저 정해져 있는 거죠. 그래서 맞춰 보면 비어 있는 축이 목록으로 나오고, 그대로 테스트 케이스를 채웁니다. 화면을 다 덮었는지와 필요한 기법을 다 썼는지, 둘 다 채워져야 끝납니다.
세 번째 겹은 한 번 더 보는 게 아니라 다른 눈으로 보는 겁니다.
만들어진 테스트 케이스는 CLI를 거쳐 토션에 올라갑니다. AI가 토션에 손을 대는 통로를 화면이 아니라 명령줄로 둔 것인데, 어디에 두어도 실행되는 단일 실행 파일 하나로 만들었습니다. 사람마다 개발 환경이 다른 탓에 패키지 설치도, 런타임 버전도, 경로도 전부 실패 지점이 되니까요.
이렇게 만든 테스트 케이스로 신규 기능을 검증하고, 그중 안정된 것들이 회귀 테스트 케이스로 편입됩니다.
버튼 하나에서 실제 기기까지
신규 기능은 사람이 봅니다. 화면을 처음 보는 눈이 필요하니까요. 자동화가 맡는 건 그렇게 쌓인 회귀 테스트 케이스입니다.
시작은 토션 화면입니다. 실행 버튼을 누르면 모달이 하나 열리는데, 실행에 필요한 게 전부 거기 있습니다. 어느 기기에서, 어느 빌드로, 어느 범위를 실행할지, 그리고 결과를 어느 테스트 런에 이어 붙일지까지요. 기록을 보던 화면이 그대로 시키는 화면이 됩니다.

러너는 QA 서버에 연결된 안드로이드·iOS 실기기에서 토션이 넘긴 실행을 처리합니다. 켜지면 스스로 토션에 등록되지만 관리자 승인 전에는 아무 작업도 받지 못합니다.
먼저 환경을 맞춥니다. 러너가 30초마다 자기 상태와 지금 붙어 있는 실기기 목록을 보고하기 때문에, 실행을 걸 때 기기 선택 목록이 화면에 이미 표시돼 있습니다. 앱도 각자 준비하는 게 아니라 토션이 발급한 빌드를 내려받아 설치한 뒤에 시작합니다. 어느 빌드에서 나온 결과인지가 흔들리면 안 되니까요.
그다음 시킨 범위만 실행합니다. 회귀 전체일 수도 있고, 특정 화면 아래만일 수도 있습니다. 모달에서 고른 범위가 그대로 실행 경로 필터가 됩니다.
실행하는 동안에는 진행 상황이 그대로 보입니다. 시나리오 하나가 끝날 때마다 어느 섹션의 무슨 테스트가 몇 초 만에 통과했는지, 실패했으면 어떤 메시지였는지가 토션에 바로 쌓입니다. 두 시간짜리 회귀를 끝날 때까지 기다려야 알 수 있는 게 아닙니다. 실행이 다 끝나면 완료 알림이 따로 옵니다.
결과에는 무엇을 담았을까요? 저장 단위를 시나리오가 아니라 스텝으로 잡았습니다. 스텝마다 상태와 소요 시간, 에러 메시지가 남고 그 시점 스크린샷이 붙으며, 시나리오에는 영상이 따로 붙습니다. ‘실패했다’ 다음에는 어느 스텝에서 멈췄는지, 그때 화면은 어땠는지를 묻게 되는데, 거기에 바로 답하기 위해서입니다.

실행을 걸 때 테스트 런을 지정하면, 그 결과가 사람이 쓰던 기록으로 합류합니다. 버튼 하나가 실기기에서 테스트를 실행하고, 그 결과가 해당 Sprint의 테스트 런에 테스트 케이스 한 줄 한 줄로 들어가는 겁니다.
그 자동화가 통과시킨 건, 정확히 어느 테스트 케이스인가요?
결과를 아무리 잘 담아도, 별도 리포트에 고립돼 있으면 소용이 없습니다.
"이 테스트 케이스는 자동화로 커버되나요?"에 답하려면 결과가 테스트 케이스 한 건 단위까지 내려와야 합니다. 그런데 보통 연동은 리포트 단위에서 멈춥니다. "회귀 200건 중 3건 실패"까지는 알려 주는데, 그 3건이 수동 테스트 케이스 목록의 어느 줄인지는 사람이 눈으로 맞춰야 하죠. 사람이 그 대조를 하지 않아도 되게 하려면 테스트 케이스를 자동화 코드로 내보내는 것과, 자동화 결과를 다시 테스트 케이스에 적어 넣는 것 두 방향이 필요했습니다.

테스트 케이스가 자동화 코드가 되는 쪽부터 보겠습니다. 스위트 화면에서 자동화 코드 생성을 누르면 자동화 코드 스캐폴드가 나옵니다. 스위트 전체를 한 번에 내보낼 수도 있고, 섹션 하나를 골라 그 아래만 내보낼 수도 있습니다.
이 스캐폴드는 저희가 토스체커에 설계해 둔 프레임워크를 그대로 따릅니다. 시나리오 파일, 플랫폼별 스텝 파일, 화면 요소를 모아 둔 로케이터 파일이 각자 자리에 놓여 나와서, 받아서 저장소에 넣으면 바로 동작합니다. 다만 전부 채워진 코드는 아닙니다. 시나리오와 스텝 함수, 로케이터 목록까지는 이름과 설명이 붙어 나오는데, 화면 요소를 실제로 가리키는 값은 비어 있습니다. 그 값이 없으니 그걸 누르고 확인하는 동작 코드도 형태만 잡혀 있습니다. 그 빈칸은 테스트 케이스를 실행 코드로 바꿔 주는 또 다른 AI 스킬이 채웁니다.
내보내는 행위 자체가 ‘이 시나리오 파일은 이 섹션의 것’이라는 연결을 만들어 토션에 저장합니다.
자동화 결과가 테스트 케이스로 돌아오는 쪽은 그 반대입니다. 실행이 끝나면 결과가 테스트 케이스 한 건 한 건에 나뉘어 들어갑니다. 사람이 수행한 항목과 자동화가 수행한 항목이 같은 테스트 런 안에 나란히 놓입니다. 자동화가 수행한 항목은 Assignee가 ‘토스체커’이고, History에 실행 번호와 Version이 함께 남습니다. 자동화가 맡은 섹션에는 자동화 표시가 붙어서, 어디까지가 자동으로 채워지는 범위인지가 목록에서 그대로 보입니다.

그리고 자동화가 실행하지 않은 테스트 케이스의 결과는 변경하지 않습니다. 그래서 iOS만 실행해도 Android 전용 테스트 케이스는 목록에 그대로 남아 있고, 사람이 손으로 배정해 둔 Assignee도 지워지지 않습니다.
그 자동화 표시를 누르면 화면 옆으로 결과 창이 열립니다. 자동화 페이지로 넘어가지 않아도 그 자리에서 모두 확인할 수 있습니다.
창 위에는 그 테스트 케이스의 누적 결과가 있습니다. 지금까지 몇 번 실행됐는지, 통과율이 얼마인지, 평균 몇 초가 걸리는지가 함께 보입니다. 결과가 자주 바뀌는 테스트 케이스에는 Flaky 표시가 붙습니다.
그 아래가 이번 실행입니다. 스텝이 순서대로 펼쳐지고, 스텝마다 상태와 소요 시간, 실패했다면 에러 메시지와 그 시점 스크린샷이 함께 붙습니다. 시나리오 전체를 녹화한 영상도 있습니다.
여기에 더해 그 스크린샷을 AI가 한 번 더 봅니다. 레이아웃이 깨졌는지, 텍스트가 잘리거나 겹쳤는지, 이미지가 표시되지 않았는지, 로딩 에러가 남아 있는지를 보고 판정과 확신도를 스텝에 붙입니다. 자동화 코드가 보는 건 "눌렀더니 다음 화면으로 갔는가"까지라, 갔는데 화면이 깨져 있으면 코드는 그대로 통과로 처리합니다. 그 빈틈을 메우려고 덧붙였습니다.
맨 아래에는 같은 테스트 케이스가 지난번엔 어땠는지가 이력으로 쌓입니다.

테스트 런을 닫으면 연결된 자동화 실행도 같이 닫힙니다. 그때의 자동화 결과가 스냅샷으로 함께 봉인되기 때문에, 나중에 열어도 스텝과 스크린샷이 그대로 남아 있습니다.
그래서, 지난번 테스트 결과는요?
이제 그 답이 한곳에 모여 있습니다.
이번 릴리즈에서 무엇을 봐야 하는지가 목록으로 나와 있고, 그걸 어떤 테스트 케이스로 검증했고, 결과가 어땠고, 마지막으로 언제 실행됐고, 사람이 한 건지 자동화가 한 건지, 어느 빌드에서 그랬는지가 한 행에 있습니다. 답을 드리는 데 시간이 걸리던 질문이었는데, 지금은 하나의 플랫폼에서 확인할 수 있습니다.
"지난번 결과가 어땠죠"를 찾는 데 쓰던 시간을, 이제는 "다음엔 무엇을 더 만들까"에 사용합니다.
