Skip to content

Commit

Permalink
refactor: EventListener와 비동기 알림 서버 요청을 통한 번쩍 개설 응답속도 개선 (#585)
Browse files Browse the repository at this point in the history
* feat: 비동기 쓰레드풀 설정을 담당하는 Config 클래스 생성

* feat: coreSize와 threadNamePrefix를 yml과 매핑하는 Config 클래스 생성

* chore: 환경별로 thread pool 설정을 주입하도록 명시

* chore: 패키지 이동

* feat: 이벤트를 전달 DTO 생성

* feat: 번쩍 생성 시 푸시 알림 비동기 전송 기능 구현

- 번쩍 생성(FlashCreatedEvent) 발생 시 푸시 알림을 비동기로 전송하도록 구현
- @TransactionalEventListener(TransactionPhase.AFTER_COMMIT) 적용하여 트랜잭션 커밋 후 실행되도록 처리
- @async("taskExecutor")를 사용하여 비동기적으로 이벤트 리스너 실행
- 알림 서버에 보낼 PushNotificationRequestDto 생성 및 전송

* feat: 비동기 예외 처리 로직 구현

- GlobalAsyncExceptionHandler 구현하여 @async 메서드에서 발생하는 예외 처리
- handleUncaughtException()을 통해 비동기 작업 중 발생한 예외 로깅

* refactor: EventPublisher로 변경해 비동기 알림 전송 방식 개선

* fix: 비동기 요청 시 웹 요청 컨텍스트가 없어서 예외가 발생하는 문제 해결

- RequestContextHolder 접근 시, 스레드에 request가 없으면 null로 세팅하고 넘어가도록 설계
- DelegatingSecurityContextExecutor를 이용하여 비동기 스레드로 SecurityContext는 전달 가능하도록 변경

* chore: 테스트용 코드 제거

* chore: 사용하지 않는 어노테이션 제거
  • Loading branch information
hoonyworld authored Mar 5, 2025
1 parent 88b271a commit ce83ea7
Show file tree
Hide file tree
Showing 19 changed files with 297 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.sopt.makers.crew.main.entity.user.User;
import org.sopt.makers.crew.main.entity.user.UserRepository;
import org.sopt.makers.crew.main.external.notification.PushNotificationService;
import org.sopt.makers.crew.main.external.notification.dto.PushNotificationRequestDto;
import org.sopt.makers.crew.main.external.notification.dto.request.PushNotificationRequestDto;
import org.sopt.makers.crew.main.external.playground.service.MemberBlockService;
import org.sopt.makers.crew.main.global.config.PushNotificationProperties;
import org.sopt.makers.crew.main.global.exception.BadRequestException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.sopt.makers.crew.main.external.notification;

import org.sopt.makers.crew.main.external.notification.dto.PushNotificationRequestDto;
import org.sopt.makers.crew.main.external.notification.dto.PushNotificationResponseDto;
import org.sopt.makers.crew.main.external.notification.dto.request.PushNotificationRequestDto;
import org.sopt.makers.crew.main.external.notification.dto.response.PushNotificationResponseDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import java.util.UUID;

import org.sopt.makers.crew.main.external.notification.dto.PushNotificationRequestDto;
import org.sopt.makers.crew.main.external.notification.dto.request.PushNotificationRequestDto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.makers.crew.main.external.notification.dto.event;

import java.util.List;

public record FlashCreatedEventDto(
List<Integer> orgIds,
Integer meetingId,
String pushNotificationContent
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.sopt.makers.crew.main.external.notification.dto.request;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor(staticName = "of")
public class PushNotificationRequestDto {

private String[] userIds;

private String title;

private String content;

private String category;

private String webLink;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.sopt.makers.crew.main.external.notification.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class PushNotificationResponseDto {

@JsonProperty("status")
private Integer status;

@JsonProperty("success")
private Boolean success;

@JsonProperty("message")
private String message;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.sopt.makers.crew.main.external.notification.event;

import static org.sopt.makers.crew.main.external.notification.PushNotificationEnums.*;

import org.sopt.makers.crew.main.external.notification.PushNotificationService;
import org.sopt.makers.crew.main.external.notification.dto.event.FlashCreatedEventDto;
import org.sopt.makers.crew.main.external.notification.dto.request.PushNotificationRequestDto;
import org.sopt.makers.crew.main.global.config.PushNotificationProperties;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class FlashCreatedEventListener {

private final PushNotificationService pushNotificationService;
private final PushNotificationProperties pushNotificationProperties;

@Async("taskExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleFlashCreatedEvent(FlashCreatedEventDto event) {
log.info("{}명 FlashCreatedEvent 수신 - meetingId: {}, title: {}", event.orgIds().size(),
event.meetingId(), event.pushNotificationContent());

String[] orgIdArray = event.orgIds().stream()
.map(String::valueOf)
.toArray(String[]::new);

String pushNotificationWeblink =
pushNotificationProperties.getPushWebUrl() + "/detail?id=" + event.meetingId();

PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(orgIdArray,
NEW_FLASH_PUSH_NOTIFICATION_TITLE.getValue(),
event.pushNotificationContent(),
PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink);

pushNotificationService.sendPushNotification(pushRequestDto);

log.info("FlashCreatedEvent 알림 발송 완료");
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.sopt.makers.crew.main.flash.v2.service;

import static org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus.*;
import static org.sopt.makers.crew.main.external.notification.PushNotificationEnums.*;
import static org.sopt.makers.crew.main.global.constant.CrewConst.*;
import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*;

Expand All @@ -14,13 +13,11 @@
import org.sopt.makers.crew.main.entity.tag.enums.WelcomeMessageType;
import org.sopt.makers.crew.main.entity.user.User;
import org.sopt.makers.crew.main.entity.user.UserReader;
import org.sopt.makers.crew.main.external.notification.PushNotificationService;
import org.sopt.makers.crew.main.external.notification.dto.PushNotificationRequestDto;
import org.sopt.makers.crew.main.external.notification.dto.event.FlashCreatedEventDto;
import org.sopt.makers.crew.main.flash.v2.dto.mapper.FlashMapper;
import org.sopt.makers.crew.main.flash.v2.dto.request.FlashV2CreateAndUpdateFlashBodyDto;
import org.sopt.makers.crew.main.flash.v2.dto.response.FlashV2CreateAndUpdateResponseDto;
import org.sopt.makers.crew.main.flash.v2.dto.response.FlashV2GetFlashByMeetingIdResponseDto;
import org.sopt.makers.crew.main.global.config.PushNotificationProperties;
import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto;
import org.sopt.makers.crew.main.global.dto.OrgIdListDto;
import org.sopt.makers.crew.main.global.exception.BadRequestException;
Expand All @@ -33,6 +30,7 @@
import org.sopt.makers.crew.main.user.v2.service.UserV2Service;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -48,18 +46,16 @@ public class FlashV2ServiceImpl implements FlashV2Service {
private final UserV2Service userV2Service;
private final TagV2Service tagV2Service;
private final MeetingV2Service meetingV2Service;
private final PushNotificationService pushNotificationService;

private final FlashRepository flashRepository;
private final ApplyRepository applyRepository;

private final UserReader userReader;
private final FlashMapper flashMapper;

private final ApplicationEventPublisher eventPublisher;
private final Time realTime;

private final PushNotificationProperties pushNotificationProperties;

@Override
@Transactional
public FlashV2CreateAndUpdateResponseDto createFlash(
Expand All @@ -85,7 +81,8 @@ public FlashV2CreateAndUpdateResponseDto createFlash(

OrgIdListDto orgIdListDto = userReader.findAllOrgIds();

sendPushNotification(orgIdListDto.getOrgIds(), flash.getMeetingId(), flash.getTitle());
eventPublisher.publishEvent(
new FlashCreatedEventDto(orgIdListDto.getOrgIds(), flash.getMeetingId(), flash.getTitle()));

return FlashV2CreateAndUpdateResponseDto.from(flash.getMeetingId());
}
Expand Down Expand Up @@ -160,19 +157,4 @@ private List<ApplyWholeInfoDto> getApplyWholeInfoDtos(Applies applies, Integer m
.map(apply -> ApplyWholeInfoDto.of(apply, apply.getUser(), userId))
.toList();
}

private void sendPushNotification(List<Integer> orgIds, Integer meetingId, String pushNotificationContent) {
String[] orgIdArray = orgIds.stream()
.map(String::valueOf)
.toArray(String[]::new);

String pushNotificationWeblink = pushNotificationProperties.getPushWebUrl() + "/detail?id=" + meetingId;

PushNotificationRequestDto pushRequestDto = PushNotificationRequestDto.of(orgIdArray,
NEW_FLASH_PUSH_NOTIFICATION_TITLE.getValue(),
pushNotificationContent,
PUSH_NOTIFICATION_CATEGORY.getValue(), pushNotificationWeblink);

pushNotificationService.sendPushNotification(pushRequestDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.sopt.makers.crew.main.global.advice;

import java.lang.reflect.Method;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

@Override
public void handleUncaughtException(@NonNull Throwable ex, @NonNull Method method, @Nullable Object... params) {
if (params == null) {
log.error("비동기 작업 중 예외 발생! Method: [{}], Params: [null], Exception: [{}]",
method.getName(), ex.getMessage(), ex);
return;
}

String paramValues = java.util.Arrays.toString(params);

log.error("비동기 작업 중 예외 발생! Method: [{}], Params: [{}], Exception: [{}]",
method.getName(), paramValues, ex.getMessage(), ex);
}
}
Loading

0 comments on commit ce83ea7

Please sign in to comment.