From 731dbb41701e1bdbd78ff747eb6db809f6596a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dean=5B=EB=B0=B1=EB=B3=91=EB=82=A8=5D?= Date: Mon, 9 Feb 2026 11:20:15 +0900 Subject: [PATCH] jenkinsfile --- .../libs/generator-dataset-for-training.jar | Bin 19774761 -> 19778283 bytes .../claudedocs/DATABASE_SETUP.md | 257 ++++++++++++++++++ .../batch/ExportGeoJsonTasklet.java | 47 +++- .../listener/BatchHistoryListener.java | 42 +++ .../listener/BatchHistoryService.java | 38 ++- .../src/main/resources/application.yml | 7 +- 6 files changed, 380 insertions(+), 11 deletions(-) create mode 100644 kamco-make-dataset-generation/claudedocs/DATABASE_SETUP.md 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 c878dbb17473bf9c6799f33236c80772f737a381..beef3c277a67ae5226d61ba5fc20abecac7aa6e3 100644 GIT binary patch delta 12420 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 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