ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WebSocket, Redis 의 테스트 코드를 작성하며 마주친 에러와 해결
    개발 2024. 8. 6. 21:39

    WebSocketStompClient로 소켓 통신을 하며 Redis에 접근하는 테스트 코드를 작성하며 발생한 에러를 해결하는 과정을 담고 있습니다.
    WebSocket은 실제로 동작하는 것을 꼼꼼히 확인해야 프론트와 연동하는 과정에서 어려움이 적을 거 같아서 통합테스트를 진행하면서 예기치 못한 상황들을 마주하였습니다.

    WebSocket에 대한 테스트 코드를 작성하고자 하는 분들에게 조금이나마 참고가 되었으면 합니다.

     


    문제 상황

    • WebSocketStompClient 로 소켓 통신에 대한 테스트 코드를 작성
    • 테스트 케이스는 4개로 아래와 같습니다.
      • JWT 토큰 검증이 성공하면 소켓을 연결
      • JWT 토큰 검증이 실패하면 에러 발생
      • 소켓이 연결되면 Redis에 사용자를 저장
      • 소켓이 해제되면 Redis에서 사용자를 제거
    • 테스트 케이스를 작성하고 테스트를 실행하면 모든 테스트는 성공을 하지만 테스트가 종료되지 않는 상황이 발생했습니다.

     

    테스트는 성공하지만 종료되지 않는다.

     

    • 테스트는 모두 성공했지만 테스트는 정상적으로 종료되지 못하고, 아래와 같은 로그를 남긴 뒤에야 종료가 되었습니다.

     


    원인

     

    • 위의 문제의 부분에서 redisTemplate 으로 소켓 연결 해제시 사용자의 정보를 제거하고 있는데 여기서 RedisCommandTimeoutException이 발생하고 있었습니다.
    • 그래서 하나씩 로그를 따라가며 확인한 결과, 소켓을 해제하는 시점에 RedisServer가 이미 종료된 뒤라서 발생하는 문제였습니다.
    • 왜 소켓을 해제하기 전에 종료가 되었는지를 파악하기 위해 다시 테스트 코드를 확인했습니다.

     

    @DisplayName("소켓 연결 시 JWT 토큰이 유효하다면 정상적으로 연결된다")
    @Test
    void connectWebSocketWithValidJwtToken_success() throws Exception {
        // given
        StompHeaders headers = new StompHeaders();
        headers.add("Authorization", "Bearer " + jwt);
        URI uri = new URI(String.format("ws://localhost:%d/ws", port));
    
        // expected
        assertDoesNotThrow(() -> {
            StompSession stompSession = this.stompClient.connectAsync(uri, null, headers, new StompSessionHandlerAdapter() {
            }).get(5, TimeUnit.SECONDS);
        });
    }

     

    • JWT 토큰이 정상적이라면 소켓을 연결하는 과정에서 에러가 발생하지 않는 것을 검증하였습니다.
    • 다른 테스트 케이스에서는 StompClient를 새로 할당하여 소켓에 연결하도록 테스트를 작성해두었는데, 이 부분에서 처음에 연결하였던 위의 코드는 StompClient를 새로 할당하였다해서 연결이 끊기지 않고 그대로 남아있는 것이었습니다.
    • 다른 테스트 케이스에서는 소켓에 연결이 되지않거나, 연결을 하고 해제를 하면서 Redis에 데이터가 반영이 되었는지 확인을 해야했기에 명시적으로 아래와 같은 코드를 작성해주었기에 문제가 되지 않았습니다.
    stompSession.disconnect();

     

    • 문제가 되는 이유는 다음과 같습니다.
      • 테스트를 실행하면 스프링 컨테이너가 올라오고 빈들이 등록된다.
      • 테스트케이스들을 실행한다.
      • 현재 테스트 환경에서는 테스트용 redis 환경을 갖추어두었고, 테스트가 끝나면 redis 서버가 종료되는 환경입니다.
      • 스프링부트에 등록된 모든 빈들은 destroy 하는 과정에 웹소켓 관련된 빈들도 destroy 하게 됩니다.
      • 웹소켓은 해제되지 않은 세션이 있기에 Redis에 접근하여 사용자의 정보 제거를 시도한다.
      • 하지만 Redis 서버는 이미 종료된 뒤라 RedisCommandTimeoutException 이 발생하는 것이었습니다.

    StompSubProtocolHandler 의 afterSessionEnded

     

    • 스프링부트가 종료되면서 StompSubProtocolHandler의 afterSessionEnded가 호출되고 세션에 있는 연결들을 끊어줍니다.
    • 의존성을 따로 받지 않고 Event 를 발행하여 세션을 끊는 것을 확인할 수 있었습니다.

    DefaultSimpUserRegistry의 onApplicationEvent

    • 디버그로 확인한 결과 DefaultSimpUserRegistry에서 SessionDisconnectEvent를 처리하여 연결된 세션 정보를 제거하는 것을 확인할 수 있었습니다.

    해결

    • 소켓 연결을 해제해야 하는 곳에 명시적으로 disconnect 하는 코드를 추가함으로써 테스트는 정상적으로 성공하고 종료할 수 있게 되었습니다.

     

    생각해보면 간단히 해결할 수 있는 문제였지만 빨리 해결하지 못한 건 WebSocket에 대한 테스트 코드에 익숙하지 않았던 게 가장 큰 원인이라고 생각합니다.

Designed by Tistory.