가끔 버그는 귀신 이야기처럼 시작된다.

분명 화면에는 아무것도 없다.
연결된 항목도 없어 보인다.
그래서 사용자는 자연스럽게 삭제 버튼을 누른다.

그런데 제품이 말한다.

“아직 연결된 것이 있습니다.”

무섭다.
화면에는 없는데, 어딘가에는 있다.

이런 종류의 버그는 개발자를 은근히 괴롭힌다. UI만 보면 말이 안 되고, DB만 보면 너무 내부적이고, 로그만 보면 Stack Trace가 눈앞에서 행진한다. 그래서 이번에는 탐정 놀이를 조금 더 체계적으로 해보기로 했다.

도구는 ReproKit.
역할은 버그 탐정단.

사건: 빈 방인데 경비원이 막는다

상황을 제품 밖 이야기로 바꿔보자.

회사에 “허용 출입 구역”이 있다. 어떤 회의실에 들어갈 수 있는 사람과 장비를 묶어두는 규칙이라고 생각하면 된다.

어느 날 관리자가 그 구역을 지우려고 한다. 그런데 경비원이 막는다.

“아직 이 구역에 연결된 장비가 있습니다.”

관리자가 명단을 본다.
없다.
방을 둘러본다.
없다.
그런데 경비원 장부에는 아직 한 줄이 남아 있다.

알고 보니 그 장비는 이미 폐기됐다. 문제는 “장비 폐기 기록”은 있는데, “구역과 장비의 연결 메모”가 완전히 정리되지 않은 상태였다.

즉, 제품이 실제 살아 있는 장비를 본 게 아니라, 죽은 장비를 가리키는 낡은 포스트잇을 보고 “아직 연결되어 있음”이라고 판단한 것이다.

이게 이번 버그의 핵심이었다.

화면의 진실과 삭제 검사의 진실

이런 버그가 재밌는 이유는 두 세계가 서로 다른 진실을 말하기 때문이다.

첫 번째 세계는 UI다.

UI는 보통 “현재 살아 있는 것”을 보여준다. 이미 삭제된 연결은 목록에서 빠지는 게 자연스럽다. 사용자가 보는 화면에서는 구역이 비어 보인다.

두 번째 세계는 삭제 전 검증 로직이다.

삭제 버튼을 눌렀을 때 제품은 안전을 위해 묻는다.

“정말 지워도 되나? 연결된 것이 남아 있나?”

문제는 여기서 검증 기준이 UI 기준보다 느슨했다는 점이다. 연결 메모가 살아 있으면 count했다. 그 메모가 가리키는 실제 연결이 삭제된 상태인지까지 보지 않았다.

그래서 이런 불일치가 생겼다.

UI 목록 기준
- 살아 있는 연결만 보여줌
- 삭제된 연결은 안 보임
- 그래서 비어 보임

삭제 검증 기준
- 살아 있는 연결 메모를 봄
- 그 메모가 삭제된 연결을 가리키는지 확인하지 않음
- 그래서 연결이 있다고 판단함

이런 순간에 버그는 마치 “없는 사람이 출석 체크되는 상황”처럼 보인다.

ReproKit 탐정단이 먼저 한 일

버그를 고칠 때 가장 위험한 말은 “아마”다.

아마 이럴 것이다.
아마 저 테이블 때문이다.
아마 한 줄 고치면 된다.

이런 추측은 빠르지만, 오래 남을 검증 자료가 되지는 못한다. 그래서 ReproKit은 먼저 공식 경로를 탄다.

이번 검증 흐름은 이렇게 잡았다.

1. 제품 API로 구역을 만든다.
2. 제품 API로 연결을 만든다.
3. 제품 API로 연결을 삭제한다.
4. DB를 읽어서 정상 제품 경로가 어떤 상태를 남기는지 본다.
5. 정상 경로로 만들 수 없는 “과거에 남은 낡은 상태”만 최소 DB fixture로 만든다.
6. 실제 삭제 경로를 실행한다.
7. 결과 패키지를 만든다.

여기서 중요한 포인트는 5번이다.

좋은 재현은 무조건 DB를 때려 넣지 않는다. 먼저 제품이 제공하는 API와 UI 경로를 써본다. 정상 제품 경로가 문제 상태를 만들지 않는다면, 그때 비로소 말한다.

“이건 현재 정상 흐름에서 생기는 데이터가 아니라, 과거에 남은 역사적 불일치 상태입니다. 그래서 로컬 검증용으로만 최소 DB fixture를 만들겠습니다.”

이 한 문장이 꽤 중요하다.

DB fallback을 쓰더라도 숨기지 않는다. 왜 썼는지, 어디까지가 제품 경로이고 어디부터가 재현용 fixture인지 분리한다.

재현 자동화에서 정직함은 기능이다.

대조군을 세우면 버그가 선명해진다

이번 검증에서 마음에 들었던 부분은 대조군이다.

테스트 데이터는 두 개였다.

stale zone
- 구역은 살아 있음
- 연결 메모도 살아 있음
- 하지만 메모가 가리키는 연결은 삭제됨

active-control zone
- 구역은 살아 있음
- 연결 메모도 살아 있음
- 메모가 가리키는 연결도 살아 있음

첫 번째는 삭제되어야 한다.
죽은 연결만 붙어 있기 때문이다.

두 번째는 삭제되면 안 된다.
진짜 살아 있는 연결이 있기 때문이다.

이 대조군이 없으면 패치가 너무 관대해질 수 있다.

예를 들어 “삭제 검사를 아예 약하게 만들자”라고 고치면 stale case는 통과한다. 하지만 진짜 연결이 붙은 구역까지 삭제될 수 있다. 그건 버그 수정이 아니라 경비원을 해고한 것이다.

좋은 패치는 경비원을 해고하지 않는다.
경비원이 낡은 포스트잇과 실제 사람을 구분하게 만든다.

수정의 모양은 작았다

핵심 수정 방향은 단순했다.

기존 기준은 이런 느낌이었다.

활성 연결 메모가 있으면 삭제 차단

수정 후 기준은 이렇게 바뀐다.

활성 연결 메모가 있고,
그 메모가 가리키는 실제 연결도 활성 상태일 때만 삭제 차단

조건 하나가 더 들어갔을 뿐이다.
하지만 그 조건 하나가 UI의 세계와 삭제 검증의 세계를 맞춰준다.

화면에서 이미 사라진 삭제 연결은 삭제 차단 대상이 아니다.
반대로 실제 활성 연결이 남아 있으면 여전히 삭제를 막는다.

이게 좋은 버그 수정의 맛이다.

코드는 작게 움직이지만, 제품의 판단 기준은 정확해진다.

PASS는 그냥 초록불이 아니다

검증 결과는 PASS였다.

하지만 여기서 PASS는 단순히 “스크립트가 0으로 끝났다”가 아니다.

ReproKit 결과 패키지는 이런 것들을 함께 확인했다.

- API health가 정상인지
- 시나리오가 standard-native로 등록되어 있는지
- doctor/setup/verify 흐름이 동작하는지
- API-first 시도가 먼저 실행됐는지
- DB fallback이 왜 필요한지 기록됐는지
- fixture invariant가 기대한 상태인지
- 실제 삭제 후 stale zone은 삭제됐는지
- active-control zone은 남아 있는지
- 결과 보고서의 이미지/JSON/로그 artifact가 모두 존재하는지
- secret 후보가 결과물에 남지 않았는지

마지막 DB 검증은 이렇게 요약된다.

stale zone: deleted = 1, active attached connection = 0
active-control zone: deleted = 0, active attached connection = 1

이 네 줄이 사건의 결말이다.

죽은 연결만 가리키던 구역은 삭제됐다.
살아 있는 연결을 가진 구역은 삭제되지 않았다.

경비원이 드디어 사람과 포스트잇을 구분했다.

이번 사건에서 배운 것

첫째, UI와 검증 로직은 같은 기준을 써야 한다.

UI에서는 안 보이는데 삭제 검증에서는 막는다면, 사용자는 제품이 거짓말한다고 느낀다. 사실 제품은 거짓말한 게 아니라 서로 다른 기준으로 말하고 있었던 것이다.

둘째, stale data는 “지우면 되지”로 끝나지 않는다.

낡은 데이터가 왜 생겼는지 모를 때는 조심해야 한다. 현재 정상 경로에서는 생기지 않는지 확인하고, 과거 데이터인지, 비표준 경로인지, 일부 실패의 흔적인지 분리해야 한다.

셋째, 재현 자동화는 설명까지 포함해야 한다.

테스트가 통과했다는 사실보다 더 중요한 건 “어떤 경로를 탔고, 어디서 fallback을 썼고, 어떤 대조군이 있었는지”다. 그래야 다음 사람이 결과를 믿고 이어갈 수 있다.

넷째, 좋은 테스트에는 대조군이 있다.

stale case만 통과시키면 위험하다. active case가 계속 차단되는지도 봐야 한다. 버그를 고치다가 안전장치를 없애버리면 안 된다.

작은 포스트잇 하나가 알려준 큰 교훈

이번 버그는 겉으로 보면 단순했다.

“삭제된 연결을 count하지 않으면 된다.”

하지만 실제로는 더 큰 이야기를 담고 있었다.

제품은 현재 상태와 과거 흔적을 구분해야 한다.
UI와 backend 검증은 같은 세계관을 공유해야 한다.
재현 자동화는 제품 경로를 먼저 존중해야 한다.
그리고 테스트는 성공 버튼 하나가 아니라, 사람이 납득할 수 있는 이야기와 증거를 남겨야 한다.

버그는 종종 불쾌한 손님처럼 찾아온다.
하지만 잘 기록하고, 잘 재현하고, 잘 검증하면 좋은 운영 지식이 된다.

이번에는 삭제된 연결이 문 앞에서 유령처럼 서 있었다.
ReproKit은 손전등을 들고 와서 말했다.

“저건 사람이 아니라 포스트잇입니다.”

그리고 제품은 그제야 문을 열 수 있었다.