ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • mono-repo / multi-module 프로젝트에서 spring 자동 설정 이용하기
    Project 2023. 8. 22. 00:49

     

     

    이번 글에서는 새로운 프로젝트를 진행하며 자동 구성을 이용해 여러 모듈에서 공통으로 쓸 수 있도록 설정한 부분에 대해 정리하도록 하겠습니다.

     

    현재 프로젝트 구조는 아래와 같습니다. gradle을 이용한 mono-repo / multi-module 형식의 프로젝트 구조입니다.

    live_feed
      |
      | - LiveFeedCommon
      | - LiveFeedCrawler
      | - LiveFeedParser
      | - LiveFeedSaver
      | - LiveFeedService

     

    각 모듈별로 각각의 역할을 갖고 있는데 이때, LiveFeedCommon 모듈의 경우 다른 모듈에서 공통으로 사용하는 부분을 묶어서 관리하고 싶었기 때문에 Common이라는 이름을 갖는 모듈로 만들었습니다.

     

    대부분의 다른 모듈에서 모두 kafka를 사용했기 때문에 가장 먼저 Kafka Producer를 자동 빈으로 등록해주도록 설정했습니다.

    자동 구성 설정에 대한 자세한 내용은 이전에 작성했던 글을 참고하면 도움이 될 것 같습니다.

     

     

    가장 먼저 프로듀서가 사용할 인터페이스를 정의했습니다. 메시지를 보내는 메서드만 필요하다고 생각했기 때문에 관련 인터페이스는 아래와 같습니다.

    public interface KafkaProducerTemplate<K, V> {
    
        void sendMessage(String topic, V value);
    
        void sendMessage(String topic, K key, V value);
    }
    

     

     

    다음으로 해당 인터페이스를 구현한 클래스입니다. 컴포지션을 사용하여 spring 프레임워크에서 제공하는 KafkaTemplate을 내부 필드로 갖고 있는 wrapper 클래스로 구현하였습니다.

    @Slf4j
    public class KafkaProducer<K, V> implements KafkaProducerTemplate<K, V> {
    
        private final KafkaTemplate<K, V> kafkaTemplate;
    
        public KafkaProducer(KafkaTemplate<K, V> kafkaTemplate) {
            this.kafkaTemplate = kafkaTemplate;
        }
    
        @Override
        public void sendMessage(String topic, V value) {
            sendMessage(topic, null, value);
        }
    
        @Override
        public void sendMessage(String topic, K key, V value) {
            ProducerRecord<K, V> record = new ProducerRecord<>(topic, key, value);
            sendProducerRecord(record);
        }
    
        private void sendProducerRecord(ProducerRecord<K, V> record) {
            CompletableFuture<SendResult<K, V>> sendResult = kafkaTemplate.send(record);
            sendResult.whenComplete((result, ex) -> {
                if (ex != null) {
                    // TODO: 2023/08/18 실제 에러 토픽으로 보낼지 혹은 로그만 작성할지 논의 필요
                    log.error("kafka producer send error", ex);
                }
            });
        }
    }
    

     

    현재 메시지 프로듀싱에 실패한 경우 로그만 남기고 따로 처리를 하고 있지 않은데 이후에 dead letter topic에 전달하는 로직이나 DB에 저장하는 로직을 추가할 예정입니다.

     

     

    다음으로 자동 구성 클래스에 대한 코드입니다. 스프링 부트에서 제공하는 @AutoConfiguration 어노테이션을 이용했습니다. 이때 @ConditionalOnProperty 어노테이션을 이용해서 특정 프로퍼티가 값을 만족하는 경우에만 해당 빈을 등록하도록 설정했습니다. 코드는 아래와 같습니다.

    @AutoConfiguration
    @ConditionalOnProperty(name = "custom.kafka.producer.is-enabled", havingValue = "true")
    @RequiredArgsConstructor
    public class KafkaProducerAutoConfiguration<K, V> {
    
        private final KafkaTemplate<K, V> kafkaTemplate;
    
        @Bean
        @ConditionalOnMissingBean
        public KafkaProducerTemplate<K, V> kafkaProducerTemplate() {
            return new KafkaProducer<>(kafkaTemplate);
        }
    }
    

     

    위에서 볼 수 있듯이 custom.kafka.producer.is-enabled 값이 true 인 경우와 동시에 KafkaProducerTemplate으로 등록된 빈이 없어야 LiveFeedCommon 모듈에서 설정한 KafkaProducerTemplate 인터페이스의 구현체가 빈으로 등록되는 것을 알 수 있습니다.

     

     

    마지막으로 spring boot 어플리케이션을 실행시킬때 해당 빈을 자동주입으로 진행할 수 있도록 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에 위에서 정의한 클래스 path를 작성하였습니다. 이를 통해 조건에 만족한다면 KafkaProducer를 원하는 모듈에 자동으로 등록할 수 있게 되었습니다.

    // LiveFeedCommon/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 
    com.livefeed.livefeedcommon.kafka.configuration.KafkaProducerAutoConfiguration
    

     


     

     

    지금부터는 실제 LiveFeedCrawler에서 사용한 모습입니다. 해당 코드는 실제 코드는 아니고 테스트용으로 간단하게 작성한 코드입니다.

    spring:
      kafka:
        bootstrap-servers: localhost:9093
        producer:
          key-serializer: org.apache.kafka.common.serialization.StringSerializer
          value-serializer: org.apache.kafka.common.serialization.StringSerializer
    
    server:
      port: 8081
    
    custom:
      kafka:
        producer:
          is-enabled: true
    

    먼저 로컬에서 테스트 하기 위한 application-local.yaml 파일입니다. 기본적인 spring.kafka 설정은 공식문서를 보면서 진행했고 가장 아래에 custom.kafka.producer.is-enabled: true 인 것을 확인할 수 있습니다.

     

    이를 통해 위에서 작성한 자동 주입으로 KafkaProducer가 빈으로 등록되고 다른 모듈에서는 단순히 사용만 하면되도록 설정했습니다.

     

     

    아래는 LiveFeedCrawler의 build.gradle 파일에서 위에서 설정한 LiveFeedCommon 모듈을 사용하기 위한 dependency 추가 모습입니다.

    plugins {
        id 'java-library'
    }
    
    version = '0.0.1'
    
    ext.VERSION = version
    
    dependencies {
    //    implementation 'org.springframework.boot:spring-boot-starter-batch'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation project(":LiveFeedCommon") // LiveFeedCommon 모듈 사용 가능하도록 설정
    }
    

     

     

    아래 코드는 간단한 테스트를 위해 실제로 KafkaProducerTemplate 빈을 주입받아서 특정 컨트롤러에서 사용한 모습입니다. 별다른 설정 없이 주입받아서 사용한 것을 확인할 수 있습니다.

    @RestController
    @RequestMapping("/crawler-hello")
    @RequiredArgsConstructor
    public class TestController {
    
        private final KafkaProducerTemplate<String, String> kafkaProducer;
    
        @GetMapping
        public String testKafkaController() {
            kafkaProducer.sendMessage("PRODUCER_CRAWLER_TEST", "자동주입 테스트입니다.");
            return "hello";
        }
    }
    

     

     

    마지막으로 실제로 빈에 등록되었는지 확인하기 위한 테스트 코드를 살펴보겠습니다.

    테스트는 간단하게 applicationContext에서 등록한 kafkaProducerTemplate이라는 이름의 빈이 있는지 확인하는 정도로 마무리 했습니다.

    @SpringBootTest
    public class AutoConfigurationTest {
    
        @Autowired
        private ApplicationContext applicationContext;
    
        @Test
        @DisplayName("kafka producer 자동 주입 관련 custom.kafka.producer.is-enabled 값이 true라면 주입한다.")
        void autoConfiguration() {
            // given
            // when
            Object kafkaProducerTemplate = applicationContext.getBean("kafkaProducerTemplate");
            // then
            assertThat(kafkaProducerTemplate).isNotNull();
        }
    }
    

     

     

    지금까지 LiveFeedCommon이라는 공통으로 사용할 클래스들을 모아둔 모듈에서 스프링의 빈 자동주입을 이용해 공통으로 이용하는 클래스를 구현하였고 이를 다른 모듈에서 사용하는 과정까지에 대한 내용이었습니다.

     

    실제 실무에서 mono-repo / multi-module 구조에서는 어떤식으로 사용하는지는 정확히 모르지만 이번에 이렇게 자동 주입을 이용하면서 좀 더 스프링의 자동주입 과정에 대해 복습할 수 있는 시간이었습니다.

     

    추후에 consumer 부분이나 JPA Entity 혹은 repository 관련 코드들을 공통으로 만들어 다른 모듈에서 바로 사용할 수 있도록 추가할 예정입니다.

    댓글

Designed by Tistory.