조용한 실패: 피해야 할 주니어 함정
Source: Dev.to

당신도 겪어봤을 겁니다. 사용자가 Refund 버튼을 클릭합니다. 스피너가 돌아갑니다. 스피너가 멈춥니다.
아무 일도 일어나지 않죠.
- 화면에 오류 메시지가 표시되지 않음.
- 콘솔에 빨간 텍스트가 없음.
- 모니터링 대시보드에 알림이 없음.
사용자는 버튼을 다섯 번 더 클릭합니다. 여전히 아무 일도 없습니다.
코드에 파고들어 버그를 찾으려 합니다. 세 단계의 추상화를 거쳐 로직을 추적한 끝에 발견합니다:
if (!success) return false;
이것이 Silent Failure—범죄 현장의 증거를 파괴하기 때문에 디버깅하기 가장 어려운 버그 유형입니다.
낙관주의 vs. 편집증
- 주니어는 종종 낙관적이다. 그들은 데이터베이스가 항상 연결되고, 주문이 항상 존재하며, API가 200 ms 안에 항상 응답하는 행복한 경로를 위해 코드를 작성한다. 앱이 충돌할까 두려워
false나null을 반환한다. - 전문가는 편집증적이어야 한다. 그들은 네트워크가 혼잡하고, 데이터베이스가 고갈되었으며, 입력이 악의적이라고 가정한다.
오늘은 여러분에게 The Integrity Check를 가르쳐 드리겠다 – 취약하고 낙관적인 로직을 견고하고 방어적인 엔지니어링으로 전환하는 패턴이다.
주니어 함정: 낙관적인 반환
환불을 처리하도록 설계된 함수를 살펴보겠습니다. “낙관적인 주니어”는 주문이 존재하고 상태가 유효하다고 가정하고 코드를 작성합니다. 문제가 있으면 false를 반환해 애플리케이션을 “살려” 둡니다.
// Before: The Optimistic Junior
// The Problem: Silent failures and no validation.
async function processRefund(orderId) {
const order = await db.getOrder(orderId);
// If the order doesn't exist... just return false?
// The caller has no idea WHY it failed. Was it the DB? The ID?
if (!order) {
return false;
}
// Business Logic mixed with control flow
if (order.status === 'completed') {
await bankApi.refund(order.amount);
return true;
}
// If status is 'pending', we fail silently again.
return false;
}
왜 이것이 “수면 부족 시니어 테스트”에 실패하는가
새벽 3시라고 가정해 보세요. 지원 티켓에 “환불이 작동하지 않는다.” 라고 적혀 있습니다. 로그를 확인해 보니 오류가 없습니다. 코드를 보면 단순히 false를 반환하고 있습니다.
- 환불이 실패한 이유가 ID가 잘못됐기 때문인가?
- 이미 환불된 주문이었기 때문인가?
- 은행 API가 다운됐기 때문인가?
추측해야만 합니다. 이는 받아들일 수 없습니다.
프로 무브: 무결성 검사
이를 해결하려면 Paranoia와 Loudness를 결합해야 합니다.
- Paranoia: 입력이나 상태를 신뢰하지 않습니다. 즉시 검증합니다.
- Loudness: 함수가 이름이 말하는 일을 수행할 수 없으면 소리쳐야 합니다(오류를 발생시킴).
Guard Clauses와 Explicit Errors를 사용해 리팩터링하겠습니다.
변환
// After: The Professional Junior
// The Fix: Loud failures, defensive coding, and context.
async function processRefund(orderId) {
const order = await db.getOrder(orderId);
// 1. Guard Clause – stop execution if the data is missing.
if (!order) {
throw new Error(`Refund failed: Order ${orderId} not found.`);
}
// 2. State Validation – ensure the order is in a refundable state.
if (order.status !== 'completed') {
throw new Error(
`Refund failed: Order is ${order.status}, not completed.`
);
}
// 3. Handle External Chaos – wrap third‑party calls to add context.
try {
await bankApi.refund(order.amount);
} catch (error) {
// Add context so we know *where* it failed.
throw new Error(`Bank Gateway Error: ${error.message}`);
}
}
왜 이것이 더 나은가
1. 가드 절 패턴
if 문을 뒤집습니다: if (success) { … } 안에 로직을 중첩하는 대신, 먼저 실패를 확인하고 즉시 종료합니다. 이렇게 하면 코드가 평평해지고 들여쓰기가 사라져 시각적으로 스캔하기 쉬워집니다 (Visual Geography).
- Junior: 정상 흐름(
if (exists))을 확인합니다. - Pro: 비정상 흐름(
if (!exists))을 확인합니다.
2. 라우드니스 법칙
After 예시에서는 false를 반환하지 않습니다.
- ID가 잘못된 경우 →
Refund failed: Order 123 not found. - 주문이 보류 중인 경우 →
Refund failed: Order is pending, not completed.
오류 메시지는 버그를 어떻게 고쳐야 하는지 정확히 알려줍니다. 코드를 디버깅할 필요 없이 로그를 읽기만 하면 됩니다.
3. 컨텍스트 래퍼
서드파티 API는 보통 500 Server Error와 같은 일반적인 오류를 발생시킵니다. 이를 그대로 전파하면 오류가 User Service, Bank, Emailer 중 어디서 발생했는지 알 수 없습니다.
bankApi 호출을 try/catch 로 감싸면 컨텍스트를 앞에 붙일 수 있습니다: Bank Gateway Error: …. 이제 어떤 통합에서 문제가 발생했는지 정확히 알 수 있습니다.
요약
다음에 문제가 발생했을 때 return null이나 return false를 입력하고 싶어질 때, 멈추세요. 스스로에게 물어보세요:
“만약 이 일이 새벽 3시에 발생한다면, 왜 일어났는지 알 수 있을까?”
답이 아니오라면, 오류를 발생시키세요. 크게 그리고 일찍 불평하는 코드는 유지보수가 쉽습니다.
편집증을 가지세요. 크게 외치세요. 전문가답게 행동하세요.
조용히 실패하는 코드를 작성하지 마세요.
이 글은 제 핸드북, “The Professional Junior: Writing Code that Matters.” 의 발췌입니다. 400페이지 분량의 교과서가 아니라, 쓰여지지 않은 엔지니어링 규칙에 대한 전술적인 현장 가이드입니다.
