From 155c910e159a659f643e7f78d0eadc5453e1dd4b Mon Sep 17 00:00:00 2001 From: Jihu Kim Date: Thu, 9 Oct 2025 16:46:41 +0900 Subject: [PATCH 01/10] Release v0.1.0-stable (#245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * README.md 작성 및 DTO 네이밍 컨벤션에 맞게 리팩토링 (#239) * docs: README.md 작성 * refactor: DTO 네이밍 컨벤션에 따라 리팩토링 * refactor: Code Formatting * 다이어그램 파일명 수정 (#240) * docs: README.md 작성 * refactor: DTO 네이밍 컨벤션에 따라 리팩토링 * refactor: Code Formatting * fix: 다이어그램 파일명 수정 * Spring Quartz에 Cluster를 이용한 schedule, workflow 실시간 반영 (#238) * feat: applcation-develop.yml에 quartz설정 추가 * feat: QuartzConfig * feat: Quartz에 스케줄 동기화 추가 * feat: ScheduleController * feat: Schedule 단건조회, 스케줄 활성상태 변경 추가 * feat: ScheduleService * feat: ScheduleUpdateDto * feat: workflowController delete,patch,활성화변경, 스케줄 삭제 추가 * feat: workflowMapper 워크플로우 활성화상태 변경 추가 * feat: Schedule Quartz 실시간 반영 관련 내용 추가 * feat: ScheduleManagementE2eTest 작성중 * chore: spotlessApply * feat: schedule workflow_id unique 조건 제거 * fix: schedule 관련 파일들 schedule 폴더로 이동 * feat: scheduleE2eTest 수정 * fix: 중복 코드 삭제 * fix: 검증코드 workflowservice -> scheduleService로 이동 * fix: 오타 수정 * fix: 정적 메서드로 변경 * fix: 정적 메서드로 변경 * fix: 인증 중복 코드 제거 * fix: hasSchedules collection으로 변경 * chore: import 추가 * chore: spotlessApply * refactor: DTO 네이밍 변경 --------- Co-authored-by: jihukimme * docs: 다이어그램 및 시연 영상 업로드 * docs: 시연 영상 유튜브 링크 업로드 * docs: 시연영상 목차 추가 * 목차 및 각 콘텐츠 앵커 링크 도입 (#242) * docs: ERD 추가 및 시퀀스 다이어그램 경로 수정 (#244) --------- Co-authored-by: bwnfo3 <142577603+bwnfo3@users.noreply.github.com> Co-authored-by: Yousung Jung --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b3ebca51..b6ffcb91 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,15 @@ 1. [서비스 개요](#1-서비스-개요) 2. [시스템 개요](#2-시스템-개요) -3. [시스템 아키텍처](#3-시스템-아키텍처) -4. [유스케이스 다이어그램](#4-유스케이스-다이어그램) -5. [시퀀스 다이어그램](#5-시퀀스-다이어그램) -6. [기술 스택](#6-기술-스택) -7. [주요 구성 요소 및 역할](#7-주요-구성-요소-및-역할) -8. [프로젝트 디렉토리 구조](#8-프로젝트-디렉토리-구조) -9. [환경 변수 관리 전략](#9-환경-변수-관리-전략) -10. [시연 영상](#10-시연-영상) +3. [데이터 베이스 설계(ERD)](#3-데이터베이스-설계-erd) +4. [시스템 아키텍처](#4-시스템-아키텍처) +5. [유스케이스 다이어그램](#5-유스케이스-다이어그램) +6. [시퀀스 다이어그램](#6-시퀀스-다이어그램) +7. [기술 스택](#7-기술-스택) +8. [주요 구성 요소 및 역할](#8-주요-구성-요소-및-역할) +9. [프로젝트 디렉토리 구조](#9-프로젝트-디렉토리-구조) +10. [환경 변수 관리 전략](#10-환경-변수-관리-전략) +11. [시연 영상](#11-시연-영상) --- @@ -36,7 +37,51 @@ --- -## 3. 시스템 아키텍처 +## 3. 데이터베이스 설계 (ERD) + +서비스의 모든 데이터를 관리하고 워크플로우의 실행 상태를 추적하기 위해, 역할과 책임에 따라 정규화된 데이터베이스 스키마를 설계했습니다. + +### 3.1. 최종 ERD + +* **ERD 다이어그램**: [ERDCloud 바로가기](https://www.erdcloud.com/d/xkfSpzL2LrRQyCShx) + +### 3.2. ERD 설명 및 설계 원칙 + +데이터베이스는 크게 **'워크플로우 정의', '실행 이력', '사용자 및 조직'** 세 가지 핵심 영역으로 구성됩니다. + +#### **1. 핵심 도메인 테이블** +* **워크플로우 정의 계층**: `WORKFLOW`, `JOB`, `TASK` 및 관계 테이블(`WORKFLOW_JOB`, `JOB_TASK`) +* **실행 이력 계층**: `WORKFLOW_RUN`, `JOB_RUN`, `TASK_RUN`, `TASK_IO_DATA`, `EXECUTION_LOG` +* **사용자 및 권한 계층**: `USER`, `ORGANIZATION`, `ROLE`, `PERMISSION` + +#### **2. JSON 타입을 활용한 동적 설정 관리** + +워크플로우의 **유연성과 재사용성**을 극대화하기 위해, `TASK`와 `WORKFLOW` 테이블의 설정 관련 컬럼에 `JSON` 데이터 타입을 적극적으로 활용했습니다. + +* **`TASK` 테이블의 `parameters` 컬럼**: + * **역할**: Task의 **'정적 설계도(Blueprint)'** 역할을 합니다. + * **내용**: 해당 Task를 실행하기 위해 필요한 고정 정보(예: `endpoint`, `method`)와, Request Body의 **구조 및 각 필드의 데이터 타입**을 JSON 형태로 정의합니다. + * **예시**: `{"endpoint": "/keywords/search", "method": "POST", "body": {"tag": "String"}}` + +* **`WORKFLOW` 테이블의 `default_config` 컬럼**: + * **역할**: 워크플로우가 실행될 때 각 Task에 주입될 **'동적 설정값(Dynamic Configuration)'** 역할을 합니다. + * **내용**: JSON 객체 형태로, `key`는 `task_id`, `value`는 해당 Task에만 적용될 설정값을 가집니다. 이 값은 `TASK`의 `parameters`에 정의된 구조를 덮어쓰거나(override) 보완합니다. + * **예시**: `{"1": {"tag": "google_trends"}, "2": {"some_param": 123}}` + +* **JSON 타입 채택 이유 (유연성 및 재사용성)**: + * **스키마 변경 없는 확장**: 만약 새로운 Task에 `timeout`이라는 파라미터가 추가되더라도, DB 스키마를 변경(`ALTER TABLE`)할 필요 없이 `parameters` JSON의 내용만 수정하면 됩니다. 이는 잦은 변경과 확장이 예상되는 플랫폼에서 **변경에 대한 유연성**을 극대화합니다. + * **Task의 재사용성 증대**: `TASK`는 순수한 '템플릿'으로 존재하고, 실제 동작에 필요한 구체적인 값은 `WORKFLOW`의 `default_config`를 통해 주입됩니다. 이 덕분에 동일한 '키워드 검색 태스크'를 A 워크플로우에서는 `naver`로, B 워크플로우에서는 `google_trends`로 **재배포 없이** 다르게 동작시킬 수 있어 **Task의 재사용성**이 크게 향상됩니다. + * **구조적 데이터 저장**: 단순 `TEXT` 타입과 달리, Key-Value 형태의 구조적인 데이터를 저장할 수 있어 애플리케이션에서 데이터를 파싱하고 사용하기 용이합니다. + +#### **3. 기타 설계 원칙** + +* **네이밍 컨벤션**: 일관성을 위해 테이블 이름은 **단수형**(`user`, `workflow`)으로, PK는 `[table_name]_id` 형식(`workflow_id`)으로 통일했습니다. +* **외래 키(FK) 제약 조건 미설정**: 물리적인 FK 제약 대신 **애플리케이션 레이어에서 참조 무결성을 보장**하여, 데이터 마이그레이션과 배포 유연성을 확보했습니다. +* **인조키(Surrogate Key) 사용**: 다대다 관계의 중간 테이블에도 독립적인 인조키를 PK로 사용하여 **JOIN 성능을 향상**시키고 유지보수성을 높였습니다. + +--- + +## 4. 시스템 아키텍처 역할과 책임을 명확히 분리하기 위해 `Spring Boot`가 **Orchestrator**, `FastAPI`가 **Worker** 역할을 수행하는 이중 레이어 아키텍처를 채택했습니다. @@ -48,7 +93,7 @@ --- -## 4. 유스케이스 다이어그램 +## 5. 유스케이스 다이어그램 시스템의 주요 액터는 **관리자(Admin)** 와 **스케줄러(Scheduler)** 입니다. 관리자는 워크플로우와 스케줄을 관리하고 수동 실행이 가능하며, 스케줄러는 자동 실행을 담당합니다. @@ -57,9 +102,9 @@ --- -## 5. 시퀀스 다이어그램 +## 6. 시퀀스 다이어그램 -### 5.1. 워크플로우 실행 흐름 (스케줄/수동) +### 6.1. 워크플로우 실행 흐름 (스케줄/수동) 1. **트리거**: Quartz 스케줄러 또는 사용자의 `POST /v0/workflows/{id}/run` 요청으로 워크플로우 실행 시작 2. **비동기 실행**: `WorkflowController`가 `WorkflowExecutionService`를 `@Async`로 호출하고 즉시 `202 Accepted` 응답 @@ -69,13 +114,13 @@ #### 수동 실행 -![Sequence Diagram](assets/시퀀스 다이어그램(수동 실행).png) +![Sequence Diagram](assets/시퀀스_다이어그램(수동_실행).png) #### 스케줄 실행 -![Sequence Diagram](assets/시퀀스 다이어그램(스케줄 실행).png) +![Sequence Diagram](assets/시퀀스_다이어그램(스케줄_실행).png) -### 5.2. CI/CD 파이프라인 +### 6.2. CI/CD 파이프라인 GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 → EC2 배포까지 자동화되어 있습니다. @@ -86,7 +131,7 @@ GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 --- -## 6. 기술 스택 +## 7. 기술 스택 ### Backend (Orchestrator - `user-service`) @@ -119,7 +164,7 @@ GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 --- -## 7. 주요 구성 요소 및 역할 +## 8. 주요 구성 요소 및 역할 * **WorkflowExecutionService**: 워크플로우 전체 실행 흐름 제어 * **TaskExecutionService**: Task 실행 및 재시도 정책 관리 @@ -130,7 +175,7 @@ GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 --- -## 8. 프로젝트 디렉토리 구조 +## 9. 프로젝트 디렉토리 구조 Monorepo 형태로, `apps` 하위에 서비스별 디렉토리가 존재합니다. @@ -173,7 +218,7 @@ pre-processing-service/ --- -## 9. 환경 변수 관리 전략 +## 10. 환경 변수 관리 전략 추후 작성 예정 @@ -182,7 +227,7 @@ pre-processing-service/ --- -## 10. 시연 영상 +## 11. 시연 영상 [https://www.youtube.com/watch?v=1vApNttVxVg](https://www.youtube.com/watch?v=1vApNttVxVg) [![Video Label](http://img.youtube.com/vi/1vApNttVxVg/0.jpg)](https://www.youtube.com/watch?v=1vApNttVxVg) From f3a8a4d312b55958be412e97e5a5f5020ce8bccc Mon Sep 17 00:00:00 2001 From: Jihu Kim Date: Mon, 13 Oct 2025 13:20:17 +0900 Subject: [PATCH 02/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6ffcb91..8983bc42 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 Monorepo 형태로, `apps` 하위에 서비스별 디렉토리가 존재합니다. ```bash -Final-4team-icebang/ +backend/ ├── apps/ │ ├── user-service/ # Java/Spring Boot 서비스 │ └── pre-processing-service/# Python/FastAPI 서비스 From 52d124779eda269e2bbf07ab1d2022a0f95493ea Mon Sep 17 00:00:00 2001 From: Jihu Kim Date: Mon, 13 Oct 2025 16:38:48 +0900 Subject: [PATCH 03/10] Update README.md --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8983bc42..3c36dc44 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 블로그 콘텐츠 자동화 플랫폼 +# 동적 워크플로우 자동화 플랫폼 ### AI 기반 워크플로우 오케스트레이터를 활용한 RAG 기반 블로그 콘텐츠 자동화 시스템 @@ -7,7 +7,7 @@ ### 목차 1. [서비스 개요](#1-서비스-개요) -2. [시스템 개요](#2-시스템-개요) +2. [주요 기능](#2-주요-기능) 3. [데이터 베이스 설계(ERD)](#3-데이터베이스-설계-erd) 4. [시스템 아키텍처](#4-시스템-아키텍처) 5. [유스케이스 다이어그램](#5-유스케이스-다이어그램) @@ -23,17 +23,19 @@ ## 1. 서비스 개요 최근 커머스 업계의 AI 기반 콘텐츠 자동화 트렌드에도 불구하고, 여전히 트렌드 분석, 상품 조사, 콘텐츠 생성 및 발행 등 각 단계에서 시간 소모적인 수작업이 필요합니다. -본 프로젝트는 이러한 비효율을 해결하기 위해, **RAG(검색 증강 생성)** 기술을 기반으로 블로그 콘텐츠 생성의 전 과정을 자동화하는 워크플로우 플랫폼을 구축하는 것을 목표로 합니다. ---- +본 프로젝트는 이러한 비효율을 해결하기 위해, 특정 도메인에 국한되지 않고 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 플랫폼을 구축하는 것을 목표로 합니다. 초기 MVP 모델로 RAG(검색 증강 생성) 기술을 활용한 블로그 콘텐츠 자동 생성 및 발행하는 마케팅 워크플로우를 구현했습니다. -## 2. 시스템 개요 +--- -본 시스템은 `Spring Boot` 기반의 오케스트레이터와 `Python(FastAPI)` 기반의 AI 워커로 구성된 **이중 레이어 아키텍처**를 채택하여, 각 서비스의 역할을 명확히 분리했습니다. +## **2. 주요 기능** -* **워크플로우 자동화**: 네이버 데이터 랩의 실시간 트렌드 키워드를 자동 수집하고, 싸다구몰(1688)에서 유사도 기반 검색을 통해 관련 상품을 매칭합니다. -* **AI 콘텐츠 생성**: 수집된 상품 정보와 이미지를 OCR 및 번역 기술로 분석한 후, RAG 기술을 통해 SEO에 최적화된 블로그 콘텐츠를 자동 생성합니다. -* **관리 및 모니터링**: 관리자 전용 대시보드에서 워크플로우 실행, 스케줄 제어, 실행 이력 및 결과를 실시간 모니터링합니다. Grafana로 서버 리소스와 API 상태를 시각적으로 확인할 수 있습니다. + * **워크플로우 자동화**: `Workflow → Job → Task`의 계층적 구조에 따라 복잡한 프로세스를 순차적으로 자동 실행합니다. + * **대표 워크플로우**: **블로그 콘텐츠 자동 생성 및 발행** + * 네이버 데이터 랩 트렌드 분석 → 싸다구몰 상품 정보 수집 → RAG 기반 AI 콘텐츠 생성 → 블로그 자동 발행 + * **동적 스케줄링**: API를 통해 재배포 없이 스케줄을 실시간으로 생성, 수정, 삭제할 수 있습니다. + * **데이터 파이프라이닝**: 이전 Task의 실행 결과(Output)를 다음 Task의 입력(Input)으로 동적으로 전달합니다. + * **관리 및 모니터링**: 관리자 대시보드에서 워크플로우 실행 이력 및 결과를 실시간으로 모니터링하고, Grafana를 통해 서버 리소스를 시각적으로 확인합니다. --- @@ -83,7 +85,7 @@ ## 4. 시스템 아키텍처 -역할과 책임을 명확히 분리하기 위해 `Spring Boot`가 **Orchestrator**, `FastAPI`가 **Worker** 역할을 수행하는 이중 레이어 아키텍처를 채택했습니다. +역할과 책임을 명확히 분리하기 위해 `Spring Boot`가 **Orchestrator**, `FastAPI`가 **AI Worker** 역할을 수행하는 이중 레이어 아키텍처를 채택했습니다. * **Spring Boot (Orchestrator)**: `Workflow → Job → Task` 구조를 기반으로 전체 비즈니스 흐름을 제어합니다. 스케줄링(`Quartz`), 상태 관리, 데이터 영속성, 인증/인가 등 핵심 로직 담당. * **FastAPI (Worker)**: 키워드 추출, 상품 검색, 웹 크롤링, AI 연동(RAG), OCR 등 Python 생태계 특화 작업을 담당. @@ -180,7 +182,7 @@ GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 Monorepo 형태로, `apps` 하위에 서비스별 디렉토리가 존재합니다. ```bash -backend/ +FlowWeaver/backend/ ├── apps/ │ ├── user-service/ # Java/Spring Boot 서비스 │ └── pre-processing-service/# Python/FastAPI 서비스 From 474b239d2b8fead0087247675bbcd2c008429fe8 Mon Sep 17 00:00:00 2001 From: Jihu Kim Date: Tue, 14 Oct 2025 16:52:45 +0900 Subject: [PATCH 04/10] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3c36dc44..182b15c6 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ GitHub Actions 기반으로 빌드 → 테스트 → Docker 빌드 및 푸시 * **WorkflowExecutionService**: 워크플로우 전체 실행 흐름 제어 * **TaskExecutionService**: Task 실행 및 재시도 정책 관리 * **TaskBodyBuilder (전략 패턴)**: 각 Task별 동적 Request Body 생성 +* **WorkflowContextService (퍼사드 패턴)**: 각 TaskBodyBuilder가 이전 Task의 결과를 조회할 때 필요한 복잡한 DB 접근 로직(TaskIoDataMapper 사용 등)을 캡슐화하고, 단순화된 인터페이스를 제공하는 퍼사드(Facade) 역할 * **FastApiAdapter**: FastAPI 서버 통신 캡슐화 * **QuartzSchedulerInitializer**: DB 스케줄 정보 Quartz 엔진 동기화 * **ExecutionMdcManager**: 비동기 환경에서도 traceId 기반 분산 추적 로깅 From cd56df593c04064f4dca374d5b04b7f5ff7fff98 Mon Sep 17 00:00:00 2001 From: Jihu Kim Date: Wed, 3 Dec 2025 15:53:21 +0900 Subject: [PATCH 05/10] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 182b15c6..58d96423 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 동적 워크플로우 자동화 플랫폼 +# 동적 워크플로우 관 플랫폼 -### AI 기반 워크플로우 오케스트레이터를 활용한 RAG 기반 블로그 콘텐츠 자동화 시스템 +### AI 기반 자동화 트렌드에 맞추어 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 관리 플랫폼 --- @@ -24,7 +24,7 @@ 최근 커머스 업계의 AI 기반 콘텐츠 자동화 트렌드에도 불구하고, 여전히 트렌드 분석, 상품 조사, 콘텐츠 생성 및 발행 등 각 단계에서 시간 소모적인 수작업이 필요합니다. -본 프로젝트는 이러한 비효율을 해결하기 위해, 특정 도메인에 국한되지 않고 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 플랫폼을 구축하는 것을 목표로 합니다. 초기 MVP 모델로 RAG(검색 증강 생성) 기술을 활용한 블로그 콘텐츠 자동 생성 및 발행하는 마케팅 워크플로우를 구현했습니다. +본 프로젝트는 이러한 비효율을 해결하기 위해, 특정 도메인에 국한되지 않고 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 플랫폼을 구축하는 것을 목표로 합니다. 초기 MVP 모델로 RAG(검색 증강 생성) 기반 블로그 콘텐츠 자동 생성 및 발행하는 마케팅 워크플로우를 구현했습니다. --- From 9666afbc041e560ed7c6e302b2763555bd7892f5 Mon Sep 17 00:00:00 2001 From: Jihu Kim Date: Sun, 28 Dec 2025 03:25:33 +0900 Subject: [PATCH 06/10] =?UTF-8?q?README.md=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 58d96423..d6b58260 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 동적 워크플로우 관 플랫폼 +# 동적 워크플로우 관리 플랫폼 ### AI 기반 자동화 트렌드에 맞추어 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 관리 플랫폼 From 673e964fe4a2368aed3a5e3bdf610f1759882dea Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 22 Jan 2026 12:14:08 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EB=B9=84=EC=A0=95=EC=83=81=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EB=B3=B5=EA=B5=AC=20=ED=94=84=EB=A1=9C=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Mapper=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/dto/RequestContextDto.java | 10 +++ .../workflow/mapper/WorkflowRunMapper.java | 11 ++- .../config/WorkflowRecoveryInitializer.java | 77 +++++++++++++++++++ .../mybatis/mapper/WorkflowRunMapper.xml | 4 + 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContextDto.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContextDto.java index 66ef57aa..47c8c5e2 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContextDto.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/dto/RequestContextDto.java @@ -19,4 +19,14 @@ public class RequestContextDto { public static RequestContextDto forScheduler(String traceId) { return new RequestContextDto(traceId, "scheduler", "quartz-scheduler"); } + + /** + * 시스템 복구 실행용 컨텍스트를 생성하는 정적 팩토리 메서드입니다. + * + * @param traceId 기존 실행에서 사용하던 추적 ID + * @return 복구용 RequestContext 객체 + */ + public static RequestContextDto forRecovery(String traceId) { + return new RequestContextDto(traceId, "system-recovery", "workflow-system-recovery"); + } } diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java index 64bbcbc6..657065a5 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java @@ -1,12 +1,15 @@ package site.icebang.domain.workflow.mapper; +import java.util.List; import org.apache.ibatis.annotations.Mapper; - +import org.apache.ibatis.annotations.Param; import site.icebang.domain.workflow.model.WorkflowRun; @Mapper public interface WorkflowRunMapper { - void insert(WorkflowRun workflowRun); + void insert(WorkflowRun workflowRun); + + void update(WorkflowRun workflowRun); - void update(WorkflowRun workflowRun); -} + List findByStatus(@Param("status") String status); +} \ No newline at end of file diff --git a/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java b/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java new file mode 100644 index 00000000..ce9224bf --- /dev/null +++ b/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java @@ -0,0 +1,77 @@ +package site.icebang.global.config; + +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.workflow.dto.RequestContextDto; +import site.icebang.domain.workflow.mapper.WorkflowRunMapper; +import site.icebang.domain.workflow.model.WorkflowRun; +import site.icebang.domain.workflow.service.WorkflowExecutionService; + +/** + * 애플리케이션 시작 시, 비정상 종료된 워크플로우를 감지하고 '실제로 복구(재실행)'하는 클래스입니다. + * + *

서버가 시작될 때 DB에 'RUNNING' 상태로 남아있는 워크플로우는 + * 시스템 장애(전원 차단, OOM 등)로 인해 중단된 작업입니다. + * 이 클래스는 해당 작업들을 찾아내어 이전 기록을 정리하고, 작업을 자동으로 재시작합니다. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class WorkflowRecoveryInitializer implements ApplicationRunner { + + private final WorkflowRunMapper workflowRunMapper; + private final WorkflowExecutionService workflowExecutionService; + + @Override + public void run(ApplicationArguments args) { + try { + List runningWorkflows = workflowRunMapper.findByStatus("RUNNING"); + + if (runningWorkflows.isEmpty()) { + return; + } + + log.warn("비정상 종료된 워크플로우 {}건 발견. 자동 복구(재실행) 프로세스를 시작합니다.", runningWorkflows.size()); + + int recoveredCount = 0; + for (WorkflowRun run : runningWorkflows) { + if (recoverAndRestart(run)) { + recoveredCount++; + } + } + log.info("총 {}건의 워크플로우가 성공적으로 복구(재실행 요청)되었습니다.", recoveredCount); + + } catch (Exception e) { + log.error("워크플로우 복구 프로세스 전체 실패", e); + } + } + + private boolean recoverAndRestart(WorkflowRun run) { + try { + // 1. 기존 실행 이력 마감 (Cleaning) + // 중단된 시점의 상태를 FAILED로 확정지어 데이터 정합성을 맞춥니다. + run.finish("FAILED"); + workflowRunMapper.update(run); + log.info("[복구 1단계] 기존 이력 정리 완료: RunID={}, WorkflowID={}", run.getId(), run.getWorkflowId()); + + // 2. 워크플로우 재실행 (Restarting) + // 기존 Trace ID를 승계하여 로그 추적성을 유지하며 서비스를 다시 시작합니다. + RequestContextDto recoveryContext = RequestContextDto.forRecovery(run.getTraceId()); + + workflowExecutionService.executeWorkflow(run.getWorkflowId(), recoveryContext); + log.info("[복구 2단계] 워크플로우 재실행 요청 완료: WorkflowID={}, TraceID={}", run.getWorkflowId(), run.getTraceId()); + + return true; + } catch (Exception e) { + log.error("워크플로우 개별 복구 실패: RunID={}", run.getId(), e); + return false; + } + } +} \ 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 8011fc6c..74883182 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/WorkflowRunMapper.xml @@ -25,4 +25,8 @@ WHERE id = #{id} + + \ No newline at end of file From a169546bd97b8417aa5075feeec27dae642083a6 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 22 Jan 2026 12:30:46 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EC=84=B1=EA=B3=B5=ED=95=9C=20Job?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8=EC=9A=A9=20Mapper=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9B=8C=ED=81=AC?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=8B=A4=ED=96=89=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/workflow/mapper/JobRunMapper.java | 13 +++++++++---- .../workflow/service/WorkflowExecutionService.java | 11 ++++++++++- .../main/resources/mybatis/mapper/JobRunMapper.xml | 11 +++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java index a3546069..bde3537c 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java @@ -1,12 +1,17 @@ package site.icebang.domain.workflow.mapper; import org.apache.ibatis.annotations.Mapper; - +import org.apache.ibatis.annotations.Param; import site.icebang.domain.workflow.model.JobRun; @Mapper public interface JobRunMapper { - void insert(JobRun jobRun); + void insert(JobRun jobRun); + + void update(JobRun jobRun); - void update(JobRun jobRun); -} + JobRun findSuccessfulJobByTraceId( + @Param("traceId") String traceId, + @Param("jobId") Long jobId + ); +} \ 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 b18b87fe..62877eee 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 @@ -85,6 +85,15 @@ public void executeWorkflow(Long workflowId, RequestContextDto context) { for (JobDto jobDto : jobDtos) { Job job = new Job(jobDto); mdcManager.setJobContext(job.getId()); + + // 이미 성공적으로 수행된 Job인지 확인합니다. + JobRun existingSuccessfulJob = jobRunMapper.findSuccessfulJobByTraceId(context.getTraceId(), job.getId()); + if (existingSuccessfulJob != null) { + workflowLogger.info("---------- Job 스킵 (이미 성공함): JobId={}, PreviousJobRunId={} ----------", + job.getId(), existingSuccessfulJob.getId()); + continue; // 이미 성공했으므로 실행하지 않고 다음 Job으로 넘어갑니다. + } + JobRun jobRun = JobRun.start(workflowRun.getId(), job.getId()); jobRunMapper.insert(jobRun); workflowLogger.info( @@ -207,4 +216,4 @@ private void saveIoData(Long taskRunId, String ioType, String name, JsonNode dat workflowLogger.error("Task IO 데이터 저장 실패: TaskRunId={}, Type={}", taskRunId, ioType, e); } } -} +} \ 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 2cc51d78..927cbafe 100644 --- a/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml +++ b/apps/user-service/src/main/resources/mybatis/mapper/JobRunMapper.xml @@ -25,4 +25,15 @@ WHERE id = #{id} + + \ No newline at end of file From 465b31c5e80af9b729811510e56bcffae0395796 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 22 Jan 2026 12:34:38 +0900 Subject: [PATCH 09/10] refactor: Code formatting and alignment adjustments across core classes --- .../domain/workflow/mapper/JobRunMapper.java | 12 ++- .../workflow/mapper/WorkflowRunMapper.java | 10 ++- .../service/WorkflowExecutionService.java | 15 ++-- .../config/WorkflowRecoveryInitializer.java | 88 ++++++++++--------- 4 files changed, 65 insertions(+), 60 deletions(-) diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java index bde3537c..6249f259 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/JobRunMapper.java @@ -2,16 +2,14 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; + import site.icebang.domain.workflow.model.JobRun; @Mapper public interface JobRunMapper { - void insert(JobRun jobRun); + void insert(JobRun jobRun); - void update(JobRun jobRun); + void update(JobRun jobRun); - JobRun findSuccessfulJobByTraceId( - @Param("traceId") String traceId, - @Param("jobId") Long jobId - ); -} \ No newline at end of file + JobRun findSuccessfulJobByTraceId(@Param("traceId") String traceId, @Param("jobId") Long jobId); +} diff --git a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java index 657065a5..c644c974 100644 --- a/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java +++ b/apps/user-service/src/main/java/site/icebang/domain/workflow/mapper/WorkflowRunMapper.java @@ -1,15 +1,17 @@ package site.icebang.domain.workflow.mapper; import java.util.List; + import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; + import site.icebang.domain.workflow.model.WorkflowRun; @Mapper public interface WorkflowRunMapper { - void insert(WorkflowRun workflowRun); + void insert(WorkflowRun workflowRun); - void update(WorkflowRun workflowRun); + void update(WorkflowRun workflowRun); - List findByStatus(@Param("status") String status); -} \ No newline at end of file + List findByStatus(@Param("status") String status); +} 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 62877eee..1e4c2ab9 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 @@ -85,13 +85,16 @@ public void executeWorkflow(Long workflowId, RequestContextDto context) { for (JobDto jobDto : jobDtos) { Job job = new Job(jobDto); mdcManager.setJobContext(job.getId()); - + // 이미 성공적으로 수행된 Job인지 확인합니다. - JobRun existingSuccessfulJob = jobRunMapper.findSuccessfulJobByTraceId(context.getTraceId(), job.getId()); + JobRun existingSuccessfulJob = + jobRunMapper.findSuccessfulJobByTraceId(context.getTraceId(), job.getId()); if (existingSuccessfulJob != null) { - workflowLogger.info("---------- Job 스킵 (이미 성공함): JobId={}, PreviousJobRunId={} ----------", - job.getId(), existingSuccessfulJob.getId()); - continue; // 이미 성공했으므로 실행하지 않고 다음 Job으로 넘어갑니다. + workflowLogger.info( + "---------- Job 스킵 (이미 성공함): JobId={}, PreviousJobRunId={} ----------", + job.getId(), + existingSuccessfulJob.getId()); + continue; // 이미 성공했으므로 실행하지 않고 다음 Job으로 넘어갑니다. } JobRun jobRun = JobRun.start(workflowRun.getId(), job.getId()); @@ -216,4 +219,4 @@ private void saveIoData(Long taskRunId, String ioType, String name, JsonNode dat workflowLogger.error("Task IO 데이터 저장 실패: TaskRunId={}, Type={}", taskRunId, ioType, e); } } -} \ No newline at end of file +} diff --git a/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java b/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java index ce9224bf..33cd20bd 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/WorkflowRecoveryInitializer.java @@ -17,61 +17,63 @@ /** * 애플리케이션 시작 시, 비정상 종료된 워크플로우를 감지하고 '실제로 복구(재실행)'하는 클래스입니다. * - *

서버가 시작될 때 DB에 'RUNNING' 상태로 남아있는 워크플로우는 - * 시스템 장애(전원 차단, OOM 등)로 인해 중단된 작업입니다. - * 이 클래스는 해당 작업들을 찾아내어 이전 기록을 정리하고, 작업을 자동으로 재시작합니다. + *

서버가 시작될 때 DB에 'RUNNING' 상태로 남아있는 워크플로우는 시스템 장애(전원 차단, OOM 등)로 인해 중단된 작업입니다. 이 클래스는 해당 작업들을 + * 찾아내어 이전 기록을 정리하고, 작업을 자동으로 재시작합니다. */ @Slf4j @Component @RequiredArgsConstructor public class WorkflowRecoveryInitializer implements ApplicationRunner { - private final WorkflowRunMapper workflowRunMapper; - private final WorkflowExecutionService workflowExecutionService; + private final WorkflowRunMapper workflowRunMapper; + private final WorkflowExecutionService workflowExecutionService; - @Override - public void run(ApplicationArguments args) { - try { - List runningWorkflows = workflowRunMapper.findByStatus("RUNNING"); - - if (runningWorkflows.isEmpty()) { - return; - } + @Override + public void run(ApplicationArguments args) { + try { + List runningWorkflows = workflowRunMapper.findByStatus("RUNNING"); - log.warn("비정상 종료된 워크플로우 {}건 발견. 자동 복구(재실행) 프로세스를 시작합니다.", runningWorkflows.size()); + if (runningWorkflows.isEmpty()) { + return; + } - int recoveredCount = 0; - for (WorkflowRun run : runningWorkflows) { - if (recoverAndRestart(run)) { - recoveredCount++; - } - } - log.info("총 {}건의 워크플로우가 성공적으로 복구(재실행 요청)되었습니다.", recoveredCount); - - } catch (Exception e) { - log.error("워크플로우 복구 프로세스 전체 실패", e); + log.warn("비정상 종료된 워크플로우 {}건 발견. 자동 복구(재실행) 프로세스를 시작합니다.", runningWorkflows.size()); + + int recoveredCount = 0; + for (WorkflowRun run : runningWorkflows) { + if (recoverAndRestart(run)) { + recoveredCount++; } + } + log.info("총 {}건의 워크플로우가 성공적으로 복구(재실행 요청)되었습니다.", recoveredCount); + + } catch (Exception e) { + log.error("워크플로우 복구 프로세스 전체 실패", e); } + } - private boolean recoverAndRestart(WorkflowRun run) { - try { - // 1. 기존 실행 이력 마감 (Cleaning) - // 중단된 시점의 상태를 FAILED로 확정지어 데이터 정합성을 맞춥니다. - run.finish("FAILED"); - workflowRunMapper.update(run); - log.info("[복구 1단계] 기존 이력 정리 완료: RunID={}, WorkflowID={}", run.getId(), run.getWorkflowId()); + private boolean recoverAndRestart(WorkflowRun run) { + try { + // 1. 기존 실행 이력 마감 (Cleaning) + // 중단된 시점의 상태를 FAILED로 확정지어 데이터 정합성을 맞춥니다. + run.finish("FAILED"); + workflowRunMapper.update(run); + log.info("[복구 1단계] 기존 이력 정리 완료: RunID={}, WorkflowID={}", run.getId(), run.getWorkflowId()); - // 2. 워크플로우 재실행 (Restarting) - // 기존 Trace ID를 승계하여 로그 추적성을 유지하며 서비스를 다시 시작합니다. - RequestContextDto recoveryContext = RequestContextDto.forRecovery(run.getTraceId()); + // 2. 워크플로우 재실행 (Restarting) + // 기존 Trace ID를 승계하여 로그 추적성을 유지하며 서비스를 다시 시작합니다. + RequestContextDto recoveryContext = RequestContextDto.forRecovery(run.getTraceId()); - workflowExecutionService.executeWorkflow(run.getWorkflowId(), recoveryContext); - log.info("[복구 2단계] 워크플로우 재실행 요청 완료: WorkflowID={}, TraceID={}", run.getWorkflowId(), run.getTraceId()); - - return true; - } catch (Exception e) { - log.error("워크플로우 개별 복구 실패: RunID={}", run.getId(), e); - return false; - } + workflowExecutionService.executeWorkflow(run.getWorkflowId(), recoveryContext); + log.info( + "[복구 2단계] 워크플로우 재실행 요청 완료: WorkflowID={}, TraceID={}", + run.getWorkflowId(), + run.getTraceId()); + + return true; + } catch (Exception e) { + log.error("워크플로우 개별 복구 실패: RunID={}", run.getId(), e); + return false; } -} \ No newline at end of file + } +} From 5c35e109358c007753d2ffebf57c9ba822d616f8 Mon Sep 17 00:00:00 2001 From: jihukimme Date: Thu, 22 Jan 2026 12:56:12 +0900 Subject: [PATCH 10/10] refactor: Code formatting and alignment adjustments across core classes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9a1e2af..d6b58260 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ 최근 커머스 업계의 AI 기반 콘텐츠 자동화 트렌드에도 불구하고, 여전히 트렌드 분석, 상품 조사, 콘텐츠 생성 및 발행 등 각 단계에서 시간 소모적인 수작업이 필요합니다. -본 프로젝트는 이러한 비효율을 해결하기 위해, 특정 도메인에 국한되지 않고 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 플랫폼을 구축하는 것을 목표로 합니다. 초기 MVP 모델로 RAG(검색 증강 생성) 기술을 활용한 블로그 콘텐츠 자동 생성 및 발행하는 마케팅 워크플로우를 구현했습니다. +본 프로젝트는 이러한 비효율을 해결하기 위해, 특정 도메인에 국한되지 않고 다양한 비즈니스 프로세스를 자동화할 수 있는 확장 가능한 워크플로우 플랫폼을 구축하는 것을 목표로 합니다. 초기 MVP 모델로 RAG(검색 증강 생성) 기반 블로그 콘텐츠 자동 생성 및 발행하는 마케팅 워크플로우를 구현했습니다. ---