처음 팀원들의 구현과 이벤트 페이로드를 비교했을 때, 이벤트 발생 시간을 나타내는 occurredAt 표기가 달랐다.
처음엔 유저 서비스가 jjwt 의존설정을 하면서 Jackson 라이브러리에 충돌이 일어나 버전을 명시해두었던게 문제였나 싶었다.
나는 OutBox 패턴을 적용하면서 kafka 직렬화를 사용하는 대신 ObjectMapper를 통해 이미 직렬화 된 객체를 Outbox에 넣고 나서 kafka 이벤트를 발행했다.
결과적으로 이벤트 생성을 나타내는 occurredAt이 ISO8601 방식으로 직렬화됐다.
"occurredAt": "2026-04-25T03:34:56Z"
TypeId 헤더 비활성화는 별다른 설정 없이 yml 파일에 속성을 추가하는것으로 처리했다.
spring:
kafka: # 이벤트 발행 시 kafka message header에 __TypeId__ 발행을 하지 않고, Json String으로 발행
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer # User 서비스에서는 직렬화된 상태로 발행
properties:
spring.json.add.type.headers: false # TypeId 헤더 비활성화
# 신뢰성 보강
acks: all # 모든 ISR 에 복제 확인 후 ACK
retries: 3 # 발행 실패 시 재시도 횟수
enable.idempotence: true # 중복 발행 방지 (Producer 측)
그런데 이 부분이 문제였다.
팀원들은 팀원이 작성해주신 문서를 따라하며 TypeId 헤더 비활성화를 하면서 문자열로 직렬화 설정도 추가했다.
@Configuration
public class KafkaProducerConfig {
/**
* Kafka 이벤트 발행용 ProducerFactory 설정
* <p>
* 목적: 1. LocalDateTime을 배열이 아닌 문자열로 직렬화한다. 2. Kafka Header에 Java 클래스 경로(__TypeId__)를 넣지 않는다. 3. MSA 환경에서 수신 서비스가
* ObjectMapper로 직접 역직렬화할 수 있게 한다.
*/
@Bean
public ProducerFactory<String, Object> producerFactory(KafkaProperties kafkaProperties) {
Map<String, Object> props = kafkaProperties.buildProducerProperties();
ObjectMapper objectMapper = new ObjectMapper(); // < -- 이 부분!!!
// LocalDateTime, Instant 등 Java Time 타입 직렬화를 지원한다.
objectMapper.registerModule(new JavaTimeModule());
// 날짜/시간 값을 [2026, 5, 12, 19, 0] 같은 배열이 아니라 문자열로 변환한다.
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
JsonSerializer<Object> jsonSerializer = new JsonSerializer<>(objectMapper);
// MSA 환경에서 발행 서버의 Java 클래스 경로를 Kafka Header에 넣지 않는다.
jsonSerializer.setAddTypeInfo(false);
DefaultKafkaProducerFactory<String, Object> factory =
new DefaultKafkaProducerFactory<>(
props,
new StringSerializer(),
jsonSerializer
);
// JsonSerializer를 코드로 직접 설정했으므로 yml 기반 추가 설정을 다시 주입하지 않도록 막는다.
factory.setConfigureSerializers(false);
return factory;
}
/**
* KafkaEventPublisher에서 주입받아 사용하는 KafkaTemplate
*/
@Bean
public KafkaTemplate<String, Object> kafkaTemplate(
ProducerFactory<String, Object> producerFactory
) {
return new KafkaTemplate<>(producerFactory);
}
}
그래서 팀원들은 occurdedAt이 Epoch Timestamp 방식으로 직렬화되었던 것이었다.
"occurredAt": 1777339234.090765000
그래서 기존 Epoch Timestamp 방식으로 시간을 직렬화했을 때 javascript쪽에서 손실이 일어나 정합성 문제가 발생할 수 있음을 설명하고, 다음 방법을 제안드렸다.
Spring Boot 2.0 이상 사용: Spring Boot는 개발자의 편의를 위해
Jackson2ObjectMapperBuilder를 통해 기본 설정을 변경합니다. Spring Boot 환경에서는WRITE_DATES_AS_TIMESTAMPS가 기본적으로 false로 설정되어 있어, 별도 설정 없이도 2026-04-25T03:34:56Z와 같은 ISO 8601 형식이 출력됩니다.
@Configuration
public class KafkaProducerConfig {
/**
* Kafka 이벤트 발행용 ProducerFactory 설정
*
* 수정사항: Spring이 관리하는 ObjectMapper를 파라미터로 주입받아 사용합니다.
*/
@Bean
public ProducerFactory<String, Object> producerFactory(
KafkaProperties kafkaProperties,
ObjectMapper objectMapper // <- Spring이 만든 빈을 주입받음
) {
Map<String, Object> props = kafkaProperties.buildProducerProperties();
// [변경 포인트]
// 1. 직접 new ObjectMapper() 하던 로직과 JavaTimeModule 등록 로직을 제거했습니다.
// 2. 이제 주입받은 objectMapper가 application.yml의 설정을 그대로 따릅니다.
JsonSerializer<Object> jsonSerializer = new JsonSerializer<>(objectMapper);
// MSA 환경에서 발행 서버의 Java 클래스 경로를 Kafka Header에 넣지 않는다.
jsonSerializer.setAddTypeInfo(false);
DefaultKafkaProducerFactory<String, Object> factory =
new DefaultKafkaProducerFactory<>(
props,
new StringSerializer(),
jsonSerializer
);
factory.setConfigureSerializers(false);
return factory;
}
@Bean
public KafkaTemplate<String, Object> kafkaTemplate(
ProducerFactory<String, Object> producerFactory
) {
return new KafkaTemplate<>(producerFactory);
}
}
혹시 모를 서버별 환경 차이나 버전 업데이트로 날짜 형식이 달라질것을 대비해,
공통 Config에 write-dates-as-timestamps: false 설정 (Instant를 ISO-8601로 표기하는 설정)을 명시하는 방법까지도 제시했다.
MVP 단계 발표 후 받은 피드백 정리.
max.message.bytes 한계. Outbox payload 크기 고려필요NicknameChangeReason.CREATE 재검토.