1. 목적

이전 포스팅에서는 자바에서 Python 파일을 직접 실행하여 한 줄 씩 결과 값을 Log로 출력하는 것을 해보았다. 이번에는 서버에서 출력하는 값을 실시간으로 클라이언트로 가져와 뷰에서 값이 출력하는 것을 해볼 것이다.

Websocket 통신으로 SockJs와 Stomp를 사용하였다.

2. Websocket 통신 - SockJS와 Stomp

WebSocket

실시간 양방향 통신을 위한 프로토콜. 서버와 클라이언트 간 웹 기반의 애플리케이션에서 데이터를 전송하기 위해 사용된다.

SockJS

WebSocket의 대안으로 사용되는 JavaScript 라이브러리. 브라우저 웹 서버 사이 짧은 지연시간과 크로스 브라우징을 지원하며 Websocket이 지원안되는 최신 브라우저에서도 잘 작동되도록 해준다.(Fallback)

STOMP

간단한 텍스트 기반 메시징 프로토콜. 중개자 역할을 하는 서버와 클라이언트 간의 표준 메시징 프로토콜이다.

나는 좀더 빠르고 가벼운 SockJs를 Stomp 를 함께 사용하기로 하였다.

3. 필요한 라이브러리 및 의존성 설정

1) 클라이언트 - Javascript 라이브러리

HTML 파일에 SockJS 클라이언트 라이브러리와 stomp 라이브러리를 추가한다. CDN이나 npm 등 원하는 방식대로 설치 후 추가. (편의를 위해 jQuery도 같이 추가하였다.)

<!-- jquery--> 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!--sockJs-->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<!--stomp-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

2) 서버 - Maven 의존성 설정

  • spring boot 인 경우
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  • spring MVC 인 경우
<dependency>
	<groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>${spring.maven.artifact.version}</version>
</dependency>

4. Websocket 구성을 위한 설정 클래스 생성

WebSocketConfig.java라는 새로운 클래스를 생성하여 WebSocket 구성을 수행한다.

@Configuration
@EnableWebSocketMessageBroker
@Slf4j
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp").setAllowedOriginPatterns("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

registerStompEndpoints

  • 역할: WebSocket 연결 엔드포인트를 등록하는 역할
    클라이언트는 이 엔드포인트를 통해 WebSocket 연결을 시도할 수 있다.

configureMessageBroker

  • enableSimpleBroker
    역할: 메시지 브로커를 활성화하고, 클라이언트로부터의 메시지를 구독 및 전파할 수 있도록 설정한다. /topic 은 클라이언트가 구독할 경로로 사용된다.
  • setApplicationDestinationPrefixes
    클라이언트로부터 수신된 메시지의 목적지 경로(prefix)를 설정한다.
    클라이언트가 /app/hello 와 같은 경로로 메시지를 전송하면 서버에서는 /hello 경로로 해당 메시지를 처리한다.

5. STOMP 프로토콜을 통한 메시지 전송과 수신 예제

1) 서버 - Controller

@MessageMapping("/start")
public void start(String filePath) throws Exception {
	new ProcessExecutor().command("python", filePath)
    	.redirectOutput(new LogOutputStream() {
        	@Override
            protected void processLine(String line) {
            	messagingTemplate.convertAndSend("/topic/progress", line);
            }
    }).execute();
}
  • 클라이언트에서 메시지를 전달 받으면(/app/start) 해당 메서드가 실행된다.
  • python 파일이 실행되며 출력되는 라인은 구독한 서버(/topic/process)에 추가하고 있다.
  • 코드는 이전 포스팅에서 작성한 것으로, python 파일을 실행하여 출력 값을 한 줄씩 가져온다.

2) 클라이언트

  • [파일 실행하기] 버튼 클릭 이벤트가 발생했을 때, 파일이 실행되도록 하였다.
  • connectStomp 함수에서 sockjs & stomp 연결을 시작한다.
<button type="button" id="start">파일 실행하기</button>
<!--라이브러리 추가 코드 생략-->
<script>
    let socket = null;
    let stomp = null;
    
    $(document).ready(()=>{
        connectStomp();
    })

    $("#start").on("click", ()=>{
        // 파이썬 파일 실행
        stomp.send("/app/start", {}, "FILE_PATH")
    })

    function connectStomp() {
        socket = new SockJS("/stomp"); // endpoint
        stomp = Stomp.over(socket);

        stomp.connect({}, function () {
            console.log("Connected stomp!");

            // 파일 정보 및 진행률이 전달되는 토픽 구독
            stomp.subscribe('/topic/progress', function (event) {
                console.log(event.body)
            });
        });
    }
</script>

(gif는 event 자체를 출력하고 있지만 event.body를 출력해야하는 것이 맞다..)
디버그 메시지를 출력되지 않도록 하고 싶다면 해당 옵션을 추가하면 된다.

stompClient.debug = null

6. Tqdm 출력문 꾸미기

현재 문자열로 출력되고 있는 tqdm을 오브젝트로 만들어서 progress bar로 뷰에 좀 더 예쁘게 출력될 수 있도록 해보고 싶었다.

70%|███████   | 7/10 [00:07<00:03,  1.01s/it]

위의 문자열을 정규식을 활용하여 [진행도(%)]와 [현재 작업], [전체 작업수], [소요 시간], [남은 시간], [작업 당 평균 처리 시간]으로 분리한다.

진행도를 표현할 수 있는 progress bar는 progressbar 라이브러리를 사용하였다.

1) 정규식 표현하기

2) Progress Bar로 진행률 보기

6. 기타 문제 발생

소켓 연결 시, 404 에러

회사에서는 현재 전자정부프레임워크를 기반한 spring mvc를 사용하고 있다. 그런데 xml 또는 Java Config에 잘못된 부분이 있는지, 계속 연결하는 부분에서 404 에러가 발생하였다. 확인해보니 web.xml 설정 부분에서

<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
    <url-pattern>.do</url-pattern>
    <url-pattern>/</url-pattern>
</servlet-mapping>

url-pattern 을 .do 로 설정해 발생한 에러였다. Websocket 엔드포인트(/stomp/*)로 추가 설정하더라도 변함없이 에러가 발생하여 일단은 default 경로(/)로 설정하여 정상 연결되도록 하고 있다. 그러나 가끔씩 서버 및 배포를 재시작하면 리소스를 불러오는데 에러가 발생하여 추후 수정이 필요하다.




다음 포스팅에서는 클라이언트 측에서 전달해야하는 인자값이 없는 경우 양방향 통신을 할 필요는 없으므로 단방향 통신 중 하나인 SSE(Server-Sent-Event) 를 했던 과정을 작성해 보겠다!

profile
주니어 개발자 9개월

1개의 댓글

comment-user-thumbnail
2023년 11월 27일

url-pattern이 저도 *.do 로 되어있는데 방법이 없는건가요?

답글 달기
Powered by GraphCDN, the GraphQL CDN