JSON 라이브러리를 바꾸는 일은 대개 성능 개선처럼 보인다. 같은 문자열을 조금 더 빨리 읽고, 메모리를 덜 쓰면 되는 일. 그런데 그 문자열을 어디까지 받아들일지 달라진다면 이야기가 조금 복잡해진다. 예전에는 정상 요청이던 것이 오류가 되고, 반대로 서로 다른 이름이라고 생각한 필드가 같은 값으로 읽히던 일이 사라질 수도 있다.
11월 4일 Go 공식 블로그는 Go 1.27에서 안정화된 encoding/json/v2와 encoding/json/jsontext를 설명했다. 새 버전 번호보다 눈에 들어온 것은 기본 동작의 변화였다. 중복된 멤버 이름은 오류로 처리하고, 구조체 필드 이름은 기본적으로 대소문자를 구분해 찾는다. 기존 encoding/json을 쓰는 프로그램이 전부 새 동작으로 옮겨가야 한다는 뜻은 아니다. 기존 패키지는 계속 지원되며, 새 API를 선택하는 쪽에서 차이를 살펴야 한다.
나는 이런 변경을 볼 때 성공한 요청보다 예전에 어찌어찌 통과하던 요청이 먼저 궁금하다. 정상 입력만 모아둔 테스트에서는 라이브러리를 바꿔도 아무 일도 없을 수 있다. 실제 서비스에는 이름이 조금 다른 필드, 두 번 들어간 키, 오래된 클라이언트가 보내는 값도 도착한다. 파서가 그 차이를 조용히 정리해주고 있었다면, 그동안 애플리케이션이 어떤 입력에 기대고 있었는지도 함께 드러난다.
같은 이름이 두 번 들어간 요청
가상의 미리보기 API를 하나 생각해보자. 요청에 mode를 넣어 작업 방식을 고른다. 다음 두 문자열은 겉으로 보면 모두 작은 객체다.
{"mode":"preview"}
{
"mode":"preview",
"mode":"apply"
}
두 번째 요청을 받아들인 뒤 값 하나만 남긴다면, 그 값에는 이미 해석이 들어가 있다. 호출한 사람은 미리보기를 요청했다고 생각하고, 다른 쪽은 마지막 값을 읽었을 수도 있다. Go의 새 API는 기본 설정에서 중복 이름을 거절한다. 어느 값을 선택해야 맞는지 추측하는 대신, 입력에 문제가 있다는 사실을 돌려주는 쪽이다.
이때 테스트에 남길 것은 최종 구조체 하나만이 아니다. 처음 들어온 문자열과 오류 여부를 같이 남겨야 한다. 객체로 한 번 변환한 뒤 다시 JSON을 만들면 중복 키 자체가 이미 사라질 수 있기 때문이다. 테스트를 위한 정리가, 확인하려던 입력을 고쳐버리는 셈이다.
아래 표는 실제 서비스 결과가 아니라 마이그레이션 전에 보낼 입력을 정리한 예제다. 마지막 행은 파싱이 끝난 다음에도 남는 일을 보여준다.
| 보낼 입력 | 먼저 살필 것 | 애플리케이션에 남는 판단 |
|---|---|---|
{"mode":"preview"} |
정상 필드가 읽히는가 | preview가 허용된 작업인가 |
{"mode":"preview","mode":"apply"} |
중복 이름을 거절하는가 | 호출자에게 어떤 오류를 돌려줄 것인가 |
{"MODE":"preview"} |
구조체 필드 이름과 맞는가 | 알 수 없는 필드를 무시할지 거절할지 |
{"retries":-3} |
숫자로 읽을 수 있는가 | 재시도 횟수에 음수를 허용할 것인가 |
엄격해진다는 말도 나누어 볼 필요가 있다
대소문자를 구분하는 것과 알 수 없는 필드를 거절하는 것은 같은 설정이 아니다. 패키지 문서는 필드 이름을 맞추는 옵션과 RejectUnknownMembers를 따로 설명한다. 이름이 맞지 않는 값이 기대한 필드에 들어오지 않았다고 해서, 그 요청 전체가 반드시 실패한 것은 아니다. 기본값이 남은 채 다음 로직으로 넘어가는 경우까지 생각해야 한다.
그래서 나는 설정 하나에 붙은 ‘엄격함’이라는 표현으로 서비스의 검증을 대신하고 싶지는 않다. JSON 문법을 읽는 일, Go 값으로 옮기는 일, 그 값으로 특정 작업을 허용하는 일은 서로 이어져 있지만 같은 판단은 아니다. API에서 생략과 null을 나누어 보던 문제도 이 지점과 닿아 있다. 음수인 retries는 JSON 숫자이고 정수로도 표현할 수 있다. 그렇다고 재시도 횟수로 쓸 만한 값이 되는 것은 아니다.
오래된 클라이언트가 대문자 필드 이름을 보내고 있었다면, 새 서버를 배포하는 순간부터 오류 비율만 볼 것이 아니라 실제로 어떤 필드가 비게 되는지도 살펴야 한다. 문서화된 입력과 운영 중 도착하는 입력이 같았는지 확인하는 기회가 될 수도 있다. 기존 동작을 유지할 필요가 있는 부분은 호환 옵션을 검토하되, 왜 유지해야 하는지를 특정 호출자와 입력으로 설명할 수 있으면 좋겠다.
성능 수치는 그 다음에 보고 싶다. 파싱이 빨라졌다는 결과와 요청의 의미가 보존됐다는 결과는 서로 대신해주지 못한다. 우선은 작은 문자열 몇 개를 그대로 남겨두는 쪽을 택하겠다. 새 라이브러리가 얼마나 빨리 읽는지 묻기 전에, 우리가 받아들이기로 한 요청을 같은 뜻으로 읽고 있는지부터.