프레임워크 개발이란 이미 있는 기술을 조합해서 어떻게 쓸지 결정하고, 툴이나 공통 모듈 정도를 만들어놓는 것이 아니다. 프레임워크란 애플리케이션의 코드가 효율적인 방법으로 개발돼서 사용될 수 있도록 새로운 틀framework를 만드는 작업이다. 스프링은 그 자체로 완벽한 프레임워크이지만 동시에 각 환경에 맞는 더 나은 프레임워크를 개발할 수 있게 해주는 이상적인 기반 프레임워크 이기도 하다. 각자 환경에 맞게 필요한 기능을 확장해서 사용할 이유가 없다면 스프링이 이토록 장황하게 확장 포인트를 정의하고 유연한 전략 패턴을 적용해놨을 리가 없지 않은가?

  • 토비의 스프링 3.1, 3장 스프링 웹 기술과 스프링 MVC 423p

아무런 생각없이 사용하던 프레임워크.

이제 그 안에 담긴 것들을 맛보기 시작할 수 있게 되었을까? 아직 멀었다.

구글 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를 꺼내어 들었다.

  • 참고 : A successful git branching model

  • 깃git의 기본 브랜치는 master에서 시작합니다.

  • master는 태그Tag를 기록하기 위한 기본브랜치로 사용됩니다.
  • 개발을 위해서는 develop 브랜치를 생성합니다.

    개발자들이 실제로 소스코드를 공유하는 브랜치가 됩니다.

    //master 브랜치에서 develop 브랜치를 생성합니다.
    (master)$ git checkout -b develop
    (develop)$
    
  • 소스코드를 push하기 전에 하는 기본적인 동작방식은 다음과 같습니다.

    1. 소스코드를 스태징 상태로 변경
      (develop)$ git add .
      
    2. 소스코드를 커밋함 - 가급적이면 개발한 내용을 상세기록하시는 걸 추천함
      (develop)$ git commit
      이후 커밋메시지 등록
      //혹은
      (develop)$ git commit -m '소스코드 중 친구목록 반환시 기준변경'
      
    3. 원격저장소(remote repository)에서 당겨오기(pull)
      (develop)$ git pull origin develop
      
    4. 별다른 충돌conflict가 발생하지 않았다면 push
      (develop)$ git push origin develop
      
  • 기능개발시 브린치 생성

    1. 브랜치 생성 - 웹 애플리케이션 설정을 하는 단계의 브랜치
      (develop)$ git checkout -b feature/web-application-config
      (feature/web-application-config)$
      
    2. 소스코드 변경작업 후 스태징 상태로 변경
      (feature/web-application-config)$ git add .
      
    3. 소스코드 커밋
      (feature/web-application-config)$ git commit -m '웹 애플리케이션 설정완료 web.xml 등 설정'
      
    4. develop 브랜치로 체크아웃
      (feature/web-application-config)$ git checkout develop
      (develop)$
      
    5. feature/web-application-config 브랜치 병합merge
      (develop)$ git merge feature/web-application-config
      
    6. feature/web-application-config 브랜치 삭제
      (develop)$ git branch -d feature/web-application-config
      

이런 형태로 기능개발하실 때에 기능 브랜치(feature)를 생성해서 작업하신 후에 커밋하시고 develop 브랜치로 체크아웃하셔서 머지merge를 해주시면 큰 충돌없이 develop에서 소스코드를 유지하실 수 있을겁니다.

깃에 익숙치 않으시다면, 소스트리sourcetree와 같은 GUI 클라이언트를 사용하시는 것을 추천드립니다.

+ Recent posts