diff --git a/kamco-make-dataset-generation/README.md b/kamco-make-dataset-generation/README.md new file mode 100644 index 0000000..f2760d2 --- /dev/null +++ b/kamco-make-dataset-generation/README.md @@ -0,0 +1,687 @@ +# KAMCO Dataset Generation Batch System + +KAMCO 학습 데이터 생성 및 처리를 위한 Spring Batch 시스템입니다. + +## 목차 +- [시스템 개요](#시스템-개요) +- [배치 작업 구조](#배치-작업-구조) +- [데이터베이스 스키마](#데이터베이스-스키마) +- [실행 흐름](#실행-흐름) +- [설정 방법](#설정-방법) +- [모니터링 및 로그](#모니터링-및-로그) +- [트러블슈팅](#트러블슈팅) + +--- + +## 시스템 개요 + +### 주요 기능 +- 검수 완료된 라벨링 데이터를 GeoJSON 형식으로 변환 +- Docker 컨테이너를 통한 학습 데이터 생성 파이프라인 실행 +- 생성된 결과물을 ZIP 파일로 압축 +- 각 처리 단계별 성공/실패 이력을 DB에 자동 기록 + +### 기술 스택 +- **Java 17+** +- **Spring Boot 3.x** +- **Spring Batch 5.x** +- **PostgreSQL** +- **Docker** + +--- + +## 배치 작업 구조 + +### 1. Parent Job: `exportGeoJsonJob` + +Parent Job은 진행 중인 모든 분석 회차를 조회하여 각 회차별로 Child Job을 실행합니다. + +``` +exportGeoJsonJob (Parent Job) +└─ Step: launchChildJobsStep + └─ Tasklet: LaunchChildJobsTasklet + ├─ AnalCntInfo 리스트 조회 + └─ 각 AnalCntInfo마다 Child Job 실행 +``` + +**실행 조건:** +- `tb_map_sheet_anal_inference` 테이블의 `anal_state = 'ING'` (진행 중) +- 검수 완료(`COMPLETE`) 건수가 1개 이상 존재 +- `all_cnt != file_cnt` (아직 파일 생성이 완료되지 않은 경우) + +--- + +### 2. Child Job: `processAnalCntInfoJob` + +각 AnalCntInfo(분석 회차)마다 독립적으로 실행되는 서브 작업입니다. + +``` +processAnalCntInfoJob (Child Job) +├─ Step 1: makeGeoJsonStep +│ └─ Tasklet: MakeGeoJsonTasklet +│ └─ 검수 완료된 라벨링 데이터를 GeoJSON 파일로 생성 +│ → /dataset/request/{resultUid}/*.geojson +│ +├─ Step 2: dockerRunStep +│ └─ Tasklet: DockerRunTasklet +│ └─ Docker 컨테이너 실행 (학습 데이터 생성 파이프라인) +│ → /dataset/response/{resultUid}/* +│ +└─ Step 3: zipResponseStep + └─ Tasklet: ZipResponseTasklet + └─ 생성된 결과물을 ZIP으로 압축 + → /dataset/response/{resultUid}.zip +``` + +**JobParameters:** +- `analUid` (Long): 분석 회차 UID +- `resultUid` (String): 결과물 고유 ID (UUID) +- `timestamp` (Long): 고유성 보장을 위한 타임스탬프 + +--- + +## 데이터베이스 스키마 + +### 1. `batch_history` 테이블 + +전체 배치 작업(Parent Job) 실행 이력을 기록합니다. + +```sql +CREATE TABLE public.batch_history ( + uuid UUID PRIMARY KEY, -- 배치 실행 고유 ID + job VARCHAR(255) NOT NULL, -- 배치 작업 이름 (exportGeoJsonJob) + id VARCHAR(255) NOT NULL, -- 비즈니스 ID + created_dttm TIMESTAMP NOT NULL, -- 생성 일시 + updated_dttm TIMESTAMP NOT NULL, -- 수정 일시 + status VARCHAR(50) NOT NULL, -- 상태 (STARTED/COMPLETED/FAILED) + completed_dttm TIMESTAMP -- 완료 일시 +); +``` + +**인덱스:** +- `idx_batch_history_job` (job) +- `idx_batch_history_status` (status) +- `idx_batch_history_created` (created_dttm DESC) + +--- + +### 2. `batch_step_history` 테이블 + +각 AnalCntInfo의 Step별 실행 이력을 기록합니다. + +```sql +CREATE TABLE public.batch_step_history ( + id BIGSERIAL PRIMARY KEY, -- Step 이력 고유 ID + anal_uid BIGINT NOT NULL, -- 분석 UID + result_uid VARCHAR(255) NOT NULL, -- 결과 UID + step_name VARCHAR(100) NOT NULL, -- Step 이름 + status VARCHAR(50) NOT NULL, -- 상태 (STARTED/SUCCESS/FAILED) + error_message TEXT, -- 에러 메시지 (최대 1000자) + started_dttm TIMESTAMP NOT NULL, -- Step 시작 일시 + completed_dttm TIMESTAMP, -- Step 완료 일시 + created_dttm TIMESTAMP NOT NULL, -- 생성 일시 + updated_dttm TIMESTAMP NOT NULL -- 수정 일시 +); +``` + +**Step 이름:** +- `makeGeoJsonStep`: GeoJSON 파일 생성 +- `dockerRunStep`: Docker 컨테이너 실행 +- `zipResponseStep`: 결과물 ZIP 압축 + +**인덱스:** +- `idx_batch_step_history_anal_uid` (anal_uid) +- `idx_batch_step_history_result_uid` (result_uid) +- `idx_batch_step_history_status` (status) +- `idx_batch_step_history_step_name` (step_name) + +--- + +## 실행 흐름 + +### 전체 프로세스 + +``` +1. Parent Job 시작 + ↓ +2. 진행 중인 AnalCntInfo 리스트 조회 + ↓ +3. 각 AnalCntInfo마다 반복: + ↓ + ├─ 3.1. Child Job 실행 (processAnalCntInfoJob) + │ ↓ + │ ├─ Step 1: makeGeoJsonStep + │ │ - beforeStep: DB에 STARTED 기록 + │ │ - Tasklet 실행: GeoJSON 파일 생성 + │ │ - afterStep: DB에 SUCCESS/FAILED 기록 + │ │ + │ ├─ Step 2: dockerRunStep + │ │ - beforeStep: DB에 STARTED 기록 + │ │ - Tasklet 실행: Docker 컨테이너 실행 + │ │ - afterStep: DB에 SUCCESS/FAILED 기록 + │ │ + │ └─ Step 3: zipResponseStep + │ - beforeStep: DB에 STARTED 기록 + │ - Tasklet 실행: 결과물 ZIP 압축 + │ - afterStep: DB에 SUCCESS/FAILED 기록 + │ + └─ 3.2. 다음 AnalCntInfo 처리 + ↓ +4. Parent Job 종료 (부분 성공 허용) +``` + +--- + +### Step 1: makeGeoJsonStep + +**목적:** 검수 완료된 라벨링 데이터를 GeoJSON 파일로 변환 + +**처리 과정:** +1. `findCompletedAnalMapSheetList()`: 검수 완료된 도엽 목록 조회 +2. 각 도엽별로: + - `findCompletedYesterdayLabelingList()`: 어제까지 검수 완료된 데이터 조회 + - GeoJSON Feature 생성 + - `/dataset/request/{resultUid}/{filename}.geojson` 저장 + - `updateLearnDataGeomFileCreateYn()`: DB에 파일 생성 완료 플래그 업데이트 + +**출력 파일 형식:** +``` +{resultUid_8자}_{compareYyyy}_{targetYyyy}_{mapSheetNum}_D15.geojson +``` + +**예시:** +``` +ED80D700_2022_2023_3724036_D15.geojson +``` + +--- + +### Step 2: dockerRunStep + +**목적:** Docker 컨테이너를 통해 학습 데이터 생성 파이프라인 실행 + +**Docker 명령어:** +```bash +docker run --rm \ + --user {dockerUser} \ + -v {datasetVolume} \ + -v {imagesVolume} \ + --entrypoint python \ + {dockerImage} \ + code/kamco_full_pipeline.py \ + --labelling-folder request/{resultUid} \ + --output-folder response/{resultUid} \ + --input_root {inputRoot} \ + --output_root {outputRoot} \ + --patch_size {patchSize} \ + --overlap_pct {overlapPct} \ + --train_val_test_ratio {train} {val} {test} \ + --keep_empty_ratio {keepEmptyRatio} +``` + +**에러 처리:** +- Docker 프로세스의 `exitCode != 0` 시 `RuntimeException` 발생 +- Step 실패로 처리되어 DB에 `FAILED` 상태 기록 +- 에러 메시지와 exit code가 `error_message` 컬럼에 저장됨 + +--- + +### Step 3: zipResponseStep + +**목적:** 생성된 학습 데이터를 ZIP 파일로 압축 + +**처리 과정:** +1. `/dataset/response/{resultUid}/` 디렉토리 검증 +2. 디렉토리 내 모든 파일과 서브디렉토리를 재귀적으로 압축 +3. `/dataset/response/{resultUid}.zip` 파일 생성 + +**압축 설정:** +- Hidden 파일 제외 +- 디렉토리 구조 유지 +- 버퍼 크기: 1024 bytes + +--- + +## 설정 방법 + +### application.yml 설정 + +```yaml +# 학습 데이터 디렉토리 경로 +training-data: + geojson-dir: /kamco-nfs/dataset + +# Docker 설정 +docker: + user: "1000:1000" + image: "kamco/dataset-generator:latest" + dataset-volume: "/kamco-nfs/dataset:/dataset" + images-volume: "/kamco-nfs/images:/images" + input-root: "/dataset" + output-root: "/dataset" + patch-size: 512 + overlap-pct: 0.2 + train-val-test-ratio: + - "0.7" + - "0.2" + - "0.1" + keep-empty-ratio: 0.5 +``` + +--- + +### 환경 변수 + +| 환경 변수 | 설명 | 기본값 | +|----------|------|--------| +| `TRAINING_DATA_GEOJSON_DIR` | GeoJSON 파일 저장 경로 | `/kamco-nfs/dataset` | +| `DOCKER_USER` | Docker 컨테이너 실행 유저 | `1000:1000` | +| `DOCKER_IMAGE` | Docker 이미지 이름 | `kamco/dataset-generator:latest` | + +--- + +## 모니터링 및 로그 + +### 로그 레벨 + +```yaml +logging: + level: + com.kamco.cd.geojsonscheduler: INFO + com.kamco.cd.geojsonscheduler.batch: DEBUG + org.springframework.batch: INFO +``` + +--- + +### 주요 로그 포인트 + +#### Parent Job 로그 +```log +[INFO] Parent Job 시작: AnalCntInfo 리스트 조회 및 Child Job 실행 +[INFO] 진행중인 회차 목록 조회 중... +[INFO] 진행중인 회차 수: 3 +[INFO] 회차 검토: AnalUid=100, ResultUid=ED80D700... +[INFO] Child Job 실행 중... (AnalUid=100, ResultUid=ED80D700...) +[INFO] Child Job 실행 완료 (AnalUid=100, ResultUid=ED80D700...) +[INFO] Parent Job 완료 - 성공: 2, 건너뜀: 1, 실패: 0 +``` + +#### Step 로그 +```log +[INFO] ======================================== +[INFO] GeoJSON 생성 시작 (AnalUid=100, ResultUid=ED80D700...) +[INFO] 검수 완료된 도엽 수: 5 +[INFO] 도엽 처리 중: MapSheetNum=3724036 +[INFO] 완료된 라벨링 데이터 수: 150 +[INFO] GeoJSON 파일 저장 완료: /dataset/request/ED80D700.../ED80D700_2022_2023_3724036_D15.geojson +[INFO] GeoJSON 생성 완료 (ResultUid=ED80D700...) - 처리된 도엽 수: 5, 생성된 파일 수: 5 +[INFO] ======================================== +``` + +#### Docker 실행 로그 +```log +[INFO] ======================================== +[INFO] Docker 컨테이너 실행 시작 (ResultUid=ED80D700...) +[INFO] Running docker command: docker run --rm --user 1000:1000... +[INFO] [docker] Loading configuration... +[INFO] [docker] Processing pipeline... +[INFO] [docker] Pipeline completed successfully +[INFO] Docker process completed successfully for resultUid: ED80D700... +[INFO] ======================================== +``` + +--- + +### DB 조회 쿼리 + +#### 1. 특정 회차의 모든 Step 실행 이력 +```sql +SELECT + step_name, + status, + started_dttm, + completed_dttm, + EXTRACT(EPOCH FROM (completed_dttm - started_dttm)) AS duration_seconds, + error_message +FROM batch_step_history +WHERE anal_uid = 100 + AND result_uid = 'ED80D700A0F5482BB0EC11A366DEA8DE' +ORDER BY started_dttm; +``` + +#### 2. 최근 실패한 Step 조회 +```sql +SELECT + anal_uid, + result_uid, + step_name, + error_message, + started_dttm, + completed_dttm +FROM batch_step_history +WHERE status = 'FAILED' +ORDER BY started_dttm DESC +LIMIT 10; +``` + +#### 3. Step별 성공률 통계 +```sql +SELECT + step_name, + COUNT(*) AS total_executions, + SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS success_count, + SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END) AS failed_count, + ROUND( + SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END)::NUMERIC / COUNT(*) * 100, + 2 + ) AS success_rate_pct +FROM batch_step_history +GROUP BY step_name; +``` + +#### 4. 평균 실행 시간 (Step별) +```sql +SELECT + step_name, + COUNT(*) AS total_executions, + AVG(EXTRACT(EPOCH FROM (completed_dttm - started_dttm))) AS avg_duration_seconds, + MIN(EXTRACT(EPOCH FROM (completed_dttm - started_dttm))) AS min_duration_seconds, + MAX(EXTRACT(EPOCH FROM (completed_dttm - started_dttm))) AS max_duration_seconds +FROM batch_step_history +WHERE status = 'SUCCESS' + AND completed_dttm IS NOT NULL +GROUP BY step_name; +``` + +#### 5. 특정 기간 동안 처리된 회차 수 +```sql +SELECT + DATE(started_dttm) AS execution_date, + COUNT(DISTINCT result_uid) AS processed_count +FROM batch_step_history +WHERE step_name = 'makeGeoJsonStep' + AND started_dttm >= CURRENT_DATE - INTERVAL '7 days' +GROUP BY DATE(started_dttm) +ORDER BY execution_date DESC; +``` + +--- + +## 트러블슈팅 + +### 1. Docker 컨테이너 실행 실패 + +**증상:** +```log +[ERROR] Docker process exited with code 1 for resultUid: ED80D700... +FileNotFoundError: Missing training pairs root at /dataset/response/.../tifs/train +``` + +**원인:** +- GeoJSON 파일이 생성되지 않았거나 잘못된 형식 +- Docker 볼륨 마운트 경로 불일치 +- 파이프라인 실행 중 필수 파일 누락 + +**해결 방법:** +1. `batch_step_history` 테이블에서 `makeGeoJsonStep` 상태 확인 + ```sql + SELECT * FROM batch_step_history + WHERE result_uid = 'ED80D700...' + AND step_name = 'makeGeoJsonStep'; + ``` + +2. GeoJSON 파일 존재 여부 확인 + ```bash + ls -la /kamco-nfs/dataset/request/ED80D700.../ + ``` + +3. Docker 볼륨 설정 확인 + ```yaml + docker: + dataset-volume: "/kamco-nfs/dataset:/dataset" # 호스트:컨테이너 + ``` + +--- + +### 2. GeoJSON 파일이 생성되지 않음 + +**증상:** +```log +[WARN] 검수 완료된 도엽이 없음. 작업 건너뜀. +``` + +**원인:** +- 검수 완료(`COMPLETE`) 상태의 데이터가 없음 +- `inspect_stat_dttm`이 오늘 이후 (어제까지만 조회) + +**해결 방법:** +1. 검수 완료 데이터 확인 + ```sql + SELECT COUNT(*) + FROM tb_labeling_assignment + WHERE anal_uid = 100 + AND inspect_state = 'COMPLETE'; + ``` + +2. 검수 완료 시간 확인 + ```sql + SELECT MAX(inspect_stat_dttm) + FROM tb_labeling_assignment + WHERE anal_uid = 100 + AND inspect_state = 'COMPLETE'; + ``` + +--- + +### 3. ZIP 파일 생성 실패 + +**증상:** +```log +[ERROR] Response 디렉토리가 존재하지 않음: /dataset/response/ED80D700... +``` + +**원인:** +- Docker Step에서 결과물이 생성되지 않음 +- Docker 컨테이너가 중간에 실패했지만 감지되지 않음 + +**해결 방법:** +1. Docker Step 상태 확인 + ```sql + SELECT * FROM batch_step_history + WHERE result_uid = 'ED80D700...' + AND step_name = 'dockerRunStep'; + ``` + +2. Response 디렉토리 확인 + ```bash + ls -la /kamco-nfs/dataset/response/ED80D700.../ + ``` + +3. Docker 컨테이너 로그 확인 (DB의 `error_message` 컬럼) + +--- + +### 4. Child Job이 실행되지 않음 + +**증상:** +```log +[INFO] 모든 파일이 이미 처리됨. 건너뜀. +``` + +**원인:** +- `all_cnt == file_cnt` (이미 모든 파일이 생성됨) +- 재실행이 필요한 경우 플래그를 초기화하지 않음 + +**해결 방법:** +1. 파일 생성 플래그 초기화 + ```sql + UPDATE tb_map_sheet_learn_data_geom + SET file_create_yn = false, + updated_dttm = NOW() + WHERE geo_uid IN ( + SELECT inference_geom_uid + FROM tb_labeling_assignment + WHERE anal_uid = 100 + ); + ``` + +2. 배치 재실행 + +--- + +### 5. 배치 작업 전체가 실패함 + +**증상:** +```log +[ERROR] Child Job 실행 실패 (AnalUid=100, ResultUid=ED80D700...): ... +[WARN] 3 개의 Child Job 실행이 실패했습니다. +``` + +**원인:** +- 여러 회차에서 동시에 실패 발생 +- 현재는 부분 실패를 허용하도록 설정됨 + +**해결 방법:** +1. 실패한 회차 확인 + ```sql + SELECT DISTINCT anal_uid, result_uid + FROM batch_step_history + WHERE status = 'FAILED' + AND started_dttm >= CURRENT_DATE; + ``` + +2. 각 회차별로 실패 원인 분석 + ```sql + SELECT step_name, error_message + FROM batch_step_history + WHERE anal_uid = 100 + AND status = 'FAILED'; + ``` + +3. 실패 정책 변경 (필요 시) + - `LaunchChildJobsTasklet.java:87-89` 주석 해제하여 하나라도 실패 시 Parent Job 실패 처리 + +--- + +## 개발자 가이드 + +### 새로운 Step 추가하기 + +1. **Tasklet 생성** + ```java + @Component + @RequiredArgsConstructor + public class NewStepTasklet implements Tasklet { + @Value("#{jobParameters['analUid']}") + private Long analUid; + + @Value("#{jobParameters['resultUid']}") + private String resultUid; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + // 로직 구현 + return RepeatStatus.FINISHED; + } + } + ``` + +2. **JobConfig에 Step 등록** + ```java + @Bean + public Step newStep() { + return new StepBuilder("newStep", jobRepository) + .tasklet(newStepTasklet, transactionManager) + .listener(stepHistoryListener) // 이력 기록 + .build(); + } + ``` + +3. **Job Flow에 추가** + ```java + @Bean + public Job processAnalCntInfoJob() { + return new JobBuilder("processAnalCntInfoJob", jobRepository) + .start(makeGeoJsonStep()) + .next(dockerRunStep()) + .next(zipResponseStep()) + .next(newStep()) // 새로운 Step 추가 + .build(); + } + ``` + +--- + +### 로그 커스터마이징 + +**로그 레벨 변경:** +```yaml +logging: + level: + com.kamco.cd.geojsonscheduler.batch.DockerRunTasklet: DEBUG +``` + +**특정 Step만 로그 출력:** +```java +@Slf4j +public class CustomTasklet implements Tasklet { + @Override + public RepeatStatus execute(...) { + if (log.isDebugEnabled()) { + log.debug("상세 디버그 정보: {}", details); + } + return RepeatStatus.FINISHED; + } +} +``` + +--- + +## 배포 및 운영 + +### 배포 절차 + +1. **빌드** + ```bash + ./gradlew clean build + ``` + +2. **Docker 이미지 빌드** + ```bash + docker build -t kamco-batch:latest . + ``` + +3. **실행** + ```bash + java -jar build/libs/kamco-geojson-scheduler-1.0.0.jar + ``` + +--- + +### 스케줄링 설정 + +Spring Scheduler를 사용한 정기 실행: + +```java +@Scheduled(cron = "0 0 2 * * *") // 매일 새벽 2시 +public void runBatch() { + JobParameters jobParameters = new JobParametersBuilder() + .addLong("timestamp", System.currentTimeMillis()) + .toJobParameters(); + + jobLauncher.run(exportGeoJsonJob, jobParameters); +} +``` + +--- + +## 라이선스 + +Copyright (c) 2024 KAMCO. All rights reserved. + +--- + +## 문의 + +기술 지원: tech-support@kamco.co.kr diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/DockerRunTasklet.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/DockerRunTasklet.java index 4b8b1be..b87d481 100644 --- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/DockerRunTasklet.java +++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/DockerRunTasklet.java @@ -10,26 +10,97 @@ import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +/** + * Docker 컨테이너 실행 Tasklet + * + *
학습 데이터 생성 파이프라인이 담긴 Docker 컨테이너를 실행합니다. 이전 Step(makeGeoJsonStep)에서 생성된 GeoJSON + * 파일들을 입력으로 받아 학습 데이터셋을 생성합니다. + * + *
주요 기능: + * + *
실행 조건: + * + *
Docker Exit Code 처리: + * + *
DockerRunnerService를 통해 학습 데이터 생성 파이프라인을 실행합니다. Docker 프로세스가 비정상 종료될 경우 + * RuntimeException이 발생하여 Step이 실패 처리됩니다. + * + * @param contribution Step 실행 정보를 담는 객체 + * @param chunkContext Chunk 실행 컨텍스트 + * @return RepeatStatus.FINISHED - 작업 완료 + * @throws RuntimeException Docker 프로세스가 비정상 종료(exitCode != 0)된 경우 + */ @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { log.info("========================================"); log.info("Docker 컨테이너 실행 시작 (ResultUid={})", resultUid); log.info("========================================"); - dockerRunnerService.run(resultUid); + // 실행 전 사전 정보 로깅 + log.info("[사전 정보]"); + log.info(" - 입력 디렉토리: /dataset/request/{}/", resultUid); + log.info(" - 출력 디렉토리: /dataset/response/{}/", resultUid); + log.info(" - 파이프라인: kamco_full_pipeline.py"); + + try { + // DockerRunnerService를 통해 Docker 컨테이너 실행 + // exitCode != 0 시 RuntimeException 발생 + log.info("[Docker 실행] 컨테이너 시작..."); + dockerRunnerService.run(resultUid); + log.info("[Docker 실행] ✓ 컨테이너 정상 종료 (exitCode=0)"); + + } catch (RuntimeException e) { + // Docker 실행 실패 시 (DockerRunnerService에서 던진 예외) + log.error("[Docker 실행] ✗ 컨테이너 실행 실패!", e); + log.error(" - ResultUid: {}", resultUid); + log.error(" - 에러 메시지: {}", e.getMessage()); + log.error(" - 확인사항:"); + log.error(" 1. request/{}/ 디렉토리에 GeoJSON 파일이 있는지 확인", resultUid); + log.error(" 2. Docker 이미지가 정상적으로 존재하는지 확인"); + log.error(" 3. Docker 볼륨 마운트 경로 확인"); + log.error(" 4. kamco_full_pipeline.py 스크립트 로그 확인"); + throw e; // 예외 재발생 → Step 실패 처리 + } log.info("========================================"); log.info("Docker 컨테이너 실행 완료 (ResultUid={})", resultUid); + log.info(" - 결과물 저장 위치: /dataset/response/{}/", resultUid); log.info("========================================"); return RepeatStatus.FINISHED; diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/LaunchChildJobsTasklet.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/LaunchChildJobsTasklet.java index e73123a..d99e482 100644 --- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/LaunchChildJobsTasklet.java +++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/LaunchChildJobsTasklet.java @@ -17,76 +17,191 @@ import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; +/** + * Child Job 실행 Tasklet (Parent Job용) + * + *
진행 중인 모든 분석 회차(AnalCntInfo)를 조회하여 각 회차마다 독립적인 Child Job을 실행합니다. 각 Child Job은 3개의 + * Step(makeGeoJson → dockerRun → zipResponse)을 순차적으로 실행합니다. + * + *
주요 기능: + * + *
실행 조건: + * + *
실패 정책: + * + *
진행 중인 모든 분석 회차를 조회하여 각 회차마다 Child Job을 실행합니다. 한 회차가 실패해도 다른 회차는 계속 처리되며, 최종적으로 통계를
+ * 로깅합니다.
+ *
+ * @param contribution Step 실행 정보를 담는 객체
+ * @param chunkContext Chunk 실행 컨텍스트
+ * @return RepeatStatus.FINISHED - 작업 완료
+ */
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("Parent Job 시작: AnalCntInfo 리스트 조회 및 Child Job 실행");
log.info("========================================");
- // 진행중인 회차 중, complete_cnt 가 존재하는 회차 목록 가져오기
- log.info("진행중인 회차 목록 조회 중...");
+ // Step 1: 진행 중인 분석 회차 목록 조회
+ log.info("[Step 1/3] 진행 중인 분석 회차 목록 조회 중...");
+ log.info(" - 조회 조건: anal_state='ING' AND complete_cnt > 0");
+
List 검수 완료된 라벨링 데이터를 GeoJSON 형식으로 변환하여 파일로 저장합니다. 각 도엽(Map Sheet)별로 별도의 GeoJSON 파일을
+ * 생성하며, 파일 생성 완료 후 DB에 플래그를 업데이트합니다.
+ *
+ * 주요 기능:
+ *
+ * 파일 명명 규칙: {resultUid_8자}_{compareYyyy}_{targetYyyy}_{mapSheetNum}_D15.geojson
+ *
+ * 예시: ED80D700_2022_2023_3724036_D15.geojson
+ *
+ * @author KAMCO Development Team
+ * @since 1.0.0
+ */
@Log4j2
@Component
@RequiredArgsConstructor
public class MakeGeoJsonTasklet implements Tasklet {
+ /** 라벨링 데이터 조회를 위한 Repository */
private final TrainingDataReviewJobRepository repository;
+ /** GeoJSON 파일이 저장될 베이스 디렉토리 경로 (예: /kamco-nfs/dataset) */
@Value("${training-data.geojson-dir}")
private String trainingDataDir;
+ /** Job Parameter로 전달받은 분석 회차 UID */
@Value("#{jobParameters['analUid']}")
private Long analUid;
+ /** Job Parameter로 전달받은 결과물 고유 ID (UUID) */
@Value("#{jobParameters['resultUid']}")
private String resultUid;
+ /**
+ * GeoJSON 파일 생성 작업 실행
+ *
+ * 검수 완료된 라벨링 데이터를 조회하여 도엽별로 GeoJSON 파일을 생성합니다.
+ *
+ * @param contribution Step 실행 정보를 담는 객체
+ * @param chunkContext Chunk 실행 컨텍스트
+ * @return RepeatStatus.FINISHED - 작업 완료
+ * @throws RuntimeException 생성된 GeoJSON 파일이 없는 경우 또는 파일 생성 실패 시
+ */
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("GeoJSON 생성 시작 (AnalUid={}, ResultUid={})", analUid, resultUid);
log.info("========================================");
- // 검수 완료된 도엽 목록 조회
- log.info("검수 완료된 도엽 목록 조회 중... (AnalUid={})", analUid);
+ // Step 1: 검수 완료된 도엽 목록 조회
+ log.info("[Step 1/4] 검수 완료된 도엽 목록 조회 중... (AnalUid={})", analUid);
List Docker 컨테이너 실행으로 생성된 학습 데이터 결과물을 ZIP 파일로 압축합니다. 압축된 파일은 다운로드 또는 배포를 위해
+ * 사용됩니다.
+ *
+ * 주요 기능:
+ *
+ * 압축 설정:
+ *
+ * 실행 조건:
+ *
+ * response/{resultUid}/ 디렉토리를 검증하고 ZIP 파일로 압축합니다.
+ *
+ * @param contribution Step 실행 정보를 담는 객체
+ * @param chunkContext Chunk 실행 컨텍스트
+ * @return RepeatStatus.FINISHED - 작업 완료
+ * @throws RuntimeException response 디렉토리가 존재하지 않거나 압축 실패 시
+ */
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("========================================");
log.info("결과물 압축 시작 (ResultUid={})", resultUid);
log.info("========================================");
+ // Step 1: 압축 대상 디렉토리 및 출력 파일 경로 설정
+ log.info("[Step 1/5] 경로 설정 중...");
Path responseDir =
Paths.get(trainingDataDir + File.separator + "response" + File.separator + resultUid);
Path zipFile =
Paths.get(
trainingDataDir + File.separator + "response" + File.separator + resultUid + ".zip");
- log.info("압축 대상 디렉토리: {}", responseDir);
- log.info("압축 파일 경로: {}", zipFile);
+ log.info("[Step 1/5] 경로 설정 완료");
+ log.info(" - 압축 대상 디렉토리: {}", responseDir);
+ log.info(" - 압축 파일 저장 경로: {}", zipFile);
+ // Step 2: response 디렉토리 존재 여부 검증
+ log.info("[Step 2/5] Response 디렉토리 검증 중...");
if (!Files.exists(responseDir)) {
- log.error("Response 디렉토리가 존재하지 않음: {}", responseDir);
+ log.error("[실패] Response 디렉토리가 존재하지 않습니다!");
+ log.error(" - 경로: {}", responseDir);
+ log.error(" - ResultUid: {}", resultUid);
+ log.error(" - 확인사항:");
+ log.error(" 1. dockerRunStep이 정상적으로 완료되었는지 확인");
+ log.error(" 2. Docker 컨테이너가 결과물을 생성했는지 확인");
+ log.error(" 3. Docker 볼륨 마운트 경로가 올바른지 확인");
throw new RuntimeException("Response 디렉토리가 존재하지 않습니다: " + responseDir);
}
+ log.info("[Step 2/5] ✓ Response 디렉토리 존재 확인");
+ // Step 3: 디렉토리 내용 확인 (파일 수 카운트)
+ log.info("[Step 3/5] 디렉토리 내용 분석 중...");
+ File responseDirFile = responseDir.toFile();
+ long fileCount = countFilesRecursively(responseDirFile);
+ log.info("[Step 3/5] 디렉토리 분석 완료");
+ log.info(" - 총 파일 수: {} 개", fileCount);
+
+ if (fileCount == 0) {
+ log.warn("[경고] 압축할 파일이 없습니다. (디렉토리가 비어있음)");
+ log.warn(" - 디렉토리: {}", responseDir);
+ }
+
+ // Step 4: ZIP 압축 실행
+ log.info("[Step 4/5] ZIP 압축 시작...");
try {
- zipDirectory(responseDir.toFile(), zipFile.toFile());
- log.info("압축 완료: {} (크기: {} bytes)", zipFile, Files.size(zipFile));
+ long startTime = System.currentTimeMillis();
+
+ zipDirectory(responseDirFile, zipFile.toFile());
+
+ long endTime = System.currentTimeMillis();
+ long duration = endTime - startTime;
+ long zipSize = Files.size(zipFile);
+ double zipSizeMB = zipSize / (1024.0 * 1024.0);
+
+ log.info("[Step 4/5] ✓ ZIP 압축 완료");
+ log.info(" - 압축 파일: {}", zipFile);
+ log.info(" - 압축 파일 크기: {} bytes ({} MB)", zipSize, String.format("%.2f", zipSizeMB));
+ log.info(" - 압축 소요 시간: {} ms", duration);
+
} catch (IOException e) {
- log.error("압축 실패: {}", e.getMessage(), e);
+ log.error("[실패] ZIP 압축 중 오류 발생!", e);
+ log.error(" - 소스 디렉토리: {}", responseDir);
+ log.error(" - 대상 ZIP 파일: {}", zipFile);
+ log.error(" - 에러 메시지: {}", e.getMessage());
throw new RuntimeException("Response 디렉토리 압축 실패: " + responseDir, e);
}
+ // Step 5: 최종 검증 및 완료
+ log.info("[Step 5/5] 최종 검증 중...");
+ if (Files.exists(zipFile)) {
+ log.info("[Step 5/5] ✓ ZIP 파일 생성 확인 완료");
+ } else {
+ log.error("[실패] ZIP 파일이 생성되지 않았습니다!");
+ throw new RuntimeException("ZIP 파일 생성 실패: " + zipFile);
+ }
+
log.info("========================================");
log.info("결과물 압축 완료 (ResultUid={})", resultUid);
+ log.info(" - ZIP 파일 위치: {}", zipFile);
log.info("========================================");
return RepeatStatus.FINISHED;
}
+ /**
+ * 디렉토리를 ZIP 파일로 압축
+ *
+ * @param sourceDir 압축할 소스 디렉토리
+ * @param zipFile 생성될 ZIP 파일
+ * @throws IOException 압축 중 IO 오류 발생 시
+ */
private void zipDirectory(File sourceDir, File zipFile) throws IOException {
+ log.debug("ZIP 압축 시작: {} -> {}", sourceDir.getAbsolutePath(), zipFile.getAbsolutePath());
+
try (FileOutputStream fos = new FileOutputStream(zipFile);
ZipOutputStream zos = new ZipOutputStream(fos)) {
zipDirectoryRecursive(sourceDir, sourceDir.getName(), zos);
}
+
+ log.debug("ZIP 압축 완료: {}", zipFile.getAbsolutePath());
}
+ /**
+ * 디렉토리를 재귀적으로 ZIP으로 압축
+ *
+ * 서브디렉토리를 포함한 모든 파일과 디렉토리를 ZIP에 추가합니다. Hidden 파일은 제외됩니다.
+ *
+ * @param fileToZip 압축할 파일 또는 디렉토리
+ * @param fileName ZIP 엔트리 이름
+ * @param zos ZipOutputStream
+ * @throws IOException 압축 중 IO 오류 발생 시
+ */
private void zipDirectoryRecursive(File fileToZip, String fileName, ZipOutputStream zos)
throws IOException {
+
+ // Hidden 파일은 제외
if (fileToZip.isHidden()) {
+ log.debug("Hidden 파일 제외: {}", fileToZip.getName());
return;
}
+ // 디렉토리인 경우
if (fileToZip.isDirectory()) {
+ log.debug("디렉토리 추가: {}", fileName);
+
+ // 디렉토리 엔트리 생성
if (fileName.endsWith("/")) {
zos.putNextEntry(new ZipEntry(fileName));
zos.closeEntry();
@@ -86,6 +210,7 @@ public class ZipResponseTasklet implements Tasklet {
zos.closeEntry();
}
+ // 하위 파일 및 디렉토리 재귀 처리
File[] children = fileToZip.listFiles();
if (children != null) {
for (File childFile : children) {
@@ -95,9 +220,13 @@ public class ZipResponseTasklet implements Tasklet {
return;
}
+ // 파일인 경우: 파일 내용을 ZIP에 추가
+ log.debug("파일 추가: {}", fileName);
try (FileInputStream fis = new FileInputStream(fileToZip)) {
ZipEntry zipEntry = new ZipEntry(fileName);
zos.putNextEntry(zipEntry);
+
+ // 파일 내용을 버퍼를 통해 복사
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) >= 0) {
@@ -105,4 +234,31 @@ public class ZipResponseTasklet implements Tasklet {
}
}
}
+
+ /**
+ * 디렉토리 내 파일 수를 재귀적으로 카운트
+ *
+ * @param directory 카운트할 디렉토리
+ * @return 디렉토리 내 총 파일 수 (서브디렉토리 포함)
+ */
+ private long countFilesRecursively(File directory) {
+ if (!directory.isDirectory()) {
+ return directory.isFile() ? 1 : 0;
+ }
+
+ File[] children = directory.listFiles();
+ if (children == null) {
+ return 0;
+ }
+
+ long count = 0;
+ for (File child : children) {
+ if (child.isFile()) {
+ count++;
+ } else if (child.isDirectory()) {
+ count += countFilesRecursively(child);
+ }
+ }
+ return count;
+ }
}
diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/StepHistoryListener.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/StepHistoryListener.java
index 14a34c7..21381b8 100644
--- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/StepHistoryListener.java
+++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/StepHistoryListener.java
@@ -8,11 +8,46 @@ import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.stereotype.Component;
+/**
+ * Step 실행 이력 Listener
+ *
+ * 각 Step의 시작과 종료 시점에 실행되어 batch_step_history 테이블에 실행 이력을 기록합니다. Step 시작 시 STARTED
+ * 상태로 INSERT하고, Step 종료 시 SUCCESS 또는 FAILED 상태로 UPDATE합니다.
+ *
+ * 주요 기능:
+ *
+ * 적용 대상:
+ *
+ * 필수 JobParameters:
+ *
+ * 각 AnalCntInfo의 Step별 실행 이력을 batch_step_history 테이블에 저장하고 조회합니다. Step 시작 시 STARTED
+ * 상태로 INSERT하고, Step 종료 시 SUCCESS 또는 FAILED 상태로 UPDATE합니다.
+ *
+ * 주요 기능:
+ *
+ * 테이블 구조:
+ *
+ * 학습 데이터 생성 파이프라인이 포함된 Docker 컨테이너를 실행하고 결과를 모니터링합니다. Docker 프로세스의 표준 출력을 실시간으로
+ * 로깅하며, 비정상 종료 시 RuntimeException을 발생시켜 Batch Step을 실패 처리합니다.
+ *
+ * 주요 기능:
+ *
+ * Docker 명령어 구조:
+ *
+ * 학습 데이터 생성 파이프라인을 Docker 컨테이너로 실행합니다. 컨테이너의 표준 출력을 실시간으로 로깅하며, 비정상 종료 시
+ * RuntimeException을 발생시켜 Step 실패로 처리합니다.
+ *
+ * @param resultUid 결과물 고유 ID (UUID)
+ * @throws RuntimeException Docker 프로세스 실패 (exitCode != 0), IO 오류, 또는 인터럽트 발생 시
+ */
public void run(String resultUid) {
+ // Step 1: Docker 명령어 생성
+ log.info("[Step 1/4] Docker 명령어 생성 중...");
List DockerProperties 설정 정보와 resultUid를 기반으로 Docker run 명령어를 생성합니다.
+ *
+ * @param resultUid 결과물 고유 ID (입력/출력 폴더 경로에 사용)
+ * @return Docker 명령어 문자열 리스트 (ProcessBuilder 실행용)
+ */
+ private List
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author KAMCO Development Team
+ * @since 1.0.0
+ */
@Log4j2
@Component
@RequiredArgsConstructor
public class ZipResponseTasklet implements Tasklet {
+ /** 학습 데이터 저장 베이스 디렉토리 경로 (예: /kamco-nfs/dataset) */
@Value("${training-data.geojson-dir}")
private String trainingDataDir;
+ /** Job Parameter로 전달받은 결과물 고유 ID (UUID) */
@Value("#{jobParameters['resultUid']}")
private String resultUid;
+ /**
+ * 결과물 압축 작업 실행
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author KAMCO Development Team
+ * @since 1.0.0
+ * @see BatchStepHistoryRepository
+ */
@Log4j2
@Component
@RequiredArgsConstructor
public class StepHistoryListener implements StepExecutionListener {
+ /** Step 이력 DB 저장을 위한 Repository */
private final BatchStepHistoryRepository batchStepHistoryRepository;
@Override
diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/BatchStepHistoryRepository.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/BatchStepHistoryRepository.java
index fbaf37f..f05a913 100644
--- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/BatchStepHistoryRepository.java
+++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/BatchStepHistoryRepository.java
@@ -8,11 +8,42 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
+/**
+ * Batch Step 실행 이력 Repository
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author KAMCO Development Team
+ * @since 1.0.0
+ */
@Log4j2
@Repository
@RequiredArgsConstructor
public class BatchStepHistoryRepository {
+ /** JDBC 쿼리 실행을 위한 Template */
private final JdbcTemplate jdbcTemplate;
/**
diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/TrainingDataReviewJobRepository.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/TrainingDataReviewJobRepository.java
index b14bdb1..6fecf2e 100644
--- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/TrainingDataReviewJobRepository.java
+++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/repository/TrainingDataReviewJobRepository.java
@@ -84,7 +84,7 @@ public class TrainingDataReviewJobRepository {
SELECT
mslg.geo_uid,
'Feature' AS type,
- ST_AsGeoJSON(mslg.geom) AS geom_str,
+ ST_AsGeoJSON(ST_Transform(mslg.geom, 4326)) AS geom_str,
CASE
WHEN mslg.class_after_cd IN ('building', 'container') THEN 'M1'
WHEN mslg.class_after_cd = 'waste' THEN 'M2'
diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/service/DockerRunnerService.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/service/DockerRunnerService.java
index f2c9da6..3d2f8b0 100644
--- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/service/DockerRunnerService.java
+++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/service/DockerRunnerService.java
@@ -10,79 +10,206 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
+/**
+ * Docker 컨테이너 실행 서비스
+ *
+ *
+ *
+ *
+ *
+ * docker run --rm
+ * --user {dockerUser}
+ * -v {datasetVolume}
+ * -v {imagesVolume}
+ * --entrypoint python
+ * {dockerImage}
+ * code/kamco_full_pipeline.py
+ * --labelling-folder request/{resultUid}
+ * --output-folder response/{resultUid}
+ * --input_root {inputRoot}
+ * --output_root {outputRoot}
+ * --patch_size {patchSize}
+ * --overlap_pct {overlapPct}
+ * --train_val_test_ratio {train} {val} {test}
+ * --keep_empty_ratio {keepEmptyRatio}
+ *
+ *
+ * @author KAMCO Development Team
+ * @since 1.0.0
+ * @see DockerProperties
+ */
@Log4j2
@Service
@RequiredArgsConstructor
public class DockerRunnerService {
+ /** Docker 실행 관련 설정 정보 (application.yml) */
private final DockerProperties dockerProperties;
+ /**
+ * Docker 컨테이너 실행 및 모니터링
+ *
+ *