
기존에 Scheduler를 사용했던 서버는 Order-Service에서 주기적으로 결제/배송 상태를 변경해주기 위해서 사용했었습니다.
이를 위해서 Order-Service 내부에 Scheduler 관련 로직을 구현했었습니다.
기존에 Scheduler 도입 관련 자세한 내용은 아래의 포스팅에 정리했습니다.
Order-Service에서 대용량 주문 요청을 처리하도록 하기 위해 로드 밸런싱을 적용하던 중 Scheduler가 인스턴스마다 동작하게 되어 로직이 중복 동작하게 된다는 문제점이 발생했습니다.
이를 해결하기 위한 방법에 대해 고민을 하던 중 서버를 따로 두면 되겠다는 생각을 하게되었습니다.
마치 동기화를 위해 여러 인스턴스에서 하나의 DB를 관리하는 것과 같은 원리입니다.
이로 인해, 기존의 Order-Service 내 Scheduler 관련 로직을 Schedule-Service로 분리하였으며, 이 과정을 담은 포스팅입니다.
우선, schedule-service라는 새로운 서버를 생성합니다.

그 다음, 스케줄러를 활성화 해줍니다.

그 다음으로 기존의 order-service에 정의되어 있던 스케줄링 관련 코드를 schedule-service로 옮겨 줍니다.

처음에는 schedule-service와 order-service 간의 통신을 feign-client를 사용하여 구현했었습니다.
하지만, 위처럼 구현 시 문제점은 schedule-service가 order-service에 의존도가 높아지게 됩니다. 이로 인해서 order-service에 문제가 발생하게 되면, schedule-service 내 장애 전파 차단 관련 로직이 추가되어야 합니다.
이는 서비스가 가진 책임에 비해서 비효율적으로 코드량이 많아지게 된다고 판단하였고, 비동기 통신을 도입해야 겠다고 생각했습니다.
위와 같은 결론에 따라, 오픈 소스 메시지 브로커인 "RabbitMQ"의 도입을 결정하였습니다.
우선, RabbitMQ 관련 의존을 추가해줍니다.
implementation 'org.springframework.boot:spring-boot-starter-amqp'
그 다음으로 RabbitMQ 관련 configuration을 추가해줍니다.
RabbitMQ 서버와 연결을 설정하기 위해서 계정 정보와 port 정보를 application.yml에서 가져와 사용하도록 @ConfigurationPropertiesScan 어노테이션을 추가해줍니다.
RabbitMQProperties를 통해 설정된 값들을 가져와 사용해줍니다.
@Configuration
@RequiredArgsConstructor
public class RabbitConfig {
private final RabbitMQProperties rabbitMQProperties;
/**
* RabbitMQ 연동을 위한 ConnectionFactory 빈을 생성하여 반환
**/
@Bean
public CachingConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(rabbitMQProperties.getHost());
connectionFactory.setPort(rabbitMQProperties.getPort());
connectionFactory.setUsername(rabbitMQProperties.getUsername());
connectionFactory.setPassword(rabbitMQProperties.getPassword());
return connectionFactory;
}
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("commonExchange");
}
// durable 을 false 로 설정하면, rabbitMQ 재시작 시에 큐가 삭제됨
@Bean
public Queue orderServiceQueue(){
return new Queue("ORDER_SERVICE_QUEUE");
}
@Bean
public Binding orderServiceBinding(Queue orderServiceQueue, DirectExchange exchange){
return BindingBuilder.bind(orderServiceQueue).to(exchange).with("orderRoutingKey");
}
/**
* RabbitTemplate
* ConnectionFactory 로 연결 후 실제 작업을 위한 Template
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}
}
그리고 메시지 큐로 메시지를 전달하기 위해 Producer를 정의해줍니다.
@Component
@RequiredArgsConstructor
public class Producer {
private final RabbitTemplate rabbitTemplate;
public void sendMessageToOrderService(String message){
rabbitTemplate.convertAndSend("commonExchange","orderRoutingKey",message);
}
}
전송할 데이터를 객체로 정의하여 ObjectMapper를 사용해 Producer로 전달해줍니다.
private <T> void convertAndSendToOrder(T dto){
String message = null;
try {
message = objectMapper.writeValueAsString(dto);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
producer.sendMessageToOrderService(message);
}
order-service에 consumer를 정의해줍니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class Consumer {
private final ObjectMapper objectMapper;
private final OrderService orderService;
@RabbitListener(queues = "ORDER_SERVICE_QUEUE")
public void receiveMessage(String message) throws IOException {
log.debug("이벤트 consuming : {}", message);
EventMessage eventMessage = objectMapper.readValue(message, EventMessage.class);
if("SHIPPING".equals(eventMessage.getEvent())){
orderService.changeOrderStatusAfterPayment();
}
else if("DONE".equals(eventMessage.getEvent())){
orderService.changeOrderStatusAfterShipping();
}
else if("RETURN".equals(eventMessage.getEvent())){
orderService.changeOrderStatusForReturn();
}
}
}
schedule-service를 기동하고 Queue와 관련 Exchange가 생성되는지 확인해보겠습니다.

정상적으로 스케줄링이 동작했고, RabbitMQ 서버 대시보드로 접속해보겠습니다.

큐 관련 설정이 담긴 exchange가 정상적으로 생성되었습니다.

"ORDER_SERVICE_QUEUE" 명칭의 메시지 큐도 정상적으로 생성되었습니다.

order-service에 정의한 Consumer를 통해서 실제 메시지를 받는지 확인했습니다.

이와 같이 RabbitMQ를 통해서 메시지가 정상적으로 Producing되고 Consuming 되는 것을 확인했습니다.
💡 고민사항
현재는 간단한 로직만 구현되어 있기 때문에 문제가 없지만, 만약 RabbitMQ 서버의 문제 발생 시 대처 방안에 대한 고민을 추가로 해볼 필요가 있습니다. 이에 대해서는 RabbitMQ 공식문서의 메시지큐 관리에 대한 내용을 학습한 뒤 진행해봐야 겠습니다.