단위 테스트는 비즈니스 의도를 지켜야 한다
비즈니스 레이어 단위 테스트는 설계를 돕고, 중요한 동작을 기록하며, 다음 개발자가 위험한 변경 앞에서 한 번 더 생각하게 할 때 의미가 있습니다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
비즈니스 서비스가 작은 구현 컴포넌트를 조합하는 역할만 한다면 그 서비스의 단위 테스트에 의미가 있을까요? 실제 로직은 finder, updater, repository와 여러 협력 객체에 들어 있을 수 있습니다. 그렇다면 서비스는 통합 테스트로만 검증해야 하지 않느냐는 생각이 듭니다.
그 의문은 타당합니다. “모든 서비스에는 단위 테스트가 있어야 한다”는 이유만으로 작성한 테스트는 쓸모없을 수 있습니다. 더 좋은 질문은 이 테스트가 어떤 불안을 줄이고 어떤 의도를 남기는가입니다.
테스트에도 맡은 일이 있어야 한다
저는 기능을 개발하는 과정에서 자연스럽게 나오는 테스트를 선호합니다. 비즈니스를 잘 모를 때 테스트는 임시 설계에 빠른 피드백을 줍니다. 시스템에 필요하다고 생각하는 동작을 먼저 적고, 그 동작에 필요한 컴포넌트를 발견하며, 이해가 나아질 때 둘 다 수정합니다.
갑자기 컵 공장에 입사해서 makeCup을 구현해야 한다고 해보겠습니다. 컵을 어떻게 만드는지 거의 모릅니다. 재료 저장소, 틀, 화로가 필요할 것이라고 짐작할 수 있습니다. 일부러 어색한 예입니다. 낯선 도메인을 처음 만났을 때는 실제로 이렇습니다. 이름이 불확실하고 책임을 엉뚱한 곳에 두며, 업무를 아는 사람은 무엇이 틀렸는지 바로 알아봅니다.
테스트는 이 추측을 구체적으로 만듭니다. 한 테스트는 비즈니스 결과를 확인할 수 있습니다. 반환된 컵에 손잡이가 있고 크기는 중간이어야 한다고 적습니다. 다른 테스트는 협력을 설명합니다. 저장소에서 재료를 가져오고, 성형하고, 화로가 필요한 작업을 수행하는지 봅니다. 두 테스트가 지키는 것은 다릅니다. 하나는 결과를, 다른 하나는 의미 있다고 판단한 순서나 책임 구분을 봅니다.
어느 방식이 언제나 옳은 것은 아닙니다. 모든 협력 객체 호출을 검증하면 테스트가 우연한 구현 세부사항에 결합되어 평범한 리팩터링도 어렵게 만들 수 있습니다. 최종 값만 확인하면 비즈니스 흐름에서 의미 있는 협력 자체를 놓칠 수도 있습니다. 비즈니스에서 걱정하는 것에 맞춰 assertion을 고르면 됩니다.
설계 과정에서 테스트가 유용한 이유는 불확실함을 클래스 안에 숨기기 전에 내가 믿는 것을 말하게 만들기 때문입니다. 도메인 전문가가 틀이 잘못됐다고 하거나 화로가 컵을 자동으로 꺼낸다고 알려주면 모델을 바꾸고 바로 피드백을 받을 수 있습니다.
이미 있는 코드에는 선택적으로 답하기
서비스가 이미 구현된 상태라면 상황이 달라집니다.
구현이 끝난 모든 메서드에 mock과 assertion을 붙인다고 커버리지와 함께 가치가 자동으로 올라가지는 않습니다. 테스트를 쓰기 전에 이 서비스에 보존할 판단이 있는지 묻습니다. 컴포넌트 조합을 이해하기 어려운가, 비즈니스 동작이 중요한가, 리팩터링을 준비하는가, 새 동료에게 의도한 흐름을 보여줘야 하는가, 버그가 빠진 사례를 알려줬는가.
답이 그렇다면 나중에 작성하는 비즈니스 레이어 단위 테스트도 충분히 의미 있습니다. 왜 이 컴포넌트를 조합했는지 기록하고, 중요한 결과를 검증하며, 리뷰어에게 변경을 보는 두 번째 관점을 줍니다. 수정된 테스트는 수정된 구현보다 더 많은 말을 할 때가 있습니다. 개발자가 기존의 어느 가정을 바꾸기로 했는지 보여주기 때문입니다.
이유가 “서비스니까 테스트해야 한다”뿐이라면 만들지 않는 편이 낫습니다. 테스트 코드도 코드입니다. 유지보수 비용이 있고 운영 코드처럼 필요에 의해 만들어져야 합니다.
이 말은 테스트 주도 개발이 언제나 우월하다는 주장도 아닙니다. 유용한 기법을 정체성이나 상황을 무시하는 규칙으로 만드는 것을 좋아하지 않습니다. 먼저 쓴 테스트는 모르는 영역에서 작업을 이끌 수 있습니다. 버그가 난 뒤 쓴 테스트는 같은 실패의 재발을 막습니다. 안정된 레거시 동작을 감싼 테스트는 리팩터링을 가능하게 합니다. 작성 시점은 테스트가 맡은 일에 따라 달라집니다.
실패하는 테스트가 잠깐 멈추게 한다
새 개발자가 makeCup을 바꾼다고 해보겠습니다. 기존 설계를 모르는 채 성형 컴포넌트를 교체하고 화로를 없애거나 메서드 시그니처를 변경합니다. 이제 컴파일이나 테스트가 실패합니다.
실패가 기존 설계의 정답을 증명하지는 않습니다. 대신 잠깐 멈추게 합니다. 개발자는 테스트가 무엇을 기대했는지 보고 비즈니스 의도도 함께 바뀌었는지 판단해야 합니다. 코드 리뷰에서는 기존 테스트와 새 테스트를 비교하며 새 컴포넌트 경계가 더 나은지, 무관한 책임을 섞었는지 이야기할 수 있습니다.
중요한 비즈니스 레이어 동작에 테스트를 쓰는 이유 중 하나가 이 멈춤입니다. 이후 변경에는 “컴파일된다”보다 한 번의 생각이 더 필요합니다. 테스트는 처음 만든 사람이 떠난 뒤에도 인수인계를 통해 의도를 전달합니다.
테스트가 불안감을 줄이는지도 실용적인 판단 기준입니다. “작은 부분만 바꿨고, 손으로 눌러봤고, 운영에서도 다시 보면 되겠지”라고 생각하며 배포한다면 불확실성이 사라진 게 아닙니다. 릴리스로 옮겨갔을 뿐입니다. 깨지기 쉬운 규칙 하나를 자동으로 확인하는 편이 넓고 얕은 커버리지 테스트 하나보다 가치 있을 수 있습니다.
테스트가 버그를 없애지는 않습니다. 그 사실로 싸우는 것은 시간을 낭비합니다. 버그가 나오면 가능할 때 재현 테스트를 만들어 똑같은 사례가 눈에 띄지 않은 채 돌아오지 못하게 합니다. 테스트 묶음은 팀이 이미 비용을 낸 실패의 기록이 됩니다.
내부가 얽혔다면 바깥 동작부터 잡기
이어받은 컴포넌트 조합이 너무 어지러우면 의미 있는 단위 테스트를 만들기 어렵습니다. 컵을 만드는 서비스가 갑자기 꽃집 같은 것에 의존한다고 해보겠습니다. 모든 협력 객체를 mock하면 혼란을 안전하게 만들기보다 그대로 보존할 수 있습니다.
이럴 때는 바깥 동작부터 시작합니다. 사용자나 호출자가 의존하는 동작을 통합 테스트로 잡습니다. 경계를 보호한 뒤 내부를 작은 단계로 리팩터링하고 기능이나 동작 단위로 배포합니다. 더 선명한 책임이 보이는 곳에는 좁은 테스트를 추가합니다.
통합 테스트와 단위 테스트는 서로 경쟁하는 신념이 아닙니다. 통합 테스트는 실제 컴포넌트를 통과하는 외부 계약을 지킵니다. 집중된 단위 테스트는 그 계약 안의 중요한 판단에 빠른 피드백을 줍니다. 현재 위험을 다루는 수준을 선택하면 됩니다.
테스트 자체는 피드백입니다. 개수, mock 프레임워크, 운영 코드보다 먼저 작성했는지가 가치를 결정하지 않습니다. 비즈니스를 이해하게 하고, 중요한 판단을 보존하고, 이미 비용을 낸 버그가 재발하지 못하게 하거나, 다음 개발자가 덜 두려운 상태에서 소프트웨어를 바꿀 수 있게 할 때 가치가 있습니다.
그 테스트는 작성하십시오. 목적 없는 테스트는 건너뛰십시오.