버그를 고칠 때 가장 위험한 순간은 언제일까?
코드를 바꿀 때?
테스트가 깨질 때?
릴리즈 직전?
내가 보기엔 그보다 조금 앞이다.
문제를 처음 이해한 뒤, 우리가 이렇게 말하는 순간.
“아, 이참에 관련된 것도 다 고치죠.”
이 문장은 친절해 보인다.
하지만 가끔은 폭탄이다.
이번 Redis Proxy 개선 계획도 딱 그 갈림길에 있었다.
겉으로는 Redis 연결 실패 하나였다.
그런데 안쪽을 들여다보니 세 명의 용의자가 동시에 서 있었다.
1. CLIENT SETINFO
2. RESP3
3. Pub/Sub
셋 다 Redis와 관련 있다.
셋 다 연결 실패와 엮일 수 있다.
셋 다 로그에서 비슷하게 시끄럽다.
하지만 셋을 한 패치에 넣으면 안 된다.
왜냐하면 한 명은 “작은 손님”이고, 나머지 둘은 “집 구조를 바꾸자”고 말하는 손님이기 때문이다.
첫 번째 손님: CLIENT SETINFO
문제의 핵심은 Redis 클라이언트가 연결할 때 자동으로 보내는 자기소개 명령이었다.
CLIENT SETINFO lib-name Lettuce
CLIENT SETINFO lib-ver 6.x.x
이 명령은 쉽게 말해 이런 뜻이다.
안녕하세요.
저는 Lettuce라는 Redis client library입니다.
제 버전은 이렇습니다.
데이터를 지우지 않는다.
설정을 바꾸지 않는다.
권한을 뒤흔들지도 않는다.
그냥 이름표를 붙이는 정도다.
그런데 Proxy가 이 명령을 모르면 이야기가 달라진다.
Client: CLIENT SETINFO lib-name Lettuce
Proxy: 모르는 명령입니다
App: Redis 연결 실패!
사용자는 CLIENT SETINFO를 직접 실행한 적이 없다.
라이브러리가 연결 초기화 과정에서 자동으로 보냈을 뿐이다.
그래서 장애는 더 헷갈린다.
개발자는 “내가 그런 명령 보낸 적 없는데?”라고 생각하고,
운영자는 “인증 문제인가?”라고 생각하고,
Proxy는 조용히 “처음 보는 문장인데요”라고 말한다.
이 경우 패치의 목표는 작고 분명하다.
Proxy가 CLIENT SETINFO를 알아듣게 한다.
작은 패치의 모양
이 패치는 생각보다 예쁘게 쪼갤 수 있다.
대략 이런 순서다.
Lexer: SETINFO라는 단어를 안다
Parser: CLIENT SETINFO attr value 형태를 안다
Command enum: CLIENT_SETINFO 타입을 추가한다
Command model: attr/value를 보관한다
Result adapter: +OK 응답을 정상 처리한다
Test: Lettuce가 보내는 실제 형태를 검증한다
여기서 중요한 점은 CLIENT SETINFO를 거창하게 해석하지 않는 것이다.
이건 Redis 전체 호환성 프로젝트가 아니다.
RESP3 전면 지원도 아니다.
Pub/Sub 중계도 아니다.
그냥 연결 초기에 들어오는, 비교적 무해한 client metadata 명령을 Proxy가 통과시킬 수 있게 하는 일이다.
그래서 복잡도는 낮음에서 중간 정도다.
물론 체크할 것은 있다.
- ANTLR parser를 다시 생성해야 하는 구조인가?
- enum 값이 로그나 DB에 숫자로 저장되지는 않는가?
- 이 명령이 UNKNOWN이나 NOTSUPPORTED로 분류되지는 않는가?
lib-ver같은 값의 점, 슬래시, 하이픈이 문자열로 잘 보존되는가?- 감사 로그에 너무 긴 값이나 이상한 문자가 들어가도 안전한가?
작은 패치라고 해서 생각 없이 넣어도 된다는 뜻은 아니다.
작다는 건 “영향 범위를 설명할 수 있다”는 뜻에 가깝다.
두 번째 손님: RESP3
이제 진짜 조심해야 할 손님이 나온다.
RESP3.
RESP는 Redis client와 server가 대화할 때 쓰는 wire protocol이다.
RESP2는 오래된 기본 문법이고, RESP3는 더 풍부한 타입을 표현한다.
예를 들어 RESP3는 map, set, boolean, double, push message 같은 타입을 더 명확하게 다룬다.
문제는 이거다.
RESP3를 제대로 지원한다는 건 단순히 HELLO 3를 통과시키는 일이 아니다.
HELLO 3 허용
-> Redis 응답 타입이 달라짐
-> command별 result adapter가 달라짐
-> UI table 표현이 달라질 수 있음
-> audit/session 로그 표현이 달라질 수 있음
-> 일부 command는 RESP2에서는 되는데 RESP3에서는 깨질 수 있음
예를 들어 어떤 명령은 RESP2에서는 array처럼 오지만, RESP3에서는 map처럼 올 수 있다.
서버가 더 좋은 표현을 보내준다고 해서, Proxy와 UI가 바로 좋아지는 건 아니다.
받는 쪽도 그 표현을 끝까지 이해해야 한다.
그래서 RESP3는 “겸사겸사” 넣을 수 있는 범위가 아니다.
이건 별도 호환성 프로젝트에 가깝다.
RESP3 전체 지원 = 큰 작업
CLIENT SETINFO 지원 = 작은 연결 호환성 패치
둘은 같은 Redis 세계에 살지만, 같은 패치가 아니다.
세 번째 손님: Pub/Sub
마지막 손님은 더 묵직하다.
Pub/Sub.
일반 Redis 명령은 보통 이런 흐름이다.
Client -> GET key
Server -> value
끝
깔끔하다.
요청 하나, 응답 하나.
그런데 Pub/Sub는 다르다.
Client -> SUBSCRIBE news
Server -> subscribed
Server -> message
Server -> message
Server -> message
...
서버가 계속 메시지를 밀어준다.
연결은 오래 유지된다.
Proxy는 그 흐름을 계속 붙잡고 있어야 한다.
이제 고민이 많아진다.
- 이 연결은 언제 끊을까?
- timeout은 어떻게 볼까?
- 권한이 회수되면 구독 중인 연결은 어떻게 할까?
- 감사 로그는 메시지마다 남길까?
- 메시지가 폭주하면 backpressure는 어떻게 할까?
- 재연결이 몰리면 Agent 리소스는 괜찮을까?
이건 그냥 parser에 명령 하나 추가하는 일이 아니다.
세션 lifecycle과 운영 정책을 건드리는 일이다.
그래서 Pub/Sub도 이번 작은 패치에 넣으면 안 된다.
좋은 패치는 “하지 않을 일”을 적는다
이번 개선 계획에서 가장 마음에 드는 부분은 이것이다.
무엇을 할지보다, 무엇을 하지 않을지를 먼저 적었다.
할 일:
CLIENT SETINFO 최소 지원
하지 않을 일:
RESP3 전체 지원
Redis 7.x 전체 명령 지원
Pub/Sub relay 지원
Dangerous command 기본 정책 변경
이렇게 적어두면 패치가 단단해진다.
리뷰어도 안심할 수 있다.
QA도 테스트 범위를 잡기 쉽다.
릴리즈 노트도 정확해진다.
고객 안내도 과장되지 않는다.
반대로 이 경계를 적지 않으면 작은 패치는 금방 큰 약속이 된다.
CLIENT SETINFO 처리했습니다
라고 말해야 할 것을,
RESP3 지원합니다
Redis 7.2 지원합니다
Spring Data Redis 지원합니다
라고 말해버릴 수 있다.
그러면 패치보다 말이 더 위험해진다.
테스트도 범위에 맞게 작아야 한다
좋은 테스트는 야심찬 테스트가 아니다.
패치의 약속을 정확히 붙잡는 테스트다.
이번 경우 최소 테스트는 이 정도면 된다.
CLIENT SETINFO lib-name Lettuce
CLIENT SETINFO lib-ver 6.3.2.RELEASE/8.0.0
client setinfo lib-name lettuce
확인할 것은 명확하다.
- parser가 실패하지 않는가?
CLIENT_SETINFO로 인식하는가?- attr/value가 보존되는가?
- 대소문자가 섞여도 되는가?
+OK응답을 adapter가 정상 처리하는가?- 기존
CLIENT SETNAME,CLIENT INFO,CLIENT TRACKINGINFO는 그대로 동작하는가?
가능하면 smoke test도 하면 좋다.
CLIENT SETINFO lib-name Lettuce
CLIENT SETINFO lib-ver 6.x.x
PING
목표는 거창하지 않다.
“이제 Lettuce가 문 앞에서 자기소개해도 Proxy가 당황하지 않는다.”
그 정도면 충분하다.
릴리즈 노트는 패치보다 커지면 안 된다
기술적으로 작은 패치를 해놓고, 릴리즈 노트에서 큰 약속을 해버리는 경우가 있다.
이번에는 그러면 안 된다.
피해야 할 표현은 이런 것들이다.
RESP3를 지원합니다.
Redis 7.2를 지원합니다.
Spring Data Redis 전체를 지원합니다.
Pub/Sub 문제가 해결됐습니다.
이건 너무 크다.
패치가 보장하지 않는 것까지 말하고 있다.
안전한 표현은 이렇게 좁다.
Spring Data Redis / Lettuce 계열 클라이언트가 연결 초기화 과정에서 자동 전송할 수 있는
CLIENT SETINFO 명령 처리 호환성을 개선했습니다.
단, RESP3 전체 명령/응답 조합 및 Redis Pub/Sub 구독 방식은 별도 지원 범위이므로,
사용 중인 client 설정과 명령 범위에 따라 추가 확인이 필요합니다.
좋은 릴리즈 노트는 멋진 문장이 아니라 정확한 문장이다.
여기서 배운 점
이번 개선 계획은 Redis 이야기이기도 하지만, 사실은 범위 관리 이야기다.
버그 하나를 잡다 보면 주변에 비슷한 문제가 많이 보인다.
그때 전부 고치고 싶어진다.
개발자라면 자연스러운 욕심이다.
하지만 제품 코드는 욕심보다 경계가 중요할 때가 많다.
이 패치는 무엇을 고치는가?
이 패치는 무엇을 고치지 않는가?
고치지 않는 것을 고객에게 어떻게 설명할 것인가?
이 세 질문에 답할 수 있으면 패치는 작아도 강하다.
이번 Redis Proxy 패치의 핵심은 이거다.
CLIENT SETINFO는 받아준다.
RESP3 전체 지원은 약속하지 않는다.
Pub/Sub도 이번 범위가 아니다.
작은 패치는 비겁한 선택이 아니다.
잘 설계된 작은 패치는 큰 사고를 막는 가장 현실적인 방법이다.
특히 Proxy처럼 가운데 서 있는 코드는 더 그렇다.
가운데 있는 코드는 양쪽 세계의 변화를 모두 맞아야 한다.
그래서 한 번에 세상을 바꾸려 하기보다, 오늘 막고 있는 문장 하나를 정확히 알아듣게 만드는 편이 낫다.
이번에는 그 문장이 이것이었다.
CLIENT SETINFO lib-name Lettuce
Proxy가 이 자기소개를 알아듣는 순간, 작은 패치는 제 역할을 다한다.