본문 바로가기
개발/기타

어떻게 하면 호출한 API의 처리 완료 시점을 알 수 있을까? (부제: socket 삽질기)

by 카펀 2022. 10. 9.

글 쓰기에 앞서, 이 문제는 결국 해결하지 못하였음을 밝힙니다.

아직 제가 발견하지 못한 적절한 해결법이 있을 것이라고 생각합니다. 나중에라도 제가 해당 방법을 찾는다면 별도 글로 작성 후 링크를 달아 두도록 하겠습니다.

목차

0. 개요

1. API 및 로직 소개

2. Socket 연결을 사용하여 예쁘게 구현했다

3. 문제점 발생, 해결법 고민

4. 정리

0. 개요

해당 내용은 업무 중 개발하다가 경험한 내용을 재구성한 것입니다. 따라서 예시에 사용되는 코드 및 구체적인 로직은 실제 업무와는 무관함을 미리 알립니다.

 

웹 서비스를 사용하다 보면, 파일을 올리면 해당 파일을 분석 후 결과를 보여 주는 식의 서비스가 있습니다. 문서를 올리면 내용을 인식해 주거나, 이미지를 올리면 고해상도로 업스케일링 해 주는 식으로요.

예를 들어 이 사이트에 2d 이미지를 올리면, 딥 러닝 기술을 이용하여 이미지의 해상도를 개선해 줍니다.

이런 사이트는 아마 내부에 이런 식으로 설계되어 있겠지요.

  1. API를 호출하여 파일을 전달한다.
  2. API 서버 내부에서 파일에 대한 처리를 한다.
  3. 처리가 완료되면, 처리 결과를 클라이언트에게 보내 준다.

그렇다면, 3번에서 "처리가 완료되면"을 클라이언트가 어떻게 알 수 있을까요? 다양한 방법이 존재하는데, 그 중 제가 시도했던 방법과 한계를 소개해 보고자 합니다.

1.  API 및 로직 소개

업무 중 맡은 개발 건은 아래와 같이 진행되었습니다.

  1. 첫 번째 API (A)를 호출합니다. username과 password를 넘기고, 값이 올바를 경우 API는 token을 json 형식으로 리턴해 줍니다.
  2. 두 번째 API (B)를 호출합니다. token과 처리를 원하는 이미지, 그 외 기타 파라미터 (2d인지 3d인지 여부 등)를 넘겨 줍니다. API는 result_key라는 값과 msg 값을 json 형식으로 리턴해 줍니다.
  3. 세 번째 API (C)를 호출합니다. token과 result_key 값을 넘겨 줍니다. API는 처리 완료된 이미지를 리턴해 줍니다.

이렇게 총 API를 세 번 호출하는데, 앞서 언급한 바와 같이 'C를 언제 호출해야 하는가?' 라는 문제가 발생하였습니다.

API 서버 내에서 아직 처리가 끝나지 않는데 C를 호출하는 경우, "Processing" 이라는 문자열 하나만을 리턴할 뿐이었습니다.

 

가장 단순하게 생각해 볼 수 있는 방법은 아래와 같습니다.

  1. B까지 호출 및 결과 수신이 끝난 후, 적당한 timeout (예: 40초) 기준을 정한다.
  2. timeout 시간이 될 때까지, 매 초마다 C를 호출한다.
  3. C로부터 받은 값이 처리 완료된 파일이라면, 호출을 중단하고 이후 로직을 진행한다.
  4. C로부터 받은 값이 "Processing"이라면, 2로 돌아간다.
  5. timeout이 될 때까지 4가 반복된다면, 호출을 멈추고 클라이언트에 timeout임을 전달 후 종료한다.

딱 봐도 좋은 방법은 아니죠? 최악의 경우에는 파일 하나당 C를 40번 호출하게 되는데, API 서버와 웹페이지 서버 모두 부하가 크겠다는 느낌을 받았습니다.

2. Socket 연결을 사용하여 예쁘게 구현했다

다른 방법을 고민하던 저는 socket 통신을 이용하는 방법을 떠올렸습니다. client-server 간 connection을 생성하고, B로부터 받은 처리가 API 서버 내에서 완료되면 client에 신호를 주고, 그 후 client가 C를 호출는 방법입니다.

API 개발자 분께서도 마침 해당 방법을 사용하도록 개발했다고 하셨고, 따라서 이를 활용하여 개발을 진행하게 되었습니다.

 

제가 개발한 흐름은 아래와 같이 진행되었는데요.

검정색은 단순 요청, 파란색은 API 호출, 초록색은 X와 Z 사이의 직접 연결입니다.

위와 같은 구조를 통해 API A, B, C를 각각 한 번만 호출하고도 원하는 로직을 구현할 수 있었습니다.

 

socket 통신을 X와 Z 사이에 연결하였는데요. 여기에는 몇 가지 이유가 있습니다.

  1. 웹 서버 (Y) - API 서버 (Z) 사이에 socket 통신을 연결해도 되는지 확신이 없었습니다. 만약 100명의 사용자가 동시에 이미지 파일을 이용한 요청을 보낸다면, Y는 100개의 socket 통신을 Z와 연결할 텐데, 이것이 웹 서비스에 부하를 발생시킬지 확실하지 않았습니다. 따라서 각 사용자 (client; 웹 브라우저)가 직접 socket 통신을 연결하는 것이 바람직하다고 생각했습니다.
    1. 특히 예를 들어서, 사용자가 응답을 기다리던 도중 (너무 오래 걸린다는 등의 이유로) 페이지를 닫아 버리면, X와 Z 사이에 연결된 socket 통신은 끊어질 것입니다. 하지만 Y와 Z 사이에 socket 통신을 연결했다면, 웹 페이지가 닫혀도 통신은 얼마간 유지됩니다. 이런 구조에서 한 사용자가 n개의 socket 통신을 연결할 수도 있겠다는 걱정이 있었습니다.
  2. 현재 사용 중인 Java / Spring의 버전이 낮아서 socket 통신을 연결하는데 어려움이 있었습니다. Java 1.7 / Spring 3.3.2를 기준으로, Spring Websocket 모듈은 Spring 4 이상부터 지원되었고, 유일하게 시도해 본 Java의 socket (java.net.Socket - jdk 1.0부터 지원)은 원하는 대로 구현이 되지 않았습니다. (내용 추가 - WebSocket과 socket.io는 완전히 다른 개념임을 알았습니다! 글 참조)
    1. https://spring.io/guides/gs/messaging-stomp-websocket/
    2. https://docs.spring.io/spring-framework/docs/4.3.x/spring-framework-reference/html/websocket.html
    3. https://docs.oracle.com/javase/7/docs/api/java/net/Socket.html

반면 X (최신 버전의 Chrome 또는 Edge)는 이러한 socket 연결에 어려움이 없었습니다. WebSocket을 이용해 연결을 생성하고, onmessage를 통해 Z로부터 메세지를 수신하는 식으로 간단히 구현할 수 있었습니다.

(JavaScript WebSocket에 대한 문서)

3. 문제점 발생, 해결법 고민

문제는 예상치 못한 곳에서 발생했습니다.

API 서버는 개발환경에서만 접근 가능합니다. 사용자는 API 서버에 직접 접근할 수 없습니다.

개발 완료 후 테스트를 하다 보니, 서비스 개발을 위해 다양한 환경에서 테스트를 진행하다 보니, 저희 팀 외의 컴퓨터에서는 서비스가 응답하지 않는 것이었습니다.

개발하면서 저는 진행 과정을 웹 브라우저의 console에 출력하도록 하였는데, 문제가 되는 사용자의 경우에는 WebSocket connection이 실패하는 것이었습니다.

조사 결과, API 서버는 외부에서 접근하면 안 되는 상황이었습니다. 단순히 생각해 보면, 저희가 이미지 처리 서비스를 Open API로 제공하는 것이 아니기 때문에, 지정된 경로 (웹서비스 내에서 호출)를 통해서만 호출하여야 하는 것은 당연했습니다.

 

당장 다음 날 해당 기능을 오픈에 앞서 시연해야 했기 때문에 (...), 저는 가능한 해결법을 다방면으로 고민해 보았습니다.

 

1. 앞서 언급했던, 매 초마다 API C를 호출하는 방법

가장 구현이 간단한 방법입니다. API 개발자분께서는 가능하면 그러지 말라고 하셨고 (API 서버가 다운될 수 있으니...), 저희로써도 썩 내키는 방법은 아니었습니다.

 

2. API B의 처리가 완료되면, API 서버에서 웹 서비스 내의 API를 호출하는 방법

이 방법 역시 단순히 글로만 적기에는 복잡한데요. 단순히 요약하면 아래와 같습니다.

  • API B의 처리 결과가 완료되면, 웹서비스 측에서 만들어 둔 API (D라고 하겠습니다)를 호출한다.
  • D는 호출되면 C를 호출하고, 해당 결과를 DB에 저장한다.
  • 한편, 웹 서버는 B를 호출한 이후, 지정 시간동안 DB에 결과가 기록되었는지 조회한다.
    • 마찬가지로 timeout 시간을 정해 두고, 매 초마다 DB 내에 결과가 기록되었는지 조회한다.
    • 지정된 시간이 지나면 timeout임을 client에게 알린다.

이 방법 역시 근본적으로 저희 웹 서버에 부하가 가는 것은 변함이 없었습니다. 대신 기존에 API 서버에 가해지는 부하를 저희 DB로 옮겼다 (...)는 차이가 있습니다.

뿐만 아니라 결과를 전부 DB에 저장하기 때문에, 기존에는 휘발성으로 받은 결과가 DB에 전부 저장된다는 문제점이 발생했습니다. 만약 사용자 한 명이 이미지를 가지고 5번 요청을 보낸다면, 5번에 해당하는 결과가 모두 DB에 기록되는 등의 문제도 있었습니다. 이에 더해, 이를 위해 개발해야 하는 범위가 너무 늘어난다는 현실적인 한계 또한 있었습니다.

 

결국 우선은 기존에 생각했던 가장 단순한 방법으로 진행하기로 의견이 모아졌습니다. 어쨌든 기능 개발을 완료하긴 해야 하니까, 라는 이유로 그렇게 진행되어 개발을 완료하였습니다. 다행히 해당 기능은 실제 운영 환경에서도 여러 사용자가 동시에 호출할 일은 잘 없는 기능이라, 부하로 인한 서비스 장애에 대한 걱정은 약간 덜 수 있었습니다.

 

X는 Y에게 요청 한 번만 보내면 결과를 받아보게 되었습니다. 그림에서는 간단해 보이지만, 결국 C를 여러 번 호출하는 구조가 되었네요.

4. 정리

여러모로 저에게 아쉬움이 많이 남는 개발 건이었습니다. 굉장히 깔끔하고 효율적인 방법으로 기능을 구현했다고 생각했는데, 여러 현실적인 제한에 타협하게 되어 언젠가 문제가 발생할 수 있는 방법으로 구현했으니까요.

 

다른 회사에서는 이런 경우 어떻게 해결할까요? 분명 사내에서만 사용하는 API가 존재할 것이고, 이번 건과 같이 처리가 완료되는 시점에 신호를 받는 형식으로 구현해야 할 텐데...

제 예상으로는 당연히 웹 서버 - API 서버 상에서 WebSocket 통신을 생성할 것 같습니다. WebSocket 통신 방법이 실제로 connection을 유지하는데 필요한 트래픽은 polling, long polling에 비해 현저히 적기도 하고요. Spring을 사용한다면 버전 4 이상을 사용하여 해결했을듯 합니다 (참고로 Spring은 버전 6이 올해 중에 나옵니다. Spring 3은 굉장히 예전 버전).

이렇게 여러 예상을 해 볼 수는 있지만, 안타깝게도 회사 업무 중에는 이런 방식에 대해 명확히 답을 얻을 수 없었습니다. 다음에 기회가 된다면, 비슷한 상황을 더 효과적으로 해결할 수 있는 상황에서 마주치면 좋겠습니다.

 

 

 

댓글