배포 방식, 그때는 맞고 지금은 틀리다
프로젝트 초기에는 AWS에서 제공하는 배포 도구인 CodeDeploy와 S3를 사용하여 배포를 진행했습니다.
CodeDeploy는 학습 비용이 낮고, GitHub Actions와 연계하여 자동 배포를 설정할 수 있어 기본적인 배포 흐름을 익히기에 적합하다고 판단했습니다. 하지만 배포를 진행하면서 몇 가지 한계를 경험했습니다.
CodeDeploy 방식은 S3에서 파일을 다운로드한 후, EC2에서 압축을 해제하고 애플리케이션을 재시작하는 과정이 필요했습니다.
아래는 CodeDeploy를 통한 배포 단계별 시간으로, 이벤트 로그를 통해 각 단계에서 소요된 시간을 확인할 수 있습니다.

CodeDeploy 배포 시간은 현재 서비스가 크지 않은 상태에서 배포 시간이 약 5 ~ 6초 정도 소요되었지만, 실제 애플리케이션이 추가되고 의존성이 많아지면서 서비스 규모가 커질 경우, 배포 시간이 점점 증가할 가능성이 높았습니다.
이에 따라 배포 방식을 Docker 기반으로 전환했습니다. Docker 기반 배포 방식에서는 GitHub Actions에서 도커 이미지를 빌드하고, DockerHub에 푸시한 후, EC2에서 docker-compose up -d 명령어를 실행하는 방식으로 진행했습니다.

GitHub Actions의 전체 과정은 1m 50s ~ 2m 30s 정도 소요되었고, 실제 컨테이너 실행 시간은 1.6s 정도 소요되는 것을 확인할 수 있었습니다.

비록 GitHub Actions의 빌드 시간이 길어질 수 있지만, 컨테이너 실행 속도가 매우 빠르고 환경이 일관되게 유지된다는 점에서 더 큰 장점이 있었습니다.
기존 CodeDeploy 방식에서는 EC2 인스턴스의 OS 및 애플리케이션 실행 환경이 동일해야 했습니다.
Docker를 사용한다면 모든 애플리케이션이 동일한 컨테이너 환경에서 실행되므로 로컬과 배포 환경이 일치하여 충돌 가능성이 낮아질 거라 생각했습니다.
새로운 기능을 추가할 때 기존 서버 설정 변경 없이 도커 이미지만 업데이트를 하면 된다는 점에서
추가적인 서비스로 Redis와 Nginx 같은 서비스가 필요하다면 Docker-Compose 파일을 수정하는 것만으로 쉽게 확장이 가능했습니다.
또한, 팀 내에서 배포만을 전담할 수 없는 상황이기 때문에, 배포 과정이 간소화되고 자동화되면서 부담이 감소할 수 있었습니다.
이러한 이유로 팀원들과 논의 끝에 Docker를 기반으로 배포 방식으로 전환하기로 결정했습니다.
결과적으로, 서비스가 성장하면서 발생할 속도 문제, 환경 차이 문제, 유지보수 복잡성 문제를 해결할 수 있었습니다.
코드 검증 없이 배포? 그건 자살 행위다
Docker 기반 배포 방식을 적용한 후, CI/CD 파이프라인을 구축하여 배포 자동화를 더욱 발전시키는 과정이 필요했습니다.
프로젝트를 진행 중 코드가 복잡해지면서 로컬에서는 정상 작동하던 코드가 실제 서버에서 오류를 일으키거나, 팀원 간 코드 충돌이 발생하는 문제가 있었습니다.
이러한 문제들을 해결하기 위해 CD/CD 파이프라인 구축이 필요하다고 판단했습니다.
초기에 CI/CD를 구축하면 코드 품질을 유지하고, 안정성을 확보할 수 있었습니다.
추가적으로, 프로젝트가 커질수록 배포 과정이 복잡해지는데, 후반부에 CI/CD를 도입하면 오히려 많은 시간과 비용이 들 것이라는 점도 고려했습니다.
CI/CD 도구를 선택할 때는 여러 가지 옵션을 고려했습니다.
구름톤에서 알려준 Jenkins와 GitLab CI/CD, GitHub Actions 등 다양한 선택지가 있었지만, GitHub Actions가 가장 적합하다고 판단했습니다.
이는 프로젝트 코드가 GitHub에 저장되어 있어 추가적인 설정 없이 손쉽게 CI/CD를 구축할 수 있었습니다.
Jenkins 같은 경우, 따로 서버를 운영해야 하지만, GitHub Actions는 클라우드 기반이므로 서버 관리 부담이 적었습니다. 또한, YAML 파일을 활용하여 워크플로우를 코드처럼 관리할 수 있어 변경 사항 추적에도 용이했습니다.
때문에 서버 비용을 줄이는 것이 중요했던 저희는 GitHub Actions가 유리하다고 판단했습니다.
워크플로우는 PR이 생성될 때와 PR이 병합될 때를 기준으로 분리하여 작성했습니다.

PR이 Merge 되면 Docker 이미지 빌드 및 배포를 자동화했습니다.
DockerHub에 이미지를 푸시하고, EC2에서 최신 이미지를 풀하여 Docker-Compose로 실행하도록 구성했습니다.


워크플로우 작성을 통해 PR 병합 후 배포까지 자동화되어 코드 작성에 집중할 수 있었고, 테스트 자동 수행으로 안정성을 확보할 수 있었습니다.
전체적인 CI/CD 파이프라인 아키텍처는 다음과 같이 동작하게 됩니다.

메모리 부족으로 내 멘탈도
서버가 배포된 상태에서 클라이언트가 전시회 게시글을 반복적으로 올리고 내리며 테스트를 진행하고 있었습니다.
그런데 예상치 못한 문제가 발생했습니다. “게시글이 사라지는 것 같은데 혹시 삭제하셨나요?” 클라이언트의 질문을 듣고 당황했습니다.
누구도 데이터를 삭제한 적이 없었기 때문입니다. 무언가 이상함을 느껴 즉시 EC2 인스턴스의 상태를 확인했습니다.
서버의 리소스 사용량을 점검한 결과, CPU 사용률 상승과 EC2 인스턴스가 주기적으로 재부팅된다는 것을 확인했습니다.
결과적으로, 서버가 강제 종료되면서 데이터가 유실되는 문제가 발생하고 있었습니다.
프리티어 EC2 인스턴스 환경에서 다수의 요청을 처리하면서 메모리가 부족해지고, 결국 Out of Memory 현상이 발생했습니다.
서버가 메모리를 확보하지 못하면 시스템이 프로세스를 강제 종료하는데, 이 과정에서 데이터 손실이 발생한 것이었습니다.
문제를 해결하기 위해 EC2 인스턴스 스펙업과 SWAP 메모리 활용을 고려해 봤습니다.
EC2 인스턴스 스펙업은 장기적인 해결책이었고, 추후 적용을 할 문제였습니다만, 개발 단계에서는 비용적인 측면에서 무리가 있다고 판단했습니다.
그래서 디스크를 가상 메모리로 사용하는 SWAP 메모리를 활용하여 부족한 물리 메모리를 디스크 공간으로 보완할 수 있었습니다.
SWAP 메모리 적용은 SWAP 파일 생성부터 시작하였습니다.

그리고 개발 단계에서는 시스템이 재부팅되는 경우가 많을 것으로 예상하여 SWAP 자동 적용을 하도록 설정했습니다.

SWAP 메모리를 적용하고 free -h 명령어를 통해 SWAP 메모리가 정상적으로 설정되었는지 확인할 수 있었습니다.
물론, SWAP 메모리는 장기적인 해결책이 될 수 없었습니다.
이후 EC2 인스턴스의 스펙업과 AWS RDS와 연동하여 데이터베이스 안정성을 확보하는 방향으로 개선을 진행했습니다.
HTTP 배포까지 완료, HTTPS 차례
현재까지의 웹 서버의 동작 과정은 클라이언트를 통해 웹 애플리케이션 서버(WAS)를 거쳐 데이터베이스 순으로 이루어집니다.
하지만 클라이언트가 보내는 요청을 WAS가 직접 처리한다면 부하가 커지고, 정적 리소스까지 제공해야 하는 부담이 생기게 됩니다.
이를 해결하기 위해서 WAS 앞단에 웹 서버(WS)를 배치하여, Reverse Proxy 역할을 수행하도록 했습니다.
또한, HTTPS 적용을 위해서는 SSL 인증서를 발급할 필요가 있었고, 이를 위해 중간에서 트래픽을 조정하는 프록시 서버가 필요했습니다.
Apache와 HAProxy 등 다양한 기술 스택을 고려했지만, 설정이 간단하고 성능이 좋은 Nginx를 선택하게 되었습니다.
Nginx는 클라이언트 요청을 WAS로 전달하는 역할뿐만 아니라, SSL/TLS를 지원하여 HTTPS 설정을 간단하게 적용할 수 있었습니다.
SSL 인증서 발급을 무료로 제공하는 Let’s Encrypt와 이를 자동화하는 Certbot을 활용했습니다.
EC2 환경에서 Certbot을 이용하여 인증서를 발급하는 과정은 다음과 같이 했습니다.

SSL 인증서가 발급되면, /etc/letsencrypt/live/dartgallery.site/ 경로에 인증서가 생성되게 됩니다.
이제 Nginx 설정 파일을 수정하여 HTTP 요청을 HTTPS로 리디렉션 하고, SSL 인증서를 적용했습니다.

이러한 과정을 거쳐서 HTTPS 배포를 성공하게 되었습니다.
추가적으로 이전 프로젝트에서 적용시키지 못했던 리프레시 토큰 또한 HTTPS 배포가 성공하게 되면서 정상적으로 클라이언트 측으로 넘어가는 것을 확인했습니다.
전체적인 배포 아키텍처는 다음과 같이 동작하게 됩니다.

마치며
초반에는 배포 과정이 익숙하지 않아 진입 장벽이 높았고, 직접 적용하는 과정에서 많은 시행착오를 겪었습니다.
특히, 개발 서버와 프로덕션 서버를 분리하지 않은 점과 무중단 배포를 적용하지 못한 점이 아쉬움으로 남았습니다.
처음에는 배포 환경을 단순하게 구성했지만, 진행하면서 안정적인 운영을 위해서는 개발 환경과 운영 환경을 분리하는 것이 필요하다는 것을 배웠습니다.
하지만 당시에는 이미 EC 인스턴스의 스펙을 확장하고 배포를 진행한 상태였기 때문에, 이를 즉시 도입하기에는 어려움이 있었습니다.
또한, 현재 프로세스는 새로운 기능 배포 시마다 서버를 재시작하는 문제가 있습니다.
이를 해결하기 위해서 무중단 배포 방식(Blue-Green Deployment)을 고민했습니다.
하지만 무중단 배포를 적용하기 위해서는 로드 밸런서 설정, 트래픽 제어 로직 설계 등 추가적인 인프라 구성이 필요했습니다.
때문에 이번 프로젝트에서는 도입하지 못하고 이후 학습을 통해 적용해 보기로 했습니다.
이번 프로젝트에서 배포를 직접 경험하면서 단순히 배포를 한다는 것 이상의 의미를 배울 수 있었습니다.
처음에는 배포가 어렵고 부담스럽게 느껴졌지만, 직접 경험하면서 배포에 대한 이해도가 향상될 수 있었고,
다음에는 더 나은 방식으로 배포할 수 있을 거라는 자신감도 생기고, 더 깊이 공부하고 적용해보고 싶다는 목표가 생기게 되었습니다.
'REFLECTION > 7️⃣ LUCKY7 트러블 슈팅' 카테고리의 다른 글
| [ 7️⃣ LUCKY-SEVEN ] JMeter와 함께한 병목 추적기 (0) | 2025.04.09 |
|---|---|
| [ 7️⃣ LUCKY-SEVEN ] 실시간 채팅 시스템 구조 재설계 이야기 (0) | 2025.03.08 |
| [ 7️⃣ LUCKY-SEVEN ] 로그인 시스템을 위한 JWT 고군분투기 (0) | 2025.02.24 |
