๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Workflow ์‹คํ–‰ ์ค‘ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์™ธ๋ถ€ API ํ˜ธ์ถœ, ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ, ์ผ์‹œ์  ์žฅ์•  ๋“ฑ์œผ๋กœ ์ธํ•œ ์‹คํŒจ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„ํ•˜๋Š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

Workflow์—์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ๋Š” ์ผ๋ฐ˜์ ์ธ try-catch ๋ฌธ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์—๋Ÿฌ๋ฅผ ๋กœ๊น…ํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์žฌ๋ฐœ์ƒ์‹œ์ผœ workflow๋ฅผ ์‹คํŒจ ์ƒํƒœ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
import { workflow } from "sonamu";

export const processPayment = workflow(
  { name: "process_payment" },
  async ({ input, step, logger }) => {
    try {
      await step.define({ name: "charge" }, async () => {
        await paymentGateway.charge(input.amount);
      }).run();
      
      return { success: true };
    } catch (error) {
      logger.error("Payment failed", { error, input });
      
      // ์—๋Ÿฌ ์žฌ๋ฐœ์ƒ (workflow ์‹คํŒจ)
      throw error;
    }
  }
);
ํ•ต์‹ฌ ๊ฐœ๋…:
  • logger.error()๋กœ ์—๋Ÿฌ ๊ธฐ๋ก
  • throw error๋กœ workflow ์‹คํŒจ ์ฒ˜๋ฆฌ
  • ์‹คํŒจํ•œ workflow๋Š” Worker๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„

Step๋ณ„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

๋ชจ๋“  step์ด ํ•„์ˆ˜๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ์ผ๋ถ€ step์€ ์‹คํŒจํ•ด๋„ workflow๋ฅผ ๊ณ„์† ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์„ ํƒ์  step์€ try-catch๋กœ ๊ฐ์‹ธ์„œ ์—๋Ÿฌ๋ฅผ ํก์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.
export const syncData = workflow(
  { name: "sync_data" },
  async ({ input, step, logger }) => {
    // Step 1: ํ•„์ˆ˜ ์ž‘์—…
    const data = await step.define({ name: "fetch_data" }, async () => {
      return await fetchFromAPI(input.url);
    }).run();
    
    // Step 2: ์„ ํƒ์  ์ž‘์—… (์‹คํŒจํ•ด๋„ ๊ณ„์† ์ง„ํ–‰)
    let cached = false;
    try {
      await step.define({ name: "cache_data" }, async () => {
        await cacheData(data);
      }).run();
      cached = true;
    } catch (error) {
      logger.warn("Cache failed, continuing...", { error });
    }
    
    // Step 3: ์ตœ์ข… ์ €์žฅ
    await step.define({ name: "save_data" }, async () => {
      await saveToDatabase(data);
    }).run();
    
    return { saved: true, cached };
  }
);
์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค:
  • ์บ์‹ฑ ์‹คํŒจ (๋ฐ์ดํ„ฐ๋Š” ์ €์žฅ๋˜์–ด์•ผ ํ•จ)
  • ์•Œ๋ฆผ ๋ฐœ์†ก ์‹คํŒจ (์ฃผ๋ฌธ์€ ์™„๋ฃŒ๋˜์–ด์•ผ ํ•จ)
  • ๋กœ๊ทธ ๊ธฐ๋ก ์‹คํŒจ (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๊ณ„์†)

์žฌ์‹œ๋„ ํŒจํ„ด

์ˆ˜๋™ ์žฌ์‹œ๋„

์™ธ๋ถ€ API๋‚˜ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์€ ์ผ์‹œ์ ์œผ๋กœ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ๋ฒˆ ์žฌ์‹œ๋„ํ•˜์—ฌ ์„ฑ๊ณต๋ฅ ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
export const fetchWithRetry = workflow(
  { name: "fetch_with_retry" },
  async ({ input, step, logger }) => {
    const maxRetries = 3;
    let lastError: Error | null = null;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        const data = await step.define(
          { name: `fetch_attempt_${attempt}` },
          async () => {
            return await unstableAPICall(input.url);
          }
        ).run();
        
        logger.info("Fetch succeeded", { attempt });
        return { data, attempts: attempt };
      } catch (error) {
        lastError = error;
        logger.warn("Fetch failed", { attempt, error });
        
        // ๋งˆ์ง€๋ง‰ ์‹œ๋„ ์ „์—๋Š” ๋Œ€๊ธฐ
        if (attempt < maxRetries) {
          await step.sleep("retry_delay", "5s");
        }
      }
    }
    
    // ๋ชจ๋“  ์‹œ๋„ ์‹คํŒจ
    throw new Error(`Failed after ${maxRetries} attempts: ${lastError?.message}`);
  }
);
์žฌ์‹œ๋„ ์ „๋žต:
  1. ์ตœ๋Œ€ 3๋ฒˆ ์‹œ๋„
  2. ์‹œ๋„ ์‚ฌ์ด์— 5์ดˆ ๋Œ€๊ธฐ
  3. ๋ชจ๋“  ์‹œ๋„ ์‹คํŒจ ์‹œ ์—๋Ÿฌ ๋ฐœ์ƒ

์ง€์ˆ˜ ๋ฐฑ์˜คํ”„

์žฌ์‹œ๋„ ๊ฐ„๊ฒฉ์„ ์ ์  ๋Š˜๋ฆฌ๋Š” ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„(Exponential Backoff) ์ „๋žต์€ ์„œ๋ฒ„ ๊ณผ๋ถ€ํ•˜๋ฅผ ๋ฐฉ์ง€ํ•˜๋ฉด์„œ ์žฌ์‹œ๋„ ์„ฑ๊ณต๋ฅ ์„ ๋†’์ž…๋‹ˆ๋‹ค.
export const fetchWithBackoff = workflow(
  { name: "fetch_with_backoff" },
  async ({ input, step, logger }) => {
    const maxRetries = 5;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await step.define(
          { name: `attempt_${attempt}` },
          async () => {
            return await externalAPI.fetch(input.url);
          }
        ).run();
      } catch (error) {
        logger.warn("Attempt failed", { attempt, error });
        
        if (attempt < maxRetries) {
          // ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 2์ดˆ, 4์ดˆ, 8์ดˆ, 16์ดˆ
          const delay = Math.pow(2, attempt);
          await step.sleep(`backoff_${attempt}`, `${delay}s`);
        } else {
          throw error;
        }
      }
    }
  }
);
๋ฐฑ์˜คํ”„ ๊ฐ„๊ฒฉ:
  • 1์ฐจ ์‹คํŒจ โ†’ 2์ดˆ ๋Œ€๊ธฐ
  • 2์ฐจ ์‹คํŒจ โ†’ 4์ดˆ ๋Œ€๊ธฐ
  • 3์ฐจ ์‹คํŒจ โ†’ 8์ดˆ ๋Œ€๊ธฐ
  • 4์ฐจ ์‹คํŒจ โ†’ 16์ดˆ ๋Œ€๊ธฐ
์žฅ์ :
  • ์ผ์‹œ์  ๊ณผ๋ถ€ํ•˜์—์„œ ํšŒ๋ณตํ•  ์‹œ๊ฐ„ ์ œ๊ณต
  • ์„œ๋ฒ„ ๋ถ€ํ•˜ ๋ถ„์‚ฐ
  • ์žฌ์‹œ๋„ ์„ฑ๊ณต๋ฅ  ํ–ฅ์ƒ

๋ณด์ƒ ํŠธ๋žœ์žญ์…˜

๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜์—์„œ๋Š” ์ผ๋ถ€ ์ž‘์—…์ด ์‹คํŒจํ•˜๋ฉด ์ด๋ฏธ ์™„๋ฃŒ๋œ ์ž‘์—…์„ ๋˜๋Œ๋ ค์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜(Compensating Transaction)์ด๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.
export const processOrder = workflow(
  { name: "process_order" },
  async ({ input, step, logger }) => {
    let paymentId: string | null = null;
    let inventoryReserved = false;
    
    try {
      // Step 1: ๊ฒฐ์ œ
      paymentId = await step.define({ name: "charge_payment" }, async () => {
        return await paymentService.charge(input.amount);
      }).run();
      
      // Step 2: ์žฌ๊ณ  ํ™•๋ณด
      await step.define({ name: "reserve_inventory" }, async () => {
        await inventoryService.reserve(input.items);
      }).run();
      inventoryReserved = true;
      
      // Step 3: ์ฃผ๋ฌธ ์ƒ์„ฑ
      const orderId = await step.define({ name: "create_order" }, async () => {
        return await orderService.create({
          paymentId,
          items: input.items,
        });
      }).run();
      
      return { orderId, success: true };
    } catch (error) {
      logger.error("Order processing failed, rolling back...", { error });
      
      // ๋ณด์ƒ: ์žฌ๊ณ  ํ•ด์ œ
      if (inventoryReserved) {
        await step.define({ name: "rollback_inventory" }, async () => {
          await inventoryService.release(input.items);
        }).run();
      }
      
      // ๋ณด์ƒ: ๊ฒฐ์ œ ์ทจ์†Œ
      if (paymentId) {
        await step.define({ name: "rollback_payment" }, async () => {
          await paymentService.refund(paymentId);
        }).run();
      }
      
      throw error;
    }
  }
);
๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ํŒจํ„ด:
  1. ๊ฐ ์ž‘์—…์˜ ์™„๋ฃŒ ์ƒํƒœ ์ถ”์ 
  2. ์‹คํŒจ ๋ฐœ์ƒ ์‹œ ์™„๋ฃŒ๋œ ์ž‘์—… ํ™•์ธ
  3. ์—ญ์ˆœ์œผ๋กœ ์ž‘์—… ์ทจ์†Œ
  4. ์—๋Ÿฌ ์žฌ๋ฐœ์ƒ
์‹ค์ „ ์‚ฌ์šฉ:
  • ๊ฒฐ์ œ ์ทจ์†Œ
  • ์žฌ๊ณ  ๋ณต์›
  • ์˜ˆ์•ฝ ์ทจ์†Œ
  • ํŒŒ์ผ ์‚ญ์ œ

ํƒ€์ž„์•„์›ƒ ์ฒ˜๋ฆฌ

์™ธ๋ถ€ API๊ฐ€ ์‘๋‹ตํ•˜์ง€ ์•Š์œผ๋ฉด workflow๊ฐ€ ๋ฌดํ•œ์ • ๋Œ€๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•˜์—ฌ ์ผ์ • ์‹œ๊ฐ„ ํ›„ ์‹คํŒจ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
async function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number
): Promise<T> {
  return Promise.race([
    promise,
    new Promise<T>((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), timeoutMs)
    ),
  ]);
}

export const fetchWithTimeout = workflow(
  { name: "fetch_with_timeout" },
  async ({ input, step, logger }) => {
    try {
      const data = await step.define({ name: "fetch" }, async () => {
        return await withTimeout(
          externalAPI.fetch(input.url),
          30000  // 30์ดˆ
        );
      }).run();
      
      return { data };
    } catch (error) {
      if (error.message === "Timeout") {
        logger.error("Request timed out");
      }
      throw error;
    }
  }
);
ํƒ€์ž„์•„์›ƒ ์ „๋žต:
  • ์งง์€ ํƒ€์ž„์•„์›ƒ (5-10์ดˆ): ๋น ๋ฅธ API
  • ์ค‘๊ฐ„ ํƒ€์ž„์•„์›ƒ (30-60์ดˆ): ์ผ๋ฐ˜ API
  • ๊ธด ํƒ€์ž„์•„์›ƒ (5-10๋ถ„): ํŒŒ์ผ ์ฒ˜๋ฆฌ

์—๋Ÿฌ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ

์—๋Ÿฌ์˜ ์ข…๋ฅ˜์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ฒ˜๋ฆฌ ์ „๋žต์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ๋Š” ์žฌ์‹œ๋„ํ•˜์ง€๋งŒ, ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ์—๋Ÿฌ๋Š” ์ฆ‰์‹œ ์‹คํŒจ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
export const processData = workflow(
  { name: "process_data" },
  async ({ input, step, logger }) => {
    try {
      await step.define({ name: "process" }, async () => {
        await dataService.process(input.data);
      }).run();
    } catch (error) {
      // ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ: ์žฌ์‹œ๋„
      if (error.code === 'ECONNREFUSED') {
        logger.warn("Connection refused, retrying...");
        await step.sleep("retry_delay", "10s");
        throw error;  // ์žฌ์‹œ๋„
      }
      
      // ๊ฒ€์ฆ ์—๋Ÿฌ: ์ฆ‰์‹œ ์‹คํŒจ
      if (error.code === 'VALIDATION_ERROR') {
        logger.error("Validation failed", { error });
        throw new Error("Invalid data, not retrying");
      }
      
      // ๊ธฐํƒ€ ์—๋Ÿฌ
      throw error;
    }
  }
);
์—๋Ÿฌ ๋ถ„๋ฅ˜:
์—๋Ÿฌ ํƒ€์ž…์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์˜ˆ์‹œ
์ผ์‹œ์  ์—๋Ÿฌ์žฌ์‹œ๋„๋„คํŠธ์›Œํฌ, ํƒ€์ž„์•„์›ƒ, 503
์˜๊ตฌ์  ์—๋Ÿฌ์ฆ‰์‹œ ์‹คํŒจ๊ฒ€์ฆ, ์ธ์ฆ, 404
๋ถ€๋ถ„ ์‹คํŒจ์„ ํƒ์  ์ฒ˜๋ฆฌ์ผ๋ถ€ ๋ฐ์ดํ„ฐ ์†์ƒ

์‹ค์ „ ์˜ˆ์ œ

1. ์ด๋ฉ”์ผ ๋ฐœ์†ก with Dead Letter Queue

์ด๋ฉ”์ผ ๋ฐœ์†ก์ด 3๋ฒˆ ์‹คํŒจํ•˜๋ฉด Dead Letter Queue์— ์ถ”๊ฐ€ํ•˜์—ฌ ๋‚˜์ค‘์— ์ˆ˜๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
export const sendEmail = workflow(
  { name: "send_email" },
  async ({ input, step, logger }) => {
    const maxRetries = 3;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        await step.define(
          { name: `send_attempt_${attempt}` },
          async () => {
            await emailService.send({
              to: input.email,
              subject: input.subject,
              body: input.body,
            });
          }
        ).run();
        
        logger.info("Email sent", { attempt });
        return { success: true, attempts: attempt };
      } catch (error) {
        logger.warn("Email send failed", { attempt, error });
        
        if (attempt < maxRetries) {
          await step.sleep("retry_delay", "30s");
        } else {
          // ์ตœ์ข… ์‹คํŒจ: Dead Letter Queue๋กœ
          await step.define({ name: "move_to_dlq" }, async () => {
            await deadLetterQueue.add({
              type: "email",
              data: input,
              error: error.message,
            });
          }).run();
          
          throw error;
        }
      }
    }
  }
);
DLQ ํŒจํ„ด:
  • 3๋ฒˆ ์žฌ์‹œ๋„ ์‹คํŒจ
  • DLQ์— ์ถ”๊ฐ€ (๋‚˜์ค‘์— ์ฒ˜๋ฆฌ)
  • ๊ด€๋ฆฌ์ž์—๊ฒŒ ์•Œ๋ฆผ

2. API ํ˜ธ์ถœ with Circuit Breaker

์™ธ๋ถ€ API๊ฐ€ ๊ณ„์† ์‹คํŒจํ•˜๋ฉด **ํšŒ๋กœ ์ฐจ๋‹จ๊ธฐ(Circuit Breaker)**๋กœ ์š”์ฒญ์„ ์ฐจ๋‹จํ•˜์—ฌ ์‹œ์Šคํ…œ์„ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค.
class CircuitBreaker {
  private failures = 0;
  private lastFailureTime = 0;
  private readonly threshold = 5;
  private readonly timeout = 60000;  // 1๋ถ„
  
  async call<T>(fn: () => Promise<T>): Promise<T> {
    // ํšŒ๋กœ๊ฐ€ ์—ด๋ฆฐ ์ƒํƒœ
    if (this.failures >= this.threshold) {
      const elapsed = Date.now() - this.lastFailureTime;
      if (elapsed < this.timeout) {
        throw new Error("Circuit breaker is open");
      }
      // ํƒ€์ž„์•„์›ƒ ํ›„ ์žฌ์‹œ๋„
      this.failures = 0;
    }
    
    try {
      const result = await fn();
      this.failures = 0;  // ์„ฑ๊ณต ์‹œ ๋ฆฌ์…‹
      return result;
    } catch (error) {
      this.failures++;
      this.lastFailureTime = Date.now();
      throw error;
    }
  }
}

const circuitBreaker = new CircuitBreaker();

export const callAPI = workflow(
  { name: "call_api" },
  async ({ input, step, logger }) => {
    try {
      const data = await step.define({ name: "api_call" }, async () => {
        return await circuitBreaker.call(() =>
          fetch(input.url).then(r => r.json())
        );
      }).run();
      
      return { data };
    } catch (error) {
      if (error.message === "Circuit breaker is open") {
        logger.error("Circuit breaker open, API unavailable");
      }
      throw error;
    }
  }
);
Circuit Breaker ์ƒํƒœ:
  • Closed: ์ •์ƒ ์ž‘๋™
  • Open: 5๋ฒˆ ์‹คํŒจ ํ›„ ์ฐจ๋‹จ (1๋ถ„๊ฐ„)
  • Half-Open: 1๋ถ„ ํ›„ ์žฌ์‹œ๋„

3. ํŒŒ์ผ ์—…๋กœ๋“œ with ๋ถ€๋ถ„ ์žฌ์‹œ๋„

์—ฌ๋Ÿฌ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ๋•Œ, ๊ฐ ํŒŒ์ผ์„ ๋…๋ฆฝ์ ์œผ๋กœ ์žฌ์‹œ๋„ํ•˜์—ฌ ์ผ๋ถ€ ์‹คํŒจํ•ด๋„ ๋‚˜๋จธ์ง€๋Š” ์„ฑ๊ณตํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
export const uploadFiles = workflow(
  { name: "upload_files" },
  async ({ input, step, logger }) => {
    const results = [];
    
    for (let i = 0; i < input.files.length; i++) {
      const file = input.files[i];
      let uploaded = false;
      
      // ๊ฐ ํŒŒ์ผ๋ณ„๋กœ ์žฌ์‹œ๋„
      for (let attempt = 1; attempt <= 3; attempt++) {
        try {
          await step.define(
            { name: `upload_${i}_attempt_${attempt}` },
            async () => {
              await s3.upload(file.key, file.content);
            }
          ).run();
          
          uploaded = true;
          results.push({ file: file.key, success: true });
          break;
        } catch (error) {
          logger.warn("Upload failed", { file: file.key, attempt, error });
          
          if (attempt < 3) {
            await step.sleep(`retry_${i}_${attempt}`, "5s");
          }
        }
      }
      
      if (!uploaded) {
        results.push({ file: file.key, success: false });
      }
    }
    
    const failed = results.filter(r => !r.success);
    if (failed.length > 0) {
      logger.error("Some uploads failed", { failed });
    }
    
    return {
      total: input.files.length,
      succeeded: results.filter(r => r.success).length,
      failed: failed.length,
    };
  }
);
๋ถ€๋ถ„ ์žฌ์‹œ๋„ ์ „๋žต:
  • ๊ฐ ํŒŒ์ผ์ด ๋…๋ฆฝ์ ์ธ step๋“ค
  • ํŒŒ์ผ๋ณ„ 3๋ฒˆ ์žฌ์‹œ๋„
  • ์ผ๋ถ€ ์‹คํŒจํ•ด๋„ workflow ์„ฑ๊ณต
  • ์‹คํŒจํ•œ ํŒŒ์ผ ๋ชฉ๋ก ๋ฐ˜ํ™˜

์ฃผ์˜์‚ฌํ•ญ

์—๋Ÿฌ ์ฒ˜๋ฆฌ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ์—๋Ÿฌ ๋กœ๊น…: ํ•ญ์ƒ ์—๋Ÿฌ๋ฅผ ๋กœ๊น…ํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    catch (error) {
      logger.error("Operation failed", { error, context });
    }
    
  2. ์žฌ์‹œ๋„ ์ œํ•œ: ๋ฌดํ•œ ์žฌ์‹œ๋„๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์ตœ๋Œ€ ํšŸ์ˆ˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”.
    const maxRetries = 3;  // ๋ช…ํ™•ํ•œ ์ œํ•œ
    
  3. ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜: ์‹คํŒจ ์‹œ ์ด๋ฏธ ์™„๋ฃŒ๋œ ์ž‘์—…์„ ์ •๋ฆฌํ•˜์„ธ์š”.
    catch (error) {
      await rollbackCompletedSteps();
      throw error;
    }
    
  4. ํƒ€์ž„์•„์›ƒ ์„ค์ •: ๋ฌดํ•œ ๋Œ€๊ธฐ๋ฅผ ๋ฐฉ์ง€ํ•˜์„ธ์š”.
    await withTimeout(promise, 30000);
    
  5. Dead Letter Queue: ์ตœ์ข… ์‹คํŒจ ์‹œ ์ˆ˜๋™ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋ณ„๋„๋กœ ์ €์žฅํ•˜์„ธ์š”.
    await deadLetterQueue.add(failedItem);
    
  6. ์—๋Ÿฌ ํƒ€์ž… ๊ตฌ๋ถ„: ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ์™€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ๋ฅผ ๊ตฌ๋ถ„ํ•˜์„ธ์š”.
    if (isRetryable(error)) {
      throw error;  // ์žฌ์‹œ๋„
    } else {
      throw new NonRetryableError();  // ์ฆ‰์‹œ ์‹คํŒจ
    }
    

๋‹ค์Œ ๋‹จ๊ณ„