ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Slack Chatbot 구현 회고록
    AI/Chatbot 2022. 12. 21. 20:40

    이 글은 Slack bot 만드는 방법에 대해 기술한 글이 아닙니다 참고 부탁드립니다:)

    출처 : Wired

     

     회사에서 자사의 챗봇 시스템을 카카오톡에 연동에 이은 Slack에도 연동할 수 있도록 파이프라인을 구축하라는 과제를 내줬습니다. 처음에는 카카오톡도 한번 해봤으니까 Slack 뭐 어렵지 않겠지? 라고 생각했었는데…. 결론적으로 매우 많은 삽질을 하였습니다😭 그래서 이번 포스팅에서는 제가 겪었던 문제들을 공유하면서(사실 다음에 팀 내 발표 및 QA 대응 하면서 본인이 까먹지 않기 위한자료….) 테크니컬 리뷰할 수 있는 시간을 가져보도록 하겠습니다.

     

     먼저 처음에는 타사에서는 어떤 식으로 연동했는지 벤치마킹하기 위해서 저는 구글의 dialogflow 그리고 단비 AI 자료를 많이 리서치 하였습니다. 일단 가이드 문서부터 너무 복잡해서 저는 멘붕 그 자체였습니다.. (카카오 오픈빌더에 연결하는 방법이 정말 간편했던 편이었습니다) 그래도 마음을 다잡고 차근차근 훑어 나가고 천천히 큰 그림을 그려 나갔습니다.

     

    1. Slack Chatbot 인프라 구축

    Slack Chatbot 동작원리

     먼저 Slack bot을 구성하려면 다음과 같이 동작하도록 만들어야 합니다. 일반적으로 생각했을 때는 서버에서 바로 Response를 보내주는 형태로 생각을 할 텐데 Slack은 봇이 사용자에게 메시지를 전달할 때는 Incoming hooks라는 것을 이용해야 합니다. 그래서 그 hook에 request 형태로 봇의 답변을 전달하여야 합니다. 그런데 여기서 이상한 문제가 발생합니다.

     

    여러번 답변하는 문제(지속적인 후크로의 요청) 어이.. 한번만 대답하라고....-_-

     Slack에서 발생한 이벤트를 구독할 수 있는 서버를 구축한 다음 사용자로부터 다음과 같이 질문이 들어오면 즉시 hooks로 응답메시지를 request 전송하도록 구현한 결과입니다. 그런데 왜 응답이 여러 번 호출되는 걸까요😭 그래서 찾아봤는데 Event Subscriptions 응답을 3초안에 하지 않으면 지속해서 호출할 수 있다는 문서를 발견했습니다.

     

    ⭐Stack overflow 글 참조✍🏻

    https://stackoverflow.com/questions/58271176/responding-to-slack-events-api

     

     그래서 저는 저희 팀 챗봇 서버가 응답을 만들어내는 로직이 복잡해서 혹시 3초 이상이 걸리나…? 라는 생각에 아, 그럼 스레드를 만들어서 hooks 요청을 보내고 메인 스레드는 바로 return 시켜야겠다. 라고 생각했습니다. 그렇게 열심히 구현하여 1차 컨펌을 받았지만 돌아오는 피드백은 다음과 같았습니다.

     

    3초 안에 응답하지 못한다면 서버에 문제가 있는 것이다. 

     

     듣고 보니 맞는 말이었고 저는 아차 싶었습니다. 제일 중요한 본질적인 문제를 놓치고 있었습니다.😭

    질문이 들어오면 바로바로 응답을 해줘야 하고 실제로도 그렇게 동작하고 있는데 Slack에서만 느리다는 것은 말이 안됐습니다. 그래서 저는 뭔가 문제가 발생할 만한 부분에 일일이 try catch를 걸어서 디버깅을 해보았고 마침내 문제를 발견하게 되었습니다.

     

    org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unrecognized token 'ok': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'ok': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
     at [Source: (PushbackInputStream); line: 1, column: 3]
            at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:240)
            at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:225)
            at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:96)
            at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:664)
            at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:622)
            at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:389)

     

    로그파일 확인 결과 `OK` 토큰을 인식할 수 없다는 오류메시지를 발견하게 됩니다. 
    아 이 `OK` 어디서 많이 봤다!
    저는 순간 부랴부랴 insomnia를 켜서 hooks에 직접 요청을 보내봤습니다. (insomnia는 제가 사용하는 API 도구입니다)

     

    insomnia를 통해 슬랙 hooks로 rest api 테스트 결과 OK라는 응답을 받았다.

     확인 결과 저 OK라는 결괏값은 content-type을 text/html 형식으로 응답하고 있었습니다.
    저는 Spring RestTemplate 라이브러리를 사용하고 있었는데 post 요청을 보낼 때 responseType을 Map.class로 받고 있어서 읽을 수 없었던 것이었습니다. Slack 입장에서는 hooks로 요청은 성공적으로 받았지만, 그에 대한 결괏값을 서버에서 제대로 처리하지 못해 3초 안에 응답을 보낼 수 없었던 것이고 그렇게 무한 request가 형성되었던 것이었습니다.

     

    3초안에 응답없는 경우 무한루프발생

     

      만약 제대로 된 컨펌을 받지 못하고 스레드로 처리했다면 버그성 코드를 그대로 배포할뻔했습니다. (도움 주신 저희 팀 책임님께 무한 감사와 존경을 드립니다🙏🏻)

     

    2. Slack Interactivity 설정 ( Slack 버튼 넌 왜 동작을 안하는거니..)

      Slack에서 답변으로 사용할 수 있는 템플릿은 텍스트, 이미지, 버튼 등 다양합니다.
    그중에서 특히 버튼 같은 경우에는 사용자가 클릭하였을 경우 해당 액션 처리에 대한 응답이 필요한데
    Slack에서는 이러한 이벤트 동작에 대해서도 클라이언트의 서버에서 처리할 수 있도록 가이드하고 있습니다.

     

     그런데 여기서 또 문제가 발생합니다.
    분명 Slack 이벤트 구독할 때와 같은 주소의 엔드포인트를 등록했는데 
    버튼만 동작하지 않고 415 오류를 발생시켰습니다.

     

    {ok: false, error: "dispatch_failed", message: "이 앱은 415 상태 코드로 응답했습니다."}

     

     관련자료도 아무리 찾아봐도 나오지 않았습니다. 그래서 저는 여러 사람에게 도움을 요청하고 https로도 바꿔보고 이것저것 많은 시도를 해봤지만, 전혀 실마리를 찾을 수 없었습니다. 컨트롤러로 데이터의 유입 자체가 되지 않으니 어딜 봐야 할지 감이 오지 않았습니다. 문의도 여러번 해가면서 슬랙의 Jin 님과 Joy 님을 여러 차례 괴롭히면서 어쩌다 힌트를 얻었는데 해답은 우연히 nginx 로그에서 찾을 수 있었습니다. nginx 로그에 415 Media type 오류에 대한 내용이 명시가 되어있었고 마침 리버스 프록시를 통해 톰캣과 연결되어 있어서 저희 쪽 서버에 문제가 있다는것을 알아차렸습니다.

     

    그럼 415는 왜 발생한 걸까?

     

     좀 더 자세히 살펴보니 Slack에서 Interactivity는 x-www-form-urlencoded 형식으로 헤더 요청이 들어온다는 것이었습니다. 이전에 이벤트 구독은 application/json 형태로 잘 넘어오길래 이쪽으로는 전혀 의심을 못 하였던 상황이라 엉뚱한 곳만 파고 있었습니다. 심지어 @RequestBody 어노테이션을 통해 데이터를 받고 있었기 때문에 컨트롤러까지 데이터가 유입되지 않았던 것이었습니다. 그래서 슬랙에서 요청이 들어오는 게이트웨이인 이벤트 구독과 Interactivity를 각각 다음과 같이 consume으로 MediaType을 나눠서 받으니 정상적으로 작동하는 것을 확인했습니다.

    @RequestMapping(value="/chat/message", consumes = MediaType.APPLICATION_JSON_VALUE)
    public ChatResponse messageForJson(@RequestBody Map<String, Object> data,
            @RequestParam(required = false) String src) throws Exception {
        return chatService.getMessage(src, data);
    }
    	
    @RequestMapping(value="/chat/message", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ChatResponse messageForForm(@RequestParam Map<String, Object> data,
            @RequestParam(required = false) String src) throws Exception {
        return chatService.getMessage(src, data);
    }

     

    구현 후기

     앞서 회고한 이슈들 외에도 챗봇마다 어떤 슬랙 채널에 게시할 것인지? 그리고 게시한 채널마다 생성되는 훅은 서버에 어떻게 저장할 것인지? 슬랙 block kit에서 제공하는 답변 포맷과 팀에서 사용하는 챗봇 빌더 답변 포맷을 어떻게 매핑할 것인지? 대화 이력 로깅은 어떻게 처리할 것인지? 등등 생각할 거리와 고려해야 될 부분이 많아 개발을 진행하는데 생각보다 많은 시간이 걸렸지만 그래도 생각보다 보람도 있고 재밌는 작업이었고 카카오, 슬랙 연동해봤으니 다음 플랫폼 연동은 그래도 좀 수월하게 해볼 수 있지 않을까 하는 생각이 듭니다.

    'AI > Chatbot' 카테고리의 다른 글

    챗봇개발자의 2022년 회고록  (0) 2022.12.31

    댓글

Designed by Tistory.