해당 글은 박재성님의 자바 웹 프로그래밍 Next Step의 2장 문자열 계산기 구현을 통한 테스트와 리팩토링의 내용을 정리한 내용입니다.
1. 내가 계산기 애플리케이션을 구현했어!
철수는 개발자다. 지난 날 계산기를 구현해달라는 영희의 요청을 받은 철수는 밤새 작업을 한 결과 사칙연산이 가능한 완벽한 계산기 애플리케이션을 구현했다. 철수의 계산기 애플리케이션의 코드는 다음과 같다.
public class Calculator {
int add(int i, int j) {
return i + j;
}
int subtract(int i, int j) {
return i - j;
}
int multiply(int i, int j) {
return i * j;
}
int divide(int i, int j) {
return i / j;
}
}
철수는 다음 날 해당 애플리케이션을 영희에게 가져다 주려다 문득 자신이 짠 코드가 정상적으로 동작하는 코드인지 의문을 갖는다. '나는 완벽을 추구하는 철두철미한 개발자인데, 내 로직에 결함이 있다는 것을 영희가 발견하면 나는 나 자신에게 당당한 개발자일 수 있을까?' 하고 철수는 생각한다. 철수는 곧 자신의 코드를 테스트 해야겠다는 생각을 한다. 그리고 자신이 사용하고 있는 IDE(Integrated Development Environment, 통합 개발 환경)인 Intellij 창에 psvm을 입력한다. (psvm는 main 메소드를 만드는 단축키이다.)
2. main 메소드 테스트로 테스트하기
철수는 위의 로직에 다음과 같은 테스트 코드를 입력한다.
public class Calculator {
...
public static void main(String[] args) {
Calculator c = new Calculator();
System.out.println(c.add(3, 4));
System.out.println(c.subtract(3, 4));
System.out.println(c.multiply(3, 4));
System.out.println(c.divide(3, 4));
}
}
'완벽한걸?' 하고 철수는 생각한다. 자신이 만든 테스트 코드에 감명한 철수는 호기롭게 손바닥을 비빈 후 메소드 실행 버튼을 눌러 메인 메소드를 실행한다. 그리고 아래에 나타난 테스트 결과를 확인한다.
'잠시만.. 3 + 4는 7이니까 첫 번째 결과는 맞고... 3 - 4는 -1이니까 두 번째 결과도 맞고... 3 x 4는 12니까..... 뭐 대충 다 맞겠지 뭐...'
콘솔창에 나타난 숫자들에 눈이 아파진 철수는 대충 테스트를 종료하고 애플리케이션을 영희에게 가져다 준다. 자신의 로직에 결함이 있었다는 사실을 알지 못한 채...
3. main 메소드 테스트의 문제점
사실 철수의 로직에는 한 가지 결함이 있었다. 그건 바로 3 나누기 4는 0이 아니라 0.75라는 점인데, 철수는 이를 알지 못한 채 자신의 로직을 영희에게 가져다 주었다. 만약 영희가 해당 로직의 문제점을 파악하지 못하고 그대로 계산기를 사용하게 될 경우 철수의 계산기 애플리케이션은 영희의 계산에 계속 왜곡된 결과를 제공할 것이다. 영희가 중요한 계산을 하는 경우라면(가령 어젯밤 친구들과 마신 술자리 가격을 사람 수대로 n분할 하는 계산이라거나..) 영희는 철수 계산기의 왜곡된 결과로 인해 잠재적인 손해 혹은 손실을 입을 것이다.
"아니 3 나누기 4가 0.75인걸 왜 몰라? 철수 바보 아니야?" 라고 생각할지도 모르겠다. 그러나 철수의 계산기 애플리케이션이 사실 상당히 복잡한 로직을 가진 애플리케이션이라고 생각을 해보자. 특정 동작을 하는데 필요한 코드의 수가 수십줄이 넘어가고 데이터가 여기에서 저기로, 또 다시 저기에서 여기로 넘어다니는 그런 애플리케이션 말이다. 만약 그런 애플리케이션에서 테스트를 구동했을 때, 테스트를 하는 시점에서 인간이 눈이 이를 완벽하게 분석하고 참/거짓을 판별할 수 있을까? 불가능하지는 않겠지만 쉽지 않을 것이다.
main 메소드로 테스트를 진행하는 부분에서 발생할 수 있는 문제점들은 다음과 같다.
- 테스트의 결과값을 신뢰할 수 없다. 위에서 언급한 사례처럼 테스트의 결과값을 사람이 직접 판단하기 때문에 얼마든지 실수의 여지가 발생한다. 실수의 여지를 줄이기 위해서는 사람이 두번, 세번 같은 테스트를 중복 검토하거나 매우 주의깊게 테스트 결과를 확인해 실수의 여지를 줄여야 한다. 그럼에도 불구하고, 테스트의 결과를 확신할 수 없기에 찜찜한 기분이 남는 것은 어쩔 수 없다.
- 테스트 코드가 복잡하다. 사실 철수의 경우 (철수에게는 미안한 말이지만) 아주 단순한 4개의 메소드를 테스트하기 때문에 테스트 코드 또한 그리 복잡하지는 않다. 그러나 만약 철수의 로직이 몇개의 클래스를 임포트해야 하는 등 복잡해지기 시작한다면 테스트 코드 내부에도 복잡한 로직들이 생기기 시작할 것이다. 테스트 코드는 단순히 테스트를 하기 위한 목적만은 갖기 때문에 테스트 코드를 읽고 해석하는데 품이 들어간다는 것 자체가 불필요한 작업이다.
- 단위테스트가 불가능하다. 철수의 테스트 코드는 main 메소드 내부에서 다수의 메소드들이 호출되는 구조를 가지고 있다. 즉, 한번 테스트를 구동할 때 main 메소드에 기술되어 있는 모든 메소드들이 일괄적으로 테스트되는 것이다. 만약 철수가 특정 메소드만을 테스트하고 싶을 경우, 테스트 코드를 일일히 읽으며 필요 없는 로직들을 제거하거나 주석 처리해야 한다. 이 또한, 2번의 경우처럼 불필요한 작업이다.
- 프로덕션 코드와 테스트 코드가 하나의 파일에 붙어 있다. 테스트 코드는 단순히 테스트를 하기 위함이다. 즉, 애플리케이션을 배포할 때는 필요가 없는 코드라는 것이다. 하지만 이와 같은 경우, 프로덕션 코드를 컴파일하게 되면 테스트 코드도 함께 컴파일되고 배포된다. 굳이 배포될 필요가 없는 코드가 배포될 수 있는 불필요함을 피할 수 없다.
3. 대안, JUnit
JUnit은 자바 생태계에서 작성된 자바 코드를 테스트 할 수 있도록 도와주는 테스트 라이브러리이다. 사실 JUnit 말고도 자바 진영에는 많은 테스트 라이브러리들이 존재하지만 JUnit은 자바 생태계에서 가장 많이, 가장 보편적으로 사용되는 라이브러리이고 러닝 커브가 낮아 사용하기 쉽다는 장점이 있다. 필요하다면 다른 테스트 라이브러리인 spock, testNG 등 어떠한 것을 이용해도 무방하다. 중요한 것은 해당 라이브러리들이 main 메소드를 통해 테스트를 진행할 때 발생할 수 있는 문제점들을 개선하고 보완할 수 있다는 부분이다.
앞서 설명한 부분처럼 main 메소드를 사용하여 테스트를 진행할 때는 많은 문제점들이 있다. JUnit을 사용하면 이 문제점들을 개선할 수 있다고 하였으니, 어떻게 이를 개선할 수 있는지 하나 하나 살펴보자.
3-1. 단위 테스트가 가능하다.
새로운 JUnit 테스트 클래스를 만들어 테스트 코드와 프로덕션을 분리한 다음 철수가 main 메소드를 작성해 완성했던 테스트 기능과 동일한 기능을 수행하는 테스트 코드를 만들어 보겠다. 결과는 아래의 코드와 같다.
import org.junit.jupiter.api.Test;
public class CalculateTest {
@Test
public void add() {
Calculator c = new Calculator();
System.out.println(c.add(3, 4));
}
@Test
public void subtract() {
Calculator c = new Calculator();
System.out.println(c.subtract(3, 4));
}
@Test
public void multiply() {
Calculator c = new Calculator();
System.out.println(c.multiply(3, 4));
}
@Test
public void divide() {
Calculator c = new Calculator();
System.out.println(c.divide(3, 4));
}
}
하나의 테스트 클래스 내부에 별도의 메소드로 각각의 테스트를 분리한 것을 확인할 수 있다. main 메소드를 사용했다면 main 메소드 내부에 뭉쳐 들어가 있던 4개의 테스트 코드들이 각각의 메소드에 쪼개져 들어간 것이다. 자바의 메소드는 독립적으로 실행이 가능하기 때문에 철수는 이제 원한다면 테스트 코드 내에서 특정 코드들을 제거하거나 주석처리하는 작업 없이, 특정 동작 혹은 기능만을 선별적으로 테스트할 수 있다. 간단하게 테스트 하고자 하는 메소드만 호출하면 되는 것이다.
3-2. 테스트 코드의 가독성을 올릴 수 있다.
위의 3-1과 연관되어 각각의 테스트 코드들이 각자 하나씩 메소드로 떨어져 들어가기 때문에 main 메소드를 사용했을 때보다 가독성이 증가했음을 알 수 있다. 또한 각각의 메소드에 add, subtract, multiply, divide와 같이 네이밍을 함으로써 각각의 테스트 코드들이 어떠한 동작을 하는 코드들인지 보다 가시적으로 확인하는 것이 가능하다. 이런 방식의 테스트 코드가 작성된다면, 비지니스 로직이 복잡해지더라도 상당한 수준의 가독성을 확보할 수 있을 것이다.
3-3. 테스트 코드를 자동화할 수 있다.
앞서 main 메소드를 통해 작성한 테스트 코드의 가장 큰 문제점 중 하나는 테스트의 결과를 신뢰할 수 없다는 부분이었을 것이다. 영희에게 가져다 준 애플리케이션에는 어쩌면 치명적인 결함이 존재할 수 있었을 지 모르는데, 개발을 진행한 철수는 모든 테스트 결과를 자신의 눈으로 판단하니 자신의 코드에 결함이 있다는 사실을 인지하기 어렵다. 그리고 자신이 작성한 코드의 동작을 전적으로 확신할 수 없기에 이상하게 왠지 모르는 찜찜함이 여기저기 스믈스믈 올라온다.
JUnit은 이러한 철수의 심적 안정을 도와주고자 코드를 자동화하여 관리할 수 있는 시스템을 제공한다. true와 false를 라이브러리 내에서 사람이 보기 편한 형태로 보여주기 때문에 개발자는 훨씬 더 손쉽게 코드 상에 존재하는 결함들을 파악할 수 있다. 아래의 코드를 통해 자동화된 테스트와 그의 결과를 확인해보자.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class CalculateTest {
@Test
public void add() {
Calculator c = new Calculator();
Assertions.assertEquals(7, c.add(3, 4));
}
@Test
public void subtract() {
Calculator c = new Calculator();
Assertions.assertEquals(-1, c.subtract(3, 4));
}
@Test
public void multiply() {
Calculator c = new Calculator();
Assertions.assertEquals(12, c.multiply(3, 4));
}
@Test
public void divide() {
Calculator c = new Calculator();
Assertions.assertEquals(0.75, c.divide(3, 4));
}
}
위에 작성된 자동화된 테스트를 구동했을 때 나오는 결과값을 확인해보면 main 메소드를 이용해 콘솔창에서 결과를 확인한 것보다 훨씬 가시적으로 테스트 결과를 확인할 수 있음을 알 수 있다. main 메소드를 사용했다면 콘솔창에 하나하나 나왔을 데이터 값들이, true와 false만으로 간략화되어 메소드명과 함께 출력되기 때문이다. (main 메소드에서도 true와 false를 하나 하나 보여주는 과정이 불가능한 것은 아니지만 구현의 품이 들어가고, 무엇보다 그런 기능들을 하나 하나 추가하면 main 메소드가 비대해지며 가독성이 떨어진다.)
철수의 코드는 나누기 기능에서 분명한 문제점이 있었다. 3 나누기 4는 0.75의 결과값이 나와야 하는데 철수의 계산기는 0을 출력했기 때문이다. JUnit은 이 결과값을 위의 사진처럼 가시적으로 보여준다. 그리고 이러한 일목요연한 테스트 결과는 개발자가 자신의 테스트 결과를 신뢰하는데 많은 도움을 준다. (심지어 아래의 사진처럼 시스템은 나오기를 기대한 값 0.75와 실제 값 0 사이에 이격이 있다는 부분까지 보여준다!)
'JAVA' 카테고리의 다른 글
[자바 웹 프로그래밍 Next Step 실습] HTTP 웹 서버 구현하며 적용 사항 기록하기 (0) | 2023.03.13 |
---|---|
java 리플렉션 적용해보기 (2) | 2022.11.25 |
[디자인 패턴 - 생성 패턴] 팩토리 메소드 패턴 (0) | 2022.11.03 |
[JPA] 연관관계 매핑 기초 (0) | 2022.05.21 |
[번역] cron4j quickstart (0) | 2022.04.01 |