- ···
- 29.
[멀티캠퍼스] 풀스택 개발자 아카데미 (29) - Spring Boot & JWT (2) - 30.[멀티캠퍼스] 풀스택 개발자 아카데미 (30) - Deployment현재
이전 글까지는 React와 Spring Boot를 기반으로 Oracle DB와 연동하고, JPA를 이용한 데이터 처리, 그리고 JWT를 활용한 보안 인증까지 적용하여 실제 동작하는 풀스택 애플리케이션을 구현하는 과정을 다루었다.
하지만 제아무리 훌륭한 코드로 기능을 완성했다 하더라도, localhost를 벗어나 실제 사용자에게 서빙되지 않는다면 그저 개인 PC에 저장된 파일 뭉치에 불과하다. 이번 포스팅에서는 "내 컴퓨터에서는 되는데 서버에서는 안 되는" 고질적인 문제를 해결하고, 가장 현대적인 방법으로 애플리케이션을 배포하는 방법에 대해 정리한다. 핵심은 Docker를 통한 컨테이너화와 GitHub Container Registry(GHCR) 를 이용한 이미지 관리다.
1. 왜 Docker를 사용해야 하는가?
Docker는 애플리케이션을 실행하는 데 필요한 런타임, 시스템 도구, 라이브러리 등 모든 환경을 **'컨테이너(Container)'**라는 독립된 단위로 패키징하는 기술이다.
가상 머신(VM)은 OS 전체를 가상화하기 때문에 무겁고 느리지만, Docker는 Host OS의 커널을 공유하면서 프로세스만 격리하기 때문에 가볍고 빠르다. 우리가 앞서 개발한 Spring Boot와 React 앱을 Docker 이미지로 만들어두면, 서버가 Rocky Linux든 Ubuntu든 상관없이 로컬 개발 환경과 100% 동일한 실행을 보장받을 수 있다.
2. 전체 배포 아키텍처
구현할 배포 구조는 다음과 같다. 프론트엔드(React)는 빌드 후 정적 파일이 되므로 가벼운 웹 서버인 Nginx 컨테이너에 담아 서비스하고, 백엔드(Spring Boot)는 JDK가 포함된 컨테이너에서 실행한다. 이 둘은 Docker Network 혹은 Docker Compose를 통해 통신하며, 기존에 구축한 Oracle DB와 연결된다.
3. Spring Boot Dockerfile 작성 (Backend)
Spring Boot 프로젝트 루트에 Dockerfile을 생성한다. Spring Boot 역시 Multi-stage Build를 적용한다. 첫 번째 단계에서 Gradle을 이용해 빌드를 수행하고, 두 번째 단계에서는 빌드된 JAR 파일만 가져와 실행함으로써, 실행 환경에는 소스코드나 Gradle 없이 순수하게 JRE만 남겨 이미지 크기를 최소화한다. Oracle DB 연결 정보나 JWT Secret Key 같은 민감한 정보는 이미지에 박제하지 않고, 실행 시점의 환경변수(Environment Variable)로 주입받도록 설계한다.
4. React Dockerfile 작성 (Frontend)
React는 Multi-stage Build(멀티 스테이지 빌드) 전략을 사용해야 한다. Node.js 환경이 포함된 이미지는 용량이 크기 때문에, 빌드 단계에서 생성된 정적 파일(dist 폴더)만 추출하여 가벼운 Nginx 이미지로 옮기는 방식이다.
- Dockerfile 작성 예시:
- nginx.conf 작성 예시 (SPA 라우팅 처리):
5. GitHub Container Registry (GHCR) 준비
이미지를 저장할 레지스트리로 Docker Hub 대신 GHCR을 선택한다. GitHub 생태계 내에서 소스 코드와 이미지를 통합 관리할 수 있으며, Actions와의 연동성이 매우 뛰어나기 때문이다.
GHCR 사용을 위해 PAT(Personal Access Token) 발급이 필요하다.
- GitHub Settings > Developer settings > Personal access tokens (Classic)
write:packages,delete:packages권한 체크 후 토큰 생성
6. GitHub Actions를 이용한 CI/CD
v0.0.0 형태의 태그가 푸시될 때마다 자동으로 Docker 이미지를 빌드하고 GHCR에 푸시하는 워크플로우를 작성한다. 이는 프론트엔드 레포지토리와 백엔드 레포지토리 각각에 설정해야 한다.
태그를 기준으로 워크플로우를 실행하는 이유는, 작은 commit이나 브랜치 푸시마다 빌드가 발생하면 불필요한 리소스 낭비가 발생할 수 있기 때문이다. 이를 위해 .github/workflows/deploy.yaml 파일을 생성한다.
7. 서버 배포 (Docker Compose)
앞선 구성에서는 각 컨테이너의 포트를 호스트에 직접 노출(80:80, 8080:8080)시켰다. 하지만 실제 운영 환경에서는 단일 진입점(Entrypoint)을 통해 트래픽을 관리하고, 트래픽이 많아질 경우 백엔드 서버를 여러 대 띄워 부하를 분산(Load Balancing)해야 한다.
이를 위해 클라우드 네이티브 엣지 라우터인 Traefik을 도입한다. Traefik은 Docker 소켓을 감시하고 있다가 컨테이너가 뜨거나 죽을 때 자동으로 라우팅 규칙을 갱신한다. 별도의 설정 파일 재시작 없이 동적으로 설정이 변경되는 것이다.
다음은 Traefik을 포함하여 재구성한 docker-compose.yaml이다.
/api 경로로 시작하는 요청을 처리하도록 코드를 작성했다고 가정한다. 모든 컨트롤러 또는 WebConfig에서 /api 접두사를 붙여주어야 한다.서버 터미널에서 docker-compose up -d 명령어를 입력하면 모든 서비스가 기동된다.
마치며
이로써 React와 Spring Boot로 만든 애플리케이션을 Docker 이미지로 말아서 GHCR에 저장하고, 서버에서 실행하는 전체 과정을 정리했다.
이제 우리는 개발 환경과 배포 환경의 차이에서 오는 불확실성을 제거했으며, 언제든 코드를 수정하고 Push만 하면 새로운 버전이 배포될 준비를 마쳤다. 이를 통해 진정한 의미의 풀스택 개발자로 한 걸음 더 나아갈 수 있을 것이다.