스프링시큐리티 + JWT 기반에서 개발을 하려고 하니 어떤 문제가 원인인지 찾기 어려웠다.
다른 코드들도 적용해보면서 여러 시도를 해봤지만 결과는 항상 4xx에러 였다.
그래서 생각한 방법은 아예 새로운 프로젝트에서 스프링시큐리티나 JWT 없이 개발하는 것이었다.
그러면 스프링시큐리티나 JWT에 대해 생각하지 않고 온전히 채팅 개발에만 집중할 수 있을 것 같았다.
공식문서를 찾아보다가 WebSocket + Stomp 관련한 가이드를 발견했다.
https://spring.io/guides/gs/messaging-stomp-websocket/
위에서 기본 코드를 가져왔다. git clone 사용하면 쉽게 가져올수 있다.
나는 initial 프로젝트와 complete 프로젝트 중 complete 프로젝트를 사용했고, 프로젝트를 열 때 Maven Poject로 할건지, Gradle 프로젝트를 할건지 물어보기에 Gradle를 선택했다.
localhost:8080 으로 접속했을 때 화면이 뜨고, 입력했을 때 잘 작동하는 것을 확인할 수 있었다.
이미 여기서부터 감격...!! .·´¯`(>▂<)´¯`·.
이제 여기에 채팅방을 생성해서 같은 방에 있는 사람끼리 채팅이 가능하도록 바꿔야 했다!
문제점1. 방 생성
EndPoint를 우리 프로젝트에 맞게 변경해주고, html에 입장할 방 Id를 입력하는 input을 생성하여, 웹 소켓을 연결할 때 해당 roomId를 구독하도록 했다.
@Controller
public class GreetingController {
private SimpMessagingTemplate msgOperation;
public GreetingController(SimpMessagingTemplate msgOperation) {
this.msgOperation = msgOperation;
}
@MessageMapping("/hello")
@SendTo("/topic/greetings/{roomId}")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(500); // simulated delay
msgOperation.convertAndSend("/topic/greetings/" + message.getRoomId());
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName() + "!"));
}
}
function connect() {
var socket = new SockJS('/ws-edit');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
var roomId = $("#create").val();
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings/' + roomId, function (greeting) {
showGreeting(JSON.parse(greeting.name).content);
});
});
}
방 생성할 때 방 번호를 입력해서 생성하고, 메시지를 보낼 때 어디에 보낼지 방 번호를 함께 적는다.
Send를 보내봤지만 아무런 일도 일어나지 않았다...
문제점1 시도
roomId를 잘 못 가져오나 싶어 구독할 때 /1를 붙여 1번 방을 무조건 구독하도록 했다.그래도 변화는 없었다.
function connect() {
var socket = new SockJS('/ws-edit');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings/1', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
문제점1 해결
@SendTo /{roomId} 를 붙여서 안된 것 같다.
@SendTo 경로 뒤에 /를 붙이지 않고 바로 rooId를 붙여줌으로써 해결되었다.
@Controller
public class GreetingController {
private SimpMessagingTemplate msgOperation;
public GreetingController(SimpMessagingTemplate msgOperation) {
this.msgOperation = msgOperation;
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(500); // simulated delay
msgOperation.convertAndSend("/topic/greetings" + message.getRoomId());
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName() + "!"));
}
}
function connect() {
var socket = new SockJS('/ws-edit');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
var roomId = $("#create").val();
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings' + roomId, function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
문제점2. Undefinded 메시지
클라이언트1과 클라이언트2를 1번 방에 연결하고 클라이언트1 에서 메시지를 보낸다.
클라이언트2 에게도 클라이언트1 메시지가 뜨지만 undefinded이다...
웹 콘솔에서는 데이터가 잘 넘어왔다.
문제점2 해결
GreetingController에서 return값과 무관하게 ConverAndSend()를 통해 message 객체를 함께 보내주도록 변경했기 때문에, 거기서 name으로 가져오도록 했다.
@Controller
public class GreetingController {
private SimpMessagingTemplate msgOperation;
public GreetingController(SimpMessagingTemplate msgOperation) {
this.msgOperation = msgOperation;
}
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(500); // simulated delay
msgOperation.convertAndSend("/topic/greetings" + message.getRoomId(), message); //message추가
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName() + "!"));
}
}
function connect() {
var socket = new SockJS('/ws-edit');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
var roomId = $("#create").val();
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings' + roomId, function (greeting) {
showGreeting(JSON.parse(greeting.body).name); // content -> name 변경
});
});
}
문제점3. 퇴장 메시지 안 나오는 문제
클라이언트가 채팅방을 나가면 채팅방에 남아있는 사람에게 퇴장했다는 메시지가 떠야 한다.
채팅방 나가기 버튼을 만들어, leaveMessage() 메서드를 만들고 메시지를 보내도록 했다.
function leaveMessage() {
stompClient.send("/pub/chat/send", {}, JSON.stringify(
{ 'type' : "LEAVE",
'sender' : $("#my-name").val(),
'roomId' : $("#connectRoomId").val(),
'message': $("#my-message").val()}));
}
ChatService에서는 message를 다음과 같이 변경하도록 했다.
public ChatDto disconnectChatRoom(SimpMessageHeaderAccessor headerAccessor) {
String roomId = (String) headerAccessor.getSessionAttributes().get("roomId");
String nickName = (String) headerAccessor.getSessionAttributes().get("nickname");
ChatDto chatDto = ChatDto.builder()
.type(ChatDto.MessageType.LEAVE)
.roomId(roomId)
.sender(nickName)
.message(nickName + "님 퇴장!! ヽ(*。>Д<)o゜")
.build();
return chatDto;
}
~님 퇴장이라는 메시지가 나타나지 않고, 이전에 보냈던 메시지가 나타났다.
마지막에 있는 메시지는 퇴장을 했더니 날라간 메시지다.
심지어 채팅방을 나갔으니 본인한테는 메시지가 떠서도 안되는데... 소켓 연결도 끊기지 않았다.
문제점3 시도
ChatController 측을 살펴보았다. 채팅방을 나가면 다음과 같은 메서드가 수행되어야 한다.
@EventListener
public void webSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
ChatDto chatDto = chatService.disconnectChatRoom(headerAccessor);
msgOperation.convertAndSend("/sub/chat/room" + chatDto.getRoomId(), chatDto);
}
웹 소켓 연결이 끊기는 이벤트가 발생하면 해당 메서드에 진입하고 service 측의 disconnectChatRoom() 메서드로 진입해 메세지를 새로 설정해주어야 하는데, 해당 이벤트로 들어오지 못하는 것 같았다.
문제점3 해결.
해당 이벤트를 메서드가 잡지 못한 이유는 바로 채팅방을 나갈 때 웹 소켓 연결을 안 끊어주었기 때문이다...
그래서 테스트할 때 퇴장 버튼을 누르면, 메시지 입력 부분에 남아있던 text를 가져와 아래 메서드가 수행되었던 것이다.
@MessageMapping("/chat/send")
@SendTo("/sub/chat/room")
public void sendChatRoom(ChatDto chatDto, SimpMessageHeaderAccessor headerAccessor) throws Exception {
Thread.sleep(500); // simulated delay
msgOperation.convertAndSend("/sub/chat/room" + chatDto.getRoomId(), chatDto);
}
나가기 버튼은 웹 소켓 연결을 해제하는 것이고, 클라이언트에서 type이나 sender이런 것들을 보내줄 필요가 없었다.
아예 서버 측에서 ChatDto를 생성해서 클라이언트로 넘겨주기만 하면 되는 것이었다.
나가기 버튼에 연결된 클라이언트측 메서드를 다음과 같이 수정했다.
기존에 있던 disconnet() 메서드를 이름만 바꿨다.
function leaveChatRoom() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
이제 퇴장하면 연결이 끊기고, 퇴장 메시지를 채팅방에 있는 사람에게 보내는 것을 확인할 수 있다.
💡느낀점
지난 이틀간 PostMan의 4xx 에러에 사로잡혀 봤던 stackoverflow를 또 보고, 봤던 블로그를 또 보고... 그래도 영 진전이 없어 힘들었었다.
처음부터 새로운 마음으로 프로젝트를 만들자는 생각이 다행히 틀리지 않았던 것 같다.
스프링 공식 문서에서 제공하는 기본 프로젝트를 기반으로 채팅방 생성, 입장 처리, 메시지 전송, 퇴장 처리까지 모두 직접 구현할 수 있었다. 다행히 오랜 시간이 걸리지 않았다. 오래 걸렸다고 해도 클라이언트가 있어 직접 메시지 보내고, 문제점을 바로 확인하니 재미있어서 그렇게 느꼈던 것 같기도 하다.
물론 공식 문서로 시작했기 때문에 얼마 안 걸린게 아니고, 이틀간 삽질하면서 다시 만들고, 다시 만들면서 어떻게 기능을 구현하면 좋을지 구조를 잘 짜고, 거기서 또 배운게 있기 때문이라고 생각한다.
PostMan으로 테스트하기 보다 프론트가 있으니 확실히 확인이 편했다. 공식문서 너무 고마워...
이렇게 스프링 시큐리티와 JWT가 없는 채팅 프로그램을 완성했다.
하지만 아직 여러 기능을 추가하고 변경해야 한다.
현재 ChatRoom을 새로 생성하면 DB에 저장하도록 되어있다. 이제 전체 채팅방 목록을 확인할 수 있는 기능을 추가할 것이다. 또한, 현재는 Connet를 통해 웹 소켓을 연결하고 Enter를 통해 채팅방으로 입장하도록 되어있는데, 생각해보면 1:1대화하기 버튼을 누르면 바로 웹 소켓이 연결되고, Enter가 되어야 한다. 그래서 입장 버튼만 만들어 웹 소켓 연결과 입장을 처리하도록 할 것이다. 마지막으로 최종적으로 완성된 내용을 토대로 진행하고 있던 프로젝트에 적용하여 스프링시큐리티와 JWT를 사용하도록 리팩토링해야한다.
아직 더 디벨롭이 필요하지만 해당 코드가 있는 깃허브다.
커밋 메시지 Docs: README 실행 화면 추가
까지가 오늘 구현한 내용이다.
https://github.com/O-Wensu/WebSocket-Stomp-Chat
'TIL' 카테고리의 다른 글
[WIL - 20230424~20230430] (0) | 2023.05.01 |
---|---|
[TIL - 20230430] WebSocket+Stomp 사용자들이 있는 채팅방 찾기 (0) | 2023.04.30 |
[TIL - 20230428] WebSocket + Stomp 적용기1 (0) | 2023.04.29 |
[TIL - 20230428] failed to lazily initialize a collection of role (0) | 2023.04.28 |
[TIL - 20230427] message properties 사용한 @Valid 메시지 처리 (0) | 2023.04.27 |