FireDrago
[치지직 채팅] 2. 치지직 채팅 웹소켓 접속하기 본문
본 포스팅은 개인적인 학습 목적으로 작성되었으며, 분석된 내용은 서비스 업데이트에 따라 언제든 변경될 수 있습니다


1. `Java Websocket` vs `Spring Websocket`
- Java 기본 웹소켓 라이브러리
- 직접 구현해야 할 점이 많고 스프링 통합 기능 지원되지 않음
- 로우레벨에서 ‘웹소켓’ 자체를 학습하는데 유리하기 때문에 선택함
2. `onOpen` 이벤트 처리
2.1 WebSocket 핸드셰이크 (HTTP -> WS 전환)
- HTTP 형식으로 Websocket 연결요청 전송 특정 헤더를 포함한다.
- Websocket 전환이 승인되면, 서버는 다음과 같은 응답을 반환한다.
2.2 onOpen 콜백 호출 및 프로토콜 전환 완료
- 클라이언트(java-websocket)는 서버로부터 상태 코드 `101 Switching Protocols`와
유효한 `Sec-WebSocket-Accept` 헤더를 포함한 응답을 수신한다. - 클라이언트는 이 응답을 확인하고 핸드셰이크가 성공적으로 완료되었다고 판단한다.
- 이 시점에서 HTTP 연결은 종료되고, 동일한 TCP/IP 소켓 연결이 WebSocket 프로토콜을 사용하는
영구적인 양방향 통신 채널로 전환된다. - `onOpen(ServerHandshake handshakeData)` 메소드가 호출된다.
- 이 모든 과정을 `java-websocket` 라이브러리는 위의 과정을 자동으로 처리해준다.
우리는 onOpen메서드만 정의하면 된다.
2.3 코드로 구현하기
public class ChzzkWebsocketClient extends WebSocketClient {
private final String chatChannelId;
private final String accessToken;
// 무거운 ObjectMapper는 한 번만 생성해서 재사용 (메모리 절약)
private final ObjectMapper objectMapper = new ObjectMapper();
private ScheduledExecutorService pingScheduler;
public ChzzkWebsocketClient(URI serverUri, String chatChannelId, String accessToken) {
// URI를 외부에서 주입받아야 한다.
super(serverUri);
this.chatChannelId = chatChannelId;
this.accessToken = accessToken;
}
@Override
public void onOpen(ServerHandshake handshakeData) {
// 1. 인증 패킷 전송
String authPacket = createAuthPacket(chatChannelId, accessToken);
if (authPacket != null) {
this.send(authPacket);
}
// 2. 스케줄러 시작 (20초마다 능동적으로 생존 신고)
pingScheduler = Executors.newSingleThreadScheduledExecutor();
// [수정 2] 메서드 레퍼런스 활용
pingScheduler.scheduleAtFixedRate(this::sendActivePing, 20, 20, TimeUnit.SECONDS);
}
private String createAuthPacket(String chatChannelId, String accessToken) {
try {
ChzzkAuthRequest.AuthRequestBody body = new ChzzkAuthRequest.AuthRequestBody(
null,
2001,
"Google Chrome/142.0.0.0",
accessToken,
"4.9.3",
"ko",
"macOS/10.15.7",
"Asia/Seoul",
"READ"
);
ChzzkAuthRequest request = new ChzzkAuthRequest(
"3", 100, "game", chatChannelId, 1, body
);
return this.objectMapper.writeValueAsString(request);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 변환 실패", e);
}
}
private void sendActivePing() {
// 만약 치지직이 cmd: 0을 무시한다면 cmd: 10000으로 변경해서 테스트 필요.
String pingPacket = "{\"cmd\": 0, \"ver\": 2}";
try {
this.send(pingPacket);
} catch (Exception e) {
System.err.println("Heartbeat 전송 실패");
}
}
}
- `onOpen` 메서드는 WebSocket 통신 채널이 확립된 직후에 호출되는 라이프사이클의 시작점이다.
따라서 이 메서드에서는 연결을 유효하게 만들고 유지하기 위한 필수 작업을 한다 - `org.java_websocket.client.WebSocketClient`를 상속받아 구현한다.
- 치지직 인증 형식에 맞춰 dto를 생성하고 `createAuthPacket` 인증패킷을 전송한다.
`ObjectMapper` 객체는 생성 부담이 큰 편이므로 한번만 생성해서 재사용 한다. - `pingScheduler = Executors.newSingleThreadScheduledExecutor()`
별도의 하트비트 전송을 위한 백그라운드 스레드를 생성한다.
메시지를 수신 하고 파싱하는 메인 스레드가 블로킹되는 상황을 방지하기 위함이다. - `pingScheduler.scheduleAtFixedRate(this::sendActivePing, 20, 20, TimeUnit.SECONDS)`
핑을 위한 별도의 스레드가 onOpen이 호출되고 20초 뒤에 20초의 간격으로 매번 실행하도록 설정했다.
3. `onMessage` 이벤트 처리
- `onMessage` 메서드는 웹소켓 이벤트 처리 스레드에 의해 호출되며, 서버가 보낸 메시지를 처리하는 역할을 한다.
이 단계의 핵심은 수신된 일반 문자열(JSON)을 해석하여 메시지 유형에 따라 적절히 분기하고 처리하는 것이다.
@Override
public void onMessage(String message) {
try {
JsonNode node = objectMapper.readTree(message);
int cmd = node.get("cmd").asInt();
switch (cmd) {
case 0: // 서버가 "살아있니?" (Ping) 물어봄
System.out.println("<<< 서버 Ping 수신 (cmd: 0)");
sendPong(); // "응 살아있어" (Pong) 대답
break;
case 10000: // [서버 -> 나] ㅇㅇ 너 살아있네 (내 핑에 대한 대답)
// 20초마다 보낸 sendActivePing()에 대해 서버가 응답.
System.out.println("<<< [수신] 서버 Pong(cmd: 10000) - 내 핑에 대답함");
break;
case 93101: // 채팅 메시지
JsonNode bdy = node.get("bdy");
// 치지직은 한 패킷에 여러 채팅이 묶여서 올 수 있다 (Array 구조)
if (bdy != null && bdy.isArray()) {
for (JsonNode chatItem : bdy) {
// 1. 채팅 내용 추출
String messageType = chatItem.path("msgTypeCode").asText();
String content = chatItem.path("msg").asText();
// (1번: 일반 채팅, 10번: 후원 채팅 등. 일단 출력)
System.out.println("[****] " + content);
}
}
break;
default:
break;
}
} catch (Exception e) {
System.err.println("메시지 파싱 실패: " + e.getMessage());
}
}
- `objectMapper.readTree(message)`: 수신된 일반 String 메시지를 `JsonNode` 객체로 즉시 파싱한다.
이 작업은 웹소켓 메인 스레드에서 수행되므로, 파싱이 느려지면 다른 이벤트 처리가 지연될 수 있다. - `cmd = node.get("cmd").asInt()`: 치지직 서버는 cmd 필드를 사용하여 메시지 유형을 정의한다.
우리는 이 값을 기준으로 switch 문을 사용해 Heartbeat 응답, 자체 핑 확인, 채팅 메시지 처리 등
모든 비즈니스 로직을 수동으로 분기해야 한다. - `if (bdy.isArray())`: 치지직 서버는 한 번의 웹소켓 메시지에 여러 개의 채팅 메시지를 묶어서 보낼 수 있다.
`bdy`가 배열인지 확인하고 `for`루프를 돌면서 각 채팅 아이템을 개별적으로 처리해야 한다.
4. `onClose`이벤트 처리: 자원 해제 및 정리
- `onClose` 메서드는 웹소켓 연결이 정상적이든 비정상적이든 종료되었을 때 호출되는 콜백이다.
`onOpen`에서 생성했던 백그라운드 스레드 자원을 깨끗하게 해제해야 한다.
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println(">>> 연결 끊김: " + reason);
// 스케줄러 종료 (자원 정리)
if (pingScheduler != null && !pingScheduler.isShutdown()) {
pingScheduler.shutdown();
}
}
- `if (pingScheduler != null && !pingScheduler.isShutdown())`:
스케줄러 객체가 null이 아니고, 이미 종료되지 않은 경우에만 shutdown()을 호출하여
NullPointerException이나 불필요한 호출을 방지한다. - 메모리 누수 방지: 이 작업을 생략하면 백그라운드 스레드가 계속 메모리에 남아
메모리 누수 및 불필요한 자원 점유를 유발한다.
'프로젝트' 카테고리의 다른 글
| [채팅 감정 분석] 4. 실시간 분석 데이터 파이프라인 설계 (0) | 2025.12.21 |
|---|---|
| [치지직 채팅] 3. 웹소켓 클라이언트와 스레드 (0) | 2025.12.13 |
| [치지직 채팅] 1. 치지직 채팅 웹소켓 프로토콜 분석 (0) | 2025.12.05 |
| [Moment] 프로젝트 쿼리 실행속도 50초 단축하기 (0) | 2025.10.19 |
| [Moment] Spring Boot 로깅 전략: AOP, Logback, Docker Volume, AWS CloudWatch 적용기 (0) | 2025.08.17 |
