320x100
반응형
🎯 핵심 개념: 역할 분담
비유로 이해하기 (학교)
학생 (Route Handler)
↓ 문제 발생!
"선생님! 숙제를 못 찾겠어요!" (throw)
↓
담임 선생님 (Async Handler)
↓ 일단 받아줌
"알았어, 교장 선생님께 보고할게" (catch → next)
↓
교장 선생님 (Error Handler)
↓ 최종 판단
"이건 단순 실수구나" → 학생에게 친절하게 설명
"이건 심각한 문제네" → 학부모(Slack) 연락
📋 규칙: 언제 throw? 언제 try-catch?
✅ Route Handler: throw만!
javascript
// ✅ 좋은 코드 (throw만 사용)
app.post('/login', asyncHandler(async (req, res) => {
const { email, password } = req.body;
// 검증 실패 → throw
if (!email) {
throw new BadRequestError('이메일을 입력하세요');
}
// 사용자 없음 → throw
const user = await User.findOne({ email });
if (!user) {
throw new UnauthorizedError('이메일 또는 비밀번호가 틀렸습니다');
}
// 비밀번호 틀림 → throw
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new UnauthorizedError('이메일 또는 비밀번호가 틀렸습니다');
}
// 성공!
res.json({ token: generateToken(user) });
}));
// ❌ 나쁜 코드 (try-catch 사용)
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email) {
return res.status(400).json({ error: '이메일을 입력하세요' }); // ← 중복!
}
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: '틀렸습니다' }); // ← 중복!
}
// ... 계속 반복
} catch (error) {
res.status(500).json({ error: '서버 오류' }); // ← 이것도 중복!
}
});
→ Route Handler에서는 그냥 throw만 하세요!
✅ Async Handler: 자동 catch
javascript
// asyncHandler가 자동으로 해줌
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next); // ← 여기서 catch!
};
}
// 사용:
app.get('/user/:id', asyncHandler(async (req, res) => {
// 여기서 throw하면 asyncHandler가 자동으로 catch
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('없음'); // ← throw
res.json(user);
}));
// → 자동으로 Error Handler로 전달됨!
→ try-catch 직접 안 써도 됨!
✅ Error Handler: 모든 에러를 받아서 처리
javascript
// 최종 에러 처리
function errorHandler(err, req, res, next) {
// 1. 로그 기록
logger.error(err.message, {
error: err,
request: {
method: req.method,
url: req.url,
},
});
// 2. Slack 알림 (심각한 에러만)
if (err.statusCode >= 500) {
slackNotify(err);
}
// 3. 사용자에게 응답
const statusCode = err.statusCode || 500;
const message = err.isOperational
? err.message // 예상된 에러 → 그대로 보여줌
: '서버 오류가 발생했습니다'; // 예상 못한 에러 → 숨김
res.status(statusCode).json({
error: {
message,
code: err.code,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
},
});
}
app.use(errorHandler);
→ 한 곳에서 모든 에러를 처리!
🎬 실제 흐름 시각화
시나리오: 사용자 조회 API
javascript
// 1. Route Handler (throw만)
app.get('/user/:id', asyncHandler(async (req, res) => {
console.log('1️⃣ Route Handler 시작');
const user = await User.findById(req.params.id);
if (!user) {
console.log('2️⃣ 에러 발생! throw');
throw new NotFoundError('사용자를 찾을 수 없습니다');
}
res.json(user);
}));
// 2. Async Handler (자동 catch)
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch((error) => {
console.log('3️⃣ Async Handler가 잡음');
next(error); // Error Handler로 전달
});
};
}
// 3. Error Logger (기록)
function errorLogger(err, req, res, next) {
console.log('4️⃣ Error Logger 기록');
logger.error(err.message);
next(err); // 다음으로 전달
}
app.use(errorLogger);
// 4. Error Handler (응답)
function errorHandler(err, req, res, next) {
console.log('5️⃣ Error Handler 응답');
res.status(err.statusCode || 500).json({
error: {
message: err.message,
},
});
}
app.use(errorHandler);
실행 결과:
1️⃣ Route Handler 시작
2️⃣ 에러 발생! throw
3️⃣ Async Handler가 잡음
4️⃣ Error Logger 기록
5️⃣ Error Handler 응답
→ 사용자에게:
{
"error": {
"message": "사용자를 찾을 수 없습니다"
}
}
🤔 언제 try-catch를 써야 할까?
규칙: 특별한 처리가 필요할 때만!
javascript
// ✅ Case 1: DB 에러를 변환할 때
app.post('/user', asyncHandler(async (req, res) => {
try {
const user = await User.create(req.body);
res.json(user);
} catch (error) {
// MongoDB 중복 키 에러를 우리 에러로 변환
if (error.code === 11000) {
throw new ConflictError('이미 사용 중인 이메일입니다');
}
// 다른 에러는 그대로 던짐
throw error;
}
}));
// ✅ Case 2: 외부 API 호출 시
app.get('/weather', asyncHandler(async (req, res) => {
try {
const response = await fetch('https://weather-api.com/...');
const data = await response.json();
res.json(data);
} catch (error) {
// 외부 API 에러를 우리 에러로 변환
throw new ServiceUnavailableError('날씨 정보를 가져올 수 없습니다');
}
}));
// ✅ Case 3: 여러 작업을 한 번에 처리
app.post('/order', asyncHandler(async (req, res) => {
try {
// 1. 재고 확인
const product = await Product.findById(req.body.productId);
if (product.stock < 1) {
throw new BadRequestError('재고가 부족합니다');
}
// 2. 주문 생성
const order = await Order.create(req.body);
// 3. 재고 감소
await product.updateStock(-1);
// 4. 결제 처리
await processPayment(order);
res.json(order);
} catch (error) {
// 롤백이 필요한 경우
if (error.name === 'PaymentError') {
await order.cancel(); // 주문 취소
await product.updateStock(+1); // 재고 복구
}
throw error; // 다시 던져서 Error Handler가 처리
}
}));
→ try-catch는 "특별한 처리"가 필요할 때만!
📊 코드 패턴 비교
❌ 안티패턴 (모든 곳에서 try-catch)
javascript
// Route 1
app.get('/user/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: '없음' }); // 중복 1
}
res.json(user);
} catch (error) {
logger.error(error); // 중복 2
res.status(500).json({ error: '서버 오류' }); // 중복 3
}
});
// Route 2
app.post('/user', async (req, res) => {
try {
const user = await User.create(req.body);
res.json(user);
} catch (error) {
logger.error(error); // 중복 2 (또!)
res.status(500).json({ error: '서버 오류' }); // 중복 3 (또!)
}
});
// Route 3, 4, 5... 계속 반복 😰
문제점:
- 😰 코드 중복 (try-catch가 모든 라우트에)
- 😰 일관성 없음 (에러 메시지가 제각각)
- 😰 유지보수 어려움 (수정할 곳이 너무 많음)
✅ 좋은 패턴 (중앙 집중식)
javascript
// Route 1
app.get('/user/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('없음'); // ← throw만!
res.json(user);
}));
// Route 2
app.post('/user', asyncHandler(async (req, res) => {
const user = await User.create(req.body);
res.json(user);
}));
// Route 3, 4, 5... 계속 간단! 😊
// 에러 처리는 한 곳에서!
app.use(errorLogger);
app.use(errorHandler);
장점:
- ✅ 코드 간결 (try-catch 없음)
- ✅ 일관성 있음 (한 곳에서 처리)
- ✅ 유지보수 쉬움 (수정할 곳 하나)
🎯 최종 정리: 당신의 질문 답변
Q: 언제 throw? 언제 try-catch?
A: 간단한 규칙!
javascript
// 1. Route Handler에서는: throw만!
app.get('/api', asyncHandler(async (req, res) => {
if (문제) throw new CustomError('메시지');
res.json(data);
}));
// 2. 특별한 처리 필요할 때만: try-catch
app.post('/api', asyncHandler(async (req, res) => {
try {
const result = await externalAPI();
res.json(result);
} catch (error) {
// 특별히 변환이 필요한 경우만
throw new ServiceError('외부 서비스 오류');
}
}));
// 3. 최종 처리는: Error Handler가 알아서!
app.use(errorLogger);
app.use(errorHandler);
Q: 예상된 에러 vs 예상 못한 에러 구분?
A: Custom Error에 플래그 추가!
javascript
// Custom Error 베이스
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // ← 예상된 에러!
}
}
// 예상된 에러들
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
this.code = 'NOT_FOUND';
}
}
class BadRequestError extends AppError {
constructor(message) {
super(message, 400);
this.code = 'BAD_REQUEST';
}
}
// Error Handler에서 구분
function errorHandler(err, req, res, next) {
// 예상된 에러 (isOperational = true)
if (err.isOperational) {
logger.info('예상된 에러:', err.message); // info 레벨
slackNotify = false; // Slack 안 보냄
message = err.message; // 사용자에게 그대로
}
// 예상 못한 에러 (isOperational = undefined/false)
else {
logger.error('예상 못한 에러!', err); // error 레벨
slackNotify = true; // Slack 보냄!
message = '서버 오류가 발생했습니다'; // 숨김
}
res.status(err.statusCode || 500).json({
error: { message }
});
}
Q: 로그와 Slack은 어디서?
A: Error Logger와 Error Handler에서!
javascript
// 1. Error Logger Middleware
function errorLogger(err, req, res, next) {
// Winston 로그
logger.error(err.message, {
error: {
message: err.message,
stack: err.stack,
code: err.code,
},
request: {
method: req.method,
url: req.url,
ip: req.ip,
},
});
next(err); // 다음으로 전달
}
// 2. Error Handler
function errorHandler(err, req, res, next) {
// Slack 알림 (심각한 것만)
if (err.statusCode >= 500 || !err.isOperational) {
slackNotify({
text: `🔥 서버 에러 발생!`,
error: err.message,
url: req.url,
});
}
// 사용자 응답
res.status(err.statusCode || 500).json({
error: { message: err.message }
});
}
// 순서가 중요!
app.use(errorLogger); // 먼저 로그
app.use(errorHandler); // 그 다음 응답
📝 완벽한 템플릿
javascript
// ========== 1. Custom Errors ==========
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(message = '리소스를 찾을 수 없습니다') {
super(message, 404);
this.code = 'NOT_FOUND';
}
}
class UnauthorizedError extends AppError {
constructor(message = '인증이 필요합니다') {
super(message, 401);
this.code = 'UNAUTHORIZED';
}
}
// ========== 2. Async Handler ==========
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// ========== 3. Routes (throw만!) ==========
app.get('/user/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('사용자 없음');
res.json(user);
}));
app.post('/login', asyncHandler(async (req, res) => {
const { email, password } = req.body;
if (!email) throw new BadRequestError('이메일 필요');
const user = await User.findOne({ email });
if (!user) throw new UnauthorizedError('틀림');
const valid = await bcrypt.compare(password, user.password);
if (!valid) throw new UnauthorizedError('틀림');
res.json({ token: generateToken(user) });
}));
// ========== 4. Error Logger ==========
function errorLogger(err, req, res, next) {
logger.error(err.message, {
error: err,
request: { method: req.method, url: req.url },
});
next(err);
}
// ========== 5. Error Handler ==========
function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || 500;
const message = err.isOperational
? err.message
: '서버 오류';
// Slack 알림 (심각한 것만)
if (statusCode >= 500) {
slackNotify(err);
}
res.status(statusCode).json({
error: {
message,
code: err.code,
...(isDevelopment && { stack: err.stack }),
},
});
}
// ========== 6. 미들웨어 등록 ==========
app.use(errorLogger);
app.use(errorHandler);
320x100
'Tech Notes' 카테고리의 다른 글
| nvm? npm? fvm? 헷갈리지? (1) | 2025.10.22 |
|---|---|
| OCR 이 뭘까? 개념과 어떻게 사용해야 하는지 알아보자! (0) | 2025.10.15 |
| aws의 cdn, cloudfront 에 대해 알아보자 (1) | 2025.10.10 |
| AWS bucket의 권한 관리 (0) | 2025.10.10 |
| The bucket does not allow ACLs... (0) | 2025.10.10 |