15시간을 컴퓨터 앞에서 불태웠다..
WebSocket을 다른 프로젝트로 분리하기
이전 포스팅에 이어서 이야기 해보자.
WebSocket를 분리한 프로젝트는 아래 링크를 통해서 찾을 수 있다.
그리고 기존에 있던 프로젝트에서 WebSocket를 분리 해야 한다. 그리고 원래 Spring Security를 통한 Login 기능도 있지만 이를 제거했다.
위 프로젝트를 구성하면서 중요한 점들을 하나 하나 살펴보자.
먼저 Frames-Websocket의 port를 변경한다.
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이다.
이제 이 두 프로젝트를 도커 허브에 저장한 다음 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
설명은 위와 다른 게 없음으로 생략한다.
이제 다음 명령으로 deployment와 service를 배포한다.
kubectl deployment -f websocket.yaml
kubectl deployment -f frames.yaml
결과는 아래와 같다.
파드와 서비스가 만들어졌지만 실제로 내부적으로 웹이 배포되는 데까지 시간이 좀 걸린다.
이때 커피 한 잔을 하도록 한다.
충분히 기다리고 External IP로 접속해 보면 아래와 같이 결과가 나온다.
다음 게시물에서는 부하분산 테스트와 grafana 대시보드로 네트워크 트래픽 등을 보도록 하겠다.
- Kubernetes (18) ,
- Docker (4) ,
- SpringBoot (45) ,
- Docker Hub (3) ,
- WebSocket (4) ,
- Prometheus (5) ,
- Grafana (2) ,
- Deployment (2) ,
- ReplicaSet (2)