개발 이야기4분 읽기

갔다가 돌아오면 다른 값이 되는 변환들

ß를 대문자로 바꿨다 되돌리면 ss입니다. 정보를 버리면서 버렸다는 사실을 남기지 않는 변환 여덟 가지.

변환을 하고 되돌리면 원래 값이 나올 것 같습니다. 자주 그렇지 않습니다.

여덟 가지

변환                입력                    왕복 결과              같나
대소문자            ß                       ss                    아니오
케이스 변환         XMLHttpRequest          xmlHttpRequest        아니오
날짜 +1달 -1달      1990-01-31              1990-01-28            아니오
JSON 숫자           1.0                     1                     아니오
JSON 큰 정수        9007199254740993        9007199254740992      아니오
YAML 무따옴표       no                      false (1.1 파서)      아니오
URL 정규화          https://e.com:443/a/../b  https://e.com/b     아니오
부동소수점          0.1 + 0.2               0.30000000000000004   아니오

공통점

전부 변환이 정보를 버리는데, 버렸다는 사실이 결과에 남지 않습니다.

ß를 대문자로 바꾸면 SS입니다. 이 SS를 보고 원래가 ß였는지 SS였는지 알 방법이 없습니다. 그래서 소문자로 되돌릴 때 ss를 고릅니다 — 둘 중 흔한 쪽입니다.

XMLHttpRequestxml_http_request가 될 때 "여기까지가 한 덩어리였다"는 경계가 사라집니다. 돌아올 때는 그 경계를 규칙으로 추측하고, 규칙은 XMLXml로 만듭니다.

1990-01-31에 한 달을 더하면 2월에는 31일이 없으니 말일인 28일로 당깁니다. 여기서 "원래 31일이었다"가 사라집니다. 한 달을 빼면 1월 28일이 나옵니다.

각각이 어디서 문제가 되는가

대소문자는 아이디 비교와 길이 제한에서 걸립니다. 표시용으로 대문자로 바꾼 값을 저장하면 원본이 사라집니다.

케이스 변환은 API 경계에서 걸립니다. 스네이크로 받은 키를 카멜로 바꿔 쓰다가 다시 스네이크로 돌려보내면, 서버가 모르는 키가 됩니다.

날짜 개월 연산은 반복 일정에서 걸립니다. 직전 실행일에서 한 달씩 더하면 1월 31일 시작이 2월 28일, 3월 28일로 굳습니다. 원래 시작일에서 n개월을 더해야 31일이 유지됩니다.

JSON 숫자는 서명에서 걸립니다. {"a":1.0}{"a":1}은 같은 데이터지만 다른 바이트이므로 서명이 달라집니다. 파싱했다 다시 쓰는 미들웨어가 중간에 있으면 웹훅 검증이 실패합니다.

큰 정수는 ID에서 걸립니다. 9007199254740993은 2^53을 넘어 배정밀도 부동소수점으로 정확히 표현되지 않습니다. JavaScript로 파싱하는 순간 마지막 자리가 바뀝니다. 스노플레이크 ID나 트위터 ID가 이 범위에 들어갑니다 — 그래서 그런 API는 ID를 문자열로도 함께 보냅니다.

YAML 무따옴표는 설정 파일에서 걸립니다. 노르웨이 국가 코드 nofalse가 되는 문제가 유명합니다. YAML 1.1이 no·off·n을 거짓으로 읽기 때문입니다.

URL 정규화는 서명된 URL에서 걸립니다. 기본 포트와 ..이 정리되면서 서명 대상 문자열이 달라집니다.

왕복시키지 않는 방법

원본을 정본으로 둡니다. 변환 결과를 저장하지 말고, 필요할 때 변환합니다. 표시용 대문자, 카멜 케이스 키, 정규화된 URL은 전부 파생값입니다.

경계를 한 곳으로 모읍니다. 외부에서 들어올 때 한 번 변환하고 내부에서는 한 표기만 씁니다. 나갈 때 다시 한 번 변환합니다. 중간에서 왔다 갔다 하지 않습니다.

바이트를 보존해야 하는 값은 파싱하지 않습니다. 서명 검증 대상은 원본 바이트를 그대로 들고 있어야 합니다.

모호할 수 있는 값은 따옴표로 감쌉니다. YAML에서 "no", JSON에서 큰 정수를 문자열로.

왕복하는 것도 있다

전부 그런 것은 아닙니다.

base64  인코딩 → 디코딩     원본 그대로
hex     인코딩 → 디코딩     원본 그대로
ROT13   두 번                원본 그대로
NFC     정규화 → 정규화      멱등

이들의 공통점은 정보를 버리지 않는다는 것입니다. base64는 표현만 바꾸고 바이트를 잃지 않습니다. 정규화는 한 번 하고 나면 다시 해도 같습니다.

변환을 고를 때 "정보를 버리는가"를 먼저 보면 왕복 가능 여부가 대체로 따라옵니다.

확인 순서

  1. 변환 결과를 저장하고 있다면 원본도 함께 있는지 봅니다
  2. 같은 값이 두 표기 사이를 여러 번 오가는 경로가 있는지 찾습니다
  3. 서명·해시 대상은 파싱을 거치지 않는지 확인합니다
  4. 2^53을 넘는 정수가 JSON으로 오가는지 봅니다
  5. YAML 설정에서 no·yes·on·off·null 같은 값이 무따옴표로 있는지 봅니다

tools.onuel.dev에서 각각을 직접 넣어 볼 수 있습니다. 케이스 변환기XMLHttpRequest를 넣어 스네이크로 바꾼 뒤 다시 카멜로 돌리면 위 결과가 나오고, YAML 포매터는 무따옴표 값이 어떤 타입으로 읽히는지 보여 줍니다. JSON 포매터1.0과 큰 정수를 넣으면 표기가 바뀌는 자리를 확인할 수 있고, URL 파서는 정규화 전후를 나눠 보여 줍니다. 반대로 정보를 잃지 않는 쪽은 Base64입니다.

  • #변환
  • #데이터
  • #버그