TIL - 20260831

juni·2026년 8월 31일

TIL

목록 보기
445/468

0831 백엔드 아키텍처 고도화 (8/N): Error Handling, Exception Filter와 표준 응답 구조


✅ 1. Error Handling이 중요한 이유

  • 백엔드에서 에러 처리는 단순히 throw new Error()를 던지는 것이 아닙니다.
  • 에러는 사용자에게 무엇이 잘못됐는지 알려주고, 프론트가 적절히 대응하게 하며, 운영자가 장애 원인을 추적할 수 있게 만드는 구조입니다.
  • API가 커질수록 에러 응답이 제각각이면 프론트 처리, QA, 장애 대응이 모두 어려워집니다.
API 요청
  ↓
문제 발생
  ↓
에러 분류
  ↓
표준 응답 반환
  ↓
로그 기록
  ↓
필요 시 Audit Log / Job 상태 저장

➕ 1-1. 에러 처리가 약할 때 생기는 문제

프론트에서 에러 메시지 처리 어려움
사용자에게 500만 표시됨
관리자가 원인을 알 수 없음
로그에 requestId가 없어 추적 어려움
개인정보/Secret이 에러 로그에 노출될 수 있음
외부 API 실패와 내부 서버 오류가 구분되지 않음
  • 좋은 에러 처리는 기능 품질의 일부입니다.
  • 특히 관리자 시스템에서는 “왜 실패했는지”를 명확히 알려줘야 운영 속도가 올라갑니다.

✅ 2. 에러의 종류

  • 모든 에러를 같은 방식으로 처리하면 안 됩니다.
  • 사용자 입력 문제, 권한 문제, 데이터 충돌, 외부 API 장애, 서버 내부 오류를 구분해야 합니다.
종류HTTP 상태예시
Validation Error400전화번호 형식 오류
Unauthorized401로그인 필요
Forbidden403권한 없음
Not Found404상담 없음
Conflict409동시 수정 충돌
Rate Limit429요청 과다
External API Error502/503알림톡 API 장애
Internal Error500예상 못한 서버 오류

➕ 2-1. 현재 프로젝트 기준 에러 예시

상담 신청:
전화번호 형식 오류
중복 신청
상품 비활성화
상담 저장 실패

관리자 상태 변경:
상담 없음
상태 전이 불가
권한 없음
동시 수정 충돌

엑셀 Export:
권한 없음
검색 조건 오류
ExportJob 생성 실패
파일 만료

알림톡:
템플릿 오류
외부 API timeout
수신자 번호 오류
재시도 초과
  • 에러는 “문제 상황의 이름”을 붙여 관리하는 것이 좋습니다.
  • 그래야 프론트, 로그, 운영 문서가 같은 언어를 쓸 수 있습니다.

✅ 3. HTTP 상태 코드 기준

  • HTTP 상태 코드는 프론트와 API 소비자가 에러를 해석하는 첫 기준입니다.
  • 모든 실패를 500으로 반환하면 안 됩니다.

➕ 3-1. 자주 쓰는 상태 코드

400 Bad Request:
요청값이 잘못됨

401 Unauthorized:
로그인이 필요함

403 Forbidden:
로그인은 했지만 권한이 없음

404 Not Found:
대상 데이터를 찾을 수 없음

409 Conflict:
현재 데이터 상태와 요청이 충돌함

422 Unprocessable Entity:
형식은 맞지만 업무 규칙상 처리 불가

429 Too Many Requests:
요청이 너무 많음

500 Internal Server Error:
예상하지 못한 서버 오류

502 Bad Gateway:
외부 API 응답 오류

503 Service Unavailable:
외부 서비스 또는 내부 서비스 일시 장애

➕ 3-2. 실무 기준

잘못된 입력:
400

로그인 안 됨:
401

권한 없음:
403

상담/상품 없음:
404

중복 신청/동시 수정:
409

상태 전이 불가:
400 또는 422

외부 API 일시 장애:
502 또는 503

예상 못한 서버 오류:
500
  • 상태 코드는 프론트 처리와 운영 판단에 영향을 줍니다.
  • 409와 403, 404를 구분하는 것만으로도 관리자 UX가 많이 좋아집니다.

✅ 4. 표준 에러 응답 구조

  • 에러 응답은 프로젝트 전체에서 일관되어야 합니다.
  • 프론트가 같은 방식으로 에러 메시지, 코드, requestId를 처리할 수 있어야 합니다.

➕ 4-1. 추천 구조

{
  "success": false,
  "error": {
    "code": "CONSULT_STATUS_CONFLICT",
    "message": "이미 다른 관리자가 상담 상태를 변경했습니다.",
    "details": null
  },
  "requestId": "req_20260831_abcd1234",
  "timestamp": "2026-08-31T09:00:00.000Z",
  "path": "/admin/consults/10/status"
}

➕ 4-2. 필드 의미

필드의미
success요청 성공 여부
error.code프론트/운영에서 식별할 에러 코드
error.message사용자 또는 관리자에게 보여줄 메시지
error.detailsvalidation 상세 등 추가 정보
requestId추적용 ID
timestamp발생 시각
path요청 경로

➕ 4-3. 주의

stack trace를 응답에 포함하지 않기
DB 에러 원문을 그대로 노출하지 않기
Secret/Token/전화번호 원본 포함 금지
운영 메시지와 사용자 메시지 구분
  • 응답 메시지는 사용자에게 보여질 수 있습니다.
  • 내부 디버깅 정보는 로그에만 안전하게 남기고, 응답에는 노출하지 않아야 합니다.

✅ 5. 성공 응답 구조도 맞출까?

  • 에러 응답을 표준화한다면 성공 응답도 어느 정도 맞추는 것이 좋습니다.
  • 다만 모든 API에 지나치게 복잡한 wrapper를 강제할 필요는 없습니다.

➕ 5-1. 기본 성공 응답

{
  "success": true,
  "data": {
    "id": 10,
    "status": "CALLED"
  },
  "requestId": "req_20260831_abcd1234"
}

➕ 5-2. 목록 응답

{
  "success": true,
  "data": [
    {
      "id": 10,
      "customerName": "김**",
      "status": "NEW"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 142,
    "totalPages": 8
  },
  "requestId": "req_20260831_abcd1234"
}

➕ 5-3. 기준

단건:
data

목록:
data + meta

비동기 작업:
jobId + status

에러:
error + requestId
  • 응답 구조가 통일되면 프론트 공통 처리와 API 문서화가 쉬워집니다.
  • 특히 관리자 화면에서는 목록 meta 구조가 중요합니다.

✅ 6. Error Code 설계

  • message는 사람이 읽는 문구이고, code는 시스템이 판단하는 식별자입니다.
  • 프론트는 message보다 code를 기준으로 특별 처리를 하는 것이 좋습니다.

➕ 6-1. 좋은 에러 코드

VALIDATION_ERROR
UNAUTHORIZED
FORBIDDEN
RESOURCE_NOT_FOUND

CONSULT_NOT_FOUND
CONSULT_DUPLICATED
CONSULT_STATUS_TRANSITION_INVALID
CONSULT_STATUS_CONFLICT

PRODUCT_NOT_FOUND
PRODUCT_INACTIVE
PRODUCT_DELETED

EXPORT_JOB_NOT_FOUND
EXPORT_FILE_EXPIRED
EXPORT_PERMISSION_DENIED

NOTIFICATION_PROVIDER_TIMEOUT
NOTIFICATION_INVALID_TEMPLATE
NOTIFICATION_RETRY_EXCEEDED

➕ 6-2. 나쁜 에러 코드

ERROR
FAIL
BAD
SERVER_ERROR
UNKNOWN
NO
WRONG

문제:

원인 파악 어려움
프론트 분기 처리 어려움
로그 검색 어려움
운영 문서화 어려움

➕ 6-3. 설계 기준

도메인_원인 형태
프론트 분기 가능한 수준
운영자가 검색 가능한 이름
너무 세밀하게 폭증하지 않게 관리
  • 에러 코드는 너무 적어도 문제고, 너무 많아도 관리가 어렵습니다.
  • 핵심 업무 흐름부터 코드화하면 됩니다.

✅ 7. Custom Exception 만들기

  • NestJS 기본 Exception만 사용해도 되지만, 프로젝트 표준 에러 코드를 넣으려면 Custom Exception을 만들 수 있습니다.

➕ 7-1. 기본 AppException

export class AppException extends HttpException {
  constructor(params: {
    statusCode: number;
    code: string;
    message: string;
    details?: unknown;
  }) {
    super(
      {
        code: params.code,
        message: params.message,
        details: params.details ?? null,
      },
      params.statusCode,
    );
  }
}

➕ 7-2. 도메인 Exception 예시

export class ConsultNotFoundException extends AppException {
  constructor() {
    super({
      statusCode: 404,
      code: 'CONSULT_NOT_FOUND',
      message: '상담을 찾을 수 없습니다.',
    });
  }
}

export class ConsultStatusConflictException extends AppException {
  constructor() {
    super({
      statusCode: 409,
      code: 'CONSULT_STATUS_CONFLICT',
      message: '이미 다른 관리자가 상담 상태를 변경했습니다. 새로고침 후 다시 시도해주세요.',
    });
  }
}

➕ 7-3. 장점

에러 코드 일관성 유지
메시지 중복 감소
프론트 분기 쉬움
테스트에서 특정 에러 확인 가능
  • 처음부터 모든 에러를 클래스로 만들 필요는 없습니다.
  • 자주 쓰이고 중요한 도메인 에러부터 분리하면 됩니다.

✅ 8. Exception Filter란 무엇인가?

  • Exception Filter는 NestJS에서 발생한 예외를 잡아 표준 응답으로 변환하는 계층입니다.
  • Controller, Use Case, Guard 등에서 던진 Exception을 마지막에 잡아 일관된 형태로 반환할 수 있습니다.
Controller / Use Case / Guard
  ↓
throw exception
  ↓
GlobalExceptionFilter
  ↓
표준 에러 응답 반환
  ↓
로그 기록

➕ 8-1. 필요한 이유

응답 구조 통일
예상 못한 에러 500 처리
requestId 포함
민감정보 응답 노출 방지
로그 형식 통일
  • Exception Filter는 에러 처리의 중앙 관문입니다.
  • 프로젝트 전체의 에러 응답 품질을 결정합니다.

✅ 9. Global Exception Filter 예시

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const request = ctx.getRequest<Request>();
    const response = ctx.getResponse<Response>();

    const requestId =
      request.headers['x-request-id']?.toString() ?? randomUUID();

    const timestamp = new Date().toISOString();
    const path = request.url;

    const normalized = this.normalizeException(exception);

    this.logger.error({
      event: 'HTTP_EXCEPTION',
      requestId,
      path,
      method: request.method,
      statusCode: normalized.statusCode,
      code: normalized.code,
      message: normalized.message,
      details: normalized.safeDetails,
    });

    response.status(normalized.statusCode).json({
      success: false,
      error: {
        code: normalized.code,
        message: normalized.message,
        details: normalized.clientDetails,
      },
      requestId,
      timestamp,
      path,
    });
  }

  private normalizeException(exception: unknown) {
    if (exception instanceof HttpException) {
      const statusCode = exception.getStatus();
      const body = exception.getResponse();

      if (typeof body === 'object' && body !== null) {
        const value = body as Record<string, unknown>;

        return {
          statusCode,
          code: String(value.code ?? this.defaultCode(statusCode)),
          message: String(value.message ?? '요청 처리 중 오류가 발생했습니다.'),
          clientDetails: value.details ?? null,
          safeDetails: sanitizeErrorDetails(value.details),
        };
      }

      return {
        statusCode,
        code: this.defaultCode(statusCode),
        message: String(body),
        clientDetails: null,
        safeDetails: null,
      };
    }

    return {
      statusCode: 500,
      code: 'INTERNAL_SERVER_ERROR',
      message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
      clientDetails: null,
      safeDetails: null,
    };
  }

  private defaultCode(statusCode: number) {
    switch (statusCode) {
      case 400:
        return 'BAD_REQUEST';
      case 401:
        return 'UNAUTHORIZED';
      case 403:
        return 'FORBIDDEN';
      case 404:
        return 'NOT_FOUND';
      case 409:
        return 'CONFLICT';
      default:
        return 'ERROR';
    }
  }
}

➕ 9-1. 설계 포인트

모든 예외를 표준 응답으로 변환
requestId 포함
예상 못한 에러는 500으로 숨김
로그에는 필요한 정보만 안전하게 기록
응답에는 stack trace 미포함
  • Global Exception Filter를 두면 에러 응답이 흔들리지 않습니다.
  • 특히 예상 못한 에러가 발생했을 때 내부 정보를 숨기는 역할이 중요합니다.

✅ 10. Validation Error 처리

  • DTO validation 실패는 프론트에서 필드별로 표시할 수 있어야 합니다.
  • NestJS의 ValidationPipe와 exceptionFactory를 이용해 표준화할 수 있습니다.

➕ 10-1. ValidationPipe 설정 예시

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    exceptionFactory: (errors) => {
      return new AppException({
        statusCode: 400,
        code: 'VALIDATION_ERROR',
        message: '입력값을 확인해주세요.',
        details: errors.map((error) => ({
          field: error.property,
          constraints: error.constraints,
        })),
      });
    },
  }),
);

➕ 10-2. 응답 예시

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "입력값을 확인해주세요.",
    "details": [
      {
        "field": "phone",
        "constraints": {
          "isString": "phone must be a string"
        }
      }
    ]
  },
  "requestId": "req_123",
  "timestamp": "2026-08-31T09:00:00.000Z",
  "path": "/consults"
}

➕ 10-3. 주의

영문 라이브러리 메시지를 그대로 노출할지 결정
프론트 표시용 한글 메시지 매핑 고려
민감 필드 validation 값 노출 금지
  • Validation Error는 사용자가 수정할 수 있는 에러입니다.
  • 어떤 필드가 잘못됐는지 프론트가 알 수 있어야 합니다.

✅ 11. Prisma Error 처리

  • Prisma에서 발생하는 DB 에러를 그대로 응답하면 안 됩니다.
  • unique constraint, record not found, foreign key constraint 등을 도메인 에러로 바꿔야 합니다.

➕ 11-1. 자주 보는 Prisma 에러

P2002:
Unique constraint failed

P2025:
Record not found

P2003:
Foreign key constraint failed

P2014:
Required relation violation

➕ 11-2. Unique constraint 예시

try {
  return await this.consultRepository.create(command);
} catch (error) {
  if (isPrismaUniqueConstraintError(error)) {
    throw new AppException({
      statusCode: 409,
      code: 'CONSULT_DUPLICATED',
      message: '이미 접수된 상담 신청입니다.',
    });
  }

  throw error;
}

➕ 11-3. Prisma 에러 응답 노출 주의

DB 테이블명/컬럼명 노출 주의
constraint 이름 그대로 노출 금지
SQL 상세 오류 응답 금지
운영 로그에도 개인정보 포함 여부 주의
  • Prisma 에러는 개발자에게는 유용하지만 사용자에게 보여줄 메시지는 아닙니다.
  • Use Case 또는 Repository 경계에서 도메인 에러로 변환하는 것이 좋습니다.

✅ 12. Domain Error와 Infra Error 구분

  • 에러는 도메인 문제와 인프라 문제를 구분해야 합니다.
구분의미예시
Domain Error업무 규칙상 처리 불가상태 전이 불가
Infra Error시스템/외부 의존성 문제DB 연결 실패, 외부 API timeout

➕ 12-1. Domain Error 예시

상담 중복 신청
상태 전이 불가
권한 없음
상품 비활성화
파일 만료

➕ 12-2. Infra Error 예시

PostgreSQL connection error
S3 upload failed
Alimtalk API timeout
Redis connection error
외부 Webhook signature config missing

➕ 12-3. 기준

Domain Error:
사용자/관리자가 조치할 수 있는 경우가 많음

Infra Error:
운영자/개발자가 확인해야 함

응답:
Infra Error는 내부 상세를 숨기고 requestId 제공
  • 도메인 에러와 인프라 에러를 구분하면 대응이 쉬워집니다.
  • 특히 외부 API 장애를 고객 입력 오류처럼 보여주면 안 됩니다.

✅ 13. 외부 API 에러 처리

  • 외부 API 에러는 Worker/Adapter에서 분류해야 합니다.
  • 알림톡/SMS, S3, 외부 CRM 등은 실패 유형에 따라 재시도 여부가 달라집니다.

➕ 13-1. Adapter 반환 타입

export type ExternalApiResult<T> =
  | {
      ok: true;
      data: T;
      providerMessageId?: string;
    }
  | {
      ok: false;
      errorKind: 'RETRYABLE' | 'NON_RETRYABLE' | 'AUTH' | 'RATE_LIMIT';
      errorCode: string;
      safeMessage: string;
    };

➕ 13-2. Worker 처리

const result = await this.alimtalkAdapter.sendTemplate(params);

if (result.ok) {
  await this.notificationJobRepository.markSent({
    jobId: job.id,
    providerMessageId: result.providerMessageId,
  });

  return;
}

if (result.errorKind === 'RETRYABLE' || result.errorKind === 'RATE_LIMIT') {
  await this.notificationJobRepository.markRetrying({
    jobId: job.id,
    errorCode: result.errorCode,
    errorMessage: result.safeMessage,
  });

  return;
}

await this.notificationJobRepository.markFailed({
  jobId: job.id,
  errorCode: result.errorCode,
  errorMessage: result.safeMessage,
});

➕ 13-3. 주의

외부 API 원본 에러 전체 저장 금지
Authorization header 로그 금지
전화번호 원본 로그 금지
safeMessage만 저장
  • 외부 API 에러는 HTTP 응답보다 Job 상태에 기록되는 경우가 많습니다.
  • 관리자 화면에서 실패 사유를 이해할 수 있게 안전한 메시지를 저장해야 합니다.

✅ 14. Worker 에러 처리

  • Worker는 API 요청과 달리 사용자에게 즉시 응답하지 않습니다.
  • 따라서 에러 발생 시 Job 상태, retryCount, errorCode, 로그가 중요합니다.

➕ 14-1. Worker 에러 흐름

Job claim
  ↓
작업 실행
  ↓
에러 발생
  ↓
재시도 가능 여부 판단
  ↓
RETRYING 또는 FAILED 저장
  ↓
로그 기록

➕ 14-2. Worker try/catch 기준

try {
  await this.processJob(job);
} catch (error) {
  const safeError = normalizeWorkerError(error);

  await this.jobRepository.markFailedOrRetry({
    jobId: job.id,
    errorCode: safeError.code,
    errorMessage: safeError.message,
    retryable: safeError.retryable,
  });

  this.logger.error({
    event: 'JOB_FAILED',
    jobId: job.id,
    jobType: job.type,
    errorCode: safeError.code,
    retryable: safeError.retryable,
  });
}

➕ 14-3. 기준

Worker가 죽지 않게 job 단위 try/catch
실패한 Job 상태 저장
retry 가능 여부 분류
로그에 jobId 포함
민감정보 로그 금지
  • Worker 하나의 Job 실패 때문에 Worker 프로세스 전체가 죽으면 안 됩니다.
  • 실패는 데이터로 남겨야 운영자가 볼 수 있습니다.

✅ 15. 에러 로그 설계

  • 에러 로그는 장애 분석을 위한 핵심 자료입니다.
  • 하지만 로그에 너무 많은 정보를 넣으면 보안 문제가 됩니다.

➕ 15-1. 좋은 에러 로그

{
  "level": "error",
  "event": "HTTP_EXCEPTION",
  "requestId": "req_123",
  "method": "POST",
  "path": "/admin/consults/10/status",
  "statusCode": 409,
  "code": "CONSULT_STATUS_CONFLICT",
  "adminId": 3,
  "targetType": "CONSULT",
  "targetId": "10"
}

➕ 15-2. 나쁜 에러 로그

전화번호 01012345678 고객 상태 변경 실패
Authorization: Bearer ...
DATABASE_URL=postgresql://...
알림톡 API key=...
상담 메모 전체 출력

➕ 15-3. 로그 필드 기준

포함:
requestId
adminId
method
path
statusCode
errorCode
targetType
targetId
durationMs

제외:
전화번호 원본
토큰
Secret
상담 메모 전체
DB 접속 문자열
  • 로그는 내부 시스템에 저장되더라도 안전해야 합니다.
  • 특히 운영 DB와 외부 API를 다루는 프로젝트에서는 로그 노출이 곧 보안 사고가 될 수 있습니다.

✅ 16. requestId와 에러 응답

  • 에러 응답에는 requestId를 포함하는 것이 좋습니다.
  • 사용자가 “이 에러가 났다”고 알려줄 때 requestId로 로그를 찾을 수 있습니다.

➕ 16-1. 에러 응답 예시

{
  "success": false,
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.",
    "details": null
  },
  "requestId": "req_123",
  "timestamp": "2026-08-31T09:00:00.000Z",
  "path": "/admin/products/10"
}

➕ 16-2. 운영 대응

관리자가 에러 화면 캡처
  ↓
requestId 확인
  ↓
CloudWatch/서버 로그에서 requestId 검색
  ↓
관련 API 로그 확인
  ↓
Audit Log/Job 상태와 연결
  • requestId는 장애 대응 시간을 줄입니다.
  • 화면에도 표시하거나, 개발자 도구 응답에서 확인 가능하게 하면 좋습니다.

✅ 17. 프론트 에러 처리 기준

  • 백엔드 에러 응답이 표준화되면 프론트에서도 공통 처리가 가능해집니다.

➕ 17-1. 프론트 공통 처리

401:
로그인 만료 처리

403:
권한 없음 안내

404:
데이터 없음 또는 이전 페이지 이동

409:
충돌 안내 + 새로고침 유도

500:
일시 오류 안내 + requestId 표시

➕ 17-2. 상태 변경 충돌 예시

상담 상태 변경 요청
  ↓
409 CONSULT_STATUS_CONFLICT
  ↓
"이미 다른 관리자가 수정했습니다. 새로고침 후 다시 시도해주세요."
  ↓
상담 상세/목록 재조회

➕ 17-3. Validation Error 처리

VALIDATION_ERROR
  ↓
field별 details 확인
  ↓
해당 input 아래 메시지 표시
  • 프론트는 message를 그대로 띄울 수도 있지만, 중요한 흐름은 error.code 기준으로 분기하는 것이 좋습니다.
  • 특히 401, 403, 409는 공통 처리 가치가 높습니다.

✅ 18. 관리자 화면 에러 UX

  • 관리자 화면의 에러는 단순 alert보다 조치 가능해야 합니다.
  • 운영자는 에러를 보고 다음 행동을 알아야 합니다.

➕ 18-1. 좋은 메시지

이미 다른 관리자가 상담 상태를 변경했습니다. 새로고침 후 다시 시도해주세요.

엑셀 파일이 만료되었습니다. 같은 조건으로 다시 다운로드 요청해주세요.

해당 상품은 현재 비활성화되어 상담 신청이 불가능합니다.

상담 엑셀 다운로드 권한이 없습니다. 관리자에게 권한을 요청해주세요.

➕ 18-2. 나쁜 메시지

Error
Failed
Server Error
Bad Request
Invalid
알 수 없는 오류

➕ 18-3. 기준

원인:
무엇이 문제인지

조치:
어떻게 해야 하는지

추적:
requestId 또는 jobId 제공
  • 관리자는 빠르게 업무를 이어가야 합니다.
  • 에러 메시지는 친절함보다 “조치 가능성”이 중요합니다.

✅ 19. 보안 관점의 에러 처리

  • 에러 메시지는 공격자에게 힌트를 줄 수 있습니다.
  • 특히 로그인, 권한, DB, 외부 API, Secret 관련 에러는 조심해야 합니다.

➕ 19-1. 로그인 에러

나쁜 예:
존재하지 않는 이메일입니다.
비밀번호가 틀렸습니다.

좋은 예:
이메일 또는 비밀번호가 올바르지 않습니다.

➕ 19-2. 권한 에러

권한 없음:
접근 권한이 없습니다.

너무 자세한 내부 권한 구조 노출은 피하기

➕ 19-3. 서버 에러

응답:
서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.

로그:
내부 stack trace, requestId, errorCode 기록
  • 사용자에게 필요한 정보와 개발자에게 필요한 정보는 다릅니다.
  • 응답은 안전하게, 로그는 추적 가능하게 남기는 것이 기준입니다.

✅ 20. 에러와 Audit Log의 관계

  • 모든 에러를 Audit Log에 남길 필요는 없습니다.
  • 하지만 중요한 작업이 실패했거나 권한 없는 접근이 반복되면 Audit Log 또는 Security Log 대상으로 볼 수 있습니다.

➕ 20-1. Audit Log 후보

권한 없는 엑셀 다운로드 시도
관리자 계정 관리 접근 실패
관리자 로그인 실패 반복
상품 삭제 실패
알림톡 수동 재발송 실패
Webhook signature 검증 실패

➕ 20-2. Application Log로 충분한 경우

일반 validation error
일시적인 목록 조회 실패
외부 API timeout
단순 404

➕ 20-3. 기준

보안/권한 관련 실패:
Audit/Security Log 고려

일반 시스템 오류:
Application Log

운영 작업 실패:
Job 상태 + Application Log
  • 에러 기록도 목적에 따라 나눠야 합니다.
  • Audit Log를 모든 에러 저장소로 쓰면 안 됩니다.

✅ 21. Exception Filter와 Logging Interceptor의 역할 분리

  • Exception Filter는 예외를 응답으로 바꾸는 역할입니다.
  • Logging Interceptor는 요청/응답 시간, 성공/실패 로그를 남기는 역할입니다.
Exception Filter:
에러 응답 변환

Logging Interceptor:
요청 처리 시간과 결과 기록

➕ 21-1. Logging Interceptor 예시 흐름

요청 시작 시간 기록
  ↓
Controller 처리
  ↓
응답 성공/실패
  ↓
durationMs 로그

➕ 21-2. 역할 기준

Exception Filter:
에러 표준화
예상 못한 에러 숨김
에러 응답 반환

Logging Interceptor:
requestId
method/path
statusCode
durationMs
adminId
  • 두 역할을 하나에 몰아넣으면 코드가 복잡해집니다.
  • 에러 응답 표준화와 요청 로그 수집은 분리하는 것이 좋습니다.

✅ 22. 외부로 보여줄 메시지와 내부 메시지 분리

  • 에러는 외부 메시지와 내부 메시지를 분리하는 것이 안전합니다.

➕ 22-1. 예시

Public message:
서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.

Internal message:
PrismaClientKnownRequestError P2002 on consults_phone_date_unique

➕ 22-2. 기준

사용자/관리자에게:
조치 가능한 안전한 메시지

로그에:
개발자가 원인 파악 가능한 내부 정보

응답에:
stack trace, SQL, Secret, 내부 경로 노출 금지
  • 관리자도 내부 개발자가 아닐 수 있습니다.
  • 운영자에게 보여줄 메시지는 조치 중심으로, 개발 로그는 원인 중심으로 남기는 것이 좋습니다.

✅ 23. 현재 프로젝트 적용 우선순위

➕ 23-1. 1순위: 표준 에러 응답 정의

success
error.code
error.message
error.details
requestId
timestamp
path

완료 기준:

모든 API 에러 응답 구조가 통일됨
프론트 공통 에러 처리 가능
requestId로 로그 추적 가능

➕ 23-2. 2순위: GlobalExceptionFilter 적용

HttpException 처리
AppException 처리
Validation Error 처리
Unknown Error 500 처리
민감정보 응답 제거

완료 기준:

예상 못한 에러도 안전한 응답으로 반환
에러 로그가 구조화됨
stack trace가 응답에 노출되지 않음

➕ 23-3. 3순위: 도메인 에러 코드 정리

CONSULT_NOT_FOUND
CONSULT_DUPLICATED
CONSULT_STATUS_TRANSITION_INVALID
CONSULT_STATUS_CONFLICT
PRODUCT_INACTIVE
EXPORT_FILE_EXPIRED
FORBIDDEN

완료 기준:

상담/상품/Export/권한 관련 주요 실패가 코드화됨
프론트가 code 기준으로 분기 가능
운영 로그 검색 가능

➕ 23-4. 4순위: Worker/외부 API 에러 정리

NOTIFICATION_PROVIDER_TIMEOUT
NOTIFICATION_INVALID_TEMPLATE
EXPORT_GENERATION_FAILED
S3_UPLOAD_FAILED
WEBHOOK_SIGNATURE_INVALID

완료 기준:

Job 실패 사유가 errorCode로 저장됨
retryable/non-retryable 구분 가능
관리자 화면에서 실패 원인 확인 가능
  • 지금 단계에서는 표준 응답과 GlobalExceptionFilter부터 적용하는 것이 가장 효과적입니다.
  • 그다음 도메인 에러 코드와 Worker 에러 코드를 정리하면 됩니다.

✅ 24. AI/Codex에게 에러 처리 구조를 맡길 때 규칙

➕ 24-1. Codex 요청 예시

NestJS 관리자 API에 표준 Error Handling 구조를 추가해줘.

조건:
1. 모든 에러 응답은 success=false, error.code, error.message, error.details, requestId, timestamp, path 구조로 통일해줘
2. AppException 클래스를 만들어 statusCode, code, message, details를 받을 수 있게 해줘
3. ConsultNotFoundException, ConsultDuplicatedException, ConsultStatusConflictException 같은 도메인 예외 예시를 만들어줘
4. GlobalExceptionFilter를 만들어 HttpException, AppException, unknown error를 표준 응답으로 변환해줘
5. unknown error는 INTERNAL_SERVER_ERROR로 응답하고 stack trace는 응답에 노출하지 마
6. ValidationPipe exceptionFactory를 설정해 VALIDATION_ERROR 형태로 반환해줘
7. Prisma P2002 unique constraint는 필요한 Use Case에서 CONSULT_DUPLICATED 같은 도메인 에러로 변환해줘
8. 에러 로그에는 requestId, method, path, statusCode, code를 남기되 phone, token, Secret, Authorization header, DATABASE_URL은 남기지 마
9. 프론트가 401/403/409/500을 공통 처리할 수 있도록 에러 코드 목록을 정리해줘
10. 변경 후 테스트 케이스와 QA 체크리스트를 작성해줘

➕ 24-2. 리뷰 기준

응답 구조가 모든 에러에서 일관되는가?
unknown error가 내부 정보를 노출하지 않는가?
Validation Error details가 프론트에서 쓰기 좋은가?
Prisma 에러 원문이 응답에 노출되지 않는가?
requestId가 응답과 로그에 모두 포함되는가?
로그에 민감정보가 남지 않는가?
도메인 에러 코드가 과하게 많지 않은가?
프론트 공통 처리 기준이 정리되었는가?
  • AI/Codex에게 에러 처리를 맡길 때는 “응답 구조”와 “노출 금지 정보”를 반드시 같이 지정해야 합니다.
  • 에러 처리는 보안과 운영 품질에 동시에 영향을 줍니다.

✅ 25. 실무 체크리스트

➕ 25-1. 표준 응답 체크리스트

  • 에러 응답 구조가 통일되어 있는가?
  • 성공 응답 구조가 일관적인가?
  • 목록 응답에 meta가 포함되는가?
  • 에러 응답에 requestId가 포함되는가?
  • timestamp와 path가 포함되는가?
  • stack trace가 응답에 노출되지 않는가?
  • DB 에러 원문이 응답에 노출되지 않는가?
  • 프론트가 error.code 기준으로 분기할 수 있는가?

➕ 25-2. Exception Filter 체크리스트

  • GlobalExceptionFilter가 적용되어 있는가?
  • HttpException을 표준 응답으로 변환하는가?
  • AppException을 처리하는가?
  • Unknown Error를 500으로 안전하게 처리하는가?
  • requestId를 응답과 로그에 포함하는가?
  • Validation Error를 표준화하는가?
  • 에러 로그가 구조화되어 있는가?
  • 민감정보를 로그에서 제거하는가?

➕ 25-3. 도메인 에러 체크리스트

  • 상담 없음은 404로 처리하는가?
  • 상담 중복은 409로 처리하는가?
  • 상태 전이 불가는 400 또는 422로 처리하는가?
  • 동시 수정 충돌은 409로 처리하는가?
  • 권한 없음은 403으로 처리하는가?
  • 파일 만료는 명확한 에러 코드로 처리하는가?
  • 상품 비활성화는 별도 코드로 처리하는가?
  • Prisma 에러를 도메인 에러로 변환하는가?

➕ 25-4. 보안/운영 체크리스트

  • 전화번호 원본이 에러 응답에 노출되지 않는가?
  • Authorization header가 로그에 남지 않는가?
  • Secret/API Key/DATABASE_URL이 로그에 남지 않는가?
  • 외부 API response 전체를 로그에 남기지 않는가?
  • 관리자 화면에 조치 가능한 메시지를 보여주는가?
  • 401/403/409/500 공통 UX가 있는가?
  • requestId로 CloudWatch/서버 로그를 추적할 수 있는가?
  • Job 실패는 Job 상태와 errorCode로 남는가?

✅ 26. AI에게 Error Handling 설계를 물어볼 때 좋은 질문법

NestJS + Prisma + PostgreSQL 기반 온라인 휴대폰 판매몰 관리자 API의 Error Handling 구조를 설계하려고 해.

서비스 상황:
1. 고객 상담 신청, 관리자 상담 상태 변경, 상품 수정, 엑셀 Export 요청, 알림톡 Worker, Webhook 수신 기능이 있음
2. 프론트에서 모든 API 에러를 공통 처리할 수 있게 표준 에러 응답 구조가 필요함
3. 상담 중복 신청, 상담 없음, 상태 전이 불가, 동시 수정 충돌, 권한 없음, 상품 비활성화, Export 파일 만료 같은 도메인 에러가 있음
4. Prisma unique constraint, record not found 같은 DB 에러를 사용자 친화적인 도메인 에러로 변환하고 싶음
5. Validation Error는 필드별 details를 내려주고 싶음
6. unknown error는 내부 정보를 숨기고 requestId만 제공하고 싶음
7. Worker와 외부 API 실패는 retryable/non-retryable로 분류하고 Job 상태에 errorCode를 저장하고 싶음
8. 전화번호 원본, token, Secret, Authorization header, DATABASE_URL, 외부 API 원본 response는 응답/로그에 남기면 안 됨
9. requestId로 API 로그, Audit Log, Worker 로그를 연결하고 싶음

요청:
- 표준 성공/에러 응답 구조
- Error Code 설계 기준
- AppException 구조
- GlobalExceptionFilter 예시
- ValidationPipe exceptionFactory 예시
- Prisma Error 변환 기준
- Domain Error와 Infra Error 구분
- 외부 API/Worker 에러 처리 방식
- 에러 로그 필드 기준
- 프론트 공통 에러 처리 기준
- 관리자 화면 에러 UX
- 보안 체크리스트
를 실무 기준으로 정리해줘.

➕ 26-1. AI 답변 검증 기준

모든 에러를 500으로 처리하지 않는가?
표준 응답에 error.code와 requestId를 포함하는가?
unknown error에서 stack trace를 숨기는가?
Validation Error details 구조를 제안하는가?
Prisma 에러 원문을 응답에 노출하지 않게 하는가?
409 Conflict를 동시 수정/중복 상황에 사용하도록 설명하는가?
외부 API 실패와 도메인 에러를 구분하는가?
로그에 전화번호/토큰/Secret을 남기지 않도록 경고하는가?
프론트 공통 처리와 관리자 UX까지 연결하는가?

📌 요약

  • Error Handling은 단순 예외 처리가 아니라 프론트 UX, 운영 추적성, 장애 대응, 보안까지 연결되는 백엔드 핵심 구조입니다.
  • API 에러는 Validation, Unauthorized, Forbidden, Not Found, Conflict, External API Error, Internal Error처럼 종류별로 구분해야 합니다.
  • 모든 에러 응답은 success=false, error.code, error.message, error.details, requestId, timestamp, path 같은 표준 구조로 통일하는 것이 좋습니다.
  • message는 사람이 읽는 문구이고, code는 프론트와 운영자가 분기/검색할 수 있는 식별자입니다.
  • NestJS에서는 AppException과 GlobalExceptionFilter를 만들어 에러 응답을 중앙에서 표준화할 수 있습니다.
  • Validation Error는 ValidationPipe의 exceptionFactory로 필드별 details를 내려주면 프론트 입력창 에러 처리에 유리합니다.
  • Prisma의 unique constraint, record not found 같은 DB 에러는 그대로 노출하지 말고 CONSULT_DUPLICATED, CONSULT_NOT_FOUND 같은 도메인 에러로 변환해야 합니다.
  • Worker와 외부 API 에러는 retryable/non-retryable로 분류하고, Job 상태에 errorCode, errorMessage, retryCount를 저장해야 운영자가 확인할 수 있습니다.
  • 에러 응답과 로그에는 전화번호 원본, 토큰, Secret, Authorization header, DATABASE_URL, 외부 API 원본 response가 노출되지 않도록 해야 합니다.
  • 프론트는 401, 403, 404, 409, 500을 공통 처리하고, 관리자 화면에는 원인과 다음 조치가 드러나는 메시지를 보여주는 것이 좋습니다.
  • requestId를 에러 응답, API 로그, Audit Log, Worker 로그에 연결하면 장애 분석과 운영 대응 속도가 크게 올라갑니다.

0개의 댓글