기록이 힘이다.

[Clean Code] 8장 경계(+Junit) 본문

IT서적

[Clean Code] 8장 경계(+Junit)

dev22 2023. 5. 17. 08:28
728x90

경계 살피고 익히기

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.

 

JUnit은 가장 널리 사용되는 자바 학습 테스트 프레임워크입니다. JUnit을 사용하면 자바 코드를 검증하고 성능을 측정할 수 있습니다. 아래는 간단한 JUnit 학습 테스트 코드의 예시입니다.

import org.junit.Test;
import static org.junit.Assert.*;

public class CalculatorTest {
    @Test
    public void testAddition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(2, 3);
        assertEquals(5, result);
    }

    @Test
    public void testSubtraction() {
        Calculator calculator = new Calculator();
        int result = calculator.subtract(5, 3);
        assertEquals(2, result);
    }

    @Test
    public void testMultiplication() {
        Calculator calculator = new Calculator();
        int result = calculator.multiply(2, 3);
        assertEquals(6, result);
    }

    @Test
    public void testDivision() {
        Calculator calculator = new Calculator();
        int result = calculator.divide(10, 2);
        assertEquals(5, result);
    }
}

위 코드는 Calculator 클래스의 네 개의 테스트 함수를 정의하고 있습니다. 각 테스트 함수는 주어진 연산이 예상대로 동작하는지 검증합니다. 예를 들어, testAddition() 함수는 2 + 3이 5와 같은지 확인하고, 만약 결과가 다르다면 AssertionError를 발생시킵니다.

JUnit을 사용하여 학습 테스트 코드를 작성할 때는 다음과 같은 지침을 따르는 것이 좋습니다.

  1. @Test 어노테이션을 사용하여 각 테스트 함수를 정의합니다.
  2. 각 테스트 함수는 assertEquals() 또는 **assertTrue()**와 같은 단언문(assertion)을 사용하여 검증합니다.
  3. @Before 어노테이션을 사용하여 각 테스트 함수의 실행 전에 필요한 초기화를 수행합니다.
  4. @After 어노테이션을 사용하여 각 테스트 함수의 실행 후에 필요한 정리 작업을 수행합니다.
  5. 예외 상황을 검증하는 테스트도 포함해야 합니다. 예를 들어, 함수의 인수가 잘못된 경우나 파일이 존재하지 않는 경우 등의 상황을 검증해야 합니다.

JUnit 외에도 TestNG, Mockito, AssertJ 등 다른 자바 학습 테스트 프레임워크도 있습니다. 이 중에서도 JUnit은 가장 많이 사용되며, 많은 자료와 문서가 제공되고 있습니다.

log4j 익히기

public class LogTest {
    private Logger logger;
    
    @Before
    public void initialize() {
        logger = Logger.getLogger("logger");
        logger.removeAllAppenders();
        Logger.getRootLogger().removeAllAppenders();
    }
    
    @Test
    public void basicLogger() {
        BasicConfigurator.configure();
        logger.info("basicLogger");
    }
    
    @Test
    public void addAppenderWithStream() {
        logger.addAppender(new ConsoleAppender(
            new PatternLayout("%p %t %m%n"),
            ConsoleAppender.SYSTEM_OUT));
        logger.info("addAppenderWithStream");
    }
    
    @Test
    public void addAppenderWithoutStream() {
        logger.addAppender(new ConsoleAppender(
            new PatternLayout("%p %t %m%n")));
        logger.info("addAppenderWithoutStream");
    }
}

위 코드는 로그 기능을 검증하는 JUnit 학습 테스트 코드입니다.

 

Logger 클래스를 사용하여 로그를 작성하고, JUnit의 @Before 어노테이션을 사용하여 각 테스트 함수가 실행되기 전에 로그 설정을 초기화합니다. 그리고 @Test 어노테이션을 사용하여 세 개의 테스트 함수를 정의합니다.

basicLogger() 함수는 **BasicConfigurator**를 사용하여 로그 설정을 초기화하고, **logger.info()**를 사용하여 로그를 작성합니다.

addAppenderWithStream() 함수는 **ConsoleAppender**를 사용하여 로그를 출력하는 appender를 추가하고, **logger.info()**를 사용하여 로그를 작성합니다.

addAppenderWithoutStream() 함수는 **ConsoleAppender**를 사용하여 로그를 출력하는 appender를 추가하지만, appender에 출력할 스트림을 지정하지 않습니다. 따라서 appender는 로그를 출력하지 않게 됩니다.

 

위 코드에서는 JUnit과 Log4j 라이브러리를 사용하여 로그 기능을 검증하는 예시를 보여주고 있습니다. 자바에서 로그를 기록하는 라이브러리는 Log4j 외에도 java.util.logging, Logback, SLF4J 등 다양하게 있으며, 각 라이브러리마다 설정 방법과 사용 방법이 다릅니다. 하지만 로그를 작성하고 검증하는 방법은 대체로 비슷하므로, 위 예시 코드를 참고하여 다른 라이브러리를 사용해도 유사한 방식으로 학습 테스트 코드를 작성할 수 있습니다.

학습 테스트는 공짜 이상이다

학습 테스트에 드는 비용은 없다. 어쨌든 API를 배워야 하므로…. 오히려 필요한 지식만 확보하는 손쉬운 방법이다. 학습 테스트는 이해도를 높여주는 정확한 실험이다.

 

학습 테스트는 공짜 이상이다. 투자하는 노력보다 얻는 성과가 더 크다. 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다.

아직 존재하지 않는 코드를 사용하기

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다. 때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다.

 

(우리가 통제하지 못하며 정의되지도 않은) 송신기 API에서 CommunicationsController를 분리했다. 우리에게 필요한 인터페이스를 정의했으므로 CommunicationsController 코드는 깔끔하고 깨끗했다.

 

이와 같은 설계는 테스트도 아주 편하다. 적절한 FakeTransmitter 클래스를 사용하면 CommunicationsController 클래스를 테스트할 수 있다. Transmitter API 인터페이스가 나온 다음 경계 테스트 케이스를 생성해 우리가 API를 올바로 사용하는지 테스트할 수도 있다.

깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다. ex) 변경

소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.

경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.

 

Map에서 봤듯이, 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.