테스트 주도로 하둡 맵리듀스 프로그래밍하기
(MapReduce Programming Driven by Test)


1. 개요
이 글에서는 테스트 주도로 하둡 맵리듀스 프로그램을 개발하는 방법을 설명합니다. 개발할 문제 영역은 하둡 맵리듀스 튜토리얼에서 다루고 있는 "Word Count" 예제입니다.

1.1 준비사항
  • 하둡 설치
    로컬 머신에서 맵리듀스 프로그램을 개발하고 테스트하는 방법을 설명하므로, 로컬 머신에 하둡이 설치되어 있어야 합니다. 하둡 클러스터에서 테스트하기 전에 먼저 로컬에서 테스트하는 방법이므로, 로컬 머신에는 하둡이 독립 모드 또는 의사 분산 모드로 설치되어 있어야 합니다. 하둡을 의사 분산 모드로 설치하는 방법을 보시려면 여기를 참고하시기 바랍니다.
  • jUnit 4와 모키토(Mockito)
    또한 테스팅 프레임워크로는 jUnit 4와 모키토 1.8.5를 사용합니다. 모키로 사용법을 모르시는 분은 여기를 참고하시기 바랍니다.
  • 메이븐(Maven)
    그리고 프로젝트는 메이븐을 사용해 생성합니다. 다시 말해 메이븐을 사용해서 의존성이 있는 라이브러리를 관리하고, 프로젝트를 구성합니다. 메이븐에 대해서는 아파치 메이븐 사이트를 참고하세요. 여기에서는 메이븐의 특수한 기능을 사용하지 않으므로 알지 못하더라도 진행하는데 큰 어려움이 없을리라고 봅니다.
  • 이클립스와 m2eclipse 플러그인
    마지막으로, 이클립스를 IDE로 사용하며 여기에서는 인디고(Indigo) 버전을 사용합니다. 그리고 메이븐 플러그인을 설치해두어야 합니다. 여기에서는 m2eclipse(http://download.eclipse.org/technology/m2e/releases) 플러그인을 설치했습니다.


2. 메이븐 프로젝트 구성하기
2.1 메이븐 프로젝트 생성하기
먼저 메이븐 프로젝트를 생성합니다. 이클립스에서 메이븐 프로젝트를 생성하려면 반드시 메이븐 플러그인이 먼저 설치되어 있어야 합니다. 아래와 같이 메이븐 프로젝트를 생성할 수 있습니다. [Maven Project]를 선택한 후, [Next]를 클릭합니다.


"New Maven Project' 창에서 기본 설정 그대로 둔 채, [Next] 버튼을 클릭합니다.


메이븐 프로젝트 아케타입(Archetype) 중에서, "maven-archetype-quickstart"를 선택한 후, [Next]를 클릭합니다.


"Group Id"와 "Artifcat Id"를 입력합니다. 여기에서는 Group Id로 "com.socurites"를 Artifact Id로는 "example.hadoop.wordcount"를 입력햇습니다. 이렇게 입력하면 자동으로 패키지(Package) 명이 구성됩니다. 패키지 명은 [Group Id].[Artifcat Id]로 구성됩니다. 또한 Artifcat Id는 이클립스에서 생성될 프로젝트 명이 됩니다. 이제 [Finish]를 클릭합니다.


이 작업이 끝나면 이클립스에 워크스페이스에 "example.hadoop.wordcount"라는 이름의 프로젝트가 생성됩니다. 여기에서 자동으로 생성된 App.java와 AppTest.java는 필요 없으므로 삭제합니다.


2.2 의존성 설정하기
이제 프로젝트의 라이브러리 의존성을 설정합니다. 여기에서 사용할 라이브러리는 총 3개로 다음과 같습니다.
  • jUnit 4.1
    단위 테스트를 만들 때 사용할 라이브러리로, 테스트 목적으로만 사용합니다.
  • mockito 1.8.5
    Mock 객체를 만들 때 사용할 라이브러리로, 테스트 목적으로만 사용합니다.
  • Hadoop 0.20.2
    MapReduce 프로그램을 개발할 때 사용할 라이브러리로, 컴파일할 목적으로만 사용합니다.
이제 pom.xml 파일을 열어, [Dependencies] 탭으로 이동해서 의존성을 설정합니다.
먼저 jUnit 4.1을 추가해 봅시다. 기본적으로 jUnit이 3.8.1이 등록되어 있으므로 [Remove] 버튼을 클릭해서 삭제합니다.
이제 [Add...] 버튼을 클릭합니다. "junit"을 검색어로 입력하면 많은 수의 라이브러리가 나오는데 여기에서 아래 그림과 같이 junit의 "4.10 [jar]"을 선택합니다. 그리고 jUnit은 테스트 단계에서만 사용할 것이므로 Scope를 "test"로 설정합니다. [OK] 버튼을 클릭합니다.


마찬가지로 mockito 1.8.5와 hadoop-core 0.20.2를 추가합니다. 이때 hadoop-core는 Scope를 "compile"로 설정해서 추가해야 합니다.


이제 맵리듀스 프로그램을 개발할 준비가 끝났습니다.


3. Mapper 개발하기
3.1 Mapper 테스트 케이스 만들기
먼저 맵 함수에 대한 테스트 케이스를 생성하고, 실패하도록 만들어야 합니다.
"src/test/java"의 com.socurites.example.hadoop.wordcount.mapper 패키지에 WordCountMapperTest라는 이름으로 매퍼에 대한 테스크 케이스를 생성합니다.



WordCountMapperTest는 아래와 같이 작성합니다.
package com.socurites.example.hadoop.wordcount.mapper;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper.Context;
import org.junit.Test;
import com.socurites.example.hadoop.wordcount.mapper.WordCountMapper;

public class WordCountMapperTest {
    @Test
    public void processValidRecord() throws IOException, InterruptedException {
        WordCountMapper mapper = new WordCountMapper();
       
        LongWritable key = null;
        Text value = new Text("2005년 12월 8일 야후! 코리아의 제안으로 Yahoo! Answers 서비스 개시");
        Context context = mock(Context.class);
       
        mapper.map(key, value, context);
       
        verify(context, times(1)).write(new Text("2005년"), new IntWritable(1));
    }
}

코드에서 보는 바와 같이 Context 클래스에 대해 Mock 객체를 생성한 후, map 함수를 호출합니다. 그리고, context 객체에서 wirte 함수가 1번 호출되고, 이때 "2005년"을 키로 가지는 값이 1이었는지 검증합니다.

이렇게 테스트케이스를 작성하고 나면 컴파일이 되지 않습니다. 이제 테스트 케이스가 컴파일 되도록 맏들어야 합니다.

3.2 Mapper 테스트 컴파일되도록 만들기
이제 Mapper 테스트가 컴파일 되도록 만들어야 합니다. WordCountMapper 클래스를 "src/main/java"의 "com.socurites.example.hadoop.wordcount.mapper" 패키지에 생성한 후, map 메서드를 구현합니다. 지금 당장은 아래와 같이 비어있는 상태로 만들어 둡니다.
package com.socurites.example.hadoop.wordcount.mapper;

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class WordCountMapper extends
        Mapper<LongWritable, Text, Text, IntWritable> {

    @Override
    public void map(LongWritable key, Text value,
            Context context)
            throws IOException, InterruptedException {
    }
}

테스트케이스를 실행해보면 아래와 같이 빨간불이 뜹니다. 즉 테스트가 실패합니다.


보다시피 context.write(2005년, 1)이 한번 호출되어야 하나, 호출되지 않았음을 확인할 수 있습니다. 이제 테스트를 성공하도록 만들어야 합니다.

3.3. Mapper 테스트 녹색불 뜨도록 만들기
WordCountMapper 클래스의 map 함수를 아래와 같이 구현합니다.
package com.socurites.example.hadoop.wordcount.mapper;

import java.io.IOException;
import java.util.StringTokenizer;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class WordCountMapper extends
        Mapper<LongWritable, Text, Text, IntWritable> {
    private final IntWritable one = new IntWritable(1);

    @Override
    public void map(LongWritable key, Text value,
            Context context)
            throws IOException, InterruptedException {
        StringTokenizer tokenizer = new StringTokenizer(value.toString());
       
        while ( tokenizer.hasMoreTokens() ) {
            context.write(new Text(tokenizer.nextToken()), one);
        }
    }
}

이렇게 구현한 후, 테스트를 실행하면 녹색불이 뜨며 테스트가 성공합니다. 이렇게 해서 Mapper 구현이 끝납니다.


4. Reducer 개발하기
4.1 Reducer 테스트 케이스 만들기
이제 리듀스 함수에 대한 테스트 케이스를 생성하고, 실패하도록 만들어야 합니다.
"src/test/java"의 com.socurites.example.hadoop.wordcount.reducer 패키지에 WordCountReducerTest라는 이름으로 리듀서에 대한 테스크 케이스를 생성합니다.
package com.socurites.example.hadoop.wordcount.reducer;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.util.Arrays;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer.Context;
import org.junit.Test;
import com.socurites.example.hadoop.wordcount.reducer.WordCountReducer;

public class WordCountReducerTest {
    @Test
    public void processValidRecord() throws IOException, InterruptedException {
        WordCountReducer reducer = new WordCountReducer();
       
        Text key = new Text("2005년");
        Iterable<IntWritable> values = Arrays.asList(new IntWritable(1), new IntWritable(1));
        Context context = mock(Context.class);
       
        reducer.reduce(key, values, context);
       
        verify(context).write(key, new IntWritable(2));
    }
}

보다시피 테스트에서는 "2005"년을 키로 하여, 리듀스한 값이 2가 되도록 구성합니다. 매퍼 테스트와 마찬가지로 테스트케이스를 작성하고 나면 컴파일이 되지 않습니다. 이제 테스트 케이스가 컴파일 되도록 맏들어야 합니다.

4.2 Reducer 테스트 컴파일 되도록 만들기
 WordCountReducer 클래스를 "src/main/java"의 "com.socurites.example.hadoop.wordcount.reducer" 패키지에 생성한 후, reduce 메서드를 구현합니다. 지금 당장은 아래와 같이 비어있는 상태로 만들어 둡니다.
package com.socurites.example.hadoop.wordcount.reducer;

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values,
            Context context)
            throws IOException, InterruptedException {
    }   
}

테스트케이스를 실행해보면 아래와 같이 빨간불이 뜹니다. 즉 테스트가 실패합니다.


보는 바와 같이 "2005년"을 키로 값이 2가 되도록 context.write()가 호출되어야 하지만 호출되지 않았음을 알 수 있습니다.

4.3 Reducer 테스트 녹색불 뜨도록 만들기
이제 Reducer 테스트가 성공하도록 reduce 메서드를 아래와 같이 구현합니다.
package com.socurites.example.hadoop.wordcount.reducer;

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values,
            Context context)
            throws IOException, InterruptedException {
        int sum = 0;
        for ( IntWritable value : values ) {
            sum = sum + value.get();
        }
       
        context.write(key, new IntWritable(sum));
    }
}

이렇게 구현한 후, 테스트를 실행하면 녹색불이 뜨며 테스트가 성공합니다. 이렇게 해서 Reducer 구현이 끝납니다.

이처럼 맵과 리듀스 함수에 대한 단위 테스트가 끝나면 하둡 클러스터에 맵리듀스 프로그램을 배포할 수 있는 수준이 됩니다. 하지만 그전에 단순한 테스트 값이 아니라, 실제 테스트 데이터 중 작은 량을 대상으로 로컬 머신에서 맵리듀스 프로그램을 테스트해 봐야 합니다.


5. Driver 만들기
맵리듀스 잡을 실제로 수행할 Driver 클래스를 만들어야 합니다. 로컬 머신에서 테스트할 수 있도록 Configured를 상속받고, Tool 인터페이스를 구현합니다. WordCountDriver 클래스를 "src/main/java"의 "com.socurites.example.hadoop.wordcount.driver" 패키지에 생성한 후, 아래와 같이 구현합니다.
package com.socurites.example.hadoop.wordcount.driver;

import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import com.socurites.example.hadoop.wordcount.mapper.WordCountMapper;
import com.socurites.example.hadoop.wordcount.reducer.WordCountReducer;

public class WordCountDriver extends Configured implements Tool {
    public int run(String[] args) throws Exception {
        // Job생성하기
        Job job = new Job(this.getConf(), "Word Count");
        job.setJarByClass(this.getClass());
               
        // Mapper 설정하기
        job.setMapperClass(WordCountMapper.class);
        // Reducer 설정하기
        job.setReducerClass(WordCountReducer.class);
       
        // 입력 경로 설정하기
        FileInputFormat.addInputPath(job, new Path(args[0]));
        // 출력 경로 설정하기
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
       
        // 입력 포맷 설정하기
        job.setInputFormatClass(TextInputFormat.class);
       
        // 출력 포맷 설정하기
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);
       
        job.waitForCompletion(true);
       
        return 0;
    }
   
    public static void main(String[] args) throws Exception {
        int exitCode = 0;
        if ( !hasEnoughArguments(args) ) {
            System.err.println("사용법: WordCountDriver [generic options] <input> <output>");
            ToolRunner.printGenericCommandUsage(System.err);
            exitCode = -1;
        } else {
            exitCode = ToolRunner.run(new WordCountDriver(), args);
        }
       
        System.exit(exitCode);
    }
   
    private static boolean hasEnoughArguments(String[] consoleArgs) {
        if ( consoleArgs.length == 2 ) {
            return true;
        }
       
        return false;
    }
}


6. 실행하기
6.1 입력 데이터 준비하기
먼저 입력 데이터를 준비합니다. 실제로 사용할 데이터에서 일부부분을 파일로 만듭니다. 여기에서는 "src/main/resources"에 "testdata.txt"라는 이름으로 파일을 생성했습니다.



테스트에서 사용한 testdata.txt 파일의 내용은 다음과 같습니다.

더보기


6.2 로컬 머신을 위한 설정파일 만들기
로컬 머신에서 테스트하는 경우 하둡 파일 시스템이 아니라 일반적인 파일 시스템을 사용하고자 한다. 그리고 맵리듀스 프로그램도 로멀 머신에서 실행하고자 한다. 이를 위한 설정 파일을 "hadoop-local.xml"이라는 이름으로 만든다. 설정 내용은 다음과 같다.
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<configuration>
  <property>
    <name>fs.default.name</name>
    <value>file:///</value>
  </property>

  <property>
    <name>mapred.job.tracker</name>
    <value>local</value>
  </property>
</configuration>

6.3 로컬 머신에서 실행하기
이제 쉘 프롬프트로 이동해서, 이클립스 워크스페이스로 이동한 후, 컴파일된 클래스 파일이 있는 최상위 디렉토리로 이동합니다.
cd [eclipse workspace]/example.hadoop.workcount/target/classes

하둡의 CLASS_PATH를 현재 디렉토리로 설정합니다.
export HADOOP_CLASSPATH=.

아래와 같이 하둡 명령어를 실행합니다.
hadoop com.socurites.example.hadoop.wordcount.driver.WordCountDriver -conf [hadoop-local.xml 파일이 있는 디렉토리]/hadoop-local.xml ./testdata.txt output

이 명령어를 실행하면 맵 리듀스 프로그램을 외부 잡트래커를 액세스 하지 않고 내부 프로세스로 실행한다. 그리고 ./testdata.txt 파일을 파일시스템에서 입력으로 읽어서, 그 결과를 output 디렉토리에 저장합니다. 이때 output 디렉토리가 있는 경우 데이터를 덮어쓰는 경우를 방지하기 위해 예외가 발생하므로, 존재하지 않는 디렉토리를 출력 디렉토리로 지정해야 합니다.

실제로 디렉토리를 보면 output 디렉토리가 생겻으며, 그 안에 part-r-0000 파일이 생겼음을 확인할 수 있습니다. 그리고 part-r-0000 파일을 열어보면 맵리듀스 프로그램의 결과가 제대로 생성되었음을 확인할 수 있습니다.


[부록 A: 소스 프로젝트]
개발한 소스 프로젝트를 아래에서 다운로드 받으신 후, 이클립스에서 임포트하시기 바랍니다.

wordcount.zip


이때 이클립스에 메이븐 플러그인이 설치되어 있어야만 의존성이 있는 라이브러리를 업데이트 받을 수 있습니다.

[참고자료]
  • 클라우드 컴퓨팅 구현 기술 / 에이콘 / 김형준, 조준호, 안성화, 김병준 지음
  • 하둡 완벽 가이드 / 한빛 미디어 / 심탁길, 김우현 옮김
  • MapReduce Tutorial


Posted by socurites