jenkinsfile

This commit is contained in:
2026-02-09 11:20:15 +09:00
parent e50295e929
commit 731dbb4170
6 changed files with 380 additions and 11 deletions

View File

@@ -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;
```

View File

@@ -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<AnalCntInfo> 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<AnalMapSheetList> 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<CompleteLabelData> completeList =
repository.findCompletedYesterdayLabelingList(
info.getAnalUid(), mapSheet.getMapSheetNum());
log.info(" 완료된 라벨링 데이터 수: {}", completeList.size());
if (!completeList.isEmpty()) {
List<Long> geoUids = completeList.stream().map(CompleteLabelData::getGeoUid).toList();
log.info(" GeoUID 목록 생성 완료: {} 건", geoUids.size());
List<GeoJsonFeature> 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;
}
}

View File

@@ -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("=========================================================");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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