WebSocket 서버는 TextWebSocketHandler 클래스를 상속받아 구현합니다. 이 클래스는 텍스트 기반의 WebSocket 통신을 처리하는 데 특화되어 있습니다.
import org.springframework.web.socket.handler.TextWebSocketHandler;
public class ChattingServer extends TextWebSocketHandler {
// 구현 내용
}
주요 메서드와 그 역할:
afterConnectionEstablish(WebSocketSession): 클라이언트가 서버에 연결되었을 때 호출됩니다. 여기서 새로운 연결에 대한 초기화 작업을 수행할 수 있습니다.handleTextMessage(WebSocketSession, TextMessage): 클라이언트로부터 텍스트 메시지를 수신했을 때 호출됩니다. 이 메서드에서 메시지를 처리하고 적절한 응답을 보낼 수 있습니다.afterConnectionClosed(WebSocketSession, CloseStatus): 클라이언트와의 연결이 종료되었을 때 호출됩니다. 여기서 연결 종료에 따른 정리 작업을 수행할 수 있습니다.Spring Framework에서 WebSocket을 사용하기 위해서는 필요한 의존성을 추가하고, WebSocket 핸들러를 Spring bean으로 등록해야 합니다.
Maven 프로젝트의 경우, pom.xml 파일에 WebSocket 관련 의존성을 추가합니다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring.version}</version>
</dependency>
spring-websocket은 WebSocket 기능을, spring-messaging은 메시지 처리 기능을 제공합니다.
XML 기반 설정을 사용하는 경우, servlet-context.xml 파일에 WebSocket 핸들러를 Spring bean으로 등록합니다.
<beans:bean id="chattingServer" class="com.example.ChattingServer"/>
Java 기반 설정을 사용하는 경우, @Configuration 클래스에 다음과 같이 bean을 등록할 수 있습니다:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ChattingServer chattingServer() {
return new ChattingServer();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chattingServer(), "/chat")
.setAllowedOrigins("*");
}
}
WebSocket 핸들러를 URL 경로에 매핑하여 등록해야 합니다.
XML 설정의 경우 (servlet-context.xml):
<websocket:handlers>
<websocket:mapping handler="chattingServer" path="/chat"/>
<websocket:handshake-interceptors>
<beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
Java 설정의 경우:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chattingServer(), "/chat")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*");
}
}
HttpSessionHandshakeInterceptor는 HTTP 세션 속성을 WebSocket 세션으로 복사하는 역할을 합니다. 이를 통해 HTTP 세션에 저장된 사용자 정보 등을 WebSocket 연결에서도 사용할 수 있습니다.
채팅 페이지로의 이동을 설정하기 위해 WebMvcConfigurer를 구현합니다. 이 설정은 특정 URL 요청을 특정 뷰로 매핑합니다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/chatpage").setViewName("chat/chatpage");
}
}
이 설정은 "/chatpage" URL 요청이 들어오면 "chat/chatpage" 뷰를 반환하도록 합니다. 실제 뷰 파일의 위치와 확장자는 ViewResolver 설정에 따라 결정됩니다.
ChattingServer 클래스는 실제 WebSocket 통신을 처리하는 핵심 로직을 포함합니다.
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class ChattingServer extends TextWebSocketHandler {
private Map<String, WebSocketSession> clients = new ConcurrentHashMap<>();
@Autowired
private ObjectMapper mapper;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("사용자 입장: {}", session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
Message msg = mapper.readValue(message.getPayload(), Message.class);
switch(msg.getType()) {
case "open": addClient(session, msg); break;
case "msg": sendMessage(msg); break;
case "close": removeClient(session); break;
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
removeClient(session);
log.info("사용자 퇴장: {}", session.getId());
}
private void addClient(WebSocketSession session, Message msg) {
clients.put(msg.getSender(), session);
sendMessage(Message.builder().type("join").sender(msg.getSender()).build());
sendClientList();
}
private void removeClient(WebSocketSession session) {
String sender = clients.entrySet().stream()
.filter(entry -> entry.getValue().equals(session))
.map(Map.Entry::getKey)
.findFirst()
.orElse(null);
if (sender != null) {
clients.remove(sender);
sendMessage(Message.builder().type("leave").sender(sender).build());
sendClientList();
}
}
private void sendClientList() {
try {
Message msg = Message.builder()
.type("clientList")
.msg(mapper.writeValueAsString(clients.keySet()))
.build();
sendMessage(msg);
} catch(Exception e) {
log.error("Error sending client list", e);
}
}
private void sendMessage(Message msg) {
String jsonMsg = null;
try {
jsonMsg = mapper.writeValueAsString(msg);
} catch (Exception e) {
log.error("Error converting message to JSON", e);
return;
}
TextMessage textMessage = new TextMessage(jsonMsg);
clients.values().forEach(session -> {
try {
session.sendMessage(textMessage);
} catch (Exception e) {
log.error("Error sending message to client", e);
}
});
}
}
주요 포인트:
ConcurrentHashMap을 사용하여 스레드 안전성을 보장합니다.ObjectMapper를 사용하여 JSON 변환을 처리합니다.Message 클래스는 클라이언트와 서버 간에 주고받는 메시지의 구조를 정의합니다.
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class Message {
private String type; // 메시지 타입 (ex: open, msg, close)
private String sender; // 발신자
private String receiver; // 수신자 (필요한 경우)
private String msg; // 실제 메시지 내용
}
Lombok의 @Data와 @Builder 어노테이션을 사용하여 코드를 간결하게 유지합니다.
ObjectMapper는 Jackson 라이브러리의 일부로, JSON과 Java 객체 간의 변환을 쉽게 해줍니다.
@Autowired
private ObjectMapper mapper;
// JSON to Object
Message message = mapper.readValue(jsonString, Message.class);
// Object to JSON
String json = mapper.writeValueAsString(message);
주의사항:
readValue()와 writeValueAsString() 메서드는 checked exception을 던질 수 있으므로 적절한 예외 처리가 필요합니다.@JsonIgnore 어노테이션을 사용하여 특정 필드를 JSON 변환에서 제외할 수 있습니다.클라이언트 측에서는 WebSocket API를 사용하여 서버와 통신합니다.
const socket = new WebSocket("ws://localhost:8080/chat");
socket.onopen = function(e) {
console.log("서버에 연결됨");
socket.send(JSON.stringify({type: "open", sender: username}));
};
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
switch(data.type) {
case "join":
console.log(`${data.sender} 님이 입장하셨습니다.`);
break;
case "msg":
displayMessage(data.sender, data.msg);
break;
case "leave":
console.log(`${data.sender} 님이 퇴장하셨습니다.`);
break;
case "clientList":
updateClientList(JSON.parse(data.msg));
break;
}
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`연결이 정상적으로 종료되었습니다, 코드=${event.code} 이유=${event.reason}`);
} else {
console.log('연결이 끊겼습니다');
}
};
socket.onerror = function(error) {
console.log(`[에러] ${error.message}`);
};
function sendMessage(message) {
socket.send(JSON.stringify({
type: "msg",
sender: username,
msg: message
}));
}
function displayMessage(sender, message) {
// DOM을 조작하여 메시지를 화면에 표시
}
function updateClientList(clients) {
// DOM을 조작하여 클라이언트 목록을 업데이트
}
이 가이드는 Spring Framework를 사용한 WebSocket 서버 구현과 JavaScript를 이용한 클라이언트 구현의 기본적인 틀을 제공합니다. 실제 프로덕션 환경에서는 보안, 성능 최적화, 에러 처리, 확장성 등 추가적인 고려사항이 필요합니다.