-
나머지를 다 고정하고 내 것만 빠르게 — 그게 fixture인가, 최선인가IT 2026. 9. 23. 21:00

▶ 동영상 개요 — 나머지를 다 고정하고 내 것만 빠르게 — 그게 fixture인가, 최선인가
6분 11초 — 테스트에서 '나머지를 다 고정하고 내 것만 실행'에는 두 도구가 섞여 있다 — 상태를 고정하는 fixture는 넉넉히 써도 되지만, 협력자를 대역으로 바꾸는 m… — NotebookLM 동영상 개요로 생성 🎧 오디오 개요 — 나머지를 다 고정하고 내 것만 빠르게 — 그게 fixture인가, 최선인가
14분 58초 — 테스트에서 '나머지를 다 고정하고 내 것만 실행'에는 두 도구가 섞여 있다 — 상태를 고정하는 fixture는 넉넉히 써도 되지만, 협력자를 대역으로 바꾸는 m…. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성 개인 서버에서 작은 서비스를 몇 개 굴린다. 코드를 고치고 저장하면 그 순간 훅이 pytest를 돌리는데, 전체가 아니라 내가 방금 건드린 파일에 걸린 테스트만 몇 초 만에 돈다. 그 테스트들을 열어 보면 공통점이 있다 — 내가 손본 함수 하나만 진짜로 실행하고, 그 함수가 부르는 데이터베이스·외부 API·다른 모듈은 죄다 가짜로 세워 둔다. 빠르다. 좋다. 그런데 어느 날 이걸 하다 문득 멈칫했다.
궁금증은 두 겹이다. 첫째, 나머지를 다 고정하고 내가 손댄 것만 실행하는 이 방식이 흔히 말하는 "fixture"인가? 둘째, 다른 모든 걸 얼려 두고 내 변경만 빠르게 확인하는 것이 로컬에서 최선의 전략이 맞는가? 답부터 말하면 첫 질문은 "반은 맞고 반은 다르다"이고, 둘째 질문은 "빠른 안쪽 루프로는 맞지만, 그것만으로는 위험하다"이다. 이 글은 그 두 답을 푸는 과정이다.
먼저 — 한 동작에 서로 다른 두 도구가 뭉쳐 있다
"나머지를 다 고정한다"는 말에는 사실 성격이 다른 두 가지 조작이 섞여 있다. 하나는 테스트가 매번 똑같은 상태에서 출발하도록 알려진 배경을 깔아 두는 것이고, 다른 하나는 내 코드가 부르는 협력자(collaborator)를 진짜 대신 내가 조종하는 가짜로 바꿔치기하는 것이다. 앞의 것이 fixture이고, 뒤의 것이 mock을 포함한 test double(테스트 대역)이다. 둘을 "고정"이라는 한 단어로 뭉뚱그리면, 어디까지 얼려도 안전하고 어디부터 얼리면 위험한지가 안 보인다. 그래서 먼저 갈라야 한다.
fixture — 실험대에 붙박은 "고정된 출발선"
fixture라는 단어는 원래 실험대나 공작 기계에 붙박이로 고정해 둔 장치를 뜻한다. 가공할 부품을 매번 똑같은 위치에 물려 주는 지그(jig) 같은 것이다. 테스트에서의 fixture도 정확히 같은 발상이다 — 테스트가 실행되기 전에 매번 동일하게 준비되는, 알려진 상태를 말한다. 임시 데이터베이스에 미리 넣어 둔 샘플 행, 테스트용으로 만들어 둔 파일 한 벌, "현재 시각"을 특정 값으로 못 박아 둔 것 — 이 모두가 fixture다. pytest에서 `@pytest.fixture`로 선언하고 함수 인자로 받아 쓰는 그 fixture가 이 개념의 구현이다.
다이어그램 설명. 이 그림이 보여주는 건 fixture의 유일한 임무다 — 테스트가 실행될 때마다 "고정된 알려진 상태"에서 출발해 "매번 똑같은 출발선"에 서게 하는 것이다. 왜 이게 필요한가? 테스트가 어제 남긴 찌꺼기 데이터나 실행할 때마다 달라지는 현재 시각 위에서 돌면, 같은 코드가 어제는 통과하고 오늘은 실패한다 — 코드가 틀린 게 아니라 배경이 흔들린 것이다. fixture는 그 배경을 못 박아 흔들림을 없앤다. 그러니 질문자가 말한 "내가 손 본 것 외의 요소를 최대한 고정한다"는 발상 중 상태를 고정하는 부분은 정확히 fixture가 맞고, 이건 아무리 많이 써도 좋은 습관이다. 여기까지는 얼릴수록 이득이다.
mock — 협력자를 진짜 대신 대역으로 세운다
그런데 "데이터베이스도 외부 API도 다른 모듈도 가짜로 세운다"는 부분은 fixture가 아니다. 이건 test double — 영화의 스턴트 대역(stunt double)에서 따온 이름으로, 진짜 협력자 대신 세우는 가짜를 통칭한다. 대역에도 급이 있어서, 그냥 자리만 채우는 dummy, 정해진 값을 돌려주는 stub, 호출 여부까지 기록해 검증하는 mock, 진짜처럼 동작하는 간이 구현 fake로 나뉜다. 흔히 이 전부를 뭉쳐 "목(mock)"이라 부른다. fixture가 상태를 깔아 주는 도구라면, mock은 협력자를 바꿔치기하는 도구다 — 하는 일이 근본적으로 다르다.
다이어그램 설명. "내가 손본 코드"가 협력자를 부를 때, 그 자리에 "협력자의 대역"을 끼워 넣어 "내가 정한 응답"을 받게 하는 그림이다. 왜 이렇게 하나? 진짜 데이터베이스를 띄우면 느리고, 외부 API는 네트워크가 끊기면 테스트가 같이 죽고, "현재 시각"이나 난수는 실행할 때마다 값이 달라 결과를 예측할 수 없다 — 즉 협력자가 느리거나·불안정하거나·비결정적이면 그걸 대역으로 치워 내 코드만 또렷이 시험할 수 있다. 여기서 놓치기 쉬운 함정이 하나 있다. mock은 fixture처럼 "많이 쓸수록 좋은" 도구가 아니라는 점이다. fixture는 배경을 고정할 뿐이라 부작용이 없지만, mock은 진짜의 행동을 내 추측으로 대체하기 때문에, 추측이 틀리면 테스트가 거짓말을 하기 시작한다. 다음 절이 그 지점이다.
"다 가짜로 세운다"의 함정 — 결국 가짜를 테스트하게 된다
협력자를 대역으로 세운다는 건 곧 "이 협력자는 이런 값을 돌려줄 것이다"라는 내 추측을 코드에 박아 넣는 것이다. 문제는 진짜 협력자가 언젠가 바뀐다는 데 있다. 데이터베이스 스키마가 바뀌고, 외부 API가 응답 형식을 손보고, 옆 모듈의 반환값이 달라진다. 그런데 내 테스트 속 대역은 옛날 추측 그대로 멈춰 있다. 그러면 진짜는 이미 깨졌는데 대역만 보고 도는 테스트는 초록불을 켠다. 테스트가 통과하니 안심하고 배포하고, 프로덕션에서 터진다. 이것이 과잉 목(over-mocking)의 핵심 실패다 — 내 코드를 시험한 게 아니라 내가 세운 가짜를 시험한 것이다.
다이어그램 설명. 이 흐름이 보여주는 건 과잉 목이 어떻게 조용히 사고로 이어지는가다. "진짜 협력자가 바뀜"에서 출발하는데, "대역은 옛 추측 그대로" 멈춰 있으니 "테스트는 초록불"을 켜고, 그 초록불을 믿고 내보낸 코드가 "프로덕션에서 진짜와 만나 터진다". 두 번째 폐해도 있다. 모든 협력자를 대역으로 세우면 테스트가 내 코드의 내부 구조에 밀착한다 — "이 함수가 저 함수를 이런 인자로 부른다"까지 대역으로 못 박아 두면, 겉으로 드러나는 동작은 그대로인데 안쪽만 다듬는 리팩터링에도 테스트가 우수수 깨진다. 테스트가 변경을 막는 그물이 아니라 변경을 방해하는 족쇄가 된다. 그래서 "나머지를 최대한 다 가짜로 세운다"는 전략은, 속도는 얻지만 그 대가로 신뢰를 잃는다.
그럼 무엇을 대역으로 세우고 무엇을 진짜로 둘까
해답은 극단 사이의 균형이고, 그 경계는 의외로 선명하다. 테스트 커뮤니티는 이 선택을 solitary(고독한) 테스트 대 sociable(어울리는) 테스트라는 축으로 부른다. solitary는 대상 하나만 남기고 협력자를 전부 대역으로 격리하는 방식이고, sociable은 외부 입출력만 대역으로 세우고 내부 협력자와는 진짜로 어울려 돌게 두는 방식이다. 마틴 파울러(Martin Fowler)는 전자를 선호하는 이들을 mockist, 후자를 선호하는 이들을 classicist라 정리했다. 실무의 무게추는 sociable 쪽으로 기운다 — 대역은 느리거나·불안정하거나·비결정적인 진짜 경계에서만 쓰고, 내부는 진짜로 둔다는 원칙이다.
다이어그램 설명. "내가 손본 코드"에서 두 갈래로 갈라, 왼쪽 "진짜로 둔다"에는 내부 협력자와 순수 함수·검증기처럼 빠르고 결정적인 것을 놓고, 오른쪽 "대역으로 세운다"에는 네트워크·데이터베이스·외부 API·현재 시각처럼 느리거나 불안정한 경계를 놓았다. 판별 기준은 딱 하나다 — "이걸 진짜로 실행하면 테스트가 느려지거나·불안정해지거나·매번 결과가 달라지는가?" 그렇다면 대역으로 세우고, 아니라면 진짜로 둔다. 내부 검증기나 매퍼는 진짜로 돌려도 빠르고 결정적이니 대역으로 세울 이유가 없다. 오히려 진짜로 둬야 그들끼리의 연결에서 생기는 문제까지 테스트가 잡아 준다. 이 원칙 한 줄이 앞 절의 과잉 목 함정을 대부분 막는다 — 추측으로 대체하는 지점을 내가 통제할 수 없는 바깥 경계로 최소화하기 때문이다.
"빠르게 내 것만"은 최선의 로컬 전략인가
이제 둘째 질문이다. 상태는 fixture로 고정하고, 대역은 바깥 경계에만 아껴 쓰기로 했다 치자. 그렇게 내가 바꾼 좁은 범위만 몇 초 만에 돌리는 것이 로컬에서 최선인가? 로컬의 안쪽 루프로는 맞다. 오타 하나 고치고 전체 스위트 10분을 기다리면 사고 흐름이 끊긴다. 저장할 때마다 관련 테스트만 몇 초에 돌아야 "고치고 → 확인하고 → 또 고치는" 리듬이 산다. 이렇게 변경에 영향받는 테스트만 골라 돌리는 방식엔 이름도 있다 — 테스트 영향 분석(Test Impact Analysis)이다. 코드 변경과 테스트의 의존 관계를 따져, 안 건드린 곳의 테스트는 건너뛰어 피드백을 앞당긴다.
다만 이건 속도와 신뢰를 맞바꾼 거래라는 걸 잊으면 안 된다. 내가 바꾼 범위만 본다는 건, 내 변경이 예상 밖의 먼 곳을 망가뜨렸을 때 그걸 놓친다는 뜻이다. 영향 분석의 의존 그래프가 그 연결을 몰랐다면, 빠른 로컬 루프는 초록불인 채로 조용히 넘어간다. 그래서 이 빠른 루프는 반드시 넓은 그물과 짝지어야 안전하다 — 병합할 때나 밤마다, 남의 변경까지 합친 전체 스위트를 느리더라도 전부 돌리는 그물이다.
다이어그램 설명. "저장할 때 내가 만진 것만" 도는 빠른 루프에서, "병합·야간 전체 스위트"라는 넓은 그물로 이어지는 관계를 보여준다. 핵심은 이 둘이 대체재가 아니라 보완재라는 점이다. 빠른 루프는 개발 리듬을 살리기 위해 일부러 범위를 좁힌 것이고, 그렇게 좁혀서 새는 것을 전체 스위트가 뒤에서 받아 낸다. 놓치기 쉬운 오해는 "로컬에서 내 것만 빠르게 돌리면 충분하다"고 여기는 것이다 — 그건 좁힌 범위 밖으로 샌 회귀(regression)를 받아 줄 그물이 없다는 뜻이고, 확률적으로 언젠가 뚫린다. 그러니 "내 것만 빠르게"는 최선의 안쪽 루프이되, 그 자체로 완결된 전략은 아니다. 완결은 빠른 루프 더하기 넓은 그물이다.
정리 — 상태는 넉넉히 얼리고, 협력자는 아껴 얼린다
질문으로 돌아가자. "나머지를 다 고정하고 내 것만 빠르게"라는 한 문장에는 얼려도 좋은 것과 아껴 얼려야 할 것이 뒤섞여 있었다. 상태를 고정하는 fixture는 넉넉히 써도 좋다 — 매번 같은 출발선을 만드는 일에는 부작용이 없다. 반면 협력자를 대역으로 세우는 mock은 내가 통제할 수 없는 바깥 경계에만 아껴 써야 한다 — 안쪽까지 다 가짜로 세우면 내 코드가 아니라 내 추측을 시험하게 되고, 리팩터링을 막는 족쇄가 된다. 그리고 그렇게 좁혀 빠르게 도는 로컬 루프는 최선이 맞지만, 병합·야간의 넓은 그물과 짝지을 때만 안전하다.
개인 작업에도 그대로 적용된다. 훅이 저장할 때마다 내가 만진 것만 몇 초에 돌리게 두되, 그 편의가 무엇을 못 보게 하는지를 기억하는 것 — 빠른 루프의 진짜 값은 속도가 아니라, 자기가 좁혀 놓은 범위를 정직하게 아는 데서 나온다.
참고한 공개 자료:
- Martin Fowler — Unit Test (solitary vs sociable, mockist vs classicist)
- testRigor — What Are Solitary and Sociable Unit Testing?
- Don't Mock What You Don't Have To (over-mocking anti-pattern)
- testkube — What Is Test Impact Analysis?
- pytest — How to use fixtures
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
MCP·RAG에 API를 넣기 전에 설명을 다듬어야 할까 — 모델이 똑똑해져도 남는 enrich의 이득, 사라지는 이득 (0) 2026.09.28 Gemini는 왜 OpenClaw의 OAuth 연동만 콕 집어 막았나 — 인증 토큰에 숨은 요금 계약 (0) 2026.09.27 DeepWiki는 이제 필요 없을까 — 모델이 코드를 읽는 시대의 문서화 (0) 2026.09.26 Kimi K2는 정말 오푸스·소네트와 붙나 — 1조 개 중 320억만 켜는 오픈 웨이트 모델의 정체 (0) 2026.09.25 GLM-5.3은 왜 갑자기 오푸스와 붙나 — 뼈대는 그대로 두고 후처리만 갈아 끼운 오픈 웨이트 모델 (0) 2026.09.24 합성 모니터링 — 아무도 안 쓰는 새벽 3시에 로그인·결제를 대신 밟아 주는 로봇 (0) 2026.09.22 카나리 릴리스 — 새 버전을 사용자 1%에게만 먼저 흘려보내고 지표로 판정한다 (0) 2026.09.22 스모크 테스트 — 배포 직후 '불이 켜지긴 하나'만 60초에 확인하는 얕고 빠른 그물 (0) 2026.09.22 테스트는 레벨마다 다른 질문에 답한다 — 로컬·통합·프로덕션에서 공통 테스트를 반복해야 할까 (0) 2026.09.22 Acorn은 플러그인도 테마도 아니다 — 워드프레스 안에 라라벨을 심는 물건의 정체와 2026년 현황 (0) 2026.09.21