# 조용한 고장

> 이 프로젝트에서 잡은 버그가 거의 전부 성공을 리턴하면서 아무것도 안 했습니다.

- 수치: 0 에러
- 사람이 보는 화면: https://aitryed.com/ko/built/loki/silent-failures/
- 반대 언어: https://aitryed.com/en/built/loki/silent-failures/index.md
- 이 사이트 안내(AI용): https://aitryed.com/ko/llms.txt

---

**이 프로젝트에서 잡은 버그를 모아 놓고 보니, 거의 전부가 같은 모양이었습니다 — «성공했다고 답하면서 아무것도 안 함».**

> **[도판]** 예외를 던지는 고장은 쉽다. 이 프로그램의 고장은 성공을 리턴하면서 아무것도 안 했다.
>
> 버그에서 두 갈래가 나간다. 보통의 고장은 예외를 던지고 스택트레이스를 남겨 눈에 걸린다. 이 프로젝트의 고장은 성공을 리턴하고 빈 결과를 주며 로그가 정상이라 몇 주 동안 발견되지 않는다.

## 두 종류의 고장

프로그램이 고장 나는 방식은 크게 둘입니다.

| 종류 | 무슨 일이 생기나 | 고치기 |
|---|---|---|
| **시끄러운 고장** | 빨간 오류가 뜨고 멈춤 | **쉬움** — 어디서 멈췄는지 알려 줌 |
| **조용한 고장** | **「됐습니다」라고 답하는데 실제로는 아무것도 안 함** | **어려움** |

이 프로젝트의 버그는 **거의 전부 아래쪽**이었습니다.

## 왜 이게 어려운가

시끄러운 고장은 **찾을 필요가 없습니다.** 프로그램이 먼저 알려 줍니다.

조용한 고장은 **아무도 안 알려 줍니다.**

- 오류 기록: **깨끗함**
- 화면: **「완료」**
- 실제: **아무 일도 안 일어남**

그래서 <strong>며칠 뒤에 「어? 그거 안 됐네」</strong>로 발견됩니다. 그때는 언제부터 안 됐는지도 모릅니다.

## 가져다 쓰던 분이 보내 준 세 건도 전부 같은 모양이었습니다

이 물건을 가져다 쓰던 분이 문제 셋을 **원인 진단과 고치는 방법까지 붙여** 보내 주셨습니다.

**셋 다 조용한 고장**이었습니다.

혼자 만든 사람이 자기 버그의 경향을 아는 건 어렵습니다. **남이 같은 모양을 셋 들고 오니 그게 우연이 아니라는 게 확실해졌습니다.**

## 배운 것 하나 — 「글자가 아니라 구조를 읽어라」

명령 하나가 안 먹던 일이 있었습니다.

원인은 **답을 「글자」로 읽고 있었던 것**이었습니다. 「이 단어가 들어 있으면 성공」 같은 식입니다.

그런데 **말투가 조금만 달라지면 그 단어가 안 나옵니다.** 그러면 성공했는데 실패로 읽거나, 반대가 됩니다.

고쳐서 **구조를 읽게** 했습니다 — 글로 된 답이 아니라 **정해진 형태의 데이터**를 보게요.

그리고 고치고 나니 **덤으로 기능이 하나 늘었습니다.** 구조를 읽게 되니 **거기 들어 있던 다른 정보도 쓸 수 있게** 됐습니다.

→ **글자로 읽던 걸 구조로 바꾸면, 고치는 김에 쓸 수 있는 게 늘어납니다.**

## 그리고 제일 아찔했던 것 — 「살아 있나 확인하는 코드가 죽일 뻔했습니다」

감시 기능을 만들 때 있었던 일입니다.

작업하는 프로그램이 **아직 살아 있는지 확인**하는 코드가 필요했습니다. 널리 쓰이는 방법이 있어서 그대로 썼습니다.

그 방법의 원리는 이렇습니다 — **「아무것도 안 보내는 신호」를 보내고, 반응이 있으면 살아 있는 것.** 원래 쓰던 환경에서는 이게 정말 아무것도 안 합니다. **존재 여부만 묻습니다.**

그런데 **제가 쓰는 환경(윈도우)에서는 그 동작이 다릅니다.**

**살아 있는지 물어보는 코드가, 물어보면서 죽일 수 있는 상태**였습니다.

이게 무서운 종류의 버그입니다. **확인하는 행위 자체가 대상을 바꿉니다.** 그러면 「확인해 보니 죽어 있더라」가 나오는데, **확인 안 했으면 살아 있었을 수도** 있습니다.

→ 배운 것: **널리 쓰이는 관용구라도, 다른 환경에서는 다른 일을 할 수 있습니다.** 「다들 이렇게 쓴다」는 근거가 못 됩니다 — **어느 환경에서 다들 그러는지**를 봐야 합니다.

## 그래서 이 프로젝트가 남긴 한 줄

> **오류가 0건이라는 건 깨끗하다는 뜻이 아니라, 아직 아무도 안 봤다는 뜻일 수 있습니다.**

## 여기부터는 자세한 기록입니다

기능을 하나씩 붙이는 동안 잡은 버그를 모아 놓고 보니 **거의 전부가 같은 모양**이었다.

## 성공을 리턴하면서 아무것도 안 하기

포크해서 쓰던 사람이 이슈 셋을 진단과 패치까지 붙여 보내줬는데, 셋 다 이 모양이었다.

**소수점 자릿수** — 슬랙 히스토리 API는 시각 인자의 소수점이 6자리를 넘으면 에러가 아니라 **`ok: true` + 빈 목록**을 준다. 파이썬 `time.time()`은 보통 7자리다. 그래서 채널 맥락 읽기가 몇 주 동안 빈 히스토리로 돌았다. 전수 확인해 보니 실수를 그대로 넘기던 곳은 여기 **하나뿐**이었다 — 그 하나가 기능 하나를 통째로 무력화했다.

**막는 규칙이 허용을 이긴다** — [손님 울타리](/ko/built/loki/permissions/index.md)가 다른 드라이브를 통째로 막는데, 작업 폴더가 그 드라이브에 있으면 **허용 목록이 전부 죽는다.** 나오는 메시지는 "권한 없음" 한 줄뿐이라 울타리가 아니라 목록이 고장 난 걸로 읽힌다.

**별칭이 저장되고 영영 안 뜬다** — 이름 충돌 검사가 출하 명령 이름을 하드코딩하고 있어서 플러그인 명령이 안 보였다. 저장은 성공하고, 부르면 아무 일도 안 일어난다.

## 문자열이 아니라 구조를 읽는다

명령 하나가 안 먹던 이야기인데, 고치고 나니 기능이 하나 늘었다.

사람이 슬랙 입력창에 `!usage`라고 치면 잘 되는데, **다른 기계의 세션이 커넥터로 대신 보내면** 명령이 전부 빗나갔다. 슬랙이 본문 끝에 출처 표기를 이어붙이기 때문이다. 그것도 줄바꿈 없이 같은 줄에.

증상이 두 갈래로 갈렸다. 앵커가 걸린 명령은 아예 매치가 안 돼 **두뇌로 새어 들어갔고**(그래서 "그런 명령 없습니다"라고 답했다), 느슨한 명령은 매치는 되고 **인자가 오염**됐다.

처음엔 그 문구를 잘라내려 했다. **틀린 접근이다 — 출처 표기는 워크스페이스 언어로 번역된다.** 한국어, 영어, 일본어가 전부 다르게 온다. 단어로 맞추는 순간 다른 나라 워크스페이스에서 깨진다.

답은 문자열이 아니라 **슬랙이 준 구조**에 있었다. 출처 표기는 본문에 섞인 게 아니라 맨 뒤에 붙은 별도 블록이고, 사람이 쓴 글은 따로 남아 있다. 그리고 **사람은 슬랙 입력창으로 그 블록을 만들 수 없다** — 오탐이 원리적으로 없다는 뜻이다.

고치고 나서 생긴 기능이 이것이다. 다른 기계의 세션이 **내 계정으로** 슬랙에 글을 올리면 이 PC의 Loki가 그걸 실행하고 스레드로 답한다. 봇이 아니라 사람 명의라 앱을 하나 더 만들 필요가 없다.

## 생존 확인이 대상을 죽일 수 있다

감시 기능을 만들면서, 워커가 살아 있는지 확인하는 코드를 `os.kill(pid, 0)`으로 썼다. 유닉스에서는 관용구다 — 시그널 0은 아무것도 보내지 않고 존재 여부만 묻는다.

**윈도우 파이썬은 이걸 프로세스 종료로 구현한다.** 시그널 0을 포함해서. 생존 확인이 워커를 죽인다.

읽기 전용 조회로 바꿨다. 같은 계열의 함정이 하나 더 있었는데, 재시작이 5분 주기 감시자와 경합해 **워커가 셋 붙은 적**이 있다. 같은 토큰에 소켓이 셋이면 이벤트가 갈린다. 그런데 헬스체크는 기록된 pid 하나만 보므로 **정상이라고 보고한다.**

지금은 개수가 아니라 **프로세스 계보**로 판정한다. 개수로 세면 안 되는 이유가 있다 — 실행 환경상 부모와 자식 두 프로세스가 뜨는 게 정상이라, "둘이면 이상"도 "하나여야 정상"도 틀리다.

## 자동 감지 하나를 접었다

"대화가 헛돌고 있다"를 자동으로 감지해 [넛지](/ko/built/loki/commands/index.md)에 물리려고 두 번 시도했고 두 번 다 실패했다. 어휘 신규성을 재는 방식이었다.

기각 근거는 튜닝 실패가 아니라 **측정 그 자체**였다. 실제로 진행되는 대화의 신규성 구간과, 말만 바꿔 같은 자리를 도는 대화의 구간이 **겹쳤다.** 어떤 임계값도 둘을 못 가른다.

이유는 구조적이다. "같은 주제로 새 질문"과 "같은 교착을 다시 서술"은 **쓰는 단어가 같다.** 임계값을 조정해 예시 세 개를 통과시키는 건 오버피팅이지 증거가 아니다.

대신 **아무것도 이해할 필요 없는 것들**로 갔다 — 시간당 요청 상한, 비용 상한, 대화를 명시적으로 끊는 명령. 비용은 이미 응답 안에 들어오고 있었고 버리는 중이었다.

## 테스트가 라이브 상태를 편집하고 있었다

같은 종류로 두 번 데었다. 각 모듈이 **불러올 때 상태 파일 경로를 잡는 구조**라, 테스트 준비 코드가 이름으로 하나씩 막지 않으면 라이브 파일이 그대로 편집된다.

실제 사고는 이랬다. 어댑터 테스트가 채널 메시지를 흘려 보냈고, 그게 **라이브 상태 파일에 기록**되면서 방금 만든 경고 하나가 프로덕션에서 조용히 꺼졌다. **테스트는 전부 통과했다.**

지금은 상태 경로 열일곱 개를 통째로 돌려 놓는다. 그리고 **새 상태 파일을 만들면 그 목록에 등재하는 게 규칙**이다 — 이런 건 다음 사람이 잊는 게 아니라 만든 사람이 잊는다.
