Skip to content

Latest commit

 

History

History
982 lines (790 loc) · 34.3 KB

File metadata and controls

982 lines (790 loc) · 34.3 KB

Chapter 02 웹 소켓 기본 구현

02-2 메시지 송수신

개요

웹 소켓의 핵심 기능은 클라이언트와 서버 간의 실시간 메시지 교환입니다. 이 섹션에서는 텍스트 메시지와 바이너리 데이터를 전송하는 방법, 메시지를 수신하고 처리하는 방법, 그리고 효율적인 메시지 형식 및 직렬화 기법에 대해 알아봅니다. 이를 통해 웹 소켓을 활용한 양방향 통신 애플리케이션을 효과적으로 구현할 수 있습니다.

텍스트 메시지 전송

웹 소켓은 텍스트 기반 메시지를 쉽게 전송할 수 있는 방법을 제공합니다. 텍스트 메시지는 일반적으로 문자열 형태로 전송되며, JSON과 같은 형식을 사용하여 구조화된 데이터를 교환하는 데 적합합니다.

클라이언트 측 텍스트 메시지 전송

클라이언트에서 서버로 텍스트 메시지를 전송하는 가장 기본적인 방법은 WebSocket 객체의 send() 메서드를 사용하는 것입니다:

const socket: WebSocket = new WebSocket('wss://example.com/chat');

// 연결이 열린 후 메시지 전송
socket.addEventListener('open', () => {
  // 단순 문자열 전송
  socket.send('안녕하세요!');
  
  // JSON 형식의 구조화된 데이터 전송
  const message = {
    type: 'chat',
    content: '안녕하세요!',
    timestamp: new Date().toISOString()
  };
  socket.send(JSON.stringify(message));
});

메시지를 전송하기 전에 연결 상태를 확인하는 것이 좋습니다:

function sendMessage(socket: WebSocket, message: string | object): boolean {
  if (socket.readyState !== WebSocket.OPEN) {
    console.error('웹 소켓이 열려있지 않습니다.');
    return false;
  }
  
  try {
    // 객체인 경우 JSON 문자열로 변환
    const data = typeof message === 'object' ? JSON.stringify(message) : message;
    socket.send(data);
    return true;
  } catch (error) {
    console.error('메시지 전송 중 오류 발생:', error);
    return false;
  }
}

서버 측 텍스트 메시지 전송 (Java)

Spring WebSocket을 사용한 예시:

public class ChatWebSocketHandler extends TextWebSocketHandler {

    private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws IOException {
        String sessionId = session.getId();
        sessions.put(sessionId, session);

        // 단일 클라이언트에 메시지 전송
        session.sendMessage(new TextMessage("서버에 연결되었습니다!"));
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        // 모든 클라이언트에 메시지 브로드캐스트
        sessions.values().stream()
            .filter(WebSocketSession::isOpen)
            .forEach(client -> {
                try {
                    client.sendMessage(message);
                } catch (IOException e) {
                    // 로그 기록 및 클라이언트 연결 상태 관리
                    logger.error("메시지 전송 실패: " + e.getMessage(), e);
                    // 연결 문제가 있는 클라이언트 세션 관리
                    handleFailedMessageDelivery(client, e);
                }
            });
    }
}
sequenceDiagram
    participant Client as 클라이언트
    participant Server as 서버
    
    Client->>Server: 연결 요청
    Server->>Client: 연결 수락
    
    Client->>Server: 텍스트 메시지 전송
    Note over Server: 메시지 처리
    Server->>Client: 응답 메시지 전송
    
    Client->>Server: JSON 형식 메시지 전송
    Note over Server: JSON 파싱 및 처리
    Server->>Client: JSON 응답 전송
    
    Note over Client,Server: 양방향 통신 계속...
Loading

바이너리 데이터 전송

텍스트 메시지 외에도 웹 소켓은 바이너리 데이터 전송을 지원합니다. 이는 이미지, 오디오, 비디오 또는 기타 바이너리 형식의 데이터를 효율적으로 전송할 때 유용합니다.

클라이언트 측 바이너리 데이터 전송

클라이언트에서 바이너리 데이터를 전송하려면 ArrayBuffer, Blob 또는 TypedArray 객체를 send() 메서드에 전달합니다:

const socket: WebSocket = new WebSocket('wss://example.com/binary');

socket.addEventListener('open', () => {
  // ArrayBuffer 전송
  const buffer = new ArrayBuffer(4);
  const view = new Uint32Array(buffer);
  view[0] = 42;
  socket.send(buffer);
  
  // Blob 전송 (예: 파일 업로드)
  const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
  if (fileInput.files && fileInput.files.length > 0) {
    const file = fileInput.files[0];
    socket.send(file); // Blob 객체 직접 전송
  }
  
  // TypedArray 전송
  const uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
  socket.send(uint8Array);
});

서버 측 바이너리 데이터 처리 (Java)

Spring WebSocket을 사용한 바이너리 메시지 처리:

public class BinaryWebSocketHandler extends BinaryWebSocketHandler {

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws IOException {
        ByteBuffer buffer = message.getPayload();
        System.out.println("바이너리 메시지 수신: " + buffer.capacity() + " 바이트");

        // 바이너리 데이터 처리
        // ...

        // 바이너리 응답 전송
        ByteBuffer response = ByteBuffer.wrap(new byte[]{1, 2, 3, 4});
        session.sendMessage(new BinaryMessage(response));
    }
}

파일 전송 예시

웹 소켓을 통한 파일 전송 구현 예시:

// 클라이언트 측 코드
const socket: WebSocket = new WebSocket('wss://example.com/file-transfer');
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const sendButton = document.getElementById('sendButton') as HTMLButtonElement;

sendButton.addEventListener('click', () => {
  if (fileInput.files && fileInput.files.length > 0) {
    const file = fileInput.files[0];
    
    // 파일 정보 먼저 전송
    const fileInfo = {
      type: 'file-info',
      name: file.name,
      size: file.size,
      mimeType: file.type
    };
    socket.send(JSON.stringify(fileInfo));
    
    // 파일 데이터 전송
    const reader = new FileReader();
    reader.onload = (e) => {
      if (e.target && e.target.result) {
        // 파일 내용 전송
        socket.send(e.target.result);
        console.log('파일 전송 완료');
      }
    };
    reader.readAsArrayBuffer(file);
  }
});
// 서버 측 코드 (Spring WebSocket)
public class FileTransferHandler extends BinaryWebSocketHandler {

    private Map<String, FileTransferSession> sessions = new ConcurrentHashMap<>();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // JSON 메시지 파싱
        ObjectMapper mapper = new ObjectMapper();
        JsonNode json = mapper.readTree(message.getPayload());

        if (!"file-info".equals(json.path("type").asText())) {
            return;
        }

        // 파일 정보 저장
        String sessionId = session.getId();
        String fileName = json.path("name").asText();
        long fileSize = json.path("size").asLong();
        String mimeType = json.path("mimeType").asText();

        // 파일 전송 세션 생성
        FileTransferSession transferSession = new FileTransferSession(fileName, fileSize, mimeType);
        sessions.put(sessionId, transferSession);

        System.out.println("파일 정보 수신: " + fileName + " (" + fileSize + " 바이트)");
    }

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
        String sessionId = session.getId();
        FileTransferSession transferSession = sessions.get(sessionId);

        if (transferSession == null) {
            return;
        }

        // 바이너리 데이터를 파일 스트림에 추가
        ByteBuffer buffer = message.getPayload();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        transferSession.appendData(bytes);

        // 파일 수신 완료 확인
        if (!transferSession.isComplete()) {
            return;
        }

        System.out.println("파일 수신 완료: " + transferSession.getFileName());

        // 파일 저장 또는 처리
        saveFile(transferSession.getFileName(), transferSession.getData());

        // 응답 전송
        session.sendMessage(new TextMessage("{\"status\":\"success\",\"message\":\"파일 수신 완료\"}"));

        // 리소스 정리
        sessions.remove(sessionId);
    }

    private void saveFile(String fileName, byte[] data) throws IOException {
        Path path = Paths.get("uploads", fileName);
        Files.createDirectories(path.getParent());
        Files.write(path, data);
    }

    // 파일 전송 세션 클래스
    private static class FileTransferSession {
        private String fileName;
        private long fileSize;
        private String mimeType;
        private ByteArrayOutputStream fileData;
        
        public FileTransferSession(String fileName, long fileSize, String mimeType) {
            this.fileName = fileName;
            this.fileSize = fileSize;
            this.mimeType = mimeType;
            this.fileData = new ByteArrayOutputStream();
        }
        
        public void appendData(byte[] data) throws IOException {
            fileData.write(data);
        }
        
        public boolean isComplete() {
            return fileData.size() >= fileSize;
        }
        
        public String getFileName() {
            return fileName;
        }
        
        public byte[] getData() {
            return fileData.toByteArray();
        }
    }
}

메시지 수신 처리

웹 소켓 메시지를 수신하고 처리하는 방법은 클라이언트와 서버 측에서 약간 다릅니다.

클라이언트 측 메시지 수신

클라이언트에서는 message 이벤트를 통해 메시지를 수신합니다:

const socket: WebSocket = new WebSocket('wss://example.com/chat');

socket.addEventListener('message', (event: MessageEvent) => {
  // 텍스트 메시지 처리
  if (typeof event.data === 'string') {
    console.log('텍스트 메시지 수신:', event.data);
    
    try {
      // JSON 메시지 파싱 시도
      const jsonData = JSON.parse(event.data);
      handleJsonMessage(jsonData);
    } catch (error) {
      // 일반 텍스트 메시지 처리
      handleTextMessage(event.data);
    }
  } 
  // 바이너리 메시지 처리
  else if (event.data instanceof ArrayBuffer) {
    console.log('ArrayBuffer 수신:', event.data);
    handleArrayBuffer(event.data);
  } 
  else if (event.data instanceof Blob) {
    console.log('Blob 수신:', event.data);
    handleBlob(event.data);
  }
});

function handleJsonMessage(data: any): void {
  // 메시지 유형에 따른 처리
  switch (data.type) {
    case 'chat':
      displayChatMessage(data.sender, data.content);
      break;
    case 'notification':
      showNotification(data.content);
      break;
    case 'status':
      updateStatus(data.status);
      break;
    default:
      console.warn('알 수 없는 메시지 유형:', data.type);
  }
}

function handleTextMessage(text: string): void {
  // 일반 텍스트 메시지 처리
  displayChatMessage('시스템', text);
}

function handleArrayBuffer(buffer: ArrayBuffer): void {
  // ArrayBuffer 처리 예시
  const view = new Uint8Array(buffer);
  console.log('첫 번째 바이트:', view[0]);
}

function handleBlob(blob: Blob): void {
  // Blob 처리 예시 (예: 이미지 표시)
  if (blob.type.startsWith('image/')) {
    const url = URL.createObjectURL(blob);
    displayImage(url);
  }
}

function displayChatMessage(sender: string, content: string): void {
  // DOM에 채팅 메시지 추가
  const chatBox = document.getElementById('chatBox');
  if (chatBox) {
    const messageElement = document.createElement('div');
    messageElement.innerHTML = `<strong>${sender}:</strong> ${content}`;
    chatBox.appendChild(messageElement);
    chatBox.scrollTop = chatBox.scrollHeight;
  }
}

function showNotification(content: string): void {
  // 알림 표시
  alert(content);
}

function updateStatus(status: string): void {
  // 상태 업데이트
  const statusElement = document.getElementById('status');
  if (statusElement) {
    statusElement.textContent = status;
  }
}

function displayImage(url: string): void {
  // 이미지 표시
  const imageContainer = document.getElementById('imageContainer');
  if (imageContainer) {
    const img = document.createElement('img');
    img.src = url;
    img.style.maxWidth = '100%';
    imageContainer.appendChild(img);
  }
}

서버 측 메시지 수신 (Java)

서버 측에서는 메서드 오버라이딩을 통해 메시지 수신을 처리합니다:

Spring WebSocket 사용:

public class ChatWebSocketHandler extends TextWebSocketHandler {

    private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        System.out.println("텍스트 메시지 수신: " + payload);
        
        try {
            // JSON 메시지 파싱 시도
            JsonNode jsonNode = objectMapper.readTree(payload);
            handleJsonMessage(jsonNode, session);
        } catch (Exception e) {
            // 일반 텍스트 메시지 처리
            handleTextMessage(payload, session);
        }
    }

    private void handleJsonMessage(JsonNode json, WebSocketSession session) throws IOException {
        String type = json.path("type").asText("unknown");
        
        switch (type) {
            case "chat":
                // 모든 클라이언트에 메시지 브로드캐스트
                TextMessage chatMessage = new TextMessage(objectMapper.writeValueAsString(json));
                sessions.values().stream()
                    .filter(WebSocketSession::isOpen)
                    .forEach(client -> {
                        try {
                            client.sendMessage(chatMessage);
                        } catch (IOException e) {
                            // 로그 기록 및 클라이언트 연결 상태 관리
                            logger.error("채팅 메시지 전송 실패: " + e.getMessage(), e);
                            // 연결 문제가 있는 클라이언트 세션 관리
                            handleFailedMessageDelivery(client, e);
                        }
                    });
                break;

            case "private":
                String recipient = json.path("recipient").asText("");
                TextMessage privateMessage = new TextMessage(objectMapper.writeValueAsString(json));

                // 특정 사용자에게만 메시지 전송
                sessions.values().stream()
                    .filter(WebSocketSession::isOpen)
                    .filter(client -> getUserId(client).equals(recipient))
                    .findFirst()
                    .ifPresent(client -> {
                        try {
                            client.sendMessage(privateMessage);
                        } catch (IOException e) {
                            // 로그 기록 및 클라이언트 연결 상태 관리
                            logger.error("개인 메시지 전송 실패: " + e.getMessage(), e);
                            // 발신자에게 전송 실패 알림
                            notifySenderOfFailedDelivery(session, recipient, e);
                        }
                    });
                break;

            default:
                System.out.println("알 수 없는 메시지 유형: " + type);
        }
    }

    private void handleTextMessage(String text, WebSocketSession session) throws IOException {
        // 일반 텍스트 메시지 처리
        TextMessage message = new TextMessage(text);
        sessions.values().stream()
            .filter(WebSocketSession::isOpen)
            .forEach(client -> {
                try {
                    client.sendMessage(message);
                } catch (IOException e) {
                    // 로그 기록 및 클라이언트 연결 상태 관리
                    logger.error("일반 텍스트 메시지 전송 실패: " + e.getMessage(), e);
                    // 연결 문제가 있는 클라이언트 세션 관리
                    handleFailedMessageDelivery(client, e);
                }
            });
    }

    private String getUserId(WebSocketSession session) {
        // 세션에서 사용자 ID 추출 (구현에 따라 다름)
        return session.getAttributes().get("userId").toString();
    }
}
graph TD
    A[메시지 수신] --> B{메시지 유형?}
    B -->|텍스트| C[텍스트 메시지 처리]
    B -->|바이너리| D[바이너리 메시지 처리]
    
    C --> E{JSON 형식?}
    E -->|예| F[JSON 파싱]
    E -->|아니오| G[일반 텍스트 처리]
    
    F --> H{메시지 타입?}
    H -->|채팅| I[채팅 메시지 처리]
    H -->|상태| J[상태 업데이트 처리]
    H -->|알림| K[알림 처리]
    H -->|기타| L[기타 메시지 처리]
    
    D --> M{데이터 유형?}
    M -->|ArrayBuffer| N[ArrayBuffer 처리]
    M -->|Blob| O[Blob 처리]
    
    style A fill:#3cb371,stroke:#333,stroke-width:2px
    style B fill:#daa520,stroke:#333,stroke-width:2px
    style E fill:#daa520,stroke:#333,stroke-width:2px
    style H fill:#daa520,stroke:#333,stroke-width:2px
    style M fill:#daa520,stroke:#333,stroke-width:2px
Loading

웹 소켓 채널

웹 소켓 통신에서 채널은 단일 웹 소켓 연결 내에서 여러 논리적 통신 경로를 제공하는 개념입니다. 이를 통해 하나의 WebSocket 인스턴스로 여러 유형의 메시지를 구조적으로 관리할 수 있습니다.

채널의 개념과 이점

웹 소켓 채널은 단일 연결을 통해 여러 종류의 데이터를 구분하여 전송할 수 있게 해줍니다:

  1. 메시지 분류: 다양한 유형의 메시지를 논리적으로 분리
  2. 코드 구조화: 각 채널별로 독립적인 처리 로직 구현 가능
  3. 확장성: 새로운 기능 추가 시 기존 채널에 영향 없이 새 채널 추가 가능
  4. 유지보수성: 관심사 분리를 통한 코드 유지보수 용이성 증가
graph TD
    A[단일 WebSocket 연결] --> B[채팅 채널]
    A --> C[알림 채널]
    A --> D[상태 업데이트 채널]
    A --> E[파일 전송 채널]
    
    style A fill:#c35b5b,stroke:#333,stroke-width:2px
    style B fill:#3cb371,stroke:#333,stroke-width:2px
    style C fill:#3cb371,stroke:#333,stroke-width:2px
    style D fill:#3cb371,stroke:#333,stroke-width:2px
    style E fill:#3cb371,stroke:#333,stroke-width:2px
Loading

채널 구현 방법

웹 소켓 프로토콜 자체는 채널 개념을 직접 지원하지 않지만, 메시지 형식을 통해 논리적 채널을 구현할 수 있습니다:

1. 메시지 타입 기반 채널

가장 간단한 방법은 각 메시지에 채널 식별자나 타입을 포함시키는 것입니다:

// 채널 기반 메시지 구조
interface ChannelMessage {
  channel: string;  // 채널 식별자
  data: any;        // 채널별 데이터
}

// 채널별 메시지 전송 함수
function sendToChannel(socket: WebSocket, channel: string, data: any): void {
  const message: ChannelMessage = {
    channel,
    data
  };
  socket.send(JSON.stringify(message));
}

// 사용 예시
sendToChannel(socket, 'chat', { sender: 'user1', content: '안녕하세요!' });
sendToChannel(socket, 'notification', { type: 'info', message: '새 메시지가 도착했습니다.' });
sendToChannel(socket, 'status', { user: 'user2', status: 'online' });
2. 채널 관리자 구현

더 구조화된 접근 방식으로, 채널 관리자 클래스를 구현할 수 있습니다:

class WebSocketChannelManager {
  private socket: WebSocket;
  private channels: Map<string, (data: any) => void> = new Map();
  
  constructor(url: string) {
    this.socket = new WebSocket(url);
    this.socket.addEventListener('message', this.handleMessage.bind(this));
  }
  
  // 채널 구독
  subscribe(channel: string, handler: (data: any) => void): void {
    this.channels.set(channel, handler);
  }
  
  // 채널 구독 해제
  unsubscribe(channel: string): void {
    this.channels.delete(channel);
  }
  
  // 채널로 메시지 전송
  send(channel: string, data: any): void {
    if (this.socket.readyState !== WebSocket.OPEN) {
      return;
    }
    const message = {
      channel,
      data
    };
    this.socket.send(JSON.stringify(message));
  }

  // 수신된 메시지 처리
  private handleMessage(event: MessageEvent): void {
    try {
      const message = JSON.parse(event.data);
      const { channel, data } = message;

      // 해당 채널의 핸들러 호출
      const handler = this.channels.get(channel);
      if (!handler) {
        return;
      }
      handler(data);
    } catch (error) {
      console.error('메시지 처리 오류:', error);
    }
  }
  
  // 연결 종료
  close(): void {
    this.socket.close();
  }
}

// 사용 예시
const channelManager = new WebSocketChannelManager('wss://example.com/socket');

// 채팅 채널 구독
channelManager.subscribe('chat', (data) => {
  console.log(`${data.sender}: ${data.content}`);
});

// 알림 채널 구독
channelManager.subscribe('notification', (data) => {
  showNotification(data.message);
});

// 채널로 메시지 전송
channelManager.send('chat', { sender: 'user1', content: '안녕하세요!' });
3. 서버 측 채널 처리 (Java)

서버 측에서도 채널 개념을 구현하여 메시지를 처리할 수 있습니다:

public class ChannelWebSocketHandler extends TextWebSocketHandler {

    private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        
        // 채널 메시지 파싱
        JsonNode jsonNode = objectMapper.readTree(payload);
        String channel = jsonNode.path("channel").asText();
        JsonNode data = jsonNode.path("data");

        // 채널별 처리
        switch (channel) {
            case "chat":
                handleChatChannel(session, data);
                break;
            case "notification":
                handleNotificationChannel(session, data);
                break;
            case "status":
                handleStatusChannel(session, data);
                break;
            default:
                System.out.println("알 수 없는 채널: " + channel);
        }
    }

    private void handleChatChannel(WebSocketSession session, JsonNode data) throws IOException {
        // 채팅 메시지 처리 로직
        String sender = data.path("sender").asText();
        String content = data.path("content").asText();

        // 모든 클라이언트에 브로드캐스트
        broadcastToChannel("chat", data);
    }

    private void handleNotificationChannel(WebSocketSession session, JsonNode data) throws IOException {
        // 알림 메시지 처리 로직
        broadcastToChannel("notification", data);
    }

    private void handleStatusChannel(WebSocketSession session, JsonNode data) throws IOException {
        // 상태 업데이트 처리 로직
        broadcastToChannel("status", data);
    }

    private void broadcastToChannel(String channel, JsonNode data) throws IOException {
        Map<String, Object> messageMap = new HashMap<>();
        messageMap.put("channel", channel);
        messageMap.put("data", objectMapper.convertValue(data, Map.class));

        TextMessage message = new TextMessage(objectMapper.writeValueAsString(messageMap));

        sessions.values().stream()
            .filter(WebSocketSession::isOpen)
            .forEach(client -> {
                try {
                    client.sendMessage(message);
                } catch (IOException e) {
                    // 로그 기록 및 클라이언트 연결 상태 관리
                    logger.error("채널 메시지 전송 실패: " + e.getMessage(), e);
                    // 연결 문제가 있는 클라이언트 세션 관리
                    handleFailedMessageDelivery(client, e);
                }
            });
    }
}

채널과 멀티플렉싱의 차이점

웹 소켓 채널과 멀티플렉싱 확장은 유사하지만 다른 개념입니다:

  1. 채널: 애플리케이션 레벨에서 구현되는 논리적 분리로, 메시지 형식을 통해 구현됩니다.
  2. 멀티플렉싱 확장: 프로토콜 레벨에서 구현되는 물리적 분리로, 웹 소켓 프레임 헤더에 채널 ID를 추가합니다.

채널은 별도의 확장 없이 표준 웹 소켓 프로토콜만으로 구현 가능하며, 대부분의 웹 소켓 애플리케이션에서 사용되는 패턴입니다.

메시지 형식 및 직렬화

효율적인 웹 소켓 통신을 위해서는 적절한 메시지 형식과 직렬화 방법을 선택하는 것이 중요합니다.

JSON 형식

JSON은 웹 소켓 메시지에 가장 널리 사용되는 형식입니다. 이는 가독성이 좋고 다양한 언어에서 쉽게 처리할 수 있기 때문입니다.

// 클라이언트 측 JSON 메시지 예시
const chatMessage = {
  type: 'chat',
  sender: 'user123',
  content: '안녕하세요!',
  timestamp: new Date().toISOString()
};

socket.send(JSON.stringify(chatMessage));
// 서버 측 JSON 처리 예시 (Jackson 사용)
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> jsonMessage = new HashMap<>();
jsonMessage.put("type", "chat");
jsonMessage.put("sender", "server");
jsonMessage.put("content", "환영합니다!");
jsonMessage.put("timestamp", Instant.now().toString());

session.sendMessage(new TextMessage(mapper.writeValueAsString(jsonMessage)));

메시지 스키마 설계

효율적인 통신을 위해 일관된 메시지 스키마를 설계하는 것이 좋습니다:

// 기본 메시지 인터페이스
interface BaseMessage {
  type: string;
  timestamp: string;
}

// 채팅 메시지
interface ChatMessage extends BaseMessage {
  type: 'chat';
  sender: string;
  content: string;
  room?: string;
}

// 상태 메시지
interface StatusMessage extends BaseMessage {
  type: 'status';
  userId: string;
  status: 'online' | 'offline' | 'away';
}

// 오류 메시지
interface ErrorMessage extends BaseMessage {
  type: 'error';
  code: number;
  message: string;
}

// 메시지 타입 유니온
type WebSocketMessage = ChatMessage | StatusMessage | ErrorMessage;

// 메시지 생성 함수
function createChatMessage(sender: string, content: string, room?: string): ChatMessage {
  return {
    type: 'chat',
    timestamp: new Date().toISOString(),
    sender,
    content,
    room
  };
}

바이너리 직렬화

텍스트 기반 JSON 외에도 바이너리 직렬화 형식을 사용하여 메시지 크기를 줄이고 성능을 향상시킬 수 있습니다:

  1. Protocol Buffers (protobuf): Google에서 개발한 효율적인 바이너리 직렬화 형식
  2. MessagePack: JSON과 유사하지만 더 작고 빠른 바이너리 형식
  3. CBOR (Concise Binary Object Representation): JSON 데이터 모델을 기반으로 한 바이너리 형식

MessagePack 예시:

// 클라이언트 측 (MessagePack 사용)
import * as msgpack from 'msgpack-lite';

const socket: WebSocket = new WebSocket('wss://example.com/msgpack');

socket.addEventListener('open', () => {
  const message = {
    type: 'chat',
    sender: 'user123',
    content: '안녕하세요!',
    timestamp: new Date().toISOString()
  };
  
  // MessagePack으로 직렬화
  const encoded = msgpack.encode(message);
  socket.send(encoded);
});

socket.addEventListener('message', (event: MessageEvent) => {
  if (event.data instanceof ArrayBuffer) {
    // MessagePack 디코딩
    const decoded = msgpack.decode(new Uint8Array(event.data));
    console.log('수신된 메시지:', decoded);
  }
});
// 서버 측 (MessagePack 사용)
public class MsgPackHandler extends BinaryWebSocketHandler {

    private MessagePack msgpack = new MessagePack();

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws IOException {
        // MessagePack 디코딩
        ByteBuffer buffer = message.getPayload();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        Map<String, Object> decodedMessage = msgpack.read(bytes, new ValueTypeDecoderBuilder().build());
        
        System.out.println("수신된 메시지: " + decodedMessage);
        
        // 응답 생성
        Map<String, Object> response = new HashMap<>();
        response.put("type", "response");
        response.put("status", "success");
        response.put("timestamp", Instant.now().toString());
        
        // MessagePack으로 직렬화
        byte[] encoded = msgpack.write(response);
        session.sendMessage(new BinaryMessage(ByteBuffer.wrap(encoded)));
    }
}

압축

대용량 메시지의 경우 압축을 사용하여 전송 크기를 줄일 수 있습니다:

// 클라이언트 측 (pako 라이브러리 사용)
import * as pako from 'pako';

function sendCompressedMessage(socket: WebSocket, message: object): void {
  const jsonString = JSON.stringify(message);
  
  // 메시지 압축
  const compressed = pako.deflate(jsonString);
  socket.send(compressed.buffer);
}

socket.addEventListener('message', (event: MessageEvent) => {
  if (event.data instanceof ArrayBuffer) {
    try {
      // 압축 해제
      const decompressed = pako.inflate(new Uint8Array(event.data), { to: 'string' });
      const message = JSON.parse(decompressed);
      console.log('압축 해제된 메시지:', message);
    } catch (error) {
      console.error('압축 해제 오류:', error);
    }
  }
});

5가지 키워드로 정리하는 핵심 포인트

  1. 메시지 전송 방식: 웹 소켓은 send() 메서드를 통해 텍스트와 바이너리 데이터를 모두 전송할 수 있으며, 연결 상태를 확인한 후 전송하는 것이 중요합니다.
  2. 데이터 형식: JSON은 가장 널리 사용되는 메시지 형식이지만, 성능이 중요한 경우 MessagePack이나 Protocol Buffers와 같은 바이너리 직렬화 형식을 고려할 수 있습니다.
  3. 메시지 처리: 클라이언트는 message 이벤트 리스너를, 서버는 핸들러 메서드를 통해 수신된 메시지를 처리하며, 메시지 유형에 따라 적절한 로직을 실행합니다.
  4. 웹 소켓 채널: 단일 웹 소켓 연결 내에서 여러 논리적 통신 경로를 구현하여 메시지를 분류하고 코드를 구조화할 수 있으며, 이는 애플리케이션 레벨에서 메시지 형식을 통해 구현됩니다.
  5. 파일 전송: 웹 소켓을 통해 파일을 전송할 때는 메타데이터(파일 이름, 크기 등)를 먼저 전송한 후 바이너리 데이터를 전송하는 패턴을 사용하는 것이 효과적입니다.

확인 문제

  1. 웹 소켓에서 텍스트 메시지를 전송하는 올바른 방법은?

    • socket.transmit("Hello");
    • socket.send("Hello");
    • socket.postMessage("Hello");
    • socket.emit("message", "Hello");
  2. 웹 소켓을 통해 바이너리 데이터를 전송할 때 사용할 수 있는 형식이 아닌 것은?

    • ArrayBuffer
    • Blob
    • TypedArray
    • JSON String
  3. 웹 소켓 채널에 대한 설명으로 올바른 것은?

    • 웹 소켓 프로토콜에 기본적으로 내장된 기능이다
    • 채널은 애플리케이션 레벨에서 메시지 형식을 통해 구현된다
    • 채널을 사용하려면 반드시 웹 소켓 확장이 필요하다
    • 하나의 웹 소켓 연결은 최대 4개의 채널만 지원한다
  4. 웹 소켓 채널 구현 방법으로 올바른 것을 모두 고르세요. (복수 응답)

    • 메시지에 채널 식별자를 포함시켜 구현한다
    • 각 채널마다 별도의 웹 소켓 연결을 생성해야 한다
    • 채널 관리자 클래스를 통해 구조화된 방식으로 구현할 수 있다
    • 서버 측에서 채널별로 다른 처리 로직을 구현할 수 있다
    • 웹 소켓 프레임 헤더를 직접 수정해야 한다
  5. 웹 소켓 채널과 멀티플렉싱 확장의 차이점으로 올바른 것은?

    • 채널은 프로토콜 레벨에서, 멀티플렉싱은 애플리케이션 레벨에서 구현된다
    • 채널은 애플리케이션 레벨에서, 멀티플렉싱은 프로토콜 레벨에서 구현된다
    • 채널은 텍스트 메시지만, 멀티플렉싱은 바이너리 메시지만 지원한다
    • 채널과 멀티플렉싱은 동일한 개념의 다른 이름일 뿐이다
  6. 웹 소켓 메시지 수신에 관한 설명으로 올바른 것을 모두 고르세요.

    • 클라이언트에서는 'message' 이벤트를 통해 메시지를 수신한다
    • 서버에서는 handleTextMessage와 handleBinaryMessage 메서드를 오버라이드하여 메시지 핸들러를 정의할 수 있다
    • 수신된 메시지는 항상 문자열 형태이다
    • MessageEvent.data 속성은 항상 JSON 객체이다
    • 바이너리 메시지와 텍스트 메시지는 동일한 이벤트 핸들러로 처리된다
  7. 웹 소켓 메시지 형식 및 직렬화에 관한 설명으로 올바른 것은?

    • JSON은 바이너리 형식이므로 항상 MessagePack보다 효율적이다
    • Protocol Buffers는 웹 소켓에서 사용할 수 없다
    • 대용량 메시지 전송 시 압축을 사용하면 전송 효율성을 높일 수 있다
    • 웹 소켓은 텍스트 메시지만 지원하므로 바이너리 직렬화는 불필요하다

정답 및 해설 보기