프로젝트에서 코드 실행 요청을 보내는 기능을 구현하고 있다.
클라이언트가 코드 실행 요청을 보내면 서버에서 그 코드를 실행해 결과를 알려주는 형태이다.
실행을 요청한 코드가 실행에 오래걸리는 코드이거나,
사용자가 많아서 내가 보낸 요청이 처리되는데 오래걸릴 수도 있다.
이럴 땐 어떻게 클라이언트에 응답을 줄 수 있을까? 생각해 본 방식은 두 가지가 있다.
하염없이 기다리기
long polling에 가깝다.
일단 polling은 주기적으로 요청하여 서버의 상태와 클라이언트 상태를 동기화 하기위해 사용하기 때문에 엄밀히 말하면 polling은 아니라고 생각한다.
하지만 요청을 보내면 응답할 내용이 생길 때까지 서버는 대기를 하다가 지정한 시간 내에 응답이 발생하면 클라이언트에게 응답을 보내고 지정한 시간내 응답할 내용이 생기지 않으면 timeout 처리를 하는 것이다.
status Code는 408을 활용할 수 있을 것이다.
주기적으로 요청보내기
서버가 코드 실행 요청을 보내면 클라이언트에 202 응답을 내려준다.
202는 해당 요청이 accept 되었음을 알려준다.
클라이언트가 202코드를 응답받으면 다시 결과를 가져오는 요청을 보낸다.
이 요청에 200 응답을 받을 때 까지 주기적으로 요청을 보낼 수 있을 것이다.
이 방식은 short polling과 유사하다고 느꼈다.
프로그래머스에서 코드 실행 버튼을 누르면 events 요청이 발생하고 202 응답 받는 것을 확인할 수 있었다.
202 응답을 받은 이후에 네트워크 요청이나 응답이 없는데 코드 실행 결과는 계속 실행되고 결과까지 나왔다.
대체 어떻게~ (요리용디톤으로)
for문 백만번 돌려서 죄송합니다…
한참을 코드를 들여다보는데 도저히 결과를 다시 요청하거나 받는 코드를 찾을 수 없었다.
그러다 문득 떠오른 생각. 혹시 문제 화면 들어올 때 웹소켓??
네트워크 도구를 켠 채로 나갔다 다시 들어오니 웹 소켓 연결을 찾을 수 있었다.
문제를 선택해 처음 문제풀이 화면에 들어올 때 웹소켓에 연결된다.
실행을 눌렀을 때 run 이벤트가 발생하고 socket.io로 서버와 웹소켓을 연결을 통해 데이터를 주고 받는다.
여기도 역시 웹소켓을 사용한다.
내가 생각했었던 방식은 결국 polling 계열인데, 얘네들은 socket 개념이 등장하기 전에 나온 개념이다.
그렇다보니 두 사이트 모두 소켓을 사용하여 서버에서 실행완료 시 (클라이언트의 요청이 없이도) 응답을 보내는 것 같다.
우리 프로젝트의 코드 실행 기능의 흐름은 다음과 같다.
우리 프로젝트는 서버가 나눠져있다.
이 구조에서 발생하는 문제는 api 서버가 요청한 코드 실행이 완료되었는지 알 수가 없다는 것이다.
api 서버에 실행 완료 여부를 탐색하는 로직이 추가로 필요하다.
1번 시나리오로 간다면 다음과 같이 구현해야한다.
(우리 서비스에서 웹소켓 연결/해제 시점은 코드 실행 요청이 있을 때가 더 좋아보인다. 방에 참여한 사람들 중 한 명이 실행 요청을 보내면 그 사람만 웹 소켓연결을 하면 모든 사람이 코드 실행 결과를 공유할 수 있다.)
2번 시나리오로 간다면 주기적으로 redis를 찾는 과정을 잘 설계하는 것이 필요하다.
이때 레디스의 부하는 무시하기로 했다.
레디스 벤치마크 테스트를 해 본 결과 백만 건의 요청도 거뜬했다.
2-1. 하염없이 기다리기
코드 실행 요청이 생겼을 때 보안 검사에 실패하면 403 응답을 주고 통과하면 메시지큐, running 서버를 거쳐 redis에 실행 결과가 쌓인다.
api 서버는 메시지 큐에 job을 push하고 주기적으로 계속 redis를 탐색해서 실행 결과를 찾고, 찾으면 그때 200 상태코드와 함께 실행 결과 응답을 보낸다.
서버는 계속 탐색하고 있어 서버 부하가 크다.
2-2. 먼저 202 응답을 주고, 클라이언트 측에서 주기적으로 요청을 보내기
계속 탐색하는 역할을 클라이언트 측으로 위임한 것이다.
2-1의 경우는 탐색 주기를 적절히 설정하는 것이 어렵고 탐색 주기와 탐색 시도 횟수에 따라 크게 성능이 달라진다.
탐색 역할을 클라이언트 측에 맡김으로써 서버 측에 부하를 줄이고 http 통신을 통한 약간에 딜레이로 시간을 벌 수 있어 두 파라미터에 대한 의존성을 약간 줄일 수 있다는 장점이 있다.
제일 처음 생각했던 것에 이제 웹소켓 옵션이 하나 더해졌다.
2-3. 웹소켓 이용
먼저 202 응답을 준다. 그리고 1번 방식과 거의 동일하다.
하지만 http 요청 핸들러의 수명주기가 끝난 상태라
job id와 웹소켓 id 정보를 어디엔가 저장하고
서버에서 주기적으로 레디스를 탐색하다가 데이터를 찾으면 웹소켓 아이디 정보를 이용해서 실행결과를 보내준다.
서비스가 나눠지니까 이렇게 복잡하다
이쯤되니 마이크로서비스의 단점이 더 와닿는다
하지만 또 마음잡고 생각해보면 api 서버에서 코드를 실행하다가 죽어버리면 이제 큰일이 나는 것이다.
로그인 조차도 못하고 그냥 서비스가 중지된다.
하지만 api 서버와 running 서버가 분리되어 있어 running 서버에서 코드를 실행하다 문제가 생겨 서버가 다운되더라도 api 서버는 멀쩡하기 때문에 서비스에서 코드 실행 기능이외에 다른 기능들을 이용할 수 있다.
지금 적은 많은 생각들을 공유하고 의논해본 후 적용해봐야겠다.
모두 해보고 임계점 테스트를 해 보는 것도 재밌을 것 같다.