Eternity's Chit-Chat

aeternum.egloos.com



의존성 끊기와 단위 테스트 – 2부 Software Quality

작년 초에 함께 일하던 분이 다른 팀으로 전배를 가게 되어 그 분이 담당하고 있던 통계 서비스를 맡게 되었다. 하루 동안 수집된 로그 데이터를 매일 밤 배치로 처리하여 분석한 다양한 통계 결과를 데이터베이스에 저장하는 기능이었다. 대용량 데이터를 다루는 작업이기 때문에 통계 로그 데이터를 분산 파일 시스템인 HDFS(Hadoop Distributed File System)에 저장하며 대용량 분산 처리를 효율적으로 처리하기 위해 프레임워크로 Map-Reduce 기술을 기반으로 하는 Apache Hadoop을 사용하고 있었다.

코드를 처음 보았을 때 받은 첫 느낌은 코드 전체적으로 Apache Hadoop이라는 인프라스트럭처에 너무 단단하게 결합되어 있었다는 점이다. 따라서 Apache Hadoop이 실행되지 않는 분산 클러스터 외부에서는 특정 클래스의 인스턴스를 생성하거나 메소드를 실행하기가 불가능했다. 결과적으로 단위 테스트 작성이 거의 불가능한 수준이었으며 테스트 케이스가 작성되었다고 하더라도 인프라스트럭처에 의존성을 가지지 않는 유틸리티 성 클래스로 그 영역이 제한될 수 밖에 없었다.

통계 코드를 테스트하는 유일한 방법은 Apache Hadoop이 구동된 의사(pseudo) 분산 환경에 테스트용 로그 데이터를 저장한 후 통합 테스트를 수행하여 데이터베이스의 변경 사항을 체크하는 것뿐이었다. 이와 같은 환경에서는 앞서 살펴본 바와 같이 복잡한 실행 경로의 어떤 위치에서 오류가 발생했는지를 추적하기가 어렵고, 로그 파일을 읽고 데이터베이스에 저장해야 하므로 실행 시간이 오래 걸리며, 다양한 실행 경로를 테스트하기 위한 테스트 데이터를 준비하기가 쉽지 않기 때문에 높은 테스트 커버리지를 기대하기도 어렵다.

결국 코드 전반적으로 테스트 커버리지를 높이고 안정적인 코드 품질을 유지하기 위해서는 의존성 끊기 게임을 시작할 수 밖에 없었다.

<리스트 1>은 일간 통계 작업을 수행하는 실제 프로젝트 코드를 발췌한 것이다. 


<리스트 1> Apache Hadoop에 대해 강한 의존 관계를 가진 클래스
import org.apache.hadoop.mapred.JobClient;
import org.apache.hadoop.mapred.JobConf;
...
public class LogDailyAnalyzeJob {

  public
void analyze(String[] parameters) throws Exception {
    JobConf conf = new JobConf(LogDailyAnalyzeJob.class);
    ...
    conf.setJobName("LogDailyAnalyzer : " + parameters[0] + " for "
     
+ parameters[1]);
    conf.setMapperClass(BasicMapper.class); 
    conf.setCombinerClass(BasicCombiner.class);
    conf.setReducerClass(analyzer.getReducerClass());
    ...
    String year = parameters[1].substring(0,4);
    String month = parameters[1].substring(4,6);
    String day = parameters[1].substring(6);
    String path = "/"+year+"/"+month+"/"+day;

    conf.setInputPath(
new Path("/user/statlogs" + path));
    conf.setOutputPath(new Path("/user/result" + path + "/" + parameters[0]
      +
"/daily"));
    ...
    JobClient.runJob(conf);
  }
}

Apache Hadoop에 대한 사전 지식이 없다고 하더라도 전반적인 주제를 이해하는데 큰 무리는 없을 것이라고 생각된다. 여기에서 중요한 것은 JobConf, JobClient와 같이 테스트 하니스에서 생성하거나 실행하기 어려운 객체에 의존성을 가지고 있는 경우 의존성을 끊어야 한다는 것이다.

<리스트 1>의 코드에서 단위 테스트와 관련된 가장 큰 문제점은 위 코드가 Apache Hadoop에서 제공하는 JobConf와 JobClient에 직접적으로 의존한다는 점이다. JobConf와 JobClient는 Apache Hadoop 클러스터 환경이 정상적으로 실행 중이어야만 인스턴스 생성 및 실행이 가능하다. 따라서 단위 테스트를 작성하기 위해서는 먼저 테스트 대상 기능으로부터 JobConf와 JobClient에 대한 의존성을 끊어야 한다.

<그림 3> 단위 테스트 작성을 어렵게 만드는 인프라에 대한 의존성

의존성을 끊기 위해서는 현재 무엇을 테스트하고자 하는 지를 생각해야 한다. <리스트 1>에서 수행하고 있는 주된 작업은 Map-Reduce 작업을 수행할 분산 파일 시스템 상의 입력 파일과 출력 파일의 경로를 구하는 것이다. 따라서 입력 파일과 출력 파일의 경로를 구하는 로직을 인프라스트럭처에 대해 독립적으로 구성하면 된다.

우선 LogDailyAnalyzeJob의 인프라스트럭처에 대한 의존성을 제거하자. Apache Haddop에 대한 의존성을 끊는 가장 간단한 방법은 JobConf와 JobClient에 대한 의존성을 특정한 클래스 내부로 고립시키는 것이다. JobConf와 JobClient를 다루는 모든 코드를 별도의 인터페이스와 클래스로 추출하여 LogDailyAnalyzeJob이 오직 인터페이스에 대해서만 의존하도록 코드를 리팩토링한다.

<그림 4> 인프라에 대한 의존성을 클래스 내부로 고립시켜 의존성 끊기