본문 바로가기
BackEnd/배포

[배포] "외국민" 서비스 배포 과정 - (1)

by 경험의 가치 2024. 5. 4.

기존 서비스 아키텍처

기존 아키텍처는 위와 같이 구성했다. 하지만, 위의 아키텍처는 여러가지 문제점이 있었다. 우선, 우리 메인 Spring 비즈니스 서버는 다른거 처리하기도 바쁘다. 게다가, 저희 서비스는 채팅을 Long Polling으로 개발하였는데, 여러 사용자가 동시 다발적으로 요청을 보낸다? 그럼 메인 비즈니스 서버의 성능을 매우 떨어질 것이며, 응답시간은 길어질 것이다. 결국 Software Quality가 떨어질 것이다. 따라서 채팅 서버를 분리할 필요가 있다.

뿐만 아니라, api 요청을 보낼때 https가 아닌 http로 보내기 때문에 데이터의 Integrity를 보장할 수 없다. 그래서 요청을 https로 암호화해서 보낼 필요가 있다.

마지막으로, 챗봇, 메인 비즈니스 서버, 채팅 서버 모두 JWT Token을 확인하는 과정이 필요하다. 하지만, 위와 같이 디자인하면, 각각 서버에서 JWT Token을 확인하는 로직을 짜야되므로 비효율적이다. 또한, API 사용량 제한을 걸기 위해서 각각 API Rate Limiter를 구현해야한다. 따라서, API Gateway를 구축해 이를 처리하는 것이 좋다.


개선 후 서비스 아키텍처

그래서 위와 같은 아키텍처로 변경했다.

 

우선, 채팅 서버 분리를 진행하였다. 채팅 서버는 Ruby On Rails로 개발하였다. 그 이유는 여러가지이다. 첫번째, Ruby라는 언어는 매우 쉽다. 파이썬처럼 프로그래밍 해보지 않은 사람들에게 입문용으로 추천이 가능할 정도로 굉장히 간단하다. 게다가 필자는 어릴 때 RPG VX ACE라는 게임 만들기 툴에서, 스크립트 언어로 Ruby를 사용하면서 어느정도 조금은 아는 상태였다. 두번째, 테스트 코드가 작성하기가 매우매우 쉽다. Spring은 Test Code를 작성하려면 굉장히 여러과정을 거쳐야하며 복잡하다. 반면에 Ruby On Rails은 굉장히 간단하게 구성되있다. 세번째, Ruby On Rails는 빠른 개발을 지향한다. COC원칙을 중요시해서, 설정이 거의 없다. Spring으로 서버 하나 만들려면 하나부터 열까지 설정할게 많아서 매우 복잡하다. 프로젝트 마감 기한이 얼마 안남은 지금, Ruby On Rails 또는 Express만큼 좋을게 없다고 생각했다. 특히, 언어의 특성이 애자일에 초점이 맞춰져 있기 때문에 생산성이 높다. 이러한 이유로 채팅 서버 개발에 Ruby On Rails를 채택했다.

 

두번째, 자동으로 CI/CD가 될 수 있도록 메인 비즈니스 서버, 채팅 서버, AI 서버에 Git Actions를 적용했다. 이로써, 굳이 우리 EC2에 접속할 필요 없이 자동으로 테스트 및 배포가 진행될 수 있게 되었다.

 

세번째, Route53을 이용해 도메인을 적용했다. 가비아 도메인에서 도메인을 구매했다. 우리 팀은 API 문서를 Swagger를 이용해서 공유하는데, 접근하기 위해서 우리 서버 도메인을 쳐야했다. 하지만, 기존 AWS EC2 기본 도메인은 기억하기가 불가능해서, 외우기 쉬운 도메인으로 변경하였다. 또한, Https를 적용하기 위해 DV SSL 인증서를 발급 받았어야 했는데, AWS EC2 기본 도메인으로는 인증서 발급이 불가능했다. 그래서 따로 도메인을 구매해서 적용했다.

 

네번째, Nginx와 Certbot을 이용하여 Https를 적용했다. 이를 이용하여 Let’s Encrypt라는 무료로 DV 인증서를 발급받을 수 있는 CA로부터 SSL인증서를 발급받아 Https를 적용했다. 이로써, 데이터의 Integrity를 보장할 수 있다. 이때, Let’s Encrypt 인증서는 90일마다 새로 발급 받아야하는데, Docker Compose에 주기적으로 인증서를 갱신하는 command를 추가함으로써 이 과정이 자동으로 실행될 수 있도록 구성했다. 또한, Spring Cloud Gateway 앞단에, Nginx가 먼저 요청을 가로채 https을 내부적으로 http로 전달함으로써, 로컬 서버 내에서는 암호화하는데 리소스를 줄일 수 있게 되었다.

 

다섯번째, Spring 메인 비즈니스 서버, Ruby 채팅 서버, AI 서버 앞단에서 JWT 토큰 Authentication을 처리하고, 요청을 전달하는 Gateway를 만들었다. 이로써, 각각 서버에서 토큰 확인 로직을 만들 필요 없이 게이트웨이에서 일괄적으로 처리할 수 있게 되었다. 또한, Gateway에서 Redis를 이용하여 Token Bucket Algorithm을 구현하여 API Rate Limiter도 적용할 수 있게 되었다. 이로써, 무분별하게 한번에 많은 요청을 우리 서버에 보내는 것을 막을 수 있게 되었다.

 

마지막으로, Spring 메인 비즈니스 서버에 Redis를 적용하였다. 이는 Redis를 이용하여 Refresh Token을 저장하고, 로그아웃시 Access Token을 Ban할 수 있는 기능을 구현할 수 있게 되었다. 또한, 글의 추천수나 조회수 등에서 Race Condition을 예방하는 용도로도 사용하였다. 원래 곧바로 우리 RDB에 저장하면 동시에 많은 사용자 요청이 들어왔을 때 Race Condition이 발생할 가능성이 매우 높았다. 하지만, Redis는 싱글스레드이기 때문에, Redis에 임시로 조회수를 저장하고, 주기적으로 Scheduler를 이용해 진짜 우리 서비스 RDB에 반영하도록 구성했다. 뿐만 아니라, Redis를 이용해 Cache를 구현할 수 있게 되었다. 매번 똑같은 정보를 RDB에 접근하여 받아오는 것은 오버헤드가 크므로 Redis에 Caching을 함으로써 Response Time을 줄일 수 있었다.


다음 포스팅에서는, Nginx의 구성, Docker 파일의 구성, Git Actions 스크립트 등에 대해서 차례차례 알아보고자 한다.