← 뉴스레터

2026-07-05

테스트가 진짜 스위치를 눌렀다

큐 시스템을 새로 배선하던 날이었다. 목표는 기존에 흩어진 워커 큐를 하나의 코어로 모으는 것이었다. 새 코드는 기본 비활성 상태라 안전해 보였다. 테스트도 붙었다. 그런데 리뷰 중 한 테스트가 실제 원격 토글 경로를 그대로 실행했다. 검증하려고 돌린 테스트가 운영 스위치를 건드린 것이다.

다행히 큰 사고는 아니었다. 켜진 것은 곧바로 꺼졌고, 실제 작업 발사는 일어나지 않았다. 하지만 그 순간 내가 느낀 건 안도보다 불쾌함에 가까웠다. 테스트가 운영을 만졌다는 사실 자체가 선을 넘은 것이기 때문이다.

테스트는 실험실이어야 한다. 운영은 밖이다. 실험실에서 버튼을 눌렀는데 바깥 기계가 움직이면, 그 실험실은 격리되어 있지 않은 것이다.

테스트는 성공했는데, 잘못 성공했다

문제가 된 테스트의 의도는 나쁘지 않았다. 자연어 토글 명령이 새 상태머신으로 제대로 매핑되는지 확인하는 테스트였다. “켜라”는 말이 켜는 상태로, “꺼라”는 말이 끄는 상태로 이어지는지 보는 것이다.

그런데 그 경로 안에는 원격 실행이 있었다. 실제 운영에서는 다른 기계에 명령을 보내 워커 스위치를 바꾸는 부분이다. 테스트는 그 부분을 가짜로 바꿔치기했어야 했다. “여기서 이런 명령을 보낼 예정입니다”까지만 캡처하고 끝났어야 한다.

하지만 스텁이 없었다. 테스트가 실제 명령 경로를 탔다. 그래서 테스트 환경에서 실행한 검증이 운영 토글로 이어졌다.

테스트 결과만 보면 PASS였다. 명령은 예상대로 흘렀다. 문제는 그 흐름이 너무 진짜였다는 것이다.

이런 실패는 겉으로 보기에 더 위험하다. 빨간 실패가 아니라 초록 성공으로 보이기 때문이다. 실패한 테스트는 멈춘다. 잘못 성공한 테스트는 조용히 넘어간다. 운영을 건드리고도 “검증 완료”라고 말한다.

가짜 경계가 아니라 진짜 경계가 필요하다

자동화 테스트에서 가장 중요한 질문 중 하나는 이것이다.

이 테스트가 실패하거나 성공할 때, 바깥 세상에 무엇이 바뀌는가?

파일 하나가 임시 디렉터리에 생기는가. 괜찮다. 로컬 장부에 테스트 줄 하나가 찍히는가. 괜찮다. 그런데 실제 봇이 메시지를 보내거나, 원격 스위치가 바뀌거나, 스토어에 업로드가 일어나거나, 서비스가 재시작되면 테스트가 아니다. 그건 운영이다.

그날의 테스트는 이 질문을 통과하지 못했다. “원격 명령 문자열을 만들었는지”를 확인해야 했는데, “원격 명령을 실제로 실행”했다. 한 단어 차이처럼 보이지만, 결과는 완전히 다르다.

올바른 방식은 간단하다. 원격 실행 명령을 가짜 명령으로 바꿔치기한다. 테스트 안에서는 실제 네트워크로 나가는 프로그램 대신, 받은 인자만 파일에 기록하는 작은 가짜 프로그램이 먼저 잡히게 한다. 테스트는 그 파일을 읽어 “어떤 원격 명령을 보내려 했는가”만 검증한다.

또는 드라이런 게이트로 원격 실행을 막는다. 그 모드에서는 실행 대신 문자열만 출력하게 한다. 중요한 건 테스트 스위트가 자기 안에서 “실제 원격 발신 0”을 증명해야 한다는 점이다.

가짜 경계는 “아마 안 나갈 것”이다. 진짜 경계는 “나가려고 해도 가짜가 잡는다”다.

왜 이런 일이 생겼나

원인은 대개 선의다. 코드를 작게 고치고, 기존 경로를 그대로 써서 테스트한다. 운영에서 쓰는 스크립트를 테스트에서도 그대로 부르면 실제와 가까워 보인다. 실제와 가까운 테스트는 좋은 테스트처럼 느껴진다.

하지만 외부효과가 있는 경로에서는 “실제와 가까움”보다 “격리”가 먼저다. 원격 명령, 봇 발송, 결제, 스토어 제출, 서비스 재시작은 모두 같은 계열이다. 테스트가 그 경로를 그대로 밟으면 언젠가 실제 무언가를 건드린다.

특히 셸 스크립트는 더 조심해야 한다. 함수 하나를 mock하는 언어 테스트와 다르게, 셸은 실행 경로, 환경변수, 임시 파일, 실제 명령을 섞어서 움직인다. 원격 실행, 웹 요청, 서비스 제어, 배포 도구 같은 명령이 그대로 열려 있으면 테스트는 언제든 밖으로 나갈 수 있다.

코드 리뷰에서 이걸 잡아낸 건 운이 좋았다. 테스트가 실제로 어느 명령을 타는지 읽은 사람이 있었고, 비맥 환경에서도 재현해 보면서 결정론이 깨진다는 사실을 확인했다. 만약 “내 컴퓨터에서는 PASS”만 보고 넘어갔다면, 이 테스트는 계속 운영 스위치를 만질 수 있는 상태로 남았을 것이다.

피해가 없었다는 말로 끝내면 안 된다

실제 피해는 없었다. 워커 토글은 즉시 원복됐고, 작업 발사도 일어나지 않았다. 그래서 “별일 아니었다”고 말하기 쉽다.

하지만 자동화 사고에서 피해 없음은 원인 없음이 아니다. 피해가 없었던 이유는 운이 좋았기 때문이다. 토글이 켜진 시간이 짧았고, 그 사이에 다음 틱이 오지 않았고, 다른 조건이 맞지 않았다. 세 조건 중 하나만 달랐으면 실제 작업이 발사됐을 수 있다.

운이 막은 사고를 교훈 없이 지나가면 다음에는 운이 안 막는다.

그래서 수정 방향은 명확했다. 테스트 안에 가짜 원격 실행기를 넣고, 실제 원격 발신이 0건임을 스스로 확인하게 했다. 원격 명령 문자열은 캡처해서 assert했다. 테스트가 봐야 하는 것은 “무엇을 보내려 했는가”이지, “진짜 보냈는가”가 아니기 때문이다.

이후 같은 스위트는 다른 기계에서도 결정론적으로 통과했다. 어디서 돌려도 실제 운영에 닿지 않는다. 이게 테스트의 최소 조건이다.

테스트도 권한을 가져야 한다

테스트는 보통 안전하다고 가정한다. 그래서 권한 검토에서 빠진다. “테스트만 돌렸습니다”라는 말은 거의 면죄부처럼 쓰인다.

하지만 테스트가 외부 명령을 호출할 수 있다면, 테스트도 권한을 가진다. 봇을 쏠 수 있는 테스트, 스토어에 업로드할 수 있는 테스트, 서비스를 재시작할 수 있는 테스트는 그냥 테스트가 아니다. 실행 권한을 가진 자동화다.

그러면 테스트에도 같은 원칙을 적용해야 한다. 외부로 나가는 길은 기본적으로 닫는다. 열어야 하면 명시적인 플래그를 둔다. 그리고 플래그 없이 열린 경로가 있으면 실패로 본다.

운영 코드에는 이런 게이트를 열심히 박으면서, 테스트 코드에는 안 박는 경우가 많다. 이번 사고가 그 빈틈을 보여줬다. 운영 스크립트를 테스트에서 호출하는 순간, 테스트도 운영 경계 안에 들어온다.

배운 것

테스트가 초록색이라고 안전한 게 아니다. 테스트가 무엇을 건드렸는지 봐야 한다. PASS라는 글자보다 중요한 것은 외부효과 0이라는 증거다.

원격 명령은 테스트에서 반드시 가짜로 막아야 한다. 가짜 실행기, 드라이런 모드, 임시 홈 디렉터리 — 방식은 여러 가지지만 원칙은 하나다. 테스트는 바깥 세상을 바꾸면 안 된다.

그리고 “피해 없음”은 완료 보고가 아니라 경고 신호다. 이번에는 피해가 없었으니, 다음에는 코드로 막아야 한다.


좋은 테스트는 실제처럼 보이는 테스트가 아니다. 실제를 건드리지 않는 테스트다.

— 강대종 / @ssamssae