1. Event Share ์ด์ ์ฒ๋ฆฌ (๋์ ์ ์ ๊ณ ๋ ค)
์ด์
- ์ฌ๋ฌ๊ฐ์ ํญ์ ๋์๋จ์ ๋ Emitter Subscribe Response๋ ๋ค OK๊ณ Event๊ฐ ์ ๋๋ก ์์ฑ์์ง๋ง ๊ฐ์ฅ ์ต๊ทผ์ ์์ฑ๋ ํญ๋ง event data ์
ํ
์ดํธ๊ฐ ๋จ
์์ธ
- API์์ ๋์ผ ์ด๋ฒคํธ๊ฐ ์๋ ๋ค์ ์์ฑ๋ ๋์ผ ํค์ ์ด๋ฒคํธ๋ก ๋ฎ์ด ์ฐ๊ธฐ๊ฐ ๋์ด์ ธ๋ฒ๋ฆผ
- = API์์ concurrentHashMap์ emitter ๋ด๊ณ ์๋๋ฐ ๋์ผ key๋ก ์ ๊ฒ์ผ๋ก ์์ด์ณ์ ธ์
ํด๊ฒฐ ๋ฐฉ๋ฒ
- emitter๋ ๊ณต์ ์๋จ
- ์ด๋ฒคํธ ๊ตฌ๋
์ด ํ์ํ ๋๋ง๋ค ์ฌ๋ฌ๊ฐ emitter๋ฅผ uuid ์ถ๊ฐ ๋ฐ idPrefix์ ์ฉํด์ Broadcast ์ idPrefix ํ์ ๊ฒ๋ค ๋ชจ๋ ์
๋ฐ์ดํธ ๋๋๋ก ๋ณ๊ฒฝ
2. ์ธ์ฆ
์ด์
- event subscribe์ auth๋ฅผ ์ถ๊ฐํ ์ ์์
์์ธ
- ์ธ์ฆ์ ์ํด ํค๋์ ํ ํฐ์ ์ ๋ฌํด์ผํ๋๋ฐ ๊ธฐ๋ณธ EventSource์๋ header ์ถ๊ฐ๊ฐ ์๋์ด๋ฒ๋ฆผ
ํด๊ฒฐ ๋ฐฉ๋ฒ
a. EventSourcePolyfill ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉํด์ ํค๋์ Authorization ์ถ๊ฐ
- ์์ ์ฝ๋
let tokenKey = Profile.env==='PRODUCTION' ? VueCookies.get(GlobalConstants.ACCESS_TOKEN) : VueCookies.get(GlobalConstants.ACCESS_TOKEN_DEV);
const EventSource = EventSourcePolyfill || NativeEventSource;
this.eventSource = new EventSource(
`${UrlsConfig.menu1Url}/api/sse/subscribe/${this.currentCmpnId}/gridList`,
{
headers: {
Authorization: `Bearer ${tokenKey}`,
},
heartbeatTimeout: 12 * 60 * 1000,
}
);
- ์ด์
i) ๊ฒ์ฌ ์๋ฃ์ ๋ธ๋ก๋์บ์คํธ๋ ์ ์ ๋์(401 ์๋จ) ์ด์๊ฐ ๊ฐ๋ฐ์ ๋๊ตฌ์์ EventStream ์
๋ฐ์ดํธ ๋๋ ๋ฐ์ดํฐ๊ฐ ์๋ณด์(pending์ ์๋๋ฐ ์๋ธ )
ii) https://github.com/Yaffle/EventSource/issues/79
- -> ๋กค๋ฐฑ
b. ๊ตฌ๋
์ ์ธ์ฆ ์ ์ธ
- ์ด๋ค ๊ณ ๊ฐ์ด ๋ค๋ฅธ ํ์ฌ์ ๋ค๋ฅธ ๊ณ ๊ฐ์ ์์ด๋ ๋ชจ๋๋ฅผ ์์์ ์ด๋ฒคํธ๋ฅผ ํ์ณ๋ณธ๋ค๊ณ ํ์๋ ์ ์ถ์ด ๋ ๋งํ ์ ๋ณด
- ์ด๋ฒคํธ ์คํธ๋ฆผ ๋ฌด๋จ ๊ตฌ๋
์ ํตํด์ ๋ญ์ง ๋ชจ๋ฅผ ๊ฒ์ฌ๊ฐ ๋๋ฌ๋ค๋ ์ฌ์ค
- companyID, dataID
- ํฌ๊ฒ ์๋ฏธ์๋ ๋ฐ์ดํฐ X โ ๊ฒฐ๋ก : Subscribe์์๋ ์ธ์ฆ ์ ์ธํ๋๋ก ๊ตฌํ
3. ํ๋ก์ ํตํ API Call๋ก์ ๋ณ๊ฒฝ
API GW ์ค์ ์ด ์๋์ด์๊ธฐ ๋๋ฌธ์ ํ๋ก์ ํธ ํ๋ก์ ํตํด์ ClientUrl โ BackendUrl๋ก ๋ณ๊ฒฝํด์ API ์ฝ ํด์ผํจ
์ด์
- ํ๋ก์ ํตํด์ clientURL๋ก subscribe ์งํ์ header connection reponse close error โ pending
ํด๊ฒฐ ๋ฐฉ๋ฒ
- webpack โ compress: false ์ต์
๋ณ๊ฒฝ
4. http 1.1 ๋ธ๋ผ์ฐ์ ์ ํ ์ด์
์ด์
- http1.1 ์ฌ์ฉ ์ ๋ธ๋ผ์ฐ์ ๋น 6๊ฐ eventSource๋ฅผ ์์ฑ ๊ฐ๋ฅ
- ์ด๋ฏธ emitter์ ๋ค์ด์๋ ์ด๋ฒคํธ ๊ฐ์ฒด๊ฐ 6๊ฐ๊ฐ ์ด๊ณผํ๋ค๊ณ ํ๋๋ผ๋ ๋ธ๋ผ์ฐ์ ๊ฐ 6๊ฐ ๋ฏธ๋ง์ด๋ฉด ์๊ด์์
ํด๊ฒฐ ๋ฐฉ๋ฒ
- ์น ์๋ฒ(์: Apache, Nginx)์์๋ HTTP/2.0๋ฅผ ์ง์, ์ด๋ฅผ ์ด์ฉํ์ฌ ํน์ ๊ฒฝ๋ก ๋๋ URL์ ๋ํด์๋ง HTTP/2.0์ ํ์ฑํํ๋ ๋ฐฉ๋ฒ
- id์ miliseconds๋ฅผ ์ถ๊ฐํ์ฌ maximum์ด ๋๋ ๊ฒฝ์ฐ ์ต์ด ๊ฒ๋ถํฐ ์ญ์ ํ๋๋ก ๋ณ๊ฒฝ
- user๊ฐ ์ถ๊ฐ๋์ด์ผํด์ URL์ด ๊ธธ์ด์ง
- โ ๋ณต์กํ๊ธฐ๋ ํ๊ณ ๋ค๋ฅธ ์ ์ ๊ฐ ์๋ ํ ๊ฐ์ธ์ด ์ฌ๋ฌ ๋ธ๋ผ์ฐ์ ๋ฅผ ๋์ฐ๋๊ฑฐ๋ผ ํฐ ์ด์๋ก ๋ณด์ฌ์ง์ง์์์ ์ฐ์ ์ Timeout์ ์ ์ ํ๊ฒ ์ค์ ๋น ๋ฅธ ํด์ง ๋น ๋ฅธ reconnect(์ ํญ์์ ๋จผ์ ๊ฐ์ ธ๊ฐ ์ ์๋๋ก)
5. SEEmitter ๊ณตํตํ
์ด์
- Event list, detail ๋๋ ๋์ โ ๋ถ๊ธฐ๊ฐ ๋ง์ด ์๊น
- detail ๊ฒ์ฌ ์งํ ์ ๊ฒ์ฌ์๋ง๋ค ์ด๋ฒคํธ๊ฐ ์๋ก ์์ฑ๋จ
ํด๊ฒฐ ๋ฐฉ๋ฒ
- vue store ์ฌ์ฉํ์ฌ ํ๋์ ์ ์ ๊ฐ Menu1 ๋ฉ๋ด์์ emitter๋ ํ๋๋ก ๊ณ์ ์ฌ์ฉํ๋๋ก ์ฒ๋ฆฌ
6. net::ERR_HTTP2_PROTOCOL_ERROR 200 (OK) ๋ฐ ์ฌ๋ฌ Nginx ์ด์
์ด์
- subscribe ์ pending ์ด์
- nginx ์ฌ์ฉ ๊ฐ๋ฐ ์ ์ฉ ์ 1๋ถ ์ฃผ๊ธฐ๋ก client์์ sse ์ ์ ์ฌ์๋ ๋ฐ Error ๋ฐ์ ์ด์
ํด๊ฒฐ ๋ฐฉ๋ฒ
- ๋ณ๋ ๋ฆฌํธ๋ผ์ด ์์ด๋ client์ชฝ์์๋ ์ฐ๊ฒฐ์ด ๋์ด์ง๋ฉด sse ์๋์ผ๋ก ํธ์ถํ๋ ๊ฒ์ผ๋ก ๋ณด์ (=api๋ก์ง์ retry ์ ๊ฑฐ ๋ฐ client proxy_read_timeout 360; ์ถ๊ฐ ์ ๊ณต)
- api ์์ ํ์์์์ client๋ณด๋ค ๋จผ์ ๋ฐ์์์ผ์ client๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์ฌ์๋ํ๋๋ก ์ค์ (api timeout 3m)
- SSE๊ด๋ จ Nginx ์ค์ ๋ค
location /api/sse/ {
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_pass http://${api}:{port}$request_uri;
proxy_read_timeout 360;
charset utf-8;
}
7. ๋ฉ๋ชจ๋ฆฌ ์ด์ ๋ฐฉ์ง (client, server lifecycle ๊ณ ๋ ค)
์ด์
- ์ด๋ฒคํธ ๋ฐ์ ํ์ด์ง๋ฅผ ๋ฒ์ด๋ ๋ค๋ฅธ ๋ฉ๋ด๋ก ๊ฐ๋ ๊ฒฝ์ฐ ์ด๋ฒคํธ๊ฐ ์ ์ง๋์ด ์ฅ๊ธฐ์ ์ผ๋ก ๋ณด๋ฉด ๋ฉ๋ชจ๋ฆฌ ์ด์ ๋ฐ์ ๊ฐ๋ฅ์ฑ์ด ์์
- client life cycle๊ณผ server lifecycle์ ๋ง์ถ์ง ์์ผ๋ฉด ์ด๋์ ๊ฐ ์ด๋ฒคํธ ํด์ง๋์ง ์์ ๋์ ๋ฐ์ ๊ฐ๋ฅ์ฑ
ํด๊ฒฐ ๋ฐฉ๋ฒ
- ๊ธฐ๋ณธ์ ์ผ๋ก client์ server ๋ ์ฌ์ดํด์ ๋ง์ถ๊ธด ์ด๋ ค์ โ ์ ์ ํ timeout ์ฃผ๊ธฐ + ๋ฉ๋ด ๋ฒ์ด๋๋ ๊ฒฝ์ฐ client, server์ ์ด๋ฒคํธ ๋ช
์์ ํด์ง
- API onComplete ๋ถ๋ถ ์์
a. ์ด์คํ ์๋ฒ๋ณ remove๋ก ์์
์งํ โ remove๊ฐ ์๋ complete๋ก ๋ณ๊ฒฝ
b. ์๋ฒ์ชฝ์์ sse ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋๋ฉด sse emitters์์ ํด๋น emitter ์ญ์ ํ๋๊ฒ ์๋๊ณ complete ์ณ์ ํ๋ก์ธ์ค๋ฅผ ํ์์ผ ์ ์์ ์ผ๋ก ์ข
๋ฃ
c. ์ง์ ์ญ์ ๋ onComplete ๋ด๋ถ์์๋ง ํ๋ฉด ๋จ
- sse client ์ด๋ฒคํธ ์ด๊ธฐํ ๋ฐ unsubscribe ๊ตฌํ (mq)
- Flow
- ์ด๋ฒคํธ ์์ฑ ์ธ ํ์ด์ง์์๋ ํ์ด์ง ์ง์
์ store์ ๋ด๊ธด event ์ด๊ธฐํ / ์ด๋ฒคํธ ์์ฑ ํ์ด์ง๋ event ์ด๊ธฐํ ๋ฐ ์ ๊ท ์์ฑ
- client์์ event close๋ฐ store ์ด๊ธฐํ ์งํํ๋ฉด์ api์ ํด์ง ์์ฒญ์ ๋ ๋ฆผ
- unsubscribe api์์ complete ์ํ
- Client
- ์์ ์ฝ๋
//list.vue
...
initSse() {
let payload = {
cmpnId : this.currentCmpnId,
menuName : 'menu1'
}
if (!this.sseEventSource || (this.currentCmpnId !== this.getCmpnIdInLastEventId(this.lastEventId))) {
this.$store.dispatch('sse/completeSseEventSource'); //ํด์ง
this.$store.dispatch('sse/createSseEventSource', payload); //์์ฑ
}
},
...
//actions.vue
import axios from 'axios';
...
const completeSseEventSource = (context) => {
if (!context.state.sseEventSource) {
return;
}
context.state.sseEventSource.close();
context.commit('setSseEventSource', null);
let url = `/api/sse/unsubscribe/${context.state.lastEventId}`;
try {
axios.delete(url);
} catch (err) {
console.error("SSE COMPLETE ERROR : ", err);
} finally {
context.commit('setLastEventId', '');
}
}
...
- API
- ์์ ์ฝ๋
//Controller
@DeleteMapping(value = "/api/sse/unsubscribe/{eventId}")
public ResultModel removeDestroyedEmitter (@PathVariable("eventId") String eventId) {
return sseService.sseUnsubscribe(eventId);
}
//Service
//์ด์คํ๋ก ์ด๋ ์๋ฒ์ ํด๋น ์ด๋ฒคํธ๊ฐ ์์์ง ๋ชจ๋ฅด๋ MQ๋ก ์ฒ๋ฆฌ
public ResultModel sseUnsubscribe(String eventId) {
ResultModel result = new ResultModel();
try {
rabbitTemplate.convertAndSend(RabbitMqConfig.EXCHANGE_SSE_UNSUBSCRIBE_BROADCAST, "", eventId);
} catch (Exception e) {
result.setError("500", e.getMessage());
result.setStatus("fail");
}
return result;
}
//RabbitMqConfig
public static final String EXCHANGE_SSE_UNSUBSCRIBE_BROADCAST = "fanout.sse.unsubscribe.broadcast";
public static final String QUEUE_SSE_UNSUBSCRIBE_BROADCAST_PROCESS = "sse.unsubscribe.broadcast.process";
@Bean
public FanoutExchange sseBroadcastUnsubscribeFanoutExchange() {
return new FanoutExchange(EXCHANGE_SSE_UNSUBSCRIBE_BROADCAST);
}
@Bean
public Queue getSseUnsubscribeQueue() {
return new Queue(QUEUE_SSE_UNSUBSCRIBE_BROADCAST_PROCESS);
}
@Bean
public Binding bindingSseUnsubscribeExchangeAndQueue(Queue getSseUnsubscribeQueue, FanoutExchange sseBroadcastUnsubscribeFanoutExchange) {
return BindingBuilder.bind(getSseUnsubscribeQueue).to(sseBroadcastUnsubscribeFanoutExchange);
}
//Consumer
@RabbitListener(queues = RabbitMqConfig.QUEUE_SSE_UNSUBSCRIBE_BROADCAST_PROCESS, concurrency = "${consumer.sse-unsubscribe-broadcast}")
public void sseUnsubscribeReceiveMessage(String eventId) {
sseService.broadcastToUnsubscribe(eventId);
}
//Service
public void broadcastToUnsubscribe(String eventId) {
try {
SseEmitter emitter = emitters.get(eventId); //subscribe์ map์ ๋ฃ์ด๋จ๋ emitter
if (!ObjectUtils.isEmpty(emitter)) {
emitter.complete();
}
} catch (Exception e) {
log.error("[Sse unsubscribe Error] cause : {}, eventId : {}", e.getMessage(), eventId);
}
}
- ๋ค๋ฅธ ์๋น์ค๋ก ๊ฐ๋์๋ ํ์์์ ์ดํ ์๋์ผ๋ก ์ด๋ฒคํธ ํด์ง ๋จ (์ฌ์ฐ๊ฒฐ์ํจ)
'๊ฐ๋ฐ์ํ > ์ด๊ฒ์ ๊ฒ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
์ด๋ฏธ ์์ฑ๋์ด ์๋ ๋์ปค ์ปจํ ์ด๋(mysql)์ ์ database ์ถ๊ฐ (0) | 2024.10.26 |
---|---|
SSE ์ ์ฉ (2) - ์ด์คํ๋ฅผ ์ ์ฉํด๋ณด์ (0) | 2024.08.19 |
H2 ๋ฐ์ดํฐ ๋ฒ ์ด์ค ์ค์น ๋ฐ ์ด๊ธฐ ์ค์ ๋ฐฉ๋ฒ (0) | 2024.04.15 |
SSE ์ ์ฉ (1) - SSE๋ฅผ ์ ์ฉํด๋ณด์ ๊ฐ๋ (0) | 2024.04.12 |
ํ๋ก์ ํธ Tree ๊ตฌ์กฐ ์ถ๊ฐ (0) | 2023.10.15 |