핵심 한 줄

ownerSearchCorruption이 arming되면 owner 검색은 HTTP 200으로 빈 결과를 돌려보낸다 — 예외도, 5xx도, ERROR 로그도 없다. 표준 관측 신호는 전부 blind이고, 오직 CorrectnessOraclepetclinic_correctness_violations_total만 이 장애를 본다. 오라클 없이는 침묵의 오염을 잡을 수 없다.

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


1. 배경 — 메트릭이 다 정상인 장애

운영에서 p99 latency↑ 알림이 뜨면 적어도 뭔가 잘못됐다는 신호는 있다. 5xx가 터지면 스택트레이스가 있다. GC 폭주는 pause rate가 드러낸다. 이 시리즈는 각 원인마다 다른 원인은 못 내는 단 하나의 신호를 찾는 작업이었다(0.서문).

W3의 주제는 다르다 — 신호가 없는 장애다. 대시보드가 녹색이고, 로그가 깨끗하고, p99도 정상이고, 5xx도 0이다. 그런데 사용자가 돌려받은 답이 틀렸다. 이것이 침묵의 오염(silent corruption)이다. 이 장애가 가장 위험한 이유는 단 하나 — 아무도 모른다는 것.


2. 무엇을 망가뜨렸나 — 침묵의 조작 seam

chaos 프로파일에서 ownerSearchCorruption을 arm하면, OwnerController.processFindForm이 owner 검색어를 sentinel 값으로 교체한다. ChaosFaults.corruptSearchTerm()이 그 seam이다.

// ActiveChaosFaults.java — chaos 프로파일에서만 살아나는 corruption 구현
@Override
public String corruptSearchTerm(String lastName) {
    if (this.state.isArmed(OWNER_SEARCH_CORRUPTION)) {
        // sentinel은 어떤 owner와도 매칭되지 않는다 — no exception, no 5xx
        return NO_MATCH_SENTINEL;   // "__chaos_nomatch__"
    }
    return lastName;
}

OwnerController는 이미 chaosFaults.normalizeLastName()으로 검색어를 통과시키는 구조였다. corruptSearchTerm()은 그 바로 다음 단계 — 오염이 터져도 요청 흐름 자체는 그대로다. DB 쿼리는 정상 실행되고, 결과만 빈 페이지로 돌아온다. seam은 @Profile("chaos")에서만 존재하므로 기본 빌드에는 영향이 없다.


3. 증상 — 없음, 그것이 문제다

POST /chaos/ownerSearchCorruption/arm 직후 실제로 검색을 날려보면:

  • HTTP 응답: 200 OK
  • /owners 5xx 카운터: 0 — Prometheus는 아무것도 보지 못한다
  • 애플리케이션 로그: ERROR 없음 — Loki도 깨끗하다
  • p99 latency: 정상 — 쿼리는 빠르다, 결과가 없을 뿐

결과가 빈 화면이다. 하지만 “해당 owner 없음”으로도 해석되니, 사용자가 직접 다른 이름으로 검색해보기 전까지 이상하다고 보고하지 않는다.

이게 이 장애가 최악인 이유다. 알림 → 대기, 5xx 급증 → 대응, 스택트레이스 → 파악 — 이 흐름 자체가 존재하지 않는다. 모니터링이 정상을 돌려주는 사이, 장애가 조용히 퍼진다. p99가 튀거나(2.p99가 튀는데 아무도 안 던졌다) 스레드가 BLOCKED(10.다 멈췄는데 데드락은 아니다)되는 다른 시나리오와 이 점이 근본적으로 다르다.


4. 판별 신호 — 기대값과 비교해야만 드러난다

표준 관측 신호(latency, error rate, throughput, log)는 전부 blind다. 유일한 경로는 정답을 알고 있는 probeCorrectnessOracle이다. chaos 프로파일에서만 살아나는 @Scheduled 빈으로, chaos.oracle.interval-ms(기본 5000ms)마다 알려진 seed 데이터(“Davis”)를 쿼리해 결과를 검증한다.

// CorrectnessOracle.java — 오라클은 seam을 통과한다, 컨트롤러와 동일한 경로로
public void runOwnerSearchCheck() {
    String term = this.chaosFaults.corruptSearchTerm(KNOWN_LAST_NAME); // "Davis"
    Page<Owner> results = this.owners.findByLastNameStartingWith(term, PageRequest.of(0, 20));
    boolean found = false;
    for (Owner each : results) {
        if (KNOWN_LAST_NAME.equals(each.getLastName())) { found = true; break; }
    }
    if (!found) {
        this.violations.increment(); // petclinic_correctness_violations_total{check="ownerSearch"}
    }
}

오라클은 컨트롤러와 동일한 seam을 통과한다. corruptSearchTerm()이 sentinel을 돌려주면 DB 결과에 “Davis”가 없고, 카운터가 오른다. 그 카운터에 SilentCorruption 알림이 걸려 있다.

신호armed 상태
http_server_requests_seconds_count (5xx 증가)0 — blind
ERROR / WARN 로그없음 — blind
p99 latency정상 — blind
petclinic_correctness_violations_total> 0 — 유일한 신호

5. 왜 자동 패치 금지인가 — 정답은 스펙에 있다

이 장애의 판별 신호는 코드나 로그에서 유도할 수 없다. “Davis가 있어야 한다”는 사실은 seed 데이터 스펙이고, CorrectnessOracle은 그 스펙을 코드로 인코딩한 것이다.

스택트레이스가 있으면 top frame이 수정 지점을 가리킨다(Class A). 에러율이 솟으면 최근 배포가 용의자고 코드 변경을 되돌릴 수 있다(Class B). 하지만 침묵의 오염은 신호 자체가 없었다 — 무엇이 틀렸는지, 얼마나 틀렸는지, 언제부터 틀렸는지를 오라클 바깥에서는 알 수 없다.

오케스트레이터가 petclinic_correctness_violations_total을 보고 corruptSearchTerm seam을 롤백할 수는 있다. 하지만 “어떤 결과가 올바른 결과인가”는 비즈니스 불변식이다 — 그 판단은 스펙을 아는 사람이 해야 한다. 이것이 Class C, 절대 자동 패치 금지. 오케스트레이터가 할 수 있는 최선은 알림을 올바른 사람에게 전달하고, 판단 맥락을 첨부하는 것이다.


6. 교훈

오라클 없이는 침묵의 오염을 잡을 수 없다

표준 관측 신호(latency, error rate, log)는 결과의 정확성에 대해 아무것도 모른다. 침묵의 오염은 기대값을 알고 있는 probe만이 볼 수 있다. CorrectnessOracle처럼 — seed 데이터에 기반한 known-answer check를 주기적으로 돌리고, 불일치를 metric으로 내보내는 것. 오라클은 스펙의 코드화다. 스펙 없이는 오라클도, 판별 신호도, 이 장애 클래스 자체도 존재하지 않는다.


관련 글