항해 9주차 회고 (WIL)

계리·2022년 11월 21일
0
post-thumbnail
post-custom-banner

항해 9주차 WIL List

실시간 채팅방 구현

  • Websocket

팀원들과 회의를 하면서 독서모임 Application에 모임 인원들끼리 소통할 수 있도록 실시간 채팅 기능이 필요 할 것 같다는 의견이 나와 자료들을 수집하여 간단한 실시간 채팅을 구현 해보았다.

실시간 채팅을 어떻게 구현할 것인가?

  • 실시간 채팅을 어떻게 구현해야 할지 고민을 하다가 실시간 채팅 ERD 설계를 해봐야겠다고 생각 했고 아래 그림과 같이 테이블 설계를 해보았다.

처음에는 이렇게 생각 했다.

  • 채팅 멤버가 가질 수 있는 채팅룸, 채팅 메세지는 여러개
  • 채팅룸이 가질 수 있는 채팅 멤버, 채팅 메세지 여러개
  • 채팅 메세지가 가질 수 있는 채팅룸 멤버, 채팅룸 여러개

채팅 멤버, 채팅룸, 채팅 메세지 이 3개의 테이블 모두가 M:N 관계가 맞을까...?
이렇게 고민을 하다가 자료를 찾아보았다. 실시간 채팅의 테이블 관계 자료들을 찾아보니 대부분의 자료들을 보면 4개의 테이블로 구성되어 있다.

한 개의 채팅룸 멤버 테이블을 별도로 생성 후 유저와 1:N, 채팅룸과 1:N 관계를 갖고 채팅룸 멤버들을 관리하고 나중에 멤버가 가지고 있는 여러 개의 채팅룸을 채팅룸 멤버 테이블에서 조회하여 가져올 수 있다.


WebSocket으로 실시간 채팅 구현

Websocket 이전에 HTTP부터 알아보기

HTTP(Hyper Text Transfer Protocol)란

'인터넷에서 데이터를 주고 받을 수 있는 프로토콜' 이며 프로토콜은 규약, 정해놓은 규칙으로 규칙을 통해서 개발을 하면 된다.
초기에는 HTML과 같은 하이퍼미디어 문서를 주로 전송했지만 최근에는 Planin text, JSON, XML 등 다양한 형태의 정보도 전송하는 애플리케이션 레이어 프로토콜이다.
또한 웹 브라우저와 웹 서버 간의 커뮤니케이션을 위해 디자인되었지만 최근에는 모바일 애플리케이션 및 IoT 등과의 커뮤니케이션과 같이 다른 목적으로도 사용되고 있다.

HTTP 특징

  • 단방향 통신(Simplex)
    • 클라이언트가 서버에 요청(request)을 보내면 서버가 응답(response) 해주는 클라이언트 - 서버 모델 방식이다.
  • HTTP는 무상태 프로토콜이며, 이는 서버가 두 요청 간에 어떠한 상태나 데이터를 유지하지 않음을 의미한다.(Cookie와 Session을 사용하여 상태를 유지) 일반적으로 안정적인 TCP/IP를 기반으로 사용한다.

Websocket이란

Transport Protocol의 일종으로 웹 버전의 TCP 또는 Socket이라고 생각하면 된다.
Websocket은 서버와 클라이언트 간에 Socket Connection을 유지해 실시간으로 양방향 통신이나 데이터 전송을 가능하도록 하는 것이다.


Websocket 특징

  • 양방향 통신(Full-Duplex)
    • 클라이언트와 서버가 동시에 통신하는 방식. 서로가 원할 때 데이터를 주고 받을 수 있다.
  • 실시간 네트워킹(Real-Time Networking)
    • 웹 환경에서 연속된 데이터를 빠르게 노출
      ex) 교환 플랫폼, 게임 App, 챗봇, 푸시 알림, SNS, 채팅 App, IoT App

실시간으로 데이터를 주고 받아야하는 실시간 채팅에는 Websocket이 적합하여 간단한 채팅 구현을 해보기로 하였다.


build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.5'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //websocket
    implementation 'org.springframework.boot:spring-boot-starter-websocket'



}

tasks.named('test') {
	useJUnitPlatform()
}

application.properties

#Websocket
spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=false
spring.freemarker.cache=false
spring.jackson.serialization.fail-on-empty-beans=false

Application

@SpringBootApplication
public class ChatApplication {

	public static void main(String[] args) {
		SpringApplication.run(ChatApplication.class, args);
	}

}

Websocket Handler

@Slf4j
@RequiredArgsConstructor
@Component

public class WebSockChatHandler extends TextWebSocketHandler {
    private final ObjectMapper objectMapper;
    private final ChatService chatService;


    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("payload {}", payload);

        ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
        ChatRoom room = chatService.findRoomById(chatMessage.getRoomId());
        room.handleActions(session, chatMessage, chatService);
    }
}
  • 웹소켓 클라이언트로부터 채팅 메시지를 전달받아 ChatMessage 객체로 변환
  • 전달받은 메시지에 담긴 채팅방 Id로 발송 대상 채팅방 정보를 조회함
  • 해당 채팅방에 입장해있는 모든 클라이언트들(Websocket session)에게 타입에 따른 메시지 발송

Websocket Config

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


    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");

    }

}
  • Websocket을 활성화하기 위한 Config 파일을 작성
  • @EnableWebSocket을 선언하여 Websocket을 활성화
  • Websocket에 접속하기 위한 endpoint는 /ws/chat으로 설정하고 도메인이 다른 서버에서도 접속 가능하도록 CORS : setAllowedOrigins(“*”)를 설정을 추가(클라이언트가 ws://localhost:8080/ws/chat으로 커넥션을 연결하고 메시지 통신을 할 수 있는 기본적인 준비가 끝)

ChatMessage

@Getter
@Setter
public class ChatMessage {
    public enum MessageType{
        ENTER, TALK
    }

    private MessageType type;
    private String roomId;
    private String sender;
    private String message;
}

enum을 이용하여 type으로 ENTER(채팅방 입장), TALK(대화하기) 두 가지 상태를 나타내고
채팅방 id(roomId), 메세지를 보낸 사람(sender), 보낸 메세지(message)로 DTO를 생성해준다.


ChatRoom

@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 handlerActions(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));
    }
}

채팅방에는 입장한 클라이언트의 정보를 가지고 있어야 하기 때문에 WebSocketSession 정보 리스트를 멤버 변수로 선언한다. 그리고 채팅방 id(roomId), 채팅방 이름(name)도 선언한다. 채팅방에 입장하기,대화하기는 handlerActions 메서드에서 ChatMessage 객체에 선언했던 ENTER, TALK 타입으로 분기를 나누어 타입에 맞게 메세지를 보낸다.


ChatService

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

chatRooms를 Map형태로 멤버 변수로 선언하여 RoomId를 key로 갖고 chatRoom을 value로 갖는다. createRoom() 메서드가 실행되면 새로운 채팅방이 생성되고 chatRooms에 chatRoom이 추가된다. 방의 아이디는 UUID로 랜덤으로 생성하여 지정되며, roomId를 사용해 findRoomById() 메서드를 통해서 해당 채팅방을 불러올 수 있다.

sendMessage() 메서드는 TALK 상태일 경우 실행되는 메서드로, 메시지를 해당 채팅방의 webSocket 세션에 보내는 메서드이다.


ChatController

@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();
    }
}

@RequestMapping 어노테이션을 통해서 “/chat” 주소로 Post요청이 들어오면 json 데이터에서 name값을 받아 해당 이름으로 된 채팅방을 생성하고, Get요청이 들어오면 현재 열려있는 모든 채팅방을 모두 조회 할 수 있게 해주었다.


테스트

해당 URL에서 채팅방을 생성하면 roomId를 결과 값으로 return하는 것을 확인 할 수 있다. 해당 roomId를 가지고 구글 확장 프로그램 WebSocketTestClient에서 채팅방 입장을 해본다.


Websocket연결할 때는 ws로 시작하고WebsocketConfig 클래스에서 ws/chat으로 URL을 설정해줬기 때문에 "ws://locahost:8080/ws/chat" 으로 URL을 Open 해주면 Status: OPENED으로 변경된 것을 확인할 수 있다.

Request에 JSON형태의 데이터를 보낸다.
{
"type" : "ENTER",
"roomId" : "6daba994-0ffe-4b42-a1c7-616b81d49298",
"sender" : "사용자_01",
"message" : ""
}

ENTER type으로 연결된 것을 확인할 수 있다. 같은 방식으로 사용자_02, 사용자_03도 채팅방에 참여를 해본다.


JSON형태의 Request에서 type을 TALK로 변경하고 message에 보낼 메세지를 입력해서 보내본다.



지금까지는 Websocket을 이용한 간단한 실시간채팅을 구현해보았다. 이제는

참고
https://surprisecomputer.tistory.com/54
https://duckdevelope.tistory.com/19
https://appmaster.io/ko/blog/websocketiran-mueosimyeo-eoddeohge-saengseonghabnigga
https://javaengine.tistory.com/entry/Spring-websocket-chatting-server1-%E2%80%93-basic-websocket-server
https://learnote-dev.com/java/Spring-%EA%B2%8C%EC%8B%9C%ED%8C%90-API-%EB%A7%8C%EB%93%A4%EA%B8%B0-webSocket%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85%EC%84%9C%EB%B2%84-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0/

profile
gyery
post-custom-banner

0개의 댓글