[Event] ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ Spring Event๋ฅผ ์ ์šฉํ•ด ๋ณต์žกํ•œ ์˜์กด ๊ด€๊ณ„ ํ•ด์†Œํ•˜๊ธฐ

Spring Event๋ฅผ ์ฒ˜์Œ ๋“ค์–ด๋ณธ๋‹ค๋ฉด ์•„๋ž˜ ๊ธ€์„ ๋จผ์ € ๋ณด๊ณ  ์˜ค๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค. 

 

https://dgjinsu.tistory.com/41

 

[Event] Spring Event ์™œ ์‚ฌ์šฉํ• ๊นŒ??

1. ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ  Spring Boot์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—, ์ด๋ฒคํŠธ๋ฅผ ์™œ ์จ์•ผํ•˜๋Š”์ง€, ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์€ ์ƒํ™ฉ์— ๋Œ€ํ•ด ๋จผ์ € ์•Œ์•„๋ณด์ž. ํšŒ์›๊ฐ€์ž…์„ ํ•˜๊ณ  ๋‚˜๋ฉด ๊ฐ€์ž… ์ถ•ํ•˜

dgjinsu.tistory.com

https://dgjinsu.tistory.com/42

 

[Event] Spring Event ๋„์ž… ์‹œ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ

1. ์„œ๋ก  ์ „ ๊ฒŒ์‹œ๊ธ€์—์„œ Spring Event๋กœ ์˜์กด์„ฑ์„ ๊ฐœ์„ ํ•˜๋Š” ๋ฒ•์„ ๋‹ค๋ค„๋ณด์•˜๋‹ค. ํ•˜์ง€๋งŒ ํ•œ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์ฒ˜๋ฆฌํ•˜๋˜ ๊ฒƒ๋“ค์ด ํฉ์–ด์ง€๋ฉด์„œ ๋ฌธ์ œ๋˜๋Š” ๋ถ€๋ถ„๋„ ์žˆ๋‹ค. ์ด๋ฒˆ์—” ๊ทธ ๋ถ€๋ถ„์„ ๊นŠ๊ฒŒ ๋‹ค๋ค„๋ณผ ์ƒ๊ฐ์ด๋‹ค

dgjinsu.tistory.com

 

์˜ˆ์ „์— Spring Event์— ๋Œ€ํ•ด ์šฐ์—ฐํžˆ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , ๊ฐœ๋ฐœํ•˜๋˜ ์ค‘ ์—ฌ๊ธฐ ๋„์ž…ํ•˜๋ฉด ์ข‹๊ฒ ๋‹ค ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด ํ•™์Šต ํ›„ ๋„์ž…ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ์šฐ์„ ์ ์œผ๋กœ ์ ์šฉํ•˜๋ ค๋Š” ๋ถ€๋ถ„์€ ์•„๋ž˜ ๋‚ด์šฉ์ด๋‹ค. 

 

 

ํ˜„์žฌ ๊ฐœ๋ฐœ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ ๊ธฐ์—…์ด ๋…ธ๋™์ž์—๊ฒŒ ์ผ์ž๋ฆฌ๋ฅผ ์ œ์•ˆํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค๊ฒŒ๋˜์—ˆ๋‹ค. 

 

์ œ์•ˆ์„ ํ•˜๊ฒŒ๋˜๋ฉด ์•Œ๋ฆผ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ  FCM์„ ์‚ฌ์šฉํ•œ PUSH์•Œ๋ฆผ ๋ฐ ์นด์นด์˜ค ์•Œ๋ฆผํ†ก์„ ๋ณด๋‚ด์ฃผ๋Š” ํ”„๋กœ์„ธ์Šค๋กœ ๋™์ž‘ํ•œ๋‹ค. 

 

 

 

1.1 ์ ์šฉ ์ „ - ์•„ํ‚คํ…์ฒ˜

๋™์ž‘ํ•˜๋Š” ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ทธ๋ฆผ์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋‚˜ํƒ€๋‚ด๋ณด์•˜๋‹ค. 

1.2 ์ ์šฉ ์ „ - ์ฝ”๋“œ

- HeadHuntingService

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class HeadHuntingCompanyService {
    private final MemberRepository memberRepository;
    private final HeadHuntingRepository headHuntingRepository;
    private final NotificationService notificationService;
    private final JobPostRepository jobPostRepository;

    public void offerJobPost(Long companyId, OfferRequest request) {
        Member company = memberRepository.findById(companyId)
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
        HeadHunting headHunting = headHuntingRepository.findByIdWithMember(request.getHeadHuntingId())
            	.orElseThrow(() -> new CustomException(ErrorCode.HEAD_HUNTING_NOT_FOUND));
        // ์š”์ฒญํ•œ jobPostId ๊ฐ€ ์œ ํšจํ•œ์ง€ ์ฒดํฌ
        List<Long> jobPostIdList = request.getOfferJobPostRequest().stream()
                .map(OfferJobPostRequest::getJobPostId)
                .collect(Collectors.toList());
        Long findJobPostCount = jobPostRepository.findByIdList(jobPostIdList);
        if (jobPostIdList.size() != findJobPostCount) {
            throw new CustomException(ErrorCode.JOB_POST_NOT_FOUND);
        }

        List<OfferJobPostRequest> offerJobPostRequestList = request.getOfferJobPostRequest();
        for (OfferJobPostRequest offerJobPostRequest : offerJobPostRequestList) {
            String dateList = offerJobPostRequest.getWorkDateList().stream()
                    .map(date -> date.toString().substring(5, 10))
                    .collect(Collectors.joining(", "));

            String content = "[" + company.getCompanyInfo().getCompanyName() + "] ์—์„œ " + dateList + "์— ์ผ์ž๋ฆฌ๋ฅผ ์ œ์•ˆํ–ˆ์Šต๋‹ˆ๋‹ค.";
            String url = "/api/worker/job-post/" + jobPost.getId();

            notificationService.saveNotification(request.getMemberId(), NotificationType.OFFER, content, url);
        }
    }

 

 

- NotificationService

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class NotificationService {
    private final NotificationRepository notificationRepository;
    private final FCMNotificationService fcmNotificationService;
    private final MemberRepository memberRepository;

    public void saveNotification(Long receiverId, NotificationType type, String content, String url) {
        Member worker = memberRepository.findById(receiverId)
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));

        Notification notification = Notification.builder()
                .content(content)
                .url(url)
                .notificationType(type)
                .receiver(worker)
                .build();

        notificationRepository.save(notification);

        // push ์•Œ๋ฆผ ์ˆ˜๋ฝ ์‹œ
        if (worker.getIsNotification()) {
            // fcm ์•Œ๋ฆผ ๋ฐœ์†ก
            FCMNotificationRequestDto request = FCMNotificationRequestDto.builder()
                    .targetMemberId(worker.getId())
                    .title(content)
                    .body(content)
                    .build();
            fcmNotificationService.sendNotificationByToken(request);
        }
        
        // ์•Œ๋ฆผํ†ก ๋ฐœ์†ก
        ...
    }
}

 

 

์˜์กด์„ฑ ๊ด€๊ณ„๊ฐ€ ์—ฐ์‡„์ ์œผ๋กœ ์—ฐ๊ฒฐ๋˜์–ด์žˆ๊ณ  ์ด๋Š” ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ ๋“ฑ ์—ฌ๋Ÿฌ ์ธก๋ฉด์—์„œ ์•ˆ ์ข‹์€ ์˜ˆ๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค. 

 

 

 

์ด๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ฆฌํŽ™ํ† ๋ง ํ•˜๋ฉด์„œ ๊ฐ•ํ•œ ์˜์กด์„ฑ์„ ๊ฐ–๊ณ  ์žˆ๋˜ ๊ธฐ์กด์˜ ๋ฌธ์ œ์ ์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. 

 

2.1 ์ ์šฉ ํ›„ - ์•„ํ‚คํ…์ฒ˜

์ด๋ฒคํŠธ๋ฅผ ์ ์šฉํ•จ์œผ๋กœ์จ ์กฐ๊ธˆ์€ ๋” ๋ณต์žกํ•˜์ง€๋งŒ HeadHuntingService, NotificationService, FCMService, AliTalkService๋Š” ์„œ๋กœ ์–ด๋– ํ•œ ์˜์กด๋„ ํ•˜์ง€ ์•Š์Œ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. 

 

2.2 ์ ์šฉ ํ›„ - ์ฝ”๋“œ

์šฐ์„  HeadHuntingService => NotificationService ์˜์กด์„ฑ ๋ถ€ํ„ฐ ๋Š์–ด์ฃผ์—ˆ๋‹ค. 

 

- Event ํด๋ž˜์Šค ์ƒ์„ฑ (1)

@AllArgsConstructor
@Getter
public class NotificationEvent {
    // content & url ์— ์‚ฌ์šฉ
    private String companyName;
    private List<LocalDate> workDateList;
    private Long jobPostId;

    private NotificationType notificationType;
    private Long receiverId;
}

 

 

- EventHandler ์ƒ์„ฑ (1)

@Component
@RequiredArgsConstructor
public class NotificationHandler {
    private final NotificationService notificationService;

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void saveNotification(NotificationEvent event) {
        String dateList = event.getWorkDateList().stream()
                .map(date -> date.toString().substring(5, 10))
                .collect(Collectors.joining(", "));

        String content = "[" + event.getCompanyName() + "] ์—์„œ " + dateList + "์— ์ผ์ž๋ฆฌ๋ฅผ ์ œ์•ˆํ–ˆ์Šต๋‹ˆ๋‹ค.";
        String url = "/api/worker/job-post/" + event.getJobPostId();
        notificationService.saveNotification(event.getReceiverId(), event.getNotificationType(), content, url);
    }
}

 

 

- HeadHuntingService ์ˆ˜์ •

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class HeadHuntingCompanyService {
    private final MemberRepository memberRepository;
    private final ProjectRepository projectRepository;
    private final HeadHuntingRepository headHuntingRepository;
    private final ApplicationEventPublisher publisher;

    public void offerJobPost(Long companyId, OfferRequest request) {
        Member company = memberRepository.findById(companyId)
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
        HeadHunting headHunting = headHuntingRepository.findByIdWithMember(request.getHeadHuntingId())
                .orElseThrow(() -> new CustomException(ErrorCode.HEAD_HUNTING_NOT_FOUND));

        // ์š”์ฒญํ•œ jobPostId ๊ฐ€ ์œ ํšจํ•œ์ง€ ์ฒดํฌ
        List<Long> jobPostIdList = request.getOfferJobPostRequest().stream()
                .map(OfferJobPostRequest::getJobPostId)
                .collect(Collectors.toList());
        Long findJobPostCount = jobPostRepository.findByIdList(jobPostIdList);
        if (jobPostIdList.size() != findJobPostCount) {
            throw new CustomException(ErrorCode.JOB_POST_NOT_FOUND);
        }

        for (OfferJobPostRequest offerJobPostRequest : request.getOfferJobPostRequest()) {
            publisher.publishEvent(new NotificationEvent(company.getCompanyInfo().getCompanyName(), offerJobPostRequest.getWorkDateList(), offerJobPostRequest.getJobPostId(), NotificationType.OFFER, headHunting.getMember().getId()));
        }
    }
}

 

 

 

์ด์   NotificationService  => FCMService, AlimTalkService ์˜์กด์„ฑ์„ ๋Š์–ด์ฃผ์ž.

 

- Event ํด๋ž˜์Šค ์ƒ์„ฑ (2)

@AllArgsConstructor
@Getter
public class AlarmEvent {
    private String content;
    private Long receiverId;
    private String phone;
    private String templateCode;
}

 

 

- EventHandler ์ƒ์„ฑ (2)

@Component
@RequiredArgsConstructor
public class AlarmHandler {
    private final FCMNotificationService fcmNotificationService;
    private final AlimTalkService alimTalkService;

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendFCM(AlarmEvent event) {
        // fcm ์•Œ๋ฆผ ๋ฐœ์†ก
        FCMNotificationRequestDto request = FCMNotificationRequestDto.builder()
                .targetMemberId(event.getReceiverId())
                .title(event.getContent())
                .body(event.getContent())
                .build();
        fcmNotificationService.sendNotificationByToken(request);
    }

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendAlimTalk(AlarmEvent event) {
        alimTalkService.sendAlimTalk(event.getPhone(), event.getTemplateCode(), event.getContent());
    }
}

 

 

- NotificationService ์ˆ˜์ •

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class NotificationService {
    private final NotificationRepository notificationRepository;
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher publisher;

    public void saveNotification(Long receiverId, NotificationType type, String content, String url) {
        Member worker = memberRepository.findById(receiverId)
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));

        Notification notification = Notification.builder()
                .content(content)
                .url(url)
                .notificationType(type)
                .receiver(worker)
                .build();

        notificationRepository.save(notification);

        publisher.publishEvent(new FCMEvent(content, worker.getId()));
    }
}

 

 

 

 

3. ๊ฒฐ๋ก 

๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ์‘์ง‘๋„๋ฅผ ๋†’์ด๋‹ค!

๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด "์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋‚ฎ๊ณ  ์‘์ง‘๋„๊ฐ€ ๋†’์•„์•ผ ์ข‹์€ ์ฝ”๋“œ๋‹ค" ๋ผ๋Š” ๋ง์„ ๋“ค์–ด๋ดค์„ ๊ฒƒ์ด๋‹ค. ๋ฌด์—‡๋ณด๋‹ค ๋™์ž‘ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์ค‘์š”ํ•˜์ง€๋งŒ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๊ณ ๋ คํ–ˆ์„ ๋•Œ ๋ฌด์—‡๋ณด๋‹ค ์ค‘์š”ํ•˜๊ฒŒ ์ƒ๊ฐํ•ด์•ผํ•œ๋‹ค.

 

์ด๋ฒˆ์— ์Šคํ”„๋ง ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ํ•™์Šตํ•˜๊ณ  ์ด๋ฅผ ๋„์ž…ํ•ด ๋นˆ(Bean) ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ๋•Œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰(Publish)ํ•˜๊ณ  ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹  ๋˜๋Š” ๊ตฌ๋…ํ•˜์—ฌ ์†Œ๋น„(Listen /Subscribe)ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค. 

 

๋Š˜์–ด๋‚œ ์ฝ”๋“œ๋Ÿ‰๊ณผ ์ค„์–ด๋“  ์ง๊ด€๋ ฅ

์žฅ์ ๋งŒ ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹ˆ์—ˆ๋‹ค. ์Šคํ”„๋ง ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค๋ณด๋‹ˆ ๋ฆฌ์Šค๋„ˆ์—๊ฒŒ ์ „๋‹ฌํ•  ๋•Œ ํ•„์š”ํ•œ Event ๊ฐ์ฒด, ๊ฐ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ ์ฝ”๋“œ๋Ÿ‰๊ณผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณต์žก๋„๋Š” ๋†’์•„์กŒ๋‹ค. ๋˜ํ•œ ์ฝ”๋“œ๊ฐ€ ์ง๊ด€์ ์ด์ง€ ์•Š๋‹ค๋ณด๋‹ˆ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ์ฝ”๋“œ๋ฅผ ๋ณผ ๋•Œ ํž˜๋“ค์ˆ˜๋„ ์žˆ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

Spring Event์˜ ์—ญํ• ์„ Apache Kafka, RabbitMQ์™€ ๊ฐ™์€ ๋ฉ”์‹œ์ง• ๋ธŒ๋กœ์ปค๋„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด๋Š” ์ถ”ํ›„์— ํ•™์Šต ํ›„ ๊ณ ๋ฏผํ•ด๋ด์•ผ๊ฒ ๋‹ค. 

 

 

์ฐธ๊ณ 

https://mangkyu.tistory.com/292