본문 바로가기
Framework/Spring Boot

[Spring Boot] 무중단 배포 전략: Nginx와 블루/그린(Blue/Green) 아키텍처

반응형

새벽 3시, 개발자가 버그를 고치고 새로운 스프링 부트(Spring Boot) 버전(V2)을 배포(Deploy)하기 위해 서버에 접속합니다. 기존에 돌고 있던 V1 서버 프로세스를 kill 명령어로 죽이고, 새로운 V2 .jar 파일을 java -jar 명령어로 기동 시킵니다. 스프링의 거대한 프레임워크가 톰캣(Tomcat)을 띄우고 메모리에 빈(Bean)들을 올리는 데 약 10~15초가 소요됩니다. 문제는 이 15초 동안 사이트에 들어온 사용자들은 끔찍한 "502 Bad Gateway" 또는 "연결할 수 없음" 에러 페이지를 마주하게 된다는 것입니다. (다운타임 발생) 과거에는 고객들이 잠든 새벽에 서버를 내리고 작업(점검 시간)하는 것이 당연했지만, 24시간 글로벌로 돌아가는 현대의 서비스(넷플릭스, 쿠팡 등)에서 단 1초의 점검 시간이나 끊김(Downtime)은 곧 수억 원의 매출 증발을 의미합니다.

이번 포스팅에서는 "유저는 서버가 업데이트되었는지조차 눈치채지 못하게 하는 백엔드 인프라의 기적!" Nginx(엔진엑스) 리버스 프록시를 활용한 블루/그린(Blue/Green) 무중단 배포 아키텍처에 대하여 다뤄보겠습니다.

🔀 1. 무중단 배포의 핵심 원리: 문지기와 2개의 방

원리는 아주 간단합니다. 유저가 들어오는 문 앞에 빠릿빠릿한 문지기(Nginx)를 세워두고, 방을 2개(포트 8080, 8081) 준비하는 것입니다.

  1. 현재는 8080 포트(Blue)에서 구버전 V1 서버가 팽팽 잘 돌아가고 있습니다. 문지기(Nginx)는 모든 유저를 8080 포트로 안내합니다.
  2. 개발자가 신버전 V2를 배포하려고 합니다. 기존 8080을 죽이지 않고, 비어있는 8081 포트(Green)에 V2 서버를 몰래 조용히 구동(Run)시킵니다. (이 15초 동안에도 유저들은 8080 포트에서 정상적으로 서비스를 이용합니다.)
  3. 8081 포트의 V2 서버가 완전히 켜져서 /actuator/health 헬스 체크 응답을 "UP"으로 내려주면 준비가 완료된 것입니다.
  4. (핵심 💥) 문지기(Nginx)에게 "지금부터 들어오는 유저들은 8080 말고 8081(V2)로 안내해!"라고 명령(Reload)합니다. Nginx의 방향 전환은 0.1초도 걸리지 않습니다.
  5. 이제 유저들은 완벽한 V2 서버를 이용하게 되며, 역할이 끝난 8080 포트의 V1 서버는 조용히 죽여버립니다(Kill).

⚙️ 2. Nginx 리버스 프록시(Reverse Proxy) 세팅의 마법

Nginx는 C언어로 만들어진 미친 듯이 가볍고 빠른 웹 서버입니다. 앞서 말한 '방향 전환(스위칭)'을 하려면 Nginx의 설정 파일(nginx.conf)을 건드려야 합니다.

# /etc/nginx/conf.d/service-url.inc
# 현재 바라보고 있는 포트를 저장하는 변수 (스크립트로 이 파일을 8080 <-> 8081로 수정할 것입니다)
set $service_url http://127.0.0.1:8080;
# /etc/nginx/nginx.conf
server {
    listen 80; # 유저는 무조건 80(기본 웹 포트)로 들어옵니다.
    server_name my-awesome-domain.com;

    # 위에서 만든 변수 파일을 쏙 끼워 넣습니다.
    include /etc/nginx/conf.d/service-url.inc;

    location / {
        # 유저가 들어오면 문지기(Nginx)가 $service_url(8080 또는 8081)로 트래픽을 던져줍니다! (리버스 프록시)
        proxy_pass $service_url;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_addrs;
        proxy_set_header Host $http_host;
    }
}

📜 3. 쉘 스크립트(Shell Script)를 통한 100% 자동화

실무에서는 이 배포 과정을 젠킨스(Jenkins)나 Github Actions가 실행하는 리눅스 쉘 스크립트(deploy.sh)로 완벽하게 자동화합니다.

  • 1. 현재 작동 중인 포트 확인: Nginx가 8080을 바라보고 있다면, 새 버전은 8081에 띄우기로 결정합니다.
  • 2. 새 버전 구동: nohup java -jar -Dserver.port=8081 app-v2.jar & 명령어로 8081 포트에 서버를 띄웁니다.
  • 3. 헬스 체크 루프: curl http://127.0.0.1:8081/actuator/health를 1초마다 찔러보며 응답이 올 때까지 대기합니다.
  • 4. Nginx 리로드(Switching): 서버가 완벽히 뜨면, 아까 만든 service-url.inc의 숫자를 8080에서 8081로 바꾸고, sudo service nginx reload 명령어를 날려 Nginx가 끊김 없이 방향을 틀게 만듭니다. (Restart가 아니라 Reload 임에 주의하세요! Reload는 기존 연결을 끊지 않고 부드럽게 설정만 새로 덮어씁니다.)

🎯 4. 마무리 및 다음 단계

지금까지 서버 업데이트 시 필연적으로 발생하던 "502 Bad Gateway" 에러 화면을 역사 속으로 지워버리고, Nginx의 리버스 프록시와 두 개의 포트를 활용하여 단 0.1초의 끊김도 없이 부드럽게 신규 버전을 서비스하는 '무중단 블루/그린 아키텍처'에 대해 다루어 보았습니다. 이제 여러분은 24시간 언제 배포 버튼을 눌러도 유저의 컴플레인을 받지 않는 자유를 얻었습니다.

자, 드디어 대망의 최종 파트인 '최신 인프라와 트러블슈팅' 영역으로 넘어갑니다. 지금까지 우리는 .jar 파일을 만들어서 EC2 리눅스 서버에 직접 자바(Java)를 깔고 배포했습니다. 그런데 개발자의 로컬은 Windows고 운영 서버는 Linux라서, "내 PC에선 잘 도는데 서버에선 안 돌아요!"라는 고전적인 변명이 발생하기 시작합니다. 이 지긋지긋한 '환경 불일치'의 고통을 찢어버리고 "소스코드뿐만 아니라 자바, OS 환경까지 통째로 압축해서 캡슐처럼 배포하자!"는 가상화 혁명이 불어닥쳤습니다. 이어지는 26단계 포스팅에서는 "클라우드 네이티브의 절대 표준!" Docker(도커)를 활용해 Spring Boot 애플리케이션을 완벽하게 컨테이너화하는 최적의 Dockerfile 작성 비법에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형