테스트 코드는 왜 필요한가
테스트 코드는 왜 필요한가
테스트 코드는 기능을 더 빨리 만드는 도구는 아니다. 대신 수정 비용을 낮추는 데 직접 도움이 된다. 코드베이스가 커질수록 이 차이는 분명해진다.
처음에는 테스트 없이도 개발이 가능하다. 하지만 기능이 늘고 수정이 반복되면 상황이 달라진다. 잘 동작하던 로직이 다른 변경 때문에 깨지고, 이미 고친 버그가 다시 들어오고, 함수 하나 바꾸는 일도 부담이 된다. 테스트 코드는 이런 비용을 줄이기 위해 필요하다.
테스트 코드가 중요한 이유
리팩터링 안정성을 높인다
코드는 작성보다 수정하는 시간이 더 길다. 변수 이름을 정리하고, 중복 로직을 함수로 분리하고, 구조를 바꾸는 일은 계속 생긴다.
문제는 구조를 정리하는 과정에서 기존 동작이 함께 바뀔 수 있다는 점이다. 테스트가 있으면 수정 전후에 같은 결과가 유지되는지 바로 확인할 수 있다. 리팩터링이 가능한 이유는 테스트가 있기 때문이다.
회귀를 막는다
버그를 한 번 수정했다고 같은 문제가 다시 나오지 않는 것은 아니다. 비슷한 로직을 건드리거나 조건 분기를 추가하는 과정에서 같은 문제가 다시 들어오는 경우가 많다.
이때 테스트는 회귀를 막는 역할을 한다. 버그를 수정한 뒤 해당 케이스를 테스트로 남겨두면, 같은 문제가 다시 생겼을 때 바로 드러난다. 특히 여러 사람이 함께 수정하는 코드베이스에서는 이 효과가 크다.
함수와 모듈의 동작 명세가 된다
테스트는 코드가 어떻게 동작해야 하는지를 직접 보여준다. 입력이 무엇이고, 결과가 무엇이어야 하는지, 어떤 값은 허용하고 어떤 값은 막아야 하는지가 테스트에 드러난다.
설명 문서만으로는 실제 동작을 정확히 전달하기 어렵다. 반면 테스트는 실행 가능한 형태로 동작 조건을 남긴다. 새로운 사람이 코드를 읽을 때도 테스트가 있으면 함수의 의도를 이해하기 쉽다.
협업과 유지보수 효율을 높인다
혼자 개발할 때보다 팀으로 일할 때 테스트의 가치가 더 분명해진다. 내가 짠 코드를 다른 사람이 수정하고, 내가 남이 작성한 로직을 건드리는 일이 계속 생기기 때문이다.
테스트가 있으면 변경 전에 확인해야 할 기준이 분명해진다. 리뷰할 때도 동작 보장이 어디까지인지 판단하기 쉽고, 배포 전에 반복적인 수동 확인에 쓰는 시간도 줄일 수 있다.
AI agent 시대에는 테스트가 더 중요해진다
예전에는 코드를 직접 한 줄씩 작성했기 때문에 변경 속도에 자연스러운 제한이 있었다. 지금은 다르다. AI agent나 코딩 도구를 쓰면 함수 하나를 고치는 수준이 아니라, 여러 파일에 걸친 수정안이 한 번에 나온다. 속도는 빨라진다. 대신 검증 비용도 같이 커진다.
문제는 AI가 코드를 많이 만들어준다고 해서, 그 코드의 의도와 경계 조건까지 자동으로 보장해주지는 않는다는 점이다. 겉으로는 그럴듯해 보여도 예외 처리, 경계 입력, 기존 모듈과의 계약 같은 부분은 쉽게 틀어진다. 특히 이미 있는 코드베이스를 바꾸는 작업에서는 더 그렇다.
이때 테스트가 없으면 사람이 다시 전부 읽고 추적해야 한다. 결국 생성 속도보다 검증 속도가 병목이 된다. AI agent를 쓸수록 테스트 코드의 역할은 더 커진다. 테스트는 “이 변경이 기존 동작을 깨지 않았는가”를 가장 빠르게 확인하는 기준점이기 때문이다.
팀 단위로 보면 의미가 더 분명하다.
- AI가 만든 변경을 리뷰할 때 확인 기준이 된다
- 여러 파일을 건드린 수정에서 회귀를 빠르게 잡는다
- agent가 반복적으로 수정하는 로직의 안전망이 된다
- 사람의 기억 대신 실행 가능한 규칙을 남긴다
정리하면, AI 도구가 테스트를 덜 필요하게 만드는 것이 아니다. 오히려 반대다. 코드 생성 속도가 빨라질수록 테스트는 선택이 아니라 검증 비용을 통제하는 장치가 된다.
테스트가 없을 때 자주 생기는 문제
예를 들어 할인 금액을 계산하는 함수가 있다고 해보자. 처음에는 정상 동작했다. 이후 요구사항이 추가되면서 계산 로직을 조금 수정했다. 수정 자체는 짧았지만, 그 과정에서 0원 처리나 잘못된 할인율 입력 같은 경계 조건이 깨질 수 있다.
이런 문제는 흔하다. 기능 하나를 수정하다가 예상하지 못한 곳이 깨지고, 배포 후에야 발견된다. 테스트가 없으면 결국 사람이 기억에 의존해서 다시 확인해야 한다. 코드가 커질수록 이 방식은 유지되기 어렵다.
아주 간단한 예제
예제는 TypeScript와 Vitest로 보겠다. 할인율을 적용해 최종 가격을 계산하는 함수 하나와, 그에 대한 테스트다.
함수 코드
export function calculateDiscountedPrice(price: number, discountRate: number): number {
if (price < 0) {
throw new Error('price must be zero or greater')
}
if (discountRate < 0 || discountRate > 100) {
throw new Error('discountRate must be between 0 and 100')
}
return price - (price * discountRate) / 100
}테스트 코드
import { describe, expect, it } from 'vitest'
import { calculateDiscountedPrice } from './calculateDiscountedPrice'
describe('calculateDiscountedPrice', () => {
it('할인율이 10%면 가격에서 10%를 뺀다', () => {
expect(calculateDiscountedPrice(10000, 10)).toBe(9000)
})
it('할인율이 0%면 원래 가격을 그대로 반환한다', () => {
expect(calculateDiscountedPrice(10000, 0)).toBe(10000)
})
it('잘못된 할인율이면 에러를 던진다', () => {
expect(() => calculateDiscountedPrice(10000, 120)).toThrowError()
})
})이 테스트에서 확인하는 포인트는 세 가지다.
- 정상 입력에서 기대한 계산 결과가 나오는지
- 경계 조건에서도 동작이 유지되는지
- 잘못된 입력을 허용하지 않는지
예제가 단순해도 테스트의 역할은 분명하다. 함수가 무엇을 보장해야 하는지 코드로 고정한다.
중요한 건 균형이다
테스트를 말할 때 모든 코드에 테스트를 붙여야 한다거나, 커버리지가 100%여야 의미가 있다는 식으로 흐르기 쉽다. 실무에서는 그렇게 접근하기 어렵다.
모든 코드를 같은 밀도로 테스트하면 작성 비용과 유지 비용이 함께 올라간다. 단순한 getter, 자주 바뀌는 UI 세부 구현, 프레임워크가 이미 보장하는 부분까지 전부 촘촘하게 테스트하는 건 효율이 떨어질 수 있다.
우선순위는 위험도가 높은 곳에 두는 편이 낫다. 조건 분기가 많은 함수, 돈 계산처럼 실수 비용이 큰 로직, 여러 곳에서 재사용되는 핵심 모듈, 이미 버그가 났던 부분부터 테스트를 붙이면 효과를 빨리 볼 수 있다.
AI agent를 같이 쓰는 팀이라면 여기에 한 가지를 더 보면 된다. agent가 자주 수정하는 영역이다. 반복적으로 건드리는 코드인데 테스트가 없다면, 그 구간이 가장 먼저 불안정해진다.
어떻게 시작하면 좋을까
처음부터 넓게 잡을 필요는 없다. 작은 단위부터 시작하면 된다.
- 조건 분기가 있는 함수 하나
- 이전에 버그가 났던 로직 하나
- 여러 곳에서 공통으로 쓰는 유틸 하나
- AI agent가 반복적으로 수정하는 핵심 모듈 하나
이 정도만 테스트로 고정해도 이후 수정이 훨씬 쉬워진다. 테스트 코드는 개발 과정에 추가되는 일이 하나 더 생기는 것이 맞다. 대신 수정과 확인에 들어가는 시간을 줄여준다.
작게 시작해서 계속 유지할 수 있는 수준으로 가져가는 편이 낫다.