Development Tools/cursor

Cursor AI와 Spring Boot 개발환경 설정 가이드 - 5편: 프로덕션 배포 및 CI/CD 파이프라인

ai-one 2025. 6. 28. 14:43
반응형

Cursor AI와 Spring Boot 개발환경 설정 가이드 - 5편: 프로덕션 배포 및 CI/CD 파이프라인

프로덕션 환경 준비

개발 환경에서 프로덕션으로 전환하는 과정은 성능 최적화, 보안 강화, 그리고 자동화된 배포 파이프라인 구축이 핵심입니다. Cursor AI의 코드 생성 능력을 활용하면 복잡한 배포 설정도 효율적으로 구성할 수 있습니다.

Docker 멀티 스테이지 빌드

Spring Boot 백엔드를 위한 최적화된 Docker 이미지를 생성합니다:

# backend/Dockerfile
# 멀티 스테이지 빌드로 이미지 크기 최적화
FROM gradle:8.5-jdk17 AS builder

WORKDIR /app
COPY build.gradle settings.gradle ./
COPY src ./src

# Gradle 캐시 활용을 위한 의존성 먼저 다운로드
RUN gradle dependencies --no-daemon

# 애플리케이션 빌드
RUN gradle clean build --no-daemon -x test

# 프로덕션 런타임 이미지
FROM openjdk:17-jre-slim

# 보안을 위한 비루트 사용자 생성
RUN groupadd -r spring && useradd -r -g spring spring

# 필수 패키지만 설치
RUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 빌드된 JAR 파일 복사
COPY --from=builder /app/build/libs/*.jar app.jar

# 애플리케이션 실행을 위한 설정
RUN chown -R spring:spring /app
USER spring

# JVM 메모리 최적화
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 헬스체크 설정
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Next.js 프로덕션 Dockerfile

# frontend/Dockerfile
FROM node:18-alpine AS base

# 의존성 설치
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci --only=production

# 빌더 스테이지
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js 텔레메트리 비활성화
ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

# 프로덕션 런타임
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# 빌드 출력물 복사
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

GitHub Actions CI/CD 파이프라인

자동화된 빌드, 테스트, 배포를 위한 GitHub Actions 워크플로우를 설정합니다:

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  REGISTRY: ghcr.io
  BACKEND_IMAGE_NAME: ${{ github.repository }}/backend
  FRONTEND_IMAGE_NAME: ${{ github.repository }}/frontend

jobs:
  # 백엔드 테스트 및 빌드
  backend-ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend

    services:
      mariadb:
        image: mariadb:10.11
        env:
          MYSQL_ROOT_PASSWORD: testpass
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - uses: actions/checkout@v4
    
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
        
    - name: Cache Gradle packages
      uses: actions/cache@v3
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        
    - name: Run tests
      run: ./gradlew test --no-daemon
      env:
        SPRING_PROFILES_ACTIVE: test
        SPRING_DATASOURCE_URL: jdbc:mariadb://localhost:3306/testdb
        SPRING_DATASOURCE_USERNAME: root
        SPRING_DATASOURCE_PASSWORD: testpass
        
    - name: Build application
      run: ./gradlew build --no-daemon
      
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: backend-test-results
        path: backend/build/reports/tests/

  # 프론트엔드 테스트 및 빌드
  frontend-ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run type check
      run: npm run type-check
      
    - name: Run linting
      run: npm run lint
      
    - name: Run tests
      run: npm test -- --watchAll=false
      
    - name: Build application
      run: npm run build
      env:
        NEXT_PUBLIC_API_BASE_URL: ${{ secrets.PRODUCTION_API_URL }}

  # Docker 이미지 빌드 및 푸시
  build-and-push:
    needs: [backend-ci, frontend-ci]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    permissions:
      contents: read
      packages: write

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Build and push backend image
      uses: docker/build-push-action@v5
      with:
        context: ./backend
        push: true
        tags: |
          ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:latest
          ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ github.sha }}

    - name: Build and push frontend image
      uses: docker/build-push-action@v5
      with:
        context: ./frontend
        push: true
        tags: |
          ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:latest
          ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ github.sha }}

  # 프로덕션 배포
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Deploy to production
      uses: appleboy/ssh-action@v1.0.0
      with:
        host: ${{ secrets.PRODUCTION_HOST }}
        username: ${{ secrets.PRODUCTION_USER }}
        key: ${{ secrets.PRODUCTION_SSH_KEY }}
        script: |
          cd /opt/app
          docker-compose pull
          docker-compose up -d
          docker system prune -f

프로덕션 Docker Compose

# docker-compose.prod.yml
version: '3.8'

services:
  mariadb:
    image: mariadb:10.11
    container_name: prod-mariadb
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - mariadb_data:/var/lib/mysql
      - ./backup:/backup
    restart: unless-stopped
    networks:
      - app-network
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

  backend:
    image: ghcr.io/${GITHUB_REPOSITORY}/backend:latest
    container_name: prod-backend
    environment:
      SPRING_PROFILES_ACTIVE: prod
      SPRING_DATASOURCE_URL: jdbc:mariadb://mariadb:3306/${DB_NAME}
      SPRING_DATASOURCE_USERNAME: ${DB_USER}
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      - mariadb
    restart: unless-stopped
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  frontend:
    image: ghcr.io/${GITHUB_REPOSITORY}/frontend:latest
    container_name: prod-frontend
    environment:
      NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: prod-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
    depends_on:
      - frontend
      - backend
    restart: unless-stopped
    networks:
      - app-network

volumes:
  mariadb_data:

networks:
  app-network:
    driver: bridge

Nginx 리버스 프록시 설정

# nginx/nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream backend {
        server backend:8080;
    }
    
    upstream frontend {
        server frontend:3000;
    }

    server {
        listen 80;
        server_name yourdomain.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name yourdomain.com;

        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        # API 요청
        location /api/ {
            proxy_pass http://backend;
            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;
            
            # CORS 헤더
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
            add_header Access-Control-Allow-Headers "Authorization, Content-Type";
        }

        # 프론트엔드 요청
        location / {
            proxy_pass http://frontend;
            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;
        }
    }
}

모니터링 및 로깅

Prometheus + Grafana 설정

# monitoring/docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3001:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
      - ./grafana/datasources:/etc/grafana/provisioning/datasources

volumes:
  prometheus_data:
  grafana_data:

Cursor AI 활용 배포 스크립트

Cursor AI를 사용하여 자동화된 배포 스크립트를 생성합니다:

#!/bin/bash
# scripts/deploy.sh

set -e

echo "🚀 프로덕션 배포를 시작합니다..."

# 환경 변수 확인
if [ -z "$PRODUCTION_HOST" ] || [ -z "$GITHUB_TOKEN" ]; then
    echo "❌ 필요한 환경 변수가 설정되지 않았습니다."
    exit 1
fi

# 최신 이미지 가져오기
echo "📦 최신 Docker 이미지를 가져오는 중..."
docker-compose -f docker-compose.prod.yml pull

# 데이터베이스 백업
echo "💾 데이터베이스 백업 중..."
docker exec prod-mariadb mysqldump -u root -p${DB_ROOT_PASSWORD} ${DB_NAME} > backup/backup_$(date +%Y%m%d_%H%M%S).sql

# 무중단 배포
echo "🔄 서비스 업데이트 중..."
docker-compose -f docker-compose.prod.yml up -d --remove-orphans

# 헬스체크
echo "🏥 서비스 상태 확인 중..."
sleep 30

if curl -f http://localhost/api/actuator/health; then
    echo "✅ 배포가 성공적으로 완료되었습니다!"
    
    # 슬랙 알림 (선택사항)
    if [ -n "$SLACK_WEBHOOK_URL" ]; then
        curl -X POST -H 'Content-type: application/json' \
            --data '{"text":"🎉 프로덕션 배포가 성공했습니다!"}' \
            $SLACK_WEBHOOK_URL
    fi
else
    echo "❌ 배포에 실패했습니다. 롤백을 진행합니다..."
    docker-compose -f docker-compose.prod.yml rollback
    exit 1
fi

# 정리 작업
echo "🧹 불필요한 리소스 정리 중..."
docker system prune -f

echo "🎯 배포 프로세스가 완료되었습니다!"

성능 최적화 체크리스트

Spring Boot 프로덕션 설정

# backend/src/main/resources/application-prod.yml
spring:
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        order_updates: true
        
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 30000
      
server:
  port: 8080
  compression:
    enabled: true
  http2:
    enabled: true
    
logging:
  level:
    org.springframework.web: INFO
    org.hibernate.SQL: WARN

마무리

이 5편 시리즈를 통해 Cursor AISpring Boot 3.2.x LTS, Next.js 15.0.4, React 18, TypeScript 5, MariaDB를 활용한 완전한 풀스택 개발 환경을 구축하는 방법을 상세히 다뤘습니다. AI 기반 개발 도구의 활용으로 개발 생산성이 크게 향상되었으며, 체계적인 환경 설정을 통해 안정적인 프로덕션 서비스를 구축할 수 있습니다.


 

반응형