
오늘은 스프링으로 웹소켓의 서버를 만드는 방법을 배워봤다.
블로그를 보고 코드를 따라 쳐보면서 웹소켓에 대한 이해를 해보았다!
이번 주제에서 가장 중요한 것은 ws인 것 같다.
웹소켓 통신을 지원하는 프로토콜로 http같은 개념이다.
http와 ws은 통신방식에서 차이가 난다.
http통신
1.요청을 보낸다.
2. 응답을 받는다.
3. 연결이 종료된다.
반면에
ws통신
1. 연결한다.
2. 서버와 클라이언트는 데이터를 계속 주고 받는다.
3. 연결이 종료되기 전까지 송수신은 계속된다.
따라서 ws는 한번 연결 한 후 요청을 계속 보내지 않아도 서버에서 데이터를 받을 수 있는 것이다.
그럼이제 코드를 보자
WebSocketConfig.java
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "ws/chat").setAllowedOrigins("*");
}
}
@EnableWebSocket 을 사용하면 웹소켓을 사용할 수 있다고 한다.
WebSocketHandlerRegistry인 registry에다가 주소("ws/chat")를 추가해줘서 해당주소로 웹소켓을 사용할 수 있게 해준다.
WebSocketHandler.java
@Slf4j
@RequiredArgsConstructor
@Component @Primary
public class WebSocketHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper;
private final ChatService chatSerivce;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception {
String payload = textMessage.getPayload();
log.info("{}",payload);
ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
ChatRoom chatRoom = chatSerivce.findRoomById(chatMessage.getRoomId());
chatRoom.handleActions(session, chatMessage, chatSerivce);
}
}
우선 @Primary는 지금 공부하고 있는 레포지토리에 웹소켓을 공부하느라 만들었던 다른 핸들러랑 헷갈리지 말라고 넣어준 코드이다!
TextWebSocketHandler를 상속받자!
스프링은 Text 타입과 Binary타입의 핸들러를 지원한다고 한다.
그래서 Text로 상속해주고 json 형태로 서버로 보내면 objectMapper 라는 이친구가 ChatMessage 형태로 샥샥 바꾸고 저장한다.
다음으론 handleActions() 메스드를 호출해서 웹소켓세션과 아까 저장했던 chatmessage와 저 밑에 있는 코드인 chatService를 보내준다.
이 액션 메소드에선 무얼하냐면
이 사람이 보낸 타입이 ENTER면 입장메세지를 출력해주고 아니면 메세지를 출력한다.
ChatRoom
@Getter
public class ChatRoom {
private String roomId;
private String name;
private Set<WebSocketSession> sessions = new HashSet<>();
public void handleActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
sessions.add(session);
chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
}
sendMessage(chatMessage,chatService);
}
private <T> void sendMessage(T message, ChatService chatService) {
sessions.parallelStream()
.forEach(session -> chatService.sendMessage(session,message));
}
@Builder
public ChatRoom(String roomId, String name) {
this.roomId = roomId;
this.name = name;
}
}
여기에 ChatHandler에서 설명했던 Chatroom.handleActions이 작성 되어있다.
ChatMessage.java
public class ChatMessage {
public enum MessageType {
ENTER, TALK
}
private MessageType type;
private String roomId;
private String sender;
private String message;
public MessageType getType() {
return type;
}
public String getRoomId() {
return roomId;
}
public String getSender() {
return sender;
}
public void setMessage(String message) {
this.message = message;
}
}
여기서는 enum으로 메세지의 타입을 분류 해줬다. 타입은 ENTER(입장), TALK(메세지보내기)로 나뉜다.
ChatService.java
@Slf4j
@RequiredArgsConstructor
@Service @Primary
public class ChatService {
private final ObjectMapper objectMapper;
private Map<String, ChatRoom> chatRooms;
@PostConstruct
private void init() {
chatRooms = new LinkedHashMap<>();
}
public List<ChatRoom> findAllRoom() {
return new ArrayList<>(chatRooms.values());
}
public ChatRoom findRoomById(String roomId) {
return chatRooms.get(roomId);
}
public ChatRoom createRoom(String name) {
String randomId = UUID.randomUUID().toString();
ChatRoom chatRoom = ChatRoom.builder()
.roomId(randomId)
.name(name)
.build();
chatRooms.put(randomId, chatRoom);
return chatRoom;
}
public <T> void sendMessage(WebSocketSession session, T message) {
try{
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
방을 만들어보자. 방을 만들때 방 Id는 UUID로 랜덤값을 받아와서 지정해준다.
name은 나중에 사용자가 json 값으로 넘겨주는 name을 사용하고
chatRooms.put()을 하여 채팅방을 만들어준다.
ChatController.java
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatService chatService;
@PostMapping
public ChatRoom createRoom(@RequestBody String name) {
return chatService.createRoom(name);
}
@GetMapping
public List<ChatRoom> findAllRoom() {
return chatService.findAllRoom();
}
}
아맞다 의존성 추가는 웹소켓을 추가해준다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
코드는 여기서 끝이다.
첨에는 form-data도 json 이겠지.... 하면서 밑의 사진처럼 보냈는데

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.socket.learn.ChatRoom com.socket.learn.ChatController.createRoom(java.lang.String)
이렇게 에러코드가 뽝 떴다.
그렇게 구글링 해보니
form-data의 형식은 multipart/form-data로
application/json과는 아예 다른 형식이더라..!
그래서 진짜 json형식으로 보냈다.

그러면 이렇게 Chatroom을 성공적으로 리턴 받았다!
그럼 웹소켓을 테스트하기 위해 확장팩을 설치해주자.

이고 설치 했다.

ws://localhost:8080/ws/chat 로 접속해주자.
입장이니까
type은 ENTER로 해주고
그리고 아까 리턴받은 Chatroom의 roomId를 가져와서 roomId에 넣어준다.
자신의 이름을 원하는 값으로 지정해주고
ENTER면 메세지는 쓰이지 않지만 그래도 메세지를 적어주자.
그리고 Send 버튼을 누르면 성공적으로 입장헀다구 나온다!

그럼 같은 주소(ws://localhost:8080/ws/chat)로 접속해주고
다른 사용자를 ENTER로 보내보자

초기사용자(테스트사용자)의 화면에서 그다음으로 입장한 사용자(테스트사용자1)이 입장한 것을 볼 수 있다!

자바 로그로는 사용자가 Send를 누를 때 마다 출력이 된다.

그럼 다른 타입인 TALK로 보내보자.

보냈던 메세지인 "안녕" 이 잘 출력되었다!

다른사용자들의 화면에서도 잘 뜬다. ㅎㅎ

이렇게 각자 다른 사용자가

각자 다른 말을 하면

대화들을 다 볼 수 있다.
자 오늘은 스프링으로 웹소켓 서버를 만들어보았다. 나날이 무언가를 배워나가는게 조금 즐겁다. 아직은 많이 부족하지만 개발자와 한 발짝 한 발짝 가까워져보자!!
화이팅!!