node-redis with binary values

Chung Hwan·2022년 11월 17일

redis에 value로 protobuf 인코딩 값을 set 해야 할 일이 생겼다.

go로 작성된 클라이언트에서 set은 잘 되는데, 이상하게 node 기반 서버에서 get 해올 때 문제가 생겼다.

const str = await this.redisClient.get(this.buildKey(...key));
if (!str) {
  return null;
}
return HandlerResult.decode(new TextEncoder().encode(str));

실제로 set 한 byte 수는 546으로 잡히는데 로그 상에서 byte 수는 548로 2 차이가 났다.

RangeError: index out of range: 548 + 10 > 548
    at indexOutOfRange (/Users/hanch/code/personal/gimi/node_modules/protobufjs/src/reader.js:13:12)
    at Reader.read_uint32 [as uint32] (/Users/hanch/code/personal/gimi/node_modules/protobufjs/src/reader.js:98:19)
    at Object.decode (/Users/hanch/code/personal/gimi/dist/packages/runtime-filter/webpack:/gimi/packages/proto/ts/messages/inspection.ts:108:26)
    at Object.decode (/Users/hanch/code/personal/gimi/dist/packages/runtime-filter/webpack:/gimi/packages/proto/ts/messages/inspection.ts:268:45)
    at AppService.<anonymous> (/Users/hanch/code/personal/gimi/dist/packages/runtime-filter/webpack:/gimi/packages/runtime-filter/src/app/app.service.ts:33:26)
    at Generator.next (<anonymous>)
    at fulfilled (/Users/hanch/code/personal/gimi/node_modules/tslib/tslib.js:115:62)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

redisClient.get 메소드를 이용하면 내부적으로 string으로 변환되어 나오기 때문에 proto로 디코딩하려면 bytes array 다시 인코딩해줘야 한다. 그런데 TextEncoder.encode 를 써서 인코딩하면, 0xc7 과 같은 큰 값이 있을 때 인코딩이 바이너리 값과 다르게 되는 문제가 있었다.

new TextEncoder().encode(new TextDecoder().decode(new Uint8Array([0x31])), 'binary')
// expect: [49], actual: Uint8Array [49]
new TextEncoder().encode(new TextDecoder().decode(new Uint8Array([0xc7])), 'binary')
// expect: [199], actual: Uint8Array(3) [239, 191, 189]

찾아보니 애초에 Text Encoder 라는 이름에서도 알 수 있듯이 binary 데이터를 다루기에 적절한 클래스가 아니라는 의견이 있었다.

그렇다면 처음부터 redis 로부터 인코딩 되기 이전의 bytes array 를 받는 게 가장 좋은 방법일 듯 싶었다. 처음부터 bytes array로 나오는 걸 내부적으로 string으로 디코딩해서 받고, 그걸 다시 bytes array로 인코딩 해서 proto로 또 디코딩하는 건 아무래도 뭔가 부자연스럽기도 하다. 찾아보니 returnBuffers: bool 옵션을 이용하면 string으로 변환되기 이전의 Buffer를 리턴받을 수 있다고 한다.

node-redis 의 클라이언트 부분 소스코드를 보면 get, set 같은 커맨드의 args, reply 의 transformer 함수를 정의하고 이를 client의 메소드로 쭉 이어 붙힌다. Get 커맨드의 경우 정의된 transformArguments 함수를 보면 파라미터를 key 하나 밖에 받지 않는다. 즉, returnBuffers 와 같은 파라미터를 따로 줄 수 없는 구조다.

다행이 이러한 pre-defined 된 커맨드 외의, 직접 redis 커맨드를 줄 수 있는 sendCommand 메소드가 있다. 이 메소드를 이용해 returnBuffers: bool 옵션을 주면 디코딩 이전의 Buffer를 리턴받을 수 있다.

proto 파일을 ts-proto 로 컴파일한 decode 메소드에 이 Buffer를 그대로 넣으면 처음 에러가 사라지고 제대로 디코딩 된다.

const buf = await this.redisClient.sendCommand(["GET", this.buildKey(...key)], {
  returnBuffers: true,
});
if (!buf) {
  return null;
}
return HandlerResult.decode(buf);

0개의 댓글