2021. 2. 4. 22:58ㆍ프로젝트/Salle(살래) 중고거래 웹
이번 글은 중고거래 웹사이트 프로젝트를 진행하면서 구현한 판매상품별 판매자 - 구매자 채팅기능(Chat Application)에 관한 글입니다. 레퍼런스는 당근마켓 입니다. 새로 배운 API나 서버 통신 개념이 많았고 채팅 기록을 불러오고 표현할 때 간단히 알고리즘을 사용하기도 했던 터라 총 3부로 나눠서 블로그에 업로드할 예정입니다.
1부: Chat Application 설계, 후기 & WebSocket 설명, Config 설정하기
2부: DB 설계 & 상품 페이지(productInfo)에서 채팅 시작하기
3부: 채팅 리스트(chatList) 만들기 + WIL(What I learned) 정리
Ⅰ. Chat Application 설계 & 스펙
1) Seller(판매자) - Buyer(구매자) 연결
중고거래 시 판매자와 구매자를 연결할 수 있는 소통 방식은 앱 내 채팅이나 댓글, 아니면 본문의 판매자 번호로 연락하는 것입니다. 프로젝트에선 구매자가 희망구매 상품을 보면 판매자에게 채팅으로 연락할 수 있는 웹 상에서 채팅 기능(Chat Application)을 만들기로 결정했습니다. 평소 당근마켓을 많이 이용하는데 앱 내 채팅을 이용하면 중고나라처럼 본문의 연락처를 찾아서 폰에 저장하고 앱을 닫고 연락하는 일없이 앱에서 all-in-one으로 해결이 되는 점이 편리하다고 느꼈습니다.
2) 기능구현에 걸린 시간
Chat Application을 코딩해보며 필요한 자료를 서칭해내는 능력이 말 중요하단 걸 여실히 깨달았습니다. 총 3주를 소요했고 실제 작업한 시간은 49시간이었습니다.(합산되지 않은 스트레스에 몸부린 친 시간 ++∞)
혼자 준비하다 보니 나태해지기 쉬워 10월부터 기록 중인 코딩/개발관련 투자 시간 데이터입니다. Google Spreadsheet에 간단한 함수식을 걸어 지금도 잘 사용하고 있습니다. 주 단위로 보는데, 안되는 날엔 지난 주들을 보며 자극을 받고 연속으로 잘 된 날에는 잘해왔구나라며 자신감이 유지되는 효과가 있습니다. 그리고 시간을 많이 투자하기보다 결과를 만들어내는 집중과 효율이 중요하다고 생각해 Gold Phrase를 하나 적어줬습니다. ㅋ Output is always more important than Input
3) 필요 기능 & 기술스펙
채팅의 목적은 메시지 전송과 텍스트 데이터 기록입니다. 채팅기록 저장, 불러오기, 특정 사용자 간 양방향 단일 전송이란 기본적인 기능을 구현하되, 1명의 멤버가 다수의 상품을 등록할 수도 있기 때문ㅐ에 통신 방식에 상품정보가 추가돼야 했습니다. 즉, Basic한 person to person에 product가 추가된 (seller + product) to buyer 이 대화방(ChatRoom)의 형태가 되었습니다.
프로젝트는 Spring MVC 패턴으로 설계되어 있습니다. 기술 스펙은 Java + Spring Boot + JSP + CSS + Javascript(jQuery)의 조합입니다.
Chat Application API는 Spring의 WebSockets + STOMP + sockJS를 사용했습니다. 자료들을 블로그도 많이 참고했지만 막바지에 발견한 Spring.io 발행 WebSocket Guide(개념/이론)와 Getting Started messaging-stomp-websocket(예제/실습코드)가 기본 개념을 잘 정리하고 실습 코드까지 제공하고 있어 Spring MVC패턴으로 Chat Application을 만드신다면 이 두 링크를 기본서로 삼으시면 됩니다.
▶ 참고자료 링크
WebSocket Guide(개념/이론)
Getting Started messaging-stomp-websocket(예제/실습코드)
Ⅱ. WebSockets 설명 & Configuration 코드
1) WebSockets 란?
정의나 개념 설명은 spring.io 사이트를 참고하였습니다. 위에 참고자료 링크 걸어뒀어요.
socket이라함은 양방향 통신/소통을 할 때 음성/텍스트/이미지 등의 데이터가 전송되는 도착지점(endpoint)입니다. 예를 들면 포트 넘버나 URL 같은 경로가 socket이 되어 TCP(Transmission Control Protocol)가 전달할 데이터를 알맞게 인지하도록 해줍니다.
그러니까 websockets는 WEB 상에서 서버나 유저들이 통신하는 데이터(메시지)가 도착하는 지점(point)들이라고 해석할 수 있을 것 같습니다. 인터넷이 없는 상황에선 서신(편지나 글따위)를 받는 주소가 곧 socket이 되겠죠? 조금 프로그래밍 지식을 덧붙이면 WebSocket은 서버와 유저 간 하나의 TCP(HTTP도 가능)로 연결하여, 표준화된 다자(=둘 이상) 간의 비동기(=동시다발적; real-time) 통신을 제공해줍니다. 그래서 나중에 WebSocket Config 코드를 보시면 endPoint, destination, broker과 같이 데이터를 처리할 지점을 지정하는 메서드들이 대부분임을 알 수 있습니다.
만약 비동기 방식 데이터 전달은 Ajax(+JSON), HTTP를 조합해도 가능할텐데 왜 굳이 Websocket을 쓰냐? 라고 물으신다면 ,데이터 양이 적을 때는(low volume) 상관이 없지만 카카오톡처럼 사용자 수천만명이 하루에 적게는 몇십에서 많게는 몇백개의 메세지를 보낼 경우 데이터 지연율이 적고(low latency) 끊김없이 연결성이 뛰어난(high frequency) WebSocket이 최선의 선택지가 되기 때문이죠.
2) STOMP란?
STOMP: Simple Text Oriented Message Protocol
대화방에선 참여한 대화상대 외엔 볼수도 없고 채팅이 금지되는 배타성을 가집니다. 다르게 얘기하면 대화방에 있는 특정 사용자에게만 메세지가 전달이 되어야 합니다. WebSocket은 low-level TCP로서 통신의 기본에만 충실한 나머지 지정 경로(particular destination/user)를 디테일 하게 특정하거나 전송 가능한 메시지 포맷, 내용(format, content)들을 알지 못합니다. STOMP가 바로 이를 서포트 해주는 기능이며 WebSocket의 sub-protocol 역할을 합니다.
HTTP를 통해 서버와 통신하기 때문에 STOMP와 sockJS는 Maven/Gradle로 dependency를 추가해주며 Javascript에서 src를 import해줘야 합니다. Javascript 코드가 쓰인 부분에 (제 경우 JSP <body></body> 내) 작성해주시면 API를 사용가능 합니다.
1
2
|
<script src="/webjars/stomp-websocket/2.3.3-1/stomp.js" type="text/javascript"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.js" type="text/javascript"></script>
|
cs |
3) SockJS란?
WebSocket이 서버와 통신이 끊기는 문제(HTTP Upgrade header에 할당되어 있지 않거나, 시간이 경과돼 닫힐 경우 등)가 발생할 때 임시적으로 기능을 대체하는 API(emulation) 입니다. 오류가 발생했을 때 발생 이전 상태로 되돌리거나 대체해주는 기능을 fallback이라고 합니다.
4) 코드
WebSocket을 사용하기 위해 설정해야하는 Config 코드입니다. 두 가지 메서드를 webSocketMessageBrokerConfigurer 클래스로부터 상속(override) 받습니다. configureMessageBroker는 유저가 메시지를 전송하거나 받을 수 있도록 중간에서 URL prefix(접두어)를 인식하여 올바르게 전송/전달(publish/subscribe)를 중계해주는 중개자(Broker) 역할을 합니다.
registerStompEndpoints는 메시지의 도착지점(endpoint)을 URL로 등록해주는 메서드입니다. 등록된 URL은 Controller의 @Messagemapping 어노테이션으로 할당해줘 SimpMessagingTemplate를 통해 약속된 경로나 유저에게 메시지를 전달해줍니다. 그리고 .withSockJS() 메서드는 fallback 기능을 하는 sockJS를 할당해줍니다.
마지막으로 heart-beat란 STOMP에서 TCP 연결이 잘 되어있는지 체킹하는 것인데, HTTP header를 통해 연결 상태를 주기적으로 확인합니다. setHeartbeatTime은 그 주기를 설정하는 메서드입니다. 참고로 java에서 언더바 [ _ ]를 int나 long에 천단위마다 사용하면 단위를 보기 쉽게 나타낼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package com.example.demo.application;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketBrokerConfig.class);
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//for subscribe prefix
registry.enableSimpleBroker("/user");
//for publish prefix
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/broadcast")
.withSockJS()
.setHeartbeatTime(60_000);
}
}
|
cs |
4) WebSocket messaging process
@EnableWebSocketMessageBroker 어노테이션으로 websocket 설정(config)을 완료했을 때 message flow는 아래와 같습니다. Request destination prefix에 따라 broker를 거치냐 마느냐가 정해집니다. /app으로 보냈을 땐 Controller의 @Messagemapping으로 받아서 처리해줄 수 있으며 /topic으로 보낼 경우 동일한 prefix로 subscribe한 유저에게 다이렉트로 메시지를 전달할 수 있습니다.
View - Javascript에서 STOMP를 설정하고 메시지를 전송하는 코드부분 입니다. 여기서 볼 것은, 7행에 들어가는 subscribe url이 '/user...'로 시작되는 것과 21행의 send는 '/app...'으로 시작한다는 것입니다. 우리는 send를 통해 메시지를 전송했을 때 broker를 거쳐 유저에게 전달이 된다는 것을 알 수 있습니다. 또한 메시지를 다이렉트로 전달 받는 경로는 subscribe로 설정한다는 것까지 알게되었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
function connect() {
var socket = new SockJS('/broadcast');
var url = '/user/' + id + '/queue/messages';
stompClient = Stomp.over(socket);
stompClient.connect({}, function() {
stompClient.subscribe(url, function(output) {
console.log("broadcastMessage working");
showBroadcastMessage(createTextNode(JSON.parse(output.body)));
});
//setConnected(true);
},
function (err) {
alert('error' + err);
});
};
function sendBroadcast(json) {
stompClient.send("/app/broadcast", {}, JSON.stringify(json));
}
|
cs |
Controller 클래스의 broker 역할을 하는 메서드 입니다. 자세한 코드 읽기는 다음 글에서 진행할 예정입니다.
1
2
3
4
5
6
7
8
9
10
11
|
@MessageMapping("/broadcast")
public void send(ChatRoom chatRoom) throws IOException {
chatRoom.setSendTime(TimeUtils.getCurrentTimeStamp());
//append message to txtFile
chatRoomService.appendMessage(chatRoom);
int id = chatRoom.getId();
String url = "/user/" + id + "/queue/messages";
simpMessage.convertAndSend(url, new ChatRoom(chatRoom.getContent(), chatRoom.getSenderName(), chatRoom.getSendTime()));
}
|
cs |
Ⅲ. 결론 및 정리
이번 글에선 Chat Application을 구현하기 위해 디자인 패턴을 설정하고 필요한 API들을 알아보았습니다. WebSocket API를 기반으로 메세지 전송기능 보완을 위한 STOMP와 연결이 끊겼을 때 임시 대체기능인 sockJS를 추가해 사용해주었습니다.
그리고 @EnableWebSocketMessageBroker 어노테이션으로 지정한 Webconfig 클래스에 broker, endpoint URL prefix를 할당해주면 어떻게 메세지가 전달되고 broker가 처리해주는지 그림과 함께 알아봤습니다.
ChatApplication은 총 3부로 구성되며, 다음 글에선 구현 코드 위주로 설명할 예정입니다. [2부 DB 설계 & MVC 코드 읽기 (1)]
좋아요와 질문/수정 댓글은 언제나 감사합니다 :)
'프로젝트 > Salle(살래) 중고거래 웹' 카테고리의 다른 글
9. 판매자-구매자 채팅 기능(Chat Application) (3) 완결 - MVC 코드 읽기 (2) & WIL(What I Learned) 정리 (0) | 2021.02.14 |
---|---|
9. 판매자-구매자 채팅 기능(Chat Application) (2) - DB 설계 & MVC 코드 읽기 (2) | 2021.02.09 |
8. 판매자 프로필 만들기 (0) | 2021.01.21 |
Error - 비정상적인 LoginSession 점유 (0) | 2021.01.12 |
7. 검색창 검색기능 (2) - Frontend 부분 (0) | 2021.01.11 |