내가 만든 방어 유틸 함수 5종 세트
C# 기본 제공 Debug.Assert()만으로는 부족했다.
운영 중 발생할 수 있는 가정 위반, 내부 버그, 즉시 대응이 필요한 크리티컬 상황을
한 체계로 다루고 싶었다.
그래서 아래 다섯 가지 유틸을 만들었다.
global using static POCU.Core.Assertion.Check;
이렇게 전역으로 불러와서 전부 대문자로 사용한다.
로직 코드 중간에서도 눈에 확 띄어서 "이건 방어 코드다"라는 걸 한눈에 알 수 있다.
🧱 공통 규칙
모든 함수의 첫 번째 인자는 반드시 bool 표현식 이다.
CHECK911_THROW(user != null, "User should not be null");
true→ 아무 일도 일어나지 않는다.false→ 각 함수에 맞는 방식으로 로그·알림·예외가 발생한다.
즉, if (!condition) { ... }을 매번 쓰지 않아도 된다.
표현식 하나로 "이게 깨지면 뭔가 잘못된 거다"를 명확히 선언할 수 있다.
📊 함수 요약
| 함수 | 목적 | 행동 요약 | 운영팀 대응 | 예외 |
|---|---|---|---|---|
| VERIFY | "그럴 리 없겠지" 가정 관찰 | 로그만 남기고 계속 실행 | 덜 바쁠 때 확인 | ❌ |
| DBG_CHECK | 디버그 전용 어서션 | Debug 빌드에서만 중단 | ❌ | ❌ |
| CHECK912 | 내부 문제 감지 (비긴급) | 알림 + 그래프 집계 | 1~2일 내 확인/수정 | ❌ |
| CHECK911 | 즉시 대응 필요 | 알림 + 그래프 집계 | 즉시 확인/수정 | ❌ |
| CHECK911_THROW | 트랜잭션 보호용 | 알림 + 그래프 집계 + 예외 발생 | 즉시 확인 | ✅ |
🔔 모든 CHECK 계열은 회사 메신저로 노티가 오고,
발생 횟수는 자동 집계되어 그래프로 시각화된다.
🧩 VERIFY — 장기 가정 검증용 센서
VERIFY는 단순한 로그가 아니다.
"현재는 절대 일어나지 않을 거라 생각하는 상황"을 장기적으로 관찰하기 위한 센서다.
VERIFY(order.TotalPrice >= 0, "Order total should never be negative");
- 지금은 이게 절대 깨질 리 없다고 생각한다.
- 하지만 혹시 모른다. 1년, 2년 동안 한 번이라도 깨질 수도 있다.
- 그때 로그가 오면, "우리의 가정이 잘못됐구나"를 알 수 있다.
- 반대로 몇 년간 한 번도 안 깨지면 그 라인은 제거해도 된다.
이건 테스트로 잡을 수 없는 실운영 환경의 가정 검증이다.
운영 중 일시적인 엣지케이스나 외부 시스템의 불규칙성을 감시하기에 적합하다.
💡 또 하나의 꼼수
가끔 이런 경우도 있다.
오래된 코드인데, "절대 일어나지 않을 조건"이라 삭제하고 싶지만 확신이 안 설 때가 있다.
이럴 땐 다음처럼 처리한다:
// TODO(delete): delete after 2028-12-31
VERIFY(someOldFlag == false, "Old flag still being used?");
- 일단 VERIFY를 걸어둔다.
- 운영 중 1년 정도 지켜본다.
- 한 번도 깨지지 않으면, 해당 코드를 안전하게 삭제한다.
이 패턴은 "실운영 환경에서 살아있는 테스트" 같은 역할을 한다. 수많은 라이브 유저가 테스트해주는 자동 테스트 시스템 완성! 최고다, 헤헤 😎 실제로 코드 정리 주기를 시스템적으로 관리하기가 훨씬 쉬워진다.
🧪 DBG_CHECK — 디버그 전용 강력 어서션
DBG_CHECK(buffer.Length == expectedSize, "Unexpected buffer size");
- Debug 빌드에서만 실행된다.
- Release 빌드에서는 완전히 제거되어 성능 부담이 없다.
- "개발 중엔 절대 이 조건이 깨지면 안 된다"를 강하게 보장할 때 쓴다.
✅ 테스트 중 빠르게 터트려야 하는 조건에는
DBG_CHECK.
✅ 운영 중 관찰해야 하는 조건에는VERIFY.
🔍 CHECK912 — 내부 문제지만 긴급 아님
CHECK912(userCache.Count > 0, "User cache is unexpectedly empty");
- 내부 버그 가능성이 높지만 당장 서비스에 영향은 없다.
- 메신저 알림이 오고, 그래프에 집계된다.
- 운영팀은 "하루나 이틀 뒤에 봐도 되는 이슈"로 분류한다.
예시:
- 캐시 불일치
- 일시적인 네트워크 재시도 성공
- 데이터 통계의 경계값 초과
🚨 CHECK911 — 즉시 대응이 필요한 크리티컬
CHECK911(paymentResponse.IsValid, "Payment gateway returned invalid data");
- 데이터 손실, 보안, 고객 영향 같은 즉시 대응이 필요한 문제다.
- 메신저로 바로 알림이 전송되고,
그래프 집계 및 운영 훅(예: 세이프모드 전환)이 트리거된다. - 하지만 예외는 던지지 않는다.
→ 워커나 파이프라인을 멈추지 않고 운영팀이 실시간 대응할 수 있게 한다.
💣 CHECK911_THROW — 트랜잭션 보호용
CHECK911_THROW(invoice != null, "Invoice must exist before commit");
CHECK911과 동일한 알림을 보내지만, 즉시 예외를 던져 트랜잭션을 중단한다.- 잘못된 상태가 DB나 외부 API로 퍼지는 걸 방지하기 위한 안전장치다.
- 보통 다음과 같은 곳에서 사용된다:
예시:
CHECK911_THROW(user != null, "User not found");
CHECK911_THROW(balance >= 0, "Negative balance detected");
트랜잭션 내부, 커밋 직전, 외부 시스템 호출 직후 같은
"사이드이펙트가 퍼지기 전에 막아야 하는 시점"에 쓴다.
⚙️ 내부 동작 개요
모든 함수는 내부적으로 같은 베이스 로직을 공유한다:
bool expression평가false면 → 공통 로거 호출- 로거는 다음을 수행
- 로그 작성 (파일 / 콘솔 / Sentry 등)
- 메신저로 노티 전송
- 메트릭 집계 (그래프용)
*_THROW계열은 마지막에throw수행
즉, 같은 알림 인프라 위에서 심각도만 다르게 분류된다.
🧩 마무리
이 다섯 가지 함수는 결국 한 가지 목적을 가진다.
"로직과 방어선을 분리하고, 발생 시점에 따라 적절히 대응하기."
VERIFY: 장기 가정 검증 센서DBG_CHECK: 디버그 전용 강한 어서션CHECK912: 내부 문제 감지 (비긴급)CHECK911: 즉시 대응 필요 (운영 크리티컬)CHECK911_THROW: 즉시 대응 + 트랜잭션 중단
이제 로직 코드 한가운데서 이렇게 쓰면 된다:
CHECK911_THROW(totalPrice >= 0, "Negative total price detected");
그 한 줄이면 끝이다.
읽는 사람은 "이건 단순 로직이 아니라, 안전선을 세운 부분"임을 바로 알 수 있다.
제대로 대우받는 개발자 | 부족한 컴공지식 배우기 | MIT급 컴공인강
최저임금으로 고통받는 일회성 프로그래머는 그만! POCU 아카데미가 올해 연봉협상을 책임지겠습니다!