성능/구조 개선 한 거 구경 한번 해보세요~ (1 - 비동기 Spring Event 편)

Uicheon·2024년 7월 8일
0

개선

목록 보기
1/1

1. 구경 한번 와보세요~

1. 구조

현재 진행하는 프로젝트에서 카프카에서 메시지를 받아, 알림을 보내는 기능이 있다.

  • 알림은 여러개의 알림 항목을 가진다.
  • 알림 항목은 제목, 본문을 가진다.
  • 알림 항목은 여러개의 알림 대상을 가진다.
  • 알림 대상은 실제 알림을 보내기 위해 필요한 데이터를 저장한다.

숙련된 개발자인 본인은 두뇌 풀가동을 시전했다.

"알림 등록(생성)하고, 카프카 이벤트 리스너를 달아놓아서, 위에 그림처럼 알림 울리면 되겠다!" 라고 생각했다.

// CreateAlarm.java
@PostMapping
public void registerAlarm(@Valid @RequestBody CreateAlarmRequest createAlarmRequest) {
		AlarmResponse response = alarmService.registerAlarm(createAlarmRequest);
}

// kafkaListener.java
@Transactional  
@KafkaListener(topics = "#{'${spring.kafka.topic}'}", containerFactory = "KafkaListenerContainerFactory")  
public void push(@Payload MessageDto messageDto) {  
	// 알림 항목을 가져오는 코드
	List<AlarmItemResponse> alarmItemResponses = alarmRepository
    		.findById(messageDto.id())
			.orElseThrow(NotFoundException::new)
			.getAlarmItems().stream().map(AlarmItemResponse::new).toList();

	// 알림 타입 (SMS, EMAIL 등)을 골라, 알림을 요청하는 코드
    alarmItemResponses.forEach(alarmItemResponse -> {
	    PushActionService pushActionService = pushActionFactory.getStrategy(alarmItemResponse.type());
        pushActionService.send(alarmItemResponse, alarmItemResponse.receivers(), messageDto);
    });
} 

위와 같이, PushActionFactory을 만들고, Map 자료구조를 만들어, Type에 따라, Push Action을 가져오는 전략 패턴을 구성했다.
개발 당시에 디자인 패턴 하나 활용하는것 자체로 나는 갓개발자다 싶었다.

2. 빈약한 도메인 코드

그러다 문득 내 알림(Alarm 코드)는 화면단의 정보만 받기만하고, 도메인 자체로서 아무런 기능을 안하고 있다는 것을 깨달았다.

그래서 Alarm 도메인에 가장 중요한 역할인 알림을 발송하는 책임Alarm에서 처리할 수 있도록 이벤트를 도입했다.

도입하려면 다음 과정이 필요하다.
1. EventConfiguration 추가

@Configuration  
public class EventsConfiguration {  
   private final ApplicationContext applicationContext;  
  
   public EventsConfiguration(ApplicationContext applicationContext) {  
      this.applicationContext = applicationContext;  
   }  
  
   @Bean  
   public InitializingBean eventsInitializer() {  
      return () -> Events.setPublisher(applicationContext);  
   }  
}
  1. Events 클래스 추가
public class Events {  
  
   private static ApplicationEventPublisher publisher;  
  
   static void setPublisher(ApplicationEventPublisher publisher) {  
      Events.publisher = publisher;  
   }  
  
   public static void raise(Object event) {  
      if (publisher != null) {  
         publisher.publishEvent(event);  
      }  
   }  
}
  1. 냅다 원하는곳에 이벤트 발생 (Event)
// 이벤트 발생시키는 곳
Events.raise(new YourCustomEvent()); 

// 이벤트 처리하는 곳
@EventListner(YourCustomEvent.class)
public void handleYourCustomEvent(YourCustomEvent yourCustomEvent){
	...
}

그래서 내 프로젝트의 Alarm 다음과 같은 역할을 추가했다.

// Alarm.java
public void poundAlarm(MessageDto messageDto, AlarmPounder alarmPounder) {  
   alarmItems  
      .forEach(alarmItem -> alarmItem.poundAlarmItem(messageDto, alarmPounder));
}

// AlarmItem.java
public void poundAlarmItem(MessageDto messageDto, AlarmPounder alarmPounder) {  
   Events.raise(new PoundAlarmEvent(this, this.receivers, messageDto, alarmPounder));  
}

// PoundAlarmEvent.java
@EventListener(PoundInnerAlarmEvent.class)  
public void poundAlarm(PoundInnerAlarmEvent alarmEvent) {  
   AlarmItemResponse alarmItemResponse = new AlarmItemResponse(alarmEvent.alarmItem());  
   List<AlarmReceiverResponse> alarmReceiverResponses = alarmEvent.alarmReceivers()  
      .stream()  
      .map(AlarmReceiverResponse::new)  
      .toList();  
   MessageDto messageDto = alarmEvent.messageDto();  
   AlarmPounder alarmPounder = alarmEvent.alarmPounder();  
  
   PushActionService pushActionService = pushActionFactory.getStrategy(alarmItemResponse.pushType());  
   pushActionService.send(alarmItemResponse, alarmReceiverResponses, messageDto, alarmPounder);  
}

이제 내 Alarm은 실제로 알림을 울리는 역할도 갖게 됐다!

3. 비동기 처리

뿌듯해서 동료분들께 자랑했더니, 동료분은 조심스럽지만 굉장히 의아해하시며 "이건 구조를 개선한 것보단 그냥 다른 곳에서 서비스를 처리해주는 거 아닐까요? 비동기 처리 하시려고 그러시는거죠?" 하셨다.

생각해보니 그랬다. 내가 생각했던 것은 아래와 같은 그림이었지만, 알림 외엔 실제 구현하는 기능이 없었다.

그러므로 실제로는 아래 짤방처럼 된것이다.

그렇다면 어쩔 수 없다. 이렇게 된 이상 쓸모를 찾아보자.
동료분 조언대로 비동기 처리를 하면 되겠다 싶었다.
@Transactional으로 하나의 쓰레드를 유지하고 있었기에, Kafka에 먼저 ACK를 보내고, 실제 알림 발송을 비동기처리하면 성능 향상을 기대할 수 있었다.

비동기 처리는 생각보다 간단했다.
1. @EnableAsync 어노테이션 추가

@EnableAsync  
public class PushModuleApplication {
	...
}
  1. 원하는 EventHandler에 @Async추가
@Async  
@EventListener(PoundAlarmEvent.class)  
public void poundAlarm(PoundAlarmEvent alarmEvent) {
	...
}

또한, 동료분께서 @Transactional 의 경우, DB의 세션을 유지해야하기 때문에 (혹시 @Transactional 전파 했을지 모르니) 다른 쓰레드를 사용하고 있는지도 확인해보라 하셨다.
그래서 실제 Thread Number(Thread.currentThread().getId())를 로그에 찍어보며 확인했다.

  • @EnableAsync 없을 시

  • @EnableAsync 적용 후

다행히도, @Async어노테이션이 붙은 곳에서 다른 쓰레드 번호를 갖고 있었다.
사실 실제 k6 테스트를 해봤을 때 결과로 사실 큰 차이가 발생하진 않았다.
그럼에도 KafkaListener - Alarm - Event 시퀀스에서, 이벤트를 발행하기만 하면 그 뒤로는 다른 쓰레드로 돌아가기에 어느정도 성능 개선을 기대해볼 수 있다.

실제 알림을 울리는 작업이 비교적 리소스를 많이먹고, 시간이 오래걸리는 Disk Write IO외부 시스템 연동을 하기 때문에 성능 개선해볼 수 있는 귀중한 경험이었다.

profile
컨셉입니다~

0개의 댓글