smithery/gdm0714

error-handling-philosophy

에러 처리 철학 - 사용자에게는 친절하게, 개발자에게는 ?

Installation

$ npx skills add smithery/gdm0714 --skill error-handling-philosophy

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/gdm0714.

npx skills add smithery/gdm0714

Browse all from smithery/gdm0714

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 14,719 B
  • docs SUMMARY.md 120 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

에러 처리 철학

핵심 원칙

"에러는 항상 발생한다. 어떻게 처리하느냐가 서비스 품질을 결정한다."

1. 빠르게 실패하라 (Fail Fast)

  • 문제를 발견하면 즉시 에러 발생
  • 잘못된 상태로 계속 실행하지 않기

2. 사용자는 친절한 메시지, 개발자는 상세한 로그

  • 사용자: "결제 처리 중 문제가 발생했습니다"
  • 로그: "Payment gateway timeout after 30s, orderId: 12345, userId: 678"

3. 복구 가능한 에러 vs 치명적 에러

  • 복구 가능: 재시도, 대체 방법 제공
  • 치명적: 즉시 중단, 알림 발송

에러 분류 체계

1. 클라이언트 에러 (4xx)

// 입력 검증 실패
class ValidationError extends Error {
  constructor(
    message: string,
    public field: string,
    public value: any
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

throw new ValidationError(
  '이메일 형식이 올바르지 않습니다',
  'email',
  'invalid-email'
);

// 사용자 메시지
{
  "error": {
    "type": "VALIDATION_ERROR",
    "message": "입력값을 확인해주세요",
    "field": "email",
    "userMessage": "올바른 이메일 주소를 입력하세요"
  }
}

2. 서버 에러 (5xx)

// 외부 서비스 장애
class ServiceUnavailableError extends Error {
  constructor(
    message: string,
    public service: string,
    public retryAfter?: number
  ) {
    super(message);
    this.name = 'ServiceUnavailableError';
  }
}

throw new ServiceUnavailableError(
  'Payment gateway timeout',
  'toss-payments',
  60
);

// 사용자 메시지
{
  "error": {
    "type": "SERVICE_UNAVAILABLE",
    "message": "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요",
    "retryAfter": 60
  }
}

실전 패턴

패턴 1: Try-Catch는 명확한 목적이 있을 때만

// ❌ 나쁜 예: 모든 것을 catch
async function getUser(id: string) {
  try {
    const user = await db.user.findUnique({ where: { id } });
    return user;
  } catch (error) {
    console.error(error);  // 그냥 삼켜버림
    return null;
  }
}

// ✅ 좋은 예: 특정 에러만 처리
async function getUser(id: string) {
  try {
    const user = await db.user.findUnique({ where: { id } });

    if (!user) {
      throw new NotFoundError('사용자를 찾을 수 없습니다', 'User', id);
    }

    return user;
  } catch (error) {
    // PrismaClientKnownRequestError만 처리
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === 'P2025') {
        throw new NotFoundError('사용자를 찾을 수 없습니다', 'User', id);
      }
    }
    // 예상하지 못한 에러는 상위로 전파
    throw error;
  }
}

패턴 2: 에러 경계 (Error Boundaries)

// React Error Boundary
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 에러 로깅 서비스로 전송
    logger.error('React Error Boundary caught error', {
      error: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>문제가 발생했습니다</h2>
          <p>페이지를 새로고침하거나 고객센터로 문의해주세요</p>
          <button onClick={() => window.location.reload()}>
            새로고침
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// 사용
<ErrorBoundary>
  <App />
</ErrorBoundary>

패턴 3: 중앙 집중식 에러 핸들러

// Express 에러 핸들러
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 에러 분류
  if (err instanceof ValidationError) {
    return res.status(400).json({
      error: {
        type: 'VALIDATION_ERROR',
        message: err.message,
        field: err.field,
      },
    });
  }

  if (err instanceof NotFoundError) {
    return res.status(404).json({
      error: {
        type: 'NOT_FOUND',
        message: err.message,
      },
    });
  }

  if (err instanceof UnauthorizedError) {
    return res.status(401).json({
      error: {
        type: 'UNAUTHORIZED',
        message: '로그인이 필요합니다',
      },
    });
  }

  // 예상하지 못한 에러
  logger.error('Unexpected error', {
    error: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
    userId: req.user?.id,
  });

  // 운영 환경에서는 상세 정보 숨김
  res.status(500).json({
    error: {
      type: 'INTERNAL_SERVER_ERROR',
      message: process.env.NODE_ENV === 'production'
        ? '서버 오류가 발생했습니다'
        : err.message,
    },
  });
});

사용자 친화적 에러 메시지

❌ 개발자 중심 메시지

"Error: ECONNREFUSED 127.0.0.1:5432"
"Uncaught TypeError: Cannot read property 'name' of undefined"
"ValidationError: email must be a valid email"

✅ 사용자 친화적 메시지

const ERROR_MESSAGES = {
  NETWORK_ERROR: {
    title: '인터넷 연결을 확인해주세요',
    description: '네트워크 연결이 불안정합니다',
    action: '새로고침',
  },

  VALIDATION_EMAIL: {
    title: '이메일 주소를 확인해주세요',
    description: '올바른 이메일 형식이 아닙니다',
    action: '다시 입력',
  },

  PAYMENT_FAILED: {
    title: '결제 처리 중 문제가 발생했습니다',
    description: '카드사 오류이거나 한도를 초과했을 수 있습니다',
    action: '다시 시도',
  },

  SERVER_ERROR: {
    title: '일시적인 오류가 발생했습니다',
    description: '잠시 후 다시 시도해주세요',
    action: '고객센터 문의',
  },
};

// 컴포넌트에서 사용
<ErrorMessage>
  <h3>{ERROR_MESSAGES.PAYMENT_FAILED.title}</h3>
  <p>{ERROR_MESSAGES.PAYMENT_FAILED.description}</p>
  <button>{ERROR_MESSAGES.PAYMENT_FAILED.action}</button>
</ErrorMessage>

재시도 전략

지수 백오프 (Exponential Backoff)

async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      // 마지막 시도에서는 에러 throw
      if (attempt === maxRetries - 1) {
        throw error;
      }

      // 재시도 가능한 에러인지 확인
      if (!isRetryableError(error)) {
        throw error;
      }

      // 지수 백오프 대기
      const delay = baseDelay * Math.pow(2, attempt);
      await sleep(delay);

      logger.warn('Retrying after error', {
        attempt: attempt + 1,
        maxRetries,
        delay,
        error: error.message,
      });
    }
  }

  throw new Error('Should not reach here');
}

function isRetryableError(error: any): boolean {
  // 네트워크 에러, 타임아웃, 일시적 서버 에러
  return (
    error.code === 'ECONNRESET' ||
    error.code === 'ETIMEDOUT' ||
    error.response?.status === 503 ||
    error.response?.status === 429
  );
}

// 사용
const data = await fetchWithRetry(() =>
  fetch('https://api.example.com/data').then(r => r.json())
);

에러 로깅 전략

로그 레벨

import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    // 에러 로그는 별도 파일
    new winston.transports.File({
      filename: 'error.log',
      level: 'error',
    }),
    // 모든 로그
    new winston.transports.File({
      filename: 'combined.log',
    }),
  ],
});

// 개발 환경에서는 콘솔 출력
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple(),
  }));
}

// 에러 레벨별 사용
logger.error('Payment failed', {
  orderId: '12345',
  userId: '678',
  amount: 50000,
  error: error.message,
  stack: error.stack,
});

logger.warn('Slow query detected', {
  query: 'SELECT * FROM users',
  duration: 3000,
});

logger.info('User logged in', {
  userId: '678',
  ip: req.ip,
});

logger.debug('Cache hit', {
  key: 'user:678',
});

민감 정보 필터링

// ❌ 민감 정보 로깅 금지
logger.error('Login failed', {
  email: '[email protected]',
  password: 'user-password',  // ❌❌❌
  ssn: '123456-1234567',      // ❌❌❌
});

// ✅ 민감 정보 제거 후 로깅
const sanitized = {
  ...data,
  password: undefined,
  ssn: data.ssn ? '***-***-****' : undefined,
  creditCard: data.creditCard ? '****-****-****-' + data.creditCard.slice(-4) : undefined,
};

logger.error('Login failed', sanitized);

에러 모니터링

Sentry 연동

import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,
});

// Express 에러 핸들러 전에 추가
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());

// 수동으로 에러 전송
try {
  await processPayment();
} catch (error) {
  Sentry.captureException(error, {
    tags: {
      section: 'payment',
    },
    extra: {
      orderId: order.id,
      amount: order.amount,
    },
  });
  throw error;
}

API 에러 응답 표준

일관된 에러 형식

interface ErrorResponse {
  error: {
    // 에러 타입 (기계 판독용)
    type: string;

    // 사용자 메시지 (사람 판독용)
    message: string;

    // 상세 정보 (선택)
    details?: Array<{
      field: string;
      message: string;
    }>;

    // 요청 ID (디버깅용)
    requestId: string;

    // 문서 링크 (선택)
    docUrl?: string;
  };
}

// 예시
{
  "error": {
    "type": "VALIDATION_ERROR",
    "message": "입력값을 확인해주세요",
    "details": [
      {
        "field": "email",
        "message": "올바른 이메일 주소를 입력하세요"
      },
      {
        "field": "password",
        "message": "비밀번호는 8자 이상이어야 합니다"
      }
    ],
    "requestId": "req_abc123",
    "docUrl": "https://docs.example.com/errors/validation"
  }
}

실전 시나리오

시나리오 1: 결제 실패

async function processPayment(orderId: string) {
  try {
    // 1. 주문 조회
    const order = await getOrder(orderId);
    if (!order) {
      throw new NotFoundError('주문을 찾을 수 없습니다', 'Order', orderId);
    }

    // 2. 재고 확인
    const hasStock = await checkStock(order.items);
    if (!hasStock) {
      throw new BusinessError(
        '일부 상품의 재고가 부족합니다',
        'OUT_OF_STOCK'
      );
    }

    // 3. 결제 처리 (재시도 가능)
    const payment = await fetchWithRetry(
      () => paymentGateway.charge(order.amount),
      3,
      1000
    );

    // 4. 주문 완료 처리
    await completeOrder(orderId, payment.id);

    return { success: true, paymentId: payment.id };

  } catch (error) {
    // 에러 분류 및 처리
    if (error instanceof BusinessError) {
      // 비즈니스 로직 에러 - 사용자에게 친절한 메시지
      logger.warn('Business error during payment', {
        orderId,
        errorCode: error.code,
      });
      throw error;
    }

    if (error.response?.status === 402) {
      // 결제 거부
      logger.error('Payment declined', {
        orderId,
        reason: error.response.data.reason,
      });
      throw new BusinessError(
        '결제가 거부되었습니다. 카드사로 문의해주세요',
        'PAYMENT_DECLINED'
      );
    }

    // 예상치 못한 에러
    logger.error('Unexpected error during payment', {
      orderId,
      error: error.message,
      stack: error.stack,
    });

    // Sentry로 알림
    Sentry.captureException(error, {
      tags: { section: 'payment' },
      extra: { orderId },
    });

    throw new Error('결제 처리 중 문제가 발생했습니다');
  }
}

시나리오 2: 외부 API 호출

async function fetchWeatherData(city: string) {
  try {
    const response = await axios.get(
      `https://api.weather.com/forecast`,
      {
        params: { city },
        timeout: 5000,  // 5초 타임아웃
      }
    );

    return response.data;

  } catch (error) {
    // Axios 에러 처리
    if (axios.isAxiosError(error)) {
      if (error.code === 'ECONNABORTED') {
        // 타임아웃
        throw new ServiceUnavailableError(
          '날씨 정보를 불러올 수 없습니다',
          'weather-api',
          60
        );
      }

      if (error.response) {
        // 서버 응답 있음
        if (error.response.status === 404) {
          throw new NotFoundError('도시를 찾을 수 없습니다', 'City', city);
        }
        if (error.response.status === 429) {
          throw new RateLimitError('요청 한도를 초과했습니다', 60);
        }
      } else if (error.request) {
        // 요청은 보냈지만 응답 없음
        throw new ServiceUnavailableError(
          '날씨 서비스에 연결할 수 없습니다',
          'weather-api',
          60
        );
      }
    }

    // 예상치 못한 에러
    throw error;
  }
}

체크리스트

에러 처리 전

  • 이 에러는 복구 가능한가?
  • 사용자에게 어떤 메시지를 보여줄 것인가?
  • 재시도가 필요한가?
  • 로그에 어떤 정보를 남길 것인가?
  • 개발자에게 알림이 필요한가?

배포 전

  • 모든 예상 에러가 처리되었는가?
  • 사용자 메시지가 친절한가?
  • 민감 정보가 로그에 없는가?
  • 에러 모니터링이 설정되었는가?
  • 에러 응답 형식이 일관적인가?

마무리 원칙

"에러는 적이 아니라 피드백이다"

  • 숨기지 마라: 에러를 무시하면 더 큰 문제가 된다
  • 명확하게: 사용자도, 개발자도 이해할 수 있게
  • 일관되게: 같은 패턴을 유지하라
  • 준비하라: 예상 가능한 에러는 모두 처리하라
  • 배우라: 에러 로그에서 개선점을 찾아라