“테스트 코드 없어도 잘 돌아가는데요?”
3년 동안 이렇게 말하고 다녔다. 시간 없다, 바쁘다, 이거 급하다. 핑계는 많았다.
그러다 터졌다.
그 날의 장애
2022년 9월 어느 금요일 오후 5시. 배포 직후였다.
[ERROR] TypeError: Cannot read property 'userId' of undefined
결제 API가 터졌다. 프론트엔드에서 넘어오는 데이터 구조가 바뀌었는데, 백엔드에서 확인을 안 한 거다.
“테스트 돌려보셨어요?” “아… 그게…”
테스트가 없었다. 정확히는, 작성하다 말다 한 테스트 파일들만 덩그러니 있었다.
왜 안 썼나
1. 시간이 없다는 착각
“기능 개발하기도 바쁜데 테스트까지?”
일정에 쫓기면 제일 먼저 삭제되는 게 테스트였다. 어차피 QA 팀에서 확인하잖아.
2. 뭘 테스트해야 하는지 모름
Controller를 테스트해? Service를? DB 연결도 해야 해?
용어도 어려웠다. Unit test, Integration test, E2E test… 다 다르다는데 뭐부터 해야 할지 감이 안 왔다.
3. 테스트 짜는 게 더 어려움
실제 로직은 10줄인데 테스트 코드가 50줄. mocking, stubbing, fixture… 처음 보는 개념들.
“이거 테스트 짜는 시간에 기능 하나 더 만들겠다”고 생각했다.

장애 이후 달라진 것
테스트 커버리지 80% 목표
팀 회의 후 결정됐다. 신규 코드는 무조건 테스트를 작성한다.
처음엔 귀찮았다. 근데 2주쯤 지나니까 습관이 됐다.
// 이제 이런 코드를 먼저 짠다
describe('PaymentService', () => {
describe('processPayment', () => {
it('정상 결제 시 영수증을 반환한다', async () => {
const result = await service.processPayment(mockPayment);
expect(result.receiptId).toBeDefined();
});
it('잔액 부족 시 InsufficientFundsError를 던진다', async () => {
await expect(
service.processPayment(insufficientPayment)
).rejects.toThrow(InsufficientFundsError);
});
});
});
리팩토링이 두렵지 않다
예전엔 “건드리면 터질까봐” 레거시 코드를 못 건드렸다.
테스트가 있으니까 자신있게 바꾼다. 테스트 통과하면 되는 거다.
버그가 줄었다
당연한 얘기지만 실감이 됐다. 특히 엣지 케이스.
it('amount가 0이면 에러를 던진다', () => {
expect(() => service.processPayment({ amount: 0 }))
.toThrow('Amount must be positive');
});
it('amount가 음수면 에러를 던진다', () => {
expect(() => service.processPayment({ amount: -100 }))
.toThrow('Amount must be positive');
});
테스트 케이스를 적다 보면 “어? 이 경우는 어떻게 처리하지?”가 보인다.

내가 정착한 테스트 전략
Unit Test: 빠르고 많이
비즈니스 로직이 담긴 Service 위주로 작성. DB 연결 없이 mock으로 처리.
// jest.mock으로 Repository 격리
const mockUserRepository = {
findOne: jest.fn(),
save: jest.fn(),
};
Integration Test: 핵심 플로우만
실제 DB 연결해서 테스트. 주요 시나리오 위주로.
describe('UserController (Integration)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
it('/users (POST) 회원가입 성공', () => {
return request(app.getHttpServer())
.post('/users')
.send({ email: 'test@test.com', password: '1234' })
.expect(201);
});
});
E2E Test: 최소한만
전체 플로우 테스트는 비용이 크다. 결제 같은 핵심 기능만.
깨달은 것들
테스트는 문서다
신규 입사자에게 “이 API 뭐 하는 거예요?”라고 물으면 테스트 파일을 보여준다.
어떤 입력에 어떤 출력이 나오는지, 어떤 에러가 발생하는지 다 적혀있다.
테스트하기 어려운 코드 = 설계가 잘못된 코드
“이거 어떻게 테스트해요?”라는 질문이 나오면 대부분 의존성이 꼬여있거나 함수가 너무 많은 일을 하고 있다.
테스트를 먼저 생각하면 자연스럽게 좋은 설계가 된다.
100% 커버리지는 목표가 아니다
처음엔 숫자에 집착했다. 90%, 95%, 100%…
지금은 “이 테스트가 진짜 의미 있나?”를 더 생각한다. 의미 없는 테스트 100개보다 핵심 로직 테스트 10개가 낫다.
결론
테스트 코드, 처음엔 귀찮다. 시간도 더 든다.
근데 장애 한 번 터지면 그 시간의 10배를 쓴다. 야근하고, 사과하고, 핫픽스 배포하고.
“시간 없어서 테스트 못 써요”가 아니라 “테스트 안 써서 시간이 없는 거”였다.
지금은 테스트 없이 PR 올리면 불안하다. 그게 정상인 것 같다.
관련 포스트가 4개 있어요.
Unity와 C++로만 읽히던 10년 된 크레인 PLC 프로토콜을, 옛 소스코드를 스펙 삼아 NestJS 게이트웨이로 다시 만든 기록 — 역공학부터 무중단 운영까지
문서 없는 산업용 프로토콜을 AI로 역공학했다
ChatGPT와 Copilot 시대에 개발자는 어떻게 일해야 하는가
AI 시대, 개발자로 살아남기
5년간 사이드 프로젝트를 하면서 배운 균형의 기술
사이드 프로젝트와 본업 사이에서
1인 개발에서 팀 개발로, 코드 리뷰 문화를 정착시킨 이야기