BindProject
[Backend] DataSerializer (for. Kafka, Redis...)
dding-shark
2025. 6. 17. 00:50
728x90
Kafka와 Redis를 위한 안전한 직렬화/역직렬화 객체 설계기
Jackson 기반으로 Redis/Kafka 환경에서 안전하고 일관되게 데이터를 직렬화/역직렬화하기 위한 유틸성 구현기
배경
- Kafka, Redis 등에서 DTO를 직접 전송하려면 객체를 JSON 문자열 또는
byte[]형태로 직렬화해야 한다. - 이때 실패에 안전하고, 타입 일관성을 유지하며, 보안적인 로깅까지 고려한 유틸리티가 필요하다.
그래서 ObjectMapper를 감싼 DataSerializer를 설계했다.
주요 목표
- Jackson 기반 JSON 직렬화/역직렬화 간소화
- Optional로 null 처리 최소화
- Kafka 전송을 위한
byte[]처리 포함 - Redis 저장/조회 대응
- 에러 로깅 시 민감정보 노출 방지
기능 구성
구현 코드
package dataserializer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* DataSerializer
* Author: MyungJoo
* Date: 2025-06-17
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DataSerializer {
private static final ObjectMapper objectMapper = initialize();
private static ObjectMapper initialize() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* Object → JSON 문자열
*/
public static Optional<String> serialize(Object object) {
try {
return Optional.of(objectMapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
log.warn("[serialize] Failed to serialize object. type={}, error={}",
object != null ? object.getClass().getSimpleName() : "null",
e.getMessage());
return Optional.empty();
}
}
/**
* JSON 문자열 → 객체
*/
public static <T> Optional<T> deserialize(String json, Class<T> clazz) {
try {
return Optional.of(objectMapper.readValue(json, clazz));
} catch (Exception e) {
log.warn("[deserialize] Failed to parse JSON. clazz={}, error={}",
clazz.getSimpleName(), e.getMessage());
return Optional.empty();
}
}
/**
* Map 혹은 Object → DTO 변환
*/
public static <T> Optional<T> convert(Object source, Class<T> targetType) {
try {
return Optional.of(objectMapper.convertValue(source, targetType));
} catch (IllegalArgumentException e) {
log.warn("[convert] Failed to convert. sourceType={}, targetType={}, error={}",
source != null ? source.getClass().getSimpleName() : "null",
targetType.getSimpleName(), e.getMessage());
return Optional.empty();
}
}
/**
* 직렬화된 JSON 문자열을 byte[]로 변환 (Kafka용)
*/
public static Optional<byte[]> serializeToBytes(Object object) {
return serialize(object).map(json -> json.getBytes(StandardCharsets.UTF_8));
}
/**
* byte[] → 역직렬화 (Kafka용)
*/
public static <T> Optional<T> deserializeFromBytes(byte[] data, Class<T> clazz) {
if (data == null) return Optional.empty();
String json = new String(data, StandardCharsets.UTF_8);
return deserialize(json, clazz);
}
}
설계 고려 사항
- ObjectMapper는 JavaTimeModule을 등록해 LocalDateTime 등 처리 가능
- DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES = false → 유연한 파싱
- 모든 메서드는 Optional 반환 → null 안전
- 로깅 시 민감한 객체 내용은 숨기고 타입만 노출
테스트
테스트 코드
package dataserializer;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* DataSerializer
* Author: MyungJoo
* Date: 2025-06-17
*/
class DataSerializerTest {
static class TestDto {
public String name;
public int age;
// 생성자 필요 시 추가
public TestDto() {}
public TestDto(String name, int age) {
this.name = name;
this.age = age;
}
}
@Test
void serialize_shouldReturnJsonString() {
TestDto dto = new TestDto("Alice", 30);
String json = DataSerializer.serialize(dto).orElse(null);
assertThat(json).isNotNull();
assertThat(json).contains("\"name\":\"Alice\"");
}
@Test
void deserialize_shouldParseValidJson() {
String json = "{\"name\":\"Bob\",\"age\":25}";
TestDto dto = DataSerializer.deserialize(json, TestDto.class).orElse(null);
assertThat(dto).isNotNull();
assertThat(dto.name).isEqualTo("Bob");
assertThat(dto.age).isEqualTo(25);
}
@Test
void convert_shouldMapBetweenObjects() {
Map<String, Object> map = new HashMap<>();
map.put("name", "Charlie");
map.put("age", 40);
TestDto dto = DataSerializer.convert(map, TestDto.class).orElse(null);
assertThat(dto).isNotNull();
assertThat(dto.name).isEqualTo("Charlie");
}
@Test
void serializeToBytes_shouldReturnValidUtf8Bytes() {
TestDto dto = new TestDto("Dana", 20);
byte[] bytes = DataSerializer.serializeToBytes(dto).orElse(null);
assertThat(bytes).isNotNull();
String json = new String(bytes);
assertThat(json).contains("\"name\":\"Dana\"");
}
@Test
void deserializeFromBytes_shouldParseValidBytes() {
String json = "{\"name\":\"Eve\",\"age\":28}";
byte[] bytes = json.getBytes();
TestDto dto = DataSerializer.deserializeFromBytes(bytes, TestDto.class).orElse(null);
assertThat(dto).isNotNull();
assertThat(dto.name).isEqualTo("Eve");
}
@Test
void errorHandling_shouldReturnEmptyOptional() {
String invalidJson = "{ name: 'MissingQuotes' }";
var result = DataSerializer.deserialize(invalidJson, TestDto.class);
assertThat(result).isEmpty();
}
}
모든 테스트 통과
728x90