diff --git a/kamco-make-dataset-generation/build/libs/generator-dataset-for-training.jar b/kamco-make-dataset-generation/build/libs/generator-dataset-for-training.jar index c878dbb..beef3c2 100644 Binary files a/kamco-make-dataset-generation/build/libs/generator-dataset-for-training.jar and b/kamco-make-dataset-generation/build/libs/generator-dataset-for-training.jar differ diff --git a/kamco-make-dataset-generation/claudedocs/DATABASE_SETUP.md b/kamco-make-dataset-generation/claudedocs/DATABASE_SETUP.md new file mode 100644 index 0000000..9f1c0b3 --- /dev/null +++ b/kamco-make-dataset-generation/claudedocs/DATABASE_SETUP.md @@ -0,0 +1,257 @@ +# 데이터베이스 설정 가이드 + +## 개요 +이 애플리케이션은 PostgreSQL 데이터베이스를 사용하며, 다음 테이블이 필요합니다: +1. Spring Batch 메타데이터 테이블 (자동 생성) +2. batch_history 테이블 (수동 생성 필요) + +## 필수 테이블 + +### 1. Spring Batch 메타데이터 테이블 +Spring Batch가 자동으로 생성합니다: +- BATCH_JOB_INSTANCE +- BATCH_JOB_EXECUTION +- BATCH_JOB_EXECUTION_PARAMS +- BATCH_STEP_EXECUTION +- BATCH_STEP_EXECUTION_CONTEXT +- BATCH_JOB_EXECUTION_CONTEXT + +**설정**: +```yaml +spring: + batch: + jdbc: + initialize-schema: always +``` + +### 2. batch_history 테이블 (커스텀) +배치 작업 실행 이력을 저장하는 커스텀 테이블입니다. + +**용도**: +- 배치 작업 시작/종료 시간 기록 +- 배치 실행 상태 추적 (STARTED/COMPLETED/FAILED) +- 비즈니스 ID별 배치 이력 관리 + +## 새 환경 데이터베이스 초기 설정 + +### Option 1: SQL 스크립트 실행 (권장) + +**1. batch_history 테이블 생성**: +```bash +# PostgreSQL에 연결 +psql -h [host] -U [username] -d [database] + +# schema.sql 실행 +\i src/main/resources/sql/schema.sql +``` + +**또는 직접 SQL 실행**: +```sql +-- batch_history 테이블 생성 +CREATE TABLE IF NOT EXISTS public.batch_history ( + uuid UUID PRIMARY KEY, + job VARCHAR(255) NOT NULL, + id VARCHAR(255) NOT NULL, + created_dttm TIMESTAMP NOT NULL, + updated_dttm TIMESTAMP NOT NULL, + status VARCHAR(50) NOT NULL, + completed_dttm TIMESTAMP +); + +-- 인덱스 생성 +CREATE INDEX IF NOT EXISTS idx_batch_history_job ON public.batch_history(job); +CREATE INDEX IF NOT EXISTS idx_batch_history_status ON public.batch_history(status); +CREATE INDEX IF NOT EXISTS idx_batch_history_created ON public.batch_history(created_dttm DESC); +``` + +**2. 권한 설정** (필요한 경우): +```sql +-- 애플리케이션 사용자에게 권한 부여 +GRANT ALL PRIVILEGES ON TABLE public.batch_history TO [app_user]; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO [app_user]; +``` + +### Option 2: 자동 초기화 활성화 (개발 환경만) + +**application-local.yml 또는 application-dev.yml**: +```yaml +spring: + sql: + init: + mode: always + schema-locations: classpath:sql/schema.sql +``` + +**주의**: +- 운영 환경에서는 `mode: never` 사용 권장 +- 테이블이 이미 존재하면 권한 에러 발생 가능 + +## 테이블 구조 + +### batch_history + +| 컬럼명 | 데이터 타입 | NULL | 설명 | +|--------|------------|------|------| +| uuid | UUID | NOT NULL | 배치 실행 고유 ID (Primary Key) | +| job | VARCHAR(255) | NOT NULL | 배치 작업 이름 | +| 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 | NULL | 완료 일시 | + +**인덱스**: +- `idx_batch_history_job`: job 컬럼 인덱스 +- `idx_batch_history_status`: status 컬럼 인덱스 +- `idx_batch_history_created`: created_dttm 컬럼 인덱스 (DESC) + +## 환경별 설정 + +### Local 환경 +```yaml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/kamco_local + username: dev_user + password: dev_password + sql: + init: + mode: always # 자동 초기화 활성화 +``` + +### Production 환경 +```yaml +spring: + datasource: + url: jdbc:postgresql://prod-db:5432/kamco_prod + username: app_user + password: ${DB_PASSWORD} + sql: + init: + mode: never # 자동 초기화 비활성화 +``` + +## 트러블슈팅 + +### 에러: "must be owner of table batch_history" + +**원인**: 테이블이 이미 존재하지만, 현재 DB 사용자가 owner가 아님 + +**해결**: +1. SQL 자동 초기화 비활성화: +```yaml +spring: + sql: + init: + mode: never +``` + +2. 또는 테이블 소유권 변경: +```sql +ALTER TABLE public.batch_history OWNER TO [app_user]; +``` + +### 에러: "relation batch_history does not exist" + +**원인**: batch_history 테이블이 생성되지 않음 + +**해결**: +1. SQL 스크립트 수동 실행: +```bash +psql -h [host] -U [user] -d [database] -f src/main/resources/sql/schema.sql +``` + +2. 또는 자동 초기화 활성화 (개발 환경만): +```yaml +spring: + sql: + init: + mode: always + schema-locations: classpath:sql/schema.sql +``` + +### 에러: "permission denied for schema public" + +**원인**: DB 사용자에게 public 스키마 권한 없음 + +**해결**: +```sql +GRANT ALL ON SCHEMA public TO [app_user]; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO [app_user]; +``` + +## 데이터베이스 마이그레이션 + +### 기존 환경에서 테이블 확인 +```sql +-- batch_history 테이블 존재 확인 +SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'batch_history' +); + +-- 테이블 구조 확인 +\d public.batch_history + +-- 인덱스 확인 +\di public.idx_batch_history_* +``` + +### 테이블 재생성 (데이터 삭제 주의!) +```sql +-- 기존 테이블 삭제 (주의: 모든 데이터 손실!) +DROP TABLE IF EXISTS public.batch_history CASCADE; + +-- schema.sql 재실행 +\i src/main/resources/sql/schema.sql +``` + +## 백업 및 복구 + +### 테이블 백업 +```bash +pg_dump -h [host] -U [user] -d [database] -t batch_history > batch_history_backup.sql +``` + +### 테이블 복구 +```bash +psql -h [host] -U [user] -d [database] < batch_history_backup.sql +``` + +## 모니터링 쿼리 + +### 최근 배치 실행 이력 조회 +```sql +SELECT + uuid, + job, + id, + created_dttm, + completed_dttm, + status, + EXTRACT(EPOCH FROM (completed_dttm - created_dttm)) as duration_seconds +FROM public.batch_history +ORDER BY created_dttm DESC +LIMIT 10; +``` + +### 실패한 배치 조회 +```sql +SELECT * +FROM public.batch_history +WHERE status = 'FAILED' +ORDER BY created_dttm DESC; +``` + +### 배치 작업별 성공률 +```sql +SELECT + job, + COUNT(*) as total_runs, + SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) as successful_runs, + ROUND(100.0 * SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate +FROM public.batch_history +GROUP BY job +ORDER BY total_runs DESC; +``` diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/ExportGeoJsonTasklet.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/ExportGeoJsonTasklet.java index 7ba9552..2931dfe 100644 --- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/ExportGeoJsonTasklet.java +++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/batch/ExportGeoJsonTasklet.java @@ -38,70 +38,115 @@ public class ExportGeoJsonTasklet implements Tasklet { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + log.info("========================================"); + log.info("배치 작업 시작"); + log.info("========================================"); // 1. StepContext를 통해 바로 가져오기 (가장 추천) String jobName = chunkContext.getStepContext().getJobName(); + log.info("Job Name: {}", jobName); // 진행중인 회차 중, complete_cnt 가 존재하는 회차 목록 가져오기 + log.info("진행중인 회차 목록 조회 중..."); List analList = repository.findAnalCntInfoList(); + log.info("진행중인 회차 수: {}", analList.size()); + int processedAnalCount = 0; for (AnalCntInfo info : analList) { + log.info("----------------------------------------"); + log.info("회차 처리 중: AnalUid={}, ResultUid={}", info.getAnalUid(), info.getResultUid()); + log.info("전체 건수: {}, 파일 건수: {}", info.getAllCnt(), info.getFileCnt()); + if (Objects.equals(info.getAllCnt(), info.getFileCnt())) { + log.info("모든 파일이 이미 처리됨. 건너뜀."); continue; } //추론 ID String resultUid = info.getResultUid(); + log.info("ResultUid: {}", resultUid); //insert 하기 jobname, resultUid , 시작시간 // 어제까지 검수 완료된 총 데이터의 도엽별 목록 가져오기 + log.info("검수 완료된 도엽 목록 조회 중... (AnalUid={})", info.getAnalUid()); List analMapList = repository.findCompletedAnalMapSheetList(info.getAnalUid()); + log.info("검수 완료된 도엽 수: {}", analMapList.size()); //TODO 도엽이 4개이상 존재할때 만 RUN 하기 if (analMapList.isEmpty()) { + log.warn("검수 완료된 도엽이 없음. 건너뜀."); continue; } //insert 하기 jobname, resultUid , 시작시간 boolean anyProcessed = false; + int processedMapSheetCount = 0; + int totalGeoJsonFiles = 0; for (AnalMapSheetList mapSheet : analMapList) { + log.info(" 도엽 처리 중: MapSheetNum={}", mapSheet.getMapSheetNum()); + //도엽별 geom 데이터 가지고 와서 geojson 만들기 List completeList = repository.findCompletedYesterdayLabelingList( info.getAnalUid(), mapSheet.getMapSheetNum()); + log.info(" 완료된 라벨링 데이터 수: {}", completeList.size()); if (!completeList.isEmpty()) { List geoUids = completeList.stream().map(CompleteLabelData::getGeoUid).toList(); + log.info(" GeoUID 목록 생성 완료: {} 건", geoUids.size()); List features = completeList.stream().map(GeoJsonFeature::from).toList(); + log.info(" GeoJSON Feature 변환 완료: {} 개", features.size()); FeatureCollection collection = new FeatureCollection(features); String filename = mapSheet.buildFilename(resultUid); + log.info(" GeoJSON 파일명: {}", filename); // 형식 /kamco-nfs/dataset/request/uuid/filename Path outputPath = Paths.get(trainingDataDir + File.separator + "request" + File.separator + resultUid, filename); + log.info(" 출력 경로: {}", outputPath); try { Files.createDirectories(outputPath.getParent()); + log.info(" 디렉토리 생성 완료: {}", outputPath.getParent()); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.writeValue(outputPath.toFile(), collection); + log.info(" GeoJSON 파일 저장 완료: {}", outputPath); repository.updateLearnDataGeomFileCreateYn(geoUids); + log.info(" DB 업데이트 완료: {} 건", geoUids.size()); + anyProcessed = true; + processedMapSheetCount++; + totalGeoJsonFiles++; } catch (IOException e) { - log.error(e.getMessage()); + log.error(" GeoJSON 파일 생성 실패: {}", e.getMessage(), e); } } } + log.info("회차 처리 완료: ResultUid={}", resultUid); + log.info(" 처리된 도엽 수: {}", processedMapSheetCount); + log.info(" 생성된 GeoJSON 파일 수: {}", totalGeoJsonFiles); + if (anyProcessed) { + log.info("Docker 컨테이너 실행 중... (ResultUid={})", resultUid); dockerRunnerService.run(resultUid); + log.info("Docker 컨테이너 실행 완료 (ResultUid={})", resultUid); + processedAnalCount++; + } else { + log.warn("처리된 도엽이 없어 Docker 실행 건너뜀 (ResultUid={})", resultUid); } } + log.info("========================================"); + log.info("배치 작업 완료"); + log.info("처리된 회차 수: {}", processedAnalCount); + log.info("========================================"); + return RepeatStatus.FINISHED; } } diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryListener.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryListener.java index ed2e8d2..92f61f6 100644 --- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryListener.java +++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryListener.java @@ -1,12 +1,15 @@ package com.kamco.cd.geojsonscheduler.listener; +import lombok.extern.log4j.Log4j2; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.BatchStatus; import org.springframework.stereotype.Component; +import java.time.Duration; import java.util.UUID; +@Log4j2 @Component public class BatchHistoryListener implements JobExecutionListener { @@ -18,8 +21,13 @@ public class BatchHistoryListener implements JobExecutionListener { @Override public void beforeJob(JobExecution jobExecution) { + log.info("========================================================="); + log.info("배치 Job 시작 - BatchHistoryListener"); + log.info("========================================================="); + // 1. UUID 생성 (또는 파라미터에서 가져오기) UUID uuid = UUID.randomUUID(); + log.info("배치 UUID 생성: {}", uuid); // 2. JobExecutionContext에 UUID 저장 (afterJob에서 쓰기 위해) jobExecution.getExecutionContext().put("batch_uuid", uuid); @@ -27,22 +35,56 @@ public class BatchHistoryListener implements JobExecutionListener { // 3. Job 이름과 비즈니스 ID(파라미터 등) 가져오기 String jobName = jobExecution.getJobInstance().getJobName(); String businessId = jobExecution.getJobParameters().getString("id", "UNKNOWN"); // 파라미터 'id'가 있다고 가정 + log.info("Job Name: {}", jobName); + log.info("Business ID: {}", businessId); + log.info("Job Instance ID: {}", jobExecution.getJobInstance().getInstanceId()); + log.info("Job Execution ID: {}", jobExecution.getId()); // 4. 시작 기록 + log.info("batch_history 테이블에 시작 기록 저장 중..."); batchHistoryService.startBatch(uuid, jobName, businessId); + log.info("배치 시작 기록 저장 완료"); } @Override public void afterJob(JobExecution jobExecution) { + log.info("========================================================="); + log.info("배치 Job 종료 - BatchHistoryListener"); + log.info("========================================================="); + // 1. 저장해둔 UUID 꺼내기 UUID uuid = (UUID) jobExecution.getExecutionContext().get("batch_uuid"); + log.info("배치 UUID: {}", uuid); // 2. 성공 여부 판단 (COMPLETED면 성공, 그 외 실패) boolean isSuccess = jobExecution.getStatus() == BatchStatus.COMPLETED; + log.info("배치 상태: {}", jobExecution.getStatus()); + log.info("배치 성공 여부: {}", isSuccess ? "성공" : "실패"); + + if (jobExecution.getStatus() == BatchStatus.FAILED) { + log.error("배치 실행 실패!"); + jobExecution.getAllFailureExceptions().forEach(t -> + log.error("실패 원인: {}", t.getMessage(), t) + ); + } + + // 실행 시간 계산 + if (jobExecution.getStartTime() != null && jobExecution.getEndTime() != null) { + Duration duration = Duration.between(jobExecution.getStartTime(), jobExecution.getEndTime()); + long seconds = duration.getSeconds(); + long millis = duration.toMillis(); + log.info("배치 실행 시간: {} ms ({} 초)", millis, seconds); + } // 3. 종료 기록 if (uuid != null) { + log.info("batch_history 테이블에 종료 기록 저장 중..."); batchHistoryService.finishBatch(uuid, isSuccess); + log.info("배치 종료 기록 저장 완료"); + } else { + log.warn("배치 UUID가 없어 종료 기록을 저장할 수 없습니다."); } + + log.info("========================================================="); } } \ No newline at end of file diff --git a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryService.java b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryService.java index 2e06fec..d94c138 100644 --- a/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryService.java +++ b/kamco-make-dataset-generation/src/main/java/com/kamco/cd/geojsonscheduler/listener/BatchHistoryService.java @@ -1,5 +1,6 @@ package com.kamco.cd.geojsonscheduler.listener; +import lombok.extern.log4j.Log4j2; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -8,6 +9,7 @@ import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.UUID; +@Log4j2 @Service public class BatchHistoryService { @@ -22,16 +24,22 @@ public class BatchHistoryService { */ @Transactional public void startBatch(UUID uuid, String jobName, String businessId) { + log.info("[BatchHistoryService] 배치 시작 기록 저장"); + log.info(" UUID: {}", uuid); + log.info(" Job Name: {}", jobName); + log.info(" Business ID: {}", businessId); + String sql = """ - INSERT INTO public.batch_history - (uuid, job, id, created_dttm, updated_dttm, status) + INSERT INTO public.batch_history + (uuid, job, id, created_dttm, updated_dttm, status) VALUES (?, ?, ?, ?, ?, ?) """; Timestamp now = Timestamp.valueOf(LocalDateTime.now()); + log.info(" 시작 시간: {}", now); // 초기 상태는 'STARTED'로 저장 - jdbcTemplate.update(sql, + int rowsAffected = jdbcTemplate.update(sql, uuid, jobName, businessId, @@ -39,6 +47,8 @@ public class BatchHistoryService { now, // updated_dttm "STARTED" ); + + log.info("[BatchHistoryService] 배치 시작 기록 저장 완료 ({} rows affected)", rowsAffected); } /** @@ -46,22 +56,34 @@ public class BatchHistoryService { */ @Transactional public void finishBatch(UUID uuid, boolean isSuccess) { + log.info("[BatchHistoryService] 배치 종료 기록 업데이트"); + log.info(" UUID: {}", uuid); + log.info(" 성공 여부: {}", isSuccess); + String sql = """ - UPDATE public.batch_history - SET status = ?, - updated_dttm = ?, - completed_dttm = ? + UPDATE public.batch_history + SET status = ?, + updated_dttm = ?, + completed_dttm = ? WHERE uuid = ? """; Timestamp now = Timestamp.valueOf(LocalDateTime.now()); String status = isSuccess ? "COMPLETED" : "FAILED"; + log.info(" 완료 시간: {}", now); + log.info(" 최종 상태: {}", status); - jdbcTemplate.update(sql, + int rowsAffected = jdbcTemplate.update(sql, status, now, // updated_dttm (마지막 변경 시간) now, // completed_dttm (완료 시간) uuid ); + + if (rowsAffected > 0) { + log.info("[BatchHistoryService] 배치 종료 기록 업데이트 완료 ({} rows affected)", rowsAffected); + } else { + log.warn("[BatchHistoryService] 업데이트된 row가 없습니다. UUID가 존재하지 않을 수 있습니다: {}", uuid); + } } } \ No newline at end of file diff --git a/kamco-make-dataset-generation/src/main/resources/application.yml b/kamco-make-dataset-generation/src/main/resources/application.yml index be7fd93..fae9cc5 100644 --- a/kamco-make-dataset-generation/src/main/resources/application.yml +++ b/kamco-make-dataset-generation/src/main/resources/application.yml @@ -13,8 +13,11 @@ spring: max-lifetime: 1800000 sql: init: - mode: always - schema-locations: classpath:sql/schema.sql + mode: never + # schema-locations: classpath:sql/schema.sql + # Note: batch_history 테이블이 이미 존재하므로 SQL 초기화 비활성화 + # 새 환경에서 테이블 생성이 필요한 경우, 아래 SQL을 수동으로 실행: + # src/main/resources/sql/schema.sql batch: job: enabled: true