BindProject

[Backend] infra-messaging 모듈 설계 및 구현

dding-shark 2025. 6. 17. 15:17
728x90

인프라 메시징 모듈 설계


도입

대규모 서비스에서 Kafka는 단순한 메시지 브로커 이상의 역할을 합니다.
이벤트 기반 아키텍처에서 비동기 통신을 안정적으로 처리하기 위한 핵심 구성 요소로 자리잡고 있죠.

이번에는 서비스 간 이벤트 흐름의 기반이 되는 인프라 메시징 모듈을 설계하고,
Kafka 전송을 추상화한 프로듀서 구조와 구성 전략, 그리고 테스트 방법까지 정리합니다.


설계 목표

  • Kafka 연동을 각 서비스 모듈에 강제하지 않고, 인프라 수준에서 추상화
  • 전송 포맷 통일 (JSON 직렬화)
  • 이벤트 타입 명시 및 시점 기록 (occurredAt) 포함
  • 모듈 분리를 통한 테스트 용이성 확보
  • 향후 Consumer 및 Outbox 확장 고려

설계 고려사항

🔹 추상화 레이어 설계

서비스는 CustomEvent만 구현하면 되도록 간결한 인터페이스 제공
→ 내부 직렬화, KafkaTemplate 사용, 토픽 분기 등은 모두 내부에서 처리

🔹 Kafka 설정 외부화

  • KafkaProperties는 Spring Boot의 자동 구성 대상이므로 그대로 사용
  • KafkaTopicProperties는 프로젝트 도메인에 맞게 별도 구성

🔹 퍼블릭 모듈 테스트

@SpringBootApplication이 없는 퍼블릭 모듈에서도 테스트 가능하도록
@ContextConfiguration 기반 테스트 환경 구성


구현 구조

패키지 구조

infra-messaging
├── config
│   └── KafkaProducerConfig.java
├── producer
│   └── KafkaEventProducer.java
├── properties
│   └── KafkaTopicProperties.java
└── resources
    └── application.yaml

KafkaEventProducer

@Component
public class KafkaEventProducer {

    private final KafkaTemplate<String, String> kafkaTemplate;
    private final KafkaTopicProperties topicProperties;

    public KafkaEventProducer(KafkaTemplate<String, String> kafkaTemplate,
                              KafkaTopicProperties topicProperties) {
        this.kafkaTemplate = kafkaTemplate;
        this.topicProperties = topicProperties;
    }

    public void send(CustomEvent event) {
        KafkaEventPayload payload = KafkaEventSerializer.serialize(event);
        String topic = resolveTopic(event.name());
        DataSerializer.serialize(payload)
                .ifPresent(json -> kafkaTemplate.send(topic, json));
    }

    private String resolveTopic(String eventName) {
        return switch (eventName) {
            case "user.registered" -> topicProperties.getUserRegistered();
            case "order.created" -> topicProperties.getOrderCreated();
            default -> throw new IllegalArgumentException("Unknown event: " + eventName);
        };
    }
}

KafkaProducerConfig

@Configuration
@EnableConfigurationProperties({KafkaProperties.class, KafkaTopicProperties.class})
public class KafkaProducerConfig {

    private final KafkaProperties kafkaProperties;

    public KafkaProducerConfig(KafkaProperties kafkaProperties) {
        this.kafkaProperties = kafkaProperties;
    }

    @Bean
    public ProducerFactory<String, String> producerFactory() {
        Map<String, Object> props = new HashMap<>(kafkaProperties.buildProducerProperties());
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return new DefaultKafkaProducerFactory<>(props);
    }

    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}

테스트 전략

퍼블릭 모듈 특성상 Spring Boot의 전체 Context를 사용할 수 없으므로,
@SpringBootTest 대신 @ContextConfiguration 기반 테스트 구성:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
    KafkaEventProducerTest.TestConfig.class
})
@TestPropertySource("classpath:application.yaml")
class KafkaEventProducerTest {

    @Autowired
    private KafkaEventProducer kafkaEventProducer;

    static class TestUserRegisteredEvent implements CustomEvent {
        @Override public String name() { return "user.registered"; }
    }

    @Test
    void 전송_테스트() {
        kafkaEventProducer.send(new TestUserRegisteredEvent());
    }

    @Configuration
    @ComponentScan(basePackages = "inframessaging.producer")
    @Import(KafkaProducerConfig.class)
    @EnableConfigurationProperties(KafkaTopicProperties.class)
    static class TestConfig {}
}

회고 및 확장 가능성

이 모듈을 통해 서비스는 Kafka 전송에 대한 책임을 지지 않고도 손쉽게 이벤트를 발행할 수 있게 되었습니다.
하지만 여전히 Consumer 책임 분리, Outbox 패턴 적용, 에러 핸들링 보완,
그리고 Tracing 및 로그 수집 구조와의 통합까지 고려할 점이 많습니다.

이번 설계를 통해 한 단계 더 나아간 이벤트 기반 아키텍처를 향한 초석을 다졌다고 생각합니다.
향후에는 이 구조를 기반으로 Outbox 모듈, Retry-Topic 처리, Kafka Tracing까지도 도전할 예정입니다.

728x90