@Repository 애노테이션을 사용한 곳에 예외변환 AOP를 적용해서 JPA 예외를 스프링프레임워크가 추상화한 예외로 변환하도록 처리

/**
 * 스프링 JPA 예외변환기 적용 <code>@Repository</code> 사용한 곳에 예외변환 AOP를 적용
 *
 * @return
 */
@Bean
public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}


JPARepository(org.springframework.data.jpa.repository.JpaRepository) 인터페이스를 구현해도 적용...

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

import org.junit.Test;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;

/**
 * Quartz CronExpression 정규식 처리를 이해하기 위한 테스트 코드
 * 
 * @author ihoneymon
 * @see http://quartz-scheduler.org/api/2.2.0/org/quartz/CronExpression.html
 */
public class QuartzCronExpressionTest {

    /**
     * Null은 예외발생
     */
    @Test(expected = IllegalArgumentException.class)
    public void testNullCronExpressionException() {
        assertThat(CronExpression.isValidExpression(null), is(false));
        fail();
    }

    /**
     * 빈값이나 cron 정규식 표현이 아니라면 실패
     */
    @Test
    public void testEmptyCronExpressionException() {
        assertThat(CronExpression.isValidExpression(""), is(false));
        assertThat(CronExpression.isValidExpression("1 1+2 1*2"), is(false));
        assertThat(CronExpression.isValidExpression("testWrongChronExpression"), is(false));
    }

    /**
     * Quartz CronExpression 중에 DayOfMonth와 DayOfWeek에 대한 확인 DayOfMonth와
     * DayOfWeek의 표현식에 대해서는 신경을 써야한다.
     * 
     * Second Minute Hour DayOfMonth Month DayOfWeek (optional)Year
     */
    @Test
    public void testDayOfMonthAndDayOfWeekCronExpression() {
        /**
         * same DayOfMonth & DayOfMonth is valid fail DayOfMonth & DayOfMonth is
         * all: false DayOfMonth & DayOfMonth is none: valid fail
         */
        assertThat(CronExpression.isValidExpression("* * * * * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * ? * ?"), is(false));

        /**
         * DayOfMonth is all and DayOfMonth is none: valid success DayOfMonth is
         * all and DayOfMonth is define: valid fail
         */
        assertThat(CronExpression.isValidExpression("* * * * * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * * * MON"), is(false));
        assertThat(CronExpression.isValidExpression("* * * * * 2"), is(false));
        assertThat(CronExpression.isValidExpression("* * * * * 2,5"), is(false));

        /**
         * DayOfMonth is none and DayOfMonth is all: valid success DayOfMonth
         * required none DayOfMonth DayOfMonth가 none(?)인 경우에는 DayOfMonth에 허용되는
         * 정규식은 대부분 통과
         */
        assertThat(CronExpression.isValidExpression("* * * ? * *"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON-FRI"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON-FRI,SUN"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * MON#2"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * 2#2"), is(true));
        assertThat(CronExpression.isValidExpression("* * * ? * 2#2,1#2"), is(false));

        /**
         * DayOfMonth에
         */
        assertThat(CronExpression.isValidExpression("* * * 1 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1-3 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1,6,9 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 1-3 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 1,6,9 * FRI"), is(false));
        /**
         * DayOfMonth use L(last) special character
         */
        assertThat(CronExpression.isValidExpression("* * * L * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * L * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L * MON"), is(false));
        /**
         * DayOfMonth에 L은 단독으로 사용되거나 '-nth day'만 허용
         */
        assertThat(CronExpression.isValidExpression("* * * 15L * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L15 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15L * MON"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15L * ?"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L-3 * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * L-3 * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * L-3 * MON"), is(false));

        /**
         * DayOfMonth use W(week: mon-fri) special character
         */
        assertThat(CronExpression.isValidExpression("* * * W * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15W * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * 15W * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 15W,16W * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 15W-17W * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 15W/2W * ?"), is(true));

        /**
         * DayOfMonth use LW(last-week: mon-fri) special character
         */
        assertThat(CronExpression.isValidExpression("* * * LW * *"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW * ?"), is(true));
        assertThat(CronExpression.isValidExpression("* * * 1LW * ?"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW2 * ?"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW * MON"), is(false));
        assertThat(CronExpression.isValidExpression("* * * LW * 2"), is(false));
    }

    @Test(expected = RuntimeException.class)
    public void testCronBuilder() {
        assertThat(CronExpression.isValidExpression("* * * LW * *"), is(false));
        CronScheduleBuilder.cronSchedule("* * * LW * *").build();
    }
}

CronExpression.isValidExpression() 에서 false가 발생한 정규식을 CronScheduleBuilder.cronSchedule()를 사용하여 스케줄을 생성하려하면 RuntimeException이 발생한다.

Spring Boot: Velocity 한글깨짐(Encoding) 문제

스프링부터SpringBoot를 이용한 프로젝트에서 TemplateViewEngine으로 Velocity를 선택했다.

@Bean
public ViewResolver viewResolver() {
    VelocityViewResolver viewResolver = new VelocityViewResolver();
    viewResolver.setPrefix("classpath:/templates");
    viewResolver.setSuffix(".vm");
    viewResolver.setOrder(Ordered.LOWEST_PRECEDENCE - 20);
    return viewResolver;
}

이 설정만 해서는 Velocity가 인코딩 설정을 제대로 하지 못한다.

스프링부트 설정에 application.yml을 이용했다. YAML을 설정DSL로 채택했는데, 설정이 무척 간결해진다.
Velocity와 관련된 설정은

spring:
  velocity:
    properties:
      input.encoding: UTF-8
      output.encoding: UTF-8

다음과 같이 해주면, Velocity 스프링 설정은 끝.

혹은 별도로 Velocity에 관한 설정을 하는 방법은 다음과 같다.

1. velocity.properties 를 이용하거나

<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  <property name="configLocation" value="classpath:/velocity.properties"/>
</bean>
resource.loader = file

file.resource.loader.description = Velocity File Resource Loader
file.resource.loader.class = org.apache.velocity.runtime.resource.loader.FileResourceLoader
file.resource.loader.path = .
file.resource.loader.cache = false
file.resource.loader.modificationCheckInterval = 2

input.encoding=UTF-8
output.encoding=UTF-8

2. 스프링 빈 설정을 하면 된다.

스프링 빈으로 설정하는 방법은

1. JavaConfig 빈 설정

@Bean
public VelocityConfigurer velocityConfigurer() {
    VelocityConfigurer configurer = new VelocityConfigurer();
    configurer.setResourceLoaderPath("classpath:/templates");
    Properties properties = new Properties();
    properties.setProperty("input.encoding", "UTF-8");
    properties.setProperty("output.encoding", "UTF-8");
    configurer.setVelocityProperties(properties);
    return configurer;
}

2. XML 빈 설정

<bean id="velocityConfigurer" class="import org.springframework.web.servlet.view.velocity.VelocityConfigurer">
    <property name="resourceLoaderPath" value="classpath:/~~"/>
    <property name="velocityProperties">
        <props>
            <prop key="input.encoding">UTF-8</prop>
            <prop key="output.encoding">UTF-8</prop>
        </props>
    </property>
</bean>

두 가지 방법이 있다.



그냥 벨로시티를 사용하기 싫었는데...

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

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

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

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

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

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

문제발생지점

엔티티 클래스에 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 객체들을 미리 호출해놓는 것으로 보인다.

+ Recent posts