15시간을 컴퓨터 앞에서 불태웠다..











WebSocket을 다른 프로젝트로 분리하기


이전 포스팅에 이어서 이야기 해보자.


WebSocket를 분리한 프로젝트는 아래 링크를 통해서 찾을 수 있다.


c


그리고 기존에 있던 프로젝트에서 WebSocket를 분리 해야 한다. 그리고 원래 Spring Security를 통한 Login 기능도 있지만 이를 제거했다.


Frames-App


위 프로젝트를 구성하면서 중요한 점들을 하나 하나 살펴보자.


먼저 Frames-Websocketport를 변경한다.


server.port = 8081


그 다음 FacilityController.java에서 SimpMessagingTemplate template를 선언 해주는 것이 필요하다. 이를 사용해 client에게 broadcasting를 할 것이기 때문이다. 아래 코드를 보자


FacilityController.java

package org.morgorithm.websocket.controller;


import org.morgorithm.websocket.dto.EventDTO;
import org.morgorithm.websocket.dto.RealTimeStatusDTO;
import org.morgorithm.websocket.service.StatusService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;


@Controller
public class FacilityController {
    @Autowired
    StatusService statusService;
    @Autowired
    SimpMessagingTemplate template;


    @MessageMapping("/status.sendMessage")
    @SendTo("/topic/public")
    @Scheduled(fixedDelay=3000)
    public void sendMessage() {
        RealTimeStatusDTO dto = statusService.getFacilityStatus();
        template.convertAndSend("/topic/public", dto);
    }
}


위 코드에서는 Scheduled 어노테이션으로 3초 마다 해당 메소드를 호출하게끔 한다. 그리고 앞서 선언한 template을 사용해 /topic/public를 구독하는 client에게 dto를 payload에 담아서 보내준다.


WebSocketConfig.java

package org.morgorithm.websocket.configuration;

import org.morgorithm.websocket.filter.ApiCheckFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

//CORS 문제를 해결하기 위해 설정한다.
@CrossOrigin(origins = "192.168.1.17:8081")
//이 어노테이션은 해당 클래스가 Bean의 설정을 할 것이라는 것을 나타낸다.
@Configuration
//WebSocket 서버를 활성화하는 데 사용한다.
@EnableWebSocketMessageBroker
//implements WebSocketMessageBrokerConfigurer
//웹 소켓 연결을 구성하기 위한 메서드를 구현하고 제공한다.
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    //클라이언트가 웹 소켓 서버에 연결하는 데 사용할 웹 소켓 엔드 포인트를 등록
    //엔드 포인트 구성에 withSockJS()를 사용한다.
    //webSocket은 통신 프로토콜 일뿐입니다.
    // 특정 주제를 구독한 사용자에게만 메시지를 보내는 방법 또는 특정 사용자에게 메시지를 보내는 방법과 같은 내용은 정의하지 않습니다.
    // 이러한 기능을 위해서는 STOMP가 필요하다.
    @Bean
    public ApiCheckFilter apiCheckFilter(){
        return new ApiCheckFilter();
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        System.out.println("end point 연결!!!***");
        registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS().setClientLibraryUrl("https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.min.js");
    }

    //한 클라이언트에서 다른 클라이언트로 메시지를 라우팅 하는 데 사용될 메시지 브로커를 구성한다.
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        System.out.println("confiureMessageBroker 연동!!**");
        //"/app" 시작되는 메시지가 message-handling methods으로 라우팅 되어야 한다는 것을 명시
        registry.setApplicationDestinationPrefixes("/app");
        //"/topic" 시작되는 메시지가 메시지 브로커로 라우팅 되도록 정의한다.
        //메시지 브로커는 특정 주제를 구독 한 연결된 모든 클라이언트에게 메시지를 broadcast한다.
        registry.enableSimpleBroker("/topic");
    }
}


그리고 WebSocketConfig.java에 보면 @CrossOrigin()라고 되어 있는 부분이 있다. 이는 CORS를 해결하기 위한 것이다. 나중에 이에 대해 더 자세히 다루겠다. 해당 ip 192.168.1.17은 쿠버네티스 클러스터에서 Frames-App를 외부에 노출 시킬 때 사용할 ip이다.


그리고 Frames-App의 핵심적인 부분을 살펴보자.


facilityStatus.js

(...)
function connect() {


    var socket = new SockJS('http://192.168.1.16/ws', null, {transports: ["xhr-streaming", "xhr-polling"]});

    chatPage.classList.remove('hidden');
    stompClient = Stomp.over(socket);
    console.log("connected3");
    stompClient.connect({}, onConnected, onError);

}
function onConnected() {
    console.log("onConnected() in facilitStatus.js 호출");
    stompClient.subscribe('/topic/public', onMessageReceived);
    console.log("after stompClient.subscribe");


    connectingElement.classList.add('hidden');
}
(...)


위 코드에서 핵심적인 것은 이제 sendMessage() 함수를 없애버리는 것과 위의 SockJS에서 특정 IP에 요청을 보내는 부분이다. IP 192.168.1.16은 쿠버네티스 클러스터에서 외부에 Frames-WebSocket 앱을 노출시킬 때 사용할 IP이다.

여기서 포트를 명시하지 않은 이유는 우리가 쿠버네티스 클러스터에서 Frames-WebSocket를 노출 시킬 때 해당 ip로 접근하게 되면 자동으로 8081 포트로 매핑될 수 있도록 명시할 것이기 때문이다


이제 이 두 프로젝트를 도커 허브에 저장한 다음 pod로 배포한다.


도커 허브에 저장하는 과정은 아래 링크를 참고한다.


SpringBoot 앱을 도커 이미지로 변환 후 Deployment로 배포하고 접근하는 방법


여기서 나는 Frames-Websocket은


docker build --tag youngkyonyou/springboot-project:websockettest .
docker push youngkyonyou/springboot-project:websockettest


으로 저장했고 Frames-App은


docker build --tag youngkyonyou/springboot-project:framesfinal .
docker push youngkyonyou/springboot-project:framesfinal


으로 저장을 했다.


이 이미지를 사용해서 deployment를 설정해 보자.


frames-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: frames-pods
  name: frames-pods
spec:
  replicas: 3
  selector:
    matchLabels:
      app: frames-pods
      tier: backend
  template:
    metadata:
      labels:
        app: frames-pods
        tier: backend
    spec:
      containers:
        - image: youngkyonyou/springboot-project:framesfinal
          name: kubernetes-spring

---
apiVersion: v1
kind: Service
metadata:
  name: frames-service
  labels:
    app: frames-pods
spec:
  selector:
    app: frames-pods
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  type: LoadBalancer
  loadBalancerIP: 192.168.1.17
  sessionAffinity: ClientIP


하나 하나 알아가자


frames-app.yaml

#apiVersion: API 버전을 명시한다
#이 오브젝트를 생성하기 위해 사용하고 있는 쿠버네티스 API 버전이 어떤 것인지
#명시한다. apps/v1에서는 .spec.selector 와 .metadata.labels 이
#설정되지 않으면 .spec.template.metadata.labels 은 기본 설정되지 않는다.
#그래서 이것들은 명시적으로 설정되어야 한다. 또한 apps/v1 에서는
#디플로이먼트를 생성한 후에는 .spec.selector 이 변경되지 않는 점을 참고한다.
apiVersion: apps/v1

#어떤 종류의 오브젝트를 생성하고자 하는지 명시한다.
kind: Deployment
metadata:
  #레이블을 설정한다. 나중에 이 레이블은 아래
  #서비스에서 서비스할 디플로이먼트를 지정하기 위해서 사용한다.
  labels:
    app: frames-pods

  #이름 문자열, UID, 그리고 선택적인 네임스페이스를 포함하여
  #오브젝트를 유일하게 구분지어 줄 데이터이다.
  #디플로이먼트 이름(name)은 frames-pods이다.
  #차후에 이 디플로이먼트를 delete할 때 이 이름으로 지운다.
  name: frames-pods
spec:
  #pod를 3개를 생성한다.
  replicas: 3

  #(.spec.selector)디플로이먼트가 관리할 파드를 찾는 방법을 정의한다.
  #이 사례에서는 파드 템플릿(아래 명시된 template)에 정의된 레이블(app:frames-pods)을 선택한다.
  selector:
    matchLabels:
      app: frames-pods
      tier: backend

  template:
    #파드는 .metadata.labels 필드를 사용해서
    #app: frames-pods레이블을 붙인다
    #바로 위의 selector.matchLabels.app이랑 동일하므로
    #이 컨테이너가 선택된다.
    metadata:
      labels:
        app: frames-pods
        tier: backend
    spec:
      containers:
        #해당 도커 허브에 저장된 이미지를 사용한다.
        - image: youngkyonyou/springboot-project:framesfinal
          name: kubernetes-spring

---
#apiVersion: API 버전을 명시한다
#이 오브젝트를 생성하기 위해 사용하고 있는 쿠버네티스 API 버전이 어떤 것인지
#명시한다.
apiVersion: v1

#어떤 종류의 오브젝트를 생성하고자 하는지 명시한다.
kind: Service

metadata:
  name: frames-service

  #여기서 위에서 deployment 관련 설정을 할때
  #.metadata.labels.app에 설정했던 이름과 동일시 해야
  #서비스가 서비스할 디플로이먼트를 찾는다.
  labels:
    app: frames-pods

spec:
  selector:
    app: frames-pods
  ports:
    #외부에서 80번 포트로 접근하는 것을 내부 8080으로 접근하게 해준다.
    - port: 80
      targetPort: 8080
      protocol: TCP

      #타입은 로드밸런서 타입으로 한다.
  type: LoadBalancer

  #로드밸런서의 고정아이피를 설정한다.
  loadBalancerIP: 192.168.1.17

  #특정 클라이언트의 연결이 매번 동일한 파드로 전달되도록 하려면
  #ervice.spec.sessionAffinity를 "ClientIP"로 설정하여
  #클라이언트의 IP 주소를 기반으로 세션 어피니티를 선택할 수 있다.
  sessionAffinity: ClientIP


세션 어피니티를 설정한 이유는 현재 앱이 Spring Security를 사용하고 있기 때문이다. 우리는 replica를 3개를 생성해서 배포하는데 이때 웹에 접속하고 다른 페이지에 접속하게 되면 로그인 세션이 유지가 안 될 수도 있다. 이는 세션 어피니티를 설정해줌으로써 해당 클라이언트가 특정 Pod로 지속적으로 연결되게 만들어주는 것이다.


websocket.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: websocket
  name: websocket
spec:
  replicas: 1
  selector:
    matchLabels:
      app: websocket
  template:
    metadata:
      labels:
        app: websocket
    spec:
      containers:
        - image: youngkyonyou/springboot-project:websockettest
          name: kubernetes-spring
---
apiVersion: v1
kind: Service
metadata:
  name: websocket-service
  labels:
    app: websocket
spec:
  selector:
    app: websocket
  ports:
    - port: 80
      targetPort: 8081
      protocol: TCP
  type: LoadBalancer
  loadBalancerIP: 192.168.1.16


설명은 위와 다른 게 없음으로 생략한다.

이제 다음 명령으로 deploymentservice를 배포한다.


kubectl deployment -f websocket.yaml
kubectl deployment -f frames.yaml


결과는 아래와 같다.




파드와 서비스가 만들어졌지만 실제로 내부적으로 웹이 배포되는 데까지 시간이 좀 걸린다.


이때 커피 한 잔을 하도록 한다.


충분히 기다리고 External IP로 접속해 보면 아래와 같이 결과가 나온다.



다음 게시물에서는 부하분산 테스트와 grafana 대시보드로 네트워크 트래픽 등을 보도록 하겠다.


YoungKyonYou

Integration of Knowledge