init spotless 적용

This commit is contained in:
2026-02-02 15:48:23 +09:00
parent 495ef7d86c
commit a1ffad1c4e
153 changed files with 12870 additions and 12931 deletions

View File

@@ -1,50 +1,50 @@
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelMngDto.SearchReq;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelMngService {
private final ModelMngCoreService modelMngCoreService;
/**
* 모델 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 모델 목록
*/
public Page<Basic> findByModels(SearchReq searchReq) {
return modelMngCoreService.findByModels(searchReq);
}
/**
* 모델 상세 조회
*
* @param modelUid 모델 UID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetail(Long modelUid) {
return modelMngCoreService.getModelDetail(modelUid);
}
/**
* 모델 상세 조회 (UUID 기반)
*
* @param uuid 모델 UUID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetailByUuid(String uuid) {
return modelMngCoreService.getModelDetailByUuid(uuid);
}
}
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelMngDto.SearchReq;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelMngService {
private final ModelMngCoreService modelMngCoreService;
/**
* 모델 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 모델 목록
*/
public Page<Basic> findByModels(SearchReq searchReq) {
return modelMngCoreService.findByModels(searchReq);
}
/**
* 모델 상세 조회
*
* @param modelUid 모델 UID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetail(Long modelUid) {
return modelMngCoreService.getModelDetail(modelUid);
}
/**
* 모델 상세 조회 (UUID 기반)
*
* @param uuid 모델 UUID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetailByUuid(String uuid) {
return modelMngCoreService.getModelDetailByUuid(uuid);
}
}

View File

@@ -1,393 +1,393 @@
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import com.kamco.cd.training.postgres.core.SystemMetricsCoreService;
import com.kamco.cd.training.postgres.entity.ModelTrainMasterEntity;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelTrainService {
private final ModelMngCoreService modelMngCoreService;
private final HyperParamCoreService hyperParamCoreService;
private final DatasetCoreService datasetCoreService;
private final SystemMetricsCoreService systemMetricsCoreService;
/**
* 학습 모델 목록 조회
*
* @return 학습 모델 목록
*/
public List<ModelMngDto.TrainListRes> getTrainModelList() {
return modelMngCoreService.findAllTrainModels();
}
/**
* 학습 설정 통합 조회
*
* @return 학습 설정 폼 데이터
*/
public ModelMngDto.FormConfigRes getFormConfig() {
// 1. 현재 실행 중인 모델 확인
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
boolean isTrainAvailable = (runningModelUuid == null);
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (isTrainAvailable) {
isTrainAvailable = systemMetricsCoreService.isStorageAvailableForTraining();
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
log.info("저장공간 체크 완료: {}MB 사용 가능, 학습 가능 여부: {}", availableMB, isTrainAvailable);
}
// 3. 하이퍼파라미터 목록
List<ModelMngDto.HyperParamInfo> hyperParams = hyperParamCoreService.findAllActiveHyperParams();
// 4. 데이터셋 목록
List<ModelMngDto.DatasetInfo> datasets = datasetCoreService.findAllActiveDatasetsForTraining();
return ModelMngDto.FormConfigRes.builder()
.isTrainAvailable(isTrainAvailable)
.hyperParams(hyperParams)
.datasets(datasets)
.runningModelUuid(runningModelUuid)
.build();
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 생성된 버전명
*/
@Transactional
public String createHyperParam(ModelMngDto.HyperParamCreateReq createReq) {
// 신규 버전 추가 시 baseHyperVer가 없으면 H1으로 설정
if (createReq.getBaseHyperVer() == null || createReq.getBaseHyperVer().isEmpty()) {
String firstVersion = hyperParamCoreService.getFirstHyperParamVersion();
createReq.setBaseHyperVer(firstVersion);
log.info("baseHyperVer가 없어 첫 번째 버전으로 설정: {}", firstVersion);
}
String newVersion = hyperParamCoreService.createHyperParam(createReq);
log.info("하이퍼파라미터 등록 완료: {}", newVersion);
return newVersion;
}
/**
* 하이퍼파라미터 단건 조회
*
* @param hyperVer 하이퍼파라미터 버전
* @return 하이퍼파라미터 정보
*/
public ModelMngDto.HyperParamInfo getHyperParam(String hyperVer) {
return hyperParamCoreService.findByHyperVer(hyperVer);
}
/**
* 하이퍼파라미터 삭제
*
* @param hyperVer 하이퍼파라미터 버전
*/
@Transactional
public void deleteHyperParam(String hyperVer) {
hyperParamCoreService.deleteHyperParam(hyperVer);
log.info("하이퍼파라미터 삭제 완료: {}", hyperVer);
}
/**
* 학습 시작
*
* @param trainReq 학습 시작 요청
* @return 학습 시작 응답
*/
@Transactional
public ModelMngDto.TrainStartRes startTraining(ModelMngDto.TrainStartReq trainReq) {
// 1. 동시 실행 방지 검증
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
if (runningModelUuid != null) {
throw new BadRequestException(
"이미 실행 중인 학습이 있습니다. 학습은 한 번에 한 개만 실행할 수 있습니다. (실행 중인 모델: " + runningModelUuid + ")");
}
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (!systemMetricsCoreService.isStorageAvailableForTraining()) {
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
long requiredMB = 10 * 1024; // 10GB
throw new BadRequestException(
String.format(
"저장공간이 부족하여 학습을 시작할 수 없습니다. (필요: %dMB, 사용 가능: %dMB)", requiredMB, availableMB));
}
// 3. 데이터셋 상태 검증 (COMPLETED 상태만 학습 가능)
validateDatasetStatus(trainReq.getDatasetIds());
// 4. 데이터 분할 비율 검증 (예: "7:2:1" 형식)
if (trainReq.getDatasetRatio() != null && !trainReq.getDatasetRatio().isEmpty()) {
validateDatasetRatio(trainReq.getDatasetRatio());
}
// 5. 학습 마스터 생성
ModelTrainMasterEntity entity = modelMngCoreService.createTrainMaster(trainReq);
// 5. 데이터셋 매핑 생성
modelMngCoreService.createDatasetMappings(entity.getId(), trainReq.getDatasetIds());
// 6. 실제 UUID 사용
String uuid = entity.getUuid().toString();
log.info(
"학습 시작: uuid={}, hyperVer={}, epoch={}, datasets={}",
uuid,
trainReq.getHyperVer(),
trainReq.getEpoch(),
trainReq.getDatasetIds());
// TODO: 비동기 GPU 학습 프로세스 트리거 로직 추가
return ModelMngDto.TrainStartRes.builder().uuid(uuid).status(entity.getStatusCd()).build();
}
/**
* 데이터셋 상태 검증
*
* @param datasetIds 데이터셋 ID 목록
*/
private void validateDatasetStatus(List<Long> datasetIds) {
for (Long datasetId : datasetIds) {
try {
var dataset = datasetCoreService.getOneById(datasetId);
// COMPLETED 상태가 아닌 데이터셋이 포함되어 있으면 예외 발생
if (dataset.getStatus() == null || !"COMPLETED".equals(dataset.getStatus())) {
throw new BadRequestException(
String.format(
"학습에 사용할 수 없는 데이터셋입니다. (ID: %d, 상태: %s). COMPLETED 상태의 데이터셋만 선택 가능합니다.",
datasetId, dataset.getStatus() != null ? dataset.getStatus() : "NULL"));
}
log.debug("데이터셋 상태 검증 통과: ID={}, Status={}", datasetId, dataset.getStatus());
} catch (NotFoundException e) {
throw new BadRequestException("존재하지 않는 데이터셋입니다. ID: " + datasetId);
}
}
log.info("모든 데이터셋 상태 검증 완료: {} 개", datasetIds.size());
}
/**
* 데이터 분할 비율 검증
*
* @param datasetRatio 데이터셋 비율 (예: "7:2:1")
*/
private void validateDatasetRatio(String datasetRatio) {
try {
String[] parts = datasetRatio.split(":");
if (parts.length != 3) {
throw new BadRequestException("데이터 분할 비율은 'Training:Validation:Test' 형식이어야 합니다 (예: 7:2:1)");
}
int train = Integer.parseInt(parts[0].trim());
int validation = Integer.parseInt(parts[1].trim());
int test = Integer.parseInt(parts[2].trim());
int sum = train + validation + test;
if (sum != 10) {
throw new BadRequestException(
String.format("데이터 분할 비율의 합계는 10이어야 합니다. (현재 합계: %d, 입력값: %s)", sum, datasetRatio));
}
if (train <= 0 || validation < 0 || test < 0) {
throw new BadRequestException("데이터 분할 비율은 모두 0 이상이어야 합니다 (Training은 1 이상)");
}
log.info(
"데이터 분할 비율 검증 완료: Training={}0%, Validation={}0%, Test={}0%", train, validation, test);
} catch (NumberFormatException e) {
throw new BadRequestException("데이터 분할 비율은 숫자로만 구성되어야 합니다: " + datasetRatio);
}
}
/**
* 학습 모델 삭제
*
* @param uuid 모델 UUID
*/
@Transactional
public void deleteTrainModel(String uuid) {
modelMngCoreService.deleteByUuid(uuid);
log.info("학습 모델 삭제 완료: uuid={}", uuid);
}
// ==================== Resume Training (학습 재시작) ====================
/**
* 학습 재시작 정보 조회
*
* @param uuid 모델 UUID
* @return 재시작 정보
*/
public ModelMngDto.ResumeInfo getResumeInfo(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
return ModelMngDto.ResumeInfo.builder()
.canResume(entity.getCanResume() != null && entity.getCanResume())
.lastEpoch(entity.getLastCheckpointEpoch())
.totalEpoch(entity.getEpochCnt())
.checkpointPath(entity.getCheckpointPath())
.failedAt(
entity.getStopDttm() != null
? entity.getStopDttm().atZone(java.time.ZoneId.systemDefault())
: null)
.build();
}
/**
* 학습 재시작
*
* @param uuid 모델 UUID
* @param resumeReq 재시작 요청
* @return 재시작 응답
*/
@Transactional
public ModelMngDto.ResumeResponse resumeTraining(
String uuid, ModelMngDto.ResumeRequest resumeReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 재시작 가능 여부 검증
if (entity.getCanResume() == null || !entity.getCanResume()) {
throw new IllegalStateException("학습 재시작이 불가능한 모델입니다: " + uuid);
}
if (entity.getLastCheckpointEpoch() == null) {
throw new IllegalStateException("Checkpoint가 존재하지 않습니다: " + uuid);
}
// 상태 업데이트
entity.setStatusCd("RUNNING");
entity.setProgressRate(0);
// 총 Epoch 수 변경 (선택사항)
if (resumeReq.getNewTotalEpoch() != null) {
entity.setEpochCnt(resumeReq.getNewTotalEpoch());
}
log.info(
"학습 재시작: uuid={}, resumeFromEpoch={}, totalEpoch={}",
uuid,
resumeReq.getResumeFromEpoch(),
entity.getEpochCnt());
// TODO: 비동기 GPU 학습 재시작 프로세스 트리거 로직 추가
// - Checkpoint 파일 로드
// - 지정된 Epoch부터 학습 재개
return ModelMngDto.ResumeResponse.builder()
.uuid(uuid)
.status(entity.getStatusCd())
.resumedFromEpoch(resumeReq.getResumeFromEpoch())
.build();
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
/**
* Best Epoch 설정
*
* @param uuid 모델 UUID
* @param bestEpochReq Best Epoch 요청
* @return Best Epoch 응답
*/
@Transactional
public ModelMngDto.BestEpochResponse setBestEpoch(
String uuid, ModelMngDto.BestEpochRequest bestEpochReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 1차 학습 완료 상태 검증
if (!"STEP1_COMPLETED".equals(entity.getStatusCd())
&& !"STEP1".equals(entity.getProcessStep())) {
log.warn(
"Best Epoch 설정 시도: 현재 상태={}, processStep={}",
entity.getStatusCd(),
entity.getProcessStep());
}
Integer previousBestEpoch = entity.getConfirmedBestEpoch();
// 사용자가 확정한 Best Epoch 설정
entity.setConfirmedBestEpoch(bestEpochReq.getBestEpoch());
// 2차 학습(Test) 단계로 상태 전이
entity.setProcessStep("STEP2");
entity.setStatusCd("STEP2_RUNNING");
entity.setProgressRate(0);
entity.setUpdatedDttm(java.time.ZonedDateTime.now());
log.info(
"Best Epoch 설정 및 2차 학습 시작: uuid={}, newBestEpoch={}, previousBestEpoch={}, reason={}, newStatus={}",
uuid,
bestEpochReq.getBestEpoch(),
previousBestEpoch,
bestEpochReq.getReason(),
entity.getStatusCd());
// TODO: 비동기 GPU 2차 학습(Test) 프로세스 트리거 로직 추가
// - Best Epoch 모델 로드
// - Test 데이터셋으로 성능 평가 실행
// - 완료 시 STEP2_COMPLETED 상태로 전환
return ModelMngDto.BestEpochResponse.builder()
.uuid(uuid)
.bestEpoch(entity.getBestEpoch()) // 자동 선택된 값
.confirmedBestEpoch(entity.getConfirmedBestEpoch()) // 사용자 확정 값
.previousBestEpoch(previousBestEpoch)
.build();
}
/**
* Epoch별 성능 지표 조회
*
* @param uuid 모델 UUID
* @return Epoch별 성능 지표 목록
*/
public List<ModelMngDto.EpochMetric> getEpochMetrics(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// TODO: 실제 학습 로그 파일이나 DB에서 Epoch별 성능 지표 조회
// 현재는 샘플 데이터 반환
List<ModelMngDto.EpochMetric> metrics = new java.util.ArrayList<>();
if (entity.getEpochCnt() != null && entity.getBestEpoch() != null) {
// 샘플 데이터 생성 (실제로는 학습 로그 파일 파싱 또는 별도 테이블 조회)
for (int i = 1; i <= Math.min(entity.getEpochCnt(), 10); i++) {
int epoch = entity.getBestEpoch() - 5 + i;
if (epoch <= 0 || epoch > entity.getEpochCnt()) {
continue;
}
metrics.add(
ModelMngDto.EpochMetric.builder()
.epoch(epoch)
.mIoU(0.80 + (Math.random() * 0.15)) // 샘플 데이터
.mFscore(0.85 + (Math.random() * 0.10)) // 샘플 데이터
.loss(0.3 - (Math.random() * 0.15)) // 샘플 데이터
.isBest(entity.getBestEpoch() != null && epoch == entity.getBestEpoch())
.build());
}
}
log.info("Epoch별 성능 지표 조회: uuid={}, metricsCount={}", uuid, metrics.size());
return metrics;
}
}
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import com.kamco.cd.training.postgres.core.SystemMetricsCoreService;
import com.kamco.cd.training.postgres.entity.ModelTrainMasterEntity;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelTrainService {
private final ModelMngCoreService modelMngCoreService;
private final HyperParamCoreService hyperParamCoreService;
private final DatasetCoreService datasetCoreService;
private final SystemMetricsCoreService systemMetricsCoreService;
/**
* 학습 모델 목록 조회
*
* @return 학습 모델 목록
*/
public List<ModelMngDto.TrainListRes> getTrainModelList() {
return modelMngCoreService.findAllTrainModels();
}
/**
* 학습 설정 통합 조회
*
* @return 학습 설정 폼 데이터
*/
public ModelMngDto.FormConfigRes getFormConfig() {
// 1. 현재 실행 중인 모델 확인
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
boolean isTrainAvailable = (runningModelUuid == null);
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (isTrainAvailable) {
isTrainAvailable = systemMetricsCoreService.isStorageAvailableForTraining();
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
log.info("저장공간 체크 완료: {}MB 사용 가능, 학습 가능 여부: {}", availableMB, isTrainAvailable);
}
// 3. 하이퍼파라미터 목록
List<ModelMngDto.HyperParamInfo> hyperParams = hyperParamCoreService.findAllActiveHyperParams();
// 4. 데이터셋 목록
List<ModelMngDto.DatasetInfo> datasets = datasetCoreService.findAllActiveDatasetsForTraining();
return ModelMngDto.FormConfigRes.builder()
.isTrainAvailable(isTrainAvailable)
.hyperParams(hyperParams)
.datasets(datasets)
.runningModelUuid(runningModelUuid)
.build();
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 생성된 버전명
*/
@Transactional
public String createHyperParam(ModelMngDto.HyperParamCreateReq createReq) {
// 신규 버전 추가 시 baseHyperVer가 없으면 H1으로 설정
if (createReq.getBaseHyperVer() == null || createReq.getBaseHyperVer().isEmpty()) {
String firstVersion = hyperParamCoreService.getFirstHyperParamVersion();
createReq.setBaseHyperVer(firstVersion);
log.info("baseHyperVer가 없어 첫 번째 버전으로 설정: {}", firstVersion);
}
String newVersion = hyperParamCoreService.createHyperParam(createReq);
log.info("하이퍼파라미터 등록 완료: {}", newVersion);
return newVersion;
}
/**
* 하이퍼파라미터 단건 조회
*
* @param hyperVer 하이퍼파라미터 버전
* @return 하이퍼파라미터 정보
*/
public ModelMngDto.HyperParamInfo getHyperParam(String hyperVer) {
return hyperParamCoreService.findByHyperVer(hyperVer);
}
/**
* 하이퍼파라미터 삭제
*
* @param hyperVer 하이퍼파라미터 버전
*/
@Transactional
public void deleteHyperParam(String hyperVer) {
hyperParamCoreService.deleteHyperParam(hyperVer);
log.info("하이퍼파라미터 삭제 완료: {}", hyperVer);
}
/**
* 학습 시작
*
* @param trainReq 학습 시작 요청
* @return 학습 시작 응답
*/
@Transactional
public ModelMngDto.TrainStartRes startTraining(ModelMngDto.TrainStartReq trainReq) {
// 1. 동시 실행 방지 검증
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
if (runningModelUuid != null) {
throw new BadRequestException(
"이미 실행 중인 학습이 있습니다. 학습은 한 번에 한 개만 실행할 수 있습니다. (실행 중인 모델: " + runningModelUuid + ")");
}
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (!systemMetricsCoreService.isStorageAvailableForTraining()) {
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
long requiredMB = 10 * 1024; // 10GB
throw new BadRequestException(
String.format(
"저장공간이 부족하여 학습을 시작할 수 없습니다. (필요: %dMB, 사용 가능: %dMB)", requiredMB, availableMB));
}
// 3. 데이터셋 상태 검증 (COMPLETED 상태만 학습 가능)
validateDatasetStatus(trainReq.getDatasetIds());
// 4. 데이터 분할 비율 검증 (예: "7:2:1" 형식)
if (trainReq.getDatasetRatio() != null && !trainReq.getDatasetRatio().isEmpty()) {
validateDatasetRatio(trainReq.getDatasetRatio());
}
// 5. 학습 마스터 생성
ModelTrainMasterEntity entity = modelMngCoreService.createTrainMaster(trainReq);
// 5. 데이터셋 매핑 생성
modelMngCoreService.createDatasetMappings(entity.getId(), trainReq.getDatasetIds());
// 6. 실제 UUID 사용
String uuid = entity.getUuid().toString();
log.info(
"학습 시작: uuid={}, hyperVer={}, epoch={}, datasets={}",
uuid,
trainReq.getHyperVer(),
trainReq.getEpoch(),
trainReq.getDatasetIds());
// TODO: 비동기 GPU 학습 프로세스 트리거 로직 추가
return ModelMngDto.TrainStartRes.builder().uuid(uuid).status(entity.getStatusCd()).build();
}
/**
* 데이터셋 상태 검증
*
* @param datasetIds 데이터셋 ID 목록
*/
private void validateDatasetStatus(List<Long> datasetIds) {
for (Long datasetId : datasetIds) {
try {
var dataset = datasetCoreService.getOneById(datasetId);
// COMPLETED 상태가 아닌 데이터셋이 포함되어 있으면 예외 발생
if (dataset.getStatus() == null || !"COMPLETED".equals(dataset.getStatus())) {
throw new BadRequestException(
String.format(
"학습에 사용할 수 없는 데이터셋입니다. (ID: %d, 상태: %s). COMPLETED 상태의 데이터셋만 선택 가능합니다.",
datasetId, dataset.getStatus() != null ? dataset.getStatus() : "NULL"));
}
log.debug("데이터셋 상태 검증 통과: ID={}, Status={}", datasetId, dataset.getStatus());
} catch (NotFoundException e) {
throw new BadRequestException("존재하지 않는 데이터셋입니다. ID: " + datasetId);
}
}
log.info("모든 데이터셋 상태 검증 완료: {} 개", datasetIds.size());
}
/**
* 데이터 분할 비율 검증
*
* @param datasetRatio 데이터셋 비율 (예: "7:2:1")
*/
private void validateDatasetRatio(String datasetRatio) {
try {
String[] parts = datasetRatio.split(":");
if (parts.length != 3) {
throw new BadRequestException("데이터 분할 비율은 'Training:Validation:Test' 형식이어야 합니다 (예: 7:2:1)");
}
int train = Integer.parseInt(parts[0].trim());
int validation = Integer.parseInt(parts[1].trim());
int test = Integer.parseInt(parts[2].trim());
int sum = train + validation + test;
if (sum != 10) {
throw new BadRequestException(
String.format("데이터 분할 비율의 합계는 10이어야 합니다. (현재 합계: %d, 입력값: %s)", sum, datasetRatio));
}
if (train <= 0 || validation < 0 || test < 0) {
throw new BadRequestException("데이터 분할 비율은 모두 0 이상이어야 합니다 (Training은 1 이상)");
}
log.info(
"데이터 분할 비율 검증 완료: Training={}0%, Validation={}0%, Test={}0%", train, validation, test);
} catch (NumberFormatException e) {
throw new BadRequestException("데이터 분할 비율은 숫자로만 구성되어야 합니다: " + datasetRatio);
}
}
/**
* 학습 모델 삭제
*
* @param uuid 모델 UUID
*/
@Transactional
public void deleteTrainModel(String uuid) {
modelMngCoreService.deleteByUuid(uuid);
log.info("학습 모델 삭제 완료: uuid={}", uuid);
}
// ==================== Resume Training (학습 재시작) ====================
/**
* 학습 재시작 정보 조회
*
* @param uuid 모델 UUID
* @return 재시작 정보
*/
public ModelMngDto.ResumeInfo getResumeInfo(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
return ModelMngDto.ResumeInfo.builder()
.canResume(entity.getCanResume() != null && entity.getCanResume())
.lastEpoch(entity.getLastCheckpointEpoch())
.totalEpoch(entity.getEpochCnt())
.checkpointPath(entity.getCheckpointPath())
.failedAt(
entity.getStopDttm() != null
? entity.getStopDttm().atZone(java.time.ZoneId.systemDefault())
: null)
.build();
}
/**
* 학습 재시작
*
* @param uuid 모델 UUID
* @param resumeReq 재시작 요청
* @return 재시작 응답
*/
@Transactional
public ModelMngDto.ResumeResponse resumeTraining(
String uuid, ModelMngDto.ResumeRequest resumeReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 재시작 가능 여부 검증
if (entity.getCanResume() == null || !entity.getCanResume()) {
throw new IllegalStateException("학습 재시작이 불가능한 모델입니다: " + uuid);
}
if (entity.getLastCheckpointEpoch() == null) {
throw new IllegalStateException("Checkpoint가 존재하지 않습니다: " + uuid);
}
// 상태 업데이트
entity.setStatusCd("RUNNING");
entity.setProgressRate(0);
// 총 Epoch 수 변경 (선택사항)
if (resumeReq.getNewTotalEpoch() != null) {
entity.setEpochCnt(resumeReq.getNewTotalEpoch());
}
log.info(
"학습 재시작: uuid={}, resumeFromEpoch={}, totalEpoch={}",
uuid,
resumeReq.getResumeFromEpoch(),
entity.getEpochCnt());
// TODO: 비동기 GPU 학습 재시작 프로세스 트리거 로직 추가
// - Checkpoint 파일 로드
// - 지정된 Epoch부터 학습 재개
return ModelMngDto.ResumeResponse.builder()
.uuid(uuid)
.status(entity.getStatusCd())
.resumedFromEpoch(resumeReq.getResumeFromEpoch())
.build();
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
/**
* Best Epoch 설정
*
* @param uuid 모델 UUID
* @param bestEpochReq Best Epoch 요청
* @return Best Epoch 응답
*/
@Transactional
public ModelMngDto.BestEpochResponse setBestEpoch(
String uuid, ModelMngDto.BestEpochRequest bestEpochReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 1차 학습 완료 상태 검증
if (!"STEP1_COMPLETED".equals(entity.getStatusCd())
&& !"STEP1".equals(entity.getProcessStep())) {
log.warn(
"Best Epoch 설정 시도: 현재 상태={}, processStep={}",
entity.getStatusCd(),
entity.getProcessStep());
}
Integer previousBestEpoch = entity.getConfirmedBestEpoch();
// 사용자가 확정한 Best Epoch 설정
entity.setConfirmedBestEpoch(bestEpochReq.getBestEpoch());
// 2차 학습(Test) 단계로 상태 전이
entity.setProcessStep("STEP2");
entity.setStatusCd("STEP2_RUNNING");
entity.setProgressRate(0);
entity.setUpdatedDttm(java.time.ZonedDateTime.now());
log.info(
"Best Epoch 설정 및 2차 학습 시작: uuid={}, newBestEpoch={}, previousBestEpoch={}, reason={}, newStatus={}",
uuid,
bestEpochReq.getBestEpoch(),
previousBestEpoch,
bestEpochReq.getReason(),
entity.getStatusCd());
// TODO: 비동기 GPU 2차 학습(Test) 프로세스 트리거 로직 추가
// - Best Epoch 모델 로드
// - Test 데이터셋으로 성능 평가 실행
// - 완료 시 STEP2_COMPLETED 상태로 전환
return ModelMngDto.BestEpochResponse.builder()
.uuid(uuid)
.bestEpoch(entity.getBestEpoch()) // 자동 선택된 값
.confirmedBestEpoch(entity.getConfirmedBestEpoch()) // 사용자 확정 값
.previousBestEpoch(previousBestEpoch)
.build();
}
/**
* Epoch별 성능 지표 조회
*
* @param uuid 모델 UUID
* @return Epoch별 성능 지표 목록
*/
public List<ModelMngDto.EpochMetric> getEpochMetrics(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// TODO: 실제 학습 로그 파일이나 DB에서 Epoch별 성능 지표 조회
// 현재는 샘플 데이터 반환
List<ModelMngDto.EpochMetric> metrics = new java.util.ArrayList<>();
if (entity.getEpochCnt() != null && entity.getBestEpoch() != null) {
// 샘플 데이터 생성 (실제로는 학습 로그 파일 파싱 또는 별도 테이블 조회)
for (int i = 1; i <= Math.min(entity.getEpochCnt(), 10); i++) {
int epoch = entity.getBestEpoch() - 5 + i;
if (epoch <= 0 || epoch > entity.getEpochCnt()) {
continue;
}
metrics.add(
ModelMngDto.EpochMetric.builder()
.epoch(epoch)
.mIoU(0.80 + (Math.random() * 0.15)) // 샘플 데이터
.mFscore(0.85 + (Math.random() * 0.10)) // 샘플 데이터
.loss(0.3 - (Math.random() * 0.15)) // 샘플 데이터
.isBest(entity.getBestEpoch() != null && epoch == entity.getBestEpoch())
.build());
}
}
log.info("Epoch별 성능 지표 조회: uuid={}, metricsCount={}", uuid, metrics.size());
return metrics;
}
}