현장 데이터를 꺼내고 옮길 때

없다, 잘렸다, 바뀌었다

품앗이가 협동조합과 의료복지사협, 로컬푸드 매장 현장에서 데이터를 꺼내고 옮기다 넘어진 기록을 다른 조직이 다시 쓸 수 있게 옮긴 글입니다. 조직과 사람, 업체와 제품 이름은 지웠습니다. 데이터베이스를 왜 먼저 세우는지와 구축 순서는 AX의 첫 세팅은 데이터베이스 구축에, 유일본 확보와 고유키 없는 표, 덤프와 권한 같은 함정은 데이터베이스 구축의 의미 3절과 4절에 있어서 여기서는 되풀이하지 않습니다.

요지

우리 기록에 따르면 현장 데이터는 대개 이미 있고, 사고는 세 자리에서 났습니다. 「없다」를 너무 빨리 말할 때, 데이터가 조용히 잘릴 때, 옮기는 길에 글자나 뜻이 바뀔 때입니다. 셋 다 오류 메시지가 뜨지 않았다는 점이 같았습니다.

1. 「없다」를 말하기 전에

빈 칸 하나로 개념 전체가 없다고 읽지 않습니다. 우리 기록에 따르면 한 협동조합의 조합원 명부 2,052명을 보고 「전화번호가 든 건은 1건뿐」이라고 보고했습니다. 품앗이가 본 칸은 유선 전화 칸이었고, 휴대폰 칸에 2,039건, 99.4%가 들어 있었습니다. 업무 프로그램은 같은 뜻을 유선, 휴대폰, 비상연락, 보호자처럼 여러 칸으로 나눠 둡니다. 그래서 채운 수가 0이나 1인 칸은 「없다」가 아니라 「여기가 아니다」라는 신호로 읽습니다.

그 오보는 조합에 전달된 문서에 실렸고, 「문자로는 연락할 수 없으니 QR이 유일한 통로」라는 설계 근거까지 그 위에 세워졌습니다. 정정은 기록만 고치지 않고 조합에도 보냈습니다. QR은 계속 쓰기로 했지만 이유가 「유일한 통로라서」에서 「빠르고 싸고 회수율이 높아서」로 바뀌었습니다. 결론이 같아도 근거는 다시 봐야 한다는 것이 그 기록의 교훈입니다.

  • 열 이름을 전수로 훑어 같은 뜻을 담을 만한 후보 칸을 다 세고, 칸마다 채운 수를 같이 낸다.
  • 찼다고 판정하면 형식까지 본다. 기록에 따르면 그 명부의 휴대폰 칸은 정상 형식이 2,029건, 서로 다른 번호가 1,869개였다.

「자료에 없다」와 「우리가 못 꺼낸다」는 다른 말입니다. 한 의료복지사협 홈페이지를 다시 지을 때 신청 양식 세 종에 개인정보 동의문이 빠져 있었습니다. 옛 사이트를 통째로 받아 온 사본에 그 쪽이 없었기 때문인데, AI는 「조합에 문안을 요청하자」고 올렸습니다. 사본에 없던 이유는 로그인이었고, 우리는 전날 받은 관리자 계정을 쥐고 있었습니다. 로그인해서 요청 세 번으로 원문을 꺼냈습니다.

없다고 말하기 전에 「왜 없나」를 묻고, 쥐고 있는 열쇠를 셉니다. 계정, 토큰, 접속 세션, 사본, 백업이 그 열쇠입니다. 사람에게 요청하는 것은 가장 비싼 선택지라고 품앗이는 봅니다. 다만 조직이 애초에 가진 적 없는 문안도 있었습니다. 그런 것은 원문을 달라고 하지 않고 우리가 초안을 만들어 확인을 받습니다.

원본을 파싱하기 전에 사람이 만든 정리본부터 엽니다. 한 의료복지사협의 소모임 폴더 304개 파일을 DB로 옮기면서 한글 문서 파서와 문자 인식 파서부터 만들었습니다. 한 시간 뒤 같은 폴더 맨 아래 엑셀을 열어 보니 담당 직원이 32개 반, 200명 명단을 이미 표로 정리해 두었고, 파서가 뽑은 값은 전부 그 표의 일부였습니다. 원문에 따르면 사람이 손으로 다루던 폴더에는 거의 늘 그 사람이 만든 현황표가 있습니다. 그 기록은 그 현황표를 정본으로, 파서를 검산용으로 봅니다.

데이터가 지나는 길의 지도부터 그립니다. 한 생협 매장에서 「어제 매장 채소 판매 내역」을 물었는데 AI가 「없다」고 답했습니다. 원천 프로그램에는 175건이 있었습니다. 수집 단계의 이름 필터가 매장이 직접 들여온 상품 코드를 전부 빼고 있었습니다. 원천, 수집, 변환, 저장의 흐름을 모르면 질의를 아무리 고쳐도 「없다」가 나옵니다. 이 지도는 AX의 첫 세팅은 데이터베이스 구축의 첫 단계인 자료 지도와 짝을 이룹니다. 자료 지도가 「무엇이 어디 있나」라면, 이 지도는 「어떤 길로 흘러오나」입니다.

검색 범위가 틀리면 「없다」가 나옵니다. 한 현장 PC에서 사용자 폴더만 뒤지고 「문서가 없다」고 판단했는데, 자료는 드라이브 맨 위의 업무 프로그램 폴더에 있었습니다. 같은 작업에서 확장자 아홉 가지마다 디스크 전체를 따로 훑어 아홉 번을 돌았고, 결과를 맨 끝에 한 번만 쓰게 짜서 도는 동안 진행률을 말할 수 없었습니다. 먼저 드라이브의 생김새를 보고 범위를 정합니다. 디스크는 한 번만 훑으면서 확장자로 거르고, 결과는 흘려 쓰면서 진행이 보이게 합니다.

공백 문자열을 값으로 읽지 않습니다. 한 매장 판매 데이터에서 카드 승인번호 칸이 전부 차 있는 것을 보고 「카드 단말기가 연동돼 있다」고 말한 일이 있습니다. 실제로는 고정 길이 문자 칸이 공백 30자로 채워진 채 넘어온 것이었고, 공백을 걷어 내니 전부 비어 있었습니다. 같은 표를 다루는 기존 질의는 이미 공백을 걷어 내고 있었습니다. 처음 보는 표는 그 표의 기존 질의 습관부터 베끼고, 「전부 찼다」 같은 전수 단정은 값 몇 개를 눈으로 본 뒤에 합니다.

제도가 종이로 바뀐 자료는 디지털에서 찾지 않습니다. 한 의료복지사협의 조합 자체 화폐 장부가 어느 날짜에서 멈춰 있어 누락이나 장애로 보였는데, 그날 온라인 관리를 그만두고 종이 통장으로 바꾼 것이었습니다. 장부가 멈춰 보이면 먼저 그때 제도가 바뀌지 않았는지 묻습니다.

2. 조용히 잘린다

DB를 REST로 조회하면 1,000행에서 오류 없이 잘릴 수 있습니다. 한 협동조합 업무 화면의 생산자 목록에 259곳 가운데 69곳만 떴습니다. 필터를 걸지 않았는데도 그랬고, 기록에 따르면 우리가 쓰던 조회 계층은 기본 상한이 1,000행이고, 넘으면 오류 없이 앞의 1,000행만 줍니다. 응답 머리글에 전체 행 수가 오지만 보지 않으면 모릅니다. 더 나빴던 것은 같은 코드가 회계 전표를 만들고 있었다는 점입니다. 1,000행이 넘는 농가는 전표가 잘린 채 나갈 뻔했고, 998행인 농가가 있어 한동안 이 문제가 드러나지 않았습니다.

나눠 받되 한 번에 받는 크기를 서버 상한보다 크게 잡으면 더 위험합니다. 5,000행씩 달라고 해도 서버는 1,000행만 주는데 코드는 5,000행을 건너뛰어 다음 덩어리로 가니, 4,000행이 통째로 사라집니다. 서버 상한을 모를 때는 한 번에 받는 크기를 1,000으로 둡니다. 중복 없는 목록이나 합계는 전부 받아 앱에서 세지 말고 DB에 뷰를 만들어 시킵니다.

파서는 마지막 꼬리를 버립니다. 회계 프로그램의 분개 문자열을 정규식으로 긁었는데, 마지막 구분자가 없는 7건이 통째로 빠졌습니다. 구분자를 있어도 되고 없어도 되게 고치자 형식 불일치가 0이 됐습니다. 「거의 다 맞는다」로 넘어가면 금액이 조용히 빈다고 그 기록은 적습니다.

이관에서 빠진 것은 참조를 따라가 실존을 대조해야만 보입니다. 한 자활기관 홈페이지를 통째로 받아 옮기면서 같은 성격의 누락을 하룻밤에 여러 번 냈습니다. 처음에는 스타일과 스크립트, 이미지를 통째로 놓쳤고, 다음에는 인라인 스타일 안의 배경 그림을, 그다음에는 양식 내려받기 파일을, 마지막에는 게시판 본문의 링크 54건을 놓쳤습니다. 받아 온 것 안에서는 다 멀쩡해 보였습니다.

  • 옮기기 전에 산출물의 모든 링크와 그림 주소를 모아 그 파일이 실제로 있는지 대조한다.
  • 확장자 허용 목록으로 거르지 않는다. 허용 목록은 반드시 무언가를 빠뜨린다.
  • HTML 특수문자 표기를 먼저 풀고, 한글 주소는 인코딩한 뒤 대조한다.
  • DB에 든 게시판 본문 안의 링크도 같은 대조를 거친다.

3. 옮기는 길에 글자와 뜻이 바뀐다

MSSQL에서 한글을 내보낼 때는 와이드 문자 모드를 씁니다. 한 매장 판매 DB를 옮길 때 문자 모드로 내보내니 코드페이지를 지정했는데도 한글이 통째로 물음표가 됐습니다. 손실은 내보내는 순간 이미 일어났습니다. 와이드 문자 모드는 UTF-16LE로 변환 없이 써서 손실이 없었습니다. 그 기록에 남은, 그 뒤로 걸린 것들은 이렇습니다.

  • 파일 맨 앞의 바이트 순서 표시가 첫 행 첫 칸에 남는다. 그 칸이 정수 칸이면 그 표만 적재가 실패하니, 파일을 읽은 직후 한 번 털어 낸다.
  • 널 바이트는 Postgres 텍스트 칸에 들어가지 않는다.
  • 구분자는 탭이나 줄바꿈 대신 본문에 나올 리 없는 여러 글자 조합으로 정하고, 파싱한 뒤 행마다 필드 수를 칸 수와 대조한다.
  • 빈 표는 0행이 정상이니 가짜 경보로 세지 않는다. 그 DB는 605개 표 가운데 350개가 원본부터 비어 있었다.

그 이관은 605개 표, 962,860행을 적재 오류 0으로 마쳤습니다. 판정은 행 수 일치, 필드 수 불일치 0, 한글을 눈으로 확인하는 세 가지였습니다.

칸 이름 없이 온 덤프는 추측으로 이름을 붙이지 않습니다. 한 협동조합 회계 프로그램의 업체 백업은 데이터만 있고 표 구조와 칸 이름이 없었습니다. 그 기록에 남은 방법은 네 가지입니다.

  1. 프로그램에서 뽑은 엑셀을 정답지로 삼아 키로 이어 붙이고, 칸 쌍마다 값이 얼마나 일치하는지로 이름을 정한다. 거래처 351행을 이어 붙여 11칸을 정했다.
  2. 정답지가 없으면 데이터 안의 규칙을 쓴다. 계좌 내역에서 「잔액은 직전 잔액에 입금을 더하고 출금을 뺀 값」이 38,758쌍 가운데 99.96%에서 성립해 세 칸이 한꺼번에 정해졌다.
  3. 상위 보고서와 원 단위로 맞춘다. 기록에 따르면 합계잔액시산표와 분개 집계가 84개 계정 가운데 82개에서 원 단위까지 일치했다.
  4. 나중에 DB에 직접 붙을 수 있게 되면 진짜 칸 이름을 받아 앞의 결과를 검산한다. 그 현장에서는 거꾸로 짐작한 이름이 전부 맞았다.

그 기록에 따르면 함정이 둘 있었는데, 하나는 구별력 없는 값의 일치였습니다. 한 칸이 엑셀 머리글 21개와 동시에 100% 일치했는데, 그 칸 값이 전부 0이라 아무 이름에나 맞았던 것입니다. 양쪽 칸 모두 서로 다른 값이 셋 이상일 때만 세고, 한 칸에 한 이름만 짝짓게 고치자 정해진 칸은 21개가 아니라 4개였습니다. 적게 나오는 것이 정직한 결과였습니다. 다른 하나는 시점이 다른 자료를 맞댄 것입니다. 시산표를 저장한 뒤 최신 데이터를 받는 사이에 전표 258건이 들어와 27개 계정이 어긋나 보였는데, 같은 시점의 데이터로 다시 맞추자 82개가 일치했습니다.

옛 시스템의 칸 이름을 화면 이름으로 그대로 내보내지 않습니다. 한 의료복지사협의 조합원 조회 도구가 옛 시스템의 「돌봄」 칸을 화면에 「돌봄」이라고 찍었습니다. 그 칸에는 실제로 분류 밖의 금액, 예를 들어 비급여 약값이 흘러들어 있었고, 원장의 AI 비서가 화면의 「돌봄」을 돌봄 비용으로 읽었습니다. 부주의가 아니라 도구가 그렇게 말한 것이라고 그 기록은 봅니다. 원장이 「그게 어떤 내용이고 출처가 어디냐」고 되물어서 잡혔습니다.

  • 원본 칸임을 이름에 드러내서, 「돌봄」 대신 「돌봄칸」으로 적는다.
  • 판단의 근거가 되는 칸, 이 경우 품목명을 같은 화면에 붙인다. 상세 화면에만 있고 요약 화면에 없으면 없는 것과 같다.
  • 오해할 수 있는 칸은 출력 맨 위에 주의 한 줄을 늘 찍는다. 주의 문구는 문서가 아니라 출력에 있어야 읽힌다.
  • 사용자가 출처를 되물으면 그것은 불신이 아니라 검산이니, 그 물음에 답할 자리를 도구에 남긴다.

「전체」 같은 만능 값을 의심합니다. 한 매장 판매 표의 매장 구분 칸에 「전체」 값이 섞여 있었는데, 그 행들이 매장별 행의 합과 정확히 같았습니다. 수집 코드가 전체 자료와 매장별 자료를 둘 다 기록하고 있었고, 구분 칸으로 거르지 않고 더하면 그 날짜 이후 판매가 두 배로 잡혔습니다. 집계하기 전에 구분 칸에 어떤 값이 몇 줄씩 있는지, 그 값이 무슨 뜻인지부터 봅니다.

4. 손으로 만든 것은 그날 파일로

표를 통째로 갈아끼우는 동기화가 그 위에 얹힌 뷰를 조용히 지웁니다. 기록에 따르면 한 협동조합 서버의 동기화는 새 자료를 임시 묶음에 넣고, 이름을 바꿔 끼운 뒤, 옛 묶음을 딸린 객체까지 함께 지우는 방식이었습니다. 그 지우기가 다른 묶음에 있던 회계 뷰 여덟 개까지 끌고 갔습니다. 오류도 로그도 남지 않았고, 아래층을 다시 짓는 스크립트는 제 일을 다 했으니 정상 종료였습니다. 뷰 정의는 어느 파일에도 없어서 여덟 개를 다시 썼습니다.

  • 손으로 만든 뷰, 함수, 권한은 그날 SQL 파일로 남긴다.
  • 갈아끼우는 동기화가 있으면, 같은 스크립트가 지우기 전에 위층까지 다시 짓게 하고 실패하면 비정상 종료하게 한다.
  • SQL 파일은 한 트랜잭션으로 묶고, 끝에 행 수와 객체 목록을 스스로 대조하는 블록을 둔다.
  • 판정은 개수가 아니라 이름 목록으로 한다. 하나가 사라지고 하나가 새로 생기면 개수는 그대로다.
  • 뷰가 맞다는 증거는 행 수가 아니라 금액 합계처럼 뜻이 있는 값이다. 그 현장은 매출 계정 대변 합계가 기존 문서 수치와 원 단위까지 맞는 것으로 확인했다.
  • 해마다 새로 생기는 표를 이름으로 박아 합친 뷰는 새해가 오면 조용히 멈춘다.

같은 이름의 DB 파일이 둘이면 앱은 조용히 빈 쪽을 봅니다. 기록에 따르면 한 교육기관의 자체 서버에서 어느 날부터 로그인이 전부 거부됐고, 7일 동안 아무도 몰랐습니다. 같은 이름의 DB 파일이 두 곳에 있었는데, 환경 변수가 빠지자 앱이 자기 옆 폴더의 빈 DB를 열었습니다. 그 파일은 표 구조는 있고 행만 0개라서, 프로그램이 죽지도 않고 「그런 사용자 없음」만 조용히 돌려줬습니다. 그 기록은 빈 DB가 없는 DB보다 위험하다고 봅니다. 없으면 바로 터지지만, 비어 있으면 정상처럼 보이기 때문입니다.

  • 환경 설정 파일은 앱 폴더 관례로 찾지 말고, 서비스 설정이 실제로 읽는 파일을 확인한다. 그 서버는 앱 폴더가 아니라 시스템 설정 폴더의 파일을 읽고 있었다.
  • 고친 뒤에는 실행 중인 프로세스에 변수가 실제로 닿았는지까지 본다.
  • 같은 기록에서 로그인 주소에 대한 실패 55건을 고장으로 읽을 뻔했다. 시각과 출발 주소를 펴 보니 한 주소에서 한 초에 55번이 몰린 자동 탐색 봇이었으니, 횟수보다 분포를 먼저 본다.

5. 회계 SaaS는 백업 칸이 아예 없을 수 있다

프로그램마다 데이터가 어디 있고, 직접 닿을 수 있는지, 꺼낼 수단이 있는지 세 칸을 재는 방법과 약관이 「데이터는 이용자 PC에 있다」고 적는데 실제로는 업체 서버에 있던 사례는 무엇을 지을지 2절에 있습니다. 여기에는 그 세 칸이 가장 나쁘게 나온 경우 하나를 덧붙입니다.

기록에 따르면 한 협동조합의 회계 SaaS는 데이터가 업체 클라우드에만 있었습니다. 공식 매뉴얼 전체를 검색해 보니 백업, 내보내기, API가 한 번도 나오지 않았고, 꺼낼 길은 화면마다 붙은 엑셀 내려받기뿐이었습니다. 해지하면 자료를 통째로 들고 나올 수단이 없다는 뜻입니다. 같은 제품은 국세청 인증서와 은행, 카드사 계정을 등록해 자동으로 자료를 모으는 기능을 두었습니다. 그 조직의 금융과 세무 계정이 업체 쪽에 모이는 구조입니다.

  • 개인정보처리방침의 처리 위탁, 국외 이전, 보유 기간 절을 같이 읽는다. 그 제품은 위탁에 국외 이전이 있었고, 해지한 뒤에도 5년을 보관했다.
  • 자동 수집용 계정이 등록된 PC는 보안의 우선순위를 높인다.
  • 화면 엑셀 내려받기가 유일한 출구라면, 그 파일을 자동으로 거둬 오게 걸어 둔다. 그 현장의 내려받기 파일은 암호 없이 바로 열렸다.

처방 정리

  • 「없다」를 말하기 전에 같은 뜻의 다른 칸, 쥐고 있는 열쇠, 사람이 만든 정리본, 데이터가 지나는 길을 차례로 본다.
  • REST 조회는 전체 행 수를 확인하고, 나눠 받는 크기는 서버 상한을 넘기지 않는다.
  • 옮기기 전에 참조를 따라가 실존을 대조한다.
  • 한글을 내보낼 때는 무손실 모드를 쓰고, 칸 이름은 증거로 정하되 구별력 없는 일치는 세지 않는다.
  • 옛 칸 이름은 원본임을 드러내고, 근거 칸을 같은 화면에 붙인다.
  • 손으로 만든 DB 객체는 그날 파일로 남기고, 동기화가 지우기 전에 다시 짓게 한다.

도구 메모

  • 후보 칸 찾기: DB의 칸 목록 표에서 칸 이름을 tel, phone, mobile, 연락, 전화 같은 낱말로 거르고 칸마다 채운 수를 센다.
  • REST 나눠 받기: 요청 머리글 Range로 0-999, 1000-1999처럼 받고, 받은 덩어리가 요청보다 작으면 멈춘다. 응답의 Content-Range 끝 숫자가 전체 행 수다.
  • MSSQL 내보내기: bcp "질의" queryout 출력.dat -w -t "~@~" -r "~#~\n". 읽을 때는 utf-16-le로 열고 첫 글자의 바이트 순서 표시를 지운다.
  • 동기화 점검: 실행 뒤 기대하는 뷰 이름 목록과 실제 목록을 비교해 다르면 비정상 종료한다.
  • 서비스 환경 확인: systemctl show 서비스 -p EnvironmentFiles -p Environment.

관련 노드