2026년 6월 30일 | 개발 깨알 상식_Tips
테스트가 한 번에 스무 개씩 깨지면 로그는 빨갛지만 정보량은 오히려 줄어든다. 마지막 traceback만 따라가다 보면 처음 깨진 입력을 놓치고, 같은 원인에서 파생된 실패를 서로 다른 버그처럼 보게 된다. 나는 이런 상황에서 바로 전체 로그를 끝까지 읽기보다 첫 실패를 고정한 짧은 실행으로 한 번 접어 본다. pytest의 `-x`나 `--maxfail=1`은 그때 쓰기 좋은 작은 브레이크다.
실패가 많을수록 원인은 흐려진다
테스트 실패가 많은 날은 대개 두 종류가 섞인다. 하나는 실제로 여러 기능이 동시에 깨진 경우다. 다른 하나는 하나의 초기 조건이 틀어져서 뒤 테스트가 줄줄이 넘어진 경우다. 예를 들어 fixture가 빈 DataFrame을 잘못 만들면 파서 테스트, 통계 테스트, 리포트 테스트가 한꺼번에 실패한다. 겉으로는 실패가 많아 보이지만 실제로는 첫 번째 잘못된 입력 하나를 고치면 대부분 사라질 수 있다.
그래서 긴 로그에서 내가 먼저 찾는 것은 “가장 마지막에 보이는 에러”가 아니라 “정상 흐름이 처음 어긋난 지점”이다. 이 기준을 잡지 않으면 수정도 커진다. 여기저기 방어 코드를 넣고 나서야 fixture 하나가 틀렸다는 사실을 알게 되는 식이다. 실패 개수가 많을수록 처음 깨진 테스트 하나만 다시 실행해 보는 시간이 오히려 아깝지 않았다.
maxfail은 속도 옵션만은 아니다
pytest에서는 `-x`가 첫 실패에서 멈추는 짧은 옵션이고, `--maxfail=1`은 같은 의미를 조금 더 드러내서 쓰는 형태다. 둘 다 실행 시간을 줄이는 데 도움이 되지만, 내가 더 중요하게 보는 쪽은 로그의 모양을 작게 만드는 효과다. 실패가 하나만 나오면 에러 위치, 입력 값, fixture 이름, 경고 메시지를 한 화면에서 함께 볼 수 있다.
python -m pytest tests/parser -x -q
python -m pytest tests/parser --maxfail=1 -vv
python -m pytest tests/parser/test_csv.py::test_empty_row --maxfail=1 -vv
처음에는 넓게 한 번 돌리고, 첫 실패가 잡히면 해당 디렉터리나 테스트 함수로 범위를 좁힌다. `-q`는 전체 흐름을 빨리 볼 때 좋고, 원인을 읽어야 할 때는 `-vv`로 테스트 이름과 parameter 값을 더 보이게 한다. 중요한 것은 옵션 조합 자체가 아니라, “지금은 모든 실패를 수집하는 단계인가, 첫 원인을 좁히는 단계인가”를 분리하는 것이다.
이 옵션을 붙인다고 테스트 설계가 좋아지는 것은 아니다. 그래도 수정 초반에는 꽤 현실적인 도움이 된다. 실패가 많으면 사람도 에이전트도 한 번에 너무 많은 가설을 만든다. 첫 실패 하나만 남기면 질문이 바뀐다. “왜 전체가 깨졌지?”가 아니라 “이 입력에서 기대값이 왜 바뀌었지?”가 된다. 디버깅 질문이 이 정도로 줄어들면 다음 행동도 훨씬 작아진다.
실패 순서가 흔들릴 때
첫 실패 기준을 쓸 때 조심할 점은 테스트 순서가 항상 의미 있는 순서가 아닐 수 있다는 것이다. 병렬 실행, 랜덤 순서 플러그인, 공유 상태가 남는 fixture가 있으면 “첫 실패”가 실행마다 바뀐다. 이때는 `--maxfail=1`만 붙여 놓고 원인을 확정하면 안 된다. 같은 명령을 두세 번 반복했을 때 같은 테스트가 먼저 깨지는지, 아니면 실패 위치가 계속 움직이는지부터 봐야 한다.
python -m pytest tests/parser --maxfail=1 -vv
python -m pytest tests/parser --maxfail=1 -vv
python -m pytest tests/parser --maxfail=1 -vv
세 번 모두 같은 테스트가 먼저 깨지면 입력이나 기대값 문제일 가능성이 크다. 반대로 매번 다른 테스트가 먼저 깨지면 공유 캐시, 임시 파일, 전역 설정, 랜덤 seed를 의심한다. 이 구분을 하지 않으면 첫 실패를 고정하려던 옵션이 오히려 잘못된 확신을 줄 수 있다. 그래서 나는 흔들리는 실패에서는 첫 실패의 이름보다 실패 순서의 안정성을 먼저 적어 둔다.
내가 남기는 실패 묶음
첫 실패에서 멈췄다면 로그도 조금 정리해서 남기는 편이 좋다. 나는 보통 네 가지를 적는다. 실행한 테스트 범위, 첫 실패 테스트 이름, 입력이나 fixture의 핵심 값, 그리고 재현 명령이다. 이 네 줄이면 다음에 같은 문제를 볼 때 전체 suite를 다시 돌리지 않고도 바로 작은 재현으로 들어갈 수 있다.
특히 코딩 에이전트에게 넘길 때는 긴 stderr 전체를 붙이기보다 이 묶음이 더 잘 먹힌다. “테스트가 많이 깨졌다”는 말은 행동 범위가 너무 넓다. 반대로 “`test_empty_row`가 빈 문자열 입력에서 먼저 깨지고, 기대값은 빈 리스트인데 실제값은 None이다”라고 쓰면 수정 후보가 파서의 빈 입력 처리로 좁혀진다. 실패를 작게 만든다는 것은 정보를 숨기는 것이 아니라 다음 수정의 반경을 줄이는 것에 가깝다.
traceback은 읽을 만큼만 펼친다
첫 실패가 잡혔는데 traceback이 너무 길면 `--tb=short`나 `--tb=long`도 의식적으로 바꾼다. 라이브러리 내부 호출이 끝없이 이어지는 실패에서는 짧은 traceback이 낫고, fixture 생성 과정이나 context manager 정리 단계가 의심될 때는 긴 traceback이 필요하다. 같은 `--maxfail=1` 실행이라도 traceback 깊이를 바꾸면 내가 보는 질문이 달라진다.
python -m pytest tests/parser --maxfail=1 --tb=short
python -m pytest tests/parser --maxfail=1 --tb=long
이 조합은 특히 경고가 많은 저장소에서 도움이 된다. 실패 원인이 warning 폭주에 묻히면, 실제 assertion이 보이는 줄까지 스크롤하다가 이미 맥락을 잃는다. 나는 이럴 때 경고를 완전히 무시하기보다, 첫 실패를 먼저 고정하고 별도 실행에서 warning을 정리한다. 실패 로그와 청소 로그를 한 번에 섞지 않는 편이 덜 헷갈렸다.
고친 뒤에는 반드시 전체로 돌아온다
다만 `--maxfail=1`을 최종 검증처럼 쓰면 위험하다. 첫 실패를 고친 뒤 두 번째 실패가 숨어 있을 수 있고, 작은 범위 테스트만 통과한 채 통합 경로가 깨진 상태로 남을 수도 있다. 그래서 내 기준은 단순하다. 디버깅 중에는 첫 실패에서 멈춰도 되지만, 커밋 전에는 다시 전체 범위로 돌아온다.
python -m pytest tests/parser --maxfail=1 -vv
# 수정 후
python -m pytest tests/parser
python -m pytest
이 순서를 지키면 짧은 로그와 전체 검증의 역할이 헷갈리지 않는다. 첫 실패 고정은 원인 후보를 좁히는 렌즈이고, 전체 테스트는 수정이 다른 곳을 망가뜨리지 않았는지 확인하는 문턱이다. 나는 실패가 많이 난 날일수록 이 둘을 섞지 않으려고 한다. 로그를 짧게 만드는 목적은 빨리 넘어가기 위해서가 아니라, 가장 먼저 봐야 할 입력을 놓치지 않기 위해서다.
'[개발 깨알 상식_Tips]' 카테고리의 다른 글
| 삭제된 로그 파일이 붙잡은 디스크 용량 (0) | 2026.07.02 |
|---|---|
| 체크섬 manifest로 잡는 깨진 다운로드 (0) | 2026.07.01 |
| 서버 동기화 직전의 삭제 파일 경고 (0) | 2026.06.26 |
| 느린 API 호출의 연결 시간과 응답 시간 분리 (0) | 2026.06.25 |
| SQLite 백업에서 빠진 WAL 로그 (0) | 2026.06.24 |