
โ๊ฐ์
ํ์ด๋ ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ผ๋ก ์๋ฆผ ๊ธฐ๋ฅ์ด ์กด์ฌํ์์ต๋๋ค. ์๋ฆผ ๊ธฐ๋ฅ์ ๋ฃ๊ธฐ์ ์์ ์ด๋ค์์ผ๋ก ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ๋ณด๋ผ์ง ๊ธฐ์ ์ ์ผ๋ก ์ ํํด์ผํ๋ ์ํฉ์ด์์ต๋๋ค. FCM๊ณผ SSE์ ๋ํด 2๊ฐ์ง ์ ํ์ฌํญ์ด ์์์ต๋๋ค. FCM ๊ฐ์ ๊ฒฝ์ฐ ์๋ฒ ๋ฆฌ์์ค๋ฅผ ์ฐ์ง ์๊ณ ์ธ๋ถ ์๋ฒ๋ฅผ ์ด์ฉํด ์๋ฆผ ๊ธฐ๋ฅ์ ์ฑ
์์ ๋ถ์ฌํ๊ณ ์ค์๊ฐ ์๋ฆผ์ด ๊ฐ๋ฅํฉ๋๋ค. ํ์ง๋ง ์ ํฌ ํ๋ก์ ํธ๊ฐ์ ๊ฒฝ์ฐ ํด๋ผ์ด์ธํธ๊ฐ ์๊ธฐ๋๋ฌธ์ ๋๋ฐ์ด์ค ํ ํฐ ์ ๋ณด๋ฅผ ๋ฐ์์ฌ ์ ์๊ณ ํ
์คํธ ์ฝ๋๋ง์ผ๋ก๋ ํ
์คํธํ๊ธฐ์๋ ๋ฌด๋ฆฌ๊ฐ ์๋ค๊ณ ํ๋จํ์ฌ SSE ๋ฅผ ํตํด ์๋ฆผ๊ธฐ๋ฅ์ ๋ณด๋ด๊ธฐ๋ก ๊ฒฐ์ ํ์์ต๋๋ค. ์ค์๊ฐ์ฑ์ FCM ์ ๋นํด ๋จ์ด์ง์ง๋ง ๊ธฐ์กด HTTP ์ธํ๋ผ๋ฅผ ๊ทธ๋๋ก ํ์ฉํ ์ ์์ผ๋ฉฐ ์ด๋ฒคํธ ํ์์ผ๋ก ์ฑ
์์ ๋ถ๋ฆฌํ ์ ์๋ ๋ฐฉ๋ฒ์ด ์กด์ฌํ๊ธฐ์ SSE ๋ฐฉ์์ผ๋ก ๊ตฌํํ๊ธฐ๋ก ๊ฒฐ์ ํ์ต๋๋ค. ๊ทธ๋ ๋ค๋ฉด ์ง๊ธ ๋ถํฐ Event + SSE ๋ฅผ ํตํด ๊ตฌํ๋ฐฉ๋ฒ์ ์ค๋ช
ํ๊ฒ ์ต๋๋ค.
โ๏ธ SSE ๋์ ์๋ฆฌ
SSE ๊ตฌํ ๋ฐฉ๋ฒ์ ์ค๋ช
ํ๊ธฐ์ ์์ ๋จผ์ SSE ๋์ ์๋ฆฌ์ ๋ํด์ ์ง๊ณ ๋์ด๊ฐ๋๋ก ํ๊ฒ ์ต๋๋ค.

GET ์์ฒญ์ ๋ณด๋
๋๋ค.text/event-stream ์ปจํ
์ธ ํ์
์ผ๋ก ์๋ตํ๊ฒ ๋ฉ๋๋ค.5xx ์๋ฌ๋ฅผ ํผํ ์ ์์ต๋๋ค.data ์ ๋์ฌ๋ฅผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.Last-Event-Id๋ฅผ ํตํด ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ์ด๋ฒคํธ๋ถํฐ ๋ค์ ๋ฐ์ ์ ์๊ฒ๋ฉ๋๋ค.
SSE๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๊ฐ ์ ์ก๋ฉ๋๋ค.WebSocket๊ณผ ๋ฌ๋ฆฌ ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก ๋ฐ์ดํฐ ์ ์ก์ ๋ถ๊ฐ๋ฅํฉ๋๋ค. ๋ํ ๋ธ๋ผ์ฐ์ ์ฐ๊ฒฐ ์ ์ ํ์ผ๋ก ์ธํด ๋์ ์ฐ๊ฒฐ ์๊ฐ ์ ํ๋ ์ ์์ต๋๋ค. ํ์ง๋งWebSocket์ ๋นํด ๊ตฌํ์ด ๊ฐ๋จํ๋ฉฐHTTP๋ฅผ ์ฌ์ฉํ๊ธฐ์ ๋ค๋ฅธ ์๋ฒ์์ ํธํ์ฑ์ด ์ข๊ธฐ ๋๋ฌธ์ ์๋ฆผ๊ธฐ๋ฅ, ์ค์๊ฐ ๋ก๊ทธ ๋ชจ๋ํฐ๋ง์ผ๋ก ๋ง์ด ์ฌ์ฉํ๋ ๋ฐฉ์์ ๋๋ค.
๐ฌ Spring Event ์ ๋ํด
๊ตฌํ ์ฝ๋๋ฅผ ์ค๋ช
ํ๊ธฐ์ ์์ Spring Event ๋ฐํ ํ๊ณ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ๊ฐ๋จํ ์์ ๋ณด๊ฒ ์ต๋๋ค.
์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋ ค๋ฉด Event ๊ฐ์ฒด๊ฐ ํ์ํ๋ฉฐ ์ด ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ๋ณด๋ด์ค ApplicationEventPublisher ๊ฐ ํ์ํฉ๋๋ค. ๋ํ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ EventListener ๋ฅผ ํตํด ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํด์ค ์ปดํฌ๋ํธ๊ฐ ํ์ํฉ๋๋ค.
์ 3๊ฐ๋ง ์๋ค๋ฉด ๊ฐ๋จํ๊ฒ ์ด๋ฒคํธ๋ฅผ ๋ฐํํด ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ๊ฐ๋จํ๊ฒ ์ฝ๋๋ก ํ์ธํด๋ณด๊ฒ ์ต๋๋ค.
public class CustomEvent extends ApplicationEvent {
private String message;
public CustomEvent(Object source, String message) {
super(source);
this.message = message;
}
}
Customํ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค.ApplicationEvent ์์๋ฐ์ ๊ตฌํํด์ผํ๋ ๊ฒ์ ์๋๋๋ค.@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishEvent() {
CustomEvent event = new CustomEvent(this, "์ด๋ฒคํธ ๋ฐ์!");
eventPublisher.publishEvent(event);
}
ApplicationEventPublisher ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ ์ ์์ต๋๋ค.publishEvent() ๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ์ด๋ฒคํธ๊ฐ์ฒด๋ฅผ ๋ด์ ์ด๋ฒคํธ๋ฅผ ๋ฐํํฉ๋๋ค.@EventListener
public void handleCustomEvent(CustomEvent event) {
// ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ก์ง
}
@EventListener ์ ๋
ธํ
์ด์
์ ์ด์ฉํด Event ๋ฅผ ์์ ํ ์ ์์ต๋๋ค.@Async ์ ๋
ธํ
์ด์
์ ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค.
Spring Event๋ฅผ ์ด์ฉํ๋ค๋ฉด ๊ฐ๋จํ๊ฒ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ์ฌ ๋น์ฆ๋์ค ๋ก์ง์์์ ์ฝ๋๋ฅผ ๋ถ๋ฆฌํด๋ผ ์ ์์ต๋๋ค. ๋ํ ํธ๋์ญ์ ์ ๋ถ๋ฆฌํ์ฌ ์๋ฆผ๊ธฐ๋ฅ์ด ์คํจํ๋๋ผ๋ ๋น์ฆ๋์ค๋ก์ง์ด ๋กค๋ฐฑ๋๋ ๊ฒฝ์ฐ๋ฅผ ๋ฐ์์ํค์ง ์๋๋ก ํ ์ ์์ต๋๋ค. ์ด์ Spring Event + SSE๋ฅผ ์ด์ฉํ์ฌ ๊ตฌํํ ์ฝ๋์ ๋ํด ์ค๋ช ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
๐งโ๐ป ์ ์ฒด ์ฝ๋
public record NotificationEvent<T extends Notification>(NotificationType type, T data) {
}
Event ๋ฅผ ํตํด ๋ณด๋ด์ค ๋ฐ์ดํฐ๋ฅผ ์ ํด์ค๋๋ค.Notification ์ ์์๋ฐ์ ์ํฐํฐ์ ํํด์ Event ๋ฅผ ๋ฐ์์ํค๋๋ก ํ์์ต๋๋ค.@Slf4j
@Service
@RequiredArgsConstructor
public class NotificationPublisherService {
private final ApplicationEventPublisher applicationEventPublisher;
private final ChatNotificationRepository chatNotificationRepository;
private final DonationNotificationRepository donationNotificationRepository;
private final ExchangeNotificationRepository exchangeNotificationRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createExchangeNotification(Long userId, String exchangeItemName, String requesterNickname, TradeStatus status) {
ExchangeNotification exchangeNotification = ExchangeNotification.builder()
.userId(userId)
.exchangeItemName(exchangeItemName)
.requestUserNickname(requesterNickname)
.tradeStatus(status)
.build();
ExchangeNotification savedNotification;
try {
savedNotification = exchangeNotificationRepository.save(exchangeNotification);
applicationEventPublisher.publishEvent(new NotificationEvent<>(NotificationType.EXCHANGE, savedNotification));
} catch (Exception e) {
log.warn("์๋ฆผ ๋ฐํ ์คํจ : {}", e.getMessage());
}
}
}
NotificationPublisherService ๋ ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ณ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋๋ฐ์ ๊ด์ฌ์ด ์๋ ์ปดํฌ๋ํธ์
๋๋ค.propagation์ ํตํด ํธ๋์ญ์
์ ๋ถ๋ฆฌํ์ต๋๋ค. ์ด ํ ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์๋ ๋ถ๋ถ์ ์๋ฌ๋ฅผ ์ก์์ฃผ์ง ์๋๋ค๋ฉด ํธ๋์ญ์
์ ๋ถ๋ฆฌํ๋ค๊ณ ํ๋๋ผ๋ ์๋ฆผ ๋ฐํ ์๋ฌ๊ฐ ๋น์ฆ๋์ค ๋ก์ง์ ๋กค๋ฐฑ์ํฌ ์ ์๊ธฐ๋๋ฌธ์ ๋ฐ์๋ ์ ์๋ ์๋ฌ๋ ์ก์์ฃผ์ด์ผ ํฉ๋๋ค.@Slf4j
@Component
@RequiredArgsConstructor
public class NotificationEventListener {
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
public SseEmitter subscribe(Long userId) {
SseEmitter emitter = new SseEmitter(Duration.ofHours(1).toMillis());
emitter.onCompletion(() -> emitters.remove(userId));
emitter.onTimeout(() -> emitters.remove(userId));
emitter.onError((ex) -> {
log.info("์๋ฆผ ๊ธฐ๋ฅ ์๋ฌ ๋ฐ์ User Id : {}", userId);
emitters.remove(userId);
});
try {
emitter.send(SseEmitter.event() // ์ด๊ธฐ ์ฐ๊ฒฐ ์ ๋๋ฏธ๋ฐ์ดํฐ ์ ์ก
.name("init")
.data("connect!!!!!!!"));
} catch (IOException e) {
log.info("์๋ ๊ตฌ๋
๊ธฐ๋ฅ ์๋ฌ ๋ฐ์ User Id : {}", userId);
throw new RuntimeException(e);
}
emitters.put(userId, emitter);
return emitter;
}
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendNotification(NotificationEvent<? extends Notification> event) {
SseEmitter emitter = emitters.get(event.data().getUserId());
if (emitter != null) {
try {
emitter.send(SseEmitter.event() // ์๋ฆผ ์ ์ก
.id(event.data().getUserId().toString())
.name(event.type().name())
.data(event.data())
.reconnectTime(Duration.ofHours(1).toMillis()) // ์ฌ์ฐ๊ฒฐ ์๊ฐ ์ค์
);
} catch (IOException e) {
log.info("์๋ฆผ ๋ฐ์ก ์คํจ User Id : {}", event.data().getUserId());
emitters.remove(event.data().getUserId());
}
}
}
}
NotificationEventListener ์ ๊ฐ์ง๊ณ ์๋ ์ญํ ์ ์ด๊ธฐ ์ฐ๊ฒฐ๊ณผ ์๋ฆผ์ ๋ฐํํ๋ ์ญํ ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.SSE์ emitter ๋ฅผ ์ด์ฉํ์ฌ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ตํฉ๋๋ค.@EventListener ๋ฅผ ํตํด ์ด๋ฒคํธ๋ฅผ ์์ ํ๊ณ ๋น๋๊ธฐ์ ์ผ๋ก ์๋ฆผ์ ์ ์กํ๊ฒ ๋ฉ๋๋ค.
Notification์ด๋ผ๋ ์ถ์ ํด๋์ค๋ฅผ ํตํด ์๋ฆผ ๊ธฐ๋ฅ์ ์ถ์ํํ์ฌ ์ฌ์ฉํ๊ฒ๋ฉ๋๋ค.NotificationPublisherService ๋ ์๋ฆผ์ ๋ฐํํ๋ ๊ด์ฌ๋ง ๊ฐ์ง๊ณ ์์ต๋๋ค.NotificationEventListener ๋ ์๋ฆผ์ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ตํ๋๋ฐ์ ๊ด์ฌ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.๐ ํบ์๋ณด๊ธฐ
Spring Event ๋ฅผ ํตํด ์๋ฆผ ๋ฐํ ๊ธฐ๋ฅ์ ๋น์ฆ๋์ค ๋ก์ง์์ ๋ถ๋ฆฌํ ์ ์์์ต๋๋ค.@TransactionalEventListener ๋ฅผ ํตํด ๋น์ฆ๋์ค๋ก์ง์ด ์ปค๋ฐ๋๊ณ ์คํ๋ ์ ์๋๋ก ํจ์ผ๋ก์จ ๋ฎ์ ๊ฒฐํฉ๋๋ฅผ ๊ฐ์ง๋ ์ฝ๋๋ฅผ ๊ตฌํํ ์ ์์์ต๋๋ค.SSE ๋ฅผ ํตํด ์๋ฆผ ๋ฐํ์ด ์๋๋๋ผ๋ ์กฐํ ์ ์นด์ดํ
/์ค์๊ฐ์ผ๋ก ๋ณํ๋ ํ์ด์ง๋ฅผ ๊ตฌํํ ์ ์๋ค๋ ๊ฒ์ ๊นจ๋ฌ์์ต๋๋ค.