MappingJacksonJsonView에 취소선이 가있었다. 이것이 삽질의 계기였다.

소스코드에서는 Jackson2로 이동해달라는 요청이 있길래 넘어갔다.

그런데 이게... 생각보다 여파가 컸다...

가장 큰 놈이... spring-data-redis.

이 녀석은 아직 Jackson2RedisSerializer가 없어서 Jackson과 Jackson2를 프로젝트에 함께 사용해야하는 문제가 있다.

spring-data-redis 1.2.0이 나올 때까지는 Jackson2로 갈아엎는 것은 보류다.

구글 SMTP를 사용하다가, 고객의 요구에 따라 네이버나 다음으로 변경하려고 했는데...
smtp 로 메일 발송테스트를 하는데 포트만 바꾸면 될 줄 알았더니... 아니더라.
상당한 삽질을 통해 확인한 결과는 다음과 같다.

application.properties

mail.host = smtp.gmail.com
mail.port = 587
mail.protocol = smtp

mailService 빈설정

<bean class="...MailServiceImpl">
        <constructor-arg name="mailSender">
            <bean class="org.springframework.mail.javamail.JavaMailSenderImpl"
                  p:password="${mail.password}"
                  p:host="${mail.host}"
                  p:port="${mail.port}"
                  p:protocol="${mail.protocol}"
                  p:username="${mail.username}"
                  p:defaultEncoding="${mail.encoding}">
                <property name="javaMailProperties">
                    <props>
                        <prop key="mail.smtp.starttls.enable">${mail.smtp.starttls.enable}</prop>
                        <prop key="mail.smtp.auth">${mail.smtp.auth}</prop>
                    </props>
                </property>
            </bean>
        </constructor-arg>
    </bean>

네이버 메일 테스트 시

mail.xml 파일의 MailServiceImpl 빈 설정을

<bean class="...impl.MailServiceImpl">
    <constructor-arg name="mailSender">
        <bean class="org.springframework.mail.javamail.JavaMailSenderImpl"
            p:password="${mail.password}" p:host="${mail.host}" p:port="${mail.port}"
            p:protocol="${mail.protocol}" p:username="${mail.username}"
            p:defaultEncoding="${mail.encoding}">
            <property name="javaMailProperties">
                <props>
                    <prop key="mail.smtp.starttls.enable">${mail.smtp.starttls.enable}</prop>
                    <prop key="mail.smtp.auth">${mail.smtp.auth}</prop>
                    <prop key="mail.smtps.ssl.checkserveridentity">true</prop>
                    <prop key="mail.smtps.ssl.trust">*</prop>
                </props>
            </property>
        </bean>
    </constructor-arg>
</bean>

의 형태로 변경한다. 기존 설정내용과의 차이는

<prop key="mail.smtps.ssl.checkserveridentity">true</prop>
<prop key="mail.smtps.ssl.trust">*</prop>

그리고 application.properties의 내용을 다음과 같이 변경한다.

mail.host = smtp.naver.com
mail.port = 465
mail.protocol = smtps

테스트를 위해서 작성된 MailServiceImplTest를 실행하여 확인하다.
테스트를 실행하기 위해서는 MailServiceImplTest-context.xml 의 내용을 다음과 같이 변경하시고,

<bean class="...MailServiceImpl">
    <constructor-arg name="mailSender">
        <bean class="org.springframework.mail.javamail.JavaMailSenderImpl"
            p:password="${mail.password}" p:host="${mail.host}" p:port="${mail.port}"
            p:protocol="${mail.protocol}" p:username="${mail.username}"
            p:defaultEncoding="${mail.encoding}">
            <property name="javaMailProperties">
                <props>
                    <prop key="mail.smtp.starttls.enable">${mail.smtp.starttls.enable}</prop>
                    <prop key="mail.smtp.auth">${mail.smtp.auth}</prop>
                    <prop key="mail.debug">true</prop> <!-- 이건 테스트를 위한 디버그 내용 확인을 위한 겁니다.  -->
                    <prop key="mail.smtps.ssl.checkserveridentity">true</prop>
                    <prop key="mail.smtps.ssl.trust">*</prop>
                </props>
            </property>
        </bean>
    </constructor-arg>
</bean>

test의 setFrom의 메일 계정은 로그인에 사용된 계정과 동일하게 변경한다.


다음 메일 SMTP 테스트 시

daum으로 발송할 경우에는, MailServiceImplTest 를 다음과 같이 변경하고

<bean class="...MailServiceImpl">
    <constructor-arg name="mailSender">
        <bean class="org.springframework.mail.javamail.JavaMailSenderImpl"
            p:password="${mail.password}" p:host="${mail.host}" p:port="${mail.port}"
            p:protocol="${mail.protocol}" p:username="${mail.username}"
            p:defaultEncoding="${mail.encoding}">
            <property name="javaMailProperties">
                <props>
                    <prop key="mail.smtp.starttls.enable">${mail.smtp.starttls.enable}</prop>
                    <prop key="mail.smtp.auth">${mail.smtp.auth}</prop>
                    <prop key="mail.debug">true</prop> <!-- 이건 테스트를 위한 디버그 내용 확인을 위한 겁니다.  -->
                </props>
            </property>
        </bean>
    </constructor-arg>
</bean>

application.properties

mail.host = smtp.daum.net
mail.port = 465
mail.protocol = smtps

메일을 발송할 때에는, from 이메일 주소는 로그인에 사용된 메일주소와 동일해야 한다. protocol이 smtp가 아니라 smtps 이다. JavaMail에서 접근방식이 다를 줄은 몰랐다.

protocol이 'smtps'여야 SSL 설정이 활성화된다.

별도의 설정 프로퍼티가 있을 줄 알았는데... 크흐.


문제발생지점

엔티티 클래스에 fetch=FetchType.LAZY로 선언해놓은 배열이나 객체를 사용하다 보면, 빈 배열로 선언되어 있는 경우,

Exception in thread "threadPoolTaskExecutor-5" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: ...{collection}..., could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:566)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:186)
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:545)
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:124)
    at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:266)
    ...{중략}...
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)

위와 같은 예외가 발생한다.


처리방법

public final class Hibernate {
...
    /**
     * Force initialization of a proxy or persistent collection.
     * <p/>
     * Note: This only ensures intialization of a proxy object or collection;
     * it is not guaranteed that the elements INSIDE the collection will be initialized/materialized.
     *
     * @param proxy a persistable object, proxy, persistent collection or <tt>null</tt>
     * @throws HibernateException if we can't initialize the proxy at this time, eg. the <tt>Session</tt> was closed
     */
    public static void initialize(Object proxy) throws HibernateException {
        if ( proxy == null ) {
            return;
        }
        else if ( proxy instanceof HibernateProxy ) {
            ( ( HibernateProxy ) proxy ).getHibernateLazyInitializer().initialize();
        }
        else if ( proxy instanceof PersistentCollection ) {
            ( (PersistentCollection) proxy ).forceInitialization();
        }
    }
...
}

위에 설명한 Hibernate.initialize(대상 오브젝트나 컬렉션) 으로 lazy 컬렉션을 지정해주면 proxy 처리를 하면서 lazy 객체들을 미리 호출해놓는 것으로 보인다.

Aspect Logger Sample

package com.sil.docsflow.common.support.spring;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created with IntelliJ IDEA.
 *
 * @author: ihoneymon
 * Date: 14. 1. 10
 */
public class AspectLogger {
    private static final Logger logger = LoggerFactory.getLogger(AspectLogger.class);

    public void afterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
        String signatureInfo = getSignatureInfo(joinPoint);
        String exceptionMessage = exception.getMessage();
        if (exceptionMessage == null || exceptionMessage.trim().length() < 1) {
            exceptionMessage = "oops! occured exception";
        }

        logger.debug("=>> ### " + signatureInfo + " : " + exceptionMessage, exception);
        logger.warn("<<= ### " + signatureInfo + " : " + exceptionMessage);
    }

    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        String signatureInfo = getSignatureInfo(joinPoint);

        logger.debug("=>> " + signatureInfo);
        Object retVal = joinPoint.proceed();
        logger.debug("<<= " + signatureInfo + (retVal != null ? " : " + retVal : ""));

        return retVal;
    }

    private String getSignatureInfo(JoinPoint joinPoint) {
        String signatureName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();

        StringBuilder sb = new StringBuilder();
        sb.append(className).append('.').append(signatureName).append('(');

        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; i++) {

                if (args[i] instanceof String) sb.append('\"');
                sb.append(args[i]);
                if (args[i] instanceof String) sb.append('\"');

                if (i < args.length - 1) {
                    sb.append(',');
                }
            }
        }
        sb.append(')');

        return sb.toString();
    }
}

일반적으로 사용가능한 Aspect Logger.

이를 사용하기 위한 logging.xml

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

    <aop:aspectj-autoproxy/>

    <bean id="loggingAspect" class="{package}.AspectLogger"/>
    <aop:config>
        <aop:aspect ref="loggingAspect">
            <aop:pointcut id="loggingPointCut" expression="execution(* {package}.{class}({method})) $amp;$amp; !execution(* {package}.AopLogger(..))"/>
            <aop:around method="aroundAdvice" pointcut-ref="loggingPointCut"/>
            <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="loggingPointCut" throwing="exception"/>
        </aop:aspect>
    </aop:config>
</beans>

위의 설정에서 빼먹지 말 것은, <aop:pointcut id="loggingPointCut" expression="execution(* {package}.{class}({method})) $amp;$amp; !execution(* {package}.AopLogger(..))"/> 에서 !execution 이다. AspectLogger 자신을 포인트컷에서 제외시켜주어야 한다. 다른 곳에서는 별다른 문제가 없었는데… 지금 개발하고 있는 프로젝트에서 끊임없이 자신을 물고 들어가면서 로그를 찍다가 예외들을 뱉는다. 그래서 묻어두었다가 logger를 입력하는 것이 귀찮아서 다시 AspectLogger를 꺼내어 들었다.

요즘 많이 사용되는 AOP Logging. 현재 작업 중인 프로젝트에서도 사용중인데, 최근 기능을 추가하면서 문제가 발생했다.


● 문제발생

현재 사용중인 프로젝트에서도 AspectLogging 기법을 사용했다. 그런데, class cast를 하는 과정에서 지속적으로

Exception in thread "main" java.lang.ClassCastException: $Proxy11 cannot be cast to classA

라고 하는 문제가 발생하면서 로깅클래스에서 동작이 멈추는 증상이 나타났다. 물론…
AOP측에서 이에 대한 예외처리를 제대로 했으면 별문제가 없었겠지만,


● 문제요인

문제가 발생하는 부분을 유심히 살펴봤다. 스프링의 ReloadableResourceBundleMessageSource를 확장extends하여 간단한 메소드를 추가한 ResourceBundleMessageSource클래스로 class casting을 하는 부분에서 문제가 발생하고 있었다.


● 힌트

이 문제가 왜 생겼는지 인터넷 검색에 들어간다.

그러다가 발견한 힌트!

classcastexception-proxy-cannot-be-cast-to-using-aop - Stackoverflow

제일 마지막 부분에

a good article about proxy creation in Spring

이 글을 보고 깨달음을 얻었다.


● 해결책

  1. BundleMessageSource 라고 하는 인터페이스를 만들고, ResourceBundleMessageSource 클래스에서 implements 로 구현한다고 선언.
  2. ResourceBundleMessageSource 를 class casting 하는 부분을 BundleMessageSource 로 변경하였다.
  3. OK~!

● 정리

… proxy에서 효과적인 캐스팅 방식은, ‘클래스 캐스팅을 사용할 경우 인터페이스를 선언하여 구현하고 인터페이스로 캐스팅하는 것’ 이다.

+ Recent posts