From 8c2cbc6461f3e4026ea62914f62162435e97a102 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 12:00:57 +0900 Subject: [PATCH 01/33] =?UTF-8?q?chore:=20workflow=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/mapper/WorkflowMapper.java | 10 +++++++ .../domain/workflow/model/Workflow.java | 29 +++++++++++++++++++ .../mybatis/mapper/WorkflowMapper.xml | 24 +++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java new file mode 100644 index 00000000..db03ab2e --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.workflow.mapper; + +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.workflow.model.Workflow; + +@Mapper +public interface WorkflowMapper { + Optional findById(Long id); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java new file mode 100644 index 00000000..01d32485 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java @@ -0,0 +1,29 @@ +package site.icebang.domain.workflow.model; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Workflow { + + private Long id; + private String name; + private String description; + private boolean isEnabled; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; + /** + * 워크플로우별 기본 설정값 (JSON) + */ + private String defaultConfig; + +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml new file mode 100644 index 00000000..24168ca8 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file From 0afe8e2396ac15000d11a0ad92e16d41e44b0071 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 12:44:45 +0900 Subject: [PATCH 02/33] =?UTF-8?q?chore:=20Job=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icebang/domain/job/mapper/JobMapper.java | 10 ++++++++ .../site/icebang/domain/job/model/Job.java | 21 +++++++++++++++++ .../mapping/mapper/WorkflowJobMapper.java | 10 ++++++++ .../resources/mybatis/mapper/JobMapper.xml | 23 +++++++++++++++++++ .../mybatis/mapper/WorkflowJobMapper.xml | 18 +++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml diff --git a/apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java new file mode 100644 index 00000000..538306b5 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.job.mapper; + +import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.job.model.Job; + +@Mapper +public interface JobMapper { + Optional findById(Long id); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java b/apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java new file mode 100644 index 00000000..9e2fe2bf --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java @@ -0,0 +1,21 @@ +package site.icebang.domain.job.model; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Job { + private Long id; + private String name; + private String description; + private boolean isEnabled; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java new file mode 100644 index 00000000..97f9b14c --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.mapping.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WorkflowJobMapper { + // A workflow can have multiple jobs, ordered by execution_order + List findJobIdsByWorkflowId(Long workflowId); +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml new file mode 100644 index 00000000..e2c6e6b1 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml new file mode 100644 index 00000000..fa2ed9c8 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file From 5f70300cbdc2e26e496327dcde95b3ef2d503be1 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 14:22:12 +0900 Subject: [PATCH 03/33] =?UTF-8?q?refactor:=20schedule=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => domain}/schedule/mapper/ScheduleMapper.java | 4 ++-- .../icebang/{ => domain}/schedule/model/Schedule.java | 2 +- .../schedule/runner/SchedulerInitializer.java | 8 ++++---- .../src/main/resources/mybatis/mapper/ScheduleMapper.xml | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) rename apps/user-service/src/main/java/site/icebang/{ => domain}/schedule/mapper/ScheduleMapper.java (63%) rename apps/user-service/src/main/java/site/icebang/{ => domain}/schedule/model/Schedule.java (84%) rename apps/user-service/src/main/java/site/icebang/{ => domain}/schedule/runner/SchedulerInitializer.java (78%) diff --git a/apps/user-service/src/main/java/site/icebang/schedule/mapper/ScheduleMapper.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java similarity index 63% rename from apps/user-service/src/main/java/site/icebang/schedule/mapper/ScheduleMapper.java rename to apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java index b1a92f1e..c757fc36 100644 --- a/apps/user-service/src/main/java/site/icebang/schedule/mapper/ScheduleMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java @@ -1,10 +1,10 @@ -package site.icebang.schedule.mapper; +package site.icebang.domain.schedule.mapper; import java.util.List; import org.apache.ibatis.annotations.Mapper; -import site.icebang.schedule.model.Schedule; +import site.icebang.domain.schedule.model.Schedule; @Mapper public interface ScheduleMapper { diff --git a/apps/user-service/src/main/java/site/icebang/schedule/model/Schedule.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java similarity index 84% rename from apps/user-service/src/main/java/site/icebang/schedule/model/Schedule.java rename to apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java index ced2900c..65c48366 100644 --- a/apps/user-service/src/main/java/site/icebang/schedule/model/Schedule.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java @@ -1,4 +1,4 @@ -package site.icebang.schedule.model; +package site.icebang.domain.schedule.model; import lombok.Getter; import lombok.Setter; diff --git a/apps/user-service/src/main/java/site/icebang/schedule/runner/SchedulerInitializer.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java similarity index 78% rename from apps/user-service/src/main/java/site/icebang/schedule/runner/SchedulerInitializer.java rename to apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java index ee8580dd..0dfb8b33 100644 --- a/apps/user-service/src/main/java/site/icebang/schedule/runner/SchedulerInitializer.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java @@ -1,4 +1,4 @@ -package site.icebang.schedule.runner; +package site.icebang.domain.schedule.runner; import java.util.List; @@ -9,9 +9,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import site.icebang.schedule.mapper.ScheduleMapper; -import site.icebang.schedule.model.Schedule; -import site.icebang.schedule.service.DynamicSchedulerService; +import site.icebang.domain.schedule.mapper.ScheduleMapper; +import site.icebang.domain.schedule.model.Schedule; +import site.icebang.domain.schedule.service.DynamicSchedulerService; @Slf4j @Component diff --git a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml index f9629b8a..3cdcc90e 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml @@ -1,16 +1,16 @@ - + - SELECT id AS scheduleId, workflow_id AS workflowId, cron_expression AS cronExpression, is_active AS isActive - FROM + FROM schedule - WHERE + WHERE is_active = #{isActive} From 694aa3492556164fa4dc29353556e83588238eb2 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 15:18:33 +0900 Subject: [PATCH 04/33] =?UTF-8?q?chore:=20Workflow=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EA=B4=80=EB=A0=A8=20Mapper=20=EB=B0=8F=20Service=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/workflow/dto/WorkflowCardDto.java | 13 +- .../workflow/mapper/WorkflowMapper.java | 7 + .../service/WorkflowExecutionService.java | 164 ++++++++++++++++++ .../mybatis/mapper/WorkflowMapper.xml | 17 ++ 4 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java index b54a29c0..91f1029c 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java @@ -1,6 +1,13 @@ package site.icebang.domain.workflow.dto; -import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; -@Data -public class WorkflowCardDto {} +@Getter +@NoArgsConstructor +public class WorkflowCardDto { + private Long id; + private String name; + private String description; + private boolean isEnabled; +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java index db03ab2e..152477af 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java @@ -1,10 +1,17 @@ package site.icebang.domain.workflow.mapper; +import java.util.List; import java.util.Optional; import org.apache.ibatis.annotations.Mapper; +import site.icebang.common.dto.PageParams; +import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.model.Workflow; @Mapper public interface WorkflowMapper { Optional findById(Long id); + + List selectWorkflowList(PageParams pageParams); + + int selectWorkflowCount(PageParams pageParams); } \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java new file mode 100644 index 00000000..74a5c42d --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -0,0 +1,164 @@ +package site.icebang.workflow.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import site.icebang.domain.execution.mapper.JobRunMapper; +import site.icebang.domain.execution.mapper.WorkflowRunMapper; +import site.icebang.domain.execution.model.JobRun; +import site.icebang.domain.execution.model.WorkflowRun; +import site.icebang.domain.job.mapper.JobMapper; +import site.icebang.domain.job.model.Job; +import site.icebang.domain.mapping.mapper.WorkflowJobMapper; + +@Slf4j +@Service +@RequiredArgsConstructor +public class WorkflowExecutionService { + + private final JobLauncher jobLauncher; + private final ApplicationContext applicationContext; + private final WorkflowJobMapper workflowJobMapper; + private final JobMapper jobMapper; + private final WorkflowRunMapper workflowRunMapper; + private final JobRunMapper jobRunMapper; + + /** + * 워크플로우 실행을 비동기적으로 조율합니다. + * 이 메서드 자체는 트랜잭션을 갖지 않으며, 내부적으로 호출하는 메서드들이 + * 각각 새로운 트랜잭션을 시작하여 실행 상태를 독립적으로 기록합니다. + */ + @Async + public void execute(Long workflowId, String triggerType, Long triggerId) { + log.info("Starting workflow execution for workflowId: {}, triggered by: {}", workflowId, triggerType); + + // Step 1: 워크플로우 실행을 시작하고, 그 결과를 별도의 트랜잭션에 기록합니다. + WorkflowRun workflowRun = this.initiateWorkflowExecution(workflowId, triggerType); + + try { + // Step 2: 워크플로우에 속한 Job들을 순차적으로 실행합니다. + List jobIds = workflowJobMapper.findJobIdsByWorkflowId(workflowId); + if (jobIds.isEmpty()) { + log.warn("No jobs found for workflowId: {}. Marking workflow as SUCCESS.", workflowId); + this.finalizeWorkflowExecution(workflowRun.getId(), "SUCCESS"); + return; + } + + AtomicInteger executionOrder = new AtomicInteger(1); + for (Long jobId : jobIds) { + // 각 Job의 실행과 상태 기록은 독립적인 트랜잭션으로 처리됩니다. + this.executeJobInWorkflow(jobId, workflowRun.getId(), workflowRun.getTraceId(), executionOrder.getAndIncrement()); + } + + // Step 3: 모든 Job이 성공적으로 완료되면, 워크플로우의 최종 상태를 'SUCCESS'로 기록합니다. + this.finalizeWorkflowExecution(workflowRun.getId(), "SUCCESS"); + log.info("Workflow execution successful for traceId: {}", workflowRun.getTraceId()); + + } catch (Exception e) { + // Step 4: Job 실행 중 예외가 발생하면, 워크플로우의 최종 상태를 'FAILED'로 기록합니다. + log.error("Workflow execution failed for traceId: {}. Reason: {}", workflowRun.getTraceId(), e.getMessage(), e); + this.finalizeWorkflowExecution(workflowRun.getId(), "FAILED"); + } + } + + /** + * 워크플로우 실행을 초기화하고 DB에 기록합니다. + * 항상 새로운 트랜잭션에서 실행되어, 이 단계의 성공이 보장됩니다. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public WorkflowRun initiateWorkflowExecution(Long workflowId, String triggerType) { + WorkflowRun workflowRun = WorkflowRun.builder() + .workflowId(workflowId) + .traceId(UUID.randomUUID().toString()) + .status("PENDING") + .triggerType(triggerType) + .build(); + workflowRunMapper.save(workflowRun); + + // 상태를 'RUNNING'으로 변경하고 시작 시간을 기록합니다. + workflowRun.setStartedAt(LocalDateTime.now()); + workflowRun.setStatus("RUNNING"); + workflowRunMapper.update(workflowRun); + log.debug("Initiated workflow run with traceId: {}", workflowRun.getTraceId()); + return workflowRun; + } + + /** + * 워크플로우 실행을 최종 상태(SUCCESS/FAILED)로 업데이트합니다. + * 항상 새로운 트랜잭션에서 실행되어, 실패 시에도 상태 기록이 롤백되지 않습니다. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void finalizeWorkflowExecution(Long workflowRunId, String status) { + WorkflowRun updatePayload = WorkflowRun.builder() + .id(workflowRunId) + .status(status) + .finishedAt(LocalDateTime.now()) + .build(); + workflowRunMapper.update(updatePayload); + log.debug("Finalized workflow run id: {} with status: {}", workflowRunId, status); + } + + /** + * 워크플로우 내의 단일 Job을 실행하고 그 결과를 DB에 기록합니다. + * 항상 새로운 트랜잭션에서 실행되어, 각 Job의 실행 결과가 독립적으로 커밋됩니다. + * 실패 시 예외를 던져 상위의 orchestrator가 인지하도록 합니다. + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void executeJobInWorkflow(Long jobId, Long workflowRunId, String traceId, int executionOrder) throws Exception { + Job job = jobMapper.findById(jobId) + .orElseThrow(() -> new IllegalStateException("Job not found with id: " + jobId)); + + JobRun jobRun = JobRun.builder() + .workflowRunId(workflowRunId) + .jobId(jobId) + .status("PENDING") + .executionOrder(executionOrder) + .build(); + jobRunMapper.save(jobRun); + + try { + org.springframework.batch.core.Job jobToRun = applicationContext.getBean(job.getName(), org.springframework.batch.core.Job.class); + + JobParameters jobParameters = new JobParametersBuilder() + .addString("traceId", traceId) + .addLong("workflowRunId", workflowRunId) + .addLong("jobRunId", jobRun.getId()) + .addString("runDateTime", LocalDateTime.now().toString()) + .toJobParameters(); + + jobRun.setStatus("RUNNING"); + jobRun.setStartedAt(LocalDateTime.now()); + jobRunMapper.update(jobRun); + log.info("Executing job '{}' (id:{}) for workflow traceId: {}", job.getName(), jobId, traceId); + + JobExecution jobExecution = jobLauncher.run(jobToRun, jobParameters); + + if (jobExecution.getStatus().isUnsuccessful()) { + throw new RuntimeException("Batch job '" + job.getName() + "' failed with status: " + jobExecution.getStatus()); + } + + jobRun.setStatus("SUCCESS"); + log.info("Successfully executed job '{}' (id:{})", job.getName(), jobId); + + } catch (Exception e) { + jobRun.setStatus("FAILED"); + log.error("Failed to execute job '{}' (id:{}). Reason: {}", job.getName(), jobId, e.getMessage()); + throw e; // 워크플로우 전체를 실패 처리하기 위해 예외를 다시 던집니다. + } finally { + jobRun.setFinishedAt(LocalDateTime.now()); + jobRunMapper.update(jobRun); + } + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml index 24168ca8..ae658fca 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -21,4 +21,21 @@ id = #{id} + + + + \ No newline at end of file From d121f0fc9545c8101f7515e6b42e1362caf6b650 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 15:20:00 +0900 Subject: [PATCH 05/33] =?UTF-8?q?chore:=20execution=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20mapper=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/execution/mapper/JobRunMapper.java | 10 +++++++ .../execution/mapper/WorkflowRunMapper.java | 10 +++++++ .../domain/execution/model/JobRun.java | 25 ++++++++++++++++ .../domain/execution/model/WorkflowRun.java | 30 +++++++++++++++++++ .../resources/mybatis/mapper/JobRunMapper.xml | 22 ++++++++++++++ .../mybatis/mapper/WorkflowRunMapper.xml | 22 ++++++++++++++ 6 files changed, 119 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java new file mode 100644 index 00000000..887e0bed --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.execution.mapper; + +import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.execution.model.JobRun; + +@Mapper +public interface JobRunMapper { + void save(JobRun jobRun); + void update(JobRun jobRun); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java new file mode 100644 index 00000000..76fda718 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.execution.mapper; + +import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.execution.model.WorkflowRun; + +@Mapper +public interface WorkflowRunMapper { + void save(WorkflowRun workflowRun); + void update(WorkflowRun workflowRun); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java new file mode 100644 index 00000000..5fd3ef5b --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java @@ -0,0 +1,25 @@ +package site.icebang.domain.execution.model; + +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class JobRun { + private Long id; + private Long workflowRunId; + private Long jobId; + private String status; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private Integer executionOrder; + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java new file mode 100644 index 00000000..ee38e036 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java @@ -0,0 +1,30 @@ +package site.icebang.domain.execution.model; + +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class WorkflowRun { + + private Long id; + private Long workflowId; + private String traceId; + private String runNumber; + private String status; + private String triggerType; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private Long createdBy; + private LocalDateTime createdAt; + +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml new file mode 100644 index 00000000..2617bdb4 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml @@ -0,0 +1,22 @@ + + + + + + INSERT INTO job_run (workflow_run_id, job_id, status, execution_order) + VALUES (#{workflowRunId}, #{jobId}, #{status}, #{executionOrder}) + + + + UPDATE job_run + + status = #{status}, + started_at = #{startedAt}, + finished_at = #{finishedAt}, + + WHERE id = #{id} + + + \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml new file mode 100644 index 00000000..972af123 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml @@ -0,0 +1,22 @@ + + + + + + INSERT INTO workflow_run (workflow_id, trace_id, run_number, status, trigger_type, started_at, finished_at, created_by) + VALUES (#{workflowId}, #{traceId}, #{runNumber}, #{status}, #{triggerType}, #{startedAt}, #{finishedAt}, #{createdBy}) + + + + UPDATE workflow_run + + status = #{status}, + started_at = #{startedAt}, + finished_at = #{finishedAt}, + + WHERE id = #{id} + + + \ No newline at end of file From 951c740accf35ee5c66f1ce28cd3ffa6ae19c958 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 15:34:22 +0900 Subject: [PATCH 06/33] =?UTF-8?q?chore:=20schedule=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20mapper=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/mapper/ScheduleMapper.java | 10 +- .../domain/schedule/model/Schedule.java | 24 ++- .../service/DynamicSchedulerService.java | 152 ++++++++++++++++++ .../schedule/service/ScheduleService.java | 58 +++++++ .../service/DynamicSchedulerService.java | 66 -------- .../mybatis/mapper/ScheduleMapper.xml | 58 +++++-- 6 files changed, 282 insertions(+), 86 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java delete mode 100644 apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java index c757fc36..b64730a9 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java @@ -1,12 +1,14 @@ package site.icebang.domain.schedule.mapper; import java.util.List; - +import java.util.Optional; import org.apache.ibatis.annotations.Mapper; - import site.icebang.domain.schedule.model.Schedule; @Mapper public interface ScheduleMapper { - List findAllByIsActive(boolean isActive); -} + void save(Schedule schedule); + void update(Schedule schedule); + Optional findById(Long id); + List findAllActive(); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java index 65c48366..d5d8f51a 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java @@ -1,14 +1,30 @@ package site.icebang.domain.schedule.model; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; @Getter -@Setter +@Setter // 서비스 레이어에서의 상태 변경 및 MyBatis 매핑을 위해 사용 +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor public class Schedule { - private Long scheduleId; + + private Long id; private Long workflowId; private String cronExpression; + private String parameters; // JSON format private boolean isActive; - // ... 기타 필요한 컬럼 -} + private String lastRunStatus; + private LocalDateTime lastRunAt; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; + private String scheduleText; +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java new file mode 100644 index 00000000..ace99da8 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java @@ -0,0 +1,152 @@ +//package site.icebang.domain.schedule.service; +// +//import java.time.LocalDateTime; +//import java.util.List; +//import java.util.Map; +//import java.util.concurrent.ConcurrentHashMap; +//import java.util.concurrent.ScheduledFuture; +//import org.springframework.batch.core.JobParametersBuilder; +//import org.springframework.batch.core.launch.JobLauncher; +//import org.springframework.context.ApplicationContext; +//import org.springframework.scheduling.TaskScheduler; +//import org.springframework.scheduling.support.CronTrigger; +//import org.springframework.stereotype.Service; +// +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import site.icebang.domain.job.mapper.JobMapper; +//import site.icebang.domain.job.model.Job; +//import site.icebang.domain.schedule.model.Schedule; +//import site.icebang.domain.mapping.mapper.WorkflowJobMapper; +// +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class DynamicSchedulerService { +// +// private final TaskScheduler taskScheduler; +// private final JobLauncher jobLauncher; +// private final ApplicationContext applicationContext; +// private final WorkflowJobMapper workflowJobMapper; +// private final JobMapper jobMapper; +// +// private final Map> scheduledTasks = new ConcurrentHashMap<>(); +// +// public void register(Schedule schedule) { +// // 1. 스케줄에 연결된 워크플로우에 속한 Job ID 목록을 조회합니다. +// // execution_order에 따라 정렬됩니다. +// List jobIds = workflowJobMapper.findJobIdsByWorkflowId(schedule.getWorkflowId()); +// +// if (jobIds.isEmpty()) { +// log.error("No jobs found for workflowId: {}. Cannot register scheduleId: {}", +// schedule.getWorkflowId(), schedule.getId()); +// return; +// } +// +// // TODO: 현재는 워크플로우의 첫 번째 Job만 실행하도록 구현되어 있습니다. +// // 향후 여러 Job을 순차적으로 실행하거나, 별도의 Workflow 실행 서비스를 호출하는 방식으로 확장해야 합니다. +// Long firstJobId = jobIds.get(0); +// Job job = jobMapper.findById(firstJobId) +// .orElseThrow(() -> new IllegalArgumentException("Job not found with id: " + firstJobId)); +// +// // 2. Job의 이름을 Spring Batch Job Bean 이름으로 사용하여 컨텍스트에서 Job을 찾습니다. +// String jobBeanName = job.getName(); +// org.springframework.batch.core.Job jobToRun; +// try { +// jobToRun = applicationContext.getBean(jobBeanName, org.springframework.batch.core.Job.class); +// } catch (Exception e) { +// log.error("Cannot find Spring Batch Job bean with name '{}' for scheduleId: {}", jobBeanName, schedule.getId(), e); +// return; +// } +// +// Runnable runnable = () -> { +// try { +// // 3. JobParameters에 동적인 값을 추가하여 매 실행이 고유하도록 보장합니다. +// JobParametersBuilder paramsBuilder = new JobParametersBuilder(); +// paramsBuilder.addString("runDateTime", LocalDateTime.now().toString()); +// paramsBuilder.addLong("scheduleId", schedule.getId()); +// paramsBuilder.addLong("workflowId", schedule.getWorkflowId()); +// +// jobLauncher.run(jobToRun, paramsBuilder.toJobParameters()); +// } catch (Exception e) { +// log.error("Error running scheduled job for scheduleId: {}", schedule.getId(), e); +// } +// }; +// +// CronTrigger trigger = new CronTrigger(schedule.getCronExpression()); +// ScheduledFuture future = taskScheduler.schedule(runnable, trigger); +// scheduledTasks.put(schedule.getId(), future); +// log.info(">>>> Schedule registered: id={}, jobBeanName={}, cron={}", schedule.getId(), jobBeanName, schedule.getCronExpression()); +// } +// +// public void remove(Long scheduleId) { +// ScheduledFuture future = scheduledTasks.get(scheduleId); +// if (future != null) { +// future.cancel(true); +// scheduledTasks.remove(scheduleId); +// log.info(">>>> Schedule removed: id={}", scheduleId); +// } +// } +//} + + +package site.icebang.domain.schedule.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.icebang.domain.schedule.model.Schedule; +import site.icebang.workflow.service.WorkflowExecutionService; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DynamicSchedulerService { + + private final TaskScheduler taskScheduler; + private final WorkflowExecutionService workflowExecutionService; + + private final Map> scheduledTasks = new ConcurrentHashMap<>(); + + public void register(Schedule schedule) { + // 스케줄 실행 시 WorkflowExecutionService를 호출하는 Runnable 생성 + Runnable runnable = () -> { + try { + log.debug("Triggering workflow execution for scheduleId: {}", schedule.getId()); + // 실제 워크플로우 실행은 WorkflowExecutionService에 위임 (비동기 호출) + workflowExecutionService.execute(schedule.getWorkflowId(), "SCHEDULE", schedule.getId()); + } catch (Exception e) { + // Async 예외는 기본적으로 처리되지 않으므로 여기서 로그를 남기는 것이 중요 + log.error("Failed to submit workflow execution for scheduleId: {}", schedule.getId(), e); + } + }; + + CronTrigger trigger = new CronTrigger(schedule.getCronExpression()); + ScheduledFuture future = taskScheduler.schedule(runnable, trigger); + + // 기존에 등록된 스케줄이 있다면 취소하고 새로 등록 (업데이트 지원) + ScheduledFuture oldFuture = scheduledTasks.put(schedule.getId(), future); + if (oldFuture != null) { + oldFuture.cancel(false); + } + + log.info(">>>> Schedule registered/updated: id={}, workflowId={}, cron='{}'", + schedule.getId(), schedule.getWorkflowId(), schedule.getCronExpression()); + } + + public void remove(Long scheduleId) { + ScheduledFuture future = scheduledTasks.remove(scheduleId); + if (future != null) { + future.cancel(true); // true: 실행 중인 태스크를 인터럽트 + log.info(">>>> Schedule removed: id={}", scheduleId); + } else { + log.warn(">>>> Attempted to remove a schedule that was not found in the scheduler: id={}", scheduleId); + } + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java new file mode 100644 index 00000000..1d9cde71 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java @@ -0,0 +1,58 @@ +package site.icebang.domain.schedule.service; + +import java.util.List; +import javax.annotation.PostConstruct; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.icebang.domain.schedule.model.Schedule; +import site.icebang.domain.schedule.mapper.ScheduleMapper; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ScheduleService { + + private final ScheduleMapper scheduleMapper; + private final DynamicSchedulerService dynamicSchedulerService; + + /** + * 애플리케이션 시작 시 활성화된 모든 스케줄을 초기화합니다. + * 이를 통해 서버가 재시작되어도 스케줄이 자동으로 복원됩니다. + */ + @PostConstruct + public void initializeSchedules() { + log.info("Initializing active schedules from database..."); + List activeSchedules = scheduleMapper.findAllActive(); + activeSchedules.forEach(dynamicSchedulerService::register); + log.info("{} active schedules have been initialized.", activeSchedules.size()); + } + + @Transactional + public Schedule createSchedule(Schedule schedule) { + // 1. DB에 스케줄 저장 + scheduleMapper.save(schedule); + + // 2. 메모리의 스케줄러에 동적으로 등록 + if (schedule.isActive()) { // Lombok Getter for boolean is isActive() + dynamicSchedulerService.register(schedule); + } + + return schedule; + } + + // TODO: 스케줄 수정 로직(updateSchedule) 구현이 필요합니다. + + @Transactional + public void deactivateSchedule(Long scheduleId) { + // 1. DB에서 스케줄을 비활성화 + Schedule schedule = scheduleMapper.findById(scheduleId) + .orElseThrow(() -> new IllegalArgumentException("Schedule not found with id: " + scheduleId)); + schedule.setActive(false); + scheduleMapper.update(schedule); + + // 2. 메모리의 스케줄러에서 제거 + dynamicSchedulerService.remove(scheduleId); + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java b/apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java deleted file mode 100644 index b81e30eb..00000000 --- a/apps/user-service/src/main/java/site/icebang/schedule/service/DynamicSchedulerService.java +++ /dev/null @@ -1,66 +0,0 @@ -package site.icebang.schedule.service; - -import java.time.LocalDateTime; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.context.ApplicationContext; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.support.CronTrigger; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.schedule.model.Schedule; - -@Slf4j -@Service -@RequiredArgsConstructor -public class DynamicSchedulerService { - - private final TaskScheduler taskScheduler; - private final JobLauncher jobLauncher; - private final ApplicationContext applicationContext; - private final Map> scheduledTasks = new ConcurrentHashMap<>(); - - public void register(Schedule schedule) { - // TODO: schedule.getWorkflowId()를 기반으로 실행할 Job의 이름을 DB에서 조회 - String jobName = "blogContentJob"; // 예시 - Job jobToRun = applicationContext.getBean(jobName, Job.class); - - Runnable runnable = - () -> { - try { - JobParametersBuilder paramsBuilder = new JobParametersBuilder(); - paramsBuilder.addString("runAt", LocalDateTime.now().toString()); - paramsBuilder.addLong("scheduleId", schedule.getScheduleId()); - jobLauncher.run(jobToRun, paramsBuilder.toJobParameters()); - } catch (Exception e) { - log.error( - "Failed to run scheduled job for scheduleId: {}", schedule.getScheduleId(), e); - } - }; - - CronTrigger trigger = new CronTrigger(schedule.getCronExpression()); - ScheduledFuture future = taskScheduler.schedule(runnable, trigger); - scheduledTasks.put(schedule.getScheduleId(), future); - log.info( - ">>>> Schedule registered: id={}, cron={}", - schedule.getScheduleId(), - schedule.getCronExpression()); - } - - public void remove(Long scheduleId) { - ScheduledFuture future = scheduledTasks.get(scheduleId); - if (future != null) { - future.cancel(true); - scheduledTasks.remove(scheduleId); - log.info(">>>> Schedule removed: id={}", scheduleId); - } - } -} diff --git a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml index 3cdcc90e..f1bab4cb 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml @@ -1,17 +1,51 @@ - - - + + - + SELECT * FROM schedule WHERE id = #{id} + + + \ No newline at end of file From 615dd60f3e24c8c9662e82fd53488b751ad054b5 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 15:48:17 +0900 Subject: [PATCH 07/33] =?UTF-8?q?chore:=20Java=20=ED=91=9C=EC=A4=80=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=ED=94=84=EC=82=AC=EC=9D=B4=ED=81=B4=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/user-service/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index 8aa7715a..1983ae8f 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -41,6 +41,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-validation' + // Java 표준 라이프사이클 어노테이션 (@PostConstruct, @PreDestroy) + implementation 'javax.annotation:javax.annotation-api:1.3.2' + // MyBatis implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5' From d78f884c594a7a7b61e14973616da97de34bf3b8 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 15:54:05 +0900 Subject: [PATCH 08/33] =?UTF-8?q?refactor:=20workflow=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/schedule/service/DynamicSchedulerService.java | 2 +- .../domain/workflow/service/WorkflowExecutionService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java index ace99da8..2a38b2cb 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java @@ -102,7 +102,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import site.icebang.domain.schedule.model.Schedule; -import site.icebang.workflow.service.WorkflowExecutionService; +import site.icebang.domain.workflow.service.WorkflowExecutionService; @Slf4j @Service diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java index 74a5c42d..9a07b4d3 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -1,4 +1,4 @@ -package site.icebang.workflow.service; +package site.icebang.domain.workflow.service; import java.time.LocalDateTime; import java.util.List; From fa758685777d567497bd3944f0b44a7f708e7723 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 16:02:01 +0900 Subject: [PATCH 09/33] =?UTF-8?q?refactor:=20ScheduleMapper=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icebang/domain/schedule/runner/SchedulerInitializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java index 0dfb8b33..a96d5402 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java @@ -24,7 +24,7 @@ public class SchedulerInitializer implements ApplicationRunner { @Override public void run(ApplicationArguments args) { log.info(">>>> Initializing schedules from database..."); - List activeSchedules = scheduleMapper.findAllByIsActive(true); + List activeSchedules = scheduleMapper.findAllActive(); activeSchedules.forEach(dynamicSchedulerService::register); log.info(">>>> {} active schedules have been registered.", activeSchedules.size()); } From 0f638b1cf8a3f5fd145c796a8ec4634023be140e Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 18:10:15 +0900 Subject: [PATCH 10/33] =?UTF-8?q?feature:=20Workflow=20=EC=88=98=EB=8F=99?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89(REST=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WorkflowController.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index 39077eca..59825f54 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -1,9 +1,7 @@ package site.icebang.domain.workflow.controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; @@ -11,6 +9,7 @@ import site.icebang.common.dto.PageParams; import site.icebang.common.dto.PageResult; import site.icebang.domain.workflow.dto.WorkflowCardDto; +import site.icebang.domain.workflow.service.WorkflowExecutionService; import site.icebang.domain.workflow.service.WorkflowService; @RestController @@ -18,6 +17,7 @@ @RequiredArgsConstructor public class WorkflowController { private final WorkflowService workflowService; + private final WorkflowExecutionService workflowExecutionService; @GetMapping("") public ApiResponse> getWorkflowList( @@ -25,4 +25,24 @@ public ApiResponse> getWorkflowList( PageResult result = workflowService.getPagedResult(pageParams); return ApiResponse.success(result); } + + + /** + * 지정된 ID의 워크플로우를 수동으로 실행합니다. + * + * @param workflowId 실행할 워크플로우의 ID + * @return HTTP 202 Accepted + */ + @PostMapping("/{workflowId}/execute") + public ResponseEntity executeWorkflow(@PathVariable Long workflowId) { + // TODO: Spring Security 등 인증 체계에서 실제 사용자 ID를 가져와야 합니다. + Long currentUserId = 1L; // 임시 사용자 ID + + // 워크플로우 실행 서비스 호출. 'MANUAL' 타입으로 실행을 요청합니다. + // @Async로 동작하므로, 이 호출은 즉시 반환되고 워크플로우는 백그라운드에서 실행됩니다. + workflowExecutionService.execute(workflowId, "MANUAL", currentUserId); + + // 작업이 성공적으로 접수되었음을 알리는 202 Accepted 상태를 반환합니다. + return ResponseEntity.accepted().build(); + } } From 1acd588e0b4b8bec801356e76244ad7306b12de8 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 18:37:29 +0900 Subject: [PATCH 11/33] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DynamicSchedulerService.java | 92 ------------------- 1 file changed, 92 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java index 2a38b2cb..2a35b711 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java @@ -1,95 +1,3 @@ -//package site.icebang.domain.schedule.service; -// -//import java.time.LocalDateTime; -//import java.util.List; -//import java.util.Map; -//import java.util.concurrent.ConcurrentHashMap; -//import java.util.concurrent.ScheduledFuture; -//import org.springframework.batch.core.JobParametersBuilder; -//import org.springframework.batch.core.launch.JobLauncher; -//import org.springframework.context.ApplicationContext; -//import org.springframework.scheduling.TaskScheduler; -//import org.springframework.scheduling.support.CronTrigger; -//import org.springframework.stereotype.Service; -// -//import lombok.RequiredArgsConstructor; -//import lombok.extern.slf4j.Slf4j; -//import site.icebang.domain.job.mapper.JobMapper; -//import site.icebang.domain.job.model.Job; -//import site.icebang.domain.schedule.model.Schedule; -//import site.icebang.domain.mapping.mapper.WorkflowJobMapper; -// -//@Slf4j -//@Service -//@RequiredArgsConstructor -//public class DynamicSchedulerService { -// -// private final TaskScheduler taskScheduler; -// private final JobLauncher jobLauncher; -// private final ApplicationContext applicationContext; -// private final WorkflowJobMapper workflowJobMapper; -// private final JobMapper jobMapper; -// -// private final Map> scheduledTasks = new ConcurrentHashMap<>(); -// -// public void register(Schedule schedule) { -// // 1. 스케줄에 연결된 워크플로우에 속한 Job ID 목록을 조회합니다. -// // execution_order에 따라 정렬됩니다. -// List jobIds = workflowJobMapper.findJobIdsByWorkflowId(schedule.getWorkflowId()); -// -// if (jobIds.isEmpty()) { -// log.error("No jobs found for workflowId: {}. Cannot register scheduleId: {}", -// schedule.getWorkflowId(), schedule.getId()); -// return; -// } -// -// // TODO: 현재는 워크플로우의 첫 번째 Job만 실행하도록 구현되어 있습니다. -// // 향후 여러 Job을 순차적으로 실행하거나, 별도의 Workflow 실행 서비스를 호출하는 방식으로 확장해야 합니다. -// Long firstJobId = jobIds.get(0); -// Job job = jobMapper.findById(firstJobId) -// .orElseThrow(() -> new IllegalArgumentException("Job not found with id: " + firstJobId)); -// -// // 2. Job의 이름을 Spring Batch Job Bean 이름으로 사용하여 컨텍스트에서 Job을 찾습니다. -// String jobBeanName = job.getName(); -// org.springframework.batch.core.Job jobToRun; -// try { -// jobToRun = applicationContext.getBean(jobBeanName, org.springframework.batch.core.Job.class); -// } catch (Exception e) { -// log.error("Cannot find Spring Batch Job bean with name '{}' for scheduleId: {}", jobBeanName, schedule.getId(), e); -// return; -// } -// -// Runnable runnable = () -> { -// try { -// // 3. JobParameters에 동적인 값을 추가하여 매 실행이 고유하도록 보장합니다. -// JobParametersBuilder paramsBuilder = new JobParametersBuilder(); -// paramsBuilder.addString("runDateTime", LocalDateTime.now().toString()); -// paramsBuilder.addLong("scheduleId", schedule.getId()); -// paramsBuilder.addLong("workflowId", schedule.getWorkflowId()); -// -// jobLauncher.run(jobToRun, paramsBuilder.toJobParameters()); -// } catch (Exception e) { -// log.error("Error running scheduled job for scheduleId: {}", schedule.getId(), e); -// } -// }; -// -// CronTrigger trigger = new CronTrigger(schedule.getCronExpression()); -// ScheduledFuture future = taskScheduler.schedule(runnable, trigger); -// scheduledTasks.put(schedule.getId(), future); -// log.info(">>>> Schedule registered: id={}, jobBeanName={}, cron={}", schedule.getId(), jobBeanName, schedule.getCronExpression()); -// } -// -// public void remove(Long scheduleId) { -// ScheduledFuture future = scheduledTasks.get(scheduleId); -// if (future != null) { -// future.cancel(true); -// scheduledTasks.remove(scheduleId); -// log.info(">>>> Schedule removed: id={}", scheduleId); -// } -// } -//} - - package site.icebang.domain.schedule.service; import java.util.Map; From 31ec5cc1d69900fe712ccb3a04aef54aeada54d7 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 23:34:49 +0900 Subject: [PATCH 12/33] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20Spring=20Quartz=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/user-service/build.gradle | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index 1983ae8f..ea0d5ff8 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -41,14 +41,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-validation' - // Java 표준 라이프사이클 어노테이션 (@PostConstruct, @PreDestroy) - implementation 'javax.annotation:javax.annotation-api:1.3.2' - // MyBatis implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.5' - // batch - implementation 'org.springframework.boot:spring-boot-starter-batch' + // Scheduler + implementation 'org.springframework.boot:spring-boot-starter-quartz' // Log4j2 - 모든 모듈을 2.22.1로 통일 implementation 'org.springframework.boot:spring-boot-starter-log4j2' From ac114ec0a829cbbda5363e9dd909ad651f5b3637 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 23:35:38 +0900 Subject: [PATCH 13/33] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20batch=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icebang/batch/common/JobContextKeys.java | 15 --- .../batch/job/BlogAutomationJobConfig.java | 115 ------------------ .../tasklet/CrawlSelectedProductTasklet.java | 60 --------- .../tasklet/ExtractTrendKeywordTasklet.java | 51 -------- .../tasklet/FindSimilarProductsTasklet.java | 60 --------- .../tasklet/GenerateBlogContentTasklet.java | 62 ---------- .../MatchProductWithKeywordTasklet.java | 57 --------- .../batch/tasklet/PublishBlogPostTasklet.java | 68 ----------- .../SearchProductsFromMallTasklet.java | 58 --------- 9 files changed, 546 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java delete mode 100644 apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java diff --git a/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java b/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java deleted file mode 100644 index d28b7bd0..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/common/JobContextKeys.java +++ /dev/null @@ -1,15 +0,0 @@ -package site.icebang.batch.common; - -/** - * Spring Batch의 JobExecutionContext에서 Step 간 데이터 공유를 위해 사용되는 Key들을 상수로 정의하는 인터페이스. 모든 Tasklet은 이 - * 인터페이스를 참조하여 데이터의 일관성을 유지합니다. - */ -public interface JobContextKeys { - - String EXTRACTED_KEYWORD = "extractedKeyword"; - String SEARCHED_PRODUCTS = "searchedProducts"; - String MATCHED_PRODUCTS = "matchedProducts"; - String SELECTED_PRODUCT = "selectedProduct"; - String CRAWLED_PRODUCT_DETAIL = "crawledProductDetail"; - String GENERATED_CONTENT = "generatedContent"; -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java b/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java deleted file mode 100644 index d0c934b9..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/job/BlogAutomationJobConfig.java +++ /dev/null @@ -1,115 +0,0 @@ -package site.icebang.batch.job; // 패키지 경로 수정 - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.job.builder.JobBuilder; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; - -import lombok.RequiredArgsConstructor; - -import site.icebang.batch.tasklet.*; - -/** [배치 시스템 구현] 트렌드 기반 블로그 자동화 워크플로우를 구성하는 Job들을 정의합니다. */ -@Configuration -@RequiredArgsConstructor -public class BlogAutomationJobConfig { - - // --- Tasklets --- - private final ExtractTrendKeywordTasklet extractTrendKeywordTask; - private final SearchProductsFromMallTasklet searchProductsFromMallTask; - private final MatchProductWithKeywordTasklet matchProductWithKeywordTask; - private final FindSimilarProductsTasklet findSimilarProductsTask; - private final CrawlSelectedProductTasklet crawlSelectedProductTask; - private final GenerateBlogContentTasklet generateBlogContentTask; - private final PublishBlogPostTasklet publishBlogPostTask; - - /** Job 1: 상품 선정 및 정보 수집 키워드 추출부터 최종 상품 정보 크롤링까지의 과정을 책임집니다. */ - @Bean - public Job productSelectionJob( - JobRepository jobRepository, - Step extractTrendKeywordStep, - Step searchProductsFromMallStep, - Step matchProductWithKeywordStep, - Step findSimilarProductsStep, - Step crawlSelectedProductStep) { - return new JobBuilder("productSelectionJob", jobRepository) - .start(extractTrendKeywordStep) - .next(searchProductsFromMallStep) - .next(matchProductWithKeywordStep) - .next(findSimilarProductsStep) - .next(crawlSelectedProductStep) - .build(); - } - - /** Job 2: 콘텐츠 생성 및 발행 수집된 상품 정보로 블로그 콘텐츠를 생성하고 발행합니다. */ - @Bean - public Job contentPublishingJob( - JobRepository jobRepository, Step generateBlogContentStep, Step publishBlogPostStep) { - return new JobBuilder("contentPublishingJob", jobRepository) - .start(generateBlogContentStep) - .next(publishBlogPostStep) - .build(); - } - - // --- Steps for productSelectionJob --- - @Bean - public Step extractTrendKeywordStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("extractTrendKeywordStep", jobRepository) - .tasklet(extractTrendKeywordTask, transactionManager) - .build(); - } - - @Bean - public Step searchProductsFromMallStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("searchProductsFromMallStep", jobRepository) - .tasklet(searchProductsFromMallTask, transactionManager) - .build(); - } - - @Bean - public Step matchProductWithKeywordStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("matchProductWithKeywordStep", jobRepository) - .tasklet(matchProductWithKeywordTask, transactionManager) - .build(); - } - - @Bean - public Step findSimilarProductsStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("findSimilarProductsStep", jobRepository) - .tasklet(findSimilarProductsTask, transactionManager) - .build(); - } - - @Bean - public Step crawlSelectedProductStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("crawlSelectedProductStep", jobRepository) - .tasklet(crawlSelectedProductTask, transactionManager) - .build(); - } - - // --- Steps for contentPublishingJob --- - @Bean - public Step generateBlogContentStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("generateBlogContentStep", jobRepository) - .tasklet(generateBlogContentTask, transactionManager) - .build(); - } - - @Bean - public Step publishBlogPostStep( - JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("publishBlogPostStep", jobRepository) - .tasklet(publishBlogPostTask, transactionManager) - .build(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java deleted file mode 100644 index 6a182c37..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/CrawlSelectedProductTasklet.java +++ /dev/null @@ -1,60 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.Map; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguCrawl; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguCrawl; - -@Slf4j -@Component -@RequiredArgsConstructor -public class CrawlSelectedProductTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 시작"); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - Map selectedProduct = - (Map) jobExecutionContext.get(JobContextKeys.SELECTED_PRODUCT); - - if (selectedProduct == null || !selectedProduct.containsKey("link")) { - throw new RuntimeException("크롤링할 상품 URL이 없습니다."); - } - String productUrl = (String) selectedProduct.get("link"); - - RequestSsadaguCrawl request = new RequestSsadaguCrawl(1, 1, null, "detail", productUrl); - ResponseSsadaguCrawl response = fastApiAdapter.requestProductCrawl(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 크롤링에 실패했습니다."); - } - - Map productDetail = response.productDetail(); - log.info(">>>> FastAPI로부터 크롤링된 상품 상세 정보 획득"); - - jobExecutionContext.put(JobContextKeys.CRAWLED_PRODUCT_DETAIL, productDetail); - - // log.info(">>>> [Step 5] 최종 상품 크롤링 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java deleted file mode 100644 index a35bebf9..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/ExtractTrendKeywordTasklet.java +++ /dev/null @@ -1,51 +0,0 @@ -package site.icebang.batch.tasklet; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestNaverSearch; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseNaverSearch; - -@Slf4j -@Component -@RequiredArgsConstructor -public class ExtractTrendKeywordTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 시작"); - - RequestNaverSearch request = - new RequestNaverSearch(1, 1, null, "naver", "50000000", null, null); - ResponseNaverSearch response = fastApiAdapter.requestNaverKeywordSearch(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI로부터 키워드를 추출하는 데 실패했습니다."); - } - String extractedKeyword = response.keyword(); - log.info(">>>> FastAPI로부터 추출된 키워드: {}", extractedKeyword); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - // 다른 클래스의 상수를 직접 참조하는 대신 공용 인터페이스의 키를 사용 - jobExecutionContext.put(JobContextKeys.EXTRACTED_KEYWORD, extractedKeyword); - - // log.info(">>>> [Step 1] 키워드 추출 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java deleted file mode 100644 index 316641e1..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/FindSimilarProductsTasklet.java +++ /dev/null @@ -1,60 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguSimilarity; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguSimilarity; - -@Slf4j -@Component -@RequiredArgsConstructor -public class FindSimilarProductsTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 시작"); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - List> matchedProducts = - (List>) jobExecutionContext.get(JobContextKeys.MATCHED_PRODUCTS); - List> searchResults = - (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); - - RequestSsadaguSimilarity request = - new RequestSsadaguSimilarity(1, 1, null, keyword, matchedProducts, searchResults); - ResponseSsadaguSimilarity response = fastApiAdapter.requestProductSimilarity(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 유사도 분석에 실패했습니다."); - } - - Map selectedProduct = response.selectedProduct(); - log.info(">>>> FastAPI로부터 최종 선택된 상품: {}", selectedProduct.get("title")); - - jobExecutionContext.put(JobContextKeys.SELECTED_PRODUCT, selectedProduct); - - // log.info(">>>> [Step 4] 상품 유사도 분석 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java deleted file mode 100644 index ecf44cbb..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/GenerateBlogContentTasklet.java +++ /dev/null @@ -1,62 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestBlogCreate; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseBlogCreate; - -@Slf4j -@Component -@RequiredArgsConstructor -public class GenerateBlogContentTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 시작"); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - Map productDetail = - (Map) jobExecutionContext.get(JobContextKeys.CRAWLED_PRODUCT_DETAIL); - - // TODO: productDetail을 기반으로 LLM에 전달할 프롬프트 생성 - RequestBlogCreate request = new RequestBlogCreate(1, 1, null); - ResponseBlogCreate response = fastApiAdapter.requestBlogCreation(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 블로그 콘텐츠 생성에 실패했습니다."); - } - - // TODO: 실제 생성된 콘텐츠를 response로부터 받아와야 함 (현재는 더미 데이터) - Map generatedContent = - Map.of( - "title", "엄청난 상품을 소개합니다! " + productDetail.get("title"), - "content", "이 상품은 정말... 좋습니다. 상세 정보: " + productDetail.toString(), - "tags", List.of("상품리뷰", "최고")); - log.info(">>>> FastAPI로부터 블로그 콘텐츠 생성 완료"); - - jobExecutionContext.put(JobContextKeys.GENERATED_CONTENT, generatedContent); - - // log.info(">>>> [Step 6] 블로그 콘텐츠 생성 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java deleted file mode 100644 index bdb15200..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/MatchProductWithKeywordTasklet.java +++ /dev/null @@ -1,57 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguMatch; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguMatch; - -@Slf4j -@Component -@RequiredArgsConstructor -public class MatchProductWithKeywordTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 시작"); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - List> searchResults = - (List>) jobExecutionContext.get(JobContextKeys.SEARCHED_PRODUCTS); - - RequestSsadaguMatch request = new RequestSsadaguMatch(1, 1, null, keyword, searchResults); - ResponseSsadaguMatch response = fastApiAdapter.requestProductMatch(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 매칭에 실패했습니다."); - } - - List> matchedProducts = response.matchedProducts(); - log.info(">>>> FastAPI로부터 매칭된 상품 {}개", matchedProducts.size()); - - jobExecutionContext.put(JobContextKeys.MATCHED_PRODUCTS, matchedProducts); - - log.info(">>>> [Step 3] 상품 매칭 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java deleted file mode 100644 index e1b75a18..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/PublishBlogPostTasklet.java +++ /dev/null @@ -1,68 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestBlogPublish; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseBlogPublish; - -@Slf4j -@Component -@RequiredArgsConstructor -public class PublishBlogPostTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 시작"); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - Map content = - (Map) jobExecutionContext.get(JobContextKeys.GENERATED_CONTENT); - - // TODO: UserConfig 등에서 실제 블로그 정보(ID, PW)를 가져와야 함 - String blogId = "my_blog_id"; - String blogPw = "my_blog_password"; - - RequestBlogPublish request = - new RequestBlogPublish( - 1, - 1, - null, - "naver", - blogId, - blogPw, - (String) content.get("title"), - (String) content.get("content"), - (List) content.get("tags")); - - ResponseBlogPublish response = fastApiAdapter.requestBlogPost(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 블로그 발행에 실패했습니다."); - } - - log.info(">>>> FastAPI를 통해 블로그 발행 성공: {}", response.metadata()); - - // log.info(">>>> [Step 7] 블로그 발행 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java b/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java deleted file mode 100644 index 3480f391..00000000 --- a/apps/user-service/src/main/java/site/icebang/batch/tasklet/SearchProductsFromMallTasklet.java +++ /dev/null @@ -1,58 +0,0 @@ -package site.icebang.batch.tasklet; - -import java.util.List; -import java.util.Map; - -import org.springframework.batch.core.StepContribution; -import org.springframework.batch.core.scope.context.ChunkContext; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.batch.common.JobContextKeys; -import site.icebang.external.fastapi.adapter.FastApiAdapter; -import site.icebang.external.fastapi.dto.FastApiDto.RequestSsadaguSearch; -import site.icebang.external.fastapi.dto.FastApiDto.ResponseSsadaguSearch; - -@Slf4j -@Component -@RequiredArgsConstructor -public class SearchProductsFromMallTasklet implements Tasklet { - - private final FastApiAdapter fastApiAdapter; - - @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) - throws Exception { - // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 시작"); - - ExecutionContext jobExecutionContext = getJobExecutionContext(chunkContext); - String keyword = (String) jobExecutionContext.get(JobContextKeys.EXTRACTED_KEYWORD); - - if (keyword == null) { - throw new RuntimeException("이전 Step에서 키워드를 전달받지 못했습니다."); - } - - RequestSsadaguSearch request = new RequestSsadaguSearch(1, 1, null, keyword); - ResponseSsadaguSearch response = fastApiAdapter.requestSsadaguProductSearch(request); - - if (response == null || !"200".equals(response.status())) { - throw new RuntimeException("FastAPI 상품 검색에 실패했습니다."); - } - List> searchResults = response.searchResults(); - log.info(">>>> FastAPI로부터 검색된 상품 {}개", searchResults.size()); - - jobExecutionContext.put(JobContextKeys.SEARCHED_PRODUCTS, searchResults); - - // log.info(">>>> [Step 2] 상품 검색 Tasklet 실행 완료"); - return RepeatStatus.FINISHED; - } - - private ExecutionContext getJobExecutionContext(ChunkContext chunkContext) { - return chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext(); - } -} From f388930a7b611426eaa22081fca444203428e929 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Tue, 16 Sep 2025 23:59:24 +0900 Subject: [PATCH 14/33] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20batch=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icebang/global/aop/logging/LoggingAspect.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java b/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java index b1806cff..126c7d35 100644 --- a/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java +++ b/apps/user-service/src/main/java/site/icebang/global/aop/logging/LoggingAspect.java @@ -22,9 +22,6 @@ public void serviceMethods() {} @Pointcut("execution(public * site.icebang..service..mapper..*(..))") public void repositoryMethods() {} - @Pointcut("execution(public * site.icebang.batch.tasklet..*(..))") - public void taskletMethods() {} - @Around("controllerMethods()") public Object logController(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); @@ -54,15 +51,4 @@ public Object logRepository(ProceedingJoinPoint joinPoint) throws Throwable { log.debug("[REPOSITORY] End: {} ({}ms)", joinPoint.getSignature(), duration); return result; } - - @Around("taskletMethods()") - public Object logTasklet(ProceedingJoinPoint joinPoint) throws Throwable { - long start = System.currentTimeMillis(); - // Tasklet 이름만으로도 구분이 되므로, 클래스명 + 메서드명으로 로그를 남깁니다. - log.info(">>>> [TASKLET] Start: {}", joinPoint.getSignature().toShortString()); - Object result = joinPoint.proceed(); // 실제 Tasklet의 execute() 메서드 실행 - long duration = System.currentTimeMillis() - start; - log.info("<<<< [TASKLET] End: {} ({}ms)", joinPoint.getSignature().toShortString(), duration); - return result; - } } From d972e1a709f05891aaf59e7d4b342059fc832153 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 01:12:49 +0900 Subject: [PATCH 15/33] =?UTF-8?q?fix(deprecated):=20FastApiDto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/site/icebang/external/fastapi/dto/FastApiDto.java | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java b/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java index 88ffe284..6a76b5f1 100644 --- a/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java +++ b/apps/user-service/src/main/java/site/icebang/external/fastapi/dto/FastApiDto.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; +@Deprecated /** FastAPI 서버와 통신하기 위한 DTO 클래스 모음. Java의 record를 사용하여 불변 데이터 객체를 간결하게 정의합니다. */ public final class FastApiDto { From d524ea2c352b5bd12ce1d0f00c5ea98af3d88ab6 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 01:14:08 +0900 Subject: [PATCH 16/33] =?UTF-8?q?refactor:=20Spring=20=EB=82=B4=EC=9E=A5?= =?UTF-8?q?=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/runner/SchedulerInitializer.java | 31 ---------- .../service/DynamicSchedulerService.java | 60 ------------------- .../schedule/service/ScheduleService.java | 58 ------------------ 3 files changed, 149 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java deleted file mode 100644 index a96d5402..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/runner/SchedulerInitializer.java +++ /dev/null @@ -1,31 +0,0 @@ -package site.icebang.domain.schedule.runner; - -import java.util.List; - -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -import site.icebang.domain.schedule.mapper.ScheduleMapper; -import site.icebang.domain.schedule.model.Schedule; -import site.icebang.domain.schedule.service.DynamicSchedulerService; - -@Slf4j -@Component -@RequiredArgsConstructor -public class SchedulerInitializer implements ApplicationRunner { - - private final ScheduleMapper scheduleMapper; - private final DynamicSchedulerService dynamicSchedulerService; - - @Override - public void run(ApplicationArguments args) { - log.info(">>>> Initializing schedules from database..."); - List activeSchedules = scheduleMapper.findAllActive(); - activeSchedules.forEach(dynamicSchedulerService::register); - log.info(">>>> {} active schedules have been registered.", activeSchedules.size()); - } -} diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java deleted file mode 100644 index 2a35b711..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/DynamicSchedulerService.java +++ /dev/null @@ -1,60 +0,0 @@ -package site.icebang.domain.schedule.service; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledFuture; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.support.CronTrigger; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import site.icebang.domain.schedule.model.Schedule; -import site.icebang.domain.workflow.service.WorkflowExecutionService; - -@Slf4j -@Service -@RequiredArgsConstructor -public class DynamicSchedulerService { - - private final TaskScheduler taskScheduler; - private final WorkflowExecutionService workflowExecutionService; - - private final Map> scheduledTasks = new ConcurrentHashMap<>(); - - public void register(Schedule schedule) { - // 스케줄 실행 시 WorkflowExecutionService를 호출하는 Runnable 생성 - Runnable runnable = () -> { - try { - log.debug("Triggering workflow execution for scheduleId: {}", schedule.getId()); - // 실제 워크플로우 실행은 WorkflowExecutionService에 위임 (비동기 호출) - workflowExecutionService.execute(schedule.getWorkflowId(), "SCHEDULE", schedule.getId()); - } catch (Exception e) { - // Async 예외는 기본적으로 처리되지 않으므로 여기서 로그를 남기는 것이 중요 - log.error("Failed to submit workflow execution for scheduleId: {}", schedule.getId(), e); - } - }; - - CronTrigger trigger = new CronTrigger(schedule.getCronExpression()); - ScheduledFuture future = taskScheduler.schedule(runnable, trigger); - - // 기존에 등록된 스케줄이 있다면 취소하고 새로 등록 (업데이트 지원) - ScheduledFuture oldFuture = scheduledTasks.put(schedule.getId(), future); - if (oldFuture != null) { - oldFuture.cancel(false); - } - - log.info(">>>> Schedule registered/updated: id={}, workflowId={}, cron='{}'", - schedule.getId(), schedule.getWorkflowId(), schedule.getCronExpression()); - } - - public void remove(Long scheduleId) { - ScheduledFuture future = scheduledTasks.remove(scheduleId); - if (future != null) { - future.cancel(true); // true: 실행 중인 태스크를 인터럽트 - log.info(">>>> Schedule removed: id={}", scheduleId); - } else { - log.warn(">>>> Attempted to remove a schedule that was not found in the scheduler: id={}", scheduleId); - } - } -} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java deleted file mode 100644 index 1d9cde71..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/ScheduleService.java +++ /dev/null @@ -1,58 +0,0 @@ -package site.icebang.domain.schedule.service; - -import java.util.List; -import javax.annotation.PostConstruct; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import site.icebang.domain.schedule.model.Schedule; -import site.icebang.domain.schedule.mapper.ScheduleMapper; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ScheduleService { - - private final ScheduleMapper scheduleMapper; - private final DynamicSchedulerService dynamicSchedulerService; - - /** - * 애플리케이션 시작 시 활성화된 모든 스케줄을 초기화합니다. - * 이를 통해 서버가 재시작되어도 스케줄이 자동으로 복원됩니다. - */ - @PostConstruct - public void initializeSchedules() { - log.info("Initializing active schedules from database..."); - List activeSchedules = scheduleMapper.findAllActive(); - activeSchedules.forEach(dynamicSchedulerService::register); - log.info("{} active schedules have been initialized.", activeSchedules.size()); - } - - @Transactional - public Schedule createSchedule(Schedule schedule) { - // 1. DB에 스케줄 저장 - scheduleMapper.save(schedule); - - // 2. 메모리의 스케줄러에 동적으로 등록 - if (schedule.isActive()) { // Lombok Getter for boolean is isActive() - dynamicSchedulerService.register(schedule); - } - - return schedule; - } - - // TODO: 스케줄 수정 로직(updateSchedule) 구현이 필요합니다. - - @Transactional - public void deactivateSchedule(Long scheduleId) { - // 1. DB에서 스케줄을 비활성화 - Schedule schedule = scheduleMapper.findById(scheduleId) - .orElseThrow(() -> new IllegalArgumentException("Schedule not found with id: " + scheduleId)); - schedule.setActive(false); - scheduleMapper.update(schedule); - - // 2. 메모리의 스케줄러에서 제거 - dynamicSchedulerService.remove(scheduleId); - } -} \ No newline at end of file From 0a7e1e6fca821ea6ffbf71b7bc8ae3e7ebf71c3a Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 01:17:01 +0900 Subject: [PATCH 17/33] =?UTF-8?q?refactor:=20Spring=20=EB=82=B4=EC=9E=A5?= =?UTF-8?q?=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/scheduler/SchedulerConfig.java | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/global/config/scheduler/SchedulerConfig.java diff --git a/apps/user-service/src/main/java/site/icebang/global/config/scheduler/SchedulerConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/scheduler/SchedulerConfig.java deleted file mode 100644 index 79fc6436..00000000 --- a/apps/user-service/src/main/java/site/icebang/global/config/scheduler/SchedulerConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -package site.icebang.global.config.scheduler; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; - -/** 동적 스케줄링을 위한 TaskScheduler Bean을 설정하는 클래스 */ -@Configuration -public class SchedulerConfig { - - @Bean - public TaskScheduler taskScheduler() { - // ThreadPool 기반의 TaskScheduler를 생성합니다. - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - - // 스케줄러가 사용할 스레드 풀의 크기를 설정합니다. - // 동시에 실행될 수 있는 스케줄 작업의 최대 개수입니다. - scheduler.setPoolSize(10); - - // 스레드 이름의 접두사를 설정하여 로그 추적을 용이하게 합니다. - scheduler.setThreadNamePrefix("dynamic-scheduler-"); - - // 스케줄러를 초기화합니다. - scheduler.initialize(); - return scheduler; - } -} From 37428d28011f389d5bbf2fae31ffb1ac0f8e13bd Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 01:41:10 +0900 Subject: [PATCH 18/33] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20WorkflowJobMapper=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapping/mapper/WorkflowJobMapper.java | 10 ---------- .../mybatis/mapper/WorkflowJobMapper.xml | 18 ------------------ 2 files changed, 28 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java delete mode 100644 apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml diff --git a/apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java deleted file mode 100644 index 97f9b14c..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/mapping/mapper/WorkflowJobMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package site.icebang.domain.mapping.mapper; - -import java.util.List; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface WorkflowJobMapper { - // A workflow can have multiple jobs, ordered by execution_order - List findJobIdsByWorkflowId(Long workflowId); -} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml deleted file mode 100644 index fa2ed9c8..00000000 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowJobMapper.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - \ No newline at end of file From ca39443da0c91af4cb34334c2fb7a4092cea9b2d Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:30:53 +0900 Subject: [PATCH 19/33] =?UTF-8?q?chore:=20job=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icebang/domain/job/mapper/JobMapper.java | 10 ------ .../domain/workflow/mapper/JobMapper.java | 12 +++++++ .../domain/{job => workflow}/model/Job.java | 2 +- .../resources/mybatis/mapper/JobMapper.xml | 35 +++++++++---------- 4 files changed, 29 insertions(+), 30 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java rename apps/user-service/src/main/java/site/icebang/domain/{job => workflow}/model/Job.java (91%) diff --git a/apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java deleted file mode 100644 index 538306b5..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/job/mapper/JobMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package site.icebang.domain.job.mapper; - -import java.util.Optional; -import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.job.model.Job; - -@Mapper -public interface JobMapper { - Optional findById(Long id); -} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java new file mode 100644 index 00000000..e85fcb1e --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java @@ -0,0 +1,12 @@ +package site.icebang.domain.workflow.mapper; + +import site.icebang.domain.workflow.model.Job; +import site.icebang.domain.workflow.model.Task; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface JobMapper { + List findJobsByWorkflowId(Long workflowId); + List findTasksByJobId(Long jobId); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java similarity index 91% rename from apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java rename to apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java index 9e2fe2bf..4aae43c8 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/job/model/Job.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java @@ -1,4 +1,4 @@ -package site.icebang.domain.job.model; +package site.icebang.domain.workflow.model; import java.time.LocalDateTime; import lombok.AccessLevel; diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml index e2c6e6b1..0f530645 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml @@ -1,23 +1,20 @@ - - - + + + + + - + SELECT j.* FROM job j + JOIN workflow_job wj ON j.id = wj.job_id + WHERE wj.workflow_id = #{workflowId} + ORDER BY wj.execution_order ASC + \ No newline at end of file From b9b4245dda14c24d9ade2121db1874e6fd82c340 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:31:27 +0900 Subject: [PATCH 20/33] =?UTF-8?q?chore:=20JobRun=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/execution/mapper/JobRunMapper.java | 10 ----- .../domain/execution/model/JobRun.java | 25 ----------- .../mapper/execution/JobRunMapper.java | 10 +++++ .../workflow/model/execution/JobRun.java | 41 +++++++++++++++++++ .../resources/mybatis/mapper/JobRunMapper.xml | 30 ++++++++------ 5 files changed, 69 insertions(+), 47 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java deleted file mode 100644 index 887e0bed..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package site.icebang.domain.execution.mapper; - -import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.execution.model.JobRun; - -@Mapper -public interface JobRunMapper { - void save(JobRun jobRun); - void update(JobRun jobRun); -} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java deleted file mode 100644 index 5fd3ef5b..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java +++ /dev/null @@ -1,25 +0,0 @@ -package site.icebang.domain.execution.model; - -import java.time.LocalDateTime; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class JobRun { - private Long id; - private Long workflowRunId; - private Long jobId; - private String status; - private LocalDateTime startedAt; - private LocalDateTime finishedAt; - private Integer executionOrder; - private LocalDateTime createdAt; -} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java new file mode 100644 index 00000000..dda90557 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.workflow.mapper.execution; + +import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.workflow.model.execution.JobRun; + +@Mapper +public interface JobRunMapper { + void insert(JobRun jobRun); + void update(JobRun jobRun); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java new file mode 100644 index 00000000..651f1280 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java @@ -0,0 +1,41 @@ +package site.icebang.domain.workflow.model.execution; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +public class JobRun { + + private Long id; + private Long workflowRunId; + private Long jobId; + private String status; // PENDING, RUNNING, SUCCESS, FAILED + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createdAt; + + private JobRun(Long workflowRunId, Long jobId) { + this.workflowRunId = workflowRunId; + this.jobId = jobId; + this.status = "RUNNING"; + this.startedAt = LocalDateTime.now(); + this.createdAt = this.startedAt; + } + + /** + * Job 실행 시작을 위한 정적 팩토리 메소드 + */ + public static JobRun start(Long workflowRunId, Long jobId) { + return new JobRun(workflowRunId, jobId); + } + + /** + * Job 실행 완료 처리 + */ + public void finish(String status) { + this.status = status; + this.finishedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml index 2617bdb4..f9d140ae 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml @@ -1,21 +1,27 @@ - - + - - INSERT INTO job_run (workflow_run_id, job_id, status, execution_order) - VALUES (#{workflowRunId}, #{jobId}, #{status}, #{executionOrder}) + + + + + + + + + + + + + + INSERT INTO job_run (workflow_run_id, job_id, status, started_at, created_at) + VALUES (#{workflowRunId}, #{jobId}, #{status}, #{startedAt}, #{createdAt}) UPDATE job_run - - status = #{status}, - started_at = #{startedAt}, - finished_at = #{finishedAt}, - + SET status = #{status}, + finished_at = #{finishedAt} WHERE id = #{id} From 75a12771265b5ffe00b3eb18ff8442f75ef06f7b Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:32:32 +0900 Subject: [PATCH 21/33] =?UTF-8?q?chore:=20Schedule=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/mapper/ScheduleMapper.java | 6 +-- .../service/QuartzScheduleService.java | 43 ++++++++++++++++ .../mybatis/mapper/ScheduleMapper.xml | 49 ++----------------- 3 files changed, 47 insertions(+), 51 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java index b64730a9..2b68239b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java @@ -1,14 +1,10 @@ package site.icebang.domain.schedule.mapper; -import java.util.List; -import java.util.Optional; import org.apache.ibatis.annotations.Mapper; import site.icebang.domain.schedule.model.Schedule; +import java.util.List; @Mapper public interface ScheduleMapper { - void save(Schedule schedule); - void update(Schedule schedule); - Optional findById(Long id); List findAllActive(); } \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java new file mode 100644 index 00000000..3a5f1aef --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/service/QuartzScheduleService.java @@ -0,0 +1,43 @@ +package site.icebang.domain.schedule.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.stereotype.Service; +import site.icebang.domain.schedule.model.Schedule; +import site.icebang.domain.workflow.scheduler.WorkflowTriggerJob; + +@Slf4j +@Service +@RequiredArgsConstructor +public class QuartzScheduleService { + + private final Scheduler scheduler; + + public void addOrUpdateSchedule(Schedule schedule) { + JobKey jobKey = JobKey.jobKey("workflow-" + schedule.getWorkflowId()); + JobDetail jobDetail = JobBuilder.newJob(WorkflowTriggerJob.class) + .withIdentity(jobKey) + .withDescription("Workflow " + schedule.getWorkflowId() + " Trigger Job") + .usingJobData("workflowId", schedule.getWorkflowId()) + .storeDurably() + .build(); + + TriggerKey triggerKey = TriggerKey.triggerKey("trigger-for-workflow-" + schedule.getWorkflowId()); + Trigger trigger = TriggerBuilder.newTrigger() + .forJob(jobDetail) + .withIdentity(triggerKey) + .withSchedule(CronScheduleBuilder.cronSchedule(schedule.getCronExpression())) + .build(); + try { + scheduler.scheduleJob(jobDetail, trigger); + log.info("Quartz 스케줄 등록/업데이트 완료: Workflow ID {}", schedule.getWorkflowId()); + } catch (SchedulerException e) { + log.error("Quartz 스케줄 등록 실패", e); + } + } + + public void deleteSchedule(Long workflowId) { + // ... (삭제 로직) + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml index f1bab4cb..00b2097c 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/ScheduleMapper.xml @@ -1,51 +1,8 @@ - - - - - - - - - - - - - - - - + - - INSERT INTO schedule (workflow_id, cron_expression, parameters, is_active, schedule_text, created_by, updated_by) - VALUES (#{workflowId}, #{cronExpression}, #{parameters}, #{isActive}, #{scheduleText}, #{createdBy}, #{updatedBy}) - - - - UPDATE schedule - - workflow_id = #{workflowId}, - cron_expression = #{cronExpression}, - parameters = #{parameters}, - is_active = #{isActive}, - last_run_status = #{lastRunStatus}, - last_run_at = #{lastRunAt}, - updated_by = #{updatedBy}, - schedule_text = #{scheduleText}, - updated_at = CURRENT_TIMESTAMP - - WHERE id = #{id} - - - + SELECT * FROM schedule WHERE is_active = true - - - \ No newline at end of file From 9dd2f92ef6dfe70d835cb728d7796dad6671c4b6 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:33:34 +0900 Subject: [PATCH 22/33] =?UTF-8?q?chore:=20Task=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/workflow/mapper/TaskMapper.java | 10 +++++++ .../icebang/domain/workflow/model/Task.java | 26 +++++++++++++++++++ .../resources/mybatis/mapper/TaskMapper.xml | 18 +++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/TaskMapper.xml diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java new file mode 100644 index 00000000..c812c649 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.workflow.mapper; + +import site.icebang.domain.workflow.model.Task; +import org.apache.ibatis.annotations.Mapper; +import java.util.Optional; + +@Mapper +public interface TaskMapper { + Optional findById(Long id); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java new file mode 100644 index 00000000..188865ce --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java @@ -0,0 +1,26 @@ +package site.icebang.domain.workflow.model; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor // MyBatis가 객체를 생성하기 위해 필요 +public class Task { + + private Long id; + private String name; + + /** + * Task의 타입 (예: "HTTP", "SPRING_BATCH") + * 이 타입에 따라 TaskRunner가 선택됩니다. + */ + private String type; + + /** + * Task 실행에 필요한 파라미터 (JSON) + * 예: {"url": "http://...", "method": "POST", "body": {...}} + */ + private JsonNode parameters; + +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/TaskMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/TaskMapper.xml new file mode 100644 index 00000000..849ba9e5 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/TaskMapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file From 144cf03c7035e52e2ef5f06425606bc40de6f3fb Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:34:01 +0900 Subject: [PATCH 23/33] =?UTF-8?q?chore:=20TaskRun=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/execution/TaskRunMapper.java | 10 ++++ .../workflow/model/execution/TaskRun.java | 46 +++++++++++++++++++ .../mybatis/mapper/TaskRunMapper.xml | 14 ++++++ 3 files changed, 70 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java create mode 100644 apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java new file mode 100644 index 00000000..3ccab2a2 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.workflow.mapper.execution; + +import site.icebang.domain.workflow.model.execution.TaskRun; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TaskRunMapper { + void insert(TaskRun taskRun); + void update(TaskRun taskRun); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java new file mode 100644 index 00000000..f1ad15a6 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java @@ -0,0 +1,46 @@ +package site.icebang.domain.workflow.model.execution; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +public class TaskRun { + + private Long id; + private Long jobRunId; + private Long taskId; + private String status; // PENDING, RUNNING, SUCCESS, FAILED + private String resultMessage; // 실행 결과 메시지 + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createdAt; + + // 생성자나 정적 팩토리 메서드를 통해 객체 생성 로직을 관리 + private TaskRun(Long jobRunId, Long taskId) { + this.jobRunId = jobRunId; + this.taskId = taskId; + this.status = "PENDING"; + this.createdAt = LocalDateTime.now(); + } + + /** + * Task 실행 시작을 위한 정적 팩토리 메서드 + */ + public static TaskRun start(Long jobRunId, Long taskId) { + TaskRun taskRun = new TaskRun(jobRunId, taskId); + taskRun.status = "RUNNING"; + taskRun.startedAt = LocalDateTime.now(); + return taskRun; + } + + /** + * Task 실행 완료 처리 + */ + public void finish(String status, String resultMessage) { + this.status = status; + this.resultMessage = resultMessage; + this.finishedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml new file mode 100644 index 00000000..17214fd6 --- /dev/null +++ b/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml @@ -0,0 +1,14 @@ + + + INSERT INTO task_run (job_run_id, task_id, status, started_at, created_at) + VALUES (#{jobRunId}, #{taskId}, #{status}, #{startedAt}, #{createdAt}) + + + + UPDATE task_run + SET status = #{status}, + finished_at = #{finishedAt}, + result_message = #{resultMessage} + WHERE id = #{id} + + \ No newline at end of file From 614026f8a7a4ef2531f488df8ede4032b600cfd3 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:34:44 +0900 Subject: [PATCH 24/33] =?UTF-8?q?chore:=20WorkflowRun=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execution/mapper/WorkflowRunMapper.java | 10 ----- .../domain/execution/model/WorkflowRun.java | 30 ------------- .../mapper/execution/WorkflowRunMapper.java | 10 +++++ .../workflow/model/execution/WorkflowRun.java | 42 +++++++++++++++++++ .../mybatis/mapper/WorkflowRunMapper.xml | 30 +++++++------ 5 files changed, 70 insertions(+), 52 deletions(-) delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java delete mode 100644 apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java deleted file mode 100644 index 76fda718..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package site.icebang.domain.execution.mapper; - -import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.execution.model.WorkflowRun; - -@Mapper -public interface WorkflowRunMapper { - void save(WorkflowRun workflowRun); - void update(WorkflowRun workflowRun); -} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java deleted file mode 100644 index ee38e036..00000000 --- a/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java +++ /dev/null @@ -1,30 +0,0 @@ -package site.icebang.domain.execution.model; - -import java.time.LocalDateTime; -import java.util.UUID; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class WorkflowRun { - - private Long id; - private Long workflowId; - private String traceId; - private String runNumber; - private String status; - private String triggerType; - private LocalDateTime startedAt; - private LocalDateTime finishedAt; - private Long createdBy; - private LocalDateTime createdAt; - -} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java new file mode 100644 index 00000000..5d83687f --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java @@ -0,0 +1,10 @@ +package site.icebang.domain.workflow.mapper.execution; + +import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.workflow.model.execution.WorkflowRun; + +@Mapper +public interface WorkflowRunMapper { + void insert(WorkflowRun workflowRun); + void update(WorkflowRun workflowRun); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java new file mode 100644 index 00000000..2f37e03e --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java @@ -0,0 +1,42 @@ +package site.icebang.domain.workflow.model.execution; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@NoArgsConstructor +public class WorkflowRun { + + private Long id; + private Long workflowId; + private String traceId; // 분산 추적을 위한 ID + private String status; // PENDING, RUNNING, SUCCESS, FAILED + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createdAt; + + private WorkflowRun(Long workflowId) { + this.workflowId = workflowId; + this.traceId = UUID.randomUUID().toString(); // 고유 추적 ID 생성 + this.status = "RUNNING"; + this.startedAt = LocalDateTime.now(); + this.createdAt = this.startedAt; + } + + /** + * 워크플로우 실행 시작을 위한 정적 팩토리 메소드 + */ + public static WorkflowRun start(Long workflowId) { + return new WorkflowRun(workflowId); + } + + /** + * 워크플로우 실행 완료 처리 + */ + public void finish(String status) { + this.status = status; + this.finishedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml index 972af123..bfd7faa0 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml @@ -1,21 +1,27 @@ - - + - - INSERT INTO workflow_run (workflow_id, trace_id, run_number, status, trigger_type, started_at, finished_at, created_by) - VALUES (#{workflowId}, #{traceId}, #{runNumber}, #{status}, #{triggerType}, #{startedAt}, #{finishedAt}, #{createdBy}) + + + + + + + + + + + + + + INSERT INTO workflow_run (workflow_id, trace_id, status, started_at, created_at) + VALUES (#{workflowId}, #{traceId}, #{status}, #{startedAt}, #{createdAt}) UPDATE workflow_run - - status = #{status}, - started_at = #{startedAt}, - finished_at = #{finishedAt}, - + SET status = #{status}, + finished_at = #{finishedAt} WHERE id = #{id} From cd13732f93ce202c05d774f3c22ac64c69582949 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:35:34 +0900 Subject: [PATCH 25/33] =?UTF-8?q?chore:=20Workflow=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WorkflowController.java | 22 +- .../workflow/runner/HttpTaskRunner.java | 40 ++++ .../domain/workflow/runner/TaskRunner.java | 14 ++ .../scheduler/WorkflowTriggerJob.java | 22 ++ .../service/WorkflowExecutionService.java | 202 +++++++----------- 5 files changed, 153 insertions(+), 147 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java create mode 100644 apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index 59825f54..f4dc51c2 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -11,6 +11,7 @@ import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.service.WorkflowExecutionService; import site.icebang.domain.workflow.service.WorkflowService; +import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/v0/workflows") @@ -26,23 +27,10 @@ public ApiResponse> getWorkflowList( return ApiResponse.success(result); } - - /** - * 지정된 ID의 워크플로우를 수동으로 실행합니다. - * - * @param workflowId 실행할 워크플로우의 ID - * @return HTTP 202 Accepted - */ - @PostMapping("/{workflowId}/execute") - public ResponseEntity executeWorkflow(@PathVariable Long workflowId) { - // TODO: Spring Security 등 인증 체계에서 실제 사용자 ID를 가져와야 합니다. - Long currentUserId = 1L; // 임시 사용자 ID - - // 워크플로우 실행 서비스 호출. 'MANUAL' 타입으로 실행을 요청합니다. - // @Async로 동작하므로, 이 호출은 즉시 반환되고 워크플로우는 백그라운드에서 실행됩니다. - workflowExecutionService.execute(workflowId, "MANUAL", currentUserId); - - // 작업이 성공적으로 접수되었음을 알리는 202 Accepted 상태를 반환합니다. + @PostMapping("/{workflowId}/run") + public ResponseEntity runWorkflow(@PathVariable Long workflowId) { + // HTTP 요청/응답 스레드를 블로킹하지 않도록 비동기 실행 + CompletableFuture.runAsync(() -> workflowExecutionService.executeWorkflow(workflowId)); return ResponseEntity.accepted().build(); } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java new file mode 100644 index 00000000..83847997 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java @@ -0,0 +1,40 @@ +package site.icebang.domain.workflow.runner; + +import site.icebang.domain.workflow.model.Task; +import site.icebang.domain.workflow.model.execution.TaskRun; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Component("httpTaskRunner") +@RequiredArgsConstructor +public class HttpTaskRunner implements TaskRunner { + private final RestTemplate restTemplate; + + @Override + public TaskExecutionResult execute(Task task, TaskRun taskRun) { + JsonNode params = task.getParameters(); + String url = params.get("url").asText(); + String method = params.get("method").asText(); + JsonNode body = params.get("body"); + + try { + HttpEntity requestEntity = new HttpEntity<>(body.toString(), new HttpHeaders() {{ + setContentType(MediaType.APPLICATION_JSON); + }}); + + ResponseEntity response = restTemplate.exchange( + url, HttpMethod.valueOf(method.toUpperCase()), requestEntity, String.class); + + return TaskExecutionResult.success(response.getBody()); + } catch (RestClientException e) { + log.error("HTTP Task 실행 실패: TaskRunId={}, Error={}", taskRun.getId(), e.getMessage()); + return TaskExecutionResult.failure(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java new file mode 100644 index 00000000..f039a14c --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java @@ -0,0 +1,14 @@ +package site.icebang.domain.workflow.runner; + +import site.icebang.domain.workflow.model.Task; +import site.icebang.domain.workflow.model.execution.TaskRun; + +public interface TaskRunner { + record TaskExecutionResult(String status, String message) { + public static TaskExecutionResult success(String message) { return new TaskExecutionResult("SUCCESS", message); } + public static TaskExecutionResult failure(String message) { return new TaskExecutionResult("FAILED", message); } + public boolean isFailure() { return "FAILED".equals(status); } + } + + TaskExecutionResult execute(Task task, TaskRun taskRun); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java new file mode 100644 index 00000000..92b7264d --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java @@ -0,0 +1,22 @@ +package site.icebang.domain.workflow.scheduler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.springframework.scheduling.quartz.QuartzJobBean; +import org.springframework.stereotype.Component; +import site.icebang.domain.workflow.service.WorkflowExecutionService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class WorkflowTriggerJob extends QuartzJobBean { + private final WorkflowExecutionService workflowExecutionService; + + @Override + protected void executeInternal(JobExecutionContext context) { + Long workflowId = context.getJobDetail().getJobDataMap().getLong("workflowId"); + log.info("Quartz가 WorkflowTriggerJob을 실행합니다. WorkflowId={}", workflowId); + workflowExecutionService.executeWorkflow(workflowId); + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java index 9a07b4d3..472c10df 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -1,164 +1,106 @@ package site.icebang.domain.workflow.service; -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.context.ApplicationContext; -import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import site.icebang.domain.execution.mapper.JobRunMapper; -import site.icebang.domain.execution.mapper.WorkflowRunMapper; -import site.icebang.domain.execution.model.JobRun; -import site.icebang.domain.execution.model.WorkflowRun; -import site.icebang.domain.job.mapper.JobMapper; -import site.icebang.domain.job.model.Job; -import site.icebang.domain.mapping.mapper.WorkflowJobMapper; +import site.icebang.domain.workflow.mapper.JobMapper; +import site.icebang.domain.workflow.mapper.execution.JobRunMapper; +import site.icebang.domain.workflow.mapper.execution.TaskRunMapper; +import site.icebang.domain.workflow.mapper.execution.WorkflowRunMapper; +import site.icebang.domain.workflow.model.Job; +import site.icebang.domain.workflow.model.Task; +import site.icebang.domain.workflow.model.execution.JobRun; +import site.icebang.domain.workflow.model.execution.TaskRun; +import site.icebang.domain.workflow.model.execution.WorkflowRun; +import site.icebang.domain.workflow.runner.TaskRunner; + +import java.util.List; +import java.util.Map; @Slf4j @Service @RequiredArgsConstructor public class WorkflowExecutionService { - private final JobLauncher jobLauncher; - private final ApplicationContext applicationContext; - private final WorkflowJobMapper workflowJobMapper; private final JobMapper jobMapper; private final WorkflowRunMapper workflowRunMapper; private final JobRunMapper jobRunMapper; + private final TaskRunMapper taskRunMapper; + private final Map taskRunners; /** - * 워크플로우 실행을 비동기적으로 조율합니다. - * 이 메서드 자체는 트랜잭션을 갖지 않으며, 내부적으로 호출하는 메서드들이 - * 각각 새로운 트랜잭션을 시작하여 실행 상태를 독립적으로 기록합니다. + * 워크플로우 실행의 시작점. 전체 과정은 하나의 트랜잭션으로 묶입니다. + * @param workflowId 실행할 워크플로우의 ID */ - @Async - public void execute(Long workflowId, String triggerType, Long triggerId) { - log.info("Starting workflow execution for workflowId: {}, triggered by: {}", workflowId, triggerType); - - // Step 1: 워크플로우 실행을 시작하고, 그 결과를 별도의 트랜잭션에 기록합니다. - WorkflowRun workflowRun = this.initiateWorkflowExecution(workflowId, triggerType); - - try { - // Step 2: 워크플로우에 속한 Job들을 순차적으로 실행합니다. - List jobIds = workflowJobMapper.findJobIdsByWorkflowId(workflowId); - if (jobIds.isEmpty()) { - log.warn("No jobs found for workflowId: {}. Marking workflow as SUCCESS.", workflowId); - this.finalizeWorkflowExecution(workflowRun.getId(), "SUCCESS"); - return; - } + @Transactional + public void executeWorkflow(Long workflowId) { + log.info("========== 워크플로우 실행 시작: WorkflowId={} ==========", workflowId); + WorkflowRun workflowRun = WorkflowRun.start(workflowId); + workflowRunMapper.insert(workflowRun); - AtomicInteger executionOrder = new AtomicInteger(1); - for (Long jobId : jobIds) { - // 각 Job의 실행과 상태 기록은 독립적인 트랜잭션으로 처리됩니다. - this.executeJobInWorkflow(jobId, workflowRun.getId(), workflowRun.getTraceId(), executionOrder.getAndIncrement()); - } + List jobs = jobMapper.findJobsByWorkflowId(workflowId); + log.info("총 {}개의 Job을 순차적으로 실행합니다.", jobs.size()); + + for (Job job : jobs) { + JobRun jobRun = JobRun.start(workflowRun.getId(), job.getId()); + jobRunMapper.insert(jobRun); + log.info("---------- Job 실행 시작: JobId={}, JobRunId={} ----------", job.getId(), jobRun.getId()); - // Step 3: 모든 Job이 성공적으로 완료되면, 워크플로우의 최종 상태를 'SUCCESS'로 기록합니다. - this.finalizeWorkflowExecution(workflowRun.getId(), "SUCCESS"); - log.info("Workflow execution successful for traceId: {}", workflowRun.getTraceId()); + boolean jobSucceeded = executeTasksForJob(jobRun); - } catch (Exception e) { - // Step 4: Job 실행 중 예외가 발생하면, 워크플로우의 최종 상태를 'FAILED'로 기록합니다. - log.error("Workflow execution failed for traceId: {}. Reason: {}", workflowRun.getTraceId(), e.getMessage(), e); - this.finalizeWorkflowExecution(workflowRun.getId(), "FAILED"); + jobRun.finish(jobSucceeded ? "SUCCESS" : "FAILED"); + jobRunMapper.update(jobRun); + + if (!jobSucceeded) { + workflowRun.finish("FAILED"); + workflowRunMapper.update(workflowRun); + log.error("Job 실패로 인해 워크플로우 실행을 중단합니다: WorkflowRunId={}", workflowRun.getId()); + return; // Job이 실패하면 전체 워크플로우를 중단 + } + log.info("---------- Job 실행 성공: JobRunId={} ----------", jobRun.getId()); } - } - /** - * 워크플로우 실행을 초기화하고 DB에 기록합니다. - * 항상 새로운 트랜잭션에서 실행되어, 이 단계의 성공이 보장됩니다. - */ - @Transactional(propagation = Propagation.REQUIRES_NEW) - public WorkflowRun initiateWorkflowExecution(Long workflowId, String triggerType) { - WorkflowRun workflowRun = WorkflowRun.builder() - .workflowId(workflowId) - .traceId(UUID.randomUUID().toString()) - .status("PENDING") - .triggerType(triggerType) - .build(); - workflowRunMapper.save(workflowRun); - - // 상태를 'RUNNING'으로 변경하고 시작 시간을 기록합니다. - workflowRun.setStartedAt(LocalDateTime.now()); - workflowRun.setStatus("RUNNING"); + workflowRun.finish("SUCCESS"); workflowRunMapper.update(workflowRun); - log.debug("Initiated workflow run with traceId: {}", workflowRun.getTraceId()); - return workflowRun; - } - - /** - * 워크플로우 실행을 최종 상태(SUCCESS/FAILED)로 업데이트합니다. - * 항상 새로운 트랜잭션에서 실행되어, 실패 시에도 상태 기록이 롤백되지 않습니다. - */ - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void finalizeWorkflowExecution(Long workflowRunId, String status) { - WorkflowRun updatePayload = WorkflowRun.builder() - .id(workflowRunId) - .status(status) - .finishedAt(LocalDateTime.now()) - .build(); - workflowRunMapper.update(updatePayload); - log.debug("Finalized workflow run id: {} with status: {}", workflowRunId, status); + log.info("========== 워크플로우 실행 성공: WorkflowRunId={} ==========", workflowRun.getId()); } /** - * 워크플로우 내의 단일 Job을 실행하고 그 결과를 DB에 기록합니다. - * 항상 새로운 트랜잭션에서 실행되어, 각 Job의 실행 결과가 독립적으로 커밋됩니다. - * 실패 시 예외를 던져 상위의 orchestrator가 인지하도록 합니다. + * 특정 Job에 속한 Task들을 순차적으로 실행합니다. + * @param jobRun 실행중인 Job의 기록 객체 + * @return 모든 Task가 성공하면 true, 하나라도 실패하면 false */ - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void executeJobInWorkflow(Long jobId, Long workflowRunId, String traceId, int executionOrder) throws Exception { - Job job = jobMapper.findById(jobId) - .orElseThrow(() -> new IllegalStateException("Job not found with id: " + jobId)); - - JobRun jobRun = JobRun.builder() - .workflowRunId(workflowRunId) - .jobId(jobId) - .status("PENDING") - .executionOrder(executionOrder) - .build(); - jobRunMapper.save(jobRun); - - try { - org.springframework.batch.core.Job jobToRun = applicationContext.getBean(job.getName(), org.springframework.batch.core.Job.class); - - JobParameters jobParameters = new JobParametersBuilder() - .addString("traceId", traceId) - .addLong("workflowRunId", workflowRunId) - .addLong("jobRunId", jobRun.getId()) - .addString("runDateTime", LocalDateTime.now().toString()) - .toJobParameters(); - - jobRun.setStatus("RUNNING"); - jobRun.setStartedAt(LocalDateTime.now()); - jobRunMapper.update(jobRun); - log.info("Executing job '{}' (id:{}) for workflow traceId: {}", job.getName(), jobId, traceId); - - JobExecution jobExecution = jobLauncher.run(jobToRun, jobParameters); - - if (jobExecution.getStatus().isUnsuccessful()) { - throw new RuntimeException("Batch job '" + job.getName() + "' failed with status: " + jobExecution.getStatus()); + private boolean executeTasksForJob(JobRun jobRun) { + List tasks = jobMapper.findTasksByJobId(jobRun.getJobId()); + log.info("Job (JobRunId={}) 내 총 {}개의 Task를 실행합니다.", jobRun.getId(), tasks.size()); + + for (Task task : tasks) { + TaskRun taskRun = TaskRun.start(jobRun.getId(), task.getId()); + taskRunMapper.insert(taskRun); + log.info("Task 실행 시작: TaskId={}, TaskRunId={}", task.getId(), taskRun.getId()); + + String runnerBeanName = task.getType().toLowerCase() + "TaskRunner"; + TaskRunner runner = taskRunners.get(runnerBeanName); + + if (runner == null) { + taskRun.finish("FAILED", "지원하지 않는 Task 타입: " + task.getType()); + taskRunMapper.update(taskRun); + log.error("Task 실행 실패 (미지원 타입): TaskRunId={}, Type={}", taskRun.getId(), task.getType()); + return false; // 실행할 Runner가 없으므로 실패 } - jobRun.setStatus("SUCCESS"); - log.info("Successfully executed job '{}' (id:{})", job.getName(), jobId); + TaskRunner.TaskExecutionResult result = runner.execute(task, taskRun); + taskRun.finish(result.status(), result.message()); + taskRunMapper.update(taskRun); - } catch (Exception e) { - jobRun.setStatus("FAILED"); - log.error("Failed to execute job '{}' (id:{}). Reason: {}", job.getName(), jobId, e.getMessage()); - throw e; // 워크플로우 전체를 실패 처리하기 위해 예외를 다시 던집니다. - } finally { - jobRun.setFinishedAt(LocalDateTime.now()); - jobRunMapper.update(jobRun); + if (result.isFailure()) { + log.error("Task 실행 실패: TaskRunId={}, Message={}", taskRun.getId(), result.message()); + return false; // Task가 실패하면 즉시 중단하고 실패 반환 + } + log.info("Task 실행 성공: TaskRunId={}", taskRun.getId()); } + + return true; // 모든 Task가 성공적으로 완료됨 } } \ No newline at end of file From aa116ae905f30fbd827f3f351d1e2e4d3f39ebe5 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:36:11 +0900 Subject: [PATCH 26/33] =?UTF-8?q?chore:=20Spring=20Quartz=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/QuartzSchedulerInitializer.java | 33 +++++++++++++++++++ .../src/main/resources/application.yml | 15 +++++++++ 2 files changed, 48 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/global/config/QuartzSchedulerInitializer.java diff --git a/apps/user-service/src/main/java/site/icebang/global/config/QuartzSchedulerInitializer.java b/apps/user-service/src/main/java/site/icebang/global/config/QuartzSchedulerInitializer.java new file mode 100644 index 00000000..233f5834 --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/QuartzSchedulerInitializer.java @@ -0,0 +1,33 @@ +package site.icebang.global.config; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import site.icebang.domain.schedule.model.Schedule; +import site.icebang.domain.schedule.mapper.ScheduleMapper; +import site.icebang.domain.schedule.service.QuartzScheduleService; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class QuartzSchedulerInitializer implements CommandLineRunner { + + private final ScheduleMapper scheduleMapper; + private final QuartzScheduleService quartzScheduleService; + + @Override + public void run(String... args) { + log.info("Quartz 스케줄러 초기화 시작: DB 스케줄을 등록합니다."); + try { + List activeSchedules = scheduleMapper.findAllActive(); + for (Schedule schedule : activeSchedules) { + quartzScheduleService.addOrUpdateSchedule(schedule); + } + log.info("총 {}개의 활성 스케줄을 Quartz에 성공적으로 등록했습니다.", activeSchedules.size()); + } catch (Exception e) { + log.error("Quartz 스케줄 초기화 중 오류가 발생했습니다.", e); + } + } +} \ No newline at end of file diff --git a/apps/user-service/src/main/resources/application.yml b/apps/user-service/src/main/resources/application.yml index 7ede99ae..df64340e 100644 --- a/apps/user-service/src/main/resources/application.yml +++ b/apps/user-service/src/main/resources/application.yml @@ -7,6 +7,21 @@ spring: context: cache: maxSize: 1 + + # Spring Quartz 스케줄러 설정 + quartz: + job-store-type: jdbc + auto-startup: true + jdbc: + initialize-schema: never # Quartz 테이블 스크립트는 직접 실행해야 함 + properties: + org.quartz.scheduler.instanceId: AUTO + org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX + org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate + org.quartz.jobStore.tablePrefix: QRTZ_ # Quartz 테이블 접두사 + org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool + org.quartz.threadPool.threadCount: 10 # 스케줄러가 사용할 스레드 개수 + mybatis: # Mapper XML 파일 위치 mapper-locations: classpath:mapper/**/*.xml From fea37fb3b8e507305ac09d8305f37c8df9175c78 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:36:48 +0900 Subject: [PATCH 27/33] =?UTF-8?q?chore:=20MyBatis=20JsonNodeTypeHandler=20?= =?UTF-8?q?=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../typehandler/JsonNodeTypeHandler.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java diff --git a/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java b/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java new file mode 100644 index 00000000..ef4b85cb --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java @@ -0,0 +1,54 @@ +package site.icebang.global.config.mybatis.typehandler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +@MappedTypes(JsonNode.class) +public class JsonNodeTypeHandler extends BaseTypeHandler { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException { + try { + ps.setString(i, objectMapper.writeValueAsString(parameter)); + } catch (JsonProcessingException e) { + throw new SQLException("Error converting JsonNode to String", e); + } + } + + @Override + public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException { + return parseJson(rs.getString(columnName)); + } + + @Override + public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return parseJson(rs.getString(columnIndex)); + } + + @Override + public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return parseJson(cs.getString(columnIndex)); + } + + private JsonNode parseJson(String json) throws SQLException { + if (json == null) { + return null; + } + try { + return objectMapper.readTree(json); + } catch (JsonProcessingException e) { + throw new SQLException("Error parsing JSON", e); + } + } +} \ No newline at end of file From b2971918ba9f180b6a81989e9fc7279885099bde Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:40:22 +0900 Subject: [PATCH 28/33] refactor: Code Formatting --- .../schedule/mapper/ScheduleMapper.java | 6 +- .../domain/schedule/model/Schedule.java | 3 +- .../controller/WorkflowController.java | 3 +- .../domain/workflow/dto/WorkflowCardDto.java | 10 +- .../domain/workflow/mapper/JobMapper.java | 13 +- .../domain/workflow/mapper/TaskMapper.java | 10 +- .../workflow/mapper/WorkflowMapper.java | 10 +- .../mapper/execution/JobRunMapper.java | 8 +- .../mapper/execution/TaskRunMapper.java | 10 +- .../mapper/execution/WorkflowRunMapper.java | 8 +- .../icebang/domain/workflow/model/Job.java | 19 +- .../icebang/domain/workflow/model/Task.java | 22 +-- .../domain/workflow/model/Workflow.java | 25 ++- .../workflow/model/execution/JobRun.java | 55 +++--- .../workflow/model/execution/TaskRun.java | 65 ++++--- .../workflow/model/execution/WorkflowRun.java | 57 +++--- .../workflow/runner/HttpTaskRunner.java | 65 ++++--- .../domain/workflow/runner/TaskRunner.java | 20 ++- .../scheduler/WorkflowTriggerJob.java | 22 +-- .../service/WorkflowExecutionService.java | 169 +++++++++--------- .../typehandler/JsonNodeTypeHandler.java | 82 ++++----- 21 files changed, 355 insertions(+), 327 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java index 2b68239b..12567a60 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/mapper/ScheduleMapper.java @@ -1,10 +1,12 @@ package site.icebang.domain.schedule.mapper; +import java.util.List; + import org.apache.ibatis.annotations.Mapper; + import site.icebang.domain.schedule.model.Schedule; -import java.util.List; @Mapper public interface ScheduleMapper { List findAllActive(); -} \ No newline at end of file +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java b/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java index d5d8f51a..c2218bd0 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java +++ b/apps/user-service/src/main/java/site/icebang/domain/schedule/model/Schedule.java @@ -1,6 +1,7 @@ package site.icebang.domain.schedule.model; import java.time.LocalDateTime; + import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -27,4 +28,4 @@ public class Schedule { private LocalDateTime updatedAt; private Long updatedBy; private String scheduleText; -} \ No newline at end of file +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java index f4dc51c2..348058ee 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/controller/WorkflowController.java @@ -1,5 +1,7 @@ package site.icebang.domain.workflow.controller; +import java.util.concurrent.CompletableFuture; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -11,7 +13,6 @@ import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.service.WorkflowExecutionService; import site.icebang.domain.workflow.service.WorkflowService; -import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/v0/workflows") diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java index 91f1029c..95a6b704 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/WorkflowCardDto.java @@ -6,8 +6,8 @@ @Getter @NoArgsConstructor public class WorkflowCardDto { - private Long id; - private String name; - private String description; - private boolean isEnabled; -} \ No newline at end of file + private Long id; + private String name; + private String description; + private boolean isEnabled; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java index e85fcb1e..a82739f4 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobMapper.java @@ -1,12 +1,15 @@ package site.icebang.domain.workflow.mapper; +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; + import site.icebang.domain.workflow.model.Job; import site.icebang.domain.workflow.model.Task; -import org.apache.ibatis.annotations.Mapper; -import java.util.List; @Mapper public interface JobMapper { - List findJobsByWorkflowId(Long workflowId); - List findTasksByJobId(Long jobId); -} \ No newline at end of file + List findJobsByWorkflowId(Long workflowId); + + List findTasksByJobId(Long jobId); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java index c812c649..0edb7812 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/TaskMapper.java @@ -1,10 +1,12 @@ package site.icebang.domain.workflow.mapper; -import site.icebang.domain.workflow.model.Task; -import org.apache.ibatis.annotations.Mapper; import java.util.Optional; +import org.apache.ibatis.annotations.Mapper; + +import site.icebang.domain.workflow.model.Task; + @Mapper public interface TaskMapper { - Optional findById(Long id); -} \ No newline at end of file + Optional findById(Long id); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java index 152477af..4ddd94d3 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowMapper.java @@ -2,16 +2,18 @@ import java.util.List; import java.util.Optional; + import org.apache.ibatis.annotations.Mapper; + import site.icebang.common.dto.PageParams; import site.icebang.domain.workflow.dto.WorkflowCardDto; import site.icebang.domain.workflow.model.Workflow; @Mapper public interface WorkflowMapper { - Optional findById(Long id); + Optional findById(Long id); - List selectWorkflowList(PageParams pageParams); + List selectWorkflowList(PageParams pageParams); - int selectWorkflowCount(PageParams pageParams); -} \ No newline at end of file + int selectWorkflowCount(PageParams pageParams); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java index dda90557..8d02b219 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java @@ -1,10 +1,12 @@ package site.icebang.domain.workflow.mapper.execution; import org.apache.ibatis.annotations.Mapper; + import site.icebang.domain.workflow.model.execution.JobRun; @Mapper public interface JobRunMapper { - void insert(JobRun jobRun); - void update(JobRun jobRun); -} \ No newline at end of file + void insert(JobRun jobRun); + + void update(JobRun jobRun); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java index 3ccab2a2..51a76ec9 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java @@ -1,10 +1,12 @@ package site.icebang.domain.workflow.mapper.execution; -import site.icebang.domain.workflow.model.execution.TaskRun; import org.apache.ibatis.annotations.Mapper; +import site.icebang.domain.workflow.model.execution.TaskRun; + @Mapper public interface TaskRunMapper { - void insert(TaskRun taskRun); - void update(TaskRun taskRun); -} \ No newline at end of file + void insert(TaskRun taskRun); + + void update(TaskRun taskRun); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java index 5d83687f..1bfea224 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java @@ -1,10 +1,12 @@ package site.icebang.domain.workflow.mapper.execution; import org.apache.ibatis.annotations.Mapper; + import site.icebang.domain.workflow.model.execution.WorkflowRun; @Mapper public interface WorkflowRunMapper { - void insert(WorkflowRun workflowRun); - void update(WorkflowRun workflowRun); -} \ No newline at end of file + void insert(WorkflowRun workflowRun); + + void update(WorkflowRun workflowRun); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java index 4aae43c8..0a3604b5 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Job.java @@ -1,6 +1,7 @@ package site.icebang.domain.workflow.model; import java.time.LocalDateTime; + import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -10,12 +11,12 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Job { - private Long id; - private String name; - private String description; - private boolean isEnabled; - private LocalDateTime createdAt; - private Long createdBy; - private LocalDateTime updatedAt; - private Long updatedBy; -} \ No newline at end of file + private Long id; + private String name; + private String description; + private boolean isEnabled; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java index 188865ce..09589cc1 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Task.java @@ -1,6 +1,7 @@ package site.icebang.domain.workflow.model; import com.fasterxml.jackson.databind.JsonNode; + import lombok.Getter; import lombok.NoArgsConstructor; @@ -8,19 +9,12 @@ @NoArgsConstructor // MyBatis가 객체를 생성하기 위해 필요 public class Task { - private Long id; - private String name; - - /** - * Task의 타입 (예: "HTTP", "SPRING_BATCH") - * 이 타입에 따라 TaskRunner가 선택됩니다. - */ - private String type; + private Long id; + private String name; - /** - * Task 실행에 필요한 파라미터 (JSON) - * 예: {"url": "http://...", "method": "POST", "body": {...}} - */ - private JsonNode parameters; + /** Task의 타입 (예: "HTTP", "SPRING_BATCH") 이 타입에 따라 TaskRunner가 선택됩니다. */ + private String type; -} \ No newline at end of file + /** Task 실행에 필요한 파라미터 (JSON) 예: {"url": "http://...", "method": "POST", "body": {...}} */ + private JsonNode parameters; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java index 01d32485..3ea80388 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/Workflow.java @@ -1,6 +1,7 @@ package site.icebang.domain.workflow.model; import java.time.LocalDateTime; + import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -13,17 +14,15 @@ @AllArgsConstructor public class Workflow { - private Long id; - private String name; - private String description; - private boolean isEnabled; - private LocalDateTime createdAt; - private Long createdBy; - private LocalDateTime updatedAt; - private Long updatedBy; - /** - * 워크플로우별 기본 설정값 (JSON) - */ - private String defaultConfig; + private Long id; + private String name; + private String description; + private boolean isEnabled; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; + private Long updatedBy; -} \ No newline at end of file + /** 워크플로우별 기본 설정값 (JSON) */ + private String defaultConfig; +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java index 651f1280..7c6eba81 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java @@ -1,41 +1,38 @@ package site.icebang.domain.workflow.model.execution; +import java.time.LocalDateTime; + import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; @Getter @NoArgsConstructor public class JobRun { - private Long id; - private Long workflowRunId; - private Long jobId; - private String status; // PENDING, RUNNING, SUCCESS, FAILED - private LocalDateTime startedAt; - private LocalDateTime finishedAt; - private LocalDateTime createdAt; + private Long id; + private Long workflowRunId; + private Long jobId; + private String status; // PENDING, RUNNING, SUCCESS, FAILED + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createdAt; - private JobRun(Long workflowRunId, Long jobId) { - this.workflowRunId = workflowRunId; - this.jobId = jobId; - this.status = "RUNNING"; - this.startedAt = LocalDateTime.now(); - this.createdAt = this.startedAt; - } + private JobRun(Long workflowRunId, Long jobId) { + this.workflowRunId = workflowRunId; + this.jobId = jobId; + this.status = "RUNNING"; + this.startedAt = LocalDateTime.now(); + this.createdAt = this.startedAt; + } - /** - * Job 실행 시작을 위한 정적 팩토리 메소드 - */ - public static JobRun start(Long workflowRunId, Long jobId) { - return new JobRun(workflowRunId, jobId); - } + /** Job 실행 시작을 위한 정적 팩토리 메소드 */ + public static JobRun start(Long workflowRunId, Long jobId) { + return new JobRun(workflowRunId, jobId); + } - /** - * Job 실행 완료 처리 - */ - public void finish(String status) { - this.status = status; - this.finishedAt = LocalDateTime.now(); - } -} \ No newline at end of file + /** Job 실행 완료 처리 */ + public void finish(String status) { + this.status = status; + this.finishedAt = LocalDateTime.now(); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java index f1ad15a6..7bafe37b 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java @@ -1,46 +1,43 @@ package site.icebang.domain.workflow.model.execution; +import java.time.LocalDateTime; + import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; @Getter @NoArgsConstructor public class TaskRun { - private Long id; - private Long jobRunId; - private Long taskId; - private String status; // PENDING, RUNNING, SUCCESS, FAILED - private String resultMessage; // 실행 결과 메시지 - private LocalDateTime startedAt; - private LocalDateTime finishedAt; - private LocalDateTime createdAt; + private Long id; + private Long jobRunId; + private Long taskId; + private String status; // PENDING, RUNNING, SUCCESS, FAILED + private String resultMessage; // 실행 결과 메시지 + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createdAt; - // 생성자나 정적 팩토리 메서드를 통해 객체 생성 로직을 관리 - private TaskRun(Long jobRunId, Long taskId) { - this.jobRunId = jobRunId; - this.taskId = taskId; - this.status = "PENDING"; - this.createdAt = LocalDateTime.now(); - } + // 생성자나 정적 팩토리 메서드를 통해 객체 생성 로직을 관리 + private TaskRun(Long jobRunId, Long taskId) { + this.jobRunId = jobRunId; + this.taskId = taskId; + this.status = "PENDING"; + this.createdAt = LocalDateTime.now(); + } - /** - * Task 실행 시작을 위한 정적 팩토리 메서드 - */ - public static TaskRun start(Long jobRunId, Long taskId) { - TaskRun taskRun = new TaskRun(jobRunId, taskId); - taskRun.status = "RUNNING"; - taskRun.startedAt = LocalDateTime.now(); - return taskRun; - } + /** Task 실행 시작을 위한 정적 팩토리 메서드 */ + public static TaskRun start(Long jobRunId, Long taskId) { + TaskRun taskRun = new TaskRun(jobRunId, taskId); + taskRun.status = "RUNNING"; + taskRun.startedAt = LocalDateTime.now(); + return taskRun; + } - /** - * Task 실행 완료 처리 - */ - public void finish(String status, String resultMessage) { - this.status = status; - this.resultMessage = resultMessage; - this.finishedAt = LocalDateTime.now(); - } -} \ No newline at end of file + /** Task 실행 완료 처리 */ + public void finish(String status, String resultMessage) { + this.status = status; + this.resultMessage = resultMessage; + this.finishedAt = LocalDateTime.now(); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java index 2f37e03e..a993069e 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java @@ -1,42 +1,39 @@ package site.icebang.domain.workflow.model.execution; -import lombok.Getter; -import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.UUID; +import lombok.Getter; +import lombok.NoArgsConstructor; + @Getter @NoArgsConstructor public class WorkflowRun { - private Long id; - private Long workflowId; - private String traceId; // 분산 추적을 위한 ID - private String status; // PENDING, RUNNING, SUCCESS, FAILED - private LocalDateTime startedAt; - private LocalDateTime finishedAt; - private LocalDateTime createdAt; + private Long id; + private Long workflowId; + private String traceId; // 분산 추적을 위한 ID + private String status; // PENDING, RUNNING, SUCCESS, FAILED + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + private LocalDateTime createdAt; - private WorkflowRun(Long workflowId) { - this.workflowId = workflowId; - this.traceId = UUID.randomUUID().toString(); // 고유 추적 ID 생성 - this.status = "RUNNING"; - this.startedAt = LocalDateTime.now(); - this.createdAt = this.startedAt; - } + private WorkflowRun(Long workflowId) { + this.workflowId = workflowId; + this.traceId = UUID.randomUUID().toString(); // 고유 추적 ID 생성 + this.status = "RUNNING"; + this.startedAt = LocalDateTime.now(); + this.createdAt = this.startedAt; + } - /** - * 워크플로우 실행 시작을 위한 정적 팩토리 메소드 - */ - public static WorkflowRun start(Long workflowId) { - return new WorkflowRun(workflowId); - } + /** 워크플로우 실행 시작을 위한 정적 팩토리 메소드 */ + public static WorkflowRun start(Long workflowId) { + return new WorkflowRun(workflowId); + } - /** - * 워크플로우 실행 완료 처리 - */ - public void finish(String status) { - this.status = status; - this.finishedAt = LocalDateTime.now(); - } -} \ No newline at end of file + /** 워크플로우 실행 완료 처리 */ + public void finish(String status) { + this.status = status; + this.finishedAt = LocalDateTime.now(); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java index 83847997..a67b43d2 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java @@ -1,40 +1,49 @@ package site.icebang.domain.workflow.runner; -import site.icebang.domain.workflow.model.Task; -import site.icebang.domain.workflow.model.execution.TaskRun; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.databind.JsonNode; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import site.icebang.domain.workflow.model.Task; +import site.icebang.domain.workflow.model.execution.TaskRun; + @Slf4j @Component("httpTaskRunner") @RequiredArgsConstructor public class HttpTaskRunner implements TaskRunner { - private final RestTemplate restTemplate; - - @Override - public TaskExecutionResult execute(Task task, TaskRun taskRun) { - JsonNode params = task.getParameters(); - String url = params.get("url").asText(); - String method = params.get("method").asText(); - JsonNode body = params.get("body"); - - try { - HttpEntity requestEntity = new HttpEntity<>(body.toString(), new HttpHeaders() {{ - setContentType(MediaType.APPLICATION_JSON); - }}); - - ResponseEntity response = restTemplate.exchange( - url, HttpMethod.valueOf(method.toUpperCase()), requestEntity, String.class); - - return TaskExecutionResult.success(response.getBody()); - } catch (RestClientException e) { - log.error("HTTP Task 실행 실패: TaskRunId={}, Error={}", taskRun.getId(), e.getMessage()); - return TaskExecutionResult.failure(e.getMessage()); - } + private final RestTemplate restTemplate; + + @Override + public TaskExecutionResult execute(Task task, TaskRun taskRun) { + JsonNode params = task.getParameters(); + String url = params.get("url").asText(); + String method = params.get("method").asText(); + JsonNode body = params.get("body"); + + try { + HttpEntity requestEntity = + new HttpEntity<>( + body.toString(), + new HttpHeaders() { + { + setContentType(MediaType.APPLICATION_JSON); + } + }); + + ResponseEntity response = + restTemplate.exchange( + url, HttpMethod.valueOf(method.toUpperCase()), requestEntity, String.class); + + return TaskExecutionResult.success(response.getBody()); + } catch (RestClientException e) { + log.error("HTTP Task 실행 실패: TaskRunId={}, Error={}", taskRun.getId(), e.getMessage()); + return TaskExecutionResult.failure(e.getMessage()); } -} \ No newline at end of file + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java index f039a14c..14bce0e5 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java @@ -4,11 +4,19 @@ import site.icebang.domain.workflow.model.execution.TaskRun; public interface TaskRunner { - record TaskExecutionResult(String status, String message) { - public static TaskExecutionResult success(String message) { return new TaskExecutionResult("SUCCESS", message); } - public static TaskExecutionResult failure(String message) { return new TaskExecutionResult("FAILED", message); } - public boolean isFailure() { return "FAILED".equals(status); } + record TaskExecutionResult(String status, String message) { + public static TaskExecutionResult success(String message) { + return new TaskExecutionResult("SUCCESS", message); } - TaskExecutionResult execute(Task task, TaskRun taskRun); -} \ No newline at end of file + public static TaskExecutionResult failure(String message) { + return new TaskExecutionResult("FAILED", message); + } + + public boolean isFailure() { + return "FAILED".equals(status); + } + } + + TaskExecutionResult execute(Task task, TaskRun taskRun); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java index 92b7264d..196c1fa0 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/scheduler/WorkflowTriggerJob.java @@ -1,22 +1,24 @@ package site.icebang.domain.workflow.scheduler; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.quartz.JobExecutionContext; import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import site.icebang.domain.workflow.service.WorkflowExecutionService; @Slf4j @Component @RequiredArgsConstructor public class WorkflowTriggerJob extends QuartzJobBean { - private final WorkflowExecutionService workflowExecutionService; + private final WorkflowExecutionService workflowExecutionService; - @Override - protected void executeInternal(JobExecutionContext context) { - Long workflowId = context.getJobDetail().getJobDataMap().getLong("workflowId"); - log.info("Quartz가 WorkflowTriggerJob을 실행합니다. WorkflowId={}", workflowId); - workflowExecutionService.executeWorkflow(workflowId); - } -} \ No newline at end of file + @Override + protected void executeInternal(JobExecutionContext context) { + Long workflowId = context.getJobDetail().getJobDataMap().getLong("workflowId"); + log.info("Quartz가 WorkflowTriggerJob을 실행합니다. WorkflowId={}", workflowId); + workflowExecutionService.executeWorkflow(workflowId); + } +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java index 472c10df..63692c85 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -1,9 +1,14 @@ package site.icebang.domain.workflow.service; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; +import java.util.Map; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + import site.icebang.domain.workflow.mapper.JobMapper; import site.icebang.domain.workflow.mapper.execution.JobRunMapper; import site.icebang.domain.workflow.mapper.execution.TaskRunMapper; @@ -15,92 +20,92 @@ import site.icebang.domain.workflow.model.execution.WorkflowRun; import site.icebang.domain.workflow.runner.TaskRunner; -import java.util.List; -import java.util.Map; - @Slf4j @Service @RequiredArgsConstructor public class WorkflowExecutionService { - private final JobMapper jobMapper; - private final WorkflowRunMapper workflowRunMapper; - private final JobRunMapper jobRunMapper; - private final TaskRunMapper taskRunMapper; - private final Map taskRunners; - - /** - * 워크플로우 실행의 시작점. 전체 과정은 하나의 트랜잭션으로 묶입니다. - * @param workflowId 실행할 워크플로우의 ID - */ - @Transactional - public void executeWorkflow(Long workflowId) { - log.info("========== 워크플로우 실행 시작: WorkflowId={} ==========", workflowId); - WorkflowRun workflowRun = WorkflowRun.start(workflowId); - workflowRunMapper.insert(workflowRun); - - List jobs = jobMapper.findJobsByWorkflowId(workflowId); - log.info("총 {}개의 Job을 순차적으로 실행합니다.", jobs.size()); - - for (Job job : jobs) { - JobRun jobRun = JobRun.start(workflowRun.getId(), job.getId()); - jobRunMapper.insert(jobRun); - log.info("---------- Job 실행 시작: JobId={}, JobRunId={} ----------", job.getId(), jobRun.getId()); - - boolean jobSucceeded = executeTasksForJob(jobRun); - - jobRun.finish(jobSucceeded ? "SUCCESS" : "FAILED"); - jobRunMapper.update(jobRun); - - if (!jobSucceeded) { - workflowRun.finish("FAILED"); - workflowRunMapper.update(workflowRun); - log.error("Job 실패로 인해 워크플로우 실행을 중단합니다: WorkflowRunId={}", workflowRun.getId()); - return; // Job이 실패하면 전체 워크플로우를 중단 - } - log.info("---------- Job 실행 성공: JobRunId={} ----------", jobRun.getId()); - } - - workflowRun.finish("SUCCESS"); + private final JobMapper jobMapper; + private final WorkflowRunMapper workflowRunMapper; + private final JobRunMapper jobRunMapper; + private final TaskRunMapper taskRunMapper; + private final Map taskRunners; + + /** + * 워크플로우 실행의 시작점. 전체 과정은 하나의 트랜잭션으로 묶입니다. + * + * @param workflowId 실행할 워크플로우의 ID + */ + @Transactional + public void executeWorkflow(Long workflowId) { + log.info("========== 워크플로우 실행 시작: WorkflowId={} ==========", workflowId); + WorkflowRun workflowRun = WorkflowRun.start(workflowId); + workflowRunMapper.insert(workflowRun); + + List jobs = jobMapper.findJobsByWorkflowId(workflowId); + log.info("총 {}개의 Job을 순차적으로 실행합니다.", jobs.size()); + + for (Job job : jobs) { + JobRun jobRun = JobRun.start(workflowRun.getId(), job.getId()); + jobRunMapper.insert(jobRun); + log.info( + "---------- Job 실행 시작: JobId={}, JobRunId={} ----------", job.getId(), jobRun.getId()); + + boolean jobSucceeded = executeTasksForJob(jobRun); + + jobRun.finish(jobSucceeded ? "SUCCESS" : "FAILED"); + jobRunMapper.update(jobRun); + + if (!jobSucceeded) { + workflowRun.finish("FAILED"); workflowRunMapper.update(workflowRun); - log.info("========== 워크플로우 실행 성공: WorkflowRunId={} ==========", workflowRun.getId()); + log.error("Job 실패로 인해 워크플로우 실행을 중단합니다: WorkflowRunId={}", workflowRun.getId()); + return; // Job이 실패하면 전체 워크플로우를 중단 + } + log.info("---------- Job 실행 성공: JobRunId={} ----------", jobRun.getId()); } - /** - * 특정 Job에 속한 Task들을 순차적으로 실행합니다. - * @param jobRun 실행중인 Job의 기록 객체 - * @return 모든 Task가 성공하면 true, 하나라도 실패하면 false - */ - private boolean executeTasksForJob(JobRun jobRun) { - List tasks = jobMapper.findTasksByJobId(jobRun.getJobId()); - log.info("Job (JobRunId={}) 내 총 {}개의 Task를 실행합니다.", jobRun.getId(), tasks.size()); - - for (Task task : tasks) { - TaskRun taskRun = TaskRun.start(jobRun.getId(), task.getId()); - taskRunMapper.insert(taskRun); - log.info("Task 실행 시작: TaskId={}, TaskRunId={}", task.getId(), taskRun.getId()); - - String runnerBeanName = task.getType().toLowerCase() + "TaskRunner"; - TaskRunner runner = taskRunners.get(runnerBeanName); - - if (runner == null) { - taskRun.finish("FAILED", "지원하지 않는 Task 타입: " + task.getType()); - taskRunMapper.update(taskRun); - log.error("Task 실행 실패 (미지원 타입): TaskRunId={}, Type={}", taskRun.getId(), task.getType()); - return false; // 실행할 Runner가 없으므로 실패 - } - - TaskRunner.TaskExecutionResult result = runner.execute(task, taskRun); - taskRun.finish(result.status(), result.message()); - taskRunMapper.update(taskRun); - - if (result.isFailure()) { - log.error("Task 실행 실패: TaskRunId={}, Message={}", taskRun.getId(), result.message()); - return false; // Task가 실패하면 즉시 중단하고 실패 반환 - } - log.info("Task 실행 성공: TaskRunId={}", taskRun.getId()); - } - - return true; // 모든 Task가 성공적으로 완료됨 + workflowRun.finish("SUCCESS"); + workflowRunMapper.update(workflowRun); + log.info("========== 워크플로우 실행 성공: WorkflowRunId={} ==========", workflowRun.getId()); + } + + /** + * 특정 Job에 속한 Task들을 순차적으로 실행합니다. + * + * @param jobRun 실행중인 Job의 기록 객체 + * @return 모든 Task가 성공하면 true, 하나라도 실패하면 false + */ + private boolean executeTasksForJob(JobRun jobRun) { + List tasks = jobMapper.findTasksByJobId(jobRun.getJobId()); + log.info("Job (JobRunId={}) 내 총 {}개의 Task를 실행합니다.", jobRun.getId(), tasks.size()); + + for (Task task : tasks) { + TaskRun taskRun = TaskRun.start(jobRun.getId(), task.getId()); + taskRunMapper.insert(taskRun); + log.info("Task 실행 시작: TaskId={}, TaskRunId={}", task.getId(), taskRun.getId()); + + String runnerBeanName = task.getType().toLowerCase() + "TaskRunner"; + TaskRunner runner = taskRunners.get(runnerBeanName); + + if (runner == null) { + taskRun.finish("FAILED", "지원하지 않는 Task 타입: " + task.getType()); + taskRunMapper.update(taskRun); + log.error("Task 실행 실패 (미지원 타입): TaskRunId={}, Type={}", taskRun.getId(), task.getType()); + return false; // 실행할 Runner가 없으므로 실패 + } + + TaskRunner.TaskExecutionResult result = runner.execute(task, taskRun); + taskRun.finish(result.status(), result.message()); + taskRunMapper.update(taskRun); + + if (result.isFailure()) { + log.error("Task 실행 실패: TaskRunId={}, Message={}", taskRun.getId(), result.message()); + return false; // Task가 실패하면 즉시 중단하고 실패 반환 + } + log.info("Task 실행 성공: TaskRunId={}", taskRun.getId()); } -} \ No newline at end of file + + return true; // 모든 Task가 성공적으로 완료됨 + } +} diff --git a/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java b/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java index ef4b85cb..4079c9f3 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/mybatis/typehandler/JsonNodeTypeHandler.java @@ -1,54 +1,56 @@ package site.icebang.global.config.mybatis.typehandler; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.ibatis.type.BaseTypeHandler; -import org.apache.ibatis.type.JdbcType; -import org.apache.ibatis.type.MappedTypes; - import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -@MappedTypes(JsonNode.class) -public class JsonNodeTypeHandler extends BaseTypeHandler { +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; - private static final ObjectMapper objectMapper = new ObjectMapper(); +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; - @Override - public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException { - try { - ps.setString(i, objectMapper.writeValueAsString(parameter)); - } catch (JsonProcessingException e) { - throw new SQLException("Error converting JsonNode to String", e); - } - } +@MappedTypes(JsonNode.class) +public class JsonNodeTypeHandler extends BaseTypeHandler { - @Override - public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException { - return parseJson(rs.getString(columnName)); - } + private static final ObjectMapper objectMapper = new ObjectMapper(); - @Override - public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { - return parseJson(rs.getString(columnIndex)); + @Override + public void setNonNullParameter( + PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException { + try { + ps.setString(i, objectMapper.writeValueAsString(parameter)); + } catch (JsonProcessingException e) { + throw new SQLException("Error converting JsonNode to String", e); } - - @Override - public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { - return parseJson(cs.getString(columnIndex)); + } + + @Override + public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException { + return parseJson(rs.getString(columnName)); + } + + @Override + public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return parseJson(rs.getString(columnIndex)); + } + + @Override + public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return parseJson(cs.getString(columnIndex)); + } + + private JsonNode parseJson(String json) throws SQLException { + if (json == null) { + return null; } - - private JsonNode parseJson(String json) throws SQLException { - if (json == null) { - return null; - } - try { - return objectMapper.readTree(json); - } catch (JsonProcessingException e) { - throw new SQLException("Error parsing JSON", e); - } + try { + return objectMapper.readTree(json); + } catch (JsonProcessingException e) { + throw new SQLException("Error parsing JSON", e); } -} \ No newline at end of file + } +} From dd5f9d665ed519581de162d1c782a2e4fbd64349 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 02:48:19 +0900 Subject: [PATCH 29/33] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/site/icebang/UserServiceApplication.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/UserServiceApplication.java b/apps/user-service/src/main/java/site/icebang/UserServiceApplication.java index 68da9f2a..29e975ba 100644 --- a/apps/user-service/src/main/java/site/icebang/UserServiceApplication.java +++ b/apps/user-service/src/main/java/site/icebang/UserServiceApplication.java @@ -1,13 +1,9 @@ package site.icebang; import org.mybatis.spring.annotation.MapperScan; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableScheduling; -@EnableScheduling -@EnableBatchProcessing @SpringBootApplication @MapperScan("site.icebang.**.mapper") public class UserServiceApplication { From 9820055277d665e19d239b28d00e73c48e99ff5e Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 10:54:00 +0900 Subject: [PATCH 30/33] =?UTF-8?q?refactor:=20execution(TaskRun,=20JobRun,?= =?UTF-8?q?=20WorkflowRun)=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execution => execution/mapper}/JobRunMapper.java | 4 ++-- .../mapper}/TaskRunMapper.java | 4 ++-- .../mapper}/WorkflowRunMapper.java | 4 ++-- .../model/execution => execution/model}/JobRun.java | 2 +- .../model/execution => execution/model}/TaskRun.java | 2 +- .../execution => execution/model}/WorkflowRun.java | 2 +- .../domain/workflow/runner/HttpTaskRunner.java | 2 +- .../icebang/domain/workflow/runner/TaskRunner.java | 2 +- .../workflow/service/WorkflowExecutionService.java | 12 ++++++------ .../main/resources/mybatis/mapper/JobRunMapper.xml | 4 ++-- .../main/resources/mybatis/mapper/TaskRunMapper.xml | 2 +- .../resources/mybatis/mapper/WorkflowRunMapper.xml | 4 ++-- 12 files changed, 22 insertions(+), 22 deletions(-) rename apps/user-service/src/main/java/site/icebang/domain/{workflow/mapper/execution => execution/mapper}/JobRunMapper.java (56%) rename apps/user-service/src/main/java/site/icebang/domain/{workflow/mapper/execution => execution/mapper}/TaskRunMapper.java (57%) rename apps/user-service/src/main/java/site/icebang/domain/{workflow/mapper/execution => execution/mapper}/WorkflowRunMapper.java (59%) rename apps/user-service/src/main/java/site/icebang/domain/{workflow/model/execution => execution/model}/JobRun.java (94%) rename apps/user-service/src/main/java/site/icebang/domain/{workflow/model/execution => execution/model}/TaskRun.java (95%) rename apps/user-service/src/main/java/site/icebang/domain/{workflow/model/execution => execution/model}/WorkflowRun.java (95%) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java similarity index 56% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java rename to apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java index 8d02b219..d5ce7e8f 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/JobRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/JobRunMapper.java @@ -1,8 +1,8 @@ -package site.icebang.domain.workflow.mapper.execution; +package site.icebang.domain.execution.mapper; import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.workflow.model.execution.JobRun; +import site.icebang.domain.execution.model.JobRun; @Mapper public interface JobRunMapper { diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/TaskRunMapper.java similarity index 57% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java rename to apps/user-service/src/main/java/site/icebang/domain/execution/mapper/TaskRunMapper.java index 51a76ec9..646a7c91 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/TaskRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/TaskRunMapper.java @@ -1,8 +1,8 @@ -package site.icebang.domain.workflow.mapper.execution; +package site.icebang.domain.execution.mapper; import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.workflow.model.execution.TaskRun; +import site.icebang.domain.execution.model.TaskRun; @Mapper public interface TaskRunMapper { diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java similarity index 59% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java rename to apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java index 1bfea224..776ec4b0 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/execution/WorkflowRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/mapper/WorkflowRunMapper.java @@ -1,8 +1,8 @@ -package site.icebang.domain.workflow.mapper.execution; +package site.icebang.domain.execution.mapper; import org.apache.ibatis.annotations.Mapper; -import site.icebang.domain.workflow.model.execution.WorkflowRun; +import site.icebang.domain.execution.model.WorkflowRun; @Mapper public interface WorkflowRunMapper { diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java similarity index 94% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java rename to apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java index 7c6eba81..f5310f12 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/JobRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/model/JobRun.java @@ -1,4 +1,4 @@ -package site.icebang.domain.workflow.model.execution; +package site.icebang.domain.execution.model; import java.time.LocalDateTime; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/TaskRun.java similarity index 95% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java rename to apps/user-service/src/main/java/site/icebang/domain/execution/model/TaskRun.java index 7bafe37b..f1ae2239 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/TaskRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/model/TaskRun.java @@ -1,4 +1,4 @@ -package site.icebang.domain.workflow.model.execution; +package site.icebang.domain.execution.model; import java.time.LocalDateTime; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java b/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java similarity index 95% rename from apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java rename to apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java index a993069e..6bd5dbc9 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/model/execution/WorkflowRun.java +++ b/apps/user-service/src/main/java/site/icebang/domain/execution/model/WorkflowRun.java @@ -1,4 +1,4 @@ -package site.icebang.domain.workflow.model.execution; +package site.icebang.domain.execution.model; import java.time.LocalDateTime; import java.util.UUID; diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java index a67b43d2..6e0a4ba2 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/HttpTaskRunner.java @@ -11,7 +11,7 @@ import lombok.extern.slf4j.Slf4j; import site.icebang.domain.workflow.model.Task; -import site.icebang.domain.workflow.model.execution.TaskRun; +import site.icebang.domain.execution.model.TaskRun; @Slf4j @Component("httpTaskRunner") diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java index 14bce0e5..a6698ec4 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/runner/TaskRunner.java @@ -1,7 +1,7 @@ package site.icebang.domain.workflow.runner; import site.icebang.domain.workflow.model.Task; -import site.icebang.domain.workflow.model.execution.TaskRun; +import site.icebang.domain.execution.model.TaskRun; public interface TaskRunner { record TaskExecutionResult(String status, String message) { diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java index 63692c85..9b7a0f33 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/service/WorkflowExecutionService.java @@ -10,14 +10,14 @@ import lombok.extern.slf4j.Slf4j; import site.icebang.domain.workflow.mapper.JobMapper; -import site.icebang.domain.workflow.mapper.execution.JobRunMapper; -import site.icebang.domain.workflow.mapper.execution.TaskRunMapper; -import site.icebang.domain.workflow.mapper.execution.WorkflowRunMapper; +import site.icebang.domain.execution.mapper.JobRunMapper; +import site.icebang.domain.execution.mapper.TaskRunMapper; +import site.icebang.domain.execution.mapper.WorkflowRunMapper; import site.icebang.domain.workflow.model.Job; import site.icebang.domain.workflow.model.Task; -import site.icebang.domain.workflow.model.execution.JobRun; -import site.icebang.domain.workflow.model.execution.TaskRun; -import site.icebang.domain.workflow.model.execution.WorkflowRun; +import site.icebang.domain.execution.model.JobRun; +import site.icebang.domain.execution.model.TaskRun; +import site.icebang.domain.execution.model.WorkflowRun; import site.icebang.domain.workflow.runner.TaskRunner; @Slf4j diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml index f9d140ae..4fd0ea3d 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml @@ -1,9 +1,9 @@ - + - + diff --git a/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml index 17214fd6..90c5e0e5 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml @@ -1,4 +1,4 @@ - + INSERT INTO task_run (job_run_id, task_id, status, started_at, created_at) VALUES (#{jobRunId}, #{taskId}, #{status}, #{startedAt}, #{createdAt}) diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml index bfd7faa0..224abd02 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml @@ -1,9 +1,9 @@ - + - + From 7dcda889973384624b928ce16029cdce1a4af69e Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 14:09:21 +0900 Subject: [PATCH 31/33] =?UTF-8?q?refactor:=20Quartz=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20datasource=20=EC=84=A4=EC=A0=95=20=ED=95=84=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-develop.yml | 14 ++++++++++++++ .../src/main/resources/application-production.yml | 4 ++++ .../src/main/resources/application.yml | 14 -------------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/user-service/src/main/resources/application-develop.yml b/apps/user-service/src/main/resources/application-develop.yml index 8c24f49d..336d62ae 100644 --- a/apps/user-service/src/main/resources/application-develop.yml +++ b/apps/user-service/src/main/resources/application-develop.yml @@ -36,6 +36,20 @@ spring: - classpath:sql/03-insert-workflow.sql encoding: UTF-8 +# # Spring Quartz 스케줄러 설정 +# quartz: +# job-store-type: jdbc +# auto-startup: true +# jdbc: +# initialize-schema: embedded # 운영 환경을 기준으로 기본값 설정 +# properties: +# org.quartz.scheduler.instanceId: AUTO +# org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX +# org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate +# org.quartz.jobStore.tablePrefix: QRTZ_ # Quartz 테이블 접두사 +# org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool +# org.quartz.threadPool.threadCount: 5 # 개발 환경 스레드 수 + mybatis: mapper-locations: classpath:mybatis/mapper/**/*.xml type-aliases-package: site.icebang.dto diff --git a/apps/user-service/src/main/resources/application-production.yml b/apps/user-service/src/main/resources/application-production.yml index 6b048fbd..032954ad 100644 --- a/apps/user-service/src/main/resources/application-production.yml +++ b/apps/user-service/src/main/resources/application-production.yml @@ -17,6 +17,10 @@ spring: minimum-idle: 5 pool-name: HikariCP-MyBatis +# quartz: +# jdbc: +# initialize-schema: never + mybatis: mapper-locations: classpath:mybatis/mapper/**/*.xml type-aliases-package: site.icebang.dto diff --git a/apps/user-service/src/main/resources/application.yml b/apps/user-service/src/main/resources/application.yml index df64340e..706eceea 100644 --- a/apps/user-service/src/main/resources/application.yml +++ b/apps/user-service/src/main/resources/application.yml @@ -8,20 +8,6 @@ spring: cache: maxSize: 1 - # Spring Quartz 스케줄러 설정 - quartz: - job-store-type: jdbc - auto-startup: true - jdbc: - initialize-schema: never # Quartz 테이블 스크립트는 직접 실행해야 함 - properties: - org.quartz.scheduler.instanceId: AUTO - org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX - org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate - org.quartz.jobStore.tablePrefix: QRTZ_ # Quartz 테이블 접두사 - org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool - org.quartz.threadPool.threadCount: 10 # 스케줄러가 사용할 스레드 개수 - mybatis: # Mapper XML 파일 위치 mapper-locations: classpath:mapper/**/*.xml From 02df6bd44a218edf6b7359773cdd8033ed05cace Mon Sep 17 00:00:00 2001 From: jihukimme Date: Wed, 17 Sep 2025 14:09:40 +0900 Subject: [PATCH 32/33] =?UTF-8?q?refactor:=20Mapper.xml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/mybatis/mapper/JobMapper.xml | 17 +++++++++++++++++ .../resources/mybatis/mapper/ScheduleMapper.xml | 3 +++ .../resources/mybatis/mapper/TaskMapper.xml | 2 +- .../resources/mybatis/mapper/TaskRunMapper.xml | 3 +++ .../resources/mybatis/mapper/WorkflowMapper.xml | 1 + 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml index 0f530645..54e29ae4 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobMapper.xml @@ -1,7 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml index 90c5e0e5..582af278 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/TaskRunMapper.xml @@ -1,3 +1,6 @@ + + + INSERT INTO task_run (job_run_id, task_id, status, started_at, created_at) diff --git a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml index ae658fca..d10c487a 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowMapper.xml @@ -2,6 +2,7 @@ +