BlueNyang
[멀티캠퍼스] 풀스택 개발자 아카데미 (30) - Deployment
Java 풀스택 아카데미
· 멀티캠퍼스 JAVA 풀스택 개발자 아카데미 6회차 (30편)

[멀티캠퍼스] 풀스택 개발자 아카데미 (30) - Deployment

BlueNyangBlueNyang
·
·
약 4분
·
# 부트캠프후기# 멀티캠퍼스it부트캠프# [현대이지웰] JAVA 풀스택 개발자 아카데미 6회차# docker# github-actions# traefik# docker-compose
시리즈·멀티캠퍼스 JAVA 풀스택 개발자 아카데미 6회차(30개의 글)
  • ···
  • 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% 동일한 실행을 보장받을 수 있다.

VM vs Docker Container 아키텍처 비교 다이어그램
VM vs Docker Container 아키텍처 비교 다이어그램

2. 전체 배포 아키텍처

구현할 배포 구조는 다음과 같다. 프론트엔드(React)는 빌드 후 정적 파일이 되므로 가벼운 웹 서버인 Nginx 컨테이너에 담아 서비스하고, 백엔드(Spring Boot)는 JDK가 포함된 컨테이너에서 실행한다. 이 둘은 Docker Network 혹은 Docker Compose를 통해 통신하며, 기존에 구축한 Oracle DB와 연결된다.

React(Nginx) <-> Spring Boot <-> Oracle DB 전체 구조도
React(Nginx) <-> Spring Boot <-> 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)로 주입받도록 설계한다.

dockerfile
# Base Image: 경량화된 Alpine 리눅스 기반의 JDK 21
FROM eclipse-temurin:21-jdk AS builder

# 필요한 패키지 설치 (find 명령어 등)
RUN apt-get update && apt-get install -y findutils && rm -rf /var/lib/apt/lists/*

# 작업 디렉토리 설정
WORKDIR /workspace

# Gradle Wrapper 복사 및 의존성 캐싱
COPY gradlew .
COPY gradle ./gradle
# 권한 부여
RUN chmod +x gradlew

# 소스 코드 복사
COPY build.gradle .
COPY settings.gradle .

# 의존성 다운로드 (캐싱 목적)
RUN ./gradlew dependencies

# 소스 전체 복사
COPY src ./src

# JAR 파일 빌드
RUN ./gradlew clean bootJar --no-daemon

# Production Image
FROM eclipse-temurin:21-jdk

WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=builder /workspace/build/libs/*.jar app.jar

# 애플리케이션 포트 노출
EXPOSE 8080

# 실행 명령어
ENTRYPOINT ["java", "-jar", "/app.jar"]
VS Code 프로젝트 구조 및 Dockerfile 위치
VS Code 프로젝트 구조 및 Dockerfile 위치
IntelliJ 프로젝트 구조 및 Dockerfile 위치
IntelliJ 프로젝트 구조 및 Dockerfile 위치

4. React Dockerfile 작성 (Frontend)

React는 Multi-stage Build(멀티 스테이지 빌드) 전략을 사용해야 한다. Node.js 환경이 포함된 이미지는 용량이 크기 때문에, 빌드 단계에서 생성된 정적 파일(dist 폴더)만 추출하여 가벼운 Nginx 이미지로 옮기는 방식이다.

  • Dockerfile 작성 예시:
dockerfile
# Node 22-slim 이미지를 베이스로 사용
FROM node:22-slim AS builder
WORKDIR /app

# package.json 및 package-lock.json 복사 후 의존성 설치
COPY package*.json ./
RUN npm ci

# 소스 코드 복사
COPY . .
RUN npm run build

# NginX를 베이스로 하는 최종 이미지
FROM nginx:1.29.3-alpine-slim AS final

# 빌드 결과물을 Nginx의 서빙 디렉토리로 복사
COPY --from=builder /app/dist /usr/share/nginx/html

# SPA 라우팅 처리를 위한 Nginx 설정 파일 복사
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  • nginx.conf 작성 예시 (SPA 라우팅 처리):
conf
server {
  listen       80;
  server_name  localhost;

  root   /usr/share/nginx/html;
  index  index.html index.htm;

  location / {
    try_files $uri $uri/ /index.html;
  }
}
Multi-stage Build의 개념도
Multi-stage Build의 개념도

5. GitHub Container Registry (GHCR) 준비

이미지를 저장할 레지스트리로 Docker Hub 대신 GHCR을 선택한다. GitHub 생태계 내에서 소스 코드와 이미지를 통합 관리할 수 있으며, Actions와의 연동성이 매우 뛰어나기 때문이다.

GHCR 사용을 위해 PAT(Personal Access Token) 발급이 필요하다.

  1. GitHub Settings > Developer settings > Personal access tokens (Classic)
  2. write:packages, delete:packages 권한 체크 후 토큰 생성
GitHub PAT 생성 및 권한 설정 화면
GitHub PAT 생성 및 권한 설정 화면

6. GitHub Actions를 이용한 CI/CD

v0.0.0 형태의 태그가 푸시될 때마다 자동으로 Docker 이미지를 빌드하고 GHCR에 푸시하는 워크플로우를 작성한다. 이는 프론트엔드 레포지토리와 백엔드 레포지토리 각각에 설정해야 한다.

태그를 기준으로 워크플로우를 실행하는 이유는, 작은 commit이나 브랜치 푸시마다 빌드가 발생하면 불필요한 리소스 낭비가 발생할 수 있기 때문이다. 이를 위해 .github/workflows/deploy.yaml 파일을 생성한다.

yaml
name: Docker Build & Push

on:
  push:
    tags:
      - "v*.*.*"
  # 수동으로 배포할 수도 있도록 워크플로우 디스패치 허용
  workflow_dispatch:

# 같은 작업이 중복 실행되지 않도록 설정(취소 정책)
concurrency:
  group: "docker-build-and-push"
  cancel-in-progress: true

env:
  # project_name 부분은 본인 레포지토리명으로 변경
  # repository_owner는 본인의 Github 사용자 명이지만, 만약 대문자가 포함되어 있다면 소문자로 기재해야 한다.
  IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/project-name

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      # GHCR 로그인
      - name: Log in to the Container registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_TOKEN }}

      # Spring Boot 이미지 빌드 및 푸시
      - name: Build and push Spring Boot
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.IMAGE_NAME }}:latest
Frontend: GitHub Actions 워크플로우 성공 로그
Frontend: GitHub Actions 워크플로우 성공 로그
Backend: GitHub Actions 워크플로우 성공 로그
Backend: GitHub Actions 워크플로우 성공 로그

7. 서버 배포 (Docker Compose)

앞선 구성에서는 각 컨테이너의 포트를 호스트에 직접 노출(80:80, 8080:8080)시켰다. 하지만 실제 운영 환경에서는 단일 진입점(Entrypoint)을 통해 트래픽을 관리하고, 트래픽이 많아질 경우 백엔드 서버를 여러 대 띄워 부하를 분산(Load Balancing)해야 한다.

이를 위해 클라우드 네이티브 엣지 라우터인 Traefik을 도입한다. Traefik은 Docker 소켓을 감시하고 있다가 컨테이너가 뜨거나 죽을 때 자동으로 라우팅 규칙을 갱신한다. 별도의 설정 파일 재시작 없이 동적으로 설정이 변경되는 것이다.

다음은 Traefik을 포함하여 재구성한 docker-compose.yaml이다.

Note
다음 설정에서는 백엔드(Spring Boot)가 /api 경로로 시작하는 요청을 처리하도록 코드를 작성했다고 가정한다. 모든 컨트롤러 또는 WebConfig에서 /api 접두사를 붙여주어야 한다.
yaml
services:
  # 1. Traefik (Reverse Proxy & Load Balancer)
  reverse-proxy:
    image: traefik:v3.6
    command:
      - "--api.insecure=true" # 대시보드 접근 허용 (보안상 운영환경에서는 false 권장)
      - "--providers.docker=true" # Docker 프로바이더 활성화
      - "--providers.docker.exposedbydefault=false" # 레이블이 있는 컨테이너만 라우팅
      - "--entrypoints.web.address=:80" # 80 포트로 진입
    ports:
      - "80:80" # 웹 서비스 포트
      - "8080:8080" # Traefik 대시보드 포트
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro # Docker 이벤트 감지용
    # 같은 도커 네트워크의 DB 컨테이너 사용 시
    extra_hosts:
      - "host.docker.internal:host-gateway"

  # 2. Backend (Spring Boot)
  backend:
    image: ghcr.io/사용자명/my-backend:latest
    # ports 섹션 제거: Traefik이 내부적으로 통신하므로 호스트 포트 노출 불필요
    deploy:
      replicas: 2 # 로드 밸런싱 테스트를 위해 2개의 컨테이너 실행
    environment:
      # 로컬 DB 연결 시 (리눅스 서버에서는 extra_hosts 설정 필수)
      - SPRING_DATASOURCE_URL=jdbc:oracle:thin:@host.docker.internal:1521:xe
      # 만약 DB도 Docker 컨테이너라면: jdbc:oracle:thin:@oracle-container:1521:xe
      # 외부 DB 연결 시: jdbc:oracle:thin:@dbserver.example.com:1521:xe
      - SPRING_DATASOURCE_USERNAME=myuser
      - SPRING_DATASOURCE_PASSWORD=mypassword
      - JWT_SECRET=mysecretkey
    extra_hosts:
      - "host.docker.internal:host-gateway"
    labels:
      - "traefik.enable=true"
      # /api 로 시작하는 요청은 이 서비스로 라우팅
      - "traefik.http.routers.backend.rule=PathPrefix(`/api`)"
      - "traefik.http.services.backend.loadbalancer.server.port=8080" # 컨테이너 내부 포트

  # 3. Frontend (React + Nginx)
  frontend:
    image: ghcr.io/사용자명/my-frontend:latest
    depends_on:
      - backend
    labels:
      - "traefik.enable=true"
      # 그 외 모든 요청(/)은 프론트엔드로 라우팅
      - "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
      - "traefik.http.services.frontend.loadbalancer.server.port=80" # 컨테이너 내부 포트

서버 터미널에서 docker-compose up -d 명령어를 입력하면 모든 서비스가 기동된다.

서버 터미널에서 docker ps로 실행 상태 확인
서버 터미널에서 docker ps로 실행 상태 확인

마치며

이로써 React와 Spring Boot로 만든 애플리케이션을 Docker 이미지로 말아서 GHCR에 저장하고, 서버에서 실행하는 전체 과정을 정리했다.

이제 우리는 개발 환경과 배포 환경의 차이에서 오는 불확실성을 제거했으며, 언제든 코드를 수정하고 Push만 하면 새로운 버전이 배포될 준비를 마쳤다. 이를 통해 진정한 의미의 풀스택 개발자로 한 걸음 더 나아갈 수 있을 것이다.

BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus