: 웹소켓이란 웹브라우저와 서버가 실시간으로 아주 빠르게 대화할 수 있게 해주는 기술이다.
사진 출처 : https://sendbird.com/ko/developer/tutorials/websocket-vs-http-communication-protocols

우리가 평소에 웹사이트를 이용하는 방식은 바로 HTTP 방식이다.
이는 요청 ↔ 응답 방식인데, 질문하고 대답을 듣는 방식이라고 할 수 있다.
내가 요청하면 서버가 응답하고 그 흐름이 뚝 끊기는 것이다. 또 다른 요청이 필요하면 다시 보내야하는 것!
이렇게 되면 새로운 정보가 생겨도 서버가 절대 먼저 알려주지 못한다.
내가 계속해서 서버한테 새로운 거 뭐 들어온거 없니? 있니? 이렇게 물어봐야해서 실시간 채팅이나 주식처럼 정보 업데이트가 필요한 서비스에는 매우매우 비효율적이다 (계속해서 내가 새로들어온 정보 있는지 찔러봐야하니까)
이 문제를 해결하기 위해 웹소켓!! 이 나왔다
얘는 한 번 연결하면 내가 이제 끊어줘 라고 하지 않는 이상 계속해서 상태를 유지해준다
→ 언제든지 필요할때마다 양쪾에서 메세지를 주고 받을 수 있따
대표적인 예시로는 카톡이나 슬랙 같은 메신저에서 메세지 보내면 바로 알림 오는게 웹소켓 덕분이다!
또 주식 ㅏ격이 실시간으로 반영되는 것이나 인스타에서 누가 내 게시물에 좋아요를 누르면 그 알림이 바로 오는 실시간 알림도 웹소켓이다!
즉 웹소켓은 웹에서, 실시간으로, 양방향 통신을 해주는 기술!!
→ 즉각적인 정보공유가 매우 중요한 서비스에서 유용하게 사용된당
스프링에서 웹소켓 연결하기? 컵라면 먹기와 같다 (핵쉽다는 뜻 나는 헤맸음ㅠㅠㄴ)가
먼저 우리가 웹소켓 연결할게! 라고 알려줘야겠죠?
@Configuration 등록해줄 WebSocketConfig 클래스를 만들어줍시다
@Configuration
@EnableWebSocket //웹소켓 활성화
public class WebSocketConfig implements WebSocketConfigurer {
private final ChatWebSocketHandler chatWebSocketHandler; //ChatWebSocketHandler를 가져오기
//여기는 @AllArgsConstructor 로 처리해도 무방하다 (그냥 생성자임)
public WebSocketConfig(ChatWebSocketHandler chatWebSocketHandler){
this.chatWebSocketHandler = chatWebSocketHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
registry.addHandler(chatWebSocketHandler, "/ws/chat") //해당 경로로 들어오는 모든 요청은 웹소켓 핸들러가 처리한다!
.setAllowedOrigins("*"); //모든 도메인 접속을 허용했지만, 향후 배포환경이라면 특정 도메인으로 제한하는 것이 좋다
}
}
이 설정이 끝나면 거의 다 된 것이나 다름없다~!
LLM을 결합한 챗봇으로 구현할 수도 있지만 우선 웹소켓으로 채팅을 만든다는 것에 집중해보자
→ 다음 글에서 인공지능이랑 소통하는 로직을 다뤄보쟈
@Component
@Slf4j
public class ChatWebSocketHandler extends TextWebSocketHandler {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
// RestTemplate과 ObjectMapper 주입
public ChatWebSocketHandler(RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
//채팅 기록을 저장할 리스트 생성
private final Map<String, List<GeminiRequestDto.Content>> chattingHistory = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("WebSocket 연결 성공: {}", session.getId());
// 해당 세션에 대한 Gemini 대화 기록을 초기화합니다.
List<GeminiRequestDto.Content> history = chattingHistory.computeIfAbsent(session.getId(), k -> {
log.info("새로운 세션 {} 에 대한 Gemini 대화 기록 초기화", k);
return new ArrayList<>();
});
if (history.isEmpty() || !history.get(0).getParts().get(0).getText().equals(SYSTEM_PROMPT)) {
history.add(0, new GeminiRequestDto.Content(
Collections.singletonList(new GeminiRequestDto.Part(SYSTEM_PROMPT)), "user"));
log.info("세션 {} 에 시스템 프롬프트 추가 완료.", session.getId());
}
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
/*
3. 웹소켓 연결이 종료
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("웹소켓 세션 종료 : {} (이유: {})", session.getId(), status.getReason());
}
/*
웹소켓 전송 중 에러가 발생했을 때 호출
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("웹소켓 전송 중 에러가 발생했습니다. 세션 {}: {}", session.getId(), exception.getMessage(), exception);
if (session.isOpen()) {
session.close(CloseStatus.SERVER_ERROR);
}
}