[PROJECT #1] N-Tier 애플리케이션 도커로 구성하기 (Web + App + DB)

2025. 6. 1. 11:45·Docker

0. 들어가며

지금까지 우리는 도커의 모든 핵심 개념을 배웠다.

  • BASIC #1: 개념과 등장 배경
  • BASIC #2: 설치와 기본 명령어
  • BASIC #3: 이미지와 컨테이너
  • BASIC #4: Dockerfile로 이미지 만들기
  • BASIC #5: Volume으로 데이터 저장
  • BASIC #6: Network로 컨테이너 간 통신
  • ADVANCED #1: Compose로 멀티 컨테이너 관리
  • ADVANCED #2: Docker Hub로 이미지 배포

이번 포스팅에서는 배운 모든 것을 종합하여 실제 운영 수준의 N-Tier 애플리케이션을 도커로 구성해보겠다.


1. 프로젝트 개요

1.1. 애플리케이션 아키텍처

다음과 같은 3계층 아키텍처를 구성할 것이다.

[Client Browser]
       |
       v
[Web Tier] Nginx (Reverse Proxy, Load Balancer)
       |
       v
[App Tier] Spring Boot (REST API) + Redis (Session)
       |
       v
[DB Tier] MySQL (Master-Slave Replication)

1.2. 사용 기술 스택

계층  기술  역할
Web Nginx 리버스 프록시, 로드 밸런서, 정적 파일 서빙
App Spring Boot REST API, 비즈니스 로직
Cache Redis 세션 클러스터링, 캐싱
DB MySQL 영구 데이터 저장
DB (복제) MySQL Slave 읽기 전용 복제본

2. 프로젝트 구조

n-tier-app/
├── docker-compose.yml
├── .env
├── nginx/
│   ├── Dockerfile
│   └── nginx.conf
├── app/
│   ├── Dockerfile
│   ├── pom.xml
│   └── src/
├── db/
│   ├── master/
│   │   └── init.sql
│   └── slave/
│       └── init.sql
└── redis/
    └── redis.conf

3. MySQL Master-Slave 구성

3.1. Master DB 설정

db/master/init.sql

CREATE USER 'repl'@'%' IDENTIFIED BY 'replpassword';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

CREATE DATABASE IF NOT EXISTS myapp;
USE myapp;

CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL
);

db/master/my.cnf

[mysqld]
server-id=1
log_bin=mysql-bin
binlog_do_db=myapp

3.2. Slave DB 설정

db/slave/init.sql

-- Slave는 Master에서 복제하므로 별도 테이블 생성 불필요
-- 복제 설정은 컨테이너 시작 후 수동 또는 스크립트로 처리

db/slave/my.cnf

[mysqld]
server-id=2
relay-log=mysql-relay-bin
read_only=1

4. Redis 설정

redis/redis.conf

bind 0.0.0.0
protected-mode yes
port 6379
requirepass redispass
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes
appendfilename "appendonly.aof"

5. Spring Boot 애플리케이션

5.1. application.yml

spring:
  application:
    name: n-tier-app

  datasource:
    master:
      jdbc-url: jdbc:mysql://mysql-master:3306/myapp
      username: root
      password: rootpass
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      jdbc-url: jdbc:mysql://mysql-slave:3306/myapp
      username: root
      password: rootpass
      driver-class-name: com.mysql.cj.jdbc.Driver

  redis:
    host: redis-cache
    port: 6379
    password: redispass
    timeout: 2000ms

  session:
    store-type: redis
    redis:
      namespace: myapp:session

server:
  port: 8080

logging:
  level:
    com.example: DEBUG

5.2. Dockerfile

# app/Dockerfile
FROM openjdk:17-jdk-slim AS builder
WORKDIR /build
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN ./mvnw dependency:go-offline

COPY src ./src
RUN ./mvnw package -DskipTests

FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar

# 애플리케이션 사용자 생성
RUN useradd -m appuser
USER appuser

EXPOSE 8080

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

6. Nginx 설정

nginx/nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream app_servers {
        least_conn;  # 부하가 적은 서버로 연결
        server app1:8080 max_fails=3 fail_timeout=30s;
        server app2:8080 max_fails=3 fail_timeout=30s;
    }

    server {
        listen 80;
        server_name localhost;

        # 정적 파일 (선택사항)
        location /static/ {
            alias /usr/share/nginx/html/static/;
            expires 30d;
        }

        # API 요청은 애플리케이션 서버로
        location /api/ {
            proxy_pass http://app_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            # 타임아웃 설정
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }

        # 기본 경로
        location / {
            proxy_pass http://app_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

nginx/Dockerfile

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

7. Docker Compose 전체 구성

docker-compose.yml

version: '3.8'

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
  db-network:
    driver: bridge

volumes:
  mysql-master-data:
  mysql-slave-data:
  redis-data:

services:
  # MySQL Master (쓰기 전용)
  mysql-master:
    image: mysql:8.0
    container_name: mysql-master
    command: --server-id=1 --log-bin=mysql-bin --binlog-do-db=myapp
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
    volumes:
      - mysql-master-data:/var/lib/mysql
      - ./db/master/init.sql:/docker-entrypoint-initdb.d/init.sql
      - ./db/master/my.cnf:/etc/mysql/conf.d/my.cnf
    networks:
      - db-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  # MySQL Slave (읽기 전용 복제본)
  mysql-slave:
    image: mysql:8.0
    container_name: mysql-slave
    command: --server-id=2 --relay-log=mysql-relay-bin --read-only=1
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
    volumes:
      - mysql-slave-data:/var/lib/mysql
      - ./db/slave/init.sql:/docker-entrypoint-initdb.d/init.sql
      - ./db/slave/my.cnf:/etc/mysql/conf.d/my.cnf
    networks:
      - db-network
    depends_on:
      mysql-master:
        condition: service_healthy
    restart: always
    # 실제 복제 설정은 컨테이너 시작 후 별도 스크립트로 수행

  # Redis Cache
  redis-cache:
    image: redis:7-alpine
    container_name: redis-cache
    command: redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
      - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  # Spring Boot App (첫 번째 인스턴스)
  app1:
    build: ./app
    container_name: app1
    image: myapp:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_MASTER_HOST: mysql-master
      DB_SLAVE_HOST: mysql-slave
      DB_NAME: ${MYSQL_DATABASE}
      DB_USER: root
      DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      REDIS_HOST: redis-cache
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    networks:
      - backend
      - db-network
    depends_on:
      mysql-master:
        condition: service_healthy
      mysql-slave:
        condition: service_started
      redis-cache:
        condition: service_healthy
    restart: always

  # Spring Boot App (두 번째 인스턴스 - 로드 밸런싱용)
  app2:
    build: ./app
    container_name: app2
    image: myapp:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_MASTER_HOST: mysql-master
      DB_SLAVE_HOST: mysql-slave
      DB_NAME: ${MYSQL_DATABASE}
      DB_USER: root
      DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      REDIS_HOST: redis-cache
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    networks:
      - backend
      - db-network
    depends_on:
      mysql-master:
        condition: service_healthy
      mysql-slave:
        condition: service_started
      redis-cache:
        condition: service_healthy
    restart: always

  # Nginx Reverse Proxy
  nginx:
    build: ./nginx
    container_name: nginx-proxy
    ports:
      - "80:80"
    networks:
      - frontend
      - backend
    depends_on:
      - app1
      - app2
    restart: always

8. 환경 변수 파일

.env

# MySQL 설정
MYSQL_ROOT_PASSWORD=rootpass123
MYSQL_DATABASE=myapp

# Redis 설정
REDIS_PASSWORD=redispass123

# 애플리케이션 설정
APP_VERSION=1.0.0

9. 실행 및 확인

9.1. 전체 애플리케이션 실행

# 환경 변수 확인
cat .env

# 이미지 빌드 및 실행
docker-compose up -d --build

# 실행 상태 확인
docker-compose ps

# 로그 확인
docker-compose logs -f

9.2. 개별 서비스 로그 확인

# Nginx 로그
docker-compose logs -f nginx

# 앱 로그
docker-compose logs -f app1
docker-compose logs -f app2

# DB 로그
docker-compose logs -f mysql-master

9.3. 애플리케이션 테스트

# API 호출 테스트
curl <http://localhost/api/health>
curl <http://localhost/api/users>
curl <http://localhost/api/products>

# 부하 테스트 (여러 번 요청)
for i in {1..10}; do
  curl -s <http://localhost/api/health> | grep "hostname"
done
# app1과 app2가 번갈아 응답하는지 확인

9.4. Redis 세션 확인

# Redis CLI 접속
docker exec -it redis-cache redis-cli -a redispass123

# 세션 키 확인
keys *
get "myapp:session:abc123"

9.5. MySQL 복제 확인

# Master 상태 확인
docker exec -it mysql-master mysql -p
mysql> SHOW MASTER STATUS;

# Slave 상태 확인
docker exec -it mysql-slave mysql -p
mysql> SHOW SLAVE STATUS\\G
# Slave_IO_Running: Yes
# Slave_SQL_Running: Yes 확인

10. 스케일링 테스트

10.1. 앱 인스턴스 추가

# app 서비스를 3개로 확장
docker-compose up -d --scale app1=0 --scale app2=0 --scale app=3

하지만 지금 설정에서는 app1, app2가 따로 정의되어 있다. 더 유연한 구성을 위해 서비스를 통합할 수 있다.

docker-compose.yml 수정 버전

services:
  app:
    build: ./app
    image: myapp:latest
    environment:
      # 동일한 환경 변수
    networks:
      - backend
      - db-network
    deploy:
      replicas: 3  # Swarm 모드에서 사용

10.2. 로드 밸런싱 확인

watch -n 1 "curl -s <http://localhost/api/health> | grep hostname"

각 요청이 다른 앱 인스턴스로 분산되는 것을 확인할 수 있다.


11. 모니터링 추가 (선택사항)

11.1. Prometheus + Grafana 추가

services:
  # 기존 서비스들...

  prometheus:
    image: prom/prometheus
    container_name: prometheus
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    networks:
      - monitoring
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - monitoring
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus-data:
  grafana-data:

12. 운영 환경 고려사항

12.1. 보안

  • .env 파일을 Git에 커밋하지 않는다. (.gitignore 추가)
  • 실제 운영에서는 Docker Swarm Secrets 또는 HashiCorp Vault 사용
  • 네트워크 분리로 불필요한 접근 차단

12.2. 로깅

services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

12.3. 백업

# DB 백업 스크립트
#!/bin/bash
docker exec mysql-master mysqldump -u root -p$MYSQL_ROOT_PASSWORD myapp > backup.sql

# 볼륨 백업
docker run --rm -v mysql-master-data:/data -v $(pwd):/backup alpine tar czf /backup/mysql-backup.tar.gz -C /data .

12.4. 무중단 배포

# 롤링 업데이트 (Swarm 모드)
docker service update --image myapp:2.0 myapp_app

# 또는 Compose로
docker-compose up -d --no-deps --build app

13. 문제 해결 가이드

13.1. 컨테이너 간 통신 문제

# 네트워크 확인
docker network ls
docker network inspect n-tier-app_backend

# DNS 확인
docker exec app1 cat /etc/resolv.conf

# ping 테스트
docker exec app1 ping mysql-master
docker exec app1 ping redis-cache

13.2. DB 연결 문제

# 앱 로그 확인
docker-compose logs app1 | grep -i error

# DB 직접 접속
docker exec -it mysql-master mysql -p
mysql> SELECT * FROM users;

13.3. 리소스 부족

# 컨테이너 리소스 사용량 확인
docker stats

# 메모리 제한 설정
services:
  app:
    mem_limit: 512m
    mem_reservation: 256m
    cpus: '0.5'

14. 정리

이번 프로젝트에서는 실제 운영 수준의 N-Tier 애플리케이션을 도커로 구성해보았다.

구성한 내용:

  • Nginx 리버스 프록시 및 로드 밸런서
  • 다중 Spring Boot 애플리케이션 인스턴스
  • Redis 세션 클러스터링
  • MySQL Master-Slave 복제
  • Docker Compose로 전체 인프라 정의
  • 환경 변수로 설정 분리
  • 스케일링 및 모니터링 고려

배운 점:

  • 도커 네트워크로 계층 간 통신 격리
  • 볼륨으로 데이터 영속성 확보
  • 환경 변수로 설정과 코드 분리
  • 복잡한 애플리케이션도 코드로 인프라 정의 가능

'Docker' 카테고리의 다른 글

[ADVANCED #2] 도커 이미지 배포와 공유: Docker Hub 활용법  (0) 2025.06.01
[ADVANCED #1] 여러 컨테이너 한 번에 관리하기: Docker Compose  (0) 2025.06.01
[BASIC #6] 컨테이너 간 통신: Docker Network 이해하기  (0) 2025.06.01
[BASIC #5] 데이터를 영구적으로 저장하는 법: Docker Volume  (0) 2025.05.31
[BASIC #4] 나만의 이미지 만들기: Dockerfile 완전 정복  (0) 2025.05.31
'Docker' 카테고리의 다른 글
  • [ADVANCED #2] 도커 이미지 배포와 공유: Docker Hub 활용법
  • [ADVANCED #1] 여러 컨테이너 한 번에 관리하기: Docker Compose
  • [BASIC #6] 컨테이너 간 통신: Docker Network 이해하기
  • [BASIC #5] 데이터를 영구적으로 저장하는 법: Docker Volume
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[PROJECT #1] N-Tier 애플리케이션 도커로 구성하기 (Web + App + DB)
상단으로

티스토리툴바