BindProject

포인트 시스템 구축기 : 이벤트 발행 시스템을 위한 플러그인 아키텍처 구축기

dding-shark 2025. 7. 29. 02:57
728x90

이벤트 발행 시스템을 위한 플러그인 아키텍처 구축기

초기 프로젝트의 시스템은 안정적으로 동작했지만, 비즈니스 요구사항이 확장되면서 기술적인 부채가 쌓일 수 있는 구조적 문제에 직면했습니다.

이 글에서는 특정 기술(Kafka)에 강하게 결합되어 있던 구조를 어떻게 유연하고 확장 가능한 플러그인 아키텍처(Plugin Architecture)로 개선했는지, 그 계기와 과정을 상세히 설명해 드리겠습니다.

 

 

 


 

 

 

1. 문제의 시작: 우리의 초기 아키텍처 (AS-IS)

저희 시스템은 MSA 환경에서 서비스 간의 비동기 통신과 데이터 정합성을 위해 아웃박스 패턴(Outbox Pattern)을 사용하고 있습니다. OutboxEventProcessor가 주기적으로 데이터베이스의 이벤트 테이블을 읽어 메시지 브로커로 발행하는 구조였죠.

초기 구현은 모든 이벤트를 카프카(Kafka)로만 보낸다는 가정하에 매우 단순하고 직관적이었습니다.

기존 OutboxEventProcessor 코드 (AS-IS)

@Slf4j
@Service
@RequiredArgsConstructor
public class OutboxEventProcessor {
    private final OutboxEventRepository outboxEventRepository;
    // ▼▼▼ 카프카 프로듀서에 직접 의존 ▼▼▼
    private final KafkaEventProducer kafkaProducerService;

    @Transactional
    public void processEvents() {
        var events = outboxEventRepository.findProcessableEvents(...);
        for (OutboxEventEntity event : events) {
            try {
                // ▼▼▼ 항상 카프카로만 이벤트를 발행 ▼▼▼
                kafkaProducerService.send(event.getTopic(), event.getPayload());
                event.markSuccess();
            } catch (Exception e) {
                event.markFailure(...);
            }
            outboxEventRepository.save(event);
        }
    }
}

이 구조는 명확했지만, 다음과 같은 한계를 가지고 있었습니다.

  • 확장성의 부재: 만약 "포인트 적립"과 같은 새로운 기능에 RabbitMQ를 도입하고 싶다면? OutboxEventProcessor의 코드를 직접 수정해야만 했습니다.
  • SOLID 원칙 위배: 새로운 메시징 기술을 추가할 때마다 기존 코드를 변경해야 하므로 개방-폐쇄 원칙(OCP)에 위배됩니다.
  • 강한 결합(Tightly Coupled): 비즈니스 로직이 특정 인프라 기술(Kafka)에 꽉 묶여 있었습니다.

 

 

 


 

 

 

2. 해결의 열쇠: 플러그인 아키텍처(Plugin Architecture)란?

새로운 요구사항이 생길 때마다 중앙 처리 로직을 수정하는 것은 지속 가능한 방법이 아니었습니다. 여기서 저희는 문제 해결의 열쇠로 플러그인 아키텍처를 선택했습니다.

플러그인 아키텍처를 가장 쉽게 비유하자면 컴퓨터의 USB 포트와 같습니다.

  • 컴퓨터 본체 (코어 시스템): USB라는 표준 규격만 알고 있습니다. 여기에 키보드가 연결될지, 마우스가 연결될지, 혹은 웹캠이 연결될지는 신경 쓰지 않습니다.
  • USB 포트 (확장 포인트/인터페이스): 주변기기(플러그인)가 컴퓨터와 통신하기 위해 반드시 지켜야 할 표준 규격(약속)입니다.
  • 키보드, 마우스 (플러그인): USB 규격에 맞게 만들어진 독립적인 하드웨어입니다. 언제든지 꽂아서(plug-in) 기능을 확장하고, 필요 없으면 뽑을 수 있습니다.

이를 소프트웨어 세계에 적용하면 다음과 같이 정의할 수 있습니다.

  • 코어 시스템 (Core System): 핵심 비즈니스 로직을 수행하며, 기능 확장을 위한 '연결 지점'을 정의합니다. (우리의 사례: OutboxEventProcessor)
  • 확장 포인트 (Extension Point / Interface): 플러그인이 코어 시스템과 통신하기 위해 반드시 구현해야 하는 메소드의 집합, 즉 약속(Contract)입니다. (우리의 사례: EventProducer 인터페이스)
  • 플러그인 (Plugin): 확장 포인트를 구현하여 특정 기능을 수행하는, 독립적이고 교체 가능한 모듈입니다. (우리의 사례: KafkaEventProducer, RabbitMQEventProducer)

이 구조를 도입하면 OutboxEventProcessor"이벤트 발행"이라는 자신의 핵심 임무만 알면 됩니다. 이벤트를 "어떻게" 보낼 것인지(Kafka 방식, RabbitMQ 방식)에 대한 구체적인 방법은 각각의 "플러그인"이 책임지게 됩니다.

 

 

 

 


 

 

 

3. 새로운 아키텍처로의 전환 (TO-BE)

이 아이디어를 실현하기 위해 다음과 같은 단계로 리팩토링을 진행했습니다.

1단계: 표준 규격(인터페이스) 정의

EventProducer 인터페이스를 정의하여 'USB 포트'와 같은 표준을 만들었습니다.

public interface EventProducer {
    void send(String destination, String payload);
    BrokerType getBrokerType(); // 자신이 어떤 종류의 브로커인지 식별자를 반환
}

2단계: 표준에 맞는 플러그인(구현체) 제작

기존 KafkaEventProducer와 새로 만들 RabbitMQEventProducer가 이 표준을 따르도록 했습니다.

@Service
public class KafkaEventProducer implements EventProducer {
    /* ... 카프카 전송 로직 ... */
    @Override
    public BrokerType getBrokerType() { return BrokerType.KAFKA; }
}

@Service
public class RabbitMQEventProducer implements EventProducer {
    /* ... RabbitMQ 전송 로직 ... */
    @Override
    public BrokerType getBrokerType() { return BrokerType.RABBITMQ; }
}

3단계: 스마트한 이벤트 분배기(Processor) 구현

OutboxEventProcessor가 모든 '플러그인'들을 관리하며, 이벤트에 맞는 플러그인을 선택해 전달하도록 코드를 완전히 변경했습니다.

리팩토링 후 OutboxEventProcessor 코드 (TO-BE)

@Service
@RequiredArgsConstructor
public class OutboxEventProcessor {
    // ▼▼▼ 모든 EventProducer '플러그인'들을 주입받음 ▼▼▼
    private final List<EventProducer> producers;
    private Map<BrokerType, EventProducer> producerMap;

    @PostConstruct
    public void init() {
        // 애플리케이션 시작 시, 플러그인 목록을 Map으로 정리
        producerMap = producers.stream()
                .collect(Collectors.toMap(EventProducer::getBrokerType, p -> p));
    }

    @Transactional
    public void processEvents() {
        var events = outboxEventRepository.findProcessableEvents(...);
        for (OutboxEventEntity event : events) {
            try {
                // 이벤트에 맞는 '플러그인'을 찾아 실행
                EventProducer producer = producerMap.get(event.getBrokerType());
                producer.send(event.getTopic(), event.getPayload());
                event.markSuccess();
            } catch (Exception e) { /* ... */ }
            outboxEventRepository.save(event);
        }
    }
}

4단계: 이벤트에 목적지 정보 추가

이벤트 스스로가 어떤 브로커로 가야 할지 정의할 수 있도록 CustomEvent 인터페이스에 default 메소드를 추가했습니다.

public interface CustomEvent {
    String getTopic();
    // 다른 브로커를 쓰고 싶으면 이 메소드를 재정의하면 됨.
    default BrokerType getBrokerType() {
        return BrokerType.KAFKA;
    }
}

 

 

 

 


 

 

 

4. 결과 및 효과: 우리가 얻게 된 것

이 리팩토링을 통해 저희 시스템은 놀라운 유연성을 얻게 되었습니다.

  • 완벽한 확장성: 이제 AWS SQS를 지원해야 한다면, SqsEventProducer를 만들어 EventProducer 인터페이스를 구현하기만 하면 됩니다. 기존 코드는 단 한 줄도 건드릴 필요가 없습니다. 진정한 개방-폐쇄 원칙(OCP)을 달성한 것입니다.
  • 관심사의 완벽한 분리: StudioService 같은 비즈니스 로직은 "스튜디오가 생성되었다"는 무엇(What)에만 집중하고, OutboxEventProcessorEventProducer는 이벤트를 어떻게(How) 보낼지에만 집중합니다.
  • 유지보수 용이성: 이제 카프카 관련 로직을 수정하고 싶으면 KafkaEventProducer만 보면 됩니다. 책임이 명확히 분리되어 디버깅과 유지보수가 훨씬 쉬워졌습니다.
  • 기존 코드의 안정성: 가장 중요한 점은, 이 모든 개선 작업을 진행하는 동안 기존 이벤트 발행 코드는 전혀 수정하지 않았다는 것입니다.

마치며

소프트웨어 아키텍처에서 "느슨한 결합(Loose Coupling)"과 "높은 응집도(High Cohesion)"는 아무리 강조해도 지나치지 않습니다. 저희는 이번 리팩토링을 통해 특정 기술에 대한 강한 의존성을 제거하고, 변화에 유연하게 대처할 수 있는 플러그인 아키텍처를 성공적으로 구축했습니다.

.

728x90