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 |
