Why RESP3 will be the only protocol supported by Redis 6

Salvatore Sanfilippo

왜 Redis 6는 RESP3만 지원하게 될 것인가

원문은 Salvatore Sanfilippo님이 에 게재했습니다. 이 블로그 구독하기

[수정! Stack Overflow의 Marc Gravell이 하위 호환성을 위해 커넥션 단위로 프로토콜을 전환하자고 제안했기 때문에 이 모든 내용을 재고하고 있습니다. RESP3를 활성화하는 명령을 보내 프로토콜을 전환하는 방식이죠. 그렇게 하면 서버의 동작을 바꾸는 전역 설정이 더 이상 필요하지 않습니다. 그렇게 보면 훨씬 수용할 만해서, 이 글의 핵심 내용 자체를 다시 고민 중입니다]

Redis 5가 출시된 지 몇 주 지나지 않아 RESP3 구현을 시작했는데, 며칠 작업해보니 마침내 이 일이 현실이 되는 걸 보니 기분이 좋습니다. RESP3는 Redis 6부터 사용하게 될 새로운 클라이언트-서버 프로토콜입니다. https://github.com/antirez/resp3에 있는 명세를 보면 기존 프로토콜인 RESP2의 이번 진화가 Redis 생태계를 어떻게 개선하는지 명확하게 설명되어 있습니다. 하지만 가장 중요한 점을 꼽자면 RESP3가 RESP2보다 훨씬 더 “의미론적(semantic)”이라는 것입니다. 예를 들어 맵(map), 셋(순서가 없는 원소들의 리스트), 반환 데이터에 부가 정보를 덧붙일 수 있는 속성(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에서도 RESP3에서 구현된 모든 새로운 데이터 타입을 반환할 수 있게 됩니다.

이런 이유 때문에 많은 사람이 내 결정에 대해 우려하고 있습니다. 나는 Redis 6를 *오직* RESP3만 지원하는 형태로 출시할 예정입니다. Redis 6 서버를 RESP2로 전환하는 호환 모드는 없을 것이므로, 클라이언트 라이브러리를 업그레이드하고 애플리케이션을 업그레이드하거나(혹은 클라이언트 라이브러리의 하위 호환 모드를 사용하거나) 하지 않으면 Redis 6로 전환할 수 없게 됩니다.

이렇게 하려는 데는 충분한 이유가 있으며, 왜 이런 결정을 내렸는지, 그리고 사용자와 클라이언트 라이브러리 작성자를 위해 어떤 완화책을 마련하고 있는지 설명하고 싶습니다. 먼저 완화책부터 살펴보겠습니다:

  • Redis 5는 Redis 6 출시 후 2년간 완전히 지원될 것입니다. 중요한 수정 사항은 모두 Redis 5로 백포트되며 패치 레벨 릴리스가 지속적으로 제공될 것입니다.
  • Redis 6는 앞으로 약 1년에서 1년 반 뒤에 출시될 예정입니다. 하지만 Redis 6의 프로토콜은 약 한 달 안에 RESP3로 전환될 것입니다. 따라서 사람들은 꽤 오랜 기간 동안 새 프로토콜을 사용하는 불안정한(unstable) 버전의 Redis를 사용하고 실험하며 다뤄보게 될 것입니다. 다른 많은 소프트웨어와 달리 Redis의 unstable 버전은 GitHub의 기본 브랜치이기도 하고, 전통적으로 실제로 그렇게 불안정하지도 않기 때문에 꽤 많은 일반 사용자가 사용한다는 점을 감안하면, 이는 사전에 충분히 노출될 기회를 제공할 것입니다.
  • 아직 100% 확신하는 건 아니지만, Lua 스크립팅 엔진에는 Redis 5와 동일한 타입을 반환하는 호환 모드가 있을 수도 있습니다. 다만 이 호환성은 기본적으로 활성화되지 않으며, Redis 명령을 호출하기 전에 특별한 redis.resp2_compat() 함수를 호출해 스크립트 실행 단위로 opt-in해야 합니다. 따라서 모든 Redis 6 서버는 설정과 관계없이 동일하게 동작할 것이며, 이는 지난 10년간 Redis가 항상 그래왔던 방식입니다.

이것이 완화책입니다. 그리고 Redis 6에서 두 버전을 모두 지원하지 않으려는 이유는 다음과 같습니다:

  1. 어차피 거의 완전히 쓸모없기 때문입니다. 사람들이 Redis 6를 RESP2 모드로 전환한다면 여전히 과거에 머무는 것이며, 결국 RESP2 지원이 빠진 Redis 7이 나와서 모든 것이 깨지기를 기다리는 셈입니다. 그사이 Redis 6 설치를 다룰 때 설정에 따라 *무엇을 응답하는지* 전혀 알 수 없게 됩니다. 그러면 동일한 클라이언트 라이브러리라도 같은 명령에 대해 Hash를 반환할 수도, Array를 반환할 수도 있게 됩니다.
  2. 정당한 이유 없이 더 많은 작업과 복잡성만 늘어나기 때문입니다(“1” 참조). 많은 명령에서 어떤 형식으로 응답해야 하는지 확인하기 위해 예전 프로토콜에 대한 검사를 넣어야 할 것입니다.
  3. 새로운 Redis 6 기능과 프로토콜 변경을 묶음으로써 사용자들이 클라이언트와 애플리케이션을 전환하고 포팅해야 할 확실한 이유를 제공하기 때문입니다. 어느 시점이 되면 모든 것이 끝나고 우리는 새로운 것들에 집중할 수 있습니다. 그렇지 않으면 Redis 6의 새로운 기능을 쓰려고 새 서버로 옮겨놓고도 여전히 예전 프로토콜을 쓰는 사용자 집합이 생기고, Redis 7에서도 똑같은 논란이 반복될 것입니다.
  4. 누군가 클라이언트 라이브러리를 고치는 게 엄청난 작업이라고 말한다면, 나는 동의하기 어렵습니다. 물론 해야 할 변경은 있지만, 지금 서버 쪽을 구현해보니 그리 끔찍한 수준은 아닙니다. 오히려 끔찍한 것은 대부분의 클라이언트 작업이 전혀 보수를 받지 못하고 오로지 열정과 나누려는 의지로 이루어진다는 점입니다. 머지않아 RESP3 구현이 많이 등장할 거라고 확신합니다.
  5. RESP3는 클라이언트가 RESP2인지 RESP3인지 자동으로 감지해 전환할 수 있도록 설계되었으므로, 새로운 클라이언트는 Redis <= 5와 Redis 6 모두에서 동작할 것입니다.

이게 전부입니다. 제 관점과 그 배경이 된 이유가 명확해졌기를 바라며, 동시에 프로토콜 전환 과정에서 마련될 완화책이 사용자들에게 이번 변화가 그리 “심한” 단절은 아닐 것이라는 확신을 주었으면 합니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글