Skip to content

Commit 272c73a

Browse files
committed
workflow 작성중
1 parent 75fb1ec commit 272c73a

File tree

4 files changed

+516
-0
lines changed

4 files changed

+516
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ArticleItem } from "@modules/article/types";
2+
import type { Category } from "@modules/category";
3+
import dayjs from "dayjs";
4+
import i1 from "./w.jpg";
5+
6+
export const title =
7+
"[TypeScript] 복구(롤백) 로직이 있는 경량 워크플로우 시스템 적용하기";
8+
export const url = "https://springfall.cc/article/2025-08/workflow";
9+
export const summary =
10+
"일련의 작업들이 모두 다 잘 진행되어야 비로소 성공하는, 하지만 각 단계에서 실패를 적절히 처리해야 하는 워크플로우 시스템을 간단히 만들어봅시다";
11+
12+
export const createdAt = dayjs("2025-08-18").toISOString();
13+
export const updatedAt = dayjs("2025-08-18").toISOString();
14+
export const image = i1;
15+
export const imageAlt = "TODO:이미지수정";
16+
export const category: Category = "기술";
17+
18+
export const item: ArticleItem = {
19+
createdAt,
20+
updatedAt,
21+
image,
22+
imageAlt,
23+
summary,
24+
title,
25+
url,
26+
category,
27+
tags: [],
28+
};
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import ArticleHeader from "@modules/article/ArticleHeader";
2+
import ArticleImage from "@modules/article/ArticleImage";
3+
import getArticleHeaderProps from "@modules/metadata/getArticleHeaderProps";
4+
import getArticleJsonLdProps from "@modules/metadata/getArticleJsonLdProps";
5+
import getArticleMetadata from "@modules/metadata/getArticleMetadata";
6+
import { ArticleJsonLd } from "next-seo";
7+
import { item } from "./metadata";
8+
import i1 from "./w.jpg";
9+
10+
export const metadata = getArticleMetadata(item);
11+
12+
<ArticleJsonLd {...getArticleJsonLdProps(item)} />
13+
14+
<ArticleHeader {...getArticleHeaderProps(item)} />
15+
16+
<ArticleImage
17+
img={i1}
18+
border
19+
alt=""
20+
caption={
21+
<>
22+
사진:{" "}
23+
<a href="https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%99%94%EC%9D%B4%ED%8A%B8-%EB%B3%B4%EB%93%9C%EC%97%90-%EA%B8%80%EC%9D%84-%EC%93%B0%EB%8A%94-%EB%82%A8%EC%9E%90---kQ4tBklJI?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">
24+
Unsplash
25+
</a>{" "}
26+
by{" "}
27+
<a href="https://unsplash.com/ko/@campaign_creators?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">
28+
Campaign Creators
29+
</a>
30+
</>
31+
}
32+
/>
33+
34+
## 목차
35+
36+
## 간단 용어 설명
37+
38+
워크플로우는 일반적인 용어이지만 보통 **반복**되는 **일련**의 작업들을 뜻합니다.
39+
40+
- 반복: 재사용할 수 있다는 의미입니다.
41+
- 일련: 특정한 순서가 있다는 의미입니다.
42+
43+
여기서 하나의 작업은 보통 스텝(Step)이라는 용어로 지칭합니다.
44+
45+
## 워크플로우가 필요한 시나리오
46+
47+
요구사항은 다음과 같습니다.
48+
49+
- 상품을 구매했을 때 연관 상품을 추천해주는 캠페인 하나를 진행하고자 합니다.
50+
- 이 캠페인으로는 각 고객에게 메시지가 딱 한번만 나가야 합니다.
51+
- 고객에게 메시지를 보내려면 적절한 이미지를 미리 업로드해야 합니다.
52+
53+
이를 바탕으로 로직을 한번 생각해봅시다.
54+
55+
1. 보낸 적이 있는지 체크합니다. (DB에서 확인합니다)
56+
2. 보내지 않았다면 보냈다는 체크를 우선 해놓습니다. (DB에 기록을 insert 합니다)
57+
3. 이미지를 업로드합니다. (이미지 링크를 얻습니다)
58+
4. 이미지 링크를 포함하여 메시지를 전송합니다.
59+
60+
자, 여기서 오류가 발생했을 시 다음과 같은 처리도 필요합니다.
61+
62+
- 3번 이미지 업로드에 실패한다면 2번에서 해놨던 체크를 되돌려야 합니다.(DB Delete)
63+
- 4번 메시지 전송에 실패한다면 2번에서 해놨던 체크를 되돌리고(DB Delete) 업로드한 이미지도 제거합니다.
64+
65+
이를 코드로 표현하면 다음과 같습니다.
66+
67+
```typescript
68+
async function runCampaign() {
69+
const checkResult = await checkCanSendMessage(); // 보낸 적이 있는지 체크. `true`면 진행.
70+
if (!checkResult) {
71+
console.warn("이미 메시지를 보냈어요.");
72+
return;
73+
}
74+
75+
const insertedLogId = await insertLog();
76+
try {
77+
const uploadedImage = await uploadImage(); // 이미지 업로드
78+
try {
79+
return await sendMessage();
80+
} catch (e) {
81+
deleteImage(uploadedImage); // 업로드한 이미지를 삭제합니다.
82+
throw e;
83+
}
84+
} catch (e) {
85+
deleteLog(insertedLogId); // 체크를 되돌립니다 (DB Delete)
86+
throw e;
87+
}
88+
}
89+
```
90+
91+
자자자... 이제 약간 단점이 보입니다.
92+
93+
1.
94+
95+
## 요약
96+
97+
sdflaksd
98+
99+
````typescript
100+
/**
101+
* Workflow Step 인터페이스
102+
*
103+
* @template C 모든 Step에서 공유하는 컨텍스트 타입
104+
* @template A 이 Step의 action이 반환하는 rollback argument 타입
105+
*/
106+
export interface IStep<C, A> {
107+
/**
108+
* 실제 업무 로직
109+
* - ctx에 결과를 기록하고, rollback에 넘길 값을 리턴
110+
* - 타입스크립트가 ctx의 타입과 반환값을 자동으로 추론하도록 구현할 것
111+
* @param ctx 공용 컨텍스트 객체
112+
* @returns rollback 실행 시 필요한 데이터 (예: 생성된 리소스 ID, 변경 전 상태 등)
113+
*/
114+
action: (ctx: C) => Promise<A> | A;
115+
116+
/**
117+
* 실패 시 롤백 로직 (선택적)
118+
* - 이후 step에서 에러 발생 시 역순으로 호출됨
119+
* - action의 반환값을 받아서 롤백 작업 수행
120+
* @param ctx 공용 컨텍스트 객체
121+
* @param arg action에서 반환된 값
122+
*/
123+
rollback?: (ctx: C, arg: A) => Promise<void>;
124+
}
125+
126+
/**
127+
* Workflow 조기 종료를 위한 신호 클래스
128+
*
129+
* Step의 action에서 이 객체를 반환하면 나머지 step들을 건너뛰고
130+
* workflow를 정상 종료합니다 (rollback은 실행되지 않음)
131+
*
132+
* @example
133+
* ```typescript
134+
* const checkConditionStep = createStep(
135+
* async (ctx: MyContext) => {
136+
* if (ctx.shouldSkip) {
137+
* return Workflow.exit("조건이 맞지 않아 워크플로우를 종료합니다");
138+
* }
139+
* return null;
140+
* }
141+
* );
142+
* ```
143+
*/
144+
export class WorkflowStop {
145+
constructor(public message?: string) {
146+
// Set the prototype explicitly to ensure instanceof works correctly!
147+
Object.setPrototypeOf(this, WorkflowStop.prototype);
148+
}
149+
}
150+
151+
/**
152+
* 여러 Step을 순차 실행하고, 실패 시 역순으로 rollback을 수행하는 Workflow 클래스
153+
*
154+
* **주의사항: rollback이 필요없는 상황에서는 이 Workflow를 사용하지 마세요!**
155+
* - 단순한 순차 처리만 필요한 경우 일반적인 함수 호출을 사용하세요
156+
* - 이 클래스는 복잡한 트랜잭션에서 rollback이 필요할 때만 사용하는 것이 목적입니다
157+
*
158+
* @template C 모든 Step에서 공유하는 컨텍스트 타입
159+
*
160+
* @example
161+
* ```typescript
162+
* interface MyContext {
163+
* userId: string;
164+
* createdResourceIds: string[];
165+
* }
166+
*
167+
* const workflow = new Workflow<MyContext>([
168+
* createResourceStep, // DB에 리소스 생성
169+
* uploadFileStep, // 파일 업로드
170+
* sendNotificationStep // 알림 발송
171+
* ]);
172+
*
173+
* try {
174+
* await workflow.execute({ userId: "123", createdResourceIds: [] });
175+
* } catch (error) {
176+
* // 실패한 지점부터 역순으로 rollback이 자동 실행됨
177+
* console.error("Workflow failed:", error);
178+
* }
179+
* ```
180+
*/
181+
export class Workflow<C> {
182+
/**
183+
* Workflow 조기 종료를 위한 헬퍼 메서드
184+
* @param message 종료 사유 (선택적)
185+
* @returns WorkflowStop 인스턴스
186+
*/
187+
static exit(message?: string) {
188+
return new WorkflowStop(message);
189+
}
190+
191+
/**
192+
* Workflow를 구성하는 Step들의 배열
193+
*/
194+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
195+
private steps: Array<IStep<C, any>>;
196+
197+
/**
198+
* Workflow 생성자
199+
* @param steps 순차 실행할 Step들의 배열
200+
*/
201+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
202+
constructor(steps: Array<IStep<C, any>>) {
203+
this.steps = steps;
204+
}
205+
206+
/**
207+
* Workflow 실행 메서드
208+
*
209+
* 1. Step들을 순차적으로 실행
210+
* 2. 각 Step의 action 결과를 rollback 용으로 저장
211+
* 3. 실패 시 완료된 Step들을 역순으로 rollback 실행
212+
* 4. WorkflowStop이 반환되면 조기 종료 (정상 종료)
213+
*
214+
* @param ctx 모든 Step에서 공유하는 컨텍스트 객체
215+
* @throws 실행 중 발생한 에러 (rollback 실행 후 재전파)
216+
*/
217+
public async execute(ctx: C): Promise<void> {
218+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
219+
const completed: Array<{ step: IStep<C, any>; arg: any }> = [];
220+
221+
for (const step of this.steps) {
222+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
223+
let res: any;
224+
try {
225+
res = await step.action(ctx);
226+
} catch (err) {
227+
// 에러면 rollback만 하고 에러 재전파
228+
for (const { step: s, arg } of completed.reverse()) {
229+
try {
230+
await s.rollback?.(ctx, arg);
231+
} catch (e) {
232+
// rollback 실패는 무시하고 계속 진행
233+
// eslint-disable-next-line no-console
234+
console.error("Rollback failed:", e);
235+
}
236+
}
237+
throw err;
238+
}
239+
240+
// early exit 신호면, 그냥 종료
241+
if (res instanceof WorkflowStop) {
242+
return;
243+
}
244+
245+
// 정상 compArg를 쌓고 다음 스텝으로
246+
completed.push({ step, arg: res });
247+
}
248+
}
249+
}
250+
251+
/**
252+
* Step 생성을 위한 헬퍼 함수
253+
*
254+
* TypeScript의 타입 추론을 활용하여 action 함수의 매개변수와 반환값을 자동으로 추론합니다.
255+
*
256+
* @template C 컨텍스트 타입 (action 함수의 매개변수에서 자동 추론)
257+
* @template A action 반환값 타입 (action 함수의 반환값에서 자동 추론)
258+
* @param action Step의 실행 로직
259+
* @param rollback Step의 롤백 로직 (선택적)
260+
* @returns IStep 인터페이스를 구현한 객체
261+
*
262+
* @example
263+
* ```typescript
264+
* // 타입이 자동으로 추론됨: IStep<{ userId: string; resourceId?: string }, string>
265+
* // \@returns ctx.resourceId
266+
* export const createResourceStep = createStep(
267+
* async (ctx: { userId: string; resourceId?: string }) => {
268+
* const resourceId = await createResource(ctx.userId);
269+
* ctx.resourceId = resourceId;
270+
* return resourceId; // rollback에서 삭제할 때 사용
271+
* },
272+
* async (ctx, resourceId) => {
273+
* await deleteResource(resourceId);
274+
* }
275+
* );
276+
*
277+
* // rollback이 없는 단순한 step
278+
* // @returns ctx.vendorInfo
279+
* export const getVendorInfoStep = createStep(
280+
* async (ctx: { sitePublicId: string; vendorInfo?: VendorInfo }) => {
281+
* ctx.vendorInfo = await getVendorInfo(ctx.sitePublicId);
282+
* return ctx.vendorInfo;
283+
* }
284+
* );
285+
* ```
286+
*/
287+
export function createStep<C, A>(
288+
action: (ctx: C) => Promise<A> | A,
289+
rollback?: (ctx: C, arg: A) => Promise<void>,
290+
): IStep<C, A> {
291+
return { action, rollback };
292+
}
293+
````
294+
295+
- https://help.asana.com/s/article/what-is-a-workflow
245 KB
Loading

0 commit comments

Comments
 (0)