본문 바로가기

프로젝트/프로젝트 예제

채팅 서버 만들기 1. 웹 소켓으로 채팅 서버 만들기

현재 진행중인 프로젝트에 들어갈 채팅 서버를 만들어야 한다.

웹소켓으로 간단하게 구현하는 1:1 채팅 서버는 몇번 본 적 있는데 수 많은 사람들이 사용하는 채팅 서버는 어떻게 구성됐는지, 어떤 기술이 필요한지 잘 몰랐다. 이번 연재 시리즈를 통해 알아가 보도록 하겠다.

 

 

 

먼저, 웹소켓

웹소켓은 http의 stateless함을 보완하기 위해 나온 기술이며 이를 보완하기 위해 등장했다. http스펙 상에서 동작한다.

 

 

일단 프로젝트 생성

 

 

 

 

 

바로 본론으로 들어와서, 고객의 요청을 처리해 줄 핸들러를 작성한다.

일반적으로 스프링에서 핸들러는 프론트 컨트롤러(서블릿)가 정한 고객의 요청을 처리해주는 거라고 보면 된다.

 

// 많은 종류의 WebSocketHandler중에 TexxtWebSocketHandler를 상속받고 component를 이용해 빈에 등록, 이후 config를 통해 핸들러로써 등록해야함.
@Slf4j
@Component
public class WebSocketChatHandler extends TextWebSocketHandler {

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("payload {}", payload);
        TextMessage textMessage = new TextMessage("Welcome chatting sever~^^");
        session.sendMessage(textMessage);
    }
}

 

 

 

이후 config에서 핸들러 레지스트리에 웹소켓핸들러를 등록해준다. 이렇게하면 ws: ~ /ws/chat으로 들어오는 request에 대한 정보를 핸들러를 통해 session과 message로 맵핑해서 받아올 수 있다.

@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSockConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
    }
}

 

 

구글의 플러그인을 이용해서 간단하게 ws 통신을 해볼 수 있다.

https://chrome.google.com/webstore/detail/simple-websocket-client/gobngblklhkgmjhbpbdlkglbhhlafjnh/related

 

Simple WebSocket Client

Construct custom Web Socket requests and handle responses to directly test your Web Socket services.

chrome.google.com

 

 

 

근데 이렇게하면 사실 그냥 1:1로 연결해서 메시지를 보내는 수준밖에 안된다. 채팅기능과는 거리가 멂..

여러명의 유저가 존재하고, 그 유저들이 여러 톡방중 골라 들어가는 기능을 개발할 필요가 있다.

그러기 위해서,,  1. 메시지 관련 DTO 2. 메시지 서비스 3. 채팅방 DTO 4. Controller를 만들어 줄 필요가 있다.

 

 

1. 메시지 DTO

@Getter
@Setter
public class ChatMessage {
    // 메시지 타입 : 입장, 채팅
    public enum MessageType {
        ENTER, TALK
    }
    private MessageType type; // 메시지 타입
    private String roomId; // 방번호
    private String sender; // 메시지 보낸사람
    private String message; // 메시지
}

 

2. 메시지 서비스

@Slf4j
@RequiredArgsConstructor
@Service
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);
        }
    }
}

 

3. 채팅방 DTO

@Getter
public class ChatRoom {
    private String roomId;
    private String name;
    private Set<WebSocketSession> sessions = new HashSet<>();

    @Builder
    public ChatRoom(String roomId, String name) {
        this.roomId = roomId;
        this.name = name;
    }

    public void handleActions(WebSocketSession session, ChatMessage chatMessage, ChatService chatService) {
        // 메시지 타입이 enter면 입장
        if (chatMessage.getType().equals(ChatMessage.MessageType.ENTER)) {
            //sessions 라는 변수에 저장... (동시성 이슈가 있을것 같다)
            sessions.add(session);
            chatMessage.setMessage(chatMessage.getSender() + "님이 입장했습니다.");
        }
        // 메시지 타입이 enter가 아니면, sendMessage
        sendMessage(chatMessage, chatService);
    }

    public <T> void sendMessage(T message, ChatService chatService) {
        //받아온 sessions에서 session을 하나씩 가져와서 사용자가 작성한 메시지를 보낸다.
        sessions.parallelStream().forEach(session -> chatService.sendMessage(session, message));
    }
}

 

4. 컨트롤러

 

@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {

    private final ChatService chatService;

    @PostMapping
    public ChatRoom createRoom(@RequestParam String name) {
        return chatService.createRoom(name);
    }

    @GetMapping
    public List<ChatRoom> findAllRoom() {
        return chatService.findAllRoom();
    }
}