# Docker & Docker Compose를 활용한 애플리케이션 배포 가이드

## 1. Docker 기본 개념

### 1.1 Docker란 무엇인가?

Docker는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 컨테이너 기반의 오픈소스 가상화 플랫폼입니다. 애플리케이션을 해당 환경과 함께 "컨테이너"라는 격리된 공간에 패키징하여, 개발 환경과 운영 환경의 차이로 인해 발생하는 문제를 해결합니다.

### 1.2 이미지(Image)와 컨테이너(Container)

-   **이미지(Image)**: 애플리케이션을 실행하는 데 필요한 모든 것(코드, 런타임, 시스템 도구, 라이브러리 등)을 포함하는 읽기 전용 템플릿입니다. 이미지는 계층(Layer)으로 구성되어 있어 효율적인 빌드와 배포가 가능합니다.
-   **컨테이너(Container)**: 이미지의 실행 가능한 인스턴스입니다. 하나의 이미지로 여러 개의 독립적인 컨테이너를 생성할 수 있으며, 각 컨테이너는 격리된 환경에서 실행됩니다.

### 1.3 Dockerfile 작성법

`Dockerfile`은 Docker 이미지를 생성하기 위한 지침이 담긴 텍스트 파일입니다. Docker는 이 파일을 읽어 순서대로 명령을 실행하여 이미지를 빌드합니다.

주요 `Dockerfile` 명령어는 다음과 같습니다. ([공식 문서 참고](https://docs.docker.com/reference/dockerfile/))

-   `FROM`: 생성할 이미지의 기반이 될 베이스 이미지를 지정합니다. (예: `FROM python:3.9-slim`)
-   `WORKDIR`: `RUN`, `CMD`, `ENTRYPOINT`, `COPY`, `ADD` 명령이 실행될 작업 디렉토리를 설정합니다.
-   `COPY` 또는 `ADD`: 호스트 머신의 파일이나 디렉토리를 컨테이너 안으로 복사합니다.
-   `RUN`: 이미지 빌드 과정에서 필요한 스크립트나 명령을 실행합니다. 주로 애플리케이션 의존성 설치 등에 사용됩니다.
-   `CMD` 또는 `ENTRYPOINT`: 컨테이너가 시작될 때 실행될 기본 명령을 설정합니다.
-   `EXPOSE`: 컨테이너가 외부에 노출할 포트를 지정합니다.

**Dockerfile 예시 (`Dockerfile` - Spring Boot)**

```dockerfile
# 1단계: Maven을 사용하여 애플리케이션 빌드 (빌드 환경)
FROM maven:3.8-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

# 2단계: 빌드된 애플리케이션을 실행 환경으로 복사
FROM eclipse-temurin:17-jre-focal
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar

# 컨테이너가 8080번 포트를 외부에 노출
EXPOSE 8080

# 컨테이너 시작 시 JAR 파일 실행
ENTRYPOINT ["java", "-jar", "app.jar"]
```

## 2. Docker Compose 활용

### 2.1 Docker Compose란 무엇인가?

Docker Compose는 여러 개의 컨테이너로 구성된 애플리케이션을 정의하고 실행하기 위한 도구입니다. `docker-compose.yml`이라는 YAML 파일을 사용하여 애플리케이션의 서비스, 네트워크, 볼륨 등을 설정할 수 있습니다. 이를 통해 복잡한 애플리케이션도 단일 명령어로 쉽게 관리할 수 있습니다.

### 2.2 `docker-compose.yml` 작성법

-   `version`: 사용할 Docker Compose 파일 형식을 지정합니다. (현재는 `services` 등 최상위 요소만으로 구성 가능)
-   `services`: 애플리케이션을 구성하는 각 컨테이너(서비스)를 정의합니다.
    -   `image`: 서비스의 기반이 될 Docker 이미지를 지정합니다.
    -   `build`: Docker 이미지를 직접 빌드할 때 `Dockerfile`이 있는 경로를 지정합니다.
    -   `ports`: `HOST:CONTAINER` 형식으로 호스트와 컨테이너 간의 포트를 매핑합니다.
    -   `volumes`: `HOST:CONTAINER` 형식으로 호스트와 컨테이너 간의 디렉토리나 파일을 공유(마운트)합니다.
    -   `environment`: 컨테이너 내에서 사용할 환경 변수를 설정합니다.
-   `networks`: 서비스들이 사용할 네트워크를 정의합니다.
-   `volumes`: 데이터를 영속적으로 저장하기 위한 볼륨을 정의합니다.

**docker-compose.yml 예시 (`docker-compose.yml` - Spring Boot)**

```yaml
services:
  web:
    build: .
    container_name: my_java_app
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=development
```

위 예시는 `web` 서비스를 정의합니다.
-   `web` 서비스는 현재 디렉토리의 `Dockerfile`을 사용하여 이미지를 빌드하고, 호스트의 8080번 포트를 컨테이너의 8080번 포트와 연결합니다.
-   `container_name`으로 컨테이너의 이름을 지정하고, `environment`를 통해 스프링 부트의 `development` 프로파일을 활성화합니다.

## 3. Shell Script를 이용한 배포 자동화

### 3.1 왜 Shell Script를 사용하는가?

애플리케이션을 배포하는 과정은 일반적으로 다음과 같은 반복적인 작업들을 포함합니다.

1.  최신 소스코드 가져오기
2.  Docker 이미지 빌드
3.  기존 컨테이너 중지 및 삭제
4.  새로운 컨테이너 실행
5.  불필요한 리소스 정리

이러한 과정들을 Shell Script로 작성해두면, 단일 명령어 실행만으로 전체 배포 과정을 자동화할 수 있어 실수를 줄이고 효율성을 높일 수 있습니다.

### 3.2 배포 스크립트 예시 (`deploy.sh`)

```bash
#!/bin/bash

# 스크립트 실행 중 오류 발생 시 즉시 중단
set -e

echo "배포 스크립트를 시작합니다..."

# 1. 최신 버전의 소스 코드 받기 (main 브랜치 기준)
echo "최신 소스코드를 가져옵니다..."
git pull origin main

# 2. Docker 이미지 빌드 (캐시 사용 안함)
echo "Docker 이미지를 빌드합니다..."
docker-compose build --no-cache

# 3. 기존 컨테이너 중지 및 삭제
echo "기존 컨테이너를 중지하고 삭제합니다..."
docker-compose down

# 4. 새로운 컨테이너 실행 (백그라운드에서 실행)
echo "새로운 컨테이너를 실행합니다..."
docker-compose up -d

# 5. 불필요한 Docker 이미지 정리
echo "불필요한 Docker 이미지를 정리합니다..."
docker image prune -f

echo "배포가 성공적으로 완료되었습니다!"
```

**스크립트 실행 권한 부여:**
스크립트를 실행하기 전에 실행 권한을 부여해야 합니다.
```bash
chmod +x deploy.sh
```

**스크립트 실행:**
```bash
./deploy.sh
```

## 4. 실전 예제: 간단한 Java Spring Boot 웹 애플리케이션 배포

아래는 Java 17과 내장 Tomcat을 사용하는 Spring Boot 웹 애플리케이션을 Docker, Docker Compose, 그리고 배포 스크립트로 배포하는 전체 예제입니다.

**프로젝트 구조:**

```
-/my-java-project
-|   |-- pom.xml
-|   |-- src
-|   |   |-- main
-|   |       |-- java
-|   |           |-- com/example/demo
-|   |               |-- DemoApplication.java
-|   |               |-- HelloController.java
|-- Dockerfile
|-- docker-compose.yml
|-- deploy.sh
```

**파일 내용:**

1.  **`pom.xml`** (Maven 빌드 설정 파일)
    ```xml
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.1.5</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>demo</name>
        <description>Demo project for Spring Boot</description>
        <properties>
            <java.version>17</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    ```

2.  **`DemoApplication.java`** (Spring Boot 메인 애플리케이션)
    ```java
    package com.example.demo;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    ```

3.  **`HelloController.java`** (웹 요청을 처리하는 컨트롤러)
    ```java
    package com.example.demo;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class HelloController {

        @GetMapping("/")
        public String hello() {
            return "Hello, Docker with Java 17 & Spring Boot!";
        }
    }
    ```

4.  **`Dockerfile`** (Java 애플리케이션을 위한 Multi-stage 빌드)
    ```dockerfile
    # 1단계: Maven을 사용하여 애플리케이션 빌드 (빌드 환경)
    FROM maven:3.8-eclipse-temurin-17 AS build
    WORKDIR /app
    COPY pom.xml .
    COPY src ./src
    RUN mvn clean package -DskipTests

    # 2단계: 빌드된 애플리케이션을 실행 환경으로 복사
    FROM eclipse-temurin:17-jre-focal
    WORKDIR /app
    COPY --from=build /app/target/*.jar app.jar

    # 컨테이너가 8080번 포트를 외부에 노출
    EXPOSE 8080

    # 컨테이너 시작 시 JAR 파일 실행
    ENTRYPOINT ["java", "-jar", "app.jar"]
    ```

5.  **`docker-compose.yml`**
    ```yaml
    services:
      web:
        build: .
        container_name: my_java_app
        ports:
          - "8080:8080"
        environment:
          - SPRING_PROFILES_ACTIVE=development
    ```

6.  **`deploy.sh`** (스크립트 내용은 이전과 거의 동일)
    ```bash
    #!/bin/bash
    set -e
    echo "배포 스크립트를 시작합니다..."
    # git pull origin main # 실제 운영 시에는 주석 해제
    docker-compose build --no-cache
    docker-compose down
    docker-compose up -d
    docker image prune -f
    echo "배포가 성공적으로 완료되었습니다!"
    ```

**실행 방법:**

1.  위 파일들을 하나의 디렉토리에 생성합니다.
2.  터미널에서 `deploy.sh` 파일에 실행 권한을 부여합니다 (`chmod +x deploy.sh`).
3.  `./deploy.sh`를 실행하여 배포를 진행합니다.
4.  배포가 완료되면 웹 브라우저에서 `http://localhost:8080`에 접속하여 "Hello, Docker with Java 17 & Spring Boot!" 메시지를 확인합니다. 