왜 Redis 6에서는 RESP3만 지원할 것인가
[수정! Stack Overflow의 Marc Gravell이 하위 호환성을 위해 연결 단위로 프로토콜을 전환하자고 제안해서 이 모든 내용을 다시 고민하고 있습니다. RESP3를 활성화하는 명령을 보내면 전환할 수 있다는 아이디어입니다. 이렇게 하면 서버 동작을 바꾸는 전역 설정이 더 이상 필요하지 않습니다. 그렇게 보면 훨씬 받아들일 만해서, 이 글의 핵심 취지를 재고하고 있습니다]
Redis 5가 출시된 지 몇 주 뒤, 저는 RESP3 구현을 시작했습니다. 며칠 작업해 보니 마침내 이 일이 현실이 되는 모습을 보는 것이 정말 기쁘게 느껴집니다. RESP3는 Redis 6부터 사용하게 될 새로운 클라이언트-서버 프로토콜입니다. https://github.com/antirez/resp3에 있는 명세에 기존 프로토콜인 RESP2의 이번 진화가 Redis 생태계를 어떻게 개선할지 명확하게 설명되어 있습니다. 하지만 가장 중요한 점을 말하자면 RESP3가 RESP2보다 훨씬 더 “의미론적(semantic)”이라는 것입니다. 예를 들어 맵(map), 셋(set, 순서가 없는 요소 목록), 반환 데이터에 부가 정보를 덧붙일 수 있는 속성(attribute) 등의 개념을 갖추고 있습니다. 궁극적인 목표는 새로운 Redis 클라이언트가 해야 할 일을 줄이는 것입니다. 즉, RESP3의 모든 응답 타입을 해당 클라이언트 라이브러리의 프로그래밍 언어에 맞는 적절한 타입으로 변환하는 고정된 규칙만 정하면 되도록 만드는 것입니다.
제가 그리는 Redis의 미래에는 클라이언트가 내부적으로는 연결과 파이프라이닝, 상태 처리를 최대한 잘 해내면서도, 사용자 입장에서는 훨씬 더 단순해 보이는 모습입니다. 이상적인 Redis 클라이언트가 다음과 같은 형태가 될 정도입니다.
result = redis.call(“GET”,keyname);
물론 그 위에 더 고차원적인 추상화를 쌓을 수도 있지만, 가장 낮은 계층은 저렇게 생겨야 하며, 반환된 응답은 특정 명령에만 맞춘 임시 필터링을 필요로 해서는 안 됩니다. RESP3의 반환 타입 자체가 적절한 데이터 타입을 돌려줄 만큼 충분한 정보를 담고 있어야 합니다. 그래서 HGETALL은 RESP3의 “map”을 반환하고, LRANGE는 “array”를, EXISTS는 RESP3의 “boolean”을 반환하게 됩니다.
이렇게 되면 클라이언트 라이브러리가 *특별히* 대응하도록 설계되지 않았더라도 새로운 명령이 기대대로 동작합니다. 반면 RESP2에서는 “method missing” 같은 메커니즘 덕분에 명령이 일단 동작하는 것처럼 보이다가, 나중에 해당 명령이 클라이언트 라이브러리에 *정식으로* 구현되면서 반환 타입이 바뀌어 미묘한 호환성 문제가 생기곤 했습니다.
하지만 새 프로토콜은 기존 프로토콜을 점진적으로 개선한 것이면서도, 클라이언트 라이브러리 측면은 물론 *애플리케이션 계층*에서도 호환성을 깨뜨리는 변화를 가져옵니다. 예를 들어 ZSCORE는 이제 문자열이 아니라 double을 반환하므로 애플리케이션 코드를 수정해야 합니다. 또는 클라이언트 라이브러리가 RESP3 응답을 원래의 RESP2 타입으로 되돌리는 호환성 옵션을 제공하는 방법도 있습니다.
Lua 스크립트 역시 새 프로토콜에 맞게 수정하지 않으면 더 이상 동작하지 않습니다. redis.call()이 반환하는 값에서 Lua도 더 의미론적인 타입을 보게 되기 때문입니다. 마찬가지로 Lua에서도 RESP3에서 구현된 모든 새로운 데이터 타입을 반환할 수 있게 됩니다.
이런 이유 때문에 사람들은 제 결정에 대해 우려하고 있습니다. 저는 Redis 6를 *오직* RESP3만 지원하는 형태로 출시할 예정입니다. Redis 6 서버를 RESP2로 전환하는 호환 모드는 없을 것입니다. 따라서 클라이언트 라이브러리를 업그레이드하고 애플리케이션을 수정하거나(혹은 클라이언트 라이브러리의 하위 호환 모드를 사용하거나) 해야 하며, 그렇지 않으면 Redis 6로 전환할 수 없습니다.
이렇게 결정한 데에는 타당한 이유가 있으며, 왜 이런 결정을 내렸는지, 그리고 사용자와 클라이언트 라이브러리 작성자를 위해 어떤 완충 장치를 마련하고 있는지 설명하고 싶습니다. 먼저 완충 장치부터 말씀드리겠습니다.
- Redis 6 출시 후 2년간 Redis 5를 완전히 지원할 것입니다. 중요한 수정 사항은 모두 Redis 5로 백포트되며, 패치 레벨 릴리스가 지속적으로 제공됩니다.
- Redis 6는 약 1년에서 1년 반 뒤에 출시될 것으로 예상됩니다. 하지만 Redis 6는 약 한 달 뒤부터 RESP3로 전환될 예정입니다. 따라서 사람들은 오랫동안 새로운 프로토콜을 사용하는 불안정 버전의 Redis를 사용하고 실험하며 적응할 시간을 갖게 됩니다. 다른 많은 소프트웨어와 달리 Redis의 불안정 버전은 Github의 기본 브랜치이기도 하고 전통적으로 그리 불안정하지 않아 일반 사용자도 많이 사용하는 만큼, 미리 충분히 노출될 기회가 될 것입니다.
- 아직 100% 확정된 것은 아니지만, Lua 스크립팅 엔진에는 Redis 5와 동일한 타입을 반환하는 호환 모드가 있을 수도 있습니다. 다만 이 호환 모드는 기본적으로 활성화되지 않으며, Redis 명령을 호출하기 전에 특별한 redis.resp2_compat() 함수를 호출해 스크립트별로 선택(opt-in)해야 합니다. 이렇게 하면 모든 Redis 6 서버가 설정과 관계없이 동일하게 동작하는데, 이는 지난 10년간 Redis가 항상 그래왔던 방식입니다.
이상이 완충 장치입니다. 그리고 Redis 6에서 두 버전을 모두 지원하지 않으려는 이유는 다음과 같습니다.
- 거의 완전히 무의미합니다. 사람들이 Redis 6를 RESP2 모드로 전환하면 여전히 과거에 머무는 것이며, 결국 RESP2 지원 없이 출시될 Redis 7이 모든 것을 깨뜨릴 때까지 기다리는 셈입니다. 그 사이 Redis 6 설치를 다룰 때 설정에 따라 *무엇을 응답하는지* 전혀 알 수 없게 됩니다. 그래서 동일한 클라이언트 라이브러리가 동일한 명령에 대해 Hash를 반환할 수도, Array를 반환할 수도 있게 됩니다.
- 정당한 이유 없이 더 많은 작업과 복잡성만 추가됩니다(“1” 참조). 많은 명령에서 어떤 형식으로 응답해야 할지 판단하기 위해 이전 프로토콜 여부를 확인해야 합니다.
- Redis 6의 새로운 기능과 프로토콜 변경을 함께 묶음으로써 사용자들이 클라이언트와 애플리케이션을 전환하고 포팅할 충분한 동기를 제공합니다. 어느 시점이 지나면 모든 전환이 끝나고 우리는 새로운 일에 집중할 수 있습니다. 그렇지 않으면 새로운 기능을 위해 새 서버로 옮겨 놓고도 여전히 이전 프로토콜을 쓰는 Redis 6 사용자군이 생기게 되고, Redis 7에서 같은 혼란이 반복될 것입니다.
- 누군가 클라이언트 라이브러리를 수정하는 일이 엄청난 작업이라고 말한다면, 저는 동의하지 않습니다. 물론 해야 할 변경은 있지만, 지금 서버 쪽을 구현해 보니 그리 끔찍한 수준은 아닙니다. 오히려 진짜 문제는 대부분의 클라이언트 작업이 전혀 보수를 받지 못하고 오직 열정과 나누려는 의지로만 이루어진다는 점입니다. 저는 머지않아 RESP3 구현이 많이 등장할 것이라고 확신합니다.
- RESP3는 클라이언트가 RESP2인지 RESP3인지 자동으로 감지해 전환할 수 있도록 설계되었으므로, 새로운 클라이언트는 Redis <= 5와 Redis 6 모두에서 동작할 것입니다.
이상으로 마칩니다. 제 관점과 그 이유가 명확히 전달되었기를 바랍니다. 그리고 프로토콜 전환 과정에서 마련될 완충 장치들이 이번 변화가 그리 “심한” 단절은 아니라는 점을 사용자들이 납득하는 데 도움이 되기를 바랍니다.
글을 무작위로 읽기