Spring 19: 배포 - Docker, Profile
컨테이너화와 환경 분리
1. 배포 개념
1.1 CI/CD 파이프라인
지속적 통합 (Continuous Integration)
개발자들이 코드 변경사항을 자주 중앙 저장소에 통합하는 개발 관행입니다. 각 통합은 자동화된 빌드와 테스트를 통해 검증됩니다.
CI의 핵심 요소:
- 소스 코드 버전 관리 (Git)
- 자동화된 빌드 프로세스
- 자동화된 테스트 실행
- 빠른 피드백 제공
- 빌드 실패 시 즉시 알림
지속적 배포 (Continuous Deployment)
코드 변경사항이 자동으로 프로덕션 환경까지 배포되는 프로세스입니다. 모든 테스트를 통과한 코드는 수동 개입 없이 자동으로 배포됩니다.
CD의 핵심 요소:
- 자동화된 배포 파이프라인
- 환경별 설정 관리
- 롤백 메커니즘
- 모니터링 및 알림
- 보안 검증
CI/CD 파이프라인 예제
# GitHub Actions CI/CD 파이프라인
name: Spring Boot CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
- name: Run tests
run: ./gradlew test
- name: Generate test report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Gradle Tests
path: build/test-results/test/*.xml
reporter: java-junit
build-and-deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -t myapp:latest .
docker tag myapp:latest myapp:${{ github.sha }}
- name: Deploy to staging
run: |
# 스테이징 환경 배포 스크립트
echo "Deploying to staging..."
- name: Run integration tests
run: |
# 통합 테스트 실행
./gradlew integrationTest
- name: Deploy to production
if: success()
run: |
# 프로덕션 환경 배포
echo "Deploying to production..."1.2 배포 전략
블루-그린 배포
두 개의 동일한 프로덕션 환경(블루/그린)을 유지하며, 한 번에 전체 트래픽을 새 버전으로 전환하는 방식입니다.
장점:
- 즉시 롤백 가능
- 다운타임 최소화
- 완전한 환경 테스트
단점:
- 리소스 비용 2배
- 데이터베이스 동기화 복잡
롤링 배포
서버를 하나씩 또는 그룹별로 순차적으로 업데이트하는 방식입니다. 전체 서비스 중단 없이 점진적으로 배포합니다.
장점:
- 리소스 효율적
- 점진적 배포
- 서비스 중단 없음
단점:
- 배포 시간 길어짐
- 버전 혼재 상황
카나리 배포
소수의 사용자에게만 새 버전을 배포하여 테스트한 후, 점진적으로 확대하는 방식입니다.
장점:
- 위험 최소화
- 실제 사용자 피드백
- 점진적 확대
단점:
- 복잡한 트래픽 관리
- 모니터링 복잡성
A/B 테스트 배포
사용자를 그룹으로 나누어 서로 다른 버전을 제공하고 성능을 비교하는 방식입니다.
장점:
- 데이터 기반 의사결정
- 사용자 경험 최적화
- 비즈니스 메트릭 검증
단점:
- 복잡한 분석 필요
- 장기간 운영 필요
Kubernetes 배포 전략 예제
# 롤링 업데이트 배포
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-app
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: spring-app
template:
metadata:
labels:
app: spring-app
spec:
containers:
- name: spring-app
image: spring-app:v2.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
---
# 카나리 배포를 위한 서비스
apiVersion: v1
kind: Service
metadata:
name: spring-app-canary
spec:
selector:
app: spring-app
version: canary
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
# Ingress를 통한 트래픽 분할
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spring-app-ingress
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spring-app-canary
port:
number: 801.3 환경 분리
환경별 특성과 목적
개발 환경 (Development)
- • 개발자 개인 작업 환경
- • 빠른 피드백과 디버깅
- • 로컬 데이터베이스 사용
- • 상세한 로깅 활성화
- • 개발 도구 통합
테스트 환경 (Testing)
- • 자동화된 테스트 실행
- • CI/CD 파이프라인 통합
- • 테스트 데이터 관리
- • 성능 테스트 수행
- • 보안 취약점 스캔
스테이징 환경 (Staging)
- • 프로덕션과 동일한 구성
- • 최종 사용자 테스트
- • 통합 테스트 수행
- • 배포 프로세스 검증
- • 성능 및 부하 테스트
프로덕션 환경 (Production)
- • 실제 사용자 서비스
- • 고가용성 및 확장성
- • 보안 강화
- • 모니터링 및 알림
- • 백업 및 재해복구
환경별 설정 관리
# application.yml (공통 설정)
spring:
application:
name: spring-deployment-demo
jpa:
hibernate:
ddl-auto: validate
show-sql: false
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
---
# application-dev.yml (개발 환경)
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:devdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example: DEBUG
org.springframework.web: DEBUG
---
# application-test.yml (테스트 환경)
spring:
config:
activate:
on-profile: test
datasource:
url: jdbc:postgresql://test-db:5432/testdb
username: ${DB_USERNAME:testuser}
password: ${DB_PASSWORD:testpass}
jpa:
hibernate:
ddl-auto: create-drop
logging:
level:
com.example: INFO
---
# application-staging.yml (스테이징 환경)
spring:
config:
activate:
on-profile: staging
datasource:
url: jdbc:postgresql://staging-db:5432/stagingdb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 10
minimum-idle: 5
logging:
level:
com.example: WARN
file:
name: /var/log/app/staging.log
---
# application-prod.yml (프로덕션 환경)
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:postgresql://prod-db:5432/proddb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
logging:
level:
com.example: ERROR
file:
name: /var/log/app/production.log
management:
endpoints:
web:
exposure:
include: health,info환경 분리 베스트 프랙티스
설정 관리
- • 환경변수를 통한 외부 설정
- • 민감한 정보는 별도 관리
- • 설정 파일 버전 관리
- • 기본값 설정으로 안정성 확보
데이터 관리
- • 환경별 독립적인 데이터베이스
- • 테스트 데이터 자동 생성
- • 프로덕션 데이터 보호
- • 데이터 마이그레이션 스크립트
보안
- • 환경별 접근 권한 관리
- • 네트워크 분리
- • 암호화 키 관리
- • 감사 로그 유지
모니터링
- • 환경별 모니터링 설정
- • 알림 규칙 차별화
- • 성능 메트릭 수집
- • 로그 중앙화
1.4 배포 파이프라인 설계
파이프라인 단계별 구성
소스 코드 관리
Git 브랜치 전략, 코드 리뷰, 머지 정책
빌드 및 테스트
컴파일, 단위 테스트, 정적 분석, 보안 스캔
아티팩트 생성
Docker 이미지 빌드, 패키지 생성, 저장소 업로드
배포 및 검증
환경별 배포, 통합 테스트, 헬스 체크
모니터링 및 알림
성능 모니터링, 에러 추적, 알림 설정
⚠️ 배포 시 고려사항
기술적 고려사항
- • 데이터베이스 마이그레이션
- • 캐시 무효화
- • 세션 관리
- • 외부 서비스 의존성
- • 리소스 사용량 모니터링
운영적 고려사항
- • 배포 시간대 선택
- • 롤백 계획 수립
- • 팀 간 커뮤니케이션
- • 사용자 공지
- • 장애 대응 절차
2. Docker
2.1 Docker 기초 개념
컨테이너화의 이점
일관성
개발, 테스트, 프로덕션 환경에서 동일한 실행 환경 보장
격리성
애플리케이션과 의존성을 독립적으로 실행
이식성
어떤 환경에서든 동일하게 실행 가능
효율성
가상머신 대비 적은 리소스 사용
2.2 Dockerfile 작성
기본 Spring Boot Dockerfile
# 기본 Dockerfile FROM openjdk:17-jdk-slim # 작업 디렉토리 설정 WORKDIR /app # JAR 파일 복사 COPY build/libs/*.jar app.jar # 포트 노출 EXPOSE 8080 # 애플리케이션 실행 ENTRYPOINT ["java", "-jar", "app.jar"]
최적화된 Dockerfile
# 최적화된 Dockerfile FROM openjdk:17-jdk-slim as builder # 빌드 도구 설치 RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* # 작업 디렉토리 설정 WORKDIR /app # Gradle Wrapper와 빌드 파일 복사 COPY gradlew . COPY gradle gradle COPY build.gradle . COPY settings.gradle . # 의존성 다운로드 (캐시 최적화) RUN ./gradlew dependencies --no-daemon # 소스 코드 복사 COPY src src # 애플리케이션 빌드 RUN ./gradlew bootJar --no-daemon # 런타임 이미지 FROM openjdk:17-jre-slim # 보안을 위한 사용자 생성 RUN groupadd -r spring && useradd -r -g spring spring # 작업 디렉토리 설정 WORKDIR /app # 빌드된 JAR 파일 복사 COPY --from=builder /app/build/libs/*.jar app.jar # 파일 소유권 변경 RUN chown spring:spring app.jar # 사용자 전환 USER spring # 헬스체크 추가 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # 포트 노출 EXPOSE 8080 # JVM 옵션 설정 ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UseContainerSupport" # 애플리케이션 실행 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Dockerfile 베스트 프랙티스
이미지 최적화
- • 경량 베이스 이미지 사용 (alpine, slim)
- • 불필요한 패키지 제거
- • 레이어 수 최소화
- • .dockerignore 파일 활용
보안
- • 비특권 사용자로 실행
- • 최신 베이스 이미지 사용
- • 민감한 정보 하드코딩 금지
- • 취약점 스캔 수행
2.3 멀티 스테이지 빌드
멀티 스테이지 빌드의 장점
- 최종 이미지 크기 대폭 감소
- 빌드 도구와 런타임 환경 분리
- 보안 향상 (빌드 도구 제거)
- 빌드 캐시 최적화
- 개발/프로덕션 환경 구분
고급 멀티 스테이지 Dockerfile
# 멀티 스테이지 빌드 Dockerfile
# Stage 1: 의존성 다운로드
FROM openjdk:17-jdk-slim as deps
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
RUN ./gradlew dependencies --no-daemon
# Stage 2: 소스 빌드
FROM deps as builder
COPY src src
RUN ./gradlew bootJar --no-daemon
# Stage 3: 테스트 실행 (선택적)
FROM builder as tester
RUN ./gradlew test --no-daemon
# Stage 4: 프로덕션 이미지
FROM openjdk:17-jre-slim as production
# 시스템 업데이트 및 필수 패키지 설치
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
dumb-init && \
rm -rf /var/lib/apt/lists/*
# 사용자 생성
RUN groupadd -r spring && useradd -r -g spring spring
# 작업 디렉토리 설정
WORKDIR /app
# 빌드된 JAR 파일만 복사
COPY --from=builder /app/build/libs/*.jar app.jar
# 파일 소유권 변경
RUN chown spring:spring app.jar
# 사용자 전환
USER spring
# 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 포트 노출
EXPOSE 8080
# 환경 변수 설정
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# dumb-init을 사용하여 신호 처리 개선
ENTRYPOINT ["dumb-init", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# Stage 5: 개발 이미지 (개발용)
FROM openjdk:17-jdk-slim as development
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src
# 개발 도구 설치
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
vim \
htop && \
rm -rf /var/lib/apt/lists/*
# 개발 환경 설정
ENV SPRING_PROFILES_ACTIVE=dev
ENV JAVA_OPTS="-Xmx1g -Xms512m -XX:+UseG1GC -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
EXPOSE 8080 5005
CMD ["./gradlew", "bootRun", "--no-daemon"]빌드 타겟 선택
# 프로덕션 이미지 빌드 docker build --target production -t myapp:prod . # 개발 이미지 빌드 docker build --target development -t myapp:dev . # 테스트 포함 빌드 docker build --target tester -t myapp:test . # 빌드 인자 사용 docker build --build-arg PROFILE=prod --target production -t myapp:latest .
2.4 Docker 이미지 최적화
레이어 캐싱 최적화
❌ 비효율적인 방법
# 소스 코드 변경 시 의존성도 재다운로드 COPY . . RUN ./gradlew dependencies RUN ./gradlew bootJar
✅ 효율적인 방법
# 의존성 먼저 다운로드 (캐시 활용) COPY build.gradle settings.gradle ./ RUN ./gradlew dependencies COPY src src RUN ./gradlew bootJar
.dockerignore 파일
# .dockerignore # 빌드 결과물 build/ target/ *.jar *.war # IDE 파일 .idea/ .vscode/ *.iml *.ipr *.iws # 버전 관리 .git/ .gitignore .gitattributes # 문서 README.md docs/ *.md # 로그 파일 logs/ *.log # 임시 파일 tmp/ temp/ .tmp # OS 파일 .DS_Store Thumbs.db # 테스트 결과 test-results/ coverage/ # 환경 설정 (민감한 정보) .env .env.local application-local.yml
이미지 크기 최적화 기법
1. 베이스 이미지 선택
# 크기 비교 openjdk:17-jdk ~470MB openjdk:17-jdk-slim ~220MB openjdk:17-jre-slim ~185MB openjdk:17-alpine ~165MB
2. 패키지 정리
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*3. 레이어 결합
# 여러 명령을 하나로 결합
RUN apt-get update && \
apt-get install -y curl && \
curl -O https://example.com/file && \
rm -rf /var/lib/apt/lists/*Docker 빌드 최적화 명령어
# 빌드 캐시 사용 docker build --cache-from myapp:latest -t myapp:new . # BuildKit 사용 (병렬 빌드) DOCKER_BUILDKIT=1 docker build -t myapp:latest . # 빌드 컨텍스트 최소화 docker build -f Dockerfile.prod -t myapp:prod . # 이미지 크기 분석 docker images myapp:latest docker history myapp:latest # 이미지 레이어 분석 도구 dive myapp:latest # 불필요한 이미지 정리 docker image prune -f docker system prune -f
2.5 Docker 보안
🔒 보안 베스트 프랙티스
사용자 권한
- • root 사용자로 실행 금지
- • 전용 사용자 계정 생성
- • 최소 권한 원칙 적용
- • 파일 권한 적절히 설정
이미지 보안
- • 신뢰할 수 있는 베이스 이미지
- • 정기적인 이미지 업데이트
- • 취약점 스캔 수행
- • 민감한 정보 제거
보안 강화 Dockerfile
FROM openjdk:17-jre-slim
# 보안 업데이트 적용
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
curl \
dumb-init && \
rm -rf /var/lib/apt/lists/*
# 비특권 사용자 생성
RUN groupadd -r -g 1000 spring && \
useradd -r -u 1000 -g spring -d /app -s /sbin/nologin spring
# 작업 디렉토리 설정 및 권한 부여
WORKDIR /app
RUN chown spring:spring /app
# 애플리케이션 파일 복사
COPY --chown=spring:spring build/libs/*.jar app.jar
# 실행 권한 설정
RUN chmod 500 app.jar
# 사용자 전환
USER spring
# 불필요한 권한 제거
RUN chmod -R go-rwx /app
# 보안 헤더 설정을 위한 환경 변수
ENV JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom \
-Dspring.security.require-ssl=true \
-Dserver.use-forward-headers=true"
# 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["dumb-init", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]3. Docker Compose
3.1 Docker Compose 개요
Docker Compose의 장점
다중 컨테이너 관리
여러 서비스를 하나의 파일로 정의하고 관리
환경 일관성
개발, 테스트, 프로덕션 환경 동일하게 구성
간편한 배포
한 번의 명령으로 전체 스택 실행
서비스 연결
컨테이너 간 네트워크 자동 구성
3.2 기본 Docker Compose 구성
기본 docker-compose.yml
version: '3.8'
services:
# Spring Boot 애플리케이션
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=myapp
- DB_USERNAME=postgres
- DB_PASSWORD=password
depends_on:
- postgres
- redis
networks:
- app-network
# PostgreSQL 데이터베이스
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-network
# Redis 캐시
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridgeDocker Compose 기본 명령어
# 서비스 시작 (백그라운드) docker-compose up -d # 서비스 중지 docker-compose down # 서비스 재시작 docker-compose restart # 로그 확인 docker-compose logs -f app # 특정 서비스만 시작 docker-compose up -d postgres redis # 빌드 후 시작 docker-compose up --build # 볼륨까지 삭제 docker-compose down -v # 스케일링 docker-compose up -d --scale app=3
3.3 다중 컨테이너 구성
완전한 마이크로서비스 스택
version: '3.8'
services:
# API Gateway (Nginx)
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- user-service
- order-service
- product-service
networks:
- frontend
- backend
# 사용자 서비스
user-service:
build: ./user-service
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_HOST=user-db
- REDIS_HOST=redis
- EUREKA_URL=http://eureka:8761/eureka
depends_on:
- user-db
- redis
- eureka
networks:
- backend
deploy:
replicas: 2
# 주문 서비스
order-service:
build: ./order-service
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_HOST=order-db
- KAFKA_BROKERS=kafka:9092
- EUREKA_URL=http://eureka:8761/eureka
depends_on:
- order-db
- kafka
- eureka
networks:
- backend
# 상품 서비스
product-service:
build: ./product-service
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_HOST=product-db
- ELASTICSEARCH_URL=http://elasticsearch:9200
- EUREKA_URL=http://eureka:8761/eureka
depends_on:
- product-db
- elasticsearch
- eureka
networks:
- backend
# 서비스 디스커버리 (Eureka)
eureka:
image: springcloud/eureka
ports:
- "8761:8761"
networks:
- backend
# 사용자 DB
user-db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=userdb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- user_db_data:/var/lib/postgresql/data
networks:
- backend
# 주문 DB
order-db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=orderdb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- order_db_data:/var/lib/postgresql/data
networks:
- backend
# 상품 DB
product-db:
image: mongo:6
environment:
- MONGO_INITDB_DATABASE=productdb
volumes:
- product_db_data:/data/db
networks:
- backend
# Redis 캐시
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
networks:
- backend
# Kafka
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
networks:
- backend
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
networks:
- backend
# Elasticsearch
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
networks:
- backend
# 모니터링 (Prometheus)
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
networks:
- monitoring
# 모니터링 (Grafana)
grafana:
image: grafana/grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
networks:
- monitoring
volumes:
user_db_data:
order_db_data:
product_db_data:
redis_data:
elasticsearch_data:
prometheus_data:
grafana_data:
networks:
frontend:
driver: bridge
backend:
driver: bridge
monitoring:
driver: bridge3.4 네트워크 구성
네트워크 타입별 특성
Bridge Network
- • 기본 네트워크 타입
- • 같은 네트워크 내 컨테이너 통신
- • 서비스명으로 DNS 해석
- • 외부 접근 시 포트 매핑 필요
Host Network
- • 호스트 네트워크 직접 사용
- • 최고 성능
- • 포트 충돌 주의
- • 보안상 권장하지 않음
Overlay Network
- • 다중 호스트 간 통신
- • Docker Swarm에서 사용
- • 암호화 지원
- • 클러스터 환경에 적합
None Network
- • 네트워크 연결 없음
- • 완전 격리
- • 보안이 중요한 작업
- • 배치 작업에 적합
고급 네트워크 설정
version: '3.8'
services:
web:
image: nginx
networks:
frontend:
aliases:
- web-server
ipv4_address: 172.20.0.10
backend:
aliases:
- api-gateway
app:
build: .
networks:
- backend
ports:
- "8080" # 동적 포트 할당
db:
image: postgres
networks:
backend:
ipv4_address: 172.21.0.10
# 외부 접근 차단 (포트 매핑 없음)
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
driver_opts:
com.docker.network.bridge.name: frontend-br
com.docker.network.bridge.enable_icc: "true"
backend:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
internal: true # 외부 인터넷 접근 차단
# 외부 네트워크 사용
external-net:
external: true
name: my-external-network네트워크 보안 설정
# 네트워크 분리 예제
version: '3.8'
services:
# DMZ 영역 (외부 접근 가능)
nginx:
image: nginx
ports:
- "80:80"
- "443:443"
networks:
- dmz
- frontend
# 애플리케이션 영역
app:
build: .
networks:
- frontend
- backend
# 외부 포트 노출 없음
# 데이터베이스 영역 (완전 격리)
database:
image: postgres
networks:
- backend
# 외부 접근 완전 차단
networks:
dmz:
driver: bridge
# 외부 접근 허용
frontend:
driver: bridge
internal: false
backend:
driver: bridge
internal: true # 인터넷 접근 차단
# 네트워크 정책 (방화벽 규칙)
# iptables 또는 Docker 네트워크 정책 사용3.5 볼륨 관리
볼륨 타입별 특성
Named Volume
- • Docker가 관리
- • 데이터 영속성
- • 컨테이너 간 공유
- • 백업 용이
Bind Mount
- • 호스트 경로 직접 매핑
- • 개발 시 편리
- • 실시간 파일 동기화
- • 보안 주의 필요
tmpfs Mount
- • 메모리에 저장
- • 임시 데이터
- • 빠른 I/O
- • 재시작 시 삭제
볼륨 설정 예제
version: '3.8'
services:
# 데이터베이스 (Named Volume)
postgres:
image: postgres:15
volumes:
# 데이터 영속성
- postgres_data:/var/lib/postgresql/data
# 초기화 스크립트 (Bind Mount)
- ./init-scripts:/docker-entrypoint-initdb.d:ro
# 설정 파일 (Bind Mount)
- ./postgres.conf:/etc/postgresql/postgresql.conf:ro
environment:
- POSTGRES_DB=myapp
# 애플리케이션 (개발 환경)
app-dev:
build: .
volumes:
# 소스 코드 실시간 동기화
- ./src:/app/src:ro
- ./build:/app/build
# 로그 파일
- app_logs:/var/log/app
# 임시 파일 (tmpfs)
- type: tmpfs
target: /tmp
tmpfs:
size: 100M
environment:
- SPRING_PROFILES_ACTIVE=dev
# 애플리케이션 (프로덕션 환경)
app-prod:
build: .
volumes:
# 로그만 외부 저장
- app_logs:/var/log/app:rw
# 설정 파일 (읽기 전용)
- ./config/application-prod.yml:/app/config/application.yml:ro
# 업로드 파일
- upload_data:/app/uploads
environment:
- SPRING_PROFILES_ACTIVE=prod
# 로그 수집기
filebeat:
image: elastic/filebeat:8.8.0
volumes:
# 애플리케이션 로그 읽기
- app_logs:/var/log/app:ro
# Filebeat 설정
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
# Filebeat 데이터
- filebeat_data:/usr/share/filebeat/data
volumes:
postgres_data:
driver: local
driver_opts:
type: none
o: bind
device: /opt/postgres-data
app_logs:
driver: local
upload_data:
driver: local
driver_opts:
type: nfs
o: addr=nfs-server,rw
device: ":/path/to/uploads"
filebeat_data:
driver: local볼륨 백업 및 복원
# 볼륨 백업 docker run --rm -v postgres_data:/data -v $(pwd):/backup \ alpine tar czf /backup/postgres_backup.tar.gz -C /data . # 볼륨 복원 docker run --rm -v postgres_data:/data -v $(pwd):/backup \ alpine tar xzf /backup/postgres_backup.tar.gz -C /data # 볼륨 정보 확인 docker volume ls docker volume inspect postgres_data # 사용하지 않는 볼륨 정리 docker volume prune # 볼륨 복사 (컨테이너 간) docker run --rm -v source_vol:/from -v dest_vol:/to \ alpine cp -av /from/. /to/
3.6 환경별 설정
docker-compose.override.yml (개발 환경)
# docker-compose.override.yml (자동으로 적용됨)
version: '3.8'
services:
app:
build:
context: .
target: development
volumes:
- ./src:/app/src:ro
- ./build:/app/build
environment:
- SPRING_PROFILES_ACTIVE=dev
- DEBUG=true
ports:
- "8080:8080"
- "5005:5005" # 디버그 포트
postgres:
ports:
- "5432:5432" # 외부 접근 허용
environment:
- POSTGRES_DB=devdb
volumes:
- ./dev-data:/docker-entrypoint-initdb.d
redis:
ports:
- "6379:6379" # 외부 접근 허용docker-compose.prod.yml (프로덕션 환경)
# docker-compose.prod.yml
version: '3.8'
services:
app:
build:
context: .
target: production
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_HOST=${DB_HOST}
- DB_PASSWORD=${DB_PASSWORD}
deploy:
replicas: 3
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
postgres:
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
# 외부 포트 노출 없음 (보안)
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx-prod.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app환경별 실행 명령어
# 개발 환경 (기본 + override) docker-compose up -d # 테스트 환경 docker-compose -f docker-compose.yml -f docker-compose.test.yml up -d # 프로덕션 환경 docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d # 환경 변수 파일 사용 docker-compose --env-file .env.prod up -d # 특정 서비스만 실행 docker-compose -f docker-compose.prod.yml up -d app postgres # 설정 검증 docker-compose -f docker-compose.prod.yml config # 로그 모니터링 docker-compose -f docker-compose.prod.yml logs -f --tail=100
4. Spring Profile
4.1 Spring Profile 개요
Profile의 필요성
환경별 설정 분리
개발, 테스트, 프로덕션 환경에 맞는 설정 적용
조건부 빈 등록
특정 환경에서만 필요한 빈을 선택적으로 등록
기능 토글
환경에 따라 특정 기능을 활성화/비활성화
보안 강화
민감한 설정을 환경별로 안전하게 관리
4.2 환경별 설정 파일
application.yml 구조
# application.yml (공통 설정)
spring:
application:
name: spring-profile-demo
# 기본 데이터소스 설정
datasource:
driver-class-name: org.postgresql.Driver
hikari:
connection-timeout: 20000
maximum-pool-size: 10
# JPA 공통 설정
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
format_sql: false
use_sql_comments: false
# 로깅 기본 설정
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# Actuator 기본 설정
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: never
---
# 개발 환경 설정
spring:
config:
activate:
on-profile: dev
# H2 인메모리 데이터베이스
datasource:
url: jdbc:h2:mem:devdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
# H2 콘솔 활성화
h2:
console:
enabled: true
path: /h2-console
# JPA 개발 설정
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
use_sql_comments: true
# 개발 환경 로깅
logging:
level:
com.example: DEBUG
org.springframework.web: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
# 개발 환경 Actuator
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
---
# 테스트 환경 설정
spring:
config:
activate:
on-profile: test
# 테스트 데이터베이스
datasource:
url: jdbc:postgresql://localhost:5432/testdb
username: ${DB_USERNAME:testuser}
password: ${DB_PASSWORD:testpass}
# JPA 테스트 설정
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
# 테스트 환경 로깅
logging:
level:
com.example: INFO
org.springframework: WARN
---
# 스테이징 환경 설정
spring:
config:
activate:
on-profile: staging
# 스테이징 데이터베이스
datasource:
url: jdbc:postgresql://staging-db:5432/stagingdb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 15
minimum-idle: 5
# JPA 스테이징 설정
jpa:
hibernate:
ddl-auto: validate
show-sql: false
# 스테이징 환경 로깅
logging:
level:
com.example: WARN
root: INFO
file:
name: /var/log/app/staging.log
---
# 프로덕션 환경 설정
spring:
config:
activate:
on-profile: prod
# 프로덕션 데이터베이스
datasource:
url: jdbc:postgresql://prod-db:5432/proddb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# JPA 프로덕션 설정
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
# 프로덕션 환경 로깅
logging:
level:
com.example: ERROR
root: WARN
file:
name: /var/log/app/production.log
max-size: 100MB
max-history: 30
# 프로덕션 환경 Actuator (보안 강화)
management:
endpoints:
web:
exposure:
include: health,info,metrics
base-path: /actuator
endpoint:
health:
show-details: when-authorized
security:
enabled: trueProfile 활성화 방법
# 1. application.yml에서 기본 프로파일 설정
spring:
profiles:
active: dev
# 2. 환경 변수로 설정
export SPRING_PROFILES_ACTIVE=prod
# 3. JVM 시스템 프로퍼티로 설정
java -Dspring.profiles.active=prod -jar app.jar
# 4. 프로그램 인수로 설정
java -jar app.jar --spring.profiles.active=prod
# 5. Docker 환경에서 설정
docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest
# 6. 여러 프로파일 동시 활성화
java -Dspring.profiles.active=prod,monitoring -jar app.jar
# 7. IDE에서 설정 (IntelliJ IDEA)
# Run Configuration > Environment variables > SPRING_PROFILES_ACTIVE=dev4.3 외부 설정 관리
설정 우선순위
Command Line Arguments
--server.port=8081
JVM System Properties
-Dserver.port=8081
OS Environment Variables
SERVER_PORT=8081
application-{profile}.yml
Profile별 설정 파일
application.yml
기본 설정 파일
외부 설정 파일 사용
# 1. 외부 설정 파일 지정 java -jar app.jar --spring.config.location=classpath:/,/opt/config/ # 2. 추가 설정 파일 지정 java -jar app.jar --spring.config.additional-location=/opt/config/ # 3. 설정 파일 이름 변경 java -jar app.jar --spring.config.name=myapp # 4. 여러 위치에서 설정 로드 java -jar app.jar \ --spring.config.location=classpath:/,file:./config/,file:/opt/config/ # 5. 환경 변수로 외부 설정 export SPRING_CONFIG_LOCATION=/opt/config/application.yml java -jar app.jar # 6. Docker에서 외부 설정 마운트 docker run -v /host/config:/app/config \ -e SPRING_CONFIG_LOCATION=/app/config/application.yml \ myapp:latest
환경 변수 매핑
# application.yml에서 환경 변수 사용
spring:
datasource:
url: ${DATABASE_URL:jdbc:h2:mem:testdb}
username: ${DB_USERNAME:sa}
password: ${DB_PASSWORD:}
server:
port: ${SERVER_PORT:8080}
app:
jwt:
secret: ${JWT_SECRET:default-secret}
expiration: ${JWT_EXPIRATION:86400}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
# 환경 변수 설정 예제
export DATABASE_URL=jdbc:postgresql://prod-db:5432/proddb
export DB_USERNAME=produser
export DB_PASSWORD=securepassword
export SERVER_PORT=8080
export JWT_SECRET=super-secret-key
export REDIS_HOST=redis-cluster
export REDIS_PASSWORD=redis-password
# .env 파일 사용 (Docker Compose)
DATABASE_URL=jdbc:postgresql://postgres:5432/myapp
DB_USERNAME=postgres
DB_PASSWORD=password
SERVER_PORT=8080
JWT_SECRET=my-jwt-secret
REDIS_HOST=redis
REDIS_PORT=63794.4 비밀 정보 관리
🔒 보안 고려사항
❌ 피해야 할 방법
- • 설정 파일에 비밀번호 하드코딩
- • Git에 민감한 정보 커밋
- • 로그에 비밀 정보 출력
- • 프로덕션에서 기본값 사용
✅ 권장하는 방법
- • 환경 변수 사용
- • 외부 비밀 관리 시스템
- • 암호화된 설정 파일
- • 런타임 비밀 주입
Spring Cloud Config 사용
# Config Server 설정
# application.yml (Config Server)
server:
port: 8888
spring:
cloud:
config:
server:
git:
uri: https://github.com/myorg/config-repo
search-paths: '{application}'
username: ${GIT_USERNAME}
password: ${GIT_PASSWORD}
encrypt:
enabled: true
# 암호화 키 설정
encrypt:
key: myencryptionkey
---
# Config Client 설정
# bootstrap.yml (Application)
spring:
application:
name: myapp
cloud:
config:
uri: http://config-server:8888
profile: ${SPRING_PROFILES_ACTIVE:dev}
label: main
fail-fast: true
retry:
initial-interval: 1000
max-attempts: 6
# 암호화된 설정 사용
# myapp-prod.yml (Config Repository)
spring:
datasource:
password: '{cipher}AQA1234567890abcdef...'
app:
jwt:
secret: '{cipher}BQB0987654321fedcba...'Kubernetes Secrets 사용
# Kubernetes Secret 생성
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
database-password: cGFzc3dvcmQ= # base64 encoded
jwt-secret: bXktand0LXNlY3JldA==
---
# Deployment에서 Secret 사용
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-app
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: database-password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: jwt-secret
# 또는 볼륨으로 마운트
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: app-secrets
# 파일에서 Secret 읽기
spring:
datasource:
password: ${DB_PASSWORD:file:/etc/secrets/database-password}HashiCorp Vault 통합
# Vault 의존성 추가
# build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-vault-config'
# Vault 설정
# bootstrap.yml
spring:
cloud:
vault:
host: vault-server
port: 8200
scheme: https
authentication: TOKEN
token: ${VAULT_TOKEN}
kv:
enabled: true
backend: secret
profile-separator: '/'
default-context: myapp
application-name: myapp
# Vault에서 비밀 정보 조회
# Java 코드
@Configuration
public class VaultConfig {
@Value("${database.password}")
private String databasePassword;
@Value("${jwt.secret}")
private String jwtSecret;
// 동적으로 비밀 정보 갱신
@EventListener
public void handleRefreshEvent(RefreshRemoteApplicationEvent event) {
// 비밀 정보 갱신 로직
}
}
# Vault CLI로 비밀 정보 저장
vault kv put secret/myapp/prod \
database.password=securepassword \
jwt.secret=super-secret-key4.5 Profile 기반 빈 등록
@Profile 어노테이션 사용
// 개발 환경 전용 설정
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
public MailSender mailSender() {
// 개발 환경에서는 콘솔에 메일 내용 출력
return new ConsoleMailSender();
}
}
// 프로덕션 환경 전용 설정
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(environment.getProperty("spring.datasource.url"));
config.setUsername(environment.getProperty("spring.datasource.username"));
config.setPassword(environment.getProperty("spring.datasource.password"));
config.setMaximumPoolSize(20);
return new HikariDataSource(config);
}
@Bean
public MailSender mailSender() {
// 실제 SMTP 서버 사용
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("smtp.gmail.com");
mailSender.setPort(587);
return mailSender;
}
}
// 여러 프로파일 조건
@Configuration
@Profile({"dev", "test"})
public class NonProdConfig {
@Bean
public SecurityConfig securityConfig() {
return SecurityConfig.builder()
.disableCsrf()
.permitAll()
.build();
}
}
// 프로파일 제외 조건
@Configuration
@Profile("!prod")
public class NonProdSecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring()
.requestMatchers("/h2-console/**")
.requestMatchers("/actuator/**");
}
}
// 복잡한 프로파일 조건
@Configuration
@Profile("prod & monitoring")
public class ProdMonitoringConfig {
@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
}
// 메서드 레벨 프로파일
@Configuration
public class DatabaseConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return DataSourceBuilder.create()
.url("jdbc:postgresql://prod-db:5432/proddb")
.build();
}
}조건부 빈 등록 고급 기법
// 커스텀 조건 클래스
public class ProductionCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.acceptsProfiles(Profiles.of("prod")) &&
"true".equals(env.getProperty("app.production.enabled"));
}
}
// 커스텀 조건 사용
@Configuration
@Conditional(ProductionCondition.class)
public class ProductionOnlyConfig {
@Bean
public ProductionService productionService() {
return new ProductionService();
}
}
// 프로퍼티 기반 조건
@Configuration
@ConditionalOnProperty(
name = "app.feature.enabled",
havingValue = "true",
matchIfMissing = false
)
public class FeatureConfig {
@Bean
public FeatureService featureService() {
return new FeatureService();
}
}
// 클래스 존재 여부 기반 조건
@Configuration
@ConditionalOnClass(RedisTemplate.class)
public class RedisConfig {
@Bean
@ConditionalOnMissingBean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
}
// 프로파일 그룹 설정
# application.yml
spring:
profiles:
group:
production: "prod,monitoring,security"
development: "dev,debug,h2"
testing: "test,testcontainers"
# 프로파일 그룹 활성화
java -jar app.jar --spring.profiles.active=production5. Kubernetes 기초
5.1 Kubernetes 개요
Kubernetes의 핵심 개념
컨테이너 오케스트레이션
다수의 컨테이너를 자동으로 배포, 관리, 확장
선언적 설정
원하는 상태를 정의하면 자동으로 유지
자동 복구
장애 발생 시 자동으로 복구 및 재시작
확장성
부하에 따라 자동으로 스케일링
5.2 Deployment
기본 Deployment 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-app
labels:
app: spring-app
version: v1.0
spec:
replicas: 3
selector:
matchLabels:
app: spring-app
template:
metadata:
labels:
app: spring-app
version: v1.0
spec:
containers:
- name: spring-app
image: myregistry/spring-app:1.0.0
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_HOST
value: "postgres-service"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"고급 Deployment 설정
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-app-advanced
labels:
app: spring-app
tier: backend
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 2
selector:
matchLabels:
app: spring-app
template:
metadata:
labels:
app: spring-app
tier: backend
spec:
containers:
- name: spring-app
image: myregistry/spring-app:2.0.0
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: management
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JVM_OPTS
value: "-Xmx768m -Xms512m"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 30