아래 샘플을 참고 하자.


/**
 * 파일 명 생성
 *
 * @param fileName
 * @param request HttpServletRequest
 * @param response HttpServletResponse
 * @throws UnsupportedEncodingException
 */
private void setDownloadFileName(String fileName, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
    fileName = URLEncoder.encode(fileName, "utf-8").replaceAll("\\+", "%20");

    if (logger.isDebugEnabled()) {
        logger.debug("fileName: " + fileName);
    }

    response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
    response.setHeader("Content-Transfer-Encoding", "binary");
}


URLEncoder 를 사용하여 encode 한다. 파일 다운로드 시 공백이 + 로 표현 되기 때문에 replaceAll 을 사용하여 추가로 변환 하면 해결 된다.


참고 사이트


파일 업로드 시 파일을 첨부 했다면 문제가 발생하지 않는다. 첨부 하지 않고 빈값으로 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) 하여 메일을 보내 보자.


참고 사이트


Java Property 는 기본적으로 String 으로 인식 하기 때문에, 다른 타입의 값을 setter 하는 경우 SpringExpressionLanguage (SpEL) 문법을 사용하여 설정 파일에서 타입 캐스팅을 해줘야 사용이 가능 하다.


<beans:property name="port" value="#{T(java.lang.Integer).parseInt(config['key'])}" 


Primitive Type 은 Null 값을 허용할 수 없기 때문에, Wrapper 클래스로 타입 캐스팅을 해서 사용 하자. Web 환경에서는 Null 값을 setter 할 수 없어 예외 상황이 발생 하기도 한다.


Primitive Wrapper Class Constructor Argument
boolean Boolean boolean or String
byte Byte byte or String
char Character char
int Integer int or String
float Float float, double or String
double Double double or String
long Long long or String
short Short short or String


Wrapper 클래스 종류는 위 표를 참고 하자.


참고 사이트


Spring MVC 를 사용하여 Web Application 을 개발 할 때에, 통상 Database 와 연동을 해서 개발 한다. 데이터의 시발점이기도 하기 때문에, 문제가 발생 하는 경우 다른 무엇보다 최우선으로 확인 한다. 이 때 필요한 것은 로그 이다. 개발자의 생명줄 이나 다름없다.


log4jdbc 는 로그를 상세하게 출력 한다. 라이브러리를 추가 하자. 0.2.7 이 stable 버전 이다.


<dependency>
    <groupId>org.lazyluke</groupId>
    <artifactId>log4jdbc-remix</artifactId>
    <version>0.2.7</version>
</dependency>


그 다음은 DataSource 이다. driverClassName 을 변경 하면 지원 하는 Database 가 제한적 이기 때문에, 보존 하면서 설정 하는 방법 이다.


<?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:beans="http://www.springframework.org/schema/beans"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">

    <!-- ConnectionPool Information -->
    <beans:bean id="dataSourceSpied" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <beans:property name="driverClassName" value="#{config['jdbc.driver']}" />
        <beans:property name="url" value="#{config['jdbc.url']}" />
        <beans:property name="username" value="#{config['jdbc.username']}" />
        <beans:property name="password" value="#{config['jdbc.password']}" />

        <beans:property name="defaultAutoCommit" value="false"/>

        <beans:property name="initialSize" value="1"/>
        <beans:property name="maxActive" value="1"/>
        <beans:property name="maxIdle" value="1"/>
        <beans:property name="maxWait" value="20000"/>
        <beans:property name="logAbandoned" value="true"/>
        <beans:property name="removeAbandoned" value="true"/>
        <beans:property name="removeAbandonedTimeout" value="60"/>
        <beans:property name="validationQuery" value="SELECT 1 FROM DUAL" />
        <beans:property name="testOnBorrow" value="true" />
        <beans:property name="testOnReturn" value="true" />
    </beans:bean>

    <beans:bean id="dataSource" class="net.sf.log4jdbc.Log4jdbcProxyDataSource">
        <beans:constructor-arg ref="dataSourceSpied" />
        <beans:property name="logFormatter">
            <beans:bean class="net.sf.log4jdbc.tools.Log4JdbcCustomFormatter">
                <beans:property name="loggingType" value="MULTI_LINE" />
                <beans:property name="margin" value="19" />
                <beans:property name="sqlPrefix" value="SQL:::" />
            </beans:bean>
        </beans:property>
    </beans:bean>
</beans>


Database 연결 정보를 가지고 있는 dataSource 는 Proxy 클래스를 생성하여 추가적으로 로그를 처리하는 부분이 구현 되어 있으며, Log4jdbcProxyDataSource 는 DataSource 를 상속 형태로 구현하여 확장되어 있다. CGLIB Proxy 라고 불 수 있다.


<logger name="jdbc.connection">
    <level value="info" />
</logger>
<logger name="jdbc.sqltiming">
    <level value="debug" />
</logger>
<logger name="jdbc.resultsettable">
    <level value="info" />
</logger>


log4j.xml 에 위 내용을 추가 하자. 접속 정보, Query, 응답 데이터에 대한 상세 로그가 출력 된다.


2014-10-23 04:19:39,130  INFO connection - 17. Connection opened
2014-10-23 04:19:39,130 DEBUG getBoardList - ooo Using Connection [net.sf.log4jdbc.ConnectionSpy@378c3efb]
2014-10-23 04:19:39,130 DEBUG getBoardList - ==>  Preparing: select no, title, content, create_dt from tb_board
2014-10-23 04:19:39,130 DEBUG getBoardList - ==> Parameters: 
2014-10-23 04:19:39,132 DEBUG sqltiming -  sun.reflect.GeneratedMethodAccessor36.invoke(null:-1)
17. select no, title, content, create_dt from tb_board  {executed in 1 msec}
2014-10-23 04:19:39,135  INFO resultsettable - |-------|--------|---------|----------------------|
2014-10-23 04:19:39,135  INFO resultsettable - |NO     |TITLE   |CONTENT  |CREATE_DT             |
2014-10-23 04:19:39,135  INFO resultsettable - |-------|--------|---------|----------------------|
2014-10-23 04:19:39,135  INFO resultsettable - |1      |title1  |ko       |2014-10-07 12:04:16.0 |
2014-10-23 04:19:39,136  INFO resultsettable - |2      |title2  |en       |2014-10-07 12:04:24.0 |
2014-10-23 04:19:39,136  INFO resultsettable - |3      |title3  |cn       |2014-10-07 12:04:31.0 |
2014-10-23 04:19:39,136  INFO resultsettable - |-------|--------|---------|----------------------|
2014-10-23 04:19:39,138  INFO connection - 17. Connection closed


Database 와 연결 되는 시나리오가 상세하게 로그로 출력 되고 있어 개발, 검증 단계에서 편리하게 사용할 수 있다.


실 서버 적용 에는 sqltiming 부분만 사용하면 장애 대응이나 데이터에 대한 문의도 편리하게 대응이 가능 하다.


log4jdbc 를 사용할 수 있는 환경이라면, 주저 말고 사용 해 보자.


참고 사이트


Java Web Application 은 /webapps 하위 에 있는 /WEB-INF 는 직접 적으로 접근이 불가능 하기 때문에 Servlet 을 통과하게 되어 있다. 단순하게 페이지만 보여주는 용도라면 모든 case by case 를 Controller 에 작성 하는 것은 불편 하기 때문에 view-controller 를 사용하면 편리하게 개발이 가능 하다.


dispatcher-servlet 를 설정 하는 xml 에 추가를 하자.


<mvc:view-controller path="/" view-name="/sample"/>


path, view-name 을 인자로 받는다. path 는 @RequestMapping 에서 사용 했던 value 값 이고, view-name 은 ModelAndView 에 setViewName(String viewName) 이라고 보면 된다.


<mvc:view-controller path="/" view-name="redirect:sample/"/>


redirection 하는 경우에는 view-name 의 앞에 redirect: 를 작성 하자.


Java Web Application 은 build 하면 war 파일로 압축 되어 생성 된다. manager 기능을 사용하면 web page 에서 upload 하여 편리하게 deploy 할 수 있다. 하지만 기능이 제한되는 단점이 있다.


manager 기능을 이용한 배포는 해당 링크를 참고 하자. (http://blog.whitelife.co.kr/73)


직접 deploy 하려면 server.xml 파일을 설정 해야 하고, $CATALINA_HOME 환경 변수 설정이 되어 있어야 한다. tomcat 의 start, stop, deploy, undeploy 를 $CATALINA_HOME/bin/catalina.sh 가 담당 하는대, $CATALINA_HOME 가 정의 되어 있지 않으면, 아무 동작도 할 수 없다.


windows 는 고급 시스템 설정에 환경 변수, linux 는 home directory 의 ~/.bashrc 파일에 추가 하자.


  • 환경 변수



  • ~/.bashrc

# User specific aliases and functions

export JAVA_HOME=/home/whitelife/java
export CATALINA_HOME=/home/whitelife/tomcat
export PATH=$PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin



server.xml 은 $CATALINA_HOME/conf 에 있는대, 이 정보를 기반으로 tomcat 은 동작 한다.


  • server.xml
<Host name="localhost"  appBase="webapps"
    unpackWARs="true" autoDeploy="true">

    <!-- SingleSignOn valve, share authentication between web applications
         Documentation at: /docs/config/valve.html -->
    <!--
    <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
    -->

    <!-- Access log processes all example.
         Documentation at: /docs/config/valve.html
         Note: The pattern used is equivalent to using pattern="common" -->
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
           prefix="localhost_access_log." suffix=".txt"
           pattern="%h %l %u %t &quot;%r&quot; %s %b" />

    <!--<Context docBase="static" path="/static" reloadable="true"/>-->
    <Context docBase="test" path="/" reloadable="true"/>

</Host>


appBase, docBase 에 대해 알아야 구체적인 설정을 할 수 있다. appBase 는 상대 경로로 적용 되어 있으면, $CATALINA_HOME/webapps 를 참조 한다. 설치 후 고양이 마크를 볼 수 있는 화면도 하위 폴더인 /ROOT 에 있다. 만약 다른 경로를 사용하고 싶다면, 절대 경로로 설정 한다. docBase 는 appBase 하위 폴더 설정 이라고 보면 되는대, 기본적으로 appBase 에 ROOT Web Application 이 deploy 된다. 추가 적으로 deploy 하는 경우에 사용한다고 보면 이해가 될 것이다. 그 때 사용하는 tag가 context 이다.


docBase: appBase 하위 경로
path: host 뒤에 정의 되는 prefix (ex: path=”/test”, http://localhost:8080/test)
reloadable: class 파일 수정 시 redeploy 여부


deploy 를 예로 들면, appBase 경로에 docBase 이름을 가진 test.war 파일을 복사 하고, $CATALINA_HOME/bin 에 있는 startup 을 실행 한다. windows 인 경우 확장자는 bat, linux 인 경우 sh 로 한다. 서버가 시작 되면, 압축이 풀리면서 deploy 가 진행 된다.


테스트 페이지에 접속 하면 deploy 여부를 확인 할 수 있을 것 이다.


지금부터는 경험담 이다. appBase 를 상대경로로 하고, docBase 에 원하는 Web Application 을 deploy 했다. 문제점이 생길떄도 있고, 아닌 경우도 있었다. ROOT Web Application 으로 인식 하고 최초에는 ROOT 로 deploy 하고, 그 후에 deploy 를 한다. 두번 deploy 되어 라이브러리 충돌 하는 경우가 생겼다. 최근에 참 두려운 Exception 으로 자리잡은 ClassCastException 이다. Class는 존재 하는대, 무엇으로 Cast 할지 판단을 하지 못하여 발생 하는 것 같다. 정말 충돌 나는 지 확인을 위해 두가지 테스트를 했다.


  1. war deploy
  2. war 압축을 풀고 수동 deploy

2번 방법으로 했을때에는 이미 압축이 풀려 있기 때문에, ROOT deploy 를 하지 않았다. 정상 동작 하였다.


해결을 위해서는 appBase 만 설정 하고, ROOT.war 로 파일 명을 지정하여 deploy 하거나, 압축을 풀고 직접 수동으로 deploy 하면 된다. 취향에 따라 방법을 선택 하자.


Maven 을 사용 하는 경우에는 pom.xml 파일에 finalName 을 추가하여 mvn package 하면 원하는 파일명으로 war 파일을 얻을 수 있다.


<build>
    <finalName>ROOT</finalName>
</build>


+ Recent posts