티스토리 뷰

 

소프트웨어 개발에서 "코드가 실행된다"는 것은 절반의 성공일 뿐입니다. 나머지 절반은 "예상치 못한 상황에서 코드가 어떻게 우아하게 실패하는가"에 달려 있습니다. 특히 네트워크 지연, 잘못된 사용자 입력, API 서버의 장애가 빈번한 현대 웹 생태계에서 에러 핸들링은 서비스의 신뢰도를 결정짓는 핵심 요소입니다.

단순히 에러를 잡는(catch) 것을 넘어, 시스템을 보호하고 사용자 경험을 해치지 않는 시니어 수준의 예외 처리 전략을 짚어보겠습니다.


1. Deep Dive: 왜 단순한 try...catch만으로는 부족할까?

대부분의 입문자는 모든 코드 블록을 try...catch로 감싸면 안전하다고 믿습니다. 하지만 이는 **"무차별적 수용"**이라는 함정에 빠지기 쉽습니다.

에러 핸들링의 본질: 전파(Propagation)와 억제(Suppression)

에러 핸들링의 핵심은 **"여기서 해결할 것인가, 아니면 상위 레이어로 보고할 것인가"**를 결정하는 것입니다. 모든 곳에서 에러를 잡아버리면(Silent Fail), 정작 디버깅이 필요한 시점에 에러의 근원지를 찾을 수 없는 '좀비 코드'가 생성됩니다.

비유로 이해하기 레스토랑에서 요리 도중 재료가 떨어졌다고 가정해 봅시다.

  • 하위 레이어(요리사): 재료가 없음을 인지하고 매니저에게 알립니다. (Error Throwing)
  • 상위 레이어(매니저): 손님에게 품절을 안내하거나 다른 메뉴를 추천합니다. (Error Handling)

만약 요리사가 재료가 없는데도 매니저에게 알리지 않고(Catch 후 아무것도 안 함) 빈 접시를 내보낸다면, 서비스 전체의 신뢰도가 무너지는 것과 같습니다.


2. Hands-on: 실무형 이커머스 결제 로직 예제

단순한 console.error가 아닌, 상태 복구로그 추적을 포함한 실전 결제 프로세스 코드를 살펴보겠습니다.

JavaScript
 
/**
 * 이커머스 결제 처리 함수
 * @param {string} orderId - 주문 ID
 * @param {object} paymentInfo - 결제 수단 정보
 */
async function processOrderPayment(orderId, paymentInfo) {
  try {
    // 1. 재고 확인 (Pre-condition Check)
    const stockAvailable = await checkInventory(orderId);
    if (!stockAvailable) {
      // 예상 가능한 비즈니스 에러는 커스텀 에러 객체를 사용합니다.
      throw new Error("INSUFFICIENT_STOCK");
    }

    // 2. 결제 API 호출
    const paymentResult = await externalPaymentGateway(paymentInfo);

    // 3. 주문 상태 업데이트
    await updateOrderStatus(orderId, 'PAID');
    
    return { success: true, transactionId: paymentResult.id };

  } catch (error) {
    // [Troubleshooting] 에러의 성격에 따른 분기 처리
    
    if (error.message === "INSUFFICIENT_STOCK") {
      // 비즈니스 로직 에러: 사용자에게 친절한 안내 필요
      console.warn(`[Order-Warn] 주문 ${orderId}: 재고 부족`);
      return { success: false, reason: "재고가 부족하여 결제가 취소되었습니다." };
    }

    if (error.name === "TimeoutError") {
      // 인프라 에러: 일시적인 문제이므로 재시도 권장
      console.error(`[Order-Error] 결제 게이트웨이 타임아웃: ${error.stack}`);
      return { success: false, reason: "결제 서버 응답이 지연되고 있습니다. 잠시 후 다시 시도해주세요." };
    }

    // 4. 예상치 못한 에러 (Critical)
    // 로그 시스템(예: Sentry, Datadog)에 에러 전송
    captureException(error, { extra: { orderId } });
    
    // 치명적 에러 발생 시 시스템 안정성을 위해 상위로 에러를 다시 던집니다 (Re-throw)
    throw new SystemError("결제 처리 중 내부 시스템 오류가 발생했습니다.", error);
  } finally {
    // 성공/실패 여부와 상관없이 실행 (예: 로딩 스피너 제거, 리소스 해제)
    hideLoadingSpinner();
  }
}

핵심 포인트 설명

  • Custom Error Identification: error.message나 error.name을 통해 에러의 성격을 분류했습니다.
  • Contextual Logging: 단순 에러 메시지가 아닌 orderId와 같은 컨텍스트를 함께 남겨 추적을 용이하게 했습니다.
  • finally의 활용: 에러가 발생하더라도 UI의 상태(로딩 인디케이터 등)는 반드시 정리되어야 합니다.

3. Trade-offs: 에러 핸들링의 비용과 대안

완벽한 에러 핸들링에도 비용(Trade-off)이 따릅니다.

  1. 가독성 저하: 모든 라인을 try...catch로 감싸면 비즈니스 로직보다 에러 처리 코드가 더 길어지는 "Pyramid of Doom"이 발생할 수 있습니다.
    • 대안: 미들웨어나 중앙 집중식 에러 핸들러(Global Error Boundary)를 활용하여 공통 로직을 분리하세요.
  2. 성능 오버헤드: 과도한 호출 스택 생성과 예외 객체 생성은 미세하게나마 메모리와 CPU를 소모합니다.
    • 대안: 제어 흐름(Control Flow)을 위해 에러를 사용하지 마세요. (예: 단순히 데이터가 없는 경우 throw를 하기보다 null이나 Optional 패턴을 반환하는 것이 효율적입니다.)

4. 결론 및 요약

에러 핸들링은 단순히 앱이 죽지 않게 만드는 방어 기제가 아닙니다. 실패한 상황에서도 시스템이 다음 동작을 예측 가능하게 만드는 설계입니다.

  • 비즈니스 에러시스템 에러를 구분하세요.
  • 사용자에게는 이해 가능한 언어로, 개발자에게는 추적 가능한 로그로 응답하세요.
  • try...catch는 해결할 수 있는 위치에서만 사용하고, 그렇지 않다면 과감히 위로 던지세요.
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함