Spring

Spring 4.0 연동 메모 모음 — JUnit · XML View · RabbitMQ · Redis · Batch · SiteMesh · Hadoop

목차

2015년 한 해 동안 Spring 4.0 프로젝트에 이것저것 붙여보면서 여덟 번에 나눠 올렸던 설정 메모를 한 페이지로 합쳤다. 하나하나는 토막글이라 따로 두면 찾기가 더 힘들었다.

순서는 시간순이 아니라 쓸모순이다. 테스트 환경이 먼저 있어야 나머지를 확인할 수 있어서 JUnit 을 앞에 뒀다.

버전은 전부 2015년 기준이다. 지금 그대로 쓸 물건은 아니고, 그때 뭘 어떻게 붙였는지의 기록이다.

JUnit 테스트케이스

나머지 연동을 확인하려면 이게 먼저 있어야 한다.

1
2
3
4
5
6
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>4.0.5.RELEASE</version>
   <scope>test</scope>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( {
  "classpath:servlet-context.xml",
  "classpath:config/context-datasource.xml"
}
)
public class MemberServiceTest {

    @Autowired
    MemberService memberService;


    @Test
    public void testSr2002() throws Exception {
        RequestData req = new RequestData(null, new DbMap());
        ResponseData res = new ResponseData(new DbMap());

        memberService.sr2002(req, res);
    }

}

DB 를 jndi-lookup 으로 받는 경우가 문제다. 테스트에서는 JNDI 가 없다. test/resources/config/context-datasource.xml 을 넣어서 기존 id 를 덮어버렸다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource"
          p:driverClassName="com.mysql.jdbc.Driver"
          p:url="jdbc:mysql://server:3306/dbname"
          p:username="sa"
          p:password="" />


</beans>

같은 id 로 나중에 로드되는 쪽이 이긴다. 이 패턴은 아래 연동 테스트에서 계속 쓴다.

Map 을 XML 로 뱉는 ViewResolver

항상 JSON 으로만 뱉다가 XML 로 뱉어야 하는 상황이 생겨서 만든 view 클래스다. 결과가 Map 인 경우에만 해당된다.

applicationServlet.xml

1
2
3
4
    <beans:bean id="xmlView2" class="org.springframework.web.servlet.view.XmlViewResolver">
        <beans:property name="order" value="1"/>
        <beans:property name="location" value="classpath:xml-views.xml"/>
    </beans:bean>

xml-views.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
        "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean name="xmlView"  class="com.xxxxx.view.AjaxResponseXMLView">
        <property name="contentType">
            <value>text/xml;charset=utf-8</value>
        </property>
    </bean>
</beans>

AjaxResponseXMLView.java

Map 과 List 를 재귀로 훑으면서 태그를 만든다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class AjaxResponseXMLView extends AbstractView {

    @Override
    protected void renderMergedOutputModel(Map map, HttpServletRequest request,
                                           HttpServletResponse response) throws Exception {
        String xmlHeader = "\r\n";

        StringBuffer xmlSb = new StringBuffer();
        xmlSb.append(xmlHeader);
        xmlSb.append("");
        writeFromMap(xmlSb, map);
        xmlSb.append("");

        response.setContentType("application/xml");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Cache-Control", "no-cache");
        response.setContentLength(xmlSb.toString().getBytes("utf-8").length);
        response.getWriter().print(xmlSb.toString());
    }

    private void writeFromMap(StringBuffer sb, Map map) {

        for(Object str : map.keySet()) {
            Object v = map.get(str);
            sb.append("<" + str + ">");
            if(v instanceof Map) {
                writeFromMap(sb, (Map) v);
            }
            else if(v instanceof List) {
                writeFromList(sb, (List) v);
            }
            else {
                writeFromData(sb, v);
            }
            sb.append("</" + str + ">");
        }
    }

    private void writeFromList(StringBuffer sb, List list) {

        for(Object v : list) {
            sb.append("");
            if(v instanceof Map) {
                writeFromMap(sb, (Map)v);
            }
            else if(v instanceof List) {
                writeFromList(sb, (List) v);
            }
            else {
                writeFromData(sb, v);
            }
            sb.append("");
        }
    }

    private void writeFromData(StringBuffer sb, Object data) {
        sb.append(escapeXml(data+""));
    }

    private String escapeXml(String src) {
        src = src.replace("\"", """);
        src = src.replace("<", "<");
        src = src.replace(">", ">");
        src = src.replace("&", "&");

        return src;
    }
}

RabbitMQ 연동

설치는 그냥 rpm 으로 했다.

1
2
3
4
5
# 서버 시작.
sbin/rabbitmq-server start

# 서버 중지
sbin/rabbitmqctl stop

pom.xml

1
2
3
4
5
<dependency>
  <groupid>org.springframework.amqp</groupId>
  <artifactid>spring-rabbit</artifactId>
  <version>1.4.1.RELEASE</version>
</dependency>

context-rabbitmq.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- A reference to the org.springframework.amqp.rabbit.connection.ConnectionFactory -->
<rabbit:connection-factory id="connectionFactory" host="localhost" username="worker" password="workerpassword" />

<!-- Creates a org.springframework.amqp.rabbit.core.RabbitTemplate for access to the broker -->
<rabbit:template id="amqpTemplate" connection-factory="connectionFactory" />

<!-- Creates a org.springframework.amqp.rabbit.core.RabbitAdmin  to manage exchanges, queues and bindings -->
<rabbit:admin connection-factory="connectionFactory" />

<!-- Creates a queue for consumers to retrieve messages -->
<rabbit:queue name="simple_queue" />

<rabbit:listener-container  connection-factory="connectionFactory">
    <rabbit:listener queues="simple_queue" ref="mqService" />
</rabbit:listener-container>

MqService.java

보내는 쪽과 받는 쪽을 한 클래스에 넣었다. MessageListener 를 구현하면 위 listener-container 가 알아서 물어다 준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class MqService implements MessageListener {
    private static final Logger logger = LoggerFactory.getLogger(MqService.class);
    private static final String TASK_QUEUE_NAME = "simple_queue";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void send(String message) throws IOException {
        rabbitTemplate.convertAndSend(TASK_QUEUE_NAME, message);

        logger.info("send message={}", message);
    }

    @Override
    public void onMessage(Message message) {
        String msg = null;
        try {
            msg = new String(message.getBody(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        logger.info("recv message=" + msg );
    }
}

Redis 연동

pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.1.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.1.0</version>
</dependency>

버전을 잘 맞춰야 한다. 안 그러면 몇몇 class 가 없어서 오류가 난다.

context-redis.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">

    <bean id="jedisConnFactory"
          class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
          p:usePool="true"
          p:hostName="172.xxx.xxx.xxx"
          p:port="6379"
    />

    <!-- redis template definition -->
    <bean id="redisTemplate"
          class="org.springframework.data.redis.core.RedisTemplate"
          p:connectionFactory-ref="jedisConnFactory"
    />
</beans>

6379 가 redis 기본 포트다. 설치할 때 바꿀 수 있다.

RedisTest.java

ValueOperations 로 만료시간까지 같이 넣는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( {
        "classpath:spring/application-context.xml"
}
)
public class RedisTest {
    private static final Logger logger = LoggerFactory.getLogger(RedisTest.class);

    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @Resource(name="redisTemplate")
    private ValueOperations<String, ResultMap> valueOps;

    @Test
    public void testTp4110() throws Exception {
        // redisTemplate.delete("1");

        ResultMap res = valueOps.get("1");
        if(res == null) {
            logger.info("create.. cache..");
            // create..
            // 10분 캐시
            valueOps.set("1", ResultMap.create(), 10, TimeUnit.MINUTES);
            res = valueOps.get("1");
        }

        logger.info("redis-test={}", res);

        res = valueOps.get("2");
        logger.info("redis-test={}", res);
    }
}

실행 결과다. 없는 키(“2”)는 null 로 온다.

1
2
3
4
5
...
[INFO ] 17:30:28.990 [main] - create.. cache..
[INFO ] 17:30:29.190 [main] - redis-test={result_code=0000, result_message=success}
[INFO ] 17:30:29.288 [main] - redis-test=null
...

Spring Batch

써봤는데 결과는 성공적이었다. 특히 트랜잭션 commit size 와 read size 를 따로 지정할 수 있다는게 좋은것 같다.

job 을 요약하면 이렇다.

reader 에서 데이터를 읽어서 process 에서 처리하고 writer 로 결과를 기록한다.

각 시작 구간마다 이벤트를 받는 listener 같은 것도 제공한다. reader, writer 는 커스텀하지 않고 mybatis 가 기본으로 주는 걸 썼다. (참고 https://mybatis.github.io/spring/ko/batch.html)

쿼리나 로직보다 아래 설정이 중요한 것 같아서 그것만 남긴다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
    <property name="jobRepository" ref="jobRepository" />
</bean>

<bean id="jobRepository"
  class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
  <property name="transactionManager" ref="transactionManager"/>
</bean>

<!-- sent type sent -->
<job:job id="rstJob" job-repository="jobRepository">
    <job:step id="step1">
        <tasklet>
            <chunk reader="rstReader" processor="memberRstProcess"
             writer="rstWriter" commit-interval="500">
         </chunk>
     </tasklet>
 </job:step>
</job:job>

<bean id="memberRstProcess" class="com.xxxxx.MemberRstProcess" />
<bean id="rstReader"
  class="org.mybatis.spring.batch.MyBatisPagingItemReader"
  p:sqlSessionFactory-ref="sqlSessionFactory"
  p:queryId="com.xxxxx.mapper.QueryMapper.selectMemberRstList"
  p:pageSize="500"
  scope="step" />

  <bean id="rstWriter" class="org.mybatis.spring.batch.MyBatisBatchItemWriter">
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    <property name="statementId" value="com.xxxxx.mapper.QueryMapper.updateMemberRst" />
</bean>

pageSize 가 읽는 단위, commit-interval 이 커밋 단위다. 둘이 따로 논다.

SiteMesh

pom.xml

1
2
3
4
5
<dependency>
    <groupId>opensymphony</groupId>
    <artifactId>sitemesh</artifactId>
    <version>2.4.2</version>
</dependency>

WEB-INF/web.xml

1
2
3
4
5
6
7
8
<filter>
    <filter-name>sitemesh</filter-name>
    <filter-class>com.opensymphony.module.sitemesh.filter.PageFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>sitemesh</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

WEB-INF/sitemesh.xml

이 파일은 수정할 부분이 거의 없다. decorators.xml 경로 정도.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<sitemesh>
    <property name="decorators-file" value="/WEB-INF/decorators.xml" />
    <excludes file="${decorators-file}" />

    <page-parsers>
        <parser content-type="text/html"
                class="com.opensymphony.module.sitemesh.parser.HTMLPageParser" />
        <parser content-type="text/html;charset=UTF-8"
                class="com.opensymphony.module.sitemesh.parser.HTMLPageParser" />
    </page-parsers>

    <decorator-mappers>
        <mapper class="com.opensymphony.module.sitemesh.mapper.PrintableDecoratorMapper">
            <param name="decorator" value="printable" />
            <param name="parameter.name" value="printable" />
            <param name="parameter.value" value="true" />
        </mapper>

        <mapper class="com.opensymphony.module.sitemesh.mapper.PageDecoratorMapper" >
            <param name="property" value="meta.decorator" />
        </mapper>

        <mapper class="com.opensymphony.module.sitemesh.mapper.ConfigDecoratorMapper">
            <param name="config" value="${decorators-file}" />
        </mapper>
    </decorator-mappers>
</sitemesh>

WEB-INF/decorators.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<decorators defaultdir="/decorators">
    <excludes>
        <pattern>/*.json</pattern>
    </excludes>
    <decorator name="top" page="/views/layout/top.jsp" />
    <decorator name="left" page="/views/layout/left.jsp" />
    <decorator name="layout2" page="/views/layout/layout2.jsp">
        <pattern>/login</pattern>
        <pattern>/login_error</pattern>
    </decorator>
    <decorator name="layout" page="/views/layout/layout.jsp">
        <pattern>/*</pattern>
    </decorator>
</decorators>

설정은 XML 만 넣어주면 끝난다.

  • <excludes> 에는 decorator 를 적용하지 않을 URL 패턴을 넣는다
  • <decorator> 는 실제 적용될 jsp 레이아웃이나 템플릿이다
  • name, page 로 구성되며 name 은 <page:applyDecorator name="top" /> 처럼 다른 decorator 에 적용될 수 있다
  • <pattern>/login</pattern> 은 decorator 를 적용할 URL 을 지정한다

레이아웃 만들기

기본 레이아웃인 layout.jsp 다. 실제로 쓰는 파일을 그대로 올린건 아니고 간단히 요약했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%>
<%@ taglib prefix="decorator" uri="http://www.opensymphony.com/sitemesh/decorator"%>
<%@ taglib prefix="page" uri="http://www.opensymphony.com/sitemesh/page" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>sample</title>
    <decorator:head/>
</head>
<body>
<div class="wrapper">
    <header class="main-header">
        <page:applyDecorator name="top" />
    </header>

    <!-- Left side column. contains the logo and sidebar -->
    <aside class="main-sidebar">
        <page:applyDecorator name="left" />
    </aside>

    <!-- Content Wrapper. Contains page content -->
    <div class="content-wrapper">
        <decorator:body />
    </div><!-- /.content-wrapper -->

    <footer class="main-footer">
        <div class="pull-right hidden-xs">
            <b>Version</b> 0.0.1
        </div>
        <strong>Copyright &copy; 2015 sample </strong> All rights reserved.
    </footer>
</div>
</body>
</html>
  • <decorator:head /> 는 대상 페이지의 <head> 내용을 가져다 붙인다
  • <page:applyDecorator name="top" /> 은 top decorator 를 가져와 붙인다. include 라고 생각하면 이해가 빠르다
  • <page:applyDecorator name="left" /> 는 left decorator
  • <decorator:body /> 는 대상 페이지의 <body> 내용을 가져와 붙인다

실제 MVC 에서 쓰는 jsp 는 이렇게 생겼다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%>
<head>
    <!-- page script -->
    <script>
      ....
    </script>
</head>
<body>
<!-- Content Header (Page header) -->
<section class="content-header">
    <h1>
        데이타 관리
        <small>버전관리</small>
    </h1>
    <ol class="breadcrumb">
        <li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li>
        <li><a href="#">데이타관리</a></li>
        <li class="active">버전관리</li>
    </ol>
</section>

<!-- Main content -->
<section class="content">
    <div class="row">
        <div class="col-xs-12">

            <div class="box">
                <div class="box-header">
                    <h3 class="box-title">버전 목록</h3>
                    <a href="create"><button type="button" class="btn btn-primary btn-lg pull-right">신규 추가</button></a>
                </div><!-- /.box-header -->
                <div class="box-body">
                    <table id="list" class="table table-bordered table-hover">
                    </table>
                </div><!-- /.box-body -->
            </div><!-- /.box -->

        </div><!-- /.col -->
    </div><!-- /.row -->
</section><!-- /.content -->

</body>

이렇게 원하는 jsp 에 <head><body> 만 구성하면 layout.jsp 형식으로 출력된다.

Hadoop 연동

설치는 OSX 요세미티에 2.6.x 버전으로 했다.

pom.xml

1
2
3
4
5
<dependency>
  <groupid>org.springframework.data</groupId>
  <artifactid>spring-data-hadoop</artifactId>
  <version>2.1.1.RELEASE</version>
</dependency>

context-hadoop.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:hdp="http://www.springframework.org/schema/hadoop"
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/hadoop http://www.springframework.org/schema/hadoop/spring-hadoop.xsd">

    <hdp:configuration id="hdConf">
        fs.default.name=hdfs://localhost:9000
    </hdp:configuration>
</beans>

파일 읽고 쓰기 테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
        "classpath:servlet-context.xml",
        "classpath:config/context-datasource.xml",
        "classpath:config/context-hadoop.xml"
})
public class HdTestServiceTest {
    private static final Logger logger = LoggerFactory.getLogger(HdTestService.class);

    @Autowired
    private org.apache.hadoop.conf.Configuration hdConf;

    @Test
    public void testDoTest() throws Exception {
        FileSystem hdfs = null;
        try {
            Path filePath = new Path("/tmp/test.txt");
            logger.info("filePath.uri={}", filePath.toUri());

            hdfs = FileSystem.get(filePath.toUri(), hdConf);

            if(hdfs.exists(filePath)) {
                logger.info("read file path={}", filePath);
                BufferedReader r = new BufferedReader(new InputStreamReader(hdfs.open(filePath), "utf-8"));
                String line = null;
                do {
                    line = r.readLine();
                    logger.info("  line={}", line);
                }
                while(line != null);
                r.close();

                // dfs.delete(filePath, true);
            } else {
                logger.info("create new file path={}", filePath);

                FSDataOutputStream out = hdfs.create(filePath, false);
                out.write("한글 생성 테스트".getBytes("utf-8"));
                out.flush();
                out.close();
            }
        }
        finally {
            IOUtils.closeQuietly(hdfs);
        }
    }
}

잘 된다. 다만 아직 로컬에서 못 벗어났다. 벗어날 서버가 없어.. 이 코드면 파일 업로드 다운로드까지는 구현이 가능하다.

Hadoop MapReduce — WordCount

위 설정에 job 을 추가한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<hdp:configuration id="hdConf">
    fs.default.name=hdfs://localhost:9000
</hdp:configuration>

<hdp:job id="wordCountJob"
        input-path="/input/"
        output-path="/output/"
        configuration-ref="hdConf"
        mapper="delim.app.service.WordCount$TokenizerMapper"
        reducer="delim.app.service.WordCount$IntSumReducer"
        >
</hdp:job>

<hdp:job-runner id="wordCountJobRunner" job-ref="wordCountJob" run-at-startup="false">
</hdp:job-runner>

WordCount.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.StringTokenizer;

public class WordCount {
    private static final Logger logger = LoggerFactory.getLogger(WordCount.class);

    public static class TokenizerMapper
            extends Mapper<Object, Text, Text, IntWritable> {

        private final static IntWritable one = new IntWritable(1);
        private Text word = new Text();

        @Override
        public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            logger.info("map key={}, value={}", key, value);

            StringTokenizer itr = new StringTokenizer(value.toString());
            while (itr.hasMoreTokens()) {
                word.set(itr.nextToken());
                context.write(word, one);
            }
        }
    }


    public static class IntSumReducer
            extends Reducer<Text, IntWritable, Text, IntWritable> {
        private IntWritable result = new IntWritable();

        @Override
        public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            logger.info("reduce key={}", key);

            int sum = 0;
            for (IntWritable val : values) {
                sum += val.get();
            }
            result.set(sum);
            context.write(key, result);
        }
    }
}

실행

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Autowired
private org.apache.hadoop.conf.Configuration hdConf;

@Autowired
private JobRunner wordCountJobRunner;

@Before
public void beforeCopyFile() throws IOException {
    String file = "/Users/paper/Desktop/4/14/debug.2015-04-09.log";

    Path srcFilePath = new Path(file);
    Path dstFilePath = new Path("/input/debug.2015-04-09.log");

    FileSystem hdfs = FileSystem.get(dstFilePath.toUri(), hdConf);
    hdfs.copyFromLocalFile(false, true, srcFilePath, dstFilePath);

    hdfs.delete(new Path("/output/"), true);
}

@Test
public void testRunJob() throws Exception {
    wordCountJobRunner.call();
}

순서는 이렇다.

  1. @Before 에서 로컬의 debug.log 를 hdfs 로 복사해둔다
  2. job 을 실행한다
  3. 실행하면 debug.log 를 line 단위로 읽어들이는걸 확인할 수 있다 (WordCount$TokenizerMapper)

정리

  • 연동 테스트는 JUnit 환경부터. JNDI 데이터소스는 테스트용 xml 로 같은 id 를 덮어쓴다
  • RabbitMQ 는 listener-containerMessageListener 구현체를 물리면 끝
  • Redis 는 jedis 와 spring-data-redis 버전을 맞춰야 한다. 안 맞으면 클래스가 없다고 터진다
  • Spring Batch 는 read size(pageSize)와 commit size(commit-interval)가 따로 논다
  • SiteMesh 는 XML 세 개(web.xml, sitemesh.xml, decorators.xml)만 넣으면 된다
  • Hadoop 은 hdp:configuration 하나로 FileSystem 을 주입받아 쓸 수 있다