옮겼다가 아니라 잃은 게 없다로 기준을 바꾼 노션 이관
문서 5,148개와 댓글 2,131건을 자체 빌드한 사내 워크스페이스로 옮기면서, 계수 게이트가 초록불인데 화면이 깨져 있던 1차 실패를 기계 전수 대조와 실브라우저 페어 대조 두 레인으로 다시 판정한 프로젝트
Problem
1차 이관은 완료로 보고됐지만 감사 수치가 전부 통과인 채로 화면은 못 쓸 수준이었다. 표가 행·열 역순인데 블록 수와 글자 수가 일치했고 제목 중복은 오히려 계수를 좋게 만들었으며, 처리 건수·워커 로그·완료 알림이 전부 임포터 자신의 보고라 스스로 버린 것은 실패로 세지 않았다
Hypothesis
성공 기준을 임포터가 몇 건을 처리했는가에서 원본과 맞대 봤을 때 잃은 글자가 있는가로 옮기면 자기 보고로는 절대 안 잡히는 결함이 드러난다고 보았다. 6,027 페이지를 사람이 다 볼 수 없으므로 기계가 전량을 덮고 사람은 기계가 가리킨 것만 실브라우저로 보는 2레인 구조를 설계했다
Solution
전수 텍스트 대조 하네스를 세우기 전에 그 하네스 자신의 거짓 차이 다섯 종을 먼저 잡았고, 이관을 8차까지 돌리며 임포터 결함 여섯 종을 수리했다. 시각 레인은 시드 고정 무작위 추출로 표본 선택의 재량을 없애고 필수 포함 두 건을 스크립트가 강제하게 했으며, 내보내기에 아예 없는 데이터베이스 뷰 유형은 aside로 노션 원본을 열어 읽어 왔다
Result
문서 5,148개와 첨부 20기가바이트를 옮기고 댓글은 원본 2,131건과 저장 2,131건이 부족도 초과도 없이 맞았다. 전수 대조 95.5퍼센트에 미통과 229건은 전부 유실이 아니었고, 확정된 진짜 유실은 2단 불릿 1,137자·13페이지다. 보고서에 크게 적은 숫자는 99.99퍼센트가 아니라 남은 1,137자였다
Lesson Learned
게이트가 초록불이라는 것은 결함이 없다는 뜻이 아니라 그 게이트가 그 결함을 보지 않는다는 뜻이다. 쓰기 검증에서 내가 만든 트리 둘을 비교하는 바람에 163페이지의 순서가 무너지는 동안 163번 초록불이 켜졌고, 검증은 서버가 실제로 저장한 것을 되읽어야 성립한다는 것을 그 대가로 배웠다
문제 정의
계수는 전부 통과인데 화면이 달랐다
자체 빌드한 사내 워크스페이스가 프로덕션에 올라간 뒤, 남은 일은 회사가 몇 년간 쌓아 온 노션 워크스페이스를 통째로 옮기는 것이었다. 내보내기 기준 6,027 페이지였고 최종적으로 문서 5,148개와 첨부 4,388개 20기가바이트, 댓글 2,131건이 대상이 됐다. 자동 임포터가 이미 있었으므로 얼핏 실행만 남은 일처럼 보였다.
1차 이관은 끝났다고 보고됐고, 실제 판정은 정반대였다. 감사 수치는 전부 통과인데 사람이 열어 본 화면이 못 쓸 수준이었다. 문제를 키운 것은 그 보고를 쓴 사람이 나였다는 점이다. 나는 시각적으로 확인했다고 적었지만 실제로 한 것은 화면 요소의 개수를 센 것이었고 스크린샷은 열어보지 않았다. 그 과장을 뒤집은 것은 감사기가 아니라 사용자 제보 두 건이었다. 이모지도 컬럼도 데이터베이스 유형도 다르게 표현됐다는 것, 그리고 메인 페이지 말고 하위 페이지에 유실이 많다는 것. 둘 다 사실이었다.
계수 감사가 왜 이것을 못 보는지가 문제의 핵심이었다. 표가 행과 열이 완전히 뒤집힌 채로 들어간 페이지가 27장 있었는데 블록 수와 글자 수는 원본과 정확히 일치했다. 이미지 503개가 이관본 안에서 내보내기 폴더의 상대 경로를 그대로 가리키고 있었는데 업로드 감사에서는 정상으로 보였다. 임포터가 문서 제목을 본문 첫 줄에도 남기는 습관은 글자 수를 오히려 늘린다. 고쳐야 할 결함 다섯 종 중 둘은 고치기 전이 계수상 더 좋아 보였다. 세는 것과 보는 것은 다른 일이었다.
여기에 신호의 출처 문제가 겹쳤다. 이관 도구는 자기가 처리한 건수를 세고, 워커 로그는 버린 페이지가 없다고 말하고, 완료 알림은 빠진 것이 없다고 말한다. 셋 다 임포터 내부의 자기 보고다. 임포터가 스스로 버리기로 한 것은 애초에 실패로 세지 않으므로, 셋이 동시에 초록불인 상태와 문서가 잘려 있는 상태는 얼마든지 공존한다. 이관 실패는 비율로 오지 않는다. 350명이 옮겨 올 도구에서 한 사람이 자기 문서를 열었을 때 뭔가 비어 있으면, 그 사람은 나머지가 멀쩡한지 확인하지 않고 노션을 다시 연다.
제약은 세 겹이었다. 6,027 페이지를 사람이 다 볼 수는 없다. 프로덕션은 구글 SSO 전용이라 평범한 헤드리스 도구로는 로그인한 화면에 도달하지 못한다. 그리고 노션 내보내기가 정보를 두 갈래로 쪼개 놓는다. 마크다운 레인에는 페이지 아이콘도 다단 컬럼도 댓글도 아예 없고 그것들은 HTML 레인에만 있으며, 데이터베이스가 보드인지 캘린더인지는 양쪽 어디에도 없다.
블록 · 글자 수 일치
업로드 정상
글자 수가 오히려 늘어남
행 수 전부 일치
잡지 못함
가설 수립
기준을 "옮겼는가"에서 "잃은 게 없는가"로 옮겼다
물론 도입 검토가 원래 제안한 방식도 있었다. 중요 문서를 골라 검수 목록을 운영하는 것이다. 실행은 가볍지만 문제가 하나 있었다. 무엇을 검수할지 사람이 고른다는 것. 8월 중순까지 시각 검수에는 표본 절차도 판정 기준도 문서로 남은 것이 0건이었고, 그것은 전적으로 실행자 재량이었다는 뜻이다. 그리고 재량은 잘 된 페이지를 고른다. 1차에서 표본 전건 통과가 실제보다 훨씬 강한 보증으로 읽힌 경로가 정확히 그것이었다.
그래서 성공 기준 자체를 바꾸기로 했다. 임포터가 몇 건을 처리했는가가 아니라, 원본과 이관본을 맞대 봤을 때 잃은 글자가 있는가로 판정한다. 자기 보고를 근거에서 빼면 남는 것은 원본뿐이고, 원본과 결과를 맞대는 것 외에는 탐지 경로가 아예 없는 결함이 존재한다는 것이 가설이었다. 그 부류를 하나라도 잡아내면 가설이 증명되고, 못 잡으면 계수 게이트를 계속 믿어도 된다는 뜻이 된다.
기준을 바꾼다고 해서 기존 계수 게이트를 버리는 것은 아니었다. 계수는 여전히 도달하지 못한 문서와 끊긴 첨부를 싸게 잡아내므로 앞단에 그대로 두고, 그 위에 원본 대조를 얹는 구조로 갔다. 계수가 통과하지 못했는데 화면을 보면 원인이 섞이기 때문에, 수치 게이트를 먼저 통과시킨 다음에만 시각 판정으로 넘어가도록 순서를 고정했다. 또 하나 미리 정해 둔 것은 유실과 표현 차이를 같은 목록에 섞지 않는다는 원칙이다. 노션의 어떤 표현은 우리 제품에 대응하는 블록 자체가 없어서 링크나 평문으로 내려앉는데, 그것을 유실로 세면 진짜 유실이 묻히고 반대로 무시하면 사용자가 배신감을 느낀다. 내려앉는 것은 목록으로 만들어 사전에 고지하고, 유실은 0을 목표로 둔다.
전량을 사람이 볼 수 없다는 제약은 레인을 나눠 풀기로 했다. 기계가 전량을 덮고, 사람은 기계가 가리킨 것만 실제 브라우저로 본다. 사람 레인의 도구로는 헤드리스 브라우저 스킬인 aside를 골랐는데, 이유가 속도가 아니라 접근이었다. 프로덕션이 SSO 전용이라 사용자의 로그인 세션을 그대로 물려받는 이 도구 말고는 실제 사용자가 보는 화면에 도달할 수단이 없었다. 노션 원본과 이관본을 같은 세션 안에서 나란히 열 수 있다는 것도 컸다. 목표는 하나로 잡았다. 표현이 달라진 것은 허용하고 사전에 고지하되, 진짜 유실은 0이다.
셋 다 임포터 자기 보고
사라진 글자를 센다
솔루션 도출
기계가 전량을 덮고, 브라우저는 기계가 가리킨 것만 봤다
글자가 사라진 페이지를 잡는다
글자는 있는데 화면이 다른 것을 잡는다
레인 1을 세우자마자 드러난 것은 이관본이 아니라 검증 도구 자신의 결함이었다. 하네스가 만들어 내는 거짓 차이가 다섯 종 있었고, 그것을 고치기 전에는 전건이 차이 판정으로 나와 아무것도 가려낼 수 없었다. 이관본 텍스트를 블록 식별자 해시 순서로 이어 붙이고 있어서 길이가 2,393과 2,356으로 거의 같은 문서가 유사도 0.139로 찍혔다. 그 하나를 고치자 유사도 0.9 이상 구간이 29건에서 273건으로 뛰었다. 링크 정규식이 대상 주소의 괄호에서 잘려 본문에 URL 꼬리를 남기는 문제도 있었는데, 이것은 이미 런북에 함정으로 적혀 있는데도 새 도구를 쓰면서 그대로 다시 밟았다.
도구가 신뢰할 만해진 뒤부터 이관을 반복해 돌렸다. 8차까지 가면서 임포터 결함을 여섯 종 고쳐 포크에 반영했다. 그중 결정적인 하나는 속성이 없는 행 페이지의 첫 본문 블록이 통째로 잘려 나가던 것으로, 125개 페이지에서 10,804자가 사라지고 네 건은 아예 빈 문서가 돼 있었다. 이 결함은 수치 게이트도, 완료 알림도, 워커 로그도 잡지 못했다. 임포터가 스스로 버린 것을 실패로 세지 않기 때문이고, 그래서 원본과 결과를 맞대는 것 말고는 탐지 경로가 처음부터 없었다. 기준을 바꾼 이유가 이 한 건으로 증명됐다.
레인 2에서 먼저 없앤 것은 표본을 고르는 재량이었다. 시드를 고정한 무작위 추출 스크립트로 표본을 뽑고 그 표본 파일을 저장소에 커밋하게 했다. 어떤 표본을 봤는지가 곧 판정의 신뢰도이기 때문이다. 여기에 필수 포함 두 건을 스크립트가 강제하도록 했다. 폴더 자식이 가장 많은 데이터베이스 페이지 하나와 행 하위 페이지 하나인데, 알려진 렌더러 결함 축이 정확히 그 둘에서 터지기 때문이다. 스크립트가 그 둘을 못 뽑으면 그 사실 자체를 결함 신호로 보고에 적게 했다. 판정은 구조와 본문과 이미지·첨부 세 축을 모두 통과해야 그 건이 통과다. aside는 호출마다 새 세션이라는 제약이 있어서 한 페이지의 판정을 한 호출 안에서 끝내도록 절차에 못박았고, 긴 루프에서 데몬이 끊기는 것도 실측해 다섯 건씩 나눠 돌렸다.
브라우저 레인이 실제로 갈라 준 것은 기계가 못 가른 것들이었다. 유사도 0.431로 심각 차이 딱지가 붙었던 페이지가 실화면에서는 157자와 157자로 완전히 일치했다. 원본 마크다운의 이미지 경로 잔재가 만든 거짓 차이였다. 반대 방향의 확인도 됐다. 정상 판정을 받은 표본은 실제로 정상이어서 노션 1,048자와 이관본 1,048자가, 다른 건은 709자와 709자가 머리글자까지 같았다. 기계가 심각으로 분류한 13건을 브라우저로 전수 판정한 결과 콘텐츠 유실은 0이었고, 내역은 이미지만 있는 페이지와 제목 잡음과 하네스 잔재였다.
여기서 이 도구의 쓰임이 하나 늘었다. 검증 수단인 줄 알았던 브라우저가 유일한 데이터 원천이 되는 항목이 있었다. 데이터베이스가 보드인지 갤러리인지 캘린더인지는 노션 내보내기 어디에도 들어 있지 않다. 마크다운도 HTML도 그냥 표 하나로 내려준다. 그 정보를 얻는 경로는 노션 원본을 브라우저로 여는 것뿐이었고, 그렇게 읽어온 87개 데이터베이스 중 29개의 뷰 유형을 이관본에 복원했다. 보드 14, 갤러리 12, 리스트 2, 캘린더 1이다. 같은 방식으로 페이지 아이콘 1,452개와 커스텀 이모지 575건, 335개 페이지의 다단 컬럼을 HTML 레인에서 끌어와 사후 패스로 복원했다.
같은 레인이 유실과 표현 차이도 갈랐다. 노션이 페이지 안에 펼쳐 보여 주던 인라인 데이터베이스가 이관본에서는 링크 한 줄로 평탄화되고, 북마크와 임베드가 평문 주소로 내려앉는다. 데이터는 온전한데 화면만 다른 경우다. 이것은 유실 목록이 아니라 알려진 표현 차이로 분리해 사용자에게 사전 고지했고, 그중 인라인 데이터베이스는 뒤이어 서버에 블록 교체 경로를 새로 만들어 실제로 펼쳐지게 고쳤다. 반대로 진짜 유실로 확정한 것도 하나 남았다. 들여쓰기 없는 2단 불릿을 임포터가 블록으로 만들지 못해 13개 파일에서 29줄 1,137자가 사라지는 건인데, 브라우저를 끝까지 스크롤해도 길이가 변하지 않고 API 대조도 같은 값을 준다는 것까지 확인해 가상화 착시를 배제했다.
댓글은 별도 문제였다. 마크다운 레인에 아예 없어 HTML 레인이 유일한 출처였고, 서버에는 댓글을 쓰는 경로 자체가 없어 포크에 두 개를 새로 만들어야 했다. 원본에 실제로 두 번 올라와 있던 사용자 게시 33건이 중복 정리에 접혀 사라진 적이 있는데, 그 33건이 진짜 이중 게시라는 것도 노션 실화면을 열어 확정했다.
결과 & 배운 점
99.99퍼센트가 아니라, 남은 1,137자를 적었다
최종적으로 문서 5,148개와 첨부 4,388개 20기가바이트가 옮겨졌고, 댓글은 원본 2,131건과 저장 2,131건이 부족도 초과도 없이 맞았다. 전수 텍스트 대조는 5,131쌍 중 4,902쌍이 통과해 95.5퍼센트였고, 통과하지 못한 229건은 조사 결과 전부 유실이 아니라 이관본이 더 크거나 주소 잔재가 남은 경우였다. 확정된 진짜 유실은 2단 불릿 1,137자, 13페이지다. 이관 대상 본문을 기준으로 하면 99.99퍼센트가 살아남았다. 그런데 보고서에 크게 적은 숫자는 99.99가 아니라 1,137이었다. 남은 것을 정확히 적어 두지 않으면 다음 사람이 그 부분을 다시 찾을 방법이 없다.
가상화 착시까지 배제해 확정
시각 게이트의 통계적 한계도 보고서에 강제로 넣게 했다. 표본 20건 전건 통과는 95퍼센트 신뢰로 시각 결함률 15퍼센트 미만을 뜻하고, 이는 비어 있지 않은 페이지 약 4,900장 중 700장이 깨져 있어도 통과할 수 있는 표본이라는 것, 5퍼센트를 방어하려면 표본이 60건 필요하다는 것까지 함께 적는다. 이 문장을 빼면 전건 통과가 실제보다 강한 보증으로 읽히고, 1차 이관에서 정확히 그렇게 됐다.
가장 크게 남은 배움은 게이트가 초록불이라는 사실이 결함이 없다는 뜻이 아니라는 것이다. 그것은 그 게이트가 그 결함을 보지 않는다는 뜻이다. 125페이지가 잘리는 동안 세 신호가 동시에 정상이었던 이유는 셋 다 임포터의 자기 보고였기 때문이고, 나는 그 뒤로 검증을 설계할 때 이 신호가 누구의 말인지부터 확인한다. 같은 부류의 실수를 한 번 더 했다. 쓰기 검증을 하면서 내가 만든 트리 둘을 서로 비교하고 있었고, 그 검사는 구성상 항상 통과하므로 163개 페이지의 블록 순서가 무너지는 동안 163번 연속으로 초록불이 켜졌다. 검증은 서버가 실제로 저장한 것을 되읽어 와야 성립한다는 것을 그 대가로 배웠다.
계수 게이트를 세는 방식도 이 과정에서 바꿨다. 원본의 각 항목이 저장 어딘가에 있는가라는 물음은 손실을 통과시킨다. 댓글 2,131건을 전부 찾고도 총계는 2,101이었다. 이후 부족과 초과를 함께 세는 다중집합 양방향으로 바꿨고, 검수 대상을 배치 로그가 아니라 문서 전수로 잡았다. 로그에서 뽑으면 고아 댓글을 붙인 조상 문서가 빠져 초과 3건을 못 본다. 그때의 0은 없다는 뜻이 아니라 안 봤다는 뜻이었다.
내 측정이 틀리는 방식도 여러 번 봤다. 행 목록을 부르면서 뷰 식별자를 데이터베이스 식별자 자리에 넣어 88개 데이터베이스가 전부 0행으로 보인 적이 있고, 행 제목을 첫 번째 칸에서 뽑다가 분류 칸을 제목으로 읽어 3,814건이 유실됐다는 오진을 낸 적도 있다. 재측정하면 87개가 전부 일치였다. 그래서 내용을 지우는 수정에는 원본 확인 가드를 붙이는 습관이 생겼다. 본문 첫 헤딩이 제목과 같으면 중복이니 지운다는 규칙을 세웠는데, 원본 노션 본문에도 그 헤딩이 있는 페이지가 411개였다. 규칙만 믿고 돌렸으면 411개에서 진짜 내용을 지웠을 것이고, 실제 삭제 대상은 149개였다.
도구에 대한 생각도 바뀌었다. 브라우저 자동화를 그전에는 사람이 하기 귀찮은 클릭을 대신 시키는 수단으로 봤는데, 이번에는 그것이 판정의 근거이자 어떤 정보의 유일한 출처였다. 기계가 심각이라고 부른 것을 기각하고, 데이터는 멀쩡한데 화면만 다른 경우를 유실 목록에서 덜어 내고, 내보내기에 아예 없는 데이터베이스 뷰 유형을 원본 화면에서 읽어 오는 일은 전부 실제 브라우저에서만 가능했다. 함께 배운 것은 재량을 없애는 쪽이 더 중요했다는 점이다. 표본을 사람이 고르는 한 결과는 고르는 사람의 기대를 닮고, 시드를 기록하는 한 줄이 그 편향을 없앴다.
Supercent
에서의 다른 경험도 살펴보기
완성 파일이 아니라 편집 상태를 내놓은 AIDRA 팟캐스트 스튜디오
프리미어 프로에서 소스를 하나씩 얹던 오디오 드라마 제작 공정을 웹으로 옮기고, 회차 본문이 낳은 대본을 유일한 입구로 삼아 가편집된 타임라인까지 자동으로 채운 프로젝트
완제품을 버리고 공개 소스로 다시 빌드한 사내 워크스페이스
350명이 쓰던 노션을 대체하려다 완제품 무료판의 좌석 잠금과 유료판의 개조 금지 약관에 동시에 막혀, 공개 AGPL 소스를 포크해 직접 빌드하는 경로로 갈아탄 프로젝트
말해줄 때까지 기다리지 않는 AIDRA 피드백 트리아지 루프
물어봐야 조금씩 말해주던 파트너팀 피드백을, 인앱 티켓과 회신이 한 바퀴 도는 루프로 바꾸고 그 운영 절차를 스킬로 코드화해 다섯 주간 열세 번 반복한 프로젝트