try-catch를 쓰는 것이 아니라, 사용자에게는 이해 가능한 메시지를 보여주고, 개발자에게는 원인 분석에 필요한 정보를 남기는 것입니다.사용자 경험 보호
장애 원인 분석
서비스 안정성 향상
사용자의 요청이 잘못되었거나, 프론트엔드에서 잘못된 데이터를 보냈을 때 발생합니다.
예시
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
422 Unprocessable Entity
백엔드 서버 내부에서 문제가 발생했을 때 나타납니다.
예시
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
클라이언트와 서버 사이의 통신 자체가 실패한 경우입니다.
예시
네트워크 에러는 HTTP 응답 자체가 없을 수 있기 때문에, 프론트엔드에서 별도로 처리해야 합니다.
{
"success": false,
"message": "전화번호 형식이 올바르지 않습니다.",
"errorCode": "INVALID_PHONE_NUMBER",
"statusCode": 400
}
| 필드 | 설명 |
|---|---|
success | 요청 성공 여부 |
message | 사용자 또는 프론트엔드에 보여줄 메시지 |
errorCode | 개발자가 구분하기 쉬운 에러 코드 |
statusCode | HTTP 상태 코드 |
{
"success": false,
"message": "필수 입력값이 누락되었습니다.",
"errorCode": "MISSING_REQUIRED_FIELD",
"statusCode": 400
}
{
"message": "PrismaClientKnownRequestError: Unique constraint failed on the fields: (`email`)"
}
{
"success": false,
"message": "이미 사용 중인 이메일입니다.",
"errorCode": "EMAIL_ALREADY_EXISTS",
"statusCode": 409
}
import { BadRequestException } from '@nestjs/common';
throw new BadRequestException('전화번호 형식이 올바르지 않습니다.');
400 Bad Request입니다.import { UnauthorizedException } from '@nestjs/common';
throw new UnauthorizedException('로그인이 필요합니다.');
401 Unauthorized입니다.import { ForbiddenException } from '@nestjs/common';
throw new ForbiddenException('접근 권한이 없습니다.');
403 Forbidden입니다.import { NotFoundException } from '@nestjs/common';
throw new NotFoundException('상품을 찾을 수 없습니다.');
404 Not Found입니다.import { ConflictException } from '@nestjs/common';
throw new ConflictException('이미 등록된 전화번호입니다.');
409 Conflict입니다.try-catch를 감싸는 것은 좋은 방식이 아닙니다.try-catch는 에러를 의미 있게 처리할 수 있을 때 사용하는 것이 좋습니다.try {
await this.smsService.send({
phone: dto.phone,
message: '상담 신청이 완료되었습니다.',
});
} catch (error) {
this.logger.error('문자 발송 실패', error);
throw new BadRequestException('문자 발송에 실패했습니다.');
}
try {
// 중요한 로직
} catch (error) {
return null;
}
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;
}
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('알 수 없는 오류가 발생했습니다.');
}
}
상담 신청에 실패했습니다. 잠시 후 다시 시도해주세요.
[ERROR] POST /api/consults
userId: 123
phone: 010****5678
errorCode: SMS_SEND_FAILED
message: 문자 발송 API timeout
timestamp: 2026-06-05T10:30:00.000Z
| 레벨 | 의미 | 예시 |
|---|---|---|
debug | 개발 중 상세 정보 | 변수값, 분기 확인 |
info | 정상적인 주요 이벤트 | 로그인 성공, 주문 생성 |
warn | 주의가 필요한 상황 | 외부 API 재시도 |
error | 오류 발생 | DB 저장 실패, API 실패 |
fatal | 서비스 중단급 오류 | 서버 실행 실패, DB 전체 장애 |
this.logger.log('상담 신청 등록 완료');
this.logger.warn('문자 발송 API 응답 지연');
this.logger.error('상담 신청 저장 실패', error.stack);
debug 로그를 너무 많이 남기면 로그 비용과 가독성 문제가 생길 수 있습니다.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을 사용하면 어떤 클래스에서 발생한 로그인지 구분하기 쉽습니다.[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
좋은 예시:
phone=010****5678
나쁜 예시:
phone=01012345678
Sentry는 프론트엔드와 백엔드 에러를 수집하는 도구입니다.
오류 메시지, 발생 위치, 브라우저 정보, 사용자 환경 등을 확인할 수 있습니다.
확인 가능한 정보
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('문자 발송에 실패했습니다.');
}
try {
await this.orderService.createOrder(dto);
} catch (error) {
// 아무 처리 없음
}
try {
await this.orderService.createOrder(dto);
} catch (error) {
this.logger.error('주문 생성 실패', error.stack);
throw new InternalServerErrorException('주문 처리 중 오류가 발생했습니다.');
}
1. 주문 생성
2. 주문 상세 생성
3. 재고 차감
4. 결제 이력 저장
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,
},
},
});
});
NestJS + Prisma에서 상담 신청 저장 중 에러가 발생해.
상황:
- POST /api/consults 호출 시 500 발생
- 프론트엔드에서는 "요청 처리 중 오류"만 표시됨
- 백엔드 로그에는 Prisma unique constraint 에러가 나옴
- phone 컬럼에 unique가 걸려 있음
관련 코드:
createConsult(dto)에서 prisma.consult.create 사용 중
원하는 처리:
- 중복 전화번호일 경우 409 Conflict 반환
- 사용자 메시지는 "이미 신청된 전화번호입니다."
- 서버 로그에는 실제 Prisma 에러를 남기고 싶음
NestJS에서 어떻게 처리하면 좋을지 코드와 함께 설명해줘.
success, message, errorCode, statusCode처럼 일정한 구조로 설계하는 것이 좋습니다.BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException, ConflictException 등을 상황에 맞게 사용할 수 있습니다.