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