핵심 한 줄

스택트레이스가 있어도 top frame으로 직행하면 오진한다. DataAccessResourceFailureException 뒤에 숨은 Caused by: ConnectException — 그 한 줄이 인프라 라우팅의 근거다. 체인을 걸지 않으면 틀린 팀에 패치를 보낸다.

시리즈 — 장애를 '구별 가능'하게 만든 카오스 벤치


1. 배경 — top frame이 매력적인 오답인 이유

스택트레이스는 신뢰의 아이콘이다. 뭔가 분명히 터졌고, 어디서 터졌는지 줄 번호까지 찍혀 있다. 자연스러운 반응은 top frame — 예외 클래스와 첫 번째 at 줄 — 으로 직행하는 것이다.

그리고 이 반응이 옳은 순간이 실제로 있다. 1.스택트레이스가 곧 원인인 유일한 순간이 보여주듯, NullPointerException처럼 예외 자체가 결함인 경우 top frame이 곧 결함 지점이다. 그 세계에서는 “top frame = 원인” 공식이 성립한다.

W4는 그 공식이 함정이 되는 반대쪽 세계다. 스택트레이스는 있다. top frame도 있다. 하지만 직행하면 오진이다.


2. 무엇을 망가뜨렸나 — wrapped 예외 세임

chaos 프로파일에서 dbDown을 arm하면, OwnerController.showOwner(GET /owners/{ownerId}, owner 상세 조회)의 최상단에서 세임이 발화한다. ActiveChaosFaults.DB_DOWN = "dbDown" 시나리오 키가 arm 상태일 때 assertDatabaseReachable()이 인프라 타입의 예외를 던진다.

// ActiveChaosFaults.assertDatabaseReachable() — dbDown 세임
@Override
public void assertDatabaseReachable() {
    if (this.state.isArmed(DB_DOWN)) {
        // 인프라 결함: DB 연결이 거부됐다.
        // root cause는 ConnectException — 앱 코드 결함(NPE)이 아니다.
        throw new DataAccessResourceFailureException(
            "could not open JDBC connection",
            new ConnectException("Connection refused"));
    }
}

핵심은 감싸기(wrapping) 다. DataAccessResourceFailureException이 겉에 보이지만, 그 원인(Caused by)은 ConnectException — 네트워크·DB 연결 실패라는 인프라 문제다. 앱 코드의 결함이 아니고, 고쳐야 할 쪽은 DBA·인프라 팀이다.

seam은 ObjectProvider<ChaosFaults>로 주입되어 chaos 프로파일에서만 살아난다. 평소 빌드에서 showOwner는 이 코드를 전혀 만나지 않는다.


3. 증상 — 그럴듯한 top frame

/owners/1에 요청이 들어오면 HTTP 500이 돌아오고, 로그에 스택트레이스가 찍힌다.

org.springframework.dao.DataAccessResourceFailureException: could not open JDBC connection
    at ...petclinic.chaos.ActiveChaosFaults.assertDatabaseReachable(...)
    at ...petclinic.owner.OwnerController.showOwner(...)
    ...
Caused by: java.net.ConnectException: Connection refused
    ...

top frame을 보면 Spring DAO 예외, 그 아래 프레임은 OwnerController. “DAO 계층에 앱 코드 문제가 있다 → PR을 열자”는 직관이 자연스럽게 온다. W1의 NPE 세계에서라면 맞는 판단이다. 하지만 여기서 PR을 열면 오진이다. Caused by: 한 줄을 읽지 않은 것이다.


4. 판별 신호 — 체인을 걸어야 나오는 세 가지 규칙

top frame을 넘어서야 하는 패턴에는 세 가지가 있다.

패턴top frame이 가리키는 곳실제 근본원인어디를 봐야 하나
Wrapped exceptionDataAccessResourceFailureException 등 래퍼Caused by: 최심부 (예: ConnectException)Caused by: 체인 끝까지 추적
Async 워커 프레임스레드풀 내부 (java.util.concurrent.*)실제 태스크 코드의 throw워커 스레드의 첫 번째 프레임
Framework-onlySpring·Tomcat 프레임만 나열, 앱 프레임 없음앱 코드 결함 아님 — 설정·배포 문제앱 프레임 존재 여부 먼저 확인

W4의 경우는 첫 번째 패턴이다. Caused by: 최심부의 java.net.ConnectException: Connection refused — 이 한 줄이 “인프라 라우팅”의 근거다. Grafana의 InfraDbDown 알림은 Loki에서 이 시그니처를 키로 쓴다.

{service="petclinic"} |= "DataAccessResourceFailureException"

그리고 이 시그니처가 뜰 때 Class A 앱 localize 알림 ClassAOwnerSearchNPE침묵해야 한다 — NullPointerException이 없으니까. 두 알림의 조합이 discriminator다.

체인 규칙

Caused by: 최심부를 확인하지 않은 진단은 미완성이다. top frame의 예외 클래스가 아니라 체인 끝의 근본원인 클래스가 라우팅을 결정한다. 이 한 줄을 건너뛰면 라우팅이 틀린다.


5. 자동 진단에서의 의미 — 잘못 localize하면 잘못된 패치

이 함정이 자동화에서 치명적인 이유가 있다. 트리아지 스킬이 “5xx + 스택트레이스 → 앱 코드 localize → PR 열기”로 반응하면 어떻게 되나.

  1. top frame DataAccessResourceFailureException @ OwnerController → 앱 코드 결함으로 localize
  2. showOwner 또는 DAO 레이어에 PR 오픈
  3. 실제 원인(DB 연결 끊김)은 그대로 → 장애 계속, 틀린 팀이 시간을 씀

scripts/smoke/misdiagnosis-to-slack.sh가 이 A13 discriminator를 검증한다. dbDown arm 후 /owners/1을 호출하면:

  • InfraDbDowncategory: infrastructure로 발화 — triage 어노테이션: “코드 PR 아님, DB·인프라 팀 라우팅”
  • ClassAOwnerSearchNPE는 침묵 — NPE 시그니처가 없으니 앱 localize 알림이 울리지 않는다

스모크가 두 알림의 조합을 어서션하는 이유가 여기 있다. InfraDbDown이 켜지는 것만으로는 부족하다 — ClassAOwnerSearchNPE가 꺼져 있어야 discriminator가 성립한다.


6. 교훈

top frame은 표면이지 원인이 아니다

스택트레이스가 있어도 top frame으로 직행하면 오진한다. W4의 답은 단 한 줄: Caused by: ConnectException: Connection refused. 그 한 줄을 읽지 않으면 올바른 팀에 라우팅할 수 없고, 잘못 localize한 스킬은 엉뚱한 PR을 연다. Caused by: 최심부·async 워커의 앱 프레임·framework-only 여부 — 세 규칙이 근본원인을 가른다. 이 편의 역할은 1.스택트레이스가 곧 원인인 유일한 순간과 정면으로 맞서는 것: top frame = 원인인 유일한 순간이 있다면, top frame ≠ 원인인 함정도 그만큼 흔하다.


관련 글