From 1872cd6ace5e364bace159b1a28802645fb2dcae Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Mon, 3 Nov 2025 23:34:20 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20auction=20history=20batch=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EB=8F=99=EA=B8=B0=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/AuctionHistoryScheduler.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java index e697ea7..55bc2ab 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java @@ -4,6 +4,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import until.the.eternity.auctionhistory.application.service.AuctionHistoryService; @@ -22,14 +23,34 @@ public class AuctionHistoryScheduler { private final AuctionHistoryFetcher fetcher; private final AuctionHistoryPersister persister; + @Value("${openapi.auction-history.delay-ms}") + private long delayMs; + @Scheduled(cron = "${openapi.auction-history.cron}", zone = "Asia/Seoul") public void fetchAndSaveAuctionHistoryAll() { List newEntities = new ArrayList<>(); - for (ItemCategory category : ItemCategory.values()) { + ItemCategory[] categories = ItemCategory.values(); + + for (int i = 0; i < categories.length; i++) { + ItemCategory category = categories[i]; try { + log.debug("> [SCHEDULE] Processing category [{}]", category.getSubCategory()); List fetchedDtos = fetcher.fetch(category); List entities = persister.filterOutExisting(fetchedDtos, category); newEntities.addAll(entities); + + // 마지막 카테고리가 아닌 경우에만 delay 적용 + if (i < categories.length - 1) { + log.debug("> [SCHEDULE] Waiting {}ms before processing next category", delayMs); + Thread.sleep(delayMs); + } + } catch (InterruptedException e) { + log.error( + "> [SCHEDULE] Thread interrupted during delay for category [{}]", + category.getSubCategory(), + e); + Thread.currentThread().interrupt(); + break; } catch (Exception e) { log.error( "> [SCHEDULE] Error during processing category [{}]", From 3cadd15c243d9abfa68db2ffea3e7e67da20bf7a Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Mon, 3 Nov 2025 23:59:42 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20auction=20history=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 3 + .../scheduler/AuctionHistoryScheduler.java | 101 +++++++++++++----- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index cab85d3..83d1cca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,6 +2,9 @@ version: "3.8" services: spring-app: + build: + context: . + dockerfile: Dockerfile image: ${DOCKER_USERNAME}/${DOCKER_REPO}:${DOCKER_IMAGE_TAG:-latest} container_name: spring-app ports: diff --git a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java index 55bc2ab..8ff81bb 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java @@ -1,7 +1,11 @@ package until.the.eternity.auctionhistory.application.scheduler; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -28,39 +32,84 @@ public class AuctionHistoryScheduler { @Scheduled(cron = "${openapi.auction-history.cron}", zone = "Asia/Seoul") public void fetchAndSaveAuctionHistoryAll() { - List newEntities = new ArrayList<>(); - ItemCategory[] categories = ItemCategory.values(); + // ItemCategory를 topCategory별로 그룹화 + Map> categoriesByTopCategory = + Arrays.stream(ItemCategory.values()) + .collect( + Collectors.groupingBy( + ItemCategory::getTopCategory, + LinkedHashMap::new, + Collectors.toList())); - for (int i = 0; i < categories.length; i++) { - ItemCategory category = categories[i]; - try { - log.debug("> [SCHEDULE] Processing category [{}]", category.getSubCategory()); - List fetchedDtos = fetcher.fetch(category); - List entities = persister.filterOutExisting(fetchedDtos, category); - newEntities.addAll(entities); + int totalSavedCount = 0; + List topCategories = new ArrayList<>(categoriesByTopCategory.keySet()); - // 마지막 카테고리가 아닌 경우에만 delay 적용 - if (i < categories.length - 1) { - log.debug("> [SCHEDULE] Waiting {}ms before processing next category", delayMs); + for (int topIndex = 0; topIndex < topCategories.size(); topIndex++) { + String topCategory = topCategories.get(topIndex); + List subCategories = categoriesByTopCategory.get(topCategory); + List newEntities = new ArrayList<>(); + + log.debug("> [SCHEDULE] Processing top category [{}]", topCategory); + + for (int subIndex = 0; subIndex < subCategories.size(); subIndex++) { + ItemCategory category = subCategories.get(subIndex); + try { + log.debug("> [SCHEDULE] Processing category [{}]", category.getSubCategory()); + List fetchedDtos = fetcher.fetch(category); + List entities = + persister.filterOutExisting(fetchedDtos, category); + newEntities.addAll(entities); + + // 마지막 서브 카테고리가 아닌 경우에만 delay 적용 + if (subIndex < subCategories.size() - 1) { + log.debug( + "> [SCHEDULE] Waiting {}ms before processing next category", + delayMs); + Thread.sleep(delayMs); + } + } catch (InterruptedException e) { + log.error( + "> [SCHEDULE] Thread interrupted during delay for category [{}]", + category.getSubCategory(), + e); + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + log.error( + "> [SCHEDULE] Error during processing category [{}]", + category.getSubCategory(), + e); + } + } + + // Top Category별로 저장 + service.saveAll(newEntities); + totalSavedCount += newEntities.size(); + log.info( + "> [SCHEDULE] Saved [{}] new auction history records for top category [{}]", + newEntities.size(), + topCategory); + + // 마지막 탑 카테고리가 아닌 경우에만 delay 적용 + if (topIndex < topCategories.size() - 1) { + try { + log.debug( + "> [SCHEDULE] Waiting {}ms before processing next top category", + delayMs); Thread.sleep(delayMs); + } catch (InterruptedException e) { + log.error( + "> [SCHEDULE] Thread interrupted during delay for top category [{}]", + topCategory, + e); + Thread.currentThread().interrupt(); + break; } - } catch (InterruptedException e) { - log.error( - "> [SCHEDULE] Thread interrupted during delay for category [{}]", - category.getSubCategory(), - e); - Thread.currentThread().interrupt(); - break; - } catch (Exception e) { - log.error( - "> [SCHEDULE] Error during processing category [{}]", - category.getSubCategory(), - e); } } - service.saveAll(newEntities); + log.info( "> [SCHEDULE] AuctionHistoryScheduler saved [{}] new auction history records complete", - newEntities.size()); + totalSavedCount); } } From 0541ad45cee9e4788039d60fa441a9a86bb0661f Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Tue, 4 Nov 2025 00:37:36 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20docker=20compose=20local=EC=9A=A9?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.local.sample | 114 +++++++++++++++++++++++++++++++++++++ .gitignore | 3 +- README.md | 59 ++++++++++++++++++++ docker-compose.local.yml | 118 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 .env.local.sample create mode 100644 docker-compose.local.yml diff --git a/.env.local.sample b/.env.local.sample new file mode 100644 index 0000000..4e2fd45 --- /dev/null +++ b/.env.local.sample @@ -0,0 +1,114 @@ +# ============================================================================= +# Local Development Environment Configuration +# ============================================================================= +# 로컬 개발 환경용 설정 파일 +# +# 사용법: +# 1. 이 파일을 .env.local로 복사하세요 +# 2. 필요한 값들을 채워넣으세요 +# 3. docker-compose -f docker-compose.local.yml up --build 로 실행하세요 +# ============================================================================= + +# ============================================================================= +# Application Configuration +# ============================================================================= +SERVER_PORT=8080 +SPRING_PROFILES_ACTIVE=local + +# ============================================================================= +# Database Configuration +# ============================================================================= +# 로컬 MySQL 사용 시 (Docker로 MySQL 실행하는 경우) +DB_IP=host.docker.internal # Docker 컨테이너에서 호스트 머신 접근 +# DB_IP=localhost # 호스트 머신에서 직접 실행 시 +DB_PORT=3306 +DB_SCHEMA=devnogi +DB_USER=root +DB_PASSWORD=your_local_password + +# ============================================================================= +# Security Configuration (로컬 개발용 - 운영 환경과 다른 값 사용) +# ============================================================================= +JWT_SECRET_KEY=local-development-secret-key-do-not-use-in-production-change-this +JWT_ACCESS_TOKEN_VALIDITY=3600000 # 1시간 (밀리초) +JWT_REFRESH_TOKEN_VALIDITY=86400000 # 24시간 (밀리초) + +# ============================================================================= +# External API Configuration +# ============================================================================= +# Nexon Open API 키 (https://openapi.nexon.com/에서 발급) +NEXON_OPEN_API_KEY=your_nexon_api_key_here + +# 경매 데이터 수집 설정 +AUCTION_HISTORY_DELAY_MS=1000 # API 호출 간 딜레이 (1초) +AUCTION_HISTORY_CRON=0 0 * * * * # 매시간 정각에 실행 (초 분 시 일 월 요일) +AUCTION_HISTORY_MIN_PRICE_CRON=0 30 * * * * # 매시간 30분에 실행 + +# ============================================================================= +# Docker Configuration (로컬 개발에서는 불필요) +# ============================================================================= +# DOCKER_USERNAME=your_dockerhub_username +# DOCKER_PASSWORD=your_dockerhub_password +# DOCKER_REPO=open-api-batch-server +# DOCKER_IMAGE_TAG=local + +# ============================================================================= +# JVM Memory Configuration (로컬 개발용 - 메모리 사용량 감소) +# ============================================================================= +# Heap Memory - 로컬에서는 적은 메모리로 실행 +JAVA_OPTS_XMS=256m # 초기 힙 메모리 +JAVA_OPTS_XMX=512m # 최대 힙 메모리 + +# Non-Heap Memory +JAVA_OPTS_MAX_METASPACE_SIZE=128m # Metaspace 최대 크기 +JAVA_OPTS_RESERVED_CODE_CACHE_SIZE=64m # JIT 컴파일된 코드 캐시 +JAVA_OPTS_MAX_DIRECT_MEMORY_SIZE=64m # Direct Buffer 최대 크기 +JAVA_OPTS_XSS=512k # 스레드 스택 크기 + +# ============================================================================= +# JVM GC Configuration (G1GC) +# ============================================================================= +JAVA_OPTS_MAX_GC_PAUSE_MILLIS=200 # GC 일시정지 목표 시간 +JAVA_OPTS_G1_HEAP_REGION_SIZE=1m # G1 힙 영역 크기 +JAVA_OPTS_INITIATING_HEAP_OCCUPANCY_PERCENT=45 # GC 시작 힙 점유율 + +# ============================================================================= +# JVM Compiler Configuration +# ============================================================================= +# 로컬 개발에서는 빠른 시작을 위해 TieredStopAtLevel=1 (C1 컴파일러만 사용) +JAVA_OPTS_TIERED_STOP_AT_LEVEL=1 # 1: 빠른 시작, 4: 최적 성능 +JAVA_OPTS_CI_COMPILER_COUNT=2 # 컴파일러 스레드 수 + +# ============================================================================= +# Docker Container Resource Limits (로컬 개발용) +# ============================================================================= +DOCKER_MEMORY_LIMIT=1g # 컨테이너 최대 메모리 +DOCKER_MEMORY_RESERVATION=512m # 예약 메모리 + +# ============================================================================= +# Container Restart Policy +# ============================================================================= +RESTART_POLICY_MAX_RETRIES=3 # 실패 시 최대 재시작 횟수 + +# ============================================================================= +# Health Check Configuration (로컬 개발용 - 더 짧은 간격) +# ============================================================================= +HEALTHCHECK_INTERVAL=30s # 헬스 체크 주기 +HEALTHCHECK_TIMEOUT=10s # 헬스 체크 타임아웃 +HEALTHCHECK_RETRIES=3 # 연속 실패 횟수 +HEALTHCHECK_START_PERIOD=60s # 시작 유예 기간 + +# ============================================================================= +# Autoheal Configuration +# ============================================================================= +AUTOHEAL_INTERVAL=30 # unhealthy 체크 주기 (초) +AUTOHEAL_START_PERIOD=0 # 체크 시작 유예 시간 +AUTOHEAL_DEFAULT_STOP_TIMEOUT=10 # 재시작 시 강제 종료 대기 시간 +AUTOHEAL_MEMORY_LIMIT=50M # autoheal 최대 메모리 +AUTOHEAL_MEMORY_RESERVATION=20M # autoheal 예약 메모리 + +# ============================================================================= +# Logging Configuration +# ============================================================================= +LOGGING_MAX_SIZE=10m # 로그 파일 최대 크기 +LOGGING_MAX_FILE=3 # 로그 파일 보관 개수 diff --git a/.gitignore b/.gitignore index e72e294..5306061 100644 --- a/.gitignore +++ b/.gitignore @@ -53,12 +53,13 @@ spy.log ### Environment Variables # Exclude actual .env file (contains real secrets) .env +.env.local .env.dev .env.prod # Include environment templates (placeholder values only) # - .env.sample: Template with all available variables -# - .env: Local development configuration template +# - .env.local.sample: Local development configuration template # - .env.dev: Development server configuration template # - .env.prod: Production server configuration template # NOTE: These template files should ONLY contain placeholder values, never real secrets! diff --git a/README.md b/README.md index dbbcae1..f2857bd 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,62 @@ - **Git branch 전략**: Git-flow [관련 블로그](https://velog.io/@kw2577/Git-branch-%EC%A0%84%EB%9E%B5)
+ +### 🐳 로컬 개발 환경 (Docker) + +로컬에서 코드를 수정하면서 개발할 때는 Docker Hub에 푸시하지 않고 로컬 빌드로 실행할 수 있습니다. + +#### 1. 환경 설정 + +```bash +# .env.local.sample을 복사하여 .env.local 생성 +cp .env.local.sample .env.local + +# .env.local 파일을 열어서 필요한 값들을 수정 +# - NEXON_OPEN_API_KEY: Nexon Open API 키 입력 +# - DB_PASSWORD: 로컬 MySQL 비밀번호 입력 +# - 기타 필요한 설정 수정 +``` + +#### 2. 로컬에서 Docker로 실행 + +```bash +# 로컬 코드를 빌드하고 Docker 컨테이너로 실행 +docker-compose -f docker-compose.local.yml up --build + +# 백그라운드 실행 +docker-compose -f docker-compose.local.yml up -d --build + +# 로그 확인 +docker-compose -f docker-compose.local.yml logs -f spring-app + +# 중지 +docker-compose -f docker-compose.local.yml down +``` + +#### 3. 코드 수정 후 재실행 + +```bash +# 코드 수정 후 다시 빌드하여 실행 +docker-compose -f docker-compose.local.yml up --build + +# 또는 기존 컨테이너 정리 후 재실행 +docker-compose -f docker-compose.local.yml down +docker-compose -f docker-compose.local.yml up --build +``` + +#### 4. 환경별 실행 방법 + +| 환경 | Docker Compose 파일 | 설명 | +|------|---------------------|------| +| **로컬 개발** | `docker-compose.local.yml` | 로컬 코드 빌드, 낮은 리소스 사용 | +| **개발/운영 서버** | `docker-compose.yaml` | Docker Hub 이미지 사용 | + +#### 5. 참고사항 + +- **로컬 개발**: 코드 수정 시마다 `--build` 옵션으로 재빌드 필요 +- **메모리 설정**: 로컬 환경은 메모리 사용량이 낮게 설정되어 있음 (512MB) +- **데이터베이스**: `DB_IP=host.docker.internal`로 호스트 머신의 MySQL에 접근 +- **포트**: 기본 8080 포트 사용 (`.env.local`에서 변경 가능) + +
diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..07b99d8 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,118 @@ +version: "3.8" + +# 로컬 개발 환경용 Docker Compose 설정 +# 사용법: docker-compose -f docker-compose.local.yml up --build + +services: + spring-app: + build: + context: . + dockerfile: Dockerfile + # 로컬 이미지 이름 (Docker Hub에 push하지 않음) + image: open-api-batch-server:local + container_name: spring-app-local + ports: + - "${SERVER_PORT:-8080}:${SERVER_PORT:-8080}" + env_file: + - .env.local # 로컬 환경 변수 파일 + labels: + autoheal: "true" + environment: + # === Application Configuration === + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-local} + LANG: C.UTF-8 + LC_ALL: C.UTF-8 + SERVER_PORT: ${SERVER_PORT:-8080} + + # === Database Configuration === + DB_IP: ${DB_IP:-localhost} + DB_PORT: ${DB_PORT:-3306} + DB_SCHEMA: ${DB_SCHEMA:-devnogi} + DB_USER: ${DB_USER:-root} + DB_PASSWORD: ${DB_PASSWORD:-password} + + # === Security Configuration === + JWT_SECRET_KEY: ${JWT_SECRET_KEY:-local-dev-secret-key-change-in-production} + JWT_ACCESS_TOKEN_VALIDITY: ${JWT_ACCESS_TOKEN_VALIDITY:-3600000} + JWT_REFRESH_TOKEN_VALIDITY: ${JWT_REFRESH_TOKEN_VALIDITY:-86400000} + + # === External API Configuration === + NEXON_OPEN_API_KEY: ${NEXON_OPEN_API_KEY} + AUCTION_HISTORY_DELAY_MS: ${AUCTION_HISTORY_DELAY_MS:-1000} + AUCTION_HISTORY_CRON: "${AUCTION_HISTORY_CRON:-0 0 * * * *}" + AUCTION_HISTORY_MIN_PRICE_CRON: "${AUCTION_HISTORY_MIN_PRICE_CRON:-0 30 * * * *}" + + # === JVM Configuration (로컬 개발용 - 메모리 사용량 감소) === + JAVA_OPTS: >- + -Xms${JAVA_OPTS_XMS:-256m} + -Xmx${JAVA_OPTS_XMX:-512m} + -XX:MaxMetaspaceSize=${JAVA_OPTS_MAX_METASPACE_SIZE:-128m} + -XX:ReservedCodeCacheSize=${JAVA_OPTS_RESERVED_CODE_CACHE_SIZE:-64m} + -XX:MaxDirectMemorySize=${JAVA_OPTS_MAX_DIRECT_MEMORY_SIZE:-64m} + -Xss${JAVA_OPTS_XSS:-512k} + -XX:+UseG1GC + -XX:MaxGCPauseMillis=${JAVA_OPTS_MAX_GC_PAUSE_MILLIS:-200} + -XX:G1HeapRegionSize=${JAVA_OPTS_G1_HEAP_REGION_SIZE:-1m} + -XX:InitiatingHeapOccupancyPercent=${JAVA_OPTS_INITIATING_HEAP_OCCUPANCY_PERCENT:-45} + -XX:+TieredCompilation + -XX:TieredStopAtLevel=${JAVA_OPTS_TIERED_STOP_AT_LEVEL:-1} + -XX:CICompilerCount=${JAVA_OPTS_CI_COMPILER_COUNT:-2} + -XX:+UseCompressedOops + -XX:+UseCompressedClassPointers + -Djava.security.egd=file:/dev/./urandom + -Dspring.jmx.enabled=false + volumes: + - ./logs:/app/logs + # 로컬 개발 시 설정 파일 마운트 (선택사항) + # - ./config:/app/config:ro + restart: on-failure:${RESTART_POLICY_MAX_RETRIES:-3} + + # 로컬 개발용 리소스 제한 (더 적은 리소스) + deploy: + resources: + limits: + memory: ${DOCKER_MEMORY_LIMIT:-1g} + reservations: + memory: ${DOCKER_MEMORY_RESERVATION:-512m} + + networks: + - app-network + + # Health Check + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${SERVER_PORT:-8080}/actuator/health"] + interval: ${HEALTHCHECK_INTERVAL:-30s} + timeout: ${HEALTHCHECK_TIMEOUT:-10s} + retries: ${HEALTHCHECK_RETRIES:-3} + start_period: ${HEALTHCHECK_START_PERIOD:-60s} + + logging: + driver: "json-file" + options: + max-size: ${LOGGING_MAX_SIZE:-10m} + max-file: "${LOGGING_MAX_FILE:-3}" + + # Autoheal: unhealthy 컨테이너 자동 재시작 + autoheal: + image: willfarrell/autoheal:latest + container_name: autoheal-local + restart: unless-stopped + environment: + AUTOHEAL_INTERVAL: ${AUTOHEAL_INTERVAL:-30} + AUTOHEAL_START_PERIOD: ${AUTOHEAL_START_PERIOD:-0} + AUTOHEAL_DEFAULT_STOP_TIMEOUT: ${AUTOHEAL_DEFAULT_STOP_TIMEOUT:-10} + DOCKER_SOCK: /var/run/docker.sock + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + networks: + - app-network + deploy: + resources: + limits: + memory: ${AUTOHEAL_MEMORY_LIMIT:-50m} + reservations: + memory: ${AUTOHEAL_MEMORY_RESERVATION:-20m} + +networks: + app-network: + driver: bridge From dd893268cc5c1a51ced3f307d4eae876ba7d4329 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Tue, 4 Nov 2025 17:39:17 +0900 Subject: [PATCH 4/7] fix: change docker file competable macos --- Dockerfile | 3 ++- docker-compose.local.yml | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 230dbea..c6456c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ # Stage 3: Runtime Stage - 최종 런타임 이미지 # Stage 1: Build Stage -FROM gradle:8.5-jdk21-alpine AS builder +# alpine 제거하여 ARM64(Apple Silicon)와 AMD64(Intel/AMD) 모두 지원 +FROM gradle:8.5-jdk21 AS builder # 작업 디렉토리 설정 WORKDIR /app diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 07b99d8..4b84411 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,7 +1,8 @@ version: "3.8" # 로컬 개발 환경용 Docker Compose 설정 -# 사용법: docker-compose -f docker-compose.local.yml up --build +# 사용법: docker-compose -f docker-compose.local.yml --env-file .env.local up --build +# 참고: 쉘 환경 변수가 .env.local 파일의 값을 오버라이드할 수 있으므로 --env-file 옵션을 명시하는 것이 좋습니다 services: spring-app: @@ -77,6 +78,7 @@ services: networks: - app-network + - my-network # MySQL 컨테이너와 통신을 위해 추가 # Health Check healthcheck: @@ -116,3 +118,6 @@ services: networks: app-network: driver: bridge + my-network: + external: true # 외부에서 생성된 네트워크 사용 + name: open-api-batch-server_my-network From 6ef83f7729eb52282f033245da3bb77e29fbd994 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Tue, 4 Nov 2025 17:39:17 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20macos=EB=A5=BC=20=EA=B3=A0=EB=A0=A4?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20docker=20file=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 ++- docker-compose.local.yml | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 230dbea..c6456c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,8 @@ # Stage 3: Runtime Stage - 최종 런타임 이미지 # Stage 1: Build Stage -FROM gradle:8.5-jdk21-alpine AS builder +# alpine 제거하여 ARM64(Apple Silicon)와 AMD64(Intel/AMD) 모두 지원 +FROM gradle:8.5-jdk21 AS builder # 작업 디렉토리 설정 WORKDIR /app diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 07b99d8..4b84411 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,7 +1,8 @@ version: "3.8" # 로컬 개발 환경용 Docker Compose 설정 -# 사용법: docker-compose -f docker-compose.local.yml up --build +# 사용법: docker-compose -f docker-compose.local.yml --env-file .env.local up --build +# 참고: 쉘 환경 변수가 .env.local 파일의 값을 오버라이드할 수 있으므로 --env-file 옵션을 명시하는 것이 좋습니다 services: spring-app: @@ -77,6 +78,7 @@ services: networks: - app-network + - my-network # MySQL 컨테이너와 통신을 위해 추가 # Health Check healthcheck: @@ -116,3 +118,6 @@ services: networks: app-network: driver: bridge + my-network: + external: true # 외부에서 생성된 네트워크 사용 + name: open-api-batch-server_my-network From e7bf18110287b945d19f3c6b322e2439766d381b Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Wed, 5 Nov 2025 00:10:30 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20docker-compose-local=EC=97=90=20mys?= =?UTF-8?q?ql=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/push-cd-dev.yml | 2 +- .github/workflows/push-cd-prod.yml | 2 +- README.md | 14 ++--- ...er-compose.yaml => docker-compose-dev.yaml | 0 ...pose.local.yml => docker-compose-local.yml | 59 ++++++++++++++++--- docs/CD_PIPELINE_GUIDE.md | 6 +- 6 files changed, 63 insertions(+), 20 deletions(-) rename docker-compose.yaml => docker-compose-dev.yaml (100%) rename docker-compose.local.yml => docker-compose-local.yml (68%) diff --git a/.github/workflows/push-cd-dev.yml b/.github/workflows/push-cd-dev.yml index 553dd03..3f96937 100644 --- a/.github/workflows/push-cd-dev.yml +++ b/.github/workflows/push-cd-dev.yml @@ -91,7 +91,7 @@ jobs: run: | ssh -i ~/.ssh/my-key.pem ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "mkdir -p /home/${{ secrets.SERVER_USER }}/app/logs" - - name: Copy docker-compose.yaml to server + - name: Copy docker-compose-dev.yaml to server run: | scp -i ~/.ssh/my-key.pem docker-compose.yaml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:/home/${{ secrets.SERVER_USER }}/app/ diff --git a/.github/workflows/push-cd-prod.yml b/.github/workflows/push-cd-prod.yml index 01256f4..c1c7e89 100644 --- a/.github/workflows/push-cd-prod.yml +++ b/.github/workflows/push-cd-prod.yml @@ -92,7 +92,7 @@ jobs: run: | ssh -i ~/.ssh/my-key.pem ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} "mkdir -p /home/${{ secrets.PROD_SERVER_USER }}/app/logs" - - name: Copy docker-compose.yaml to server + - name: Copy docker-compose-dev.yaml to server run: | scp -i ~/.ssh/my-key.pem docker-compose.yaml ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }}:/home/${{ secrets.PROD_SERVER_USER }}/app/ diff --git a/README.md b/README.md index f2857bd..60ff1c8 100644 --- a/README.md +++ b/README.md @@ -63,27 +63,27 @@ cp .env.local.sample .env.local ```bash # 로컬 코드를 빌드하고 Docker 컨테이너로 실행 -docker-compose -f docker-compose.local.yml up --build +docker-compose -f docker-compose-local.yml up --build # 백그라운드 실행 -docker-compose -f docker-compose.local.yml up -d --build +docker-compose -f docker-compose-local.yml up -d --build # 로그 확인 -docker-compose -f docker-compose.local.yml logs -f spring-app +docker-compose -f docker-compose-local.yml logs -f spring-app # 중지 -docker-compose -f docker-compose.local.yml down +docker-compose -f docker-compose-local.yml down ``` #### 3. 코드 수정 후 재실행 ```bash # 코드 수정 후 다시 빌드하여 실행 -docker-compose -f docker-compose.local.yml up --build +docker-compose -f docker-compose-local.yml up --build # 또는 기존 컨테이너 정리 후 재실행 -docker-compose -f docker-compose.local.yml down -docker-compose -f docker-compose.local.yml up --build +docker-compose -f docker-compose-local.yml down +docker-compose -f docker-compose-local.yml up --build ``` #### 4. 환경별 실행 방법 diff --git a/docker-compose.yaml b/docker-compose-dev.yaml similarity index 100% rename from docker-compose.yaml rename to docker-compose-dev.yaml diff --git a/docker-compose.local.yml b/docker-compose-local.yml similarity index 68% rename from docker-compose.local.yml rename to docker-compose-local.yml index 4b84411..987c850 100644 --- a/docker-compose.local.yml +++ b/docker-compose-local.yml @@ -1,7 +1,7 @@ version: "3.8" # 로컬 개발 환경용 Docker Compose 설정 -# 사용법: docker-compose -f docker-compose.local.yml --env-file .env.local up --build +# 사용법: docker-compose -f docker-compose-local.yml --env-file .env.local up --build # 참고: 쉘 환경 변수가 .env.local 파일의 값을 오버라이드할 수 있으므로 --env-file 옵션을 명시하는 것이 좋습니다 services: @@ -26,11 +26,12 @@ services: SERVER_PORT: ${SERVER_PORT:-8080} # === Database Configuration === - DB_IP: ${DB_IP:-localhost} - DB_PORT: ${DB_PORT:-3306} + # Docker 네트워크 내부 연결 설정 (고정값) + DB_IP: mysql # Docker 서비스 이름 + DB_PORT: 3306 # Docker 네트워크 내부 포트 DB_SCHEMA: ${DB_SCHEMA:-devnogi} - DB_USER: ${DB_USER:-root} - DB_PASSWORD: ${DB_PASSWORD:-password} + DB_USER: root # MySQL 컨테이너의 root 사용자 + DB_PASSWORD: ${DB_ROOT_PASSWORD:-password} # === Security Configuration === JWT_SECRET_KEY: ${JWT_SECRET_KEY:-local-dev-secret-key-change-in-production} @@ -80,13 +81,18 @@ services: - app-network - my-network # MySQL 컨테이너와 통신을 위해 추가 + # MySQL이 준비될 때까지 대기 + depends_on: + mysql: + condition: service_healthy + # Health Check healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${SERVER_PORT:-8080}/actuator/health"] interval: ${HEALTHCHECK_INTERVAL:-30s} timeout: ${HEALTHCHECK_TIMEOUT:-10s} retries: ${HEALTHCHECK_RETRIES:-3} - start_period: ${HEALTHCHECK_START_PERIOD:-60s} + start_period: ${HEALTHCHECK_START_PERIOD:-90s} # MySQL 초기화 시간 고려하여 증가 logging: driver: "json-file" @@ -94,6 +100,41 @@ services: max-size: ${LOGGING_MAX_SIZE:-10m} max-file: "${LOGGING_MAX_FILE:-3}" + # MySQL Database + mysql: + image: mysql:8.0 + container_name: open-api-batch-mysql + restart: unless-stopped + env_file: + - .env.local # 환경 변수 파일 로드 + ports: + - "${MYSQL_EXTERNAL_PORT:-3306}:3306" # 외부 접속용 포트 (호스트에서 접근 시) + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-password} + MYSQL_DATABASE: ${DB_SCHEMA:-devnogi} + # MYSQL_USER는 root를 지정할 수 없으므로 제거 + # root 사용자는 MYSQL_ROOT_PASSWORD로 자동 생성됨 + LANG: C.UTF_8 + TZ: Asia/Seoul + volumes: + - mysql_data:/var/lib/mysql + networks: + - my-network + # MySQL Health Check + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD:-password}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_0900_ai_ci + - --skip-character-set-client-handshake + - --sql-mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION + - --default-time-zone=+09:00 # MySQL 레벨 타임존 설정 + - --explicit_defaults_for_timestamp=1 # TIMESTAMP 기본값 명시 허용 + # Autoheal: unhealthy 컨테이너 자동 재시작 autoheal: image: willfarrell/autoheal:latest @@ -115,9 +156,11 @@ services: reservations: memory: ${AUTOHEAL_MEMORY_RESERVATION:-20m} +volumes: + mysql_data: + networks: app-network: driver: bridge my-network: - external: true # 외부에서 생성된 네트워크 사용 - name: open-api-batch-server_my-network + driver: bridge diff --git a/docs/CD_PIPELINE_GUIDE.md b/docs/CD_PIPELINE_GUIDE.md index 78d2fb4..7a3512b 100644 --- a/docs/CD_PIPELINE_GUIDE.md +++ b/docs/CD_PIPELINE_GUIDE.md @@ -277,7 +277,7 @@ jobs: **Step 3-3: docker-compose.yaml 복사** ```yaml -- name: Copy docker-compose.yaml to server +- name: Copy docker-compose-dev.yaml to server run: | scp -i ~/.ssh/my-key.pem docker-compose.yaml ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:/home/${{ secrets.SERVER_USER }}/app/ ``` @@ -1396,7 +1396,7 @@ ssh-keyscan -H 138.2.126.248 >> ~/.ssh/known_hosts **6. docker-compose.yaml 복사** ```bash scp -i ~/.ssh/my-key.pem \ - docker-compose.yaml \ + docker-compose-dev.yaml \ ubuntu@138.2.126.248:/home/ubuntu/app/ ``` @@ -1727,7 +1727,7 @@ docker logs autoheal docker inspect spring-app | grep -A 5 Labels # "autoheal": "true" 있어야 함 -# 4. docker-compose.yaml 확인 +# 4. docker-compose-dev.yaml 확인 # spring-app에 다음이 있어야 함: labels: autoheal: "true" From 754a3a6c860d59d82a80cb4d9117c74d99347eee Mon Sep 17 00:00:00 2001 From: Sanghyun Yi Date: Wed, 5 Nov 2025 00:37:55 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20docker=20compose=20logs=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=84=B0=EB=A6=AC=20=EA=B6=8C=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++++ docker-compose-dev.yaml | 6 +++++- docker-compose-local.yml | 5 +++-- src/main/resources/logback/logback-appender.xml | 5 +++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index c6456c7..7dad255 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,10 @@ COPY --from=extractor --chown=spring:spring /app/spring-boot-loader/ ./ COPY --from=extractor --chown=spring:spring /app/snapshot-dependencies/ ./ COPY --from=extractor --chown=spring:spring /app/application/ ./ +# 로그 디렉토리 생성 및 권한 설정 (Named volume 마운트 전에 실행됨) +RUN mkdir -p /app/logs /app/logs/archive && \ + chown -R spring:spring /app/logs + # 사용자 전환 USER spring:spring diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 83d1cca..01410f4 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -65,7 +65,7 @@ services: -Djava.security.egd=file:/dev/./urandom -Dspring.jmx.enabled=false volumes: - - ./logs:/app/logs + - app-logs:/app/logs # Named volume 사용 (권한 문제 해결) # - ./config:/app/config:ro # Optional: mount external config directory # Restart Policy: # - always: 항상 재시작 (수동 stop 포함) @@ -127,6 +127,10 @@ services: reservations: memory: ${AUTOHEAL_MEMORY_RESERVATION} +volumes: + app-logs: + driver: local + networks: app-network: driver: bridge \ No newline at end of file diff --git a/docker-compose-local.yml b/docker-compose-local.yml index bfc6e92..6244012 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -13,7 +13,6 @@ services: image: open-api-batch-server:local container_name: spring-app-local ports: - - - "${SERVER_PORT:-8080}:${SERVER_PORT:-8080}" env_file: - .env.local # 로컬 환경 변수 파일 @@ -65,7 +64,7 @@ services: -Djava.security.egd=file:/dev/./urandom -Dspring.jmx.enabled=false volumes: - - ./logs:/app/logs + - app-logs:/app/logs # Named volume 사용 (권한 문제 해결) # 로컬 개발 시 설정 파일 마운트 (선택사항) # - ./config:/app/config:ro restart: on-failure:${RESTART_POLICY_MAX_RETRIES:-3} @@ -159,6 +158,8 @@ services: volumes: mysql_data: + app-logs: + driver: local networks: app-network: diff --git a/src/main/resources/logback/logback-appender.xml b/src/main/resources/logback/logback-appender.xml index 754d2ce..de6c225 100644 --- a/src/main/resources/logback/logback-appender.xml +++ b/src/main/resources/logback/logback-appender.xml @@ -5,8 +5,9 @@ class="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/> - - + + +