TIL - 20260605

juni·2026년 6월 5일

TIL

목록 보기
370/468

0605 풀스택 실무 기초 (8/N): 에러 처리와 로깅 전략


✅ 1. 에러 처리란 무엇인가?

  • 에러 처리(Error Handling)란 애플리케이션에서 문제가 발생했을 때, 프로그램이 갑자기 죽거나 사용자에게 이상한 화면을 보여주지 않도록 적절히 대응하는 작업입니다.
  • 웹서비스에서는 프론트엔드, 백엔드, DB, 외부 API, 네트워크 등 여러 지점에서 에러가 발생할 수 있습니다.
  • 좋은 에러 처리는 단순히 try-catch를 쓰는 것이 아니라, 사용자에게는 이해 가능한 메시지를 보여주고, 개발자에게는 원인 분석에 필요한 정보를 남기는 것입니다.

➕ 1-1. 에러 처리가 중요한 이유

  • 사용자 경험 보호

    • 오류가 발생해도 사용자가 무엇을 해야 하는지 알 수 있어야 합니다.
    • 예를 들어 “서버 오류”만 보여주는 것보다 “잠시 후 다시 시도해주세요”가 더 낫습니다.
  • 장애 원인 분석

    • 로그가 없으면 장애 원인을 추측으로 찾아야 합니다.
    • 요청 시간, 사용자 ID, API 경로, 에러 메시지 등이 남아 있어야 빠르게 분석할 수 있습니다.
  • 서비스 안정성 향상

    • 예상 가능한 실패를 미리 처리하면 전체 서비스 장애로 번지는 것을 막을 수 있습니다.
    • 외부 API 실패, DB 연결 실패, 인증 만료 같은 상황에 대비할 수 있습니다.

✅ 2. 에러의 종류

➕ 2-1. 클라이언트 에러

  • 사용자의 요청이 잘못되었거나, 프론트엔드에서 잘못된 데이터를 보냈을 때 발생합니다.

  • 예시

    • 필수 입력값 누락
    • 전화번호 형식 오류
    • 존재하지 않는 페이지 접근
    • 로그인하지 않고 인증 API 호출
    • 권한 없는 사용자가 관리자 API 호출
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
422 Unprocessable Entity

➕ 2-2. 서버 에러

  • 백엔드 서버 내부에서 문제가 발생했을 때 나타납니다.

  • 예시

    • 코드 예외 처리 누락
    • DB 연결 실패
    • 환경변수 누락
    • 외부 API 호출 실패
    • 파일 업로드 실패
    • 예상하지 못한 데이터 형식
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout

➕ 2-3. 네트워크 에러

  • 클라이언트와 서버 사이의 통신 자체가 실패한 경우입니다.

  • 예시

    • 인터넷 연결 끊김
    • 서버 다운
    • DNS 문제
    • CORS 문제
    • 요청 시간 초과
  • 네트워크 에러는 HTTP 응답 자체가 없을 수 있기 때문에, 프론트엔드에서 별도로 처리해야 합니다.


✅ 3. 좋은 에러 응답 구조

  • 백엔드 API는 에러가 발생했을 때도 일정한 형식으로 응답하는 것이 좋습니다.
  • 에러 응답 형식이 제각각이면 프론트엔드에서 처리하기 어렵습니다.

➕ 3-1. 기본 에러 응답 예시

{
  "success": false,
  "message": "전화번호 형식이 올바르지 않습니다.",
  "errorCode": "INVALID_PHONE_NUMBER",
  "statusCode": 400
}
필드설명
success요청 성공 여부
message사용자 또는 프론트엔드에 보여줄 메시지
errorCode개발자가 구분하기 쉬운 에러 코드
statusCodeHTTP 상태 코드

➕ 3-2. 실무에서 좋은 에러 메시지

{
  "success": false,
  "message": "필수 입력값이 누락되었습니다.",
  "errorCode": "MISSING_REQUIRED_FIELD",
  "statusCode": 400
}
  • 사용자가 이해할 수 있어야 합니다.
  • 프론트엔드가 분기 처리할 수 있어야 합니다.
  • 서버 내부 정보는 숨겨야 합니다.

➕ 3-3. 피해야 할 에러 응답

{
  "message": "PrismaClientKnownRequestError: Unique constraint failed on the fields: (`email`)"
}
  • DB 구조나 내부 라이브러리 에러를 그대로 노출하면 보안상 좋지 않습니다.
  • 사용자에게는 간단한 메시지를 보여주고, 상세 에러는 서버 로그에 남겨야 합니다.
{
  "success": false,
  "message": "이미 사용 중인 이메일입니다.",
  "errorCode": "EMAIL_ALREADY_EXISTS",
  "statusCode": 409
}

✅ 4. NestJS에서 에러 처리

  • NestJS는 기본적으로 HTTP 예외 클래스를 제공합니다.
  • 상황에 맞는 예외를 던지면 NestJS가 적절한 상태 코드로 응답합니다.

➕ 4-1. BadRequestException

import { BadRequestException } from '@nestjs/common';

throw new BadRequestException('전화번호 형식이 올바르지 않습니다.');
  • 잘못된 요청일 때 사용합니다.
  • 상태 코드는 400 Bad Request입니다.

➕ 4-2. UnauthorizedException

import { UnauthorizedException } from '@nestjs/common';

throw new UnauthorizedException('로그인이 필요합니다.');
  • 인증되지 않은 사용자가 보호된 API에 접근할 때 사용합니다.
  • 상태 코드는 401 Unauthorized입니다.

➕ 4-3. ForbiddenException

import { ForbiddenException } from '@nestjs/common';

throw new ForbiddenException('접근 권한이 없습니다.');
  • 로그인은 되어 있지만 권한이 부족할 때 사용합니다.
  • 상태 코드는 403 Forbidden입니다.

➕ 4-4. NotFoundException

import { NotFoundException } from '@nestjs/common';

throw new NotFoundException('상품을 찾을 수 없습니다.');
  • 요청한 리소스가 없을 때 사용합니다.
  • 상태 코드는 404 Not Found입니다.

➕ 4-5. ConflictException

import { ConflictException } from '@nestjs/common';

throw new ConflictException('이미 등록된 전화번호입니다.');
  • 중복 데이터나 충돌 상황에서 사용합니다.
  • 상태 코드는 409 Conflict입니다.

✅ 5. try-catch 사용 기준

  • 모든 코드에 무작정 try-catch를 감싸는 것은 좋은 방식이 아닙니다.
  • try-catch는 에러를 의미 있게 처리할 수 있을 때 사용하는 것이 좋습니다.

➕ 5-1. try-catch가 필요한 경우

  • 외부 API 호출
  • 파일 업로드
  • 결제 요청
  • 문자 발송
  • 이메일 발송
  • 복구 가능한 실패 처리
  • 에러 로그를 추가로 남기고 싶은 경우
try {
  await this.smsService.send({
    phone: dto.phone,
    message: '상담 신청이 완료되었습니다.',
  });
} catch (error) {
  this.logger.error('문자 발송 실패', error);

  throw new BadRequestException('문자 발송에 실패했습니다.');
}

➕ 5-2. try-catch를 남발하면 생기는 문제

  • 실제 에러 원인이 숨겨질 수 있습니다.
  • 모든 에러가 같은 메시지로 바뀌면 디버깅이 어려워집니다.
  • 이미 NestJS가 처리할 수 있는 예외까지 불필요하게 감쌀 수 있습니다.
  • 로그 없이 에러를 삼키면 장애 추적이 불가능합니다.
try {
  // 중요한 로직
} catch (error) {
  return null;
}
  • 위 방식은 위험합니다.
  • 에러가 발생했는데 아무 일도 없었던 것처럼 넘어가면 나중에 더 큰 장애가 됩니다.

✅ 6. 프론트엔드에서 에러 처리

  • 프론트엔드에서는 API 요청 실패, 입력값 오류, 인증 만료, 네트워크 장애 등을 처리해야 합니다.
  • 사용자가 어떤 상황인지 알 수 있도록 적절한 메시지를 보여주는 것이 중요합니다.

➕ 6-1. fetch 에러 처리 예시

async function submitConsult(data: {
  name: string;
  phone: string;
  model: string;
}) {
  const response = await fetch('/api/consults', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });

  const result = await response.json();

  if (!response.ok) {
    throw new Error(result.message || '상담 신청에 실패했습니다.');
  }

  return result;
}

➕ 6-2. axios 에러 처리 예시

import axios from 'axios';

async function submitConsult(data: {
  name: string;
  phone: string;
  model: string;
}) {
  try {
    const response = await axios.post('/api/consults', data);
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      const message =
        error.response?.data?.message || '요청 처리 중 오류가 발생했습니다.';

      throw new Error(message);
    }

    throw new Error('알 수 없는 오류가 발생했습니다.');
  }
}

✅ 7. 사용자 메시지와 개발자 로그 분리

  • 사용자에게 보여주는 메시지와 개발자가 확인하는 로그는 달라야 합니다.

➕ 7-1. 사용자에게 보여줄 메시지

상담 신청에 실패했습니다. 잠시 후 다시 시도해주세요.
  • 짧고 이해하기 쉬워야 합니다.
  • 내부 시스템 구조를 노출하면 안 됩니다.
  • 사용자가 다음 행동을 알 수 있어야 합니다.

➕ 7-2. 개발자 로그에 남길 정보

[ERROR] POST /api/consults
userId: 123
phone: 010****5678
errorCode: SMS_SEND_FAILED
message: 문자 발송 API timeout
timestamp: 2026-06-05T10:30:00.000Z
  • 장애 원인 분석에 필요한 정보가 있어야 합니다.
  • 단, 개인정보와 토큰은 마스킹하거나 제외해야 합니다.

✅ 8. 로깅이란 무엇인가?

  • 로깅(Logging)은 애플리케이션에서 발생한 주요 이벤트와 오류를 기록하는 작업입니다.
  • 로그는 장애 분석, 보안 감사, 사용자 행동 추적, 성능 개선에 사용됩니다.

➕ 8-1. 로그가 필요한 이유

  • 어떤 요청에서 문제가 발생했는지 확인할 수 있습니다.
  • 특정 시간대의 장애 원인을 추적할 수 있습니다.
  • 외부 API 실패 여부를 확인할 수 있습니다.
  • 관리자 작업 이력을 남길 수 있습니다.
  • 반복적으로 발생하는 문제를 찾아 개선할 수 있습니다.

✅ 9. 로그 레벨

  • 로그는 중요도에 따라 레벨을 나누어 관리합니다.
레벨의미예시
debug개발 중 상세 정보변수값, 분기 확인
info정상적인 주요 이벤트로그인 성공, 주문 생성
warn주의가 필요한 상황외부 API 재시도
error오류 발생DB 저장 실패, API 실패
fatal서비스 중단급 오류서버 실행 실패, DB 전체 장애

➕ 9-1. 로그 레벨 사용 예시

this.logger.log('상담 신청 등록 완료');
this.logger.warn('문자 발송 API 응답 지연');
this.logger.error('상담 신청 저장 실패', error.stack);
  • 운영 환경에서는 debug 로그를 너무 많이 남기면 로그 비용과 가독성 문제가 생길 수 있습니다.
  • 개발 환경과 운영 환경의 로그 레벨을 다르게 설정하는 것이 좋습니다.

✅ 10. NestJS Logger

  • NestJS는 기본 Logger를 제공합니다.
  • 서비스나 컨트롤러에서 Logger를 사용하면 로그를 일관되게 남길 수 있습니다.

➕ 10-1. Logger 사용 예시

import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class ConsultService {
  private readonly logger = new Logger(ConsultService.name);

  async createConsult() {
    try {
      this.logger.log('상담 신청 생성 시작');

      // DB 저장 로직

      this.logger.log('상담 신청 생성 완료');
    } catch (error) {
      this.logger.error('상담 신청 생성 실패', error.stack);
      throw error;
    }
  }
}
  • ConsultService.name을 사용하면 어떤 클래스에서 발생한 로그인지 구분하기 쉽습니다.

✅ 11. 요청 로그

  • 요청 로그는 사용자가 어떤 API를 호출했는지 기록하는 로그입니다.
  • 장애 분석에서 매우 중요합니다.

➕ 11-1. 요청 로그에 포함하면 좋은 정보

  • HTTP 메서드
  • 요청 URL
  • 상태 코드
  • 응답 시간
  • 사용자 ID
  • IP 주소
  • User-Agent
  • 요청 시간
  • 에러 코드
[INFO] GET /api/products 200 35ms userId=123 ip=123.123.123.123
[ERROR] POST /api/consults 500 830ms userId=anonymous errorCode=DB_SAVE_FAILED

➕ 11-2. 요청 로그 주의점

  • 비밀번호를 남기면 안 됩니다.
  • JWT 토큰을 남기면 안 됩니다.
  • 주민등록번호, 계좌번호, 인증번호를 남기면 안 됩니다.
  • 전화번호나 이메일은 필요한 경우 마스킹해야 합니다.
좋은 예시:
phone=010****5678

나쁜 예시:
phone=01012345678

✅ 12. 프론트엔드 에러 로깅

  • 프론트엔드에서도 오류가 발생합니다.
  • 사용자의 브라우저에서 발생한 JavaScript 오류는 서버 로그에 남지 않기 때문에 별도 수집이 필요합니다.

➕ 12-1. 프론트엔드에서 자주 발생하는 오류

  • JavaScript 런타임 오류
  • API 호출 실패
  • 이미지 로딩 실패
  • 브라우저 호환성 문제
  • 모바일 화면에서만 발생하는 오류
  • 특정 사용자 행동에서만 발생하는 오류

➕ 12-2. Sentry 같은 도구 활용

  • Sentry는 프론트엔드와 백엔드 에러를 수집하는 도구입니다.

  • 오류 메시지, 발생 위치, 브라우저 정보, 사용자 환경 등을 확인할 수 있습니다.

  • 확인 가능한 정보

    • 오류 발생 파일
    • 오류 라인
    • 사용자 브라우저
    • 발생 시간
    • 배포 버전
    • 영향을 받은 사용자 수

✅ 13. 외부 API 에러 처리

  • 실무에서는 문자 발송, 본인인증, 결제, 광고 API, 통신사 API 등 외부 API를 자주 사용합니다.
  • 외부 API는 내가 통제할 수 없기 때문에 실패 가능성을 항상 고려해야 합니다.

➕ 13-1. 외부 API 실패 원인

  • API 서버 장애
  • 네트워크 timeout
  • 인증 키 만료
  • 요청 형식 오류
  • 호출 한도 초과
  • 응답 지연
  • 일시적인 점검

➕ 13-2. 외부 API 처리 기준

  1. timeout을 설정한다.
  2. 실패 로그를 남긴다.
  3. 사용자에게 적절한 안내를 보여준다.
  4. 재시도 가능 여부를 판단한다.
  5. 중요한 요청은 실패 이력을 DB에 남긴다.
  6. 장애가 반복되면 알림을 받게 한다.
try {
  await this.externalApi.sendMessage(payload);
} catch (error) {
  this.logger.error('외부 문자 API 호출 실패', error.stack);

  await this.failedMessageRepository.create({
    phone: payload.phone,
    reason: 'SMS_API_FAILED',
  });

  throw new BadRequestException('문자 발송에 실패했습니다.');
}

✅ 14. 에러를 삼키지 않기

  • 실무에서 가장 위험한 습관 중 하나는 에러를 잡아놓고 아무것도 하지 않는 것입니다.
try {
  await this.orderService.createOrder(dto);
} catch (error) {
  // 아무 처리 없음
}
  • 이 코드는 문제가 발생해도 로그가 남지 않고, 사용자도 실패를 알 수 없습니다.
  • 결과적으로 데이터 불일치, 중복 처리, 장애 추적 실패로 이어질 수 있습니다.

➕ 14-1. 최소한 해야 하는 처리

try {
  await this.orderService.createOrder(dto);
} catch (error) {
  this.logger.error('주문 생성 실패', error.stack);

  throw new InternalServerErrorException('주문 처리 중 오류가 발생했습니다.');
}
  • 최소한 로그를 남기고, 호출자에게 실패를 알려야 합니다.

✅ 15. 에러 처리와 트랜잭션

  • DB 작업이 여러 개 연결되어 있을 때는 중간에 실패하면 전체를 되돌려야 할 수 있습니다.
  • 이때 사용하는 것이 트랜잭션(Transaction)입니다.

➕ 15-1. 트랜잭션이 필요한 예시

1. 주문 생성
2. 주문 상세 생성
3. 재고 차감
4. 결제 이력 저장
  • 위 과정에서 3번 재고 차감이 실패했는데 1번 주문만 저장되면 데이터가 꼬입니다.
  • 이런 경우 전체 작업이 모두 성공하거나, 하나라도 실패하면 전체를 취소해야 합니다.

➕ 15-2. Prisma 트랜잭션 예시

await prisma.$transaction(async (tx) => {
  const order = await tx.order.create({
    data: {
      userId: 1,
      status: 'PENDING',
    },
  });

  await tx.orderItem.create({
    data: {
      orderId: order.id,
      productId: 10,
      quantity: 1,
    },
  });

  await tx.product.update({
    where: {
      id: 10,
    },
    data: {
      stock: {
        decrement: 1,
      },
    },
  });
});
  • 트랜잭션 안에서 하나라도 실패하면 전체 작업이 롤백됩니다.

✅ 16. 실무 체크리스트

➕ 16-1. 백엔드 에러 처리 체크리스트

  1. 상황에 맞는 HTTP 상태 코드를 반환하는가?
  2. 에러 응답 형식이 일정한가?
  3. 사용자에게 내부 에러를 그대로 노출하지 않는가?
  4. 서버 로그에는 원인 분석에 필요한 정보가 남는가?
  5. 외부 API 실패 시 timeout과 로그가 있는가?
  6. DB 작업 실패 시 데이터가 꼬이지 않도록 처리했는가?
  7. 인증 실패와 권한 실패를 구분하는가?
  8. 에러를 catch하고 그냥 삼키지 않는가?

➕ 16-2. 프론트엔드 에러 처리 체크리스트

  1. API 실패 시 사용자에게 메시지를 보여주는가?
  2. 로딩 상태와 실패 상태를 구분하는가?
  3. 중복 클릭을 방지하는가?
  4. 401 발생 시 재로그인 또는 토큰 재발급 처리가 있는가?
  5. 403 발생 시 권한 없음 메시지를 보여주는가?
  6. 네트워크 오류와 서버 오류를 구분하는가?
  7. 콘솔에 민감정보를 출력하지 않는가?
  8. 사용자 입력값 검증을 프론트와 백엔드 모두에서 하는가?

➕ 16-3. 로깅 체크리스트

  1. 요청 URL, 메서드, 상태 코드, 응답 시간이 기록되는가?
  2. 주요 관리자 작업 이력이 남는가?
  3. 에러 로그에 stack trace가 남는가?
  4. 개인정보와 토큰이 로그에 남지 않는가?
  5. 로그 레벨이 환경별로 적절히 설정되어 있는가?
  6. 장애 발생 시 로그를 쉽게 검색할 수 있는가?
  7. 반복되는 에러에 대해 알림을 받을 수 있는가?

✅ 17. AI를 활용해 에러를 해결할 때 질문법

  • 에러 해결을 AI에게 맡길 때는 단순히 “에러남”이라고 하면 안 됩니다.
  • 에러 메시지, 발생 상황, 관련 코드, 기대 동작, 실제 동작을 함께 줘야 정확도가 높아집니다.

➕ 17-1. 좋은 질문 예시

NestJS + Prisma에서 상담 신청 저장 중 에러가 발생해.

상황:
- POST /api/consults 호출 시 500 발생
- 프론트엔드에서는 "요청 처리 중 오류"만 표시됨
- 백엔드 로그에는 Prisma unique constraint 에러가 나옴
- phone 컬럼에 unique가 걸려 있음

관련 코드:
createConsult(dto)에서 prisma.consult.create 사용 중

원하는 처리:
- 중복 전화번호일 경우 409 Conflict 반환
- 사용자 메시지는 "이미 신청된 전화번호입니다."
- 서버 로그에는 실제 Prisma 에러를 남기고 싶음

NestJS에서 어떻게 처리하면 좋을지 코드와 함께 설명해줘.

➕ 17-2. AI 답변 검증 기준

  1. 모든 에러를 500으로 처리하지 않는가?
  2. Prisma 에러를 사용자에게 그대로 노출하지 않는가?
  3. 중복 데이터에 409 Conflict를 사용하는가?
  4. 서버 로그와 사용자 메시지를 분리하는가?
  5. 개인정보를 로그에 그대로 남기지 않는가?
  6. 프론트엔드에서 에러 메시지를 표시하는 흐름까지 설명하는가?

📌 요약

  • 에러 처리는 문제가 발생했을 때 사용자 경험을 보호하고, 개발자가 원인을 분석할 수 있게 만드는 작업입니다.
  • 클라이언트 에러, 서버 에러, 네트워크 에러를 구분해야 정확한 대응이 가능합니다.
  • API 에러 응답은 success, message, errorCode, statusCode처럼 일정한 구조로 설계하는 것이 좋습니다.
  • NestJS에서는 BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException, ConflictException 등을 상황에 맞게 사용할 수 있습니다.
  • 사용자에게는 이해 가능한 메시지를 보여주고, 개발자 로그에는 원인 분석에 필요한 정보를 남겨야 합니다.
  • 로그에는 비밀번호, JWT, 인증번호, 개인정보 같은 민감정보를 남기면 안 됩니다.
  • 외부 API는 실패 가능성을 전제로 timeout, 로그, 재시도, 실패 이력 저장을 고려해야 합니다.
  • 여러 DB 작업이 하나의 흐름으로 묶여 있다면 트랜잭션을 사용해 데이터 불일치를 막아야 합니다.
  • AI에게 에러 해결을 요청할 때는 에러 메시지, 상황, 관련 코드, 기대 동작을 함께 제공해야 정확한 답변을 받을 수 있습니다.

0개의 댓글