Spring Event๋ฅผ ์ฒ์ ๋ค์ด๋ณธ๋ค๋ฉด ์๋ ๊ธ์ ๋จผ์ ๋ณด๊ณ ์ค๋ ๊ฒ์ ์ถ์ฒํ๋ค.
https://dgjinsu.tistory.com/41
https://dgjinsu.tistory.com/42
์์ ์ 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์ ๊ฐ์ ๋ฉ์์ง ๋ธ๋ก์ปค๋ ์ํํ ์ ์๋๋ฐ ์ด๋ ์ถํ์ ํ์ต ํ ๊ณ ๋ฏผํด๋ด์ผ๊ฒ ๋ค.
์ฐธ๊ณ