brunch

You can make anything
by writing

C.S.Lewis

by zwoo Feb 09. 2023

[정글]프로젝트가 끝난 후 보완한 것

nginx 리버스 프록시

최근 진행한 웹서비스 프로젝트를 배포할 때 발생한 미스터리가 있었다. 그 미스터리는 프로젝트 시연발표 당일날까지 해결되지 못했고, 아쉽지만 임시방편으로 배포를 마무리해야 했다. 프로젝트가 끝나고 회고를 적으려고 했지만 해당 부분이 계속해서 마음에 걸렸다. 그래서 나는 다시 문제를 해결해보려고 시도했고 약 일주일이 흐른 지금, 미스터리를 해결했고 이 내용을 공유해보고자 한다.


메타버스 웹서비스 파라솔로 (https://para-solo.site/)

우리의 프로젝트를 간단히 소개하자면 메타버스 웹서비스이다. 나는 게더타운 서비스에 매료되어 있었고, 기존 게더타운에 인터랙티브 요소를 추가한 서비스를 만들어보면 재미있지 않겠냐는 동료의 말에 곧바로 설득되어 팀원으로 참여했다. 처음에 만난 난관은 기술스택에 대한 고민이었다. 맵과 캐릭터 UI를 자바스크립트와 CSS로 구현하는 것은 렌더링 최적화가 만만치 않아보였다. 우리는 메타버스를 일종의 게임 플랫폼으로 생각하고 웹으로 구현된 게임 서비스들을 찾아보다가 스카이오피스라는 메타버스 서비스를 발견하고 해당 서비스가 사용한 기술스택을 우리도 사용하기로 했다. 맵과 캐릭터는 phaser라는 애니메이션으로 구현하였다. 이 라이브러리는 json파일에 담긴 정보를 가지고 Layer를 생성해서 한층씩 쌓아올리는 방식으로 렌더링하는 함수를 제공해준다. 각 Layer가 depth를 가지며, depth가 높은 Layer가 가장 위에 쌓인다. 맵은 여러 개의 Layer로 이루어져있고, 어떤 Layer에는 충돌(collision) 속성도 부여되어 있어서 캐릭터가 통과할 수 없다. json파일을 만드는 것은 Tiled-map 에디터를 활용했다. 

타일맵 에디터 예시 : https://doc.mapeditor.org/en/latest/manual/export-tbin/
실제 구현된 맵

웹소켓 통신, 문제의 시작

우리 서비스에는 웹소켓 통신이 활용되었다. 우리는 웹소켓으로 많은 일들을 해야 했다. 우선 여러 유저가 서로의 움직임을 보아야 하는 서비스 특성상, 실시간성이 보장되어야 했다. 그래서 유저들의 위치좌표 업데이트를 위해 Colyseus라는 웹소켓 기반 멀티플레이어 프레임워크를 활용했다. 


그리고 또한 1대1 채팅 기능에도 소켓 통신이 필요했다. 실시간으로 유저정보가 업데이트시키는 것과 채팅기능을 하나의 소켓포트가 감당하도록 하면 유저가 많아질 경우 통신 지연이 발생할 것 같아서 두개의 서버로 분리해서 로드밸런싱을 하기로 했다. 이왕 분리하는 거, 1대1 채팅 기능은 메신저에 많이 사용되는 Socket i.o라는 라이브러리를 사용해 구현하기로 했다. 참고할 수 있는 자료가 많고, socket i.o가 제공하는 폴링(지속적 연결 요청) 기능의 이점을 누릴 수 있다고 생각했기 때문이다. 


포트를 두개로 나누고, 각 포트에서 서버 리스닝을 하도록 했다. 바로 여기서 문제가 시작되었다. 


첫번째 문제는 Heroku 서비스로 인한 문제였다. Heroku는 Git 레포지토리와 연결해두면 간단하게 배포 자동화를 도와주는 배포 플랫폼이다. 그런데 이 플랫폼에서는 여러개의 포트를 리스닝하는 것을 허용하지 않는 정책이 있었다. 하나의 도메인에서 두개의 포트를 사용하고 싶다면 Heroku 서비스는 적합하지 않은 선택이었다. 개발효율을 높이기 위해 빠르고 간단하게 배포 자동화를 하기 위해 이 서비스를 사용하고 있었지만, 이제 다른 방법을 찾아야 했다. 그래서 EC2 인스턴스에 직접 빌드한 앱을 올려서 호스팅하는 것을 선택했다. 여력이 된다면 깃허브 액션으로 배포 자동화를 도입할 수도 있겠으나, 서비스의 주요 기능을 개발하는 것이 우선이라서 일단 하루 이틀에 한번 정도 직접 빌드해서 배포하기로 했다. 


여기서 발생한 두번째 문제는, 인스턴스에서 열어둔 두개의 포트를 HTTPS 보안레이어로 감싸주어야 한다는 것이었다. 실제 배포된 서비스들은 브라우저 정책에 의해 HTTP 통신을 할 수 없도록 되어있다. 클라이언트 서버는 ssl 인증서를 발급받아서 HTTPS 통신을 지원해야 하고, 클라이언트 서버와 요청을 주고받는 백엔드 서버는 보안 포트인 443번 포트를 사용하도록 되어있는 것이다. 다시 말해, 인스턴스가 받은 연결을 https (443)포트로 받은 후에, 들어온 요청의 타입을 구분하여 실제로 요청을 수행할 서버포트로 전송해주는 기능이 필요했다. 이것이 바로 '리버스 프록시'라고 부르는 기능이다.


일반적으로 nginx 라는 소프트웨어를 리버스 프록시를 위해 많이 사용한다. nginx는 경쟁소프트웨어인 아파치와 달리 동시에 많은 요청이 들어오더라도 이 요청들을 Queue에 담아두기에 요청이 유실될 가능성이 낮다는 장점이 있다고 판단해서 이를 채택했다. 하지만 여기서 세번째 문제가 발생했는데, 어렵게 모든 구성을 마쳤음에도 socket i.o 웹소켓 연결이 맺어지지 않는 문제였다.


socket i.o 웹소켓 연결 불가 문제는 끝내 해결되지 않았고, 프로젝트 시연발표를 해야 했기에 임시방편을  마련해야 했다.


1안은 EC2 인스턴스의 로드밸런싱 기능을 활용해보는 것이었다. AWS의 EC2인스턴스에는 로드밸런서를 붙일 수 있고 이 로드밸런서는 ACM이라는 AWS 서비스를 통해 SSL/TLS 인증서를 발급받을 수 있다. 우리는 로드밸런서가 443포트로 받은 요청을 인스턴스 내부로 리다이렉트할 때 포트를 구분하도록 리버스 프록시 역할도 시키고 싶었지만, 로드밸런서는 받은 요청에 따라 리다이렉트할 포트를 결정하는 설정을 할 수 있는 기능을 제공하지 않았다. 즉, 무조건 하나의 포트로만 리다이렉트할 수 있었다. 


발표 하루 전, 우리는 2안을 마련했다. EC2 두개를 생성하고, Colyseus & API 서버와 Socket i.o 서버를 각각의 EC2에서 실행하는 것이었다. 비효율적인 방식이었고, 두 서버가 통신을 하기 위해서 별도의 API를 거쳐야 하기에 속도 지연이 우려되는 방식이었다. 그러나 어쩔 수 없었다.


Socket i.o 라우트 구성 방식 확인, 문제의 해결

프로젝트 종료 후 일주일 간 Nginx 리버스 프록시를 다시 구현해보았다. 결국 원인을 파악했는데, 그건 바로 socket i.o 클라이언트의 요청을 리버스 프록시로 전달할 때 반드시 지켜야 하는 url 규칙 때문이었다. 아니, 사실 규칙인지 버그인지 모호한 측면이 있다. 


리버스 프록시를 구성할 때 설정파일에서 특정 라우트 경로로 온 요청을 파싱하여 목적 서버로 보내주도록 할 수 있는데, Socket i.o 공식문서에는 기본으로 붙는 /socket.io/ 경로 이외에 추가로 라우트 경로를 커스터마이징할 수 있다고 쓰여있지만 실제로 클라이언트 단에서 테스트해보니 우리가 추가한 커스텀 경로는 무시되었다.

가령,


socket.io-client.io ( 'https://ourserver.com/socket-server/socket.io/')  


이렇게 연결을 시도하더라도, 


socket.io-client.io ( 'https://ourserver.com/socket.io/')  


이렇게 커스텀 경로인 /socket-server/ 가 무시되어 연결이 시도되었다. 우리가 만든 버그인지, Socket i.o측의 버그인지는 파악하지 못했지만 동작이 이렇게 이루어지기 때문에 nginx 설정파일에서도 소켓통신은 /socket.io/ 로 받는 것으로 제한해야 했다. 기존에는 우리가 커스텀 경로를 사용해서 리버스프록시를 하도록 정의했기에, 커스텀 경로가 생략되어버린 Socket i.o의 연결요청을 받아줄 location 경로가 준비되지 않았던 셈이다. 


팀원들과 논의해본 결과 아무도 예상하지 못한 동작방식이었고, 프로젝트 기간 막바지에 시간 압박을 받는 가운데에 놓친 것 같다고 결론지었다. 조금 허탈하기도 했지만, 어떻게 보면 생각하기 어려운 동작방식이기도 해서 충분히 그럴 수 있는 일이라는 생각이 들었기에, 팀원들 모두 너무 자책하거나 남을 탓하기보다는 반성하고 서로 다독여주었다.


https://gist.github.com/yeonwooz/32b3249a93d4984fa3e328a58c7f9a84


프로젝트를 마치며

미스터리한 문제까지 해결하니까 이제 드디어 진짜 프로젝트를 마친 느낌이 든다. 팀원들과도 기술에 대한 논의를 하면서 깊이 있는 마지막 회고를 한 것 같아서 좋았다. 


정글에서 공부한 반년간 쌓은 기본기를 바탕으로 어디서든 능동적으로 성장할 수 있다는 확신이 생겼다. 이제 본격적으로 구직 활동을 시작한다. 사실 벌써 불합격 소식을 받은 곳도 몇 군데 있지만, 포기하지 않고 꾸준히 노력해서 2월 안에 일을 시작하는 것이 목표이다.




Photo by Catalin Sandru on Unsplash



참고자료

https://socket.io/docs/v4/reverse-proxy/

매거진의 이전글 [정글]열네째주. 서버개발자로서의 첫 고민
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari