현재 진행하는 프로젝트에서 카프카에서 메시지를 받아, 알림을 보내는 기능이 있다.
숙련된 개발자인 본인은 두뇌 풀가동을 시전했다.
"알림 등록(생성)하고, 카프카 이벤트 리스너를 달아놓아서, 위에 그림처럼 알림 울리면 되겠다!" 라고 생각했다.
// 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을 가져오는 전략 패턴을 구성했다.
개발 당시에 디자인 패턴 하나 활용하는것 자체로 나는 갓개발자다 싶었다.
그러다 문득 내 알림(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);
}
}
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);
}
}
}
// 이벤트 발생시키는 곳
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
은 실제로 알림을 울리는 역할도 갖게 됐다!
뿌듯해서 동료분들께 자랑했더니, 동료분은 조심스럽지만 굉장히 의아해하시며 "이건 구조를 개선한 것보단 그냥 다른 곳에서 서비스를 처리해주는 거 아닐까요? 비동기 처리 하시려고 그러시는거죠?" 하셨다.
생각해보니 그랬다. 내가 생각했던 것은 아래와 같은 그림이었지만, 알림 외엔 실제 구현하는 기능이 없었다.
그러므로 실제로는 아래 짤방처럼 된것이다.
그렇다면 어쩔 수 없다. 이렇게 된 이상 쓸모를 찾아보자.
동료분 조언대로 비동기 처리
를 하면 되겠다 싶었다.
@Transactional
으로 하나의 쓰레드를 유지하고 있었기에, Kafka에 먼저 ACK
를 보내고, 실제 알림 발송을 비동기처리하면 성능 향상을 기대할 수 있었다.
비동기 처리
는 생각보다 간단했다.
1. @EnableAsync
어노테이션 추가
@EnableAsync
public class PushModuleApplication {
...
}
@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
와 외부 시스템 연동
을 하기 때문에 성능 개선해볼 수 있는 귀중한 경험이었다.