Real-time APIs#
실시간 API의 개념과 중요성#
실시간 API란 무엇인가?#
실시간 API(Real-time API)는 클라이언트와 서버 간에 지속적인 연결을 유지하면서 데이터가 생성되거나 변경되는 즉시 자동으로 전송하는 API이다. 전통적인 RESTful API가 요청-응답 모델에 기반하여 클라이언트가 데이터를 명시적으로 요청해야 하는 것과 달리, 실시간 API는 새로운 정보가 발생하면 서버에서 클라이언트로 즉시 푸시(push)된다.
왜 실시간 API가 중요한가?#
실시간 API는 다음과 같은 이유로 현대 애플리케이션 개발에서 중요한 위치를 차지한다:
- 즉각적인 사용자 경험: 사용자는 최신 데이터를 즉시 볼 수 있어 더 나은 사용자 경험을 제공한다.
- 리소스 효율성: 주기적인 폴링(polling)에 비해 네트워크 트래픽과 서버 부하를 줄일 수 있다.
- 양방향 통신: 클라이언트와 서버 간의 양방향 데이터 흐름을 지원한다.
- 확장성: 최신 실시간 기술은 수많은 동시 연결을 효율적으로 처리할 수 있다.
실시간 API 기술 및 프로토콜#
실시간 API를 구현하기 위한 다양한 기술과 프로토콜이 있으며, 각각 고유한 특성과 장단점을 가지고 있다.
WebSocket#
WebSocket은 가장 널리 사용되는 실시간 통신 프로토콜로, HTTP를 통해 초기 핸드셰이크를 수행한 후 지속적인 양방향 통신 채널을 설정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 클라이언트 측 WebSocket 구현 예시
const socket = new WebSocket('wss://api.example.com/realtime');
// 연결 이벤트 처리
socket.addEventListener('open', (event) => {
console.log('WebSocket 연결이 열렸습니다');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
});
// 메시지 수신 처리
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('서버로부터 메시지 수신:', data);
updateUI(data); // UI 업데이트 함수
});
// 오류 처리
socket.addEventListener('error', (event) => {
console.error('WebSocket 오류 발생:', event);
});
// 연결 종료 처리
socket.addEventListener('close', (event) => {
console.log('WebSocket 연결이 닫혔습니다', event.code, event.reason);
// 재연결 로직 구현
setTimeout(connectWebSocket, 5000);
});
|
장점:
- 낮은 지연 시간과 오버헤드
- 양방향 전이중(full-duplex) 통신
- 대부분의 브라우저와 플랫폼에서 지원
단점:
- 복잡한 프록시와 로드 밸런서 설정 필요
- 연결 관리와 재연결 로직을 직접 구현해야 함
Server-Sent Events (SSE)#
SSE는 서버에서 클라이언트로의 단방향 통신을 위한 간단한 프로토콜로, 표준 HTTP 연결을 통해 서버에서 클라이언트로 데이터를 스트리밍한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 클라이언트 측 SSE 구현 예시
const eventSource = new EventSource('https://api.example.com/events');
// 메시지 수신 처리
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('이벤트 수신:', data);
updateUI(data);
});
// 특정 이벤트 타입 처리
eventSource.addEventListener('update', (event) => {
const updateData = JSON.parse(event.data);
console.log('업데이트 이벤트:', updateData);
handleUpdate(updateData);
});
// 오류 처리
eventSource.addEventListener('error', (event) => {
console.error('SSE 오류 발생:', event);
if (eventSource.readyState === EventSource.CLOSED) {
// 연결이 닫힌 경우 재연결 시도
setTimeout(() => {
new EventSource('https://api.example.com/events');
}, 5000);
}
});
|
장점:
- 구현이 간단하고 HTTP를 통해 동작
- 자동 재연결 메커니즘 내장
- 방화벽 친화적
단점:
- 단방향 통신만 지원 (서버 → 클라이언트)
- IE에서는 기본적으로 지원되지 않음
HTTP Long Polling#
Long Polling은 전통적인 폴링의 효율성을 개선한 기법으로, 클라이언트가 서버에 요청을 보내고 새로운 데이터가 있을 때까지 연결을 유지한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 클라이언트 측 Long Polling 구현 예시
function longPoll() {
fetch('https://api.example.com/poll', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Last-Event-ID': lastEventId // 마지막으로 수신한 이벤트 ID
},
credentials: 'include'
})
.then(response => response.json())
.then(data => {
// 데이터 처리
console.log('데이터 수신:', data);
if (data.events && data.events.length > 0) {
// 이벤트 처리
data.events.forEach(event => {
handleEvent(event);
lastEventId = event.id; // 마지막 이벤트 ID 업데이트
});
}
// 즉시 다음 Long Poll 요청 시작
longPoll();
})
.catch(error => {
console.error('Long Polling 오류:', error);
// 오류 발생 시 잠시 대기 후 재시도
setTimeout(longPoll, 5000);
});
}
// 초기 Long Polling 시작
longPoll();
|
장점:
- 거의 모든 브라우저와 환경에서 동작
- 기존 HTTP 인프라와 호환성이 좋음
단점:
- 잦은 연결 설정/해제로 인한 오버헤드
- 많은 동시 연결 시 서버 리소스 소모가 큼
GraphQL Subscriptions#
GraphQL은 쿼리 언어이지만, Subscriptions 기능을 통해 실시간 데이터 업데이트를 제공한다. 일반적으로 WebSocket 위에 구현된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| // Apollo Client를 사용한 GraphQL Subscription 예시
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
import { SubscriptionClient } from 'subscriptions-transport-ws';
// HTTP 링크 설정 (일반 쿼리 및 뮤테이션용)
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql'
});
// WebSocket 링크 설정 (구독용)
const wsLink = new WebSocketLink(
new SubscriptionClient('wss://api.example.com/graphql', {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem('token')
}
})
);
// 요청 유형에 따라 적절한 링크 선택
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
// Apollo 클라이언트 설정
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});
// 구독 실행
const MESSAGES_SUBSCRIPTION = gql`
subscription OnNewMessage {
messageAdded {
id
content
user {
id
name
}
}
}
`;
// 컴포넌트에서 구독 사용
function ChatComponent() {
const { data, loading, error } = useSubscription(MESSAGES_SUBSCRIPTION);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h3>New Message:</h3>
<p>{data.messageAdded.content}</p>
<p>From: {data.messageAdded.user.name}</p>
</div>
);
}
|
장점:
- 강력한 타입 시스템과 스키마 정의
- 클라이언트가 필요한 데이터만 구독 가능
- 기존 GraphQL 인프라와 통합 용이
단점:
gRPC와 Protocol Buffers#
gRPC는 구글이 개발한 고성능 RPC(원격 프로시저 호출) 프레임워크로, Protocol Buffers를 사용하여 데이터를 직렬화한다. 양방향 스트리밍을 지원하여 실시간 통신에 적합하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Protocol Buffers 정의 예시 (chat.proto)
syntax = "proto3";
package chat;
service ChatService {
// 양방향 스트리밍 RPC
rpc ChatStream (stream ChatMessage) returns (stream ChatMessage) {}
}
message ChatMessage {
string user_id = 1;
string content = 2;
int64 timestamp = 3;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| // Node.js에서 gRPC 서버 구현 예시
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('chat.proto');
const chatProto = grpc.loadPackageDefinition(packageDefinition).chat;
// 활성 스트림을 추적하는 배열
const activeStreams = [];
function chatStream(call) {
// 새 스트림을 활성 스트림 목록에 추가
activeStreams.push(call);
// 클라이언트로부터 메시지 수신 시 처리
call.on('data', (chatMessage) => {
console.log(`메시지 수신: ${chatMessage.content} from ${chatMessage.user_id}`);
// 모든 활성 스트림으로 메시지 브로드캐스트
const broadcastMessage = {
user_id: chatMessage.user_id,
content: chatMessage.content,
timestamp: Date.now()
};
activeStreams.forEach(stream => {
try {
stream.write(broadcastMessage);
} catch (error) {
console.error('스트림 쓰기 오류:', error);
}
});
});
// 스트림 종료 처리
call.on('end', () => {
// 종료된 스트림을 활성 목록에서 제거
const index = activeStreams.indexOf(call);
if (index !== -1) {
activeStreams.splice(index, 1);
}
call.end();
});
// 오류 처리
call.on('error', (error) => {
console.error('스트림 오류:', error);
const index = activeStreams.indexOf(call);
if (index !== -1) {
activeStreams.splice(index, 1);
}
});
}
// gRPC 서버 설정 및 시작
const server = new grpc.Server();
server.addService(chatProto.ChatService.service, {
chatStream: chatStream
});
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
(error, port) => {
if (error) {
console.error('서버 시작 실패:', error);
return;
}
console.log(`gRPC 서버가 ${port}에서 실행 중입니다`);
server.start();
}
);
|
장점:
- 매우 높은 성능과 낮은 지연 시간
- 강력한 타입 시스템과 계약 정의
- 다양한 언어 지원
단점:
- 브라우저에서 직접 사용 어려움 (변환 필요)
- 학습 곡선이 가파름
- 디버깅이 상대적으로 복잡함
실시간 API 설계 시 고려사항#
실시간 API를 설계할 때는 여러 중요한 측면을 고려해야 한다:
연결 관리#
실시간 시스템에서 연결 관리는 핵심 과제이다:
- 연결 유지: 연결이 끊어질 경우의 자동 재연결 메커니즘을 구현해야 한다.
- 하트비트(Heartbeat): 연결이 활성 상태인지 주기적으로 확인하는 메커니즘이 필요하다.
- 연결 상태 모니터링: 클라이언트와 서버 모두 연결 상태를 추적하고 문제를 감지해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
| // 클라이언트 측 WebSocket 연결 관리 예시
class RealtimeClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 1000,
maxReconnectAttempts: 5,
heartbeatInterval: 30000,
...options
};
this.socket = null;
this.reconnectAttempts = 0;
this.heartbeatTimer = null;
this.listeners = {
message: [],
open: [],
close: [],
error: []
};
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.addEventListener('open', (event) => {
console.log('연결 성공');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.notifyListeners('open', event);
});
this.socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
// 하트비트 응답 처리
if (data.type === 'heartbeat') {
return;
}
this.notifyListeners('message', data);
});
this.socket.addEventListener('close', (event) => {
this.stopHeartbeat();
this.notifyListeners('close', event);
// 비정상 종료인 경우 재연결 시도
if (!event.wasClean) {
this.scheduleReconnect();
}
});
this.socket.addEventListener('error', (event) => {
this.notifyListeners('error', event);
// 오류 발생 시 재연결 시도
this.scheduleReconnect();
});
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
console.error('최대 재연결 시도 횟수 초과');
return;
}
this.reconnectAttempts++;
const delay = this.options.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1);
console.log(`${delay}ms 후 재연결 시도 (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})`);
setTimeout(() => this.connect(), delay);
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ type: 'heartbeat' }));
}
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
on(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback);
}
return this;
}
notifyListeners(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
console.error('WebSocket이 연결되지 않았습니다');
}
}
close() {
this.stopHeartbeat();
if (this.socket) {
this.socket.close();
}
}
}
// 사용 예시
const client = new RealtimeClient('wss://api.example.com/realtime');
client.on('open', () => {
console.log('연결됨');
client.send({ type: 'subscribe', channel: 'orders' });
});
client.on('message', (data) => {
console.log('메시지 수신:', data);
});
client.on('close', () => {
console.log('연결 종료');
});
client.on('error', (error) => {
console.error('오류 발생:', error);
});
|
확장성 관리#
실시간 시스템은 많은 동시 연결을 처리해야 하므로 확장성을 고려한 설계가 필수적이다:
- 수평적 확장: 부하 분산을 위한 여러 서버에 걸친 확장 방법을 계획해야 한다.
- 메시지 브로커 사용: Redis, Kafka, RabbitMQ와 같은 메시지 브로커를 사용하여 서버 간 통신을 조정한다.
- 연결 샤딩: 지리적 위치나 사용자 ID 등을 기준으로 연결을 샤딩하여 효율적으로 분산한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
| // Node.js + Redis + Socket.IO를 사용한 확장 가능한 실시간 서버 예시
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const Redis = require('ioredis');
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
adapter: require('socket.io-redis')({
pubClient: new Redis(process.env.REDIS_URL),
subClient: new Redis(process.env.REDIS_URL)
})
});
// Redis 구독 클라이언트 설정
const redisSubscriber = new Redis(process.env.REDIS_URL);
redisSubscriber.subscribe('global-events');
redisSubscriber.on('message', (channel, message) => {
const event = JSON.parse(message);
io.to(event.room || 'all').emit(event.type, event.data);
});
// 소켓 연결 처리
io.on('connection', (socket) => {
console.log('새 클라이언트 연결:', socket.id);
// 사용자 인증 및 초기화
socket.on('authenticate', async (userData) => {
try {
// 사용자 인증 로직
const user = await authenticateUser(userData.token);
// 사용자 정보를 소켓에 저장
socket.data.user = user;
// 사용자별 룸에 조인
socket.join(`user:${user.id}`);
// 구독 채널에 조인
if (userData.subscriptions) {
userData.subscriptions.forEach(channel => {
socket.join(`channel:${channel}`);
});
}
socket.emit('authenticated', { success: true });
} catch (error) {
socket.emit('authenticated', {
success: false,
error: error.message
});
}
});
// 채널 구독
socket.on('subscribe', (channel) => {
socket.join(`channel:${channel}`);
socket.emit('subscribed', { channel });
});
// 채널 구독 해제
socket.on('unsubscribe', (channel) => {
socket.leave(`channel:${channel}`);
socket.emit('unsubscribed', { channel });
});
// 연결 종료 처리
socket.on('disconnect', () => {
console.log('클라이언트 연결 종료:', socket.id);
// 정리 로직
});
});
// 이벤트 발행 헬퍼 함수
function publishEvent(type, data, room = null) {
const redis = new Redis(process.env.REDIS_URL);
redis.publish('global-events', JSON.stringify({
type,
data,
room
}));
redis.quit();
}
// 외부 API에서 사용할 수 있는 이벤트 발행 엔드포인트
app.post('/api/events', express.json(), async (req, res) => {
try {
const { type, data, room } = req.body;
await publishEvent(type, data, room);
res.status(202).json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 서버 시작
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`실시간 서버가 포트 ${PORT}에서 실행 중입니다`);
});
// 사용자 인증 함수 (실제 구현은 별도로 필요)
async function authenticateUser(token) {
// 토큰 검증 및 사용자 정보 조회 로직
return { id: 'user-123', name: 'John Doe' };
}
|
데이터 일관성#
실시간 시스템에서 데이터 일관성을 유지하는 것은 중요한 과제이다:
- 이벤트 순서화: 이벤트가 발생한 순서대로 처리되도록 보장해야 한다.
- 충돌 해결: 동시 업데이트 충돌을 감지하고 해결하는 전략이 필요하다.
- 상태 동기화: 연결이 끊어졌다가 재연결될 때 상태를 동기화하는 메커니즘이 필요하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
| // 클라이언트 측 상태 동기화 구현 예시
class SynchronizedState {
constructor(initialState = {}) {
this.state = initialState;
this.version = 0;
this.pendingUpdates = [];
this.listeners = [];
}
// 상태 업데이트 (로컬)
update(patch) {
const updateObject = {
id: generateUUID(),
version: this.version + 1,
timestamp: Date.now(),
patch
};
// 로컬 상태 업데이트
this.applyUpdate(updateObject);
// 업데이트를 서버로 전송
this.pendingUpdates.push(updateObject);
this.sendPendingUpdates();
return updateObject.id;
}
// 업데이트 적용
applyUpdate(updateObject) {
if (updateObject.version > this.version) {
// 패치 적용
this.state = deepMerge(this.state, updateObject.patch);
this.version = updateObject.version;
// 리스너 알림
this.notifyListeners();
}
}
// 서버로부터 업데이트 수신
receiveUpdate(serverUpdate) {
// 로컬 업데이트와 서버 업데이트 간 충돌 확인
const conflictIndex = this.pendingUpdates.findIndex(
update => update.id === serverUpdate.id
);
if (conflictIndex !== -1) {
// 승인된 업데이트는 대기열에서 제거
this.pendingUpdates.splice(conflictIndex, 1);
} else {
// 서버 업데이트 적용
this.applyUpdate(serverUpdate);
}
}
// 서버와 재동기화
resynchronize(serverState, serverVersion) {
// 서버 상태가 더 최신인 경우
if (serverVersion > this.version) {
this.state = serverState;
this.version = serverVersion;
// 서버 상태보다 최신인 대기 중인 업데이트만 유지
this.pendingUpdates = this.pendingUpdates.filter(
update => update.version > serverVersion
);
// 대기 중인 업데이트 재적용
this.pendingUpdates.forEach(update => {
this.state = deepMerge(this.state, update.patch);
});
this.notifyListeners();
}
}
// 대기 중인 업데이트 전송 (예시 구현)
sendPendingUpdates() {
if (this.pendingUpdates.length === 0) return;
// WebSocket이 연결되어 있는지 확인
if (socket && socket.readyState === WebSocket.OPEN) {
this.pendingUpdates.forEach(update => {
socket.send(JSON.stringify({
type: 'state_update',
update
}));
});
}
}
// 변경 리스너 등록
onChange(callback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(cb => cb !== callback);
};
}
// 리스너 알림
notifyListeners() {
this.listeners.forEach(callback => callback(this.state));
}
}
// 헬퍼 함수: 깊은 병합
function deepMerge(target, source) {
const output = { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) {
output[key] = source[key];
} else {
output[key] = deepMerge(target[key], source[key]);
}
} else {
output[key] = source[key];
}
});
}
return output;
}
// 헬퍼 함수: 객체 확인
function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item)); }
/ UUID 생성 헬퍼 함수
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 사용 예시
const appState = new SynchronizedState({
todos: [],
settings: { theme: 'light' }
});
// 리스너 등록
appState.onChange(state => {
console.log('상태 변경:', state);
renderUI(state);
});
// 로컬 업데이트
function addTodo(text) {
appState.update({
todos: [...appState.state.todos, {
id: generateUUID(),
text,
completed: false
}]
});
}
|
보안 고려사항#
실시간 API는 특별한 보안 과제를 제기한다:
- 인증 및 권한 부여: 연결 시점과 메시지 송수신 시점 모두에서 적절한 인증이 필요하다.
- 메시지 검증: 모든 수신 메시지의 형식과 내용을 철저히 검증해야 한다.
- 속도 제한: 클라이언트당 메시지 속도를 제한하여 서비스 거부(DoS) 공격을 방지한다.
- 전송 중 암호화: WebSocket을 사용할 때는 반드시 WSS(WebSocket Secure)를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
| // Node.js에서 인증 및 권한 부여가 포함된 WebSocket 서버 예시
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const https = require('https');
const fs = require('fs');
// HTTPS 서버 생성
const server = https.createServer({
cert: fs.readFileSync('/path/to/cert.pem'),
key: fs.readFileSync('/path/to/key.pem')
});
// WebSocket 서버 생성
const wss = new WebSocket.Server({ server });
// JWT 비밀 키
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// 클라이언트별 메시지 속도 제한 추적
const rateLimits = new Map();
// 속도 제한 검사 함수
function checkRateLimit(clientId) {
const now = Date.now();
const clientRateInfo = rateLimits.get(clientId) || {
count: 0,
lastReset: now
};
// 1분마다 카운터 리셋
if (now - clientRateInfo.lastReset > 60000) {
clientRateInfo.count = 0;
clientRateInfo.lastReset = now;
}
// 분당 100개 메시지 제한
if (clientRateInfo.count >= 100) {
return false;
}
// 카운터 증가 및 저장
clientRateInfo.count++;
rateLimits.set(clientId, clientRateInfo);
return true;
}
// 권한 확인 함수
function checkPermission(user, action, resource) {
// 권한 확인 로직 구현
// 예: 특정 채널에 대한 메시지 전송 권한 확인
if (action === 'send' && resource.startsWith('channel:')) {
const channelId = resource.split(':')[1];
return user.channels.includes(channelId);
}
return false;
}
// 연결 이벤트 처리
wss.on('connection', (ws, req) => {
// 초기 상태 설정
ws.isAlive = true;
ws.user = null;
// 핑/퐁 하트비트 설정
ws.on('pong', () => {
ws.isAlive = true;
});
// 메시지 수신 처리
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
// 인증 메시지 처리
if (data.type === 'auth') {
// JWT 토큰 검증
try {
const decoded = jwt.verify(data.token, JWT_SECRET);
ws.user = decoded;
ws.clientId = decoded.id;
// 인증 성공 응답
ws.send(JSON.stringify({
type: 'auth_response',
success: true
}));
} catch (error) {
// 인증 실패 응답
ws.send(JSON.stringify({
type: 'auth_response',
success: false,
error: 'Invalid token'
}));
// 인증 실패 시 연결 종료
ws.terminate();
}
return;
}
// 인증되지 않은 메시지 거부
if (!ws.user) {
ws.send(JSON.stringify({
type: 'error',
code: 'unauthorized',
message: 'Authentication required'
}));
return;
}
// 속도 제한 확인
if (!checkRateLimit(ws.clientId)) {
ws.send(JSON.stringify({
type: 'error',
code: 'rate_limited',
message: 'Rate limit exceeded'
}));
return;
}
// 메시지 유형별 처리
switch (data.type) {
case 'subscribe':
// 채널 구독 권한 확인
if (checkPermission(ws.user, 'subscribe', `channel:${data.channel}`)) {
// 구독 처리 로직
ws.send(JSON.stringify({
type: 'subscribed',
channel: data.channel
}));
} else {
ws.send(JSON.stringify({
type: 'error',
code: 'permission_denied',
message: 'No permission to subscribe to this channel'
}));
}
break;
case 'message':
// 메시지 전송 권한 확인
if (checkPermission(ws.user, 'send', `channel:${data.channel}`)) {
// 메시지 검증 (예: 콘텐츠 길이, 금지된 콘텐츠 등)
if (!data.content || data.content.length > 1000) {
ws.send(JSON.stringify({
type: 'error',
code: 'invalid_message',
message: 'Message content invalid or too long'
}));
return;
}
// 메시지 처리 및 브로드캐스트 로직
// …
} else {
ws.send(JSON.stringify({
type: 'error',
code: 'permission_denied',
message: 'No permission to send to this channel'
}));
}
break;
default:
ws.send(JSON.stringify({
type: 'error',
code: 'unknown_message_type',
message: `Unknown message type: ${data.type}`
}));
}
} catch (error) {
// 메시지 처리 오류
ws.send(JSON.stringify({
type: 'error',
code: 'message_processing_error',
message: 'Failed to process message'
}));
console.error('메시지 처리 오류:', error);
}
});
// 연결 종료 처리
ws.on('close', () => {
// 정리 로직
if (ws.clientId) {
rateLimits.delete(ws.clientId);
}
});
});
// 비활성 연결 감지를 위한 간격 검사
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
// 서버 종료 시 간격 타이머 정리
wss.on('close', () => {
clearInterval(interval);
});
// 서버 시작
server.listen(8080, () => {
console.log('Secure WebSocket 서버가 포트 8080에서 실행 중입니다');
});
|
4. 실시간 API 사용 사례#
실시간 API는 다양한 애플리케이션 시나리오에서 활용된다:
채팅 및 메시징 애플리케이션
채팅은 실시간 API의 가장 대표적인 사용 사례이다:
- 즉각적인 메시지 전달: 사용자 간 즉시 메시지를 전송한다.
- 상태 알림: 사용자 상태(온라인, 오프라인, 입력 중 등)를 실시간으로 업데이트한다.
- 읽음 확인: 메시지 수신 및 읽음 상태를 실시간으로 표시한다.
협업 도구
문서 공동 작업, 화이트보드, 프로젝트 관리 등의 협업 도구는 실시간 기능을 통해 생산성을 높인다:
- 동시 편집: 여러 사용자가 동시에 같은 문서를 편집할 수 있다.
- 변경 사항 실시간 반영: 다른 사용자의 변경 사항이 즉시 모든 참여자에게 표시된다.
- 커서 위치 공유: 각 사용자의 현재 작업 위치를 실시간으로 표시한다.
금융 및 거래 플랫폼
금융 시장 데이터와 거래 정보는 실시간 전송이 중요하다:
- 시장 데이터 스트리밍: 주식, 암호화폐, 외환 등의 가격 정보를 실시간으로 업데이트한다.
- 거래 알림: 거래 실행, 주문 상태 변경 등을 즉시 알린다.
- 위험 감지: 이상 거래나 위험 상황을 실시간으로 감지하고 알린다.
온라인 게임
게임은 매우 낮은 지연 시간이 요구되는 실시간 통신의 대표적인 예:
- 위치 업데이트: 플레이어 위치와 동작을 실시간으로 동기화한다.
- 게임 상태 동기화: 게임 세계의 상태를 모든 플레이어에게 일관되게 유지한다.
- 실시간 상호작용: 플레이어 간 상호작용을 즉시 반영한다.
IoT(사물인터넷) 애플리케이션
IoT 기기는 센서 데이터를 실시간으로 전송하고 원격 제어를 위한 명령을 수신한다:
- 센서 데이터 스트리밍: 온도, 습도, 위치 등의 센서 데이터를 실시간으로 전송한다.
- 원격 제어: 기기 상태를 원격으로 제어하고 즉시 응답을 받는다.
- 알림 및 경고: 중요한 이벤트나 임계값 초과 시 즉시 알림을 보낸다.
실시간 API의 모범 사례#
실시간 API를 구현할 때 다음과 같은 모범 사례를 고려해야 한다:
프로토콜 선택
애플리케이션 요구사항에 맞는 적절한 프로토콜을 선택하는 것이 중요하다:
- 양방향 통신이 필요한 경우: WebSocket 또는 gRPC
- 서버에서 클라이언트로의 단방향 업데이트만 필요한 경우: SSE(Server-Sent Events)
- 브라우저 호환성이 중요한 경우: WebSocket 또는 Long Polling
- 높은 성능이 중요한 경우: gRPC 또는 최적화된 WebSocket
효율적인 메시지 형식
메시지 형식은 효율성과 유연성을 고려해야 한다:
- JSON: 가독성이 좋고 대부분의 환경에서 지원되지만, 크기가 상대적으로 크다.
- MessagePack: JSON보다 압축률이 높으며 이진 형식으로 효율적이다.
- Protocol Buffers: 매우 효율적인 이진 직렬화 형식으로 스키마 정의가 필요하다.
- CBOR: JSON과 유사하지만 더 효율적인 인코딩을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // JSON 형식의 메시지 예시
{
"type": "update",
"channel": "stocks",
"data": {
"symbol": "AAPL",
"price": 150.25,
"change": 2.75,
"timestamp": 1633456789
}
}
// JSON과 MessagePack 비교
// 위 JSON 메시지의 크기: 약 106 바이트
// MessagePack으로 인코딩 시 크기: 약 70 바이트
|
에러 처리 및 복원력
실시간 시스템에서 오류는 불가피하므로 이에 대비해야 한다:
- 명확한 오류 메시지: 클라이언트가 이해하고 대응할 수 있는 명확한 오류 코드와 메시지를 제공한다.
- 자동 재연결: 연결 끊김 시 자동으로 재연결하는 메커니즘을 구현한다.
- 상태 복구: 재연결 후 누락된 메시지나 상태를 복구하는 방법을 제공한다.
- 점진적 기능 감소: 일부 기능에 문제가 있어도 가능한 많은 기능을 유지한다.
문서화
실시간 API의 문서화는 특히 중요하다:
- 프로토콜 및 형식 설명: 사용된 프로토콜과 메시지 형식을 명확히 설명한다.
- 메시지 타입 및 필드: 모든 메시지 타입과 필드를 상세히 문서화한다.
- 이벤트 목록: 구독 가능한 모든 이벤트와 그 형식을 나열한다.
- 인증 및 권한: 인증 방법과 필요한 권한을 명확히 설명한다.
- 예제 코드: 다양한 언어와 플랫폼에 대한 예제 코드를 제공한다.
테스트 및 시뮬레이션
실시간 API는 테스트가 복잡할 수 있으므로 적절한 도구와 방법이 필요하다:
- 부하 테스트: 많은 동시 연결을 처리할 수 있는지 확인한다.
- 지연 시간 테스트: 메시지 전달 지연 시간을 측정한다.
- 네트워크 조건 시뮬레이션: 불안정한 네트워크 환경에서의 동작을 테스트한다.
- 장애 시뮬레이션: 서버 장애나 연결 끊김 시의 복구 동작을 확인한다.
용어 정리#
참고 및 출처#
Types of Real-time APIs Real-time API는 클라이언트와 서버 간의 데이터를 거의 즉각적으로 주고받을 수 있는 API로, 실시간 데이터 교환을 가능하게 한다. 이는 사용자 경험을 향상시키고, 데이터 정확성과 응답성을 높이는 데 중요한 역할을 한다.
Real-time API의 주요 유형 WebSocket API 특징: 단일 TCP 연결을 통해 양방향 통신을 지원. 클라이언트와 서버가 모두 데이터를 주고받을 수 있음. 낮은 지연 시간과 효율적인 데이터 전송 가능. 사용 사례: 채팅 애플리케이션, 온라인 게임, 협업 도구. Server-Sent Events (SSE) API 특징: HTTP 기반 단방향 통신(서버 → 클라이언트). 지속적인 연결 유지 및 자동 재연결 지원. 텍스트 기반 데이터 전송(UTF-8). 사용 사례: 실시간 알림, 뉴스 피드, 주식 가격 업데이트. Streaming API 특징: 서버에서 클라이언트로 지속적인 데이터 스트림 제공. 대규모 데이터 처리에 적합(예: 비디오, 오디오 스트리밍). WebSocket 또는 SSE를 기반으로 구현 가능. 사용 사례: 라이브 비디오 스트리밍, 소셜 미디어 피드, IoT 센서 데이터. Pub/Sub API 특징: Publish-Subscribe 패턴 기반. 발행자(Publisher)가 특정 주제(Topic)에 메시지를 게시하면 구독자(Subscriber)가 이를 수신. 데이터 생산자와 소비자를 분리하여 확장성과 효율성 제공. 사용 사례: 메시징 시스템(Kafka, PubNub), IoT 장치 간 통신. Push API 특징: 서버에서 클라이언트로 푸시 알림 전송. 클라이언트가 활성화되지 않아도 메시지 수신 가능. 모바일 애플리케이션에서 주로 사용됨. 사용 사례: 모바일 푸시 알림(Firebase Cloud Messaging), 이메일 알림. Event-Driven API 특징: 이벤트 중심 설계로 상태 변화나 특정 이벤트 발생 시 데이터를 전달. 이벤트 구독 및 처리에 최적화됨. 사용 사례: IoT 애플리케이션, 실시간 모니터링 시스템. Real-Time API 기술 비교 기본 특성 비교 특성 WebSocket SSE (Server-Sent Events) Streaming API Pub/Sub API Push API Event-Driven API 통신 방향 양방향(전이중) 단방향(서버→클라이언트) 단방향/양방향 가능 다방향(다대다) 단방향(서버→클라이언트) 이벤트 기반 프로토콜 WS/WSS HTTP/HTTPS HTTP/HTTPS 다양(MQTT, AMQP 등) HTTP/HTTPS 다양 연결 유지 지속 연결 지속 연결 지속 연결 지속/비지속 가능 비연결성 이벤트 발생 시 자동 재연결 수동 구현 필요 내장 지원 구현에 따라 다름 구현에 따라 다름 구현에 따라 다름 구현에 따라 다름 메시지 포맷 텍스트/바이너리 텍스트(UTF-8) 다양(JSON, XML 등) 다양 JSON 다양 데이터 크기 프레임 크기 제한 제한 없음 청크 단위 전송 일반적으로 작은 메시지 작은 메시지 이벤트 크기 기술적 특성 및 구현 비교 특성 WebSocket SSE (Server-Sent Events) Streaming API Pub/Sub API Push API Event-Driven API 연결 설정 HTTP 업그레이드 후 WS 프로토콜 일반 HTTP 연결 HTTP 연결 다양한 연결 방식 서비스 워커 등록 이벤트 리스너 등록 클라이언트 API WebSocket EventSource HTTP/Fetch 라이브러리별 다양 Push API, Service Worker 이벤트 리스너 서버 구현 WebSocket 서버 필요 일반 HTTP 서버 일반 HTTP 서버 메시지 브로커 서버 푸시 서비스 이벤트 처리 시스템 확장성 연결 유지 부담 상대적으로 가벼움 리소스 집약적 높은 확장성 높은 확장성 높은 확장성 헤더 오버헤드 낮음(최초 연결 후) 중간 중간 낮음 중간 구현에 따라 다름 통합 난이도 중간 쉬움 중간 중간~어려움 어려움 중간~어려움 활용 사례 및 지원 비교 특성 WebSocket SSE (Server-Sent Events) Streaming API Pub/Sub API Push API Event-Driven API 즉시성 매우 높음 높음 중간~높음 중간~높음 중간 중간~높음 브라우저 지원 대부분 지원 대부분 지원(IE 제외) 모두 지원 라이브러리 필요 대부분 지원 구현에 따라 다름 보안 고려사항 WSS 필수, 인증 필요 HTTPS 권장, 인증 필요 HTTPS 권장, 인증 필요 인증/권한 관리 중요 인증 키/토큰 관리 이벤트 검증 중요 리소스 사용량 중간~높음 낮음~중간 중간~높음 중간 낮음 중간 최적 사용 사례 채팅, 게임, 협업 도구 알림, 뉴스 피드, 실시간 데이터 대용량 데이터 전송 분산 메시징, IoT 알림, 백그라운드 메시지 마이크로서비스, 이벤트 기록 성능 및 구현 고려사항 특성 WebSocket SSE (Server-Sent Events) Streaming API Pub/Sub API Push API Event-Driven API 지연 시간 매우 낮음(~100ms) 낮음(~500ms) 중간(~1s) 중간 높음(몇 초~몇 분) 구현에 따라 다름 처리량 높음 중간 매우 높음 매우 높음 낮음 구현에 따라 다름 배터리 영향 중간~높음 낮음~중간 중간~높음 구현에 따라 다름 낮음(백그라운드) 구현에 따라 다름 방화벽 통과 일부 제한 가능 대부분 허용 대부분 허용 혼합 대부분 허용 구현에 따라 다름 저대역폭 환경 적합하지 않음 적합함 적합하지 않음 구현에 따라 다름 적합함 구현에 따라 다름 오프라인 지원 미지원 미지원 미지원 일부 지원 가능 지원(백그라운드) 일부 지원 가능 용어 정리 용어 설명 참고 및 출처