기능 개발을 마치고 로컬 서버를 띄운 뒤, Postman으로 API를 호출해 DB에 정상적으로 저장되는지 확인한다. 콘솔에 출력된 쿼리와 로그를 눈으로 훑고 나서야 PR(Pull Request)을 올린다. 실무를 경험하며 누구나 거쳐왔을 익숙한 흐름이다.
하지만 서비스 규모가 커지고 비즈니스 로직이 복잡해지면, 이러한 수동 테스트는 명확한 한계를 드러낸다. 첫 번째로 짚고 넘어가야 할 부분은, 왜 비용을 들여가며 테스트 코드를 작성해야 하는가에 대한 근본적인 이유다.
눈으로 확인하는 수동 테스트의 함정
수동 테스트는 실행 비용이 높고 반복하기 엉렵다. 특정 비즈니스 로직의 분기문이 10개라면, 모든 엣지 케이스를 검증하기 위해 서버를 재시작하고 파라미터를 바꿔가며 10번의 API 호출을 수행해야 한다.
- 휴먼 에러의 발생 : 사람이 직접 눈으로 결과값을 검증하므로, 피로도나 부주의에 따라 놓치는 엣지 케이스가 반드시 발생한다.
- 낮은 커버리지와 회귀 버그 : 새로운 기능을 추가했을 때, 기존에 잘 동작하던 기능이 망가지지 않았는지 확인하는 회귀 테스트(Regression Test)를 전체 시스템 대상으로 매번 수동 수행하는 것은 불가능하다.
[Bad Code] System.out.println을 활용한 수동 검증
public class CalculatorTest {
public static void main(String[] args) {
Calculator calc = new Calculator();
int result = calc.add(10, 20);
// 개발자가 콘솔을 눈으로 직접 확인해야 하며, CI/CD 파이프라인에서 자동화될 수 없다.
if (result == 30) {
System.out.println("덧셈 테스트 성공");
} else {
System.out.println("덧셈 테스트 실패");
}
}
}
위와 같은 main 메서드 기반의 검증 방식은 프로젝트의 크기가 커질수록 방치되어 결국 죽은 코드가 된다.
[Good Code] JUnit 5와 AssertJ를 활용한 자동화된 검증
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CalculatorTest {
@Test
@DisplayName("두 정수를 더하면 정확한 합이 반환되어야 한다")
void add_ShouldReturnSum_WhenGivenTwoIntegers() {
// given
Calculator calc = new Calculator();
// when
int result = calc.add(10, 20);
// then
// 단언(Assert)을 통해 기계가 성공 여부를 즉시 판별한다.
assertThat(result).isEqualTo(30);
}
}
단위 테스트 프레임워크를 사용하면 검증 로직이 시스템적으로 자동화되어 수천 개의 테스트도 단 몇 초 만에 끝낼 수 있다.
개발자의 책임과 심리적 안정감
테스트 코드를 작성하는 것은 단순히 버그를 찾는 행위를 넘어, 자신이 작성한 코드가 의도한 대로 동작함을 증명하는 개발자의 초소한의 책임이다.
실무에서 레거시 시스템을 개편하거나 라이브러리 버전을 올릴 때 가장 두려운 것은 예상치 못한 사이드 이펙트다. 촘촘하게 짜여진 테스트 코드는 이러한 변경 작업 시 든든한 안전망 역할을 한다. 코드를 리팩토링한 후 테스트 셋을 모두 돌려 통과(Green) 하는 것을 확인했을 때 얻는 심리적 안정감은 개발 속도를 비약적으로 끌어올리고 코드 품질에 대한 확신을 준다.
좋은 테스트 코드는 좋은 설계를 유도한다
단위 테스트를 작성하다 보면 유독 테스트하기 어려운 코드를 만나게 된다. 외부 API나 DB에 강하게 결합되어 있거나, 하나의 메서드가 너무 많은 책임(SRP 위배)을 가지고 있는 경우가 대표적이다.
즉, 테스트 코드는 객체지향 설계의 지표가 된다. 단위 테스트 작성이 고통스럽다면 테스트 프레임워크의 문제가 아니라, 프로덕션 코드의 결합도가 지나치게 높거나 의존성 주입(DI)이 제대로 이루어지지 않았다는 설계쌍의 경고음으로 받아들여야 한다.
결론
수동 테스트에 의존하는 개발 프로세스는 결국 기술 부채를 누적시킨다. 시간 부족을 이유로 테스트 코드 작성을 미루는 것은, 향후 디버깅과 장애 처리에 훨씬 더 많은 시간을 지불하겠다는 의미와 같다.
단위 테스트는 초기의 학습 비용과 작성 시간이 수반되지만, 장기적으로는 코드의 품질을 보장하고 유지보수 비용을 획기적으로 낮추는 가장 확실한 투자다. 다음 2편에서는 기존 레거시 시스템에서 많이 사용되던 JUnit 4와 현재 실무 표준인 JUnit 5의 아키텍처 차이, 그리고 마이그레이션 포인트를 살펴본다.
'Software Engineering' 카테고리의 다른 글
| [Software Engineering] 단위 테스트 도입과 JaCoCo 커버리지 게이트 구축 (0) | 2026.08.24 |
|---|---|
| [Software Engineering] GitLab Runner 구축과 Smoke Build 파이프라인 설계 (0) | 2026.08.20 |
| [Software Enginnering] 수동 배포의 한계와 CI/CD 파이프라인의 엔지니어링 가치 (0) | 2026.08.19 |
| [Software Enginnering] 애자일(Agile) 백엔드 아키텍처: 변화를 수용하는 진화형 설계 (1) | 2026.05.27 |
| [Software Enginnering] 애자일(Agile) 빠른 피드백의 기술: CI/CD 파이프라인과 자동화 (0) | 2026.05.22 |
