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/.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/.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/Dockerfile b/Dockerfile
index 230dbea..7dad255 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
@@ -60,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/README.md b/README.md
index dbbcae1..60ff1c8 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.yaml b/docker-compose-dev.yaml
similarity index 96%
rename from docker-compose.yaml
rename to docker-compose-dev.yaml
index cab85d3..01410f4 100644
--- a/docker-compose.yaml
+++ b/docker-compose-dev.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:
@@ -62,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 포함)
@@ -124,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
new file mode 100644
index 0000000..6244012
--- /dev/null
+++ b/docker-compose-local.yml
@@ -0,0 +1,168 @@
+version: "3.8"
+
+# 로컬 개발 환경용 Docker Compose 설정
+# 사용법: docker-compose -f docker-compose-local.yml --env-file .env.local up --build
+# 참고: 쉘 환경 변수가 .env.local 파일의 값을 오버라이드할 수 있으므로 --env-file 옵션을 명시하는 것이 좋습니다
+
+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 ===
+ # Docker 네트워크 내부 연결 설정 (고정값)
+ DB_IP: mysql # Docker 서비스 이름
+ DB_PORT: 3306 # Docker 네트워크 내부 포트
+ DB_SCHEMA: ${DB_SCHEMA:-devnogi}
+ 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}
+ 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:
+ - app-logs:/app/logs # Named volume 사용 (권한 문제 해결)
+ # 로컬 개발 시 설정 파일 마운트 (선택사항)
+ # - ./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
+ - 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:-90s} # MySQL 초기화 시간 고려하여 증가
+
+ logging:
+ driver: "json-file"
+ options:
+ 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
+ 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}
+
+volumes:
+ mysql_data:
+ app-logs:
+ driver: local
+
+networks:
+ app-network:
+ driver: bridge
+ 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"
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..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,9 +1,14 @@
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;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import until.the.eternity.auctionhistory.application.service.AuctionHistoryService;
@@ -22,24 +27,89 @@ 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()) {
- try {
- List fetchedDtos = fetcher.fetch(category);
- List entities = persister.filterOutExisting(fetchedDtos, category);
- newEntities.addAll(entities);
- } catch (Exception e) {
- log.error(
- "> [SCHEDULE] Error during processing category [{}]",
- category.getSubCategory(),
- e);
+ // ItemCategory를 topCategory별로 그룹화
+ Map> categoriesByTopCategory =
+ Arrays.stream(ItemCategory.values())
+ .collect(
+ Collectors.groupingBy(
+ ItemCategory::getTopCategory,
+ LinkedHashMap::new,
+ Collectors.toList()));
+
+ int totalSavedCount = 0;
+ List topCategories = new ArrayList<>(categoriesByTopCategory.keySet());
+
+ 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;
+ }
}
}
- service.saveAll(newEntities);
+
log.info(
"> [SCHEDULE] AuctionHistoryScheduler saved [{}] new auction history records complete",
- newEntities.size());
+ totalSavedCount);
}
}
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"/>
-
-
+
+
+