자가 치유 테스트가 고쳐도 되는 실패, 고치면 안 되는 실패
![]()
자가 치유(self-healing) 테스트라고 하면 대개 깨진 테스트를 알아서 고쳐 주는 기능을 떠올립니다. 틀린 기대는 아닌데, 그 안에는 성격이 전혀 다른 두 가지가 섞여 있어요. 테스트가 앱을 잘못 본 걸 바로잡는 일, 그리고 앱이 달라진 걸 테스트가 조용히 받아들이는 일. 앞은 치유고, 뒤는 은폐에 가깝습니다. 둘 사이 선을 어디에 긋느냐가 이 기술의 거의 전부라고 저는 봐요.
토스 QA Platform 팀이 공개한 토스닥터 V2는 이 선을 한 곳에서는 정확히 그었고, 다른 곳에서는 흐릿하게 남겨 뒀습니다. 두 지점을 나눠 보면 자가 치유를 들여오려는 팀이 무엇을 같이 설계해야 하는지가 드러나죠.
치유가 끼어드는 자리는 세 군데
토스닥터가 사람 대신 손을 대는 지점을 위험도 순으로 다시 놓으면 이렇습니다. 요소를 다르게 찾는 것, 화면을 가린 걸 치우는 것, 테스트 코드 자체를 고치는 것.
첫 번째는 판단보다 비용 문제입니다. 요소를 찾으려고 화면과 UI 트리를 통째로 LLM에 넘기자 토큰을 감당할 수 없었고, 그래서 탐색을 세 단계로 쪼갰어요. 스크린샷과 화면에 드러난 요소 목록으로 먼저 찾고, 안 되면 화면 구조 전체(XML)를 뒤지고, 그래도 안 되면 사람에게 묻는다. 값싼 수단부터 쓰고 비싼 수단은 꼭 필요할 때만 꺼내는 설계라, LLM 호출을 운영비로 계산해 본 팀이라는 게 보입니다.
판단이 필요해지는 건 두 번째와 세 번째부터예요. 테스트가 요소를 못 찾고 쓰러지려는 순간 서버가 부르는 스마트파인더는 두 가지를 시도합니다.

'확인'이 '확인하기'로 바뀐 버튼을 같은 뜻으로 보고 누르는 게 오른쪽의 similar-match입니다. 동작을 수행하는 스텝이라면 합리적이에요. 엉뚱한 걸 눌렀다면 다음 화면이 안 맞아서 어차피 드러나거든요. 검증(then) 스텝은 사정이 다릅니다. 거기서 비슷한 요소를 찾아 통과시키면 그 통과가 곧 판정이 되고, 틀린 판정이 뒤 스텝으로 번집니다. 토스 팀은 이걸 겪었어요. "실제로 5스텝이 줄줄이 거짓 성공한 적이 있었습니다". 그 뒤로 then 스텝에서는 similar-match를 금지하고, 가린 걸 치우는 것까지만 허용했습니다.
이 대목이 글 전체에서 가장 값지다고 생각해요. 자가 치유의 원칙을 한 줄로 줄이면 이렇게 됩니다. 무엇을 검증하는지를 바꾸지 않는 범위에서만 고친다. Gherkin의 Given-When-Then 시나리오를 사람이 쓰게 한 것도 같은 원칙의 앞단이고요. "시나리오는 사람이 쓰고, 코드는 도구가 씁니다." 검증 대상은 사람이 고정하고, 거기 닿는 경로만 도구가 바꾼다는 선언인 셈입니다.
닫고 지나간 툴팁은 통과일까
그런데 이 원칙이 '가린 걸 치우는 것'까지 지켜 주지는 않습니다. 실제 사례를 보면 타행 계좌 송금 스텝에서 외부 앱 공유 툴팁이 계좌번호 입력칸을 덮고 있었고, 스마트파인더가 그걸 블로커로 판단해 닫은 뒤 원래 스텝을 이어갔어요.

기록 맨 아래에 '시나리오 전체 통과'와 '사람이 손댄 곳 0'이 나란히 찍혀 있습니다. 테스트 입장에선 맞는 결과죠. 하지만 계좌번호 입력칸을 가리는 툴팁은 실제 사용자도 똑같이 만납니다. 사람은 닫기 버튼을 찾거나, 못 찾고 헤매거나, 송금을 접잖아요. 테스트가 그 마찰을 대신 치워 주면 사용자가 겪을 문제가 초록색 한 줄로 바뀌어 버립니다.
여기에 학습이 붙으면 문제가 한 겹 더 생깁니다. 스마트파인더는 살려낸 기록을 JSONL로 쌓아 두고, 같은 화면 같은 스텝에서 막히면 과거 성공 사례부터 참고해요. 자주 나오는 팝업일수록 더 빨리, 더 정확히 넘긴다는 게 장점으로 소개됩니다. 테스트 안정성만 보면 맞는 말인데, 뒤집어 보면 자주 나오는 블로커일수록 점점 더 안 보이게 된다는 뜻이기도 해요. 한 번 뜨는 툴팁은 우연일 수 있지만 매번 뜨는 툴팁은 설계 문제고, 그걸 가장 매끄럽게 삼키는 게 학습된 치유입니다.
치운 흔적은 실행 결과에 남으니 괜찮다는 반론도 가능합니다. 제가 걸리는 건 색이에요. 블로커를 닫은 스텝이 통과와 같은 초록색으로 칠해지면, 매주 쌓이는 RC 빌드 결과를 훑는 사람 눈에는 들어오지 않습니다. 치유한 스텝은 통과가 아니라 별도의 상태로 보여야 해요.
recoverable 판정이 삼키는 것
세 번째, 코드를 고치는 일은 판정에서 시작합니다. 실패가 나면 /diagnosis가 추정 원인과 필요한 조치, 그리고 recoverable 여부를 내놓습니다.

판정을 만드는 과정은 꼼꼼해요. 실패 화면(_ERROR.png) 하나가 아니라 직전 스텝들의 화면을 시간 순서대로 같이 보고, 복구 실행은 전체 실행과 달리 첫 에러에서 바로 멈춰 그 화면에 Appium MCP를 붙입니다. 추정을 실제 요소로 확인하는 절차죠. 같은 '송금 실패' 두 건이 한 실행 안에서 서로 다른 판정을 받은 사례도 소개됩니다. "하나는 코드로 고치면 되고, 하나는 코드 밖의 일이죠."
다시 그림을 보면 locator 변경은 true 칸에, 앱 자체 버그는 false 칸에 있습니다. 문제는 둘이 늘 따로 오지 않는다는 거예요. 글에 나온 복구 사례가 정확히 그 경계에 서 있습니다. E-1에서 ImageView locator가 타임아웃 나고, E-2에서 '보낼까요'가 TextView라는 걸 확인하고, E-3에서 locator를 고치고, E-4 재실행은 PASSED, E-5에서는 lessons.md에 "앱 업데이트 시 요소 타입 변경 주의"를 남겼습니다. 그리고 이렇게 적었죠. "E-1부터 E-5까지, 사람이 손댄 곳은 없어요."
'보낼까요'가 ImageView에서 TextView로 바뀐 건 테스트가 틀린 게 아니라 앱이 바뀐 겁니다. 의도한 리팩터링이었다면 테스트를 고치는 게 맞습니다. 의도하지 않은 변경, 예컨대 접근성 속성이 딸려서 바뀐 거라면 앱에 버그를 올려야 하고요. 그 판단은 앱의 의도를 아는 사람만 할 수 있는데, 복구 루프는 그 질문을 건너뛴 채 초록불로 끝납니다. lessons.md에 쌓이는 것도 테스트 쪽 교훈이지 앱 쪽 보고가 아닙니다. 그 파일을 누군가 정기적으로 읽지 않는다면, 앱이 바뀐 이력은 테스트 저장소 한구석의 회고 메모로만 남게 되고요.
사람이 봐야 하는 건 실패가 아니라 복구 기록
사람이 어디까지 돌봐야 하느냐는 질문에 토스닥터가 내놓은 답은 "사람에게는 정말 사람이 봐야 할 실패만 남깁니다"입니다. 저는 여기에 하나를 더 남겨야 한다고 봐요. 도구가 무엇을 닫았고 무엇을 바꿨는지, 그 목록이요. 자동화가 잘 돌수록 실패 목록은 짧아지지만 복구 목록은 반대로 길어집니다. 앱의 변화가 쌓이는 곳이 바로 거기라서요.
플랫폼마다 RC 빌드가 매주 5~7개씩 쌓이는 규모라면 손으로 누르는 스모크 테스트는 이미 병목이고, 치유 루프로 얻는 게 분명합니다. 토스체커가 세 명의 테스터가 이틀을 매달리던 리그레션을 대신하고 1,375개 테스트 케이스를 3주 만에 자동화했다는 숫자가 그 속도를 보여 주죠. 다만 이건 자동화의 속도고, 버그를 잡아내는 힘에 대한 숫자는 아직 없습니다. 그리고 토스체커는 한국어 BDD 시나리오와 self-healing을 그대로 리그레션으로 넓혔어요. 스모크가 앱이 숨은 붙어 있는지를 본다면 리그레션은 어디가 달라졌는지를 찾는 검사라, 달라진 걸 흡수하는 치유와는 목적부터 부딪힙니다. 선을 흐리게 둔 비용이 스모크보다 리그레션에서 훨씬 커지는 이유예요. 검증 스텝 치유 금지는 규모와 상관없이 첫날부터 넣을 규칙이에요. 블로커 닫기와 locator 수정은 통과와 다른 색으로 표시하고, 그 목록이 주 단위로 앱 개발자에게 가는 경로까지 만들어야 합니다.
그 경로를 만들 수 있는 팀이라면 토스닥터의 설계를 그대로 가져와도 됩니다. 만들 수 없는 팀이라면 복구 루프를 E-2, 그러니까 멈춘 화면을 확인하는 데서 끊고 수정은 사람에게 넘기는 편이 낫다고 봅니다. 요소 탐색에는 이미 사람에게 묻는 마지막 단계가 있으니, 복구에도 같은 출구를 하나 두자는 거죠. 초록불이 늘어나는 만큼 앱에서 무엇이 바뀌었는지는 모르게 되는 거래를, 모르고 하게 되니까요.