문제
localhost:63790> subscribe home
1) "subscribe"
2) "home"
3) (integer) 1
1) "message"
2) "home"
3) "{\"messageId\":\"abcde\",\"sender\":\"1234\",\"message\":\"abcde\"}"
1) "message"
2) "home"
3) "{\"messageId\":\"abcde\",\"sender\":\"1234\",\"message\":\"\xed\x95\x98\xec\x9d\xb4\"}"
1) "message"
2) "home"
3) "{\"messageId\":\"abcde\",\"sender\":\"1234\",\"message\":\"\xea\xb0\x80\"}"
레디스로 pub/sub 테스트를 해보던 중에 한글이 깨지던 문제가 있었다.
해결
❯ redis-cli -h localhost -p 63790 --raw
localhost:63790> subscribe home
subscribe
home
1
message
home
{"messageId":"abcde","sender":"1234","message":"가"}
redis-cli로 접속 시 --raw를 붙여주면 해결된다.
레디스는 --raw를 통해 원시 출력을 강제할 수 있다. (출처 : 레디스 공식 문서)
❯ redis-cli -p 63790
127.0.0.1:63790> set my "hello\nworld"
127.0.0.1:63790> get my
"hello\nworld"
127.0.0.1:63790> set hangul "한글 입니다."
OK
127.0.0.1:63790> get hangul
"\xed\x95\x9c\xea\xb8\x80 \xec\x9e\x85\xeb\x8b\x88\xeb\x8b\xa4."
❯ redis-cli -p 63790 --raw
127.0.0.1:63790> get my
hello
world
127.0.0.1:63790> get hangul
한글 입니다.
실제로 --raw를 붙이면 잘 나오는 것을 볼 수 있다.
Spring Data Redis 가 한글을 인코딩하는 방식
아까 위에서 봤듯, 한글 "가" 는 "\xea\xb0\x80"로 표기가 됐는데, 레디스는 아스키 문자로 저장이 된다.
한글 "가"는 \x가 3번 나왔으니까, 3byte 로 저장이 된다는 소린데, 3byte로 한글이 저장되는 인코딩 방식은 UTF8이다.
그래서 인코딩 사이트로 UTF8 한글 "가"를 검색해보니,
레디스에서 저장이 되던 eab080을 확인할 수 있다.
즉 자바에서 문자열을 UTF8로 변환하여 주지만, 레디스에서는 byte 단위로 읽어서 8비트씩 끊어서 저장하느라 저렇게 표기된 것.
하지만 자바는 UTF16을 사용한다.(정확히 말하면 ASCII 로 표기할 수 있으면 1byte, 하나라도 아니라면 2byte. 이에 대해서는 내 또 다른 글을 참고하면 좋다,,) 그러면 2byte여야 할 텐데 왜 3byte로 변환이 됐는지 찾아보니
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(RedisSerializer.string()); // 키 직렬화
redisTemplate.setValueSerializer(RedisSerializer.json()); // 값 직렬화
return redisTemplate;
}
레디스 설정에 사용한 직렬화 방식.
그리고 RedisSerializer.string() 내부를 들어가 보니
static RedisSerializer<String> string() {
return StringRedisSerializer.UTF_8;
}
우선 키를 직렬화할 땐 UTF8로 직렬화를 한다.
다음으로 RedisSerializer.json()이 문자열을 어떤 인코딩 방식으로 직렬화하는지 살펴보자면
static RedisSerializer<Object> json() {
return new GenericJackson2JsonRedisSerializer();
}
RedisSerializer.json()은 GenericJackson2JsonRedisSerializer 를 반환한다.
public GenericJackson2JsonRedisSerializer() {
this((String)null);
}
public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName) {
this(typeHintPropertyName, JacksonObjectReader.create(), JacksonObjectWriter.create());
}
public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName, JacksonObjectReader reader, JacksonObjectWriter writer) {
this(new ObjectMapper(), reader, writer, typeHintPropertyName);
registerNullValueSerializer(this.mapper, typeHintPropertyName);
this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(this.getObjectMapper(), typeHintPropertyName));
}
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create());
}
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader, JacksonObjectWriter writer) {
this(mapper, reader, writer, (String)null);
}
private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader, JacksonObjectWriter writer, @Nullable String typeHintPropertyName) {
Assert.notNull(mapper, "ObjectMapper must not be null");
Assert.notNull(reader, "Reader must not be null");
Assert.notNull(writer, "Writer must not be null");
this.mapper = mapper;
this.reader = reader;
this.writer = writer;
this.defaultTypingEnabled = Lazy.of(() -> {
return mapper.getSerializationConfig().getDefaultTyper((JavaType)null) != null;
});
this.typeResolver = newTypeResolver(mapper, typeHintPropertyName, this.defaultTypingEnabled);
}
GenericJackson2JsonRedisSerializer 생성자를 맨 위에서부터 차례대로 보면 된다.
this를 타고 내려가면서 2번째 생성자를 보면 JacksonObjectWriter.create()라는 메서드를 통해 JSON을 직렬화하기 위한 객체를 만든다.
그리고 6번째 생성자에서, 만든 JacksonObjectWriter를 this.writer 에 넣어준다.
JacksonObjectWriter.create() 내부를 봐보면,
@FunctionalInterface
public interface JacksonObjectWriter {
byte[] write(ObjectMapper mapper, Object source) throws IOException;
static JacksonObjectWriter create() {
return ObjectMapper::writeValueAsBytes;
}
}
내부를 보니 ObjectMapper.writeValueAsBytes()라는 메서드를 실행시킨다. 안을 또 봐보면
public byte[] writeValueAsBytes(Object value) throws JsonProcessingException {
BufferRecycler br = this._jsonFactory._getBufferRecycler();
byte[] var6;
try {
ByteArrayBuilder bb = new ByteArrayBuilder(br);
Throwable var4 = null;
try {
// 여기
this._writeValueAndClose(this.createGenerator((OutputStream)bb, JsonEncoding.UTF8), value);
byte[] result = bb.toByteArray();
bb.release();
var6 = result;
} catch (Throwable var26) {
var4 = var26;
throw var26;
} finally {
if (bb != null) {
if (var4 != null) {
try {
bb.close();
} catch (Throwable var25) {
var4.addSuppressed(var25);
}
} else {
bb.close();
}
}
}
} catch (JsonProcessingException var28) {
JsonProcessingException e = var28;
throw e;
} catch (IOException var29) {
IOException e = var29;
throw JsonMappingException.fromUnexpectedIOE(e);
} finally {
br.releaseToPool();
}
return var6;
}
위 코드를 다 볼 필욘 없고 2번째 try문의 여기라고 써둔 바로 밑 줄을 보면 JsonEncoding.UTF8이라고 되어 있다.
우리의 목적은 자바 객체가 JSON으로 변환되면서 어떤 인코딩으로 변환되는지 확인하는 거였으니 더 깊게 안 들어가고 (들어가도 더 복잡하고 이해하기 힘들다,,) 여기까지 확인하는 걸로.
결론
- Redis는 기본적으로 byte-encoded character로 표기한다. (not human readable)
- Spring Data Redis는 UTF8로 인코딩한다.
- 사람이 읽을 수 있게 표기하려면 redis-cli에서 --raw 옵션을 추가하면 된다.
'TIL ✍️' 카테고리의 다른 글
24/08/12(월) 97번째 TIL : Spring cloud gateway에서 최종 라우팅 서비스 URI 가져오기 (0) | 2024.08.12 |
---|---|
24/08/09(금) 96번째 TIL : Spring Gateway 애플 실리콘 맥 에러 (0) | 2024.08.10 |
24/08/07(수) 94번째 TIL : EurekaServerConfig 빈 중복 해결하기 (0) | 2024.08.07 |
24/08/06(화) 93번째 TIL : 도커 볼륨으로 레디스 데이터 공유하기 (3) | 2024.08.06 |
24/08/05(월) 92번째 TIL : 레디스 인증 설정 (0) | 2024.08.05 |