[SpringBoot(5)] 채팅 기능 구현

배지원·2022년 12월 19일
0

실습

목록 보기
24/24
post-custom-banner

이번시간에는 간단한 채팅 기능을 구현해보도록 하겠다.
채팅을 구현하기 위해서는 클라이언트와 서버가 실시간으로 데이터를 주고 받아야한다. 따라서 그 기능을 구현하기 위해 대표적으로 폴링, SSE, 소켓 방식을 사용한다.

1. 정의

(1) 폴링

  • 요청과 응답, 연결 해제를 계속해서 반복해가는 방식이다.
  • 클라이언트는 서버에게 계속해서 Request를 보내어 이벤트를 응답을 받는다.
  • 서버는 클라이언트에게 Request를 받는 즉시 Response를 해주고 연결을 끊는다.
  • 클라이언트가 계속해서 Request를 서버에 보내기 때문에 서버가 과부화가 걸리기 쉬움
  • 처리 속도가 느림
  • 이를 보완하기 위해 Long Polling이 생김

Long Polling

  • 클라이언트가 서버에게 Request를 하면 이벤트가 발생하기 전까지 기달리다가 이벤트가 발생하면 Response를 보내고 연결을 끊는다.
  • 기존 폴링 방식보다는 서버 부담이 덜 하지만, 이벤트 발생 간격이 좁다면 차이가 없다.


(2) SSE

  • 폴링 방식과 동일하게 클라이언트와 서버간 통신과정을 실시간으로 구현할 수 있는 방식이다.
  • 요청과 응답을 반복하되 연결 해제는 한번만 진행함
  • 한번 Request를 받으면 브라우저가 종료되기 전까지 이벤트가 발생할때마다 서버에서만 Response를 하는 서버 -> 클라이언트인 단방향 처리이다.
  • Request가 한번이므로 서버에 과부하도 적기 때문에 속도가 폴링보다 빠르다.


(3) 웹 소켓

  • 클라이언트와 서버 사이의 동적인 양방향 연결 채널을 구성한다.
  • WebSocket API를 통해 Request없이 Response를 받아올 수 있다.
  • 폴링, SSE보다 빠르고 많은 수의 동시 접속자를 수용할 수 있다.


2. 실습

(1) 구조

(1.1) 메세지들을 그리는 구조(폴링방식)


1. 채팅방 입장하면 클라이언트는 서버에게 Request를 보냄
2. 서버는 클라이언트에게 현재 저장되어 있는 전체 메세지를 Response해주고 연결해제함(채팅방 보면 이전 데이터를 출력해줘야 하기 때문에)
3. 클라이언트에서는 현재 메세지 ID번호를 서버에게 Request함
4. 서버에서는 해당 ID를 기준으로 ID다음의 번호를 가진 메세지만을 Response함(만약 ID가 없다면 전체 출력)
예) 현재 채팅방에 3개의 메세지가 존재한 상태에서 유저가 채팅방에 접속하면 3개의 메세지를 전체 응답받고나서 클라이언트는 ID=4번인 메세지를 다시 요청한다. 만약 그 사이에 다른 유저가 채팅을 작성하여 서버에 메세지가 6개가 저장되어 있다고 한다면 서버는 ID가 4번다음인 값 즉, ID=5, ID=6번인 데이터를 반환해준다.
5. 유저가 브라우저를 닫기 전까지 3~4번 과정을 반복함


(1.2) 메세지들을 그리는 구조(폴링방식)


1. 클라이언트가 채팅 메세지를 작성하면 서버로 해당 데이터를 전송함
2. 서버에서는 해당 데이터를 DB에 저장한다.
3. 브라우저를 닫기 전까지 반복함


(2) 설계

(2.1) BackEnd

1. Response Class

@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Response<T>{
    private String resultCode;
    private T resultData;

    public static <T> Response<T> success(T resultData){
        return new Response<>("Success", resultData);
    }
    
    public static Response<Void> error(String resultCode){
    	return new Response<>(resultCode,null);
    }
}
  • 반환값이 정상인지 오류인지 확인하기 위한 Response
  • 정상일때는 ResponsDTO를 success로 감싸서 유저에게 반환
  • 오류일때는 ResponsDTO를 error로 감싸서 유저에게 반환

2. ChatMessage Class

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class ChatMessage {
    private Long id;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
    private LocalDateTime localDateTime;
    private String authorName;
    private String content;

    public ChatMessage(String authorName, String content){
        this(ChatMessageIdGenerator.genNextId(),LocalDateTime.now(),authorName,content);
    }
}

class ChatMessageIdGenerator {
    private static long id = 0;

    public static long genNextId() {
        return ++id;
    }
}
  • Entity 역할을 하는 클래스이지만 현재 실습에서는 간단하게 채팅기능 구현을 할 것이기 때문에 DB와 연동하지는 않음. 따라서, @Entity는 없음
  • 데이터를 저장할때 Id,Date,Name,Content를 저장한다.
  • 이때 시간은 유저가 작성한 시간을 바로 저장할 수 있도록
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
       private LocalDateTime localDateTime;
    을 사용했고 ID의 값은 DB와 연동하지 않았으므로 임으로 호출할때마다 +1로 카운트를 해주었다.

3. DTO Class

@AllArgsConstructor
@Getter
@NoArgsConstructor

public class ChatRequest {
    private String authorName;
    private String content;
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class ChatResponse {
    private LocalDateTime localDateTime;
    private String authorName;
    private String content;
}
  • 유저에게 입력받을 Request DTO와 반환해줄때 필요한 Response DTO이다.

4. ChatController Class

@Controller
@RequestMapping("/chat")
@Slf4j
public class ChatController {

    private List<ChatMessage> chatMessage = new ArrayList<>();

    /* jdk 16버전 부터 record라는 것을 지원한다. 이전에는 request와 response를 따로 클래스를 만들어서
    @AllArgsConstructor,@Getter 어노테이션을 사용하여 DTO 클래스를 만들었었는데 record는 이 모든것을 가지고 있는 클래스이다.
    즉, record를 사용하면 아래처럼 바로 사용이 가능해진다. 생성자를 통해 값만 입력받는 경우 DTO를 만들필요가 없다.

    public record WriteMessageRequest(String authorName, String content) {
    }

    public record WriteMessageResponse(long id) {
    }

     */

    
    // (1) 유저에게 값을 입력받아 리스트에 저장후 결과 반환
    @PostMapping("/writeMessage")
    @ResponseBody
    public Response<List<ChatMessage>> writeMessage(@RequestBody ChatRequest chatRequest){
        ChatMessage cm = new ChatMessage(chatRequest.getAuthorName(),chatRequest.getContent());
        chatMessage.add(cm);
        log.info(chatRequest.getAuthorName());
        return Response.success(chatMessage);
    }
    
    // (2) 특정 게시물 번호 이후 게시물만 출력
    @GetMapping("/messages/{id}")
    @ResponseBody
    public Response<List<ChatMessage>> findmessage(@PathVariable Long id){
        List<ChatMessage> messages = chatMessage;

        // 번호가 입력되었다면
        if (id != null) {
            // 해당 번호의 채팅메세지가 전체 리스트에서의 배열인덱스 번호를 구한다.
            // 없다면 -1
            int index = IntStream.range(0, messages.size())     // 리스트에 저장된 데이터만큼 반복 IntStream.range() => for문 처럼 반복
                    .filter(i -> chatMessage.get(i).getId() == id)  // filter() => if문 처럼 조건문 동작
                    .findFirst()            // filter조건에 true인 값을 리턴함 즉, index에 true값 저장
                    .orElse(-1);        // 조건에 해당하지 않다면 -1을 반환함 즉, index에 -1넣음

            if (index != -1) {
                // 만약에 index가 있다면, 0번 부터 index 번 까지 제거한 리스트를 만든다.
                // index가 1이고 리스트에 저장되어 있는 데이터가 3개일때 subList(2,3) 사이의 값이 출력된다. 즉, 2번째 3번째 값 messages에 덮어씌우기 (subList는 1부터 시작)
                messages = messages.subList(index + 1, messages.size());
            }
        }

        return Response.success(messages);
    }

}
  • DB와 연동이되지 않았기 때문에 리스트에 저장하도록 한다.
  • (1) 유저에게 값을 받으면 해당 값을 list에 저장하고 list에 저장된 값을 전부 유저에게 반환해준다.
  • (2) 실시간으로 채팅을 최신화 해야하므로 현재까지 출력된 ID를 서버로 보내고 해당 ID와 메세지가 저장되어 있는 list의 ID와 비교해 존재한다면 해당 ID를 추출하여 그 이상의 ID를 가진 데이터를 유저에게 반환한다.

❓ record 클래스란?

  • jdk 16버전부터 새로 생긴 클래스로 이전에는 DTO 클래스 안에 @AllArgsConstructor, @Getter 어노테이션을 사용하여 아래와 같이
@AllArgsConstructor
@Getter
public class ChatRequest {
    private String authorName;
    private String content;
}

데이터를 통신할 수 있는 구조로 만들었는데 record가 이 모든 역할을 대신해준다. 따라서 위의 코드를 아래의 코드로 줄일 수 있다.

public record ChatRequest(String authorName, String content) {
    }

(2.2) FrontEnd

1. HTML

<div class="chat">
  <form action="https://www.naver.com" target="_blank" onsubmit="Chat__submitWriteMessageForm(this); return false;">
    <input name="authorName" type="text" placeholder="작성자">
    <input name="content" type="text" placeholder="내용">
    <input type="submit" value="작성">
  </form>
</div>

<hr>

<a href="https://www.naver.com" target="_blank" onclick="return false;">네이버로 이동</a>
  • 사용자에게 작성자, 내용을 입력받도록 input사용함
  • 이때 submit버튼 클릭시 Chat__submitWriteMessageForm(this) 함수가 동작하도록 구현함( JS에서 input안에 있는 데이터를 서버로 보내주기 위함)
  • onsubmit = return false를 통해 버튼만 눌러도 action의 값을 수행하지 않음

2. JS

console.clear();

// 유틸리티 시작
function fetchPost(url, data) {
  return fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json"
    },
    body: JSON.stringify(data)
  }).then((response) => response.json());
}

function fetchGet(url, data) {
  let query = Object.keys(data)
    .map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(data[k]))
    .join("&");

  return fetch(url + "?" + query, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json"
    }
  }).then((response) => response.json());
}
// 유틸리티 끝

// 비지니스 시작
// html에서 submit버튼 클릭시 동작
function Chat__submitWriteMessageForm(form) {
  // console.log("submit 실행됨");

  // 입력란에 좌우에 존재하는 공백제거
  form.authorName.value = form.authorName.value.trim();

  // 작성자 이름이 공백이라면 alert 출력
  if (form.authorName.value.length == 0) {
    form.authorName.focus();
    alert("작성자를 입력해주세요");

    return;
  }

  form.content.value = form.content.value.trim();

  // 내용이 공백이라면 alert 출력
  if (form.content.value.length == 0) {
    form.content.focus();
    alert("내용을 입력해주세요");
    
    return;
  }

  // html에서 작성한 이름과, 내용의 데이터를 서버 api주소로 보낸다. 이때, 비동기식 방식으로 반환값은 없음
 	 fetchPost("http://localhost:8020/chat/writeMessage", {
    authorName: form.authorName.value,
    content: form.content.value
  }).then((data) => console.log(data));


  form.content.value = ""; // 내용칸 입력한 내용 비우기
}

// 비지니스 끝
  • ajax 방식 사용하여 Front와 Back 통신함

(1) fetchPost 함수 : Post 형식으로 반환해주는 코드로 HTML에서 input을 통해 입력한 데이터를 서버 API 주소로 JSON형식으로 보내준다.

(2) fetchGet 함수 :

(3) 비즈니스 Chat__submitWriteMessageForm 함수 : HTML에서 submit버튼 클릭시 동작하는 함수로 작성자와 내용을 입력하는 공간의 공백여부를 조사하고 모두 true라면 fetchPost함수를 호출하여 서버 api주소로 해당 데이터를 보내준다.

profile
Web Developer
post-custom-banner

0개의 댓글