공개

무인 회귀는 성능이 아니라 복구 구조가 만든다

실기기를 밤새 물려 300 사이클을 무인으로 완주했다. 완주를 만든 것은 빠른 코드가 아니라 끊는 법이었다.

밤 하나 300사이클 유지

밤새 도는 자동화를 만든 것은 빠른 코드가 아니라 끊는 법이었다. 실기기 한 대를 USB로 물려 300 사이클 · 9시간 57분을 무인으로 완주했고 실패 0 · 크래시 0이었는데, 그 완주를 만든 부품은 셋이다 — 실패해도 국소적으로 끝나는 복구 사다리, 영원히 기다리지 않게 하는 끊기, 그리고 판정을 픽셀이 아니라 로그에 둔 것. 사이클은 중앙값 111.5초인데 최대 161.8초까지 흔들렸고(중앙값의 1.45배), 그래도 완주한 이유는 빨라서가 아니라 흔들림이 옆으로 번지지 않아서였다. 배터리는 96%에서 95%로 1%p 줄었다 — 소모가 충전보다 빠르면 코드가 아무리 완벽해도 밤샘은 성립하지 않으므로, 이건 성능 지표가 아니라 통과 조건이다. 그리고 무인이 실제로 산출한 것은 새로운 발견이 아니었다. 300 사이클 중 147회(49%)에서 같은 경고가 로그에 찍혔는데, 그 경고는 내내 거기 있었고 아무도 세지 않았을 뿐이다. 빈도는 사람이 못 세는 종류의 증거다. 마지막으로, 이 자산이 얼마나 오래 사는지는 케이스가 무엇에 의존하느냐로 정해졌다 — 실기기용으로 쓴 126개가 전송층만 갈아끼우자 PC 빌드에서 그대로 살아났고, 그건 설계를 잘해서가 아니라 기기 어휘가 맨 아래 한 층에만 있었기 때문이며 반쯤 운이었다.

이 글이 답하는 질문

런이 느릴 때 어디가 병목인지 짚는 방법네 번 다 숫자는 맞았다에 있고, 결과가 PASS 도 FAIL 도 아닐 때 어디에 넣는지판정 임계 편에 있다. 이 글은 그 사이 — 사람이 안 볼 때 루프가 어떻게 살아남는가만 다룬다.

밤 하나의 해부 — 300 사이클 · 9시간 57분 · 무인 · 실기기 1대 USB 연결 사이클 벽시계 분포 (n=300) 98.8초 중앙값 111.5초 161.8초 최대는 중앙값의 1.45배 — 이 편차를 「최적화 대상」으로 읽으면 손댈 곳을 놓친다 완주 조건 — 소프트웨어 지표와 하드웨어 수지를 같이 본다 실패 0 · 크래시 0 한 밤 전체에서 검증 지점 26개 300회 전부 성립 배터리 96% → 95% 순감 1%p · 통과 조건 ⇒ 완주를 만든 것은 속도가 아니라, 그 편차가 다음 사이클로 번지지 않은 것이다 ⚠ 단계별 소요 수치에는 내 스크립트의 고정 대기가 섞여 있다 — 성능 지표로 인용할 수 없다
한 기기 · 한 빌드 · 한 밤의 분포다. 여러 밤·여러 기기의 분산은 재지 않았다.

무인 10시간을 만든 것은 성능이 아니라 복구 구조다

실기기 한 대를 USB로 물려 300 사이클 · 9시간 57분을 사람 없이 돌렸다. 실패 0, 크래시 0, 사이클당 검증 지점 26개가 300회 전부 성립했다. 이 300 사이클은 수렴이 끝난 뒤의 스모크 런이다 — 자동화를 고치는 루프는 따로 있었고, 그 루프가 두 사이클 연속 전건 통과에 도달한 다음에 돌린 것이 이 밤이다.

여기서 눈여겨볼 값은 완주 시간이 아니라 분포다. 사이클 벽시계는 중앙값 111.5초, 최소 98.8초, 최대 161.8초였다. 최대가 중앙값의 1.45배다. 열 시간짜리 무인 런에서 어떤 사이클은 다른 사이클보다 절반 가까이 더 걸렸다는 뜻이고, 그런 흔들림은 실기기를 쓰는 한 없앨 수 없다 — 기기가 열을 받고, 서버가 잠깐 느려지고, 로딩이 한 번 튄다.

이 편차를 「최적화 대상」으로 읽었다면 손댈 곳은 성능이었을 것이다. 사이클을 111초에서 90초로 줄이는 작업 말이다. 그런데 완주를 만든 것은 그 작업이 아니라, 그 편차가 다음 사이클로 번지지 않게 한 복구 사다리였다. 161.8초짜리 사이클이 하나 있어도 그다음 사이클은 다시 111초에서 시작했다. 만약 한 번 늘어진 사이클이 다음 사이클의 상태를 오염시켰다면, 300회를 도는 동안 그 오염이 누적돼 어딘가에서 멈췄을 것이다.

산수로 보면 더 분명하다. 사이클을 20% 줄이면 열 시간짜리 밤이 여덟 시간이 된다. 나쁘지 않은 개선이다. 그런데 한 사이클의 실패가 다음 다섯 사이클을 먹는 구조라면, 밤새 그런 사고가 세 번만 나도 열다섯 사이클이 날아가고 어느 시점에는 루프가 아예 못 돌아온다. 앞의 개선은 밤을 짧게 만들고 뒤의 개선은 밤을 끝나게 만든다. 무인에서 필요한 것은 뒤쪽이고, 앞쪽만 계속하면 빠른데 아침에 죽어 있는 루프가 나온다.

한 가지는 밝혀 둔다. 이 300 사이클은 자동화가 다 고쳐진 뒤의 런이다. 고치는 과정은 이 숫자에 안 들어 있고, 그 루프는 아래에서 따로 다룬다. 그러니까 이 절의 「실패 0」은 「자동화가 원래 튼튼했다」가 아니라 「튼튼해진 다음에 잰 값」이라는 뜻이다.

여기서 값을 치른 교훈은 무인의 지표가 평균이 아니라는 것이다. 무인에서 봐야 하는 값은 「한 사이클이 몇 초냐」가 아니라 「한 사이클의 실패가 몇 사이클을 먹느냐」다. 앞의 값은 개선하면 밤이 짧아지고, 뒤의 값은 개선하지 않으면 밤이 아예 안 끝난다. 둘은 다른 작업이고, 순서를 바꾸면 빠른데 완주 못 하는 루프가 나온다.

배터리 1%p 가 무인의 통과 조건이다

9시간 57분을 USB로 물려 무인 주행한 뒤 배터리가 96%에서 95%로 떨어졌다. 순감 1%p다. 측정 해상도가 정수 % 한 자리라 이 값 자체는 거칠지만, 이 한 줄이 밤샘의 성립 여부를 먼저 결정한다.

이유는 단순하다. USB로 물려 놓고 돌리면 충전과 소모가 동시에 일어나는데, 소모가 충전보다 빠르면 밤샘 자체가 안 된다. 코드가 아무리 완벽하고 복구 사다리가 아무리 촘촘해도 아침에 기기가 꺼져 있으면 그 밤은 0이다. 그래서 이 값은 성능 지표가 아니라 통과 조건이다 — 넘으면 좋은 값이 아니라, 못 넘으면 나머지 전부가 무의미해지는 값.

이 값을 재라고 알려 준 문서가 없었다. 자동화 글은 전부 통과율과 소요 시간을 재는데, 밤샘의 첫 번째 실패 모드는 「기기가 꺼져 있었다」다. 코드 결함이 아니라 전력 수지 문제이고, 소프트웨어 지표 어디에도 안 나타난다. 리포트는 「런이 중단됨」까지만 말하고, 왜 중단됐는지는 기기를 집어 들어야 안다.

그리고 이 값은 거칠다는 것도 같이 적어야 한다. 해상도가 정수 % 한 자리라 96 → 95 는 실제 순감이 0.01%p 일 수도 1.99%p 일 수도 있다. 그래서 이 한 스텝으로 「그럼 며칠도 돌릴 수 있겠네」로 넘어가면 안 된다 — 1.99%p 였다면 스무 시간쯤에서 수지가 뒤집힌다. 이 글이 말할 수 있는 것은 「열 시간짜리 한 밤은 전력 수지 안에 들어왔다」까지다.

여기서 값을 치른 교훈은 이 값이 사후에 못 재는 종류라는 것이다. 시작 시각과 종료 시각의 배터리를 둘 다 찍어 두지 않으면 다음 날 아침에는 영영 모른다. 로그를 아무리 뒤져도 어제 밤 11시의 배터리는 없다. 무인 런을 설계할 때 「런 시작 전에 찍어야 하는 것」 목록이 따로 필요한 이유가 이것이고, 그 목록은 코드가 아니라 런 자체의 준비물이다.

조작이 틀리는 비용과 판정이 틀리는 비용은 다르다 — 그래서 증거를 갈랐다

이 화면은 붙잡을 게 없다. 클라이언트가 화면 전체를 네이티브 서피스 하나에 그려서, 접근성 트리를 뽑으면 버튼도 텍스트도 좌표도 0인 빈 껍데기가 나온다. 그래서 조작 경로는 스크린샷 → 비전 → 좌표 탭 하나뿐이고, 그 정확도를 못 믿으니 눌러서 화면이 안 바뀌면 다시 찾는 구조로 짰다.

문제는 같은 도구를 판정에까지 쓸 때 생긴다. 처음에 그렇게 했고, 그게 이 프로젝트에서 제일 크게 틀린 판단이었다. 조작이 틀리는 비용과 판정이 틀리는 비용은 크기가 다르다. 조작은 틀려도 복구된다 — 잘못 누르면 화면이 안 바뀌고, 안 바뀌면 다시 찾는다. 비용은 재시도 한 번이고 그 사이클 안에서 흡수된다. 판정은 틀리면 그대로 결과가 된다. 되돌릴 지점이 없고, 아침에 보는 리포트에 그 값이 이미 들어가 있다. 게다가 틀린 방향이 한쪽으로 기운다 — 애매한 화면을 비전으로 보면 대개 통과 쪽으로 기울고, 그러면 초록불 하나가 조용히 늘어난다.

사람이 있는 실행에서는 이 차이가 안 드러난다. 애매하면 사람이 화면을 보고 판정하면 되기 때문이다. 무인에는 그 사람이 없다. 그래서 판정을 어디에 두느냐가 「정확도 개선」이 아니라 「무인이 성립하느냐」의 문제가 된다.

같은 도구로 둘 다 하면 안 되는 이유 — 틀렸을 때 그다음이 다르다 조작이 틀렸다 → 화면이 안 바뀐다 → 다시 찾는다 → 복구된다 비용 = 재시도 한 번 그래서 조작은 비전으로 해도 된다. 틀릴 것을 전제로 구조를 짜면 흡수된다 판정이 틀렸다 → 그대로 결과가 된다 → 리포트에 실린다 → 되돌릴 지점이 없다 비용 = 그 밤 전체 무인에는 애매한 화면에서 부를 사람이 없다. 그래서 판정은 비전 밖으로 옮겼다 ⇒ 증거 우선순위 — 수신 RPC > 상태 전이 > 화면. 앞의 둘은 엔진이 기기에 직접 쓰는 파일 로그에 전량 남는다 ⚠ 처음엔 「로그로는 내부 상태를 알 수 없다」고 단정했다. 막혀 있던 것은 표준 채널 하나뿐이었다
조작은 눈으로, 판정은 로그로. 이 분리가 없으면 장시간 무인이 성립하지 않는다.

증거 우선순위를 «수신 RPC > 상태 전이 > 화면» 으로 고정했다. 앞의 둘을 쓸 수 있었던 것은 순전히 처음의 단정이 틀린 덕이다. 「로그로는 내부 상태를 알 수 없다」고 적어 두고 있었는데, 막혀 있던 채널은 표준 출력 하나뿐이었다. 엔진이 기기 안에 따로 떨어뜨리는 파일 쪽에는 상태 전이도 RPC 송수신도 빠짐없이 들어 있었고, 그 파일을 한 번 열어 본 것이 이 프로젝트에서 제일 값싸게 얻은 개선이다.

남의 기법을 검토할 때도 같은 순서가 필요했다. 어떤 디바이스 팜의 화면 덤프가 2,350ms 라는 실측을 보고 그것을 수확으로 적었는데, 코드를 전수 대조하니 내 파이프라인에 그 호출이 0건이었다. 접근성 트리가 안 잡히는 화면이라 10배 빨라질 대상 자체가 존재하지 않았다. 그리고 그 사실은 내 프로젝트 문서에 이미 적혀 있었다 — 남의 벤치마크를 읽는 데 쓴 시간이 내 문서 한 장을 여는 시간보다 길었다.

조작 쪽에도 대가가 있다. 좌표로 누르는 구조는 정확히 눌러서 통과한 것과 대충 눌러서 통과한 것이 같은 통과로 기록된다. 화면이 바뀌었으니 다음으로 넘어가고, 리포트에는 초록불 하나가 남는다. 지금 그 둘을 가르는 계수가 없어서 폴백이 늘어나고 있는지도 모르는데, 이건 판정을 로그로 옮겨서 해결된 문제가 아니라 조작 쪽에 그대로 남아 있는 문제다.

여기서 값을 치른 교훈은 두 가지다. 하나 — 판정을 픽셀에서 로그로 옮기고 나서야 장시간 무인이 가능해졌다. 비전 판정은 애매한 화면에서 사람을 부르는데, 무인에는 부를 사람이 없다. 둘 — 남의 벤치마크를 재기 전에 「그 호출이 내 런에서 실제로 일어나는가」를 먼저 확인해야 한다. 2,350ms 는 실측이 맞았고 대상이 틀렸다.

무인에서 실패보다 나쁜 것은 대기다

무인 런에서 제일 비싼 상태는 실패가 아니다. 멈춰서 기다리는 상태다. 실패한 케이스는 기록을 남기고 루프가 다음으로 넘어가니 그 밤의 나머지는 살아 있다. 반면 기다리는 루프는 아침까지 한 칸도 못 나가고, 열 시간을 쓰고 얻는 것이 0이다. 같은 열 시간인데 산출이 하나는 부분이고 하나는 전무다.

제일 어려운 것이 무증상 정지였다. 예외도 안 나고 판정도 안 틀린 채로 루프가 그냥 서 버린다. 에러를 세는 장치는 이걸 못 본다 — 셀 대상이 애초에 없기 때문이고, 카운터를 아무리 정교하게 만들어도 결과는 같다. 그래서 감지를 실패 수가 아니라 화면이 바뀌는지에 걸고, 한동안 아무 변화가 없으면 복구를 단계별로 시도하다 마지막에 끊도록 만들었다. 끊는 순간 그 상태는 「대기」에서 「실패」로 바뀌고, 실패는 루프가 처리할 줄 아는 상태다.

이 구조를 만들면서 알게 된 게 하나 있다. 복구 사다리와 절단은 같이 있어야 한다. 복구만 있고 절단이 없으면 복구 시도 자체가 새로운 무한 대기가 되고, 절단만 있고 복구가 없으면 잠깐 튄 화면 하나에 밤이 끝난다. 실제로 단계를 세 칸으로 나눈 이유가 그것이다 — 가벼운 것부터 시도하다가, 다 안 먹히면 그 사이클을 버리고 다음으로 간다.

무인 런의 세 상태 — 가운데 하나만 에러 카운터가 0이다 실패 로그에 남는다 다음 케이스로 넘어간다 에러 카운터 +1 대기 아무것도 실패하지 않는다 아침에 그대로 있다 에러 카운터 0 ← 안 잡힌다 끊김 화면 변화량 축이 감지 단계 복구 → 절단 대기를 실패로 바꾼다 ⇒ 감지 축을 에러 카운터와 분리해야 한다. 무증상 정지는 에러가 0인 상태다 타임아웃은 성능 파라미터가 아니라 무인의 필수 부품이다 — 대기를 실패로 바꾸는 유일한 장치다 ⚠ 반례 — 24시간 테스트를 위해 타임아웃을 전부 30시간으로 올린 사례를 봤다 그건 멈춤 감지기를 끈 것이다. 상시 기본값이 되면 「멈춘 세션」과 「긴 세션」을 가를 수단이 사라진다
실패를 줄이는 작업과 대기를 없애는 작업은 반대 방향이다. 타임아웃을 늘리면 실패는 줄고 대기는 는다.

반례가 남의 회고에 있었다. 24시간 테스트를 돌리려고 타임아웃을 전부 30시간으로 올린 사례인데, 그건 멈춤 감지기를 끈 것이다. 그 한 번의 테스트에 한해서는 정당하지만 상시 기본값이 되면 「멈춘 세션」과 「긴 세션」을 가를 수단이 통째로 사라진다.

같은 유형이 기기 런 밖에도 있다. 다른 시스템의 예다 — 내 작업용 하네스에서 보조 앱이 꺼진 채 권한 훅만 살아 있으면 내가 거절하지 않았는데 「도구 사용이 거절됐다」가 뜬다. 실기기 런에 권한 훅은 없지만, 증상이 조용해서 무인 런을 통째로 날린다는 성격이 같다.

여기서 값을 치른 교훈은 감지 축을 분리해야 한다는 것이다. 「실패를 줄이자」와 「대기를 없애자」는 반대 방향의 작업이고, 타임아웃을 늘리면 실패는 줄지만 대기는 늘어 무인에서는 그 교환이 손해다. 그리고 무증상 정지는 에러가 0인 상태라서, 에러를 세는 축으로는 영원히 안 잡힌다. 종료 코드가 0인 채로 아무것도 안 한 런이 얼마나 비싼지는 무인 회귀에서 제일 무서운 것에 따로 적었다.

정답을 아는 시험지로 돌리면 실패의 주어가 정해진다

이 절의 주어는 밤샘 300 사이클이 아니라, 그 앞에 있던 126케이스 시트 런을 반복하며 자동화를 고친 루프다. 두 루프는 다르다.

그 루프의 설계가 특이했다. 검증 대상 빌드를 이미 전부 통과하는 것으로 확인된 케이스 세트로 돌렸다. 그러면 전제가 하나 선다 — 제품이 전건 통과라면, 자동화가 내는 모든 FAIL 은 자동화 자신의 결함이다. 자동화 도구를 만들 때 제일 어려운 질문이 「이 실패가 제품 문제인가 내 문제인가」인데, 정답을 아는 시험지로 채점하면 그 질문이 통째로 사라진다.

이 질문이 왜 비싼지는 겪어 보면 안다. FAIL 하나를 앞에 두고 제품 쪽을 의심하면 빌드를 바꿔 가며 재현을 해야 하고, 내 쪽을 의심하면 스크립트를 뜯어야 한다. 어느 쪽인지 모르면 둘 다 해야 하고, 대개는 둘 다 하다가 어느 쪽도 못 끝낸다. 시험지의 정답을 미리 아는 상태로 만들면 그 갈림길 자체가 없어진다 — 모든 FAIL 의 주어가 처음부터 「나」다.

물론 공짜는 아니다. 이 배치에서는 제품 결함을 못 찾는다. 전건 통과가 전제이므로 제품 쪽에서 새로 나온 문제는 정의상 시야 밖이고, 이 루프의 산출은 오직 「자동화가 얼마나 튼튼해졌나」뿐이다. 그래서 이건 회귀 테스트가 아니라 자동화의 자체 시험이고, 두 목적을 한 런에 섞으면 FAIL 의 주어가 다시 흐려진다.

사이클 규약도 못 박았다. 돌린다 → 진단한다 → 고친다 → 원장에 적는다 → 다시 돌린다. 진단 없는 재실행은 금지. 이 한 줄이 없으면 「한 번 더 돌려 보자」가 손이 먼저 가는 선택이 되는데, 그러면 어쩌다 통과하는 케이스가 손도 안 댄 채로 통과율에만 반영된다. 숫자는 좋아지고 자동화는 그대로다.

종료 조건도 착수 전에 적었다 — 전건 기계 판정 통과 · 차단 0 · 그게 두 사이클 연속. 밤샘 300 사이클은 이 루프가 수렴한 뒤에 돌린 런이고, 그래서 실패 0이 나온 것이다. 두 숫자를 같은 루프의 것으로 읽으면 앞뒤가 안 맞는다.

여기서 값을 치른 교훈은 끝나는 조건을 착수 전에 적어야 한다는 것이다. 안 적으면 끝이 안 난다 — 자동화에는 늘 조금 더 손볼 데가 남아서, 기준이 없으면 「아직 완벽하지 않다」로 무한히 이어진다. 두 사이클 연속을 붙인 것도 같은 이유다. 한 번의 전건 통과와 안정된 루프는 화면이 같고, 그 둘을 가르는 수단은 반복 말고 없다.

무인 반복이 하는 일은 발견이 아니라 누적이다

밤새 돌려서 얻은 유일한 「자동화 결함이 아닌 것」은 자동화가 똑똑해서 찾은 게 아니다. 그 신호는 내내 거기 있었고, 반복이 그걸 무시할 수 없는 크기로 키웠을 뿐이다.

한 밤 300 사이클 가운데 147 사이클의 로그에 같은 경고가 들어 있었다. 렌더 설정값 하나가 파일에는 분명히 지정돼 있는데 실제로는 반영되지 않는다는 내용이다. 새로 생긴 경고가 아니다 — 300번 내내 있었고, 사이클 하나만 놓고 보면 로그 한 줄이라 눈이 그냥 지나간다. 147/300 이라는 형태가 되고 나서야 무시할 수 없어졌다.

같은 비율, 다른 사실 — 표본 크기가 증거의 종류를 바꾼다 손으로 10번 → 5번 「가끔 나네」 — 넘어간다 300 사이클 → 147회 147 = 49%, 무시할 수 없는 크기 ⇒ 비율은 둘 다 절반 가까이로 같다. 다른 것은 표본 크기 하나뿐이다 그래서 판정 항목을 하나 늘렸다 — 런이 끝나면 경고 종류별 빈도표가 같이 나온다 전건 통과인 런에서도 이 표는 나온다. 초록불 뒤에 무엇이 쌓이는지 보는 유일한 창이다 ⚠ 여기서 한 칸 더 안 나간다 — 「제품 결함으로 확정」이 아니라 「자동화 결함은 아니다」까지다
빈도는 사람이 못 세는 종류의 증거다. 사람이 열 번 돌리고 다섯 번 봤다면 그건 인상이지 데이터가 아니다.

이게 무인 반복의 실제 산출이다. 자동화가 똑똑해서 못 보던 걸 본 게 아니다. 판정 로직은 이 경고를 볼 줄도 몰랐고, 지금도 모른다. 달라진 것은 같은 신호를 300번 모았다는 것뿐이고, 그 모으기는 사람이 못 하는 일이다 — 사람이 손으로 300번 돌리려면 열 시간을 앉아 있어야 하고, 그렇게 앉아 있으면 로그를 300번 읽지 못한다.

여기서 비율이 아니라 표본이 일한다는 점이 중요하다. 사람이 열 번 돌려 다섯 번 봤어도 비율은 똑같이 절반이다. 그런데 그건 「가끔 나네」로 남고 아무 조치도 안 부른다. 같은 절반이 147/300 이 되면 버그 리포트를 쓸 수 있는 형태가 된다. 비율은 동일한데 한쪽만 행동을 부르는 것이고, 차이를 만든 것은 통찰이 아니라 반복 횟수다.

이 발견 뒤에 판정 항목을 하나 늘렸다. 런이 끝나면 경고 종류별 빈도표가 같이 나온다. 중요한 것은 이 표가 전건 통과인 런에서도 나온다는 점이다. 전부 초록불인 런에서 무엇이 쌓이고 있었는지를 보는 유일한 창이라, 통과 여부와 분리해 두지 않으면 통과한 밤에는 아무도 안 본다. 실패한 밤에만 로그를 여는 습관은 자연스럽지만, 이 종류의 신호는 정확히 성공한 밤에 쌓인다.

여기서 값을 치른 교훈은 빈도표를 붙이고 나서 드러났다. 셀 수 있게 된 것은 빈도뿐이고 기점은 여전히 못 센다 — 이 신호가 언제부터 있었는지 아무도 모른다. 첫 밤의 로그에도 있었는지, 그 전 빌드에도 있었는지 확인할 방법이 없다. 그래서 여기서 한 칸 더 나가지 않는 것이 중요했다. 이건 제품 결함으로 확정된 것이 아니라 「자동화 결함은 아니다」까지다.

기기 의존은 맨 아래 한 층뿐이었다

이 절의 숫자는 같은 케이스 세트를 PC 빌드로 돌린 런의 것이다. 실기기용으로 쓴 케이스 126개를 다른 플랫폼에 옮겨야 했는데, 다시 쓰는 대신 전송층만 갈아끼웠다 — 하네스가 기기 명령을 기대하는 자리에 파일 브리지를 끼우고 명령 표면은 그대로 뒀다. 결과는 PC 빌드 풀런 126케이스 937초 완주였다.

통한 이유는 설계를 잘해서가 아니다. 케이스가 애초에 기기에 관심이 없었기 때문이다. 케이스가 아는 것은 «여기를 누르고, 기다리고, 이게 떴는지 확인한다» 뿐이고, 그 문장 어디에도 안드로이드가 들어가지 않는다. 기기라는 단어는 맨 아래 통로 한 곳에만 있었고, 거기만 바꾸자 위쪽 126개가 손 하나 안 대고 넘어왔다.

그런데 이 결과를 「이식성 좋은 설계」로 읽으면 안 된다. 그 한 층에 실제로 무엇이 남아 있는지를 세어 보면 답이 나온다 — 남아 있는 것은 기기 명령을 보내고 받는 통로 하나뿐이고, 그게 한 층에 모여 있었던 것은 애초에 그 자리에 다른 선택지가 없었기 때문이다. 화면을 붙잡을 방법이 없어서 조작이 좌표 탭 하나로 좁혀졌고, 좁혀진 표면은 갈아 끼우기도 쉽다. 제약이 만들어 준 이식성이지 내가 설계한 이식성이 아니다.

비교해 보면 분명하다. 접근성 트리가 제대로 나오는 앱이었다면 케이스는 당연히 그 트리를 썼을 것이다 — 「이름이 확인 인 버튼을 누른다」가 「좌표 (620, 880) 을 누른다」보다 모든 면에서 낫기 때문이다. 그리고 그 순간 케이스마다 그 플랫폼의 어휘가 박히고, 이식은 한 층 교체가 아니라 126개 재작성이 된다. 이번에 그 일을 안 겪은 이유는 내가 잘 참아서가 아니라 쓸 수 있는 어휘가 처음부터 없었기 때문이다.

그래서 이 이식이 알려 주는 것은 설계 원칙이 아니라 점검 항목 하나다. 케이스 본문을 열어 플랫폼 고유 단어가 몇 개나 들어가 있는지 세어 보면, 그 세트가 다음 플랫폼에서 얼마나 살아남을지가 대충 나온다. 이번 세트에서 그 숫자는 0이었다.

여기서 값을 치른 교훈은 이 성공을 근거로 쓰면 안 된다는 것이다. 기기 어휘가 한 층에만 있었던 건 반쯤 운이었고, 다음 플랫폼에서 같은 운을 기대할 근거가 없다. 조작 표면이 넓은 플랫폼 — 접근성 트리가 제대로 나오는 앱 — 으로 옮기면 케이스가 그 트리의 어휘를 쓰게 되고, 그 순간 기기 의존은 한 층이 아니라 전 층에 퍼진다. 이번에 안 겪은 이유는 처음부터 쓸 수 있는 어휘가 없었기 때문이다.

못 한 것

  • 단계별 소요 수치를 성능 지표로 인용할 수 없다. 왜 못 쓰는지는 루프 쪽에 적어 뒀다 — 요약하면 그 숫자에 제품 시간과 내가 넣은 대기가 함께 들어 있고, 둘이 분리돼 있지 않다. 분리하려면 계측을 따로 붙여야 하는데 아직 안 붙였다. 그래서 이 글의 어떤 수치도 「제품이 몇 초」로는 못 읽는다.
  • 배터리 연속 주행 한계는 안 재봤다. 순감 1%p 는 측정 해상도와 같은 크기라 실제 순감이 0.01%p 일 수도 1.99%p 일 수도 있다. 한 스텝 변화로는 배율을 말할 수 없으므로 「며칠 연속 가능」은 이 글에 쓰지 않는다. 승격 경로는 있다 — 배터리를 %가 아니라 mAh 로 남기면 다음 밤에 잰다.
  • 좌표 폴백의 계수를 안 세고 있다. 이 결함은 남의 디바이스 팜이 화면에 그 계수를 띄우는 것을 보고 알았다 — 내 리포트에는 그 칸이 없다. 없으면 초록불의 질이 떨어지는 것을 초록불로만 보게 된다. 무인 회귀에서 제일 나쁜 종류의 눈금 부재다.
  • 자동화가 대상을 얼마나 방해하는지 재본 적이 없다. 재는 도구가 대상을 얼마나 느리게 만드는지를 같이 재서 내놓은 사례를 봤는데, 내 쪽에는 그 값이 아예 없다. 그래서 이 글의 사이클 분포에 자동화 자신의 오버헤드가 얼마나 섞여 있는지도 모른다.
  • 시각 버그는 여전히 못 본다. 판정을 로그에 둔 구조의 대가다. 텍스처가 깨져도, 이펙트가 안 나와도, 상태 전이와 RPC 가 정상이면 통과로 찍힌다. 그 자리는 사람이고, 무인이 커질수록 그 사람의 몫이 어디인지가 더 흐려진다.
  • 임계값을 재측정하지 않았다. 남의 발표에서 본 프레임·로딩 임계 몇 개는 원 자료가 스스로 「재측정 필요」로 태그해 둔 값이라 인용하지 않았고, 내 쪽에서 다시 잰 적도 없다.
  • 표본이 밤 하나다. 300 사이클은 한 기기·한 빌드·한 밤의 분포다. 여러 밤·여러 기기의 분산은 재지 않았고, 그래서 「최대가 중앙값의 1.45배」가 이 밤의 특성인지 이 셋업의 특성인지 모른다.
  • 이 목록의 절반은 「안 재서 모른다」이지 「재봤더니 괜찮다」가 아니다. 무인 회귀에서 제일 위험한 문장이 「지금까지 아무 일 없었다」라서, 안 잰 자리를 이름 붙여 남기지 않으면 그게 근거처럼 쌓인다.