Java Application을 원격지에서 감시하려면 JMX를 활성화해야 하고, Client로는 jConsole, VisualVM으로 접근하여 확인해야 합니다. GUI 환경이 갖추어지지 않는 CLI 환경은 jstatd (Virtual Machine jstat Daemon)을 활성화하고 rmi (Java Remote Method Invocation)을 이용하여 확인해야 합니다.


jstatd -10080 -J-Djava.security.policy=./java.policy
jps ---v rmi:192.168.0.2:10080
jstat -gcutil -h20 -t rmi:17755@192.168.0.2:10080 1000 1000


Jolokia는 원격지에서 JMX에 접근을 목적으로 사용되는 Java Specification Request (JSR 160)을 구현해놓은 Agent이며, Client와 Agent간의 통신은 HTTP/JSON으로 요청, JSON으로 정보를 받습니다. 



Java Application에 결합된 형태로 배포할 수 있기 때문에 사용성이 좋습니다. 예시는 Spring Boot 기준으로 합니다. Jolokia 활성화를 위해 모듈을 추가합니다.


dependencies {
    compile 'org.jolokia:jolokia-core'
}


Http Endpoint, MBean name 중복방지를 위해 추가 설정이 필요합니다. application.properties에 추가합니다.


management:
    context-path
/manage
    security
:
        enabled
false
 
endpoints
:
    beans
:
        sensitive
false
    jmx
:
        unique-names
true
        enabled
true
    jolokia
:
        enabled
true


Jolokia Protocol은 https://jolokia.org/reference/html/protocol.html에서 확인할 수 있습니다.


버전 확인
GET /manage/jolokia/version
 
Heap Memory 사용량 확인
GET /manage/jolokia/read/java.lang:type=Memory
 
Thread 수 확인
GET /manage/jolokia/read/java.lang:type=Threading/ThreadCount
 
Thread Dump 확인
POST /manage/jolokia
{
   "type":"EXEC",
   "mbean":"java.lang:type=Threading",
   "operation":"dumpAllThreads",
   "arguments":[true,true]
}
 
GC 현황 확인
POST /manage/jolokia
{
    "mbean": "java.lang:type=GarbageCollector,*",
    "type": "read"
}


대표적으로는 Memory, Thread, GC를 확인했으며, 자세한 내용은 Jolokia Protocol 문서를 참고하시기 바랍니다.


참고 사이트

  • https://jolokia.org/
  • https://jolokia.org/reference/html/protocol.html
  • https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#production-ready-jolokia


어렸을 때부터 지속되어 온 생활습관들이 세월이 흘러 중년이 되면 몸에서 질병으로 나타난다. 프로젝트의 요구사항을 기반으로 개발자가 탄생시킨 프로그램도 마찬가지로 에러 메시지로 상황을 알려준다. 


지속해서 눈감고 모른 체하게 되면 프로그램이 중단되는 사태가 발생하는데 질병을 무시했다가 큰 병을 얻는 경우와 같은 상황이다. 

사람은 정기적으로 건강검진을 받는 것처럼, 프로그램도 정기적으로 관리를 해야 한다. 


프로그래밍 언어마다 감시하는 방법이 존재하지만, 예시는 Java 프레임워크 Spring Boot 기준으로 하겠다. 


Spring Boot는 Actuator를 사용하면 프로그램의 상황을 감시할 수 있는데, Http Endpoint, JMX, 원격 SSH 3가지 방법으로 확인할 수 있다.


Actuator를 활성화하려면 모듈을 추가해야 한다. 


dependencies {
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile 'org.springframework.boot:spring-boot-starter-security'
    compile 'org.springframework.boot:spring-boot-starter-remote-shell'
}


모듈이 정상적으로 컴파일되면 Http Endpoint, 원격 SSH가 활성화된다. port는 설정하지 않으면 기본값 2000이다.

Http Endpoint의 중복을 방지하기 위해 추가 설정이 필요하다. application.properties에 추가한다.


management:
    context-path: /manage
    security:
        enabled: false
    shell:
        ssh:
            enabled: true
            port: 18080
    health:
        db:
            enabled: true
        diskspace:
            enabled: true
 
endpoints:
    beans:
        sensitive: false


Http Endpoint 목록은 /manage/mappings로 요청하면 확인할 수 있다.


{
  "{[/manage/metrics/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint.value(java.lang.String)"
  },
  "{[/manage/metrics || /manage/metrics.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/auditevents || /manage/auditevents.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public org.springframework.http.ResponseEntity<?> org.springframework.boot.actuate.endpoint.mvc.AuditEventsMvcEndpoint.findByPrincipalAndAfterAndType(java.lang.String,java.util.Date,java.lang.String)"
  },
  "{[/manage/configprops || /manage/configprops.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/mappings || /manage/mappings.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/application || /application.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public org.springframework.hateoas.ResourceSupport org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint.links()"
  },
  "{[/manage/trace || /manage/trace.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/heapdump || /manage/heapdump.json],methods=[GET],produces=[application/octet-stream]}": {
    "bean": "endpointHandlerMapping",
    "method": "public void org.springframework.boot.actuate.endpoint.mvc.HeapdumpMvcEndpoint.invoke(boolean,javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse) throws java.io.IOException,javax.servlet.ServletException"
  },
  "{[/manage/autoconfig || /manage/autoconfig.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/beans || /manage/beans.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/env/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpoint.value(java.lang.String)"
  },
  "{[/manage/env || /manage/env.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/health || /manage/health.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(javax.servlet.http.HttpServletRequest,java.security.Principal)"
  },
  "{[/manage/dump || /manage/dump.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/info || /manage/info.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  },
  "{[/manage/loggers/{name:.*}],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.get(java.lang.String)"
  },
  "{[/manage/loggers/{name:.*}],methods=[POST],consumes=[application/vnd.spring-boot.actuator.v1+json || application/json],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.LoggersMvcEndpoint.set(java.lang.String,java.util.Map<java.lang.String, java.lang.String>)"
  },
  "{[/manage/loggers || /manage/loggers.json],methods=[GET],produces=[application/vnd.spring-boot.actuator.v1+json || application/json]}": {
    "bean": "endpointHandlerMapping",
    "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter.invoke()"
  }
}


대표적으로 많이 쓰이는 것은 2가지이다. 

상태 값 확인은 /manage/health를 사용하면 되고, 추가 설정을 하게 되면 redis, mysql 저장소도 확인할 수 있다.


{
  "status": "UP",
  "diskSpace": {
    "status": "UP",
    "total": 214734073856,
    "free": 195815051264,
    "threshold": 10485760
  }
}


Cpu, Memory, Threads, GC, Datasource를 감시하기 위해서는 /manage/metrics를 사용한다.


{
  "mem": 12496579,
  "mem.free": 8820382,
  "processors": 8,
  "instance.uptime": 81630586,
  "uptime": 81641671,
  "systemload.average": 0.05,
  "heap.committed": 12340992,
  "heap.init": 12582912,
  "heap.used": 3520609,
  "heap": 12340992,
  "nonheap.committed": 159528,
  "nonheap.init": 2496,
  "nonheap.used": 155587,
  "nonheap": 0,
  "threads.peak": 342,
  "threads.daemon": 234,
  "threads.totalStarted": 3086,
  "threads": 287,
  "classes": 14031,
  "classes.loaded": 14031,
  "classes.unloaded": 0,
  "gc.parnew.count": 522,
  "gc.parnew.time": 9718,
  "gc.concurrentmarksweep.count": 2,
  "gc.concurrentmarksweep.time": 605,
  "httpsessions.max": -1,
  "httpsessions.active": 0,
  "datasource.primary.active": 1,
  "datasource.primary.usage": 0.033333335,
  "datasource.slave.active": 0,
  "datasource.slave.usage": 0.0,
}


원격 SSH는 Spring Boot 2.0에서는 제거되었다. 하지만 기능이 매우 괜찮아서 아쉽기도 하다. 

별도로 유저, 비밀번호 설정을 하지 않으면, 최초 시작 시 로그에서 무작위로 생성되는 비밀번호를 확인해야 한다.


Using default security password: 6a37007e-01c9-465c-b9d8-95a7c88afd38


정상적으로 접속되면 help를 입력하여 확인하자.


$ ssh -18080 user@localhost
Password authentication
Password:
 
 :: Spring Boot ::  (v1.5.8.RELEASE) on localhost
> help
Try one of these commands with the -h or --help switch:
 
NAME       DESCRIPTION
autoconfig Display auto configuration report from ApplicationContext
beans      Display beans in ApplicationContext
cron       manages the cron plugin
dashboard  a monitoring dashboard
egrep      search file(s) for lines that match a pattern
endpoint   Invoke actuator endpoints
env        display the term env
filter     a filter for a stream of map
java       various java language commands
jul        java.util.logging commands
jvm        JVM informations
less       opposite of more
mail       interact with emails
man        format and display the on-line manual pages
metrics    Display metrics provided by Spring Boot
shell      shell related command
sleep      sleep for some time
sort       sort a map
system     vm system properties commands
thread     JVM thread commands
help       provides basic help
repl       list the repl or change the current repl
 
>


dashboard를 입력하면 Thread, Memory 현황을 실시간으로 확인할 수 있다.



Java에서 제공하는 jmap, jstat등을 이용한다면 기본적인 감시는 가능하겠지만, Spring Boot Actuator는 기능이 확장되어 좀 더 쉽게 상황을 진단할 수 있다. 


프로젝트 일정에 너무 치이다 보면 실수를 인지하지 못한 상태로 운영단계에 들어가게 되는데, 반드시 검수 일정을 확보하여 Spring Boot Actuator 기능을 기반으로 실수를 사전에 감시하여 제거했으면 한다.


참고 사이트

  • https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready


보통 Web 개발 하면 Spring 이 자연스럽게 떠오른다. 그 만큼 보편화 되어 있다. 개발 경험이 있다면 @ModelAttribute 라는 어노테이션이 친숙할 것 이다. 아래 그림을 보자.



Spring 을 사용하면 대표적으로 떠오르는 Servlet 이 있다. Servlet 를 직접 개발 하면 HttpServletRequest.getParameter(String name) 을 직접 사용하지만, DispatcherServlet 는 name 값과 Model Class 의 field 가 mapping 되어 값을 넣어준다. 그 시점은 Controller 호출 되기 직전 이라고 보면 된다. 참 편리한 기능 이다.


여기서 한번 더 생각 해보자. List 로 받을 수는 없을 까? 여러 번의 시도 끝에 방법을 찾았다.


@RequestMapping(value="/sample", method=RequestMethod.POST)
public ModelAndView sample(List<Sample> sampleList) throws Exception {
    try {
        ModelAndView mav = new ModelAndView();

        // Doing...

        return mav;
    } catch (Exception e) {
        throw e;
    }
}


Controller 소스 중 일부 이다. @ModelAttribute 는 생략이 가능 한 어노테이션 이다. 방금 설명 했던 mapping 작업은 Object 기준으로 이루어 진다. 결국 위 소스는 무용지물 이다. 다른 방법으로 접근 해 보자.


public class Sample {

    private String sample1;
    private String sample2;
    private String sample3;
    private String files;

    private List<Sample> sampleList;

    // Doing...

}


Model Class 에 List 로 선언 하면 mapping 이 가능하다.


<table class="sample_table" border="0" cellspacing="0" summary="">
    <tr>
        <th scope="row">Sample 1</th>
        <td class="long">
            <input type="text" name="sampleList[index].sample1" />
        </td>
    </tr>
    <tr>
        <th scope="row">Sample 2</th>
        <td class="long">
            <input type="text" name="sampleList[index].sample2" />
        </td>
    </tr>
    <tr>
        <th scope="row">Sample 3</th>
        <td>
            <input type="text" name="sampleList[index].sample3" />
        </td>
    </tr>
    <tr>
        <th scope="row">Sample File</th>
        <td class="long">
            <input type="file" name="sampleList[index].files" />
        </td>
    </tr>
</table>


값은 전달 하는 Html 이다. name 을 주시 하자. Model Class 에서 선언 했던 List 값과 동일 하다. 배열을 사용하듯 name 값을 작성 한다.


@RequestMapping(value="/sample", method=RequestMethod.POST)
public ModelAndView sample(Sample sample) throws Exception {
    try {
        ModelAndView mav = new ModelAndView();

        logger.debug(sample.toString());

        // Doing...

        return mav;
    } catch (Exception e) {
        throw e;
    }
}


실제 요청을 받아 보자. log 을 보면 값을 확인 할 수 있을 것 이고, 이로서 흔히 게시글 N개 를 동시에 등록할 때 사용하면 편리하게 처리가 가능 하다.


참고 사이트


많이 사용되는 예외 처리 기법 중 ExceptionResolver 의 일종인 SimpleMappingExceptionResolver 를 사용하여 Exception 종류 별로 특정 페이지로 이동 할 수 있게 하는 방법 이 있다.



Controller 의 기능만 사용해서 개발 한다면 Spring MVC 의 범위 안에서 Exception 을 해결 할 수 있기 때문에 크게 문제가 되지 않는다. 실제 개발 하는 부분은 Execute Business Logic 가 전부 이기 때문 이다.


회사에서 Spring MVC 라이브러리를 바탕으로 하여 만들어 지는 프레임워크는 Handler Mapping, Handler Adapter, View Resolver 의 확장이 대부분 이며, 예외 처리는 직접 고려해야 한다.


Business Logic 을 처리 하고 난 후 사용자에게 제공 되는 화면이 요구사항에 따라 달라질 수 있고, 잦을 수도 있다. 이에 대응 하기 위해서 View 를 확장 한다.


  1. AbstractExcelView 를 사용하여 Excel 연동 하기 (http://blog.whitelife.co.kr/197)
  2. AbstractView 사용하여 Ajax 처리 하기 (http://blog.whitelife.co.kr/208)
  3. AbstractView 사용하여 파일 다운로드 처리 하기 (http://blog.whitelife.co.kr/211)

AbstractView 를 사용해서 작업한 내용의 글 이다. 필요한 경우 참고 하자.


View 를 확장 했기 때문에 ExceptionResolver 가 예외 처리를 할 수 없다. 시점 이 그 이후 이기 때문 이다. 추가 적인 예외 처리가 필요 한대 WAS(Web Application Server) 에서 제공 하는 기능을 사용 해야 한다.


실제 서비스 중에 알파벳이 난무 하는 화면이 보여진 다면, 문제가 심각할 것 이다. 디버깅을 해서 보완을 하더라도, 그 시간동안은 대체 하는 화면이 필요 하다.


<error-page>
    <error-code>400</error-code>
    <location>/WEB-INF/view/error/badrequest.jsp</location>
</error-page>
<error-page>
    <error-code>404</error-code>
    <location>/WEB-INF/view/error/notfound.jsp</location>
</error-page>
<error-page>
    <error-code>414</error-code>
    <location>/WEB-INF/view/error/requesturitoolong.jsp</location>
</error-page>
<error-page>
    <error-code>500</error-code>
    <location>/WEB-INF/view/error/internalservererror.jsp</location>
</error-page>


web.xml 에 추가 하자. 보통 /WEB-INF/web.xml 에 파일이 있다.


error-page

description
400 (잘못된 요청): 서버가 요청의 구문을 인식하지 못했다.
404 (찾을 수 없음): 서버가 요청한 페이지를 찾을 수 없다. 예를 들어 서버에 존재하지 않는 페이지에 대한 요청이 있을 경우 서버는 이 코드를 제공한다.
414 (요청 URI가 너무 긺): 요청 URI(일반적으로 URL)가 너무 길어 서버가 처리할 수 없다.
500 (내부 서버 오류): 서버에 오류가 발생하여 요청을 수행할 수 없다.


Http Status Code 에 따라 보여질 화면을 설정 한다.


강제로 에러를 발생해서 확인 해 보면 의도하는 화면이 표현 될 것 이다.


요구사항에 대한 기능은 정상적으로 동작 하기 때문에, 별거 아니라고 생각하고 지나칠 수 있다. 신경 써서 마무리 한다면 완성도 가 높은 프로젝트를 경험 할 수 있을 것 이다.


참고 사이트


Controller 에서 요청 처리 후 응답 할 때 RedirectView 를 사용한다.


return "redirect:/go";


보통 위와 같이 간단하게 사용하면 옵션 값을 주기 불편하다. 직접 객체로 작성 해야 상세하게 설정 할 수 있다.


ModelAndView mav = new ModelAndView();

RedirectView redirectView = new RedirectView(); // redirect url 설정
redirectView.setUrl(String url);
redirectView.setExposeModelAttributes(false);

mav.setView(redirectView);

return mav;


setExposeModelAttributes(false) 로 설정 하면, 직접 ?test=a&test=b 붙여서 작성 하는 부분은 파라미터가 붙지만, ModelAndView.addObject(attributeName, attributeValue) 로 추가 하는 부분은 파라미터가 생략 된다.


조금 번거롭다 하더라도, 직접 객체로 작성 하는 것을 권장 한다.


파일 업로드 시 파일을 첨부 했다면 문제가 발생하지 않는다. 첨부 하지 않고 빈값으로 Form Submit 을 하는 경우 예외 상황이 발생 한다.


Model 에 선언되어 있는 타입은 MultipartFile 로 되어 있지만, 빈 문자열 String 으로 인식 되어 값을 넣을 수 없어 Client 에는 400 Bad Request 가 응답이 되기도 한다.


해결 방법은 빈 문자열을 null 로 바꿔주는 것 이다. Spring 은 Model Binding 을 하기전에 @initBinder 로 선언 되어 있는 메소드가 있다면 우선으로 invoking 한다.


2014-10-24 11:48:19,803 TRACE HandlerMethod - Invoking [SampleController.initBinder] method with arguments [org.springframework.web.servlet.mvc.method.annotation.ExtendedServletRequestDataBinder@5552bb15]


이 시점에서 설명했던 작업을 한다. 사용하고 있는 Controller 에 아래 코드를 추가 하자.


@InitBinder
public void initBinder(WebDataBinder binder) throws Exception {
    binder.registerCustomEditor(MultipartFile.class, new PropertyEditorSupport() {

        @Override
        public void setAsText(String text) {
            logger.debug("initBinder MultipartFile.class: {}; set null;", text);
            setValue(null);
        }

    });
}


String 으로 인식 되면 setAsText(String text) 로 전달 되는대 MultipartFile 은 객체 이기 때문에 null 값으로 setValue(Object value) 에 전달 한다.


파일 첨부 할때 사용 되는 MimeMessageHelper.addAttachment(String attachmentFilename, InputStreamSource inputStreamSource) 메소드에 MultipartHttpRequest 에서 직접 InputStream 객체를 연결 하면 문제가 발생 한다.


Buffer 에 파일을 임시로 담아 놓고, InputStream 으로 변환하면 해결 된다.


for (MultipartFile uploadFile : uploadFiles) {
    String originalFilename = uploadFile.getOriginalFilename();

    logger.debug("addAttachment: {}", originalFilename);
    logger.debug("addAttachment size: {}", uploadFile.getSize());

    mimeMessageHelper.addAttachment(MimeUtility.encodeText(originalFilename), new ByteArrayResource(IOUtils.toByteArray(uploadFile.getInputStream())));
}


IOUtils 이 없다면, pom.xml 에 추가 하자.


<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>1.4</version>
</dependency>

참고 사이트


Java Mail을 기반으로 하여 작성된 Spring Mail 라이브러리가 있다. 적용 해 보자.


pom.xml에 라이브러리를 추가 한다.


<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>3.0.5.RELEASE</version>
</dependency>


applicationContext.xml 에 Mail Sender Class 설정을 추가 하자. 필요에 따라 Properties 에서 SpringExpressionLanguage (SpEL) 문법을 사용해도 된다.


<util:properties id="config" location="classpath:config/config.properties" />

<!-- Spring mail configuration -->
<beans:bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
     <beans:property name="host" value="#{config['mail.host']}" />
     <beans:property name="port" value="#{T(java.lang.Integer).parseInt(config['mail.port'])}" />
     <beans:property name="username" value="#{config['mail.username']}" />
     <beans:property name="password" value="#{config['mail.password']}" />
     <beans:property name="defaultEncoding" value="#{config['mail.defaultEncoding']}" />

     <beans:property name="javaMailProperties">
        <beans:props>
            <beans:prop key="mail.transport.protocol">#{config['mail.protocol']}</beans:prop>
            <beans:prop key="mail.smtp.auth">true</beans:prop>
            <beans:prop key="mail.smtp.starttls.enable">false</beans:prop>
        </beans:props>
     </beans:property>
</beans:bean>


메일을 보낼 수 있는 Interface 를 작성 한다. 제네릭 문법으로 report 를 받는 것은 원하는 타입에 맞게 메일 내용을 작성 할 수 있게 하기 위해서 이다.


public interface Publisher {
    public <T> boolean publish(T report);
}


실제 메일 발송에 대한 구현 클래스 이다.


@Service("EmailPublisherService")
public class EmailPublisherService implements Publisher {

    private static final Logger logger = LoggerFactory.getLogger(EmailPublisherService.class);

    @Autowired
    private JavaMailSender javaMailSender;

    @Override
    public <T> boolean publish(T report) {
        logger.debug("Sending report by email...");
        boolean retVal = false;
        try {
            final String emailTo = "to@test.co.kr";
            final String emailFrom = "from@test.co.kr";
            final String subject = "test subject";
            final String message = (String) report;

            javaMailSender.send(new MimeMessagePreparator() {

                @Override
                public void prepare(MimeMessage paramMimeMessage) throws Exception {
                    MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(paramMimeMessage, true, "UTF-8");

                    mimeMessageHelper.setTo(emailTo);
                    mimeMessageHelper.setFrom(emailFrom);
                    mimeMessageHelper.setSubject(subject);
                    mimeMessageHelper.setText(message);

                    final File file = new File("test filename");

                    mimeMessageHelper.addAttachment(MimeUtility.encodeText("filename"), new InputStreamSource() {

                        @Override
                        public InputStream getInputStream() throws IOException {
                            // TODO Auto-generated method stub
                            return new FileInputStream(file);
                        }
                    });

                };
            });

            retVal = true;
        } catch (Exception e) {
            logger.error("Can't send email... " + e.getMessage(), e);
        }
        return retVal;
    }
}


메일에 첨부 파일이 있는 경우 new MimeMessageHelper(MimeMessage mimeMessage, boolean multipart, String encoding) 중 multipart 값을 true 로 변경 하고, MimeMessageHelper.addAttachment(attachmentFilename, inputStreamSource) 메소드를 사용 하자. 단 파일 명을 MimeUtility.encodeText(String text) 하지 않으면, 한글이 께져서 발송 된다.


@Controller
@RequestMapping(value="/mail")
public class SampleController {

    @Resource(name="EmailPublisherService")
    private EmailPublisherService emailPublisherService;

    @RequestMapping(value="/send")
    public @ResponseBody String send() throws Exception {
        try {
            emailPublisherService.publish("text message...");
            return "Success";
        } catch (Exception e) {
            throw e;
        }
    }
}


Controller 에 작성한 EmailPublisherService 를 Dependency Injection(DI) 하여 메일을 보내 보자.


참고 사이트


+ Recent posts