실전 프로젝트 14,15일차 회고

SaGo_MunGcci·2022년 9월 9일
0

실전 프로젝트

목록 보기
11/19

Today do list

⦁ Websocket 공부

⦁ Stomp 공부

⦁ 채팅 기본 예제 공부.

⦁ 경매 카테고리 별 인기순 조회

⦁ 경매 지역 별 인기순 조회



TIL

WebSocket 소개

WebSocket 프로토콜은 표준된 방법으로 서버-클라이언트 간에 단일 TCP 커넥션을 이용해서 양방향 통신을 제공한다.

특징
기존의 다른 TCP 기반의 프로토콜과 다르게, WebSocket은 HTTP 요청 기반으로 Handshake 과정을 거쳐 커넥션을 생성한다.

덕분에, 초기 WebSocket Handshake 요청은 추가적인 방화벽 설정없이 80, 443 포트를 사용하여 양방향 통신이 가능하다.

뿐만 아니라, HTTP 규격 그대로 유지할 수 있기 때문에 HTTP 인증, CORS 등을 동일하게 적용할 수 있다는 장점이 있다.

HTTP vs WebSocket

WebSocket이 HTTP 요청을 시작되는 호환성을 가지고 있지만, 분명하게 두 프로토콜은 다른 방식으로 동작한다.

HTTP는 여러 URL을 기반으로 서버 애플리케이션과 Request/Response 형식으로 상호 작용한다.

WebSocket은 반대로 오직 초기의 커넥션 수립을 위한 하나의 URL만 있고, 모든 애플리케이션 메시지는 동일한 TCP 커넥션에서 전달된다.

즉, WebSocket은 HTTP 프로토콜과 다른 asynchronous, event-driven, messaging 아키텍쳐 모델이다.

또한, HTTP 경우에는 서버가 URI, Method, Headers 정보로 적절한 핸들러로 라우팅해 처리할 수 있다.

하지만 WebSocket은 HTTP 와 다르게 메시지 내용에 의미를 두지 않기 때문에, 클라이언트-서버 간에 임의로 메시지에 의미를 부여하지 않으면 처리할 방법이 마땅히 없다.

이러한 문제를 STOMP 메시징 프로토콜을 통해서 해결할 수 있는데, 상위 프로토콜이 규정한 협약을 토대로 메시지를 처리할 수 있다.

Http Vs Websocket

polling : 일정주기마다 서버에 계속 요청 (이벤트가 없어도 요청 → 서버, 클라 부담)

long polling : 서버에 요청을 보내놓고 서버는 이벤트가 발생하면 응답, 다시 연결 (이벤트가 발생하면 연결된 모든 클라이언트에게 동시에 응답을 보내고 연결을 끊고, 새로 요청을 받는다 → 서버 부담)

Streaming : 서버에 연결 요청을 보내놓고 계속 응답 데이터를 다운받는다. 서버는 이벤트가 발생하면 응답을 보낸다 (클라이언트가 서버에 데이터를 보내기가 힘들다)

Websocket

위와 같은 HTTP Long Polling, Streaming 방식이 가지고 있는 문제를 해결하고, 서버-클라이언트 간에 양방향 통신이 가능하도록 WebSocket 이라는 기술이 만들어지게 됐다.

Http Vs Websocket 결론

WebSocket은 서비스를 동적으로 만들어 주지만, AJAX, HTTP Streaming, HTTP Long Polling 기술이 보다 효과적인 경우도 있다. 예를 들어, 변경 사항의 빈도가 자주 일어나지 않고 데이터의 크기가 작은 경우에는 AJAX, HTTP Streaming, HTTP Long Polling 기술이 효과적일 수 있다.

즉, 실시간성을 보장해야 하고 변경 사항의 빈도가 크다면 WebSocket은 좋은 해결책이 될 수 있다.

참고 : Spring MVC 3.2 Preview: Techniques for Real-time Updates

참고 : https://ws-pace.tistory.com/104

Stomp

STOMP : Simple Text Oriented Messaging Protocol

stomp는 메시지 전송을 효율적으로 하기 위한 프로토콜로, 기본적으로 PUB , SUB 구조로 되어있다. 따라서 메시지를 전송하고, 받아서 처리하는 부분이 확실하게 구조로 정해져있기 때문에 명확하게 인지하고 개발할 수 있다.

STOMP 프로토콜은 클라이언트/서버 간 전송할 메시지의 유형, 형식, 내용들을 정의한 규칙이다. TCP 또는 WebSocket과 같은 [양방향 네트워크 프로토콜 기반]으로 동작한다. 헤더에 값을 세팅할 수 있어서 헤더 값을 기반으로 통신 시 인증처리를 구현할 수 있다.

PUB / SUB 구조

pub/sub란 메시지를 공급하는 주체와 소비하는 주체를 분리해 제공하는 메시징 방법이다. 대표적인 예를 들어 설명해 보겠다.

우체통(Topic)이 있다면 집배원(Publisher)이 신문을 우체통에 배달하는 행위가 있고, 우체통에 신문이 배달되는 것을 기다렸다가 빼서 보는 구독자(Subscriber)의 행위가 있다. 이때 구독자는 다수가 될 수 있다.

즉, 채팅방을 생성하는 것은 우체통 Topic을 만드는 것이고 채팅방에 들어가는 것은 구독자로서 Subsciber가 되는 것이다. 채팅방에 글을 써서 보내는 행위는 우체통에 신문을 넣는 Publisher가 된다.

Message Brocker
이때 Message Brocker란 개념이 있는데, 이것은 [Publisher]로 부터 전달받은 메시지를 [Subscriber]에게 메시지를 주고 받게 해주는 중간 역활을 하는 것을 말한다.

클라이언트는 SEND, SUBSCRIBE 명령을 통해서 메시지의 내용과 수신 대상을 설명하는 "destination" 헤더와 함께 메시지에 대한 전송이나 구독을 할 수 있다. 이것이 브로커를 통해 연결된 다른 클라이언트로 메시지를 보내거나, 서버로 메시지로 보내 일부 작업을 요청할 수 있는 PUB/SUB 메커니즘을 가능하게 한다.

스프링이 지원하는 STOMP에서는 스프링 웹 소켓 애플리케이션이 클라이언트에게 STOMP 브로커의 역활을 한다. 이때 메시지는 @Controller 메시지 처리 방법이나, Subscriber를 추적해서 구독중인 사용자에게 메시지를 전파(Broadcast)하는 Simple In Momory 브로커에게 라우팅 된다.

이렇게 Spring 환경에서 추가적인 설정없이 STOMP 프로토콜을 사용하면 메시지 브로커는 자동으로 In Memory Brocker를 사용하게 된다.

문제는 이러한 기본 In Memory Brocker를 사용해도 상관이 없을까이다. 결론은 상관이 있다. 몇가지 단점들을 보면

  1. 세션을 수용할 수 있는 크기가 제한되어 있다.
  2. 장애 발생 시 메세지의 유실 가능성이 높다.
  3. 따로 모니터링 하는것이 불편하다.

따라서, In Memory 브로커 대신에 STOMP 전용 외부 브로커를 사용하는것을 지향한다.

외부 브로커로는 RabbitMQ, ActiveMQ 등이 있으며 Message Brocker 기능을 제공한다. (일반적으로 초당 5000 ~ 수만번의 메시지가 발생되는 규모에서 사용한다고 한다)

참고 : https://ws-pace.tistory.com/106

채팅 기본 예제

  • ws(웹소켓)에 접근할때는 http통신으로 접근한다.

@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

// @EnableWebSocket을 통해 webSocket을 활성화 해주고,
// WebSocektConfigurer 인터페이스를 적용하여 WebSocketConfig라는 클래스를 만들어 이를 설정해준다.
// 우리가 정보를 처리할 Handler와 webSocket 주소를 WebSocketHandlerRegistry에 추가해주면,
// 해당 주소로 접근하면 웹소켓 연결을 할 수가 있다.
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

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

    }

}

@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
    
   // @RequestMapping 어노테이션을 통해서 “/chat” 주소로 Post요청이 들어오면
   // json 데이터에서 name값을 받아 해당 이름으로 된 채팅방을 생성하고,
   // Get요청이 들어오면 현재 열려있는 모든 채팅방을 모두 조회 할 수 있게 해줌.    
    
    private final ChatService chatService;

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

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

@Slf4j
@RequiredArgsConstructor
@Service
public class ChatService {

// ChatService는 chatRooms를 클래스 변수로 갖는데,
// chatRooms는 RoomId를 key로 갖고 chatRoom을 value로 갖는 Map으로,
// createRoom() 메서드가 실행되면 새로 방이 만들어지고 chatRooms에 chatRoom이 추가된다.
// 방의 아이디는 UUID로 랜덤으로 생성하여 지정되며,
// roomId를 사용해 findRoomById() 메서드를 통해서 해당 채팅방을 불러올 수 있다.
//
// sendMessage() 메서드는 TALK 상태일 경우 실행되는 메서드로,
// 메시지를 해당 채팅방의 webSocket 세션에 보내는 메서드이다.

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

        }
    }
}
// 위의 WebSocketHandler에서 사용했던 ChatMessage와 ChatRoom 클래스를 만들어
// json데이터로 받아온 정보를 통해 해당 채팅방으로 채팅 메시지를 보낼 수 있도록
// 해당 json에 맞는 DTO들을 만듬.
@Getter
@Setter
public class ChatMessage {
    
// 간단하게 사용자가 ENTER, 처음 채팅방에 들어오는 상태인지 TALK,
// 이미 session에 연결되어있고 채팅중인 상태인지를 파악하기 위해 ENUM 타입으로 Message 타입을 선언해주고,
// 이를 type으로 갖게 하도록한다. 그 이외에는 간단하게, 이 메시지를 보낼 채팅방 id인 roomId와,
// 보내는 사람의 닉네임인 sender, 메시지를 담는 변수 message를 만들어,
// json 데이터로 채팅 데이터가 들어오면 이를 ChatMessage로 변환해 줄 수 있도록하자

    public enum MessageType{
        ENTER, TALK
    }

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


}

// 스프링은 Text 타입과 Binary타입의 핸들러를 지원하는데,
// 우리는 채팅 서비스를 만들 것 이므로 TextWebSocketHandler를 상속하여 생성한 WebSocketHandler
@Slf4j
@RequiredArgsConstructor
@Component
public class WebSocketHandler extends TextWebSocketHandler {

    // ObjectMapper json파싱해주는 클래스
    // ObjectMapper provides functionality for reading and writing JSON,
    // either to and from basic POJOs (Plain Old Java Objects),
    // or to and from a general-purpose JSON Tree Model (JsonNode),
    // as well as related functionality for performing conversions.

    private final ObjectMapper objectMapper;
    private final ChatService chatService;


// 우리가 메세지를 json형식을 통해서 웹소켓을 통해 서버로 보내면,
// Handler는 이를받아 ObjectMapper를 통해서 해당 json 데이터를 chatMessage.class에 맞게 파싱하여
// ChatMessage 객체로 변환하고,
// json 데이터에 들어있는 roomId를 이용해서 해당 채팅방을 찾아
// handlerAction() 이라는 메서드를 실행시킬 것이다.
// 그러면 handlerAction() 메서드는 이 참여자가 현재 이미 채팅방에 접속된 상태인지,
// 아니면 이미 채팅에 참여해있는 상태인지를 판별하여,
// 만약 채팅방에 처음 참여하는거라면 session을 연결해줌과 동시에 메시지를 보낼것이고
// 아니라면 메시지를 해당 채팅방으로 보내게 될 것이다.

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message)
            throws JsonProcessingException {

        String payload = message.getPayload();
        log.info("{}", payload);
        ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);

        ChatRoom chatRoom = chatService.findRoomById(chatMessage.getRoomId());
        chatRoom.handlerAction(session, chatMessage, chatService);

    }

}

@Getter
public class ChatRoom {


// ChatRoom은 roomId와 name, 그리고 session을 관리할 sessions를 갖는다.
// json 데이터를 받아 WebSocketHandler에서 해당 데이터에 담긴 roomId를 chatService를 통해서
// 조회 하여 해당 id의 채팅방을 찾아 json데이터에 담긴 메시지를 해당 채팅방으로 보내게된다.
// 이 기능을 담당하는 곳이 handlerActions 메서드로,
// 해당 roomId의 채팅방을 찾아 handlerActions로 메시지와 세션을 보내면
// 이 메시지의 상태가 ENTER 상태인지 TALK 상태인지 판별하여
// 만약 ENTER 상태라면 session을 연결한뒤에 해당 sender가 입장했다는 메시지를 해당 채팅방에 보내고,
// 만약 이미 연결된 TALK 상태라면 해당 메시지를 해당 채팅방에 입장해있는 모든 클라이언트들 (Websocket session)
// 에게 메시지를 보낸다.

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


}

참고 : 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/

참고 : https://stackoverflow.com/questions/68279167/stomp-websocket-architecture-in-spring-with-decoupling-between-client-and-broker



Tommorrow do list

  • 추석으로 인한 큰집 방문


profile
이리저리 생각만 많은 사고뭉치입니다.

0개의 댓글