본문 바로가기
카테고리 없음

[출하 정보 연동 프로젝트] ObjectRecord, StringRedisSerializer를 통한 Redis Stream 구현

by 소고기 굽는 개발자 2025. 5. 11.

서론

Spring Redis를 활용하여 Redis Stream 기반의 발행/구독 시스템을 구현한 경험을 공유합니다.

본 글에서는 MapRecordObjectRecord의 차이를 분석하고, ObjectRecord를 사용하여 커스텀 DTO 객체를 처리한 것을 설명합니다.

 

사용된 DTO는 타입 세이프(Type Safety)프로퍼티의 명시적인 확인(Explicit Property Access)을 보장하기 위해 선택되었습니다.

Redis Stream은 이진 형식으로 데이터를 저장하므로 직렬화와 역직렬화가 필수이며, 이를 위해 StringRedisSerializerObjectMapper를 활용했습니다.

 

MapRecord와 ObjectRecord 비교

MapRecord: 단순한 키-값 데이터 처리

https://docs.spring.io/spring-data/redis/reference/redis/redis-streams.html

 

MapRecord는 키-값 쌍으로 구성된 맵을 페이로드로 사용하여 데이터를 저장합니다.

 

 

ObjectRecord: 타입 세이프와 명시적 프로퍼티 접근

ObjectRecord는 POJO(Plain Old Java Object)를 페이로드로 사용하여 데이터를 저장합니다.

https://docs.spring.io/spring-data/redis/reference/redis/redis-streams.html

공식문서에 나온대로 ObjectRecord는 POJO 방식의 페이로드를 지원하기에 타입을 안전하게 바인딩할 수 있습니다. 

 

이를 통해, 데이터가 객체의 속성에 할당되기 전에 올바른 타입으로 변환되는지를 검증할 수 있고, 

필요에 따라 getter/setter로 접근할 수 있기에 사용하는 프로퍼티를 명시적으로 확인할 수도 있습니다.

 

ObjectRecord를 선택한 이유

ObjectRecord를 선택한 이유는 Runtime 환경에서의 오류를 줄이기 위함입니다. 

MapRecord는 단순한 키-값 구조로 눈에 보기는 편하지만 유지보수를 하며 개발자의 실수로 타입 매핑을 잘못했을 경우 컴파일 당시엔 문제가 되지않던 것이 Runtime 환경에서 문제가 될 수 있습니다. 

 

Runtime 환경에서 오류가 발생한다는 것은 디버깅을 어렵게하고 시스템의 안정성을 해칠 수 있다고 생각합니다. 

그렇기에 Runtime 환경에서의 오류를 최소화 하기 위해서라도 타입 세이프한 ObjectRecord를 선택하게 되었습니다. 

또한, MapRecord는 값을 조회하는 과정에서 잘못된 키값을 적용하면 null을 리턴하는 상황이 발생할 수 있지만 ObjectRecord는 명시적으로 프로퍼티가 존재하기에 저장된 값을 확실히 가져올 수 있다고 생각했습니다. 

 

결론적으로 ObjectRecord를 이용해 코드 가독성과 유지보수성을 향상시킬 수 있었고, 또한 IDE의 자동완성 지원으로 오타를 줄일 수 있었습니다.

 

ObjectRecord를 활용한 Redis Stream 구현

발행(Publish)

객체를 ObjectRecord로 래핑하여 Redis Stream에 저장했습니다.

발행을 할때의 구조는 streamKey - data로 설정했고, data는 다양한 객체가 들어올 수 있어 T타입으로 설정하였습니다. 

또한 Consumer에서 메시지 처리 속도가 느려서 google의 guava 라이브러리를 이용해 속도를 조절하였습니다. 

 

아래는 발행 과정의 예제 코드입니다:

public <T> void objectPublish(String streamKey, T data) {
	try {
		rateLimiter.acquire();  // 메시지를 보내기 전에 RateLimiter로부터 대기
		ObjectRecord<String, String> record = StreamRecords.newRecord().in(streamKey)
                                                           .ofObject(OBJECT_MAPPER.writeValueAsString(data))
                                                           .withId(RecordId.autoGenerate());
		redisTemplate.opsForStream().add(record);
	} catch (JsonProcessingException e) {
		throw new RuntimeException(e);
	}
}

 

구독(Subscribe)

StreamMessageListenerContainer를 구성하여 Redis Stream에서 메시지를 수신했습니다.

ObjectRecord로 저장된 데이터는 JSON 문자열 형태로 전달되며, 이를 ObjectMapper를 사용하여 원래의 객체로 역직렬화했습니다.

 

아래는 메시지 리스너의 예제 코드입니다:

@RequiredArgsConstructor
public class ParallelConsumer implements StreamListener<String, ObjectRecord<String, String>> {
	private final ObjectMapper mapper;
	private final CommonServiceMap commonServiceMap;
	... 
    
	@Override
	public void onMessage(ObjectRecord<String, String>) {
		...
		WrappedWebClientRequest requestDto = OBJECT_MAPPER.readValue(modelMessage, WrappedWebClientRequest.class);
        
		this.commonServiceMap.commonServices.get(requestDto.getRedisKey()).transApi(requestDto);
	}
}

 

 

직렬화 방식: StringRedisSerializer 선택

https://docs.spring.io/spring-data/redis/reference/redis/redis-streams.html

Redis Stream은 이진 형식으로 데이터를 저장하므로 직렬화가 필수적입니다.

 

직렬화 방법으로는 StringRedisSerializer, JdkSerializationRedisSerializer, Jackson2JsonRedisSerializer가 있습니다.

 

JdkSerializationRedisSerializer

https://mangkyu.tistory.com/402

 

블로그의 내용을 보면 JdkSerializationRedisSerializer는 기본적인 java 직렬화를 사용한다고 합니다. 

 

기본적인 java 직렬화는 Serializable을 반드시 구현해야 한다는 것과 serialVersionUID(클래스 해시값)을 설정하지 않을 경우 문제가 될 수 있는 부분이 있는데, JdkSerializationRedisSerializer 또한 같은 문제를 가지고 있습니다.

https://yonguri.tistory.com/14

또한, 키앞에 부가적으로 unicode를 붙어 redis-cli에서 조회를 어렵게 만든다는 글을 확인했습니다.

 

Jackson2JsonRedisSerializer

https://mangkyu.tistory.com/402

 

Jackson2JsonRedisSerializer는 JSON 구조를 생성하고 파싱하므로 추가적인 계산 비용이 발생합니다.

또한, JSON 문자열 그 자체를 저장함으로 저장 용량이 많이 차지한다는 문제점도 가지고 있습니다.

 

StringRedisSerializer

https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/serializer/StringRedisSerializer.html

StringRedisSerializer는 지정된 문자 인코딩(UTF-8)을 참고해 문자열을 바이트 배열로 바이트 배열을 문자열로 변환하는 작업을 한다고 합니다. 

 

https://docs.spring.io/spring-data/redis/reference/redis/template.html

또한 StringRedisTemplate에서 StringRedisSerializer와 관련된 설명글을 보면 Redis에 저장되는 키와 값이 java.lang.String인 경우엔 사람이 읽을 수 있는 형태로 저장된다고 합니다. 

 

실제로 위의 테스트는 제가 직접 StringRedisSerializer를 통해 저장했을 때의 이미지입니다. 

 

StringRedisSerializer를 선택한 이유

JdkSerializationRedisSerializer 는 자바 직렬화가 가지는 문제점을 그대로 가지고 있다는 것 Jackson2JsonRedisSerializer는 JSON으로 직렬화/역직렬화를 하는 과정에서 비용이 많이 소모된다는 점들로 인해 StringRedisSerializer를 선택하게 되었습니다. 

 

물론 StringRedisSerializer의 value도 json 형식으로 저장되어 ObjectMapper를 사용하는 과정에서 비용이 발생하지만, redis-cli에서 곧바로 키-값을 읽을 수 있다는 것, redis의 메모리를 최소한으로 사용해 데이터를 저장할 수 있다는 점을 이유로 선택하게 되었습니다.
(ObjectMapper 대신 ObjectHashMapper를 사용하려 했으나, class명만 저장되는 문제로 인해 곧바로 적용을 하진 못했습니다)