노트

[SolveGO #17] 땅을 다지는 기초공사 - Docker 패키징 & 수동 배포 본문

Project/SolveGO

[SolveGO #17] 땅을 다지는 기초공사 - Docker 패키징 & 수동 배포

forwarder 2026. 7. 10. 18:47

 
 
 

 
 
 
이제 SolveGO 프로젝트의 CI까지 성공적으로 구축해두었으며, 따라서 모든 커밋을 올릴 때마다 Test code에 대한 수행 결과와 Test Coverage를 리포트로 확인할 수 있게 되었다. 또한 main 브런치에 안전한 커밋들만 merge 시킬 수 있게하여, 보다 안정적인 개발 환경을 마련했다.
 
한편, 지금까지의 프로젝트는 오직 나의 컴퓨터에서만 실행된다. 이는 프로그램이 내 개발 환경에만 존재한다는 의미이며, 결국 물리적으로 나의 컴퓨터를 사용할 수 있는 사람 외에는 누구도 SolveGO를 이용할 수 없다(GitHub에서 소스코드를 받아서 직접 실행하는 것이 아닌 브라우저를 통한 즉시 이용). 아무리 뛰어난 프로젝트라 하더라도, 사용자가 실제로 접근할 수 없다면 서비스로서의 가치는 퇴색되는 법. 따라서 다음 단계는 이 프로젝트를 내 컴퓨터 밖의 서버에서 실행되도록 하여 인터넷을 통해 누구나 접근할 수 있도록 만드는 배포일 것이다.
 
그렇다면, 지금부터 풀어야 하는 문제는 다음과 같을 것이다. 

이 프로젝트를 내 컴퓨터 밖의 서버에서 실행되도록 하자

 
 
문제는 정의되었으니 이 문제를 풀기 위해 문제를 자세히 관찰해보자. 문제에서 프로젝트는 현재까지 작성된 소스코드를 뜻한다. 그리고 내 컴퓨터 밖의 서버는 임의의 서버가 될 수 있다. 다시 말해 현재 내 mac과는 다른 h/w를 가지고 있을 것이고 당연히 macOS 운영체제로 동작하리라는 보장 또한 없다. 마지막으로 실행이라는 의미는 뭘까. 단순히 소스코드가 서버에 복사되어 있는 상태를 뜻하지는 않는다. 애플리케이션이 해당 서버의 운영체제와 실행 환경 위에서 정상적으로 프로세스로 동작하고 필요한 데이터베이스와 통신하며 외부 사용자의 요청을 받아 응답할 수 있는 상태를 의미한다.
 
따라서 우리는 지금까지의 소스코드를 다른 서버에 옮기고, 더 나아가 해당 서버에도 Java, MySQL, 환경변수, 네트워크 설정 등 프로젝트 실행에 필요한 환경을 동일하게 준비하면 될 것이다. 위 문제는 다음과 같이 한 걸음 더 구체화 시킬 수 있다.
 
 

서로 다른 하드웨어와 운영체제를 가진 서버에서도,
현재 개발 환경과 동일한 방식으로 실행될 수 있도록 하자.

 
 
그러면 가장 먼저 떠오르는 방법은 새로운 서버에 현재 Mac과 동일한 실행 환경을 직접 구성하는 것이다. 서버 앞에 가서 Java 버전 맞추고, MySQL을 설치하고, DB 관리자 계정과 JWT Secret Key 등의 환경변수를 설정한 뒤에 이들이 서로 통신할 수 있도록 포트 번호를 맞추고 애플리케이션을 실행하면 된다. 그러나 이는 그다지 현명한 방법이 아니다. 왜냐하면 새로운 서버를 증설하고자 할 때마다 수작업으로 실행 환경을 직접 구성해주어야 하고, 심지어 macOS, Window, Linux 등등의 여러 운영체제 위에서 실행 환경을 동일하게 맞추는 작업은 여간 쉬운 일이 아니기 때문이다.
 
이는 서버마다 "서로 다른 운영체제" 위에서 동일한 애플리케이션을 실행하려다 보니 생겨난 문제이다. 기반이 되는 땅의 모양과 성질이 다른데, 그 위에 동일한 건축물을 세우려 하다보니 문제가 복잡해지는 것이다. 그렇다면 어떻게 하면 문제를 단순화 시킬 수 있을까? "서로 다른 운영체제"여도 "동일한 기반" 위에서 실행되는 것처럼 바꿀 수 있을까? 건축에서는 건물을 올리기 전에 땅을 균일하게 다지고 콘크리트를 타설하는 기초공사가 있다. 그 밑이 단단한 화강암 지반이든, 부드러운 점토 지반이든 간에 일단 기초공사를 마치면 그 위에는 동일한 방식으로 건물을 올릴 수 있다.
 

 
개발에서도 기초공사와 비슷한 역할을 하는 기술이 존재한다. 서로 다른 기반 위에서도 애플리케이션이 동일한 실행 환경에서 동작하도록 만들어 주는 가상화(Virtualization) 기술이다. 대표적인 방법으로는 Virtual Machine과 Docker(Container) 가 있다. 둘 다 실행 환경을 표준화한다는 공통점이 있지만, 그 방법은 서로 다르다. Virtual Machine은 하이퍼바이저 위에서 운영체제 전체를 하나 더 실행하는 방식이다. 운영체제와 커널까지 함께 가상화하기 때문에 서로 다른 운영체제에서도 동일한 환경을 제공할 수 있지만, 그만큼 메모리와 저장 공간을 많이 사용하고 실행 비용도 크다. 반면 Docker는 운영체제 전체를 가상화하지 않는다. Docker Engine이 실행되는 운영체제의 커널을 공유하면서, 애플리케이션과 실행에 필요한 라이브러리만을 컨테이너에 포함한다. 따라서 Virtual Machine보다 훨씬 가볍고 빠르게 실행할 수 있으며, 오늘날 애플리케이션 배포에서 가장 널리 사용되는 방식이 되었다.
 
그렇다면 임의의 서버에 Docker Engine만 설치되어 있다면, 적은 비용으로 동일한 실행 환경을 제공할 수 있다. 다시 말해 현재 macOS에서 실행되는 애플리케이션을 Docker Engine 위에서 실행될 수 있는 형태로 패키징한다면, Docker Engine이 설치된 어떤 서버에서도 동일한 방식으로 실행할 수 있을 것이다. 따라서 앞서 정의한 문제는 다음과 같이 해결할 수 있다.
현재 macOS에서 실행되는 애플리케이션을 Docker 이미지로 패키징하고, 이를 Docker Engine이 설치된 서버에서 실행하자.
 

 
 
 


 
 
 

Docker 이미지로 패키징

 
현재 SolveGO는 macOS 위에서 Spring Boot와 MySQL이 직접 실행되고 있다. 이러한 구조는 개발 환경에서는 문제가 없지만, 다른 서버에서도 동일하게 실행하기 위해서는 해당 서버에도 동일한 실행 환경을 직접 구성해야 한다. 따라서 먼저 현재의 실행 환경을 Docker Image로 패키징하여, Docker Engine 위에서 실행될 수 있는 형태로 변경하자.
 
먼저 Gradle을 이용해 소스코드를 빌드하고, Spring Boot 애플리케이션을 실행 가능한 bootJar 형태로 만든다. 이후 생성된 JAR 파일을 Java 17 실행 환경이 포함된 Docker Image 안에 넣고 실행하도록 Dockerfile을 작성하자. Dockerfile은 빌드 환경과 실행 환경을 분리하는 멀티 스테이지 빌드 방식으로 작성하였다.

FROM eclipse-temurin:17-jdk AS build

WORKDIR /app

COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src

RUN chmod +x ./gradlew
RUN ./gradlew clean bootJar --exclude-task test


FROM eclipse-temurin:17-jre

WORKDIR /app

COPY --from=build /app/build/libs/*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

 
이 Dockerfile을 빌드하면 아래와 같은 Docker 이미지가 생성된다.

 
 
앞서 작성한 Dockerfile을 통해 Spring Boot 애플리케이션을 실행할 수 있는 Docker Image를 만들 수 있게 되었다. 그러나 SolveGO는 Spring Boot 애플리케이션만으로 동작하지 않는다. 애플리케이션이 데이터를 저장하고 조회하기 위해서는 MySQL도 함께 실행되어야 한다. 따라서 다음으로 해야 할 일은 Spring Boot 컨테이너와 MySQL 컨테이너를 각각 실행하고, 두 컨테이너가 서로 통신할 수 있도록 구성하는 것이다. 이를 위해 docker-compose.yml을 작성하자.

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: solvego-app
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_URL: jdbc:mysql://db:3306/solvego
      DB_USERNAME: ${DB_USERNAME}
      DB_PASSWORD: ${DB_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mysql:8.0
    container_name: solvego-db
    ports:
      - "3307:3306"
    environment:
      MYSQL_DATABASE: solvego
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - solvego-db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 10

volumes:
  solvego-db-data:

 
 
위 설정에서는 앞서 작성한 Dockerfile을 이용해 Spring Boot 컨테이너를 생성하고, 공식 MySQL 이미지를 이용해 데이터베이스 컨테이너를 생성했다. 그리고 그들이 동일한 네트워크에서 통신할 수 있도록 구성하고, 환경변수와 포트, 데이터 저장소 등을 함께 설정했다. 이제 다음 명령어 하나만으로 Spring Boot와 MySQL을 동시에 실행할 수 있다.
 

docker compose up -d
 
 
명령 실행 이후 docker compose ps로 컨테이너 상태를 확인하면, 아래와 같이 Spring boot와 SQL 컨테이너가 모두 정상적으로 실행되고 있음을 확인할 수 있다.

 

docker compose ps
NAME          IMAGE         COMMAND                  SERVICE   CREATED          STATUS                    PORTS
solvego-app   solvego-app   "java -jar app.jar"      app       7 minutes ago    Up 7 minutes              0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp
solvego-db    mysql:8.0     "docker-entrypoint.s…"    db        10 minutes ago   Up 10 minutes (healthy)   0.0.0.0:3307->3306/tcp, [::]:3307->3306/tcp

 

 

 
 

이어서 curl로 회원가입 요청을 보내면 애플리케이션이 정상적으로 요청을 처리하는 것도 확인할 수 있다.

 

curl --include --request POST http://localhost:8080/api/users \
  --header "Content-Type: application/json" \
  --data '{"username":"dockeruser","password":"1234"}'
HTTP/1.1 201
...

 

 

 

 
마지막으로 애플리케이션 상태 확인을 위해 Spring Boot Actuator를 추가했다. 아래 명령어로 헬스 체크 엔드포인트를 호출하면 
curl --include http://localhost:8080/actuator/health
{"status":"UP"}

Spring Boot 애플리케이션이 정상적으로 실행 중임을 의미하는 응답을 받을 수 있다.

 

 

 
 
 
이로써 나의 로컬 서버 머신인 macbook에서 docker engine 위에 Spring Application container와 MySQL container가 성공적으로 동작하는 것을 확인하였다. 이는 Docker를 이용한 실행 구성이 정상적으로 동작한다는 의미이다. 이제 남은 것은 외부 서버에 Docker Engine을 설치하고, 동일한 Dockerfile과 Docker Compose 설정을 이용하여 이미지를 다시 빌드하고 실행하는 것이다.
 
 

 
 
 

외부 서버(EC2)에 배포

 
 
AWS 콘솔에서 EC2 instance를 launch하고, SSH를 이용하여 해당 EC2에 접속하자. 이후 EC2에 도커와 깃허브에 올린 소스코드를 clone하여 실행하면 된다.
 

SSH를 이용하여 서버에 접속하자.

ssh -i ~/.ssh/solvego-ec2-key.pem ubuntu@<EC2_PUBLIC_IP>

 

먼저 Docker와 Git이 정상적으로 설치되어 있는지 확인하였다.

docker --version
docker compose version
git --version

 

Docker 서비스가 실행 중인지도 함께 확인하자.

sudo systemctl status docker

 

 

 

이제 GitHub에 업로드한 SolveGO 프로젝트를 EC2로 가져오자.

git clone https://github.com/...
cd solvego
git pull origin main

 

Git에는 포함되지 않는 민감한 정보들은 .env 파일에서 관리하였다.

cp .env.example .env
nano .env

 

 

.env에는 데이터베이스 계정 정보와 JWT Secret 등을 입력한다.

DB_USERNAME=...
DB_PASSWORD=...
MYSQL_ROOT_PASSWORD=...
JWT_SECRET=...

 

 

환경 구성이 끝났다면 Docker Compose를 이용하여 Spring Boot와 MySQL을 동시에 실행한다.

docker compose up --build -d

 

이 명령은 Dockerfile을 이용해 Spring 애플리케이션 이미지를 빌드하고, MySQL 컨테이너와 함께 백그라운드에서 실행한다.

실행 후 컨테이너 상태를 확인하자.

docker compose ps

 

 

정상적으로 실행되면 다음과 같이 두 컨테이너가 모두 실행 중인 것을 확인할 수 있다.

solvego-app   Up
solvego-db    Up (healthy)

 

먼저 EC2 내부에서 애플리케이션이 정상적으로 실행되었는지 확인하자.

curl http://localhost:8080/actuator/health
{"status":"UP"}

정상적으로 실행되면 위와 같은 응답이 반환된다.(위에서 언급한 Spring Boot Actuator의 헬스체크)

 

 

이후 외부에서도 동일하게 접근 가능한지 확인하자.

curl http://<EC2_PUBLIC_IP>:8080/actuator/health

브라우저에서도 동일한 주소로 접속하여 정상 응답을 확인할 수 있다. 참고로 외부에서 접근하려면 EC2 Security Group에서 8080 포트가 허용되어 있어야 한다.

 

 

배포가 완료된 이후에는 실제 API가 정상적으로 동작하는지 확인해 볼 수 있다. 

curl --include --request POST http://<EC2_PUBLIC_IP>:8080/api/users \
  --header "Content-Type: application/json" \
  --data '{"username":"deployuser","password":"1234"}'

회원가입 API를 호출하여 새로운 사용자를 생성하고, 201 Created 응답을 확인하였다. 

 

 

curl --include --request POST http://<EC2_PUBLIC_IP>:8080/api/auth/login \
  --header "Content-Type: application/json" \
  --data '{"username":"deployuser","password":"1234"}'

이후 로그인 API도 호출하여 JWT가 정상적으로 발급되는 것을 확인함으로써, 배포가 정상적으로 완료된 것을 확인할 수 있었다.

 
 
 
 

 
 
따라서 이제는 서론에 말한 바와 같이, 기반이 되는 운영체제가 무엇이든 간에 Docker Engine 이라는 기초공사만 되어 있다면 그 위에 우리의 SolveGO 프로젝트가 배포될 수 있는 상태가 되었다. 이제 EC2 인스턴스를 켜놓기만 한다면 24시간 내내 모든 사용자들이 서비스에 접근할 수 있게 되는 것이다. 다만 AWS는 인스턴스 실행 시간에 따라 비용이 발생하므로 항상 실행해 둘 예정은 아니다. 한편 프로젝트의 전체 소스코드는 아래 GitHub Repository에서 확인할 수 있다.
 
 

 

GitHub - SolveGO/solvego-backend

Contribute to SolveGO/solvego-backend development by creating an account on GitHub.

github.com

 
 
 
한편, 이렇게 배포를 마친 뒤 수정 사항이 발생하면 어떻게 될까? 지난 글에서 CI를 구축하면서 코드를 GitHub에 Push하면 테스트가 자동으로 돌고, 안전한 Commit만 Main Branch에 Merge되도록 만들었다. 그러나 현재는 Main Branch가 변경될 때마다 EC2에 직접 접속하여 GitHub에서 최신 코드를 다시 Pull한 뒤 애플리케이션을 재실행해야 한다. 이러한 수작업은 실제 서비스의 배포 과정에서는 반복되는 불필요한 작업이다. 따라서 다음 글에서는 이 과정을 자동화하여, Main Branch에 변경 사항이 반영되면 서버에도 자동으로 배포되는 CD(Continuous Deployment)를 구축해 보자.