책 읽기

[자바 웹 프로그래밍 - Next Step] - 테스트와 리팩토링

지팡구 2022. 12. 27. 15:15

서비스를 개발하다 보면 우리는 구현한 기능 혹은 메소드를 테스트해야할 경우가 발생할텐데, 이는 개발자로서 중요한 순서이자 역량이다.

 

하지만 이를 접하지 않는다면 코드 구현력은 물론 더 큰 문제가 발생할 가능성이 높다. 그래서 테스트를 통해 내가 만든 기능들이 올바른 방향으로 흘러가는지에 관해 check 할 필요가 있는데, 이 과정 속에서 Test가 중요하다.

 

또한 내가 만든 메소드가 비효율적이라면 이는 추후 큰 비용의 발생을 야기할 수 있다 (내가 말하는 큰 비용이라하면 유지보수나, 사용되는 리소스를 의미한다.) 그래서 개발자 입장에선 불필요하거나, 책임에 맞지 않는 코드를 사용하는 것을 줄여야한다.

 

간단한 예제를 통해 문제점을 발견하고 직접 Test, refactoring까지  경험해 보았다.

 

public class Calculator {


	//Production Code ( 실제 서비스를 담당하는 부분 )
    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;}


	// Main (여기선 윗 프로덕션 코드가 바르게 동작하는지 확인하기 위한 Test Code)
    public static void main(String[] args) {
        Calculator calculator =new Calculator();
        System.out.println(calculator.add(1, 2));
        // 이하 생략
        }
    }

 

간단한 사칙연산을 구현한 코드인데,  여기서 확인할 수 있는 문제점은 다음과 같다.

 

프로덕션 코드와 테스트 코드가 같은 클래스에 위치하고 있다는 점

 

테스트 코드라하면 테스트 단계에서만 필요하다. 그래서 굳이 서비스 시점에서 같이 배포할 필요가 없는데, 이 문제를 해결하기 위해 프로덕션 코드와 테스트 코드를 분리할 수 있다.

 

public class CalculatorTest {
    public static void main(String[] args) {
        Calculator cal = new Calculator();
        System.out.println(cal.add(9, 3));
        // 이하는 생략
   	}
   }

윗 코드처럼 Test부와 프로덕션 코드를 분리할 수있는데, 여전히 main 메소드에서 프로덕션의 코드의 여러 메소드의 테스트를 동시에 진행한다는 점이 동일하다. 그래서 앞서 작성한 프로덕션의 코드의 복잡도가 증가할수록 결국 이 메인부도 복잡도가 증가할 수 밖에 없는 구조를 가지고 있다.

 

이 문제를 해결하기 위해선 테스트 코드를 각 메소드 별로 분리할 수 있다.

 

public static void main(String[] args) {
    Calculator cal = new Calculator();
    add(cal);
    multiply(cal);
    subtract(cal);
}

private static void add(Calculator cal) {
    System.out.println(cal.add(9, 3));
}

private static void multiply(Calculator cal) {
    System.out.println(cal.multiply(9, 3));
}

private static void subtract(Calculator cal){
    System.out.println(cal.subtract(3,5));
}

과연 윗 코드처럼 이렇게 분리함으로써 우리가 가진 근본적인 문제를 해결할 수 있을까?

 

솔직히 나는 잘 모르겠다. 나는 내가 구현하고있는 메서드에만 집중하고 싶은데 그렇지 못한다는 점에서 결국 이 또한 최종 해결책이 될 순 없다.   테스트 코드는 Calculator 클래스의 모든 메소드를 테스트 할 수 밖에 없다. ( 이 말이 와닿지 않음 )  그렇다고 주석처리를 한다? 이것 또한 바르지 못함.

 

그래서 이러한 문제점을 해결하기 위해 등장한 라이브러리가 바로 Junit이다.

이렇게 라이브러리를 추가하면 되고, 이로서 @Test 어노테이션을 사용할 수 있다.

 

lib 사용

아래 print문을 이용해 직접 값을 눈으로 확인할 수 있으며, 내가 기대한 결과가 맞는지 assertEquals 메소드를 이용해 확인할 수 있는데, @Test 어노테이션이 붙은 add() 메소드를 실행시켜보면 다음과 같다. 

 

Test 결과

 

이렇게 메소드에 대해 각 각 테스트를 진행할 수 있고, 테스트 메소드를 독립적으로 시행할 수 있어서 내가 구현한 프로덕션 코드의 메소드에만 집중이 가능하다. 또 다른 메소드에 영향을 받지 않는다는 장점이 있다. 하지만 위와 같이 메서드가 여러개 존재하면 중복성 코드가 발견될 것이다. 

 

중복성 코드

 

static으로 선언해버리기 새로운 객체 생성을..

그러나 문법상에선 문제가 없지만 Junit은 테스트를 실행하기 위해 초기화 작업을 윗 방식으로 구현하는 것을 추천하지 않는다.

 

Junit에서는 이렇게 초기화 하는 방식이 아닌 @Before ( @BeforeEach junit 버전에 따라 다름) ,@After (@AfterEach) 방식을 권장한다. 그 이유는 @Runwith, @Rule과 같은 어노테이션을 사용하려면 @Before내 위치해야 하며, 이 어노테이션을 사용해서 테스트 메소드에 대해 초기화 하는 작업을 하는 것이 추후 문제가 발생할 가능성을 없앨 수 있다.

 

 

public class CalculatorTest {
    private Calculator cal;
    @BeforeEach 
    public void setup() {
        cal = new Calculator();
        System.out.println("before");
    }
    @Test
    public void add() {
        assertEquals(9, cal.add(4, 5));
        System.out.println("add");
    }
    @Test
    public void subtract() {
        assertEquals(3, cal.subtract(6, 3));
        System.out.println("subtract");
    }

    @AfterEach
    public void teardown(){
        System.out.println("teardown");
    }
}

이렇게 코드를 작성할 수 있는데, 실행시켜보면 다음과 같다.

 

실행결과

 

해당 코드로 확인해본 결과로는 각 테스트 메서드가 실행될 때, @before과 @After을 거쳐간다는 점이다. 이렇게 매 메서드마다 전처리와 후처리를 통해서 테스트간 영향을 미치지 않도록 하는 것이 좋다.

(근데 궁금한점이 왜 여기서 subtract가 먼저 실행됐는지가 궁금하다.)

 

참고자료 : 

https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-execution-order