잊지 않겠습니다.

querydsl 4.x대의 경우, 다음 링크 글을 참고해주세요.

http://netframework.tistory.com/entry/gradle-%EC%A0%95%EB%A6%AC-queryDSL-code-generate-v-4x


기존 Gradle을 이용한 개발환경구성글에서 다음이 변경되었습니다.

Gradle 2.0의 지원

Gradle 2.0에 호환되도록 Project를 변경했습니다. Gradle 2.0의 가장 큰 변화는 groovy 문법의 변경으로 인한 += operator가 변경된 점입니다. 기존 sourceSet에 추가 path를 할 때, 코드는

sourceDir += file('src/main/generated')

로 구성되었지만, groovy의 변경으로 += operator는 모두 같은 형태의 객체들에서만 사용되도록 변경되었습니다. 그래서 이 부분을 다음과 같이 수정하면 됩니다.

sourceDir += [ file('src/main/generated') ]

Java 8의 지원

java 8이 지원될 수 있도록 souceCompatibility와 targetCompatibility가 모두 변경되었습니다.

build.gradle의 간소화

기존에는 base.gradle, domain.gradle과 같이 여러개의 build.gradle 파일로 나뉜 상태를 간소화시켰습니다.
build.gradle과 각 module에서 필요한 gradle 파일을 각기 나누어 처리하도록 변경하였습니다.


web deploy container의 변경

기존 gradle tomcat plugin에서부터 gretty로 변경했습니다. gretty의 특징은 다음과 같습니다.

  • tomcat/jetty 지원
  • tomcat8, jetty9 지원
  • hot-deploy의 지원
  • jvmargs의 지원

무엇보다도 기존 gradle tomcat plugin보다 사용이 편리한 것이 장점입니다. web server를 실행시키는 명령어는 다음과 같습니다.

gradle :privateWeb:jettyRun   //Jetty9을 이용한 web server 실행
gradle :privateWeb:tomcatRun  //tomcat8을 이용한 web server 실행

git hub에 코드를 올려뒀습니다. 모두들 Happy coding~

https://github.com/xyzlast/gradle_multi_project_test

Posted by Y2K
,

Spring을 사용할 때, Proxy mode에서의 @Transactional의 문제점에 대해서 전 글에서 알아봤습니다.

Proxy 모드에서의 가장 큰 문제는 @Transaction이 적용되지 않은 method 내부에서의 @Transaction의 호출입니다. 이 문제를 해결하기 위해서는 Proxy mode에서의 @Transaction이 아닌, AspectJ mode에서 @Transaction을 사용해줘야지 됩니다.

AspectJ mode는 2가지 방법을 제공합니다.

Load-Time Weaver

객체를 Load 할때, AspectJ에 의해서 wearving된 객체를 넘겨주는 방식입니다. 아래와 같은 방식으로 동작하게 됩니다.

  1. application context에 로드된 객체의 loading
  2. aspectj weaver에 의한 객체 weaving (@Transaction annotation이 있는 class, method에 대한 transaction 처리가 된 객체로 변경)
  3. 객체의 이용

위 순서를 보시면 아실 수 있듯이, 이는 객체의 사용에 대해서 약간의 performance의 하락을 가지고 오게 됩니다. application context에서 객체를 load 할 때, aspectj weaver에서 하는 또 다른 일들을 지정하게 됩니다.

Compile-Time Weaver

객체를 Load 할 때, 위와 같은 문제가 있기 때문에, compile 시에 aspectj에서 간섭해서 필요한 객체에 weaving을 시켜서 class를 만들어내는 방식입니다. 이렇게 되면, application context의 load시에 다른 절차가 없기 때문에, performance의 하락도 없는 거의 완벽한 방법으로 구성이 가능합니다.

LTW vs CTW

Load-Time Weaver의 단점은 다음과 같습니다.

  1. application context에 객체가 로드될 때, aspectj weaver와 spring-instrument에 의한 객체 handling이 발생하기 때문에 performance가 저하된다.
  2. web container의 실행시, LTW를 위한 설정이 필요하다.

반면에 Compile-Time Weaver의 단점은 다음과 같습니다.

  1. 개발환경의 구성이 어렵다.
  2. lombok과 같은 compile시에 간섭하는 여러 plugin들과의 매우 다채로운 충돌이 발생한다. 특히 lombok과는 같이 사용하지 못한다고 생각해도 과언이 아니다.

LTW는 운영상의 문제를 발생시킬 수 있고, CTW는 개발상의 문제를 발생시킬 수 있다는 생각이 듭니다. (이런 생각이 들면 무조건 CTW로 가야지 되긴 하는데….;;;)

LTW를 이용한 @Transaction의 처리

먼저 DomainConfiguration에 LTW를 이용한 @Transaction을 다음과 같이 지정해줍니다.

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ, order = 0)

그리고, LTW를 이용하기 위해서 LTW를 활성화시켜야지 됩니다. LTW활성화는 다음과 같습니다.

@EnableLoadTimeWeaving

이것만으로 끝이 아닙니다. LTW는 반드시 다음 jvm option을 가져야지 됩니다. 위에서 서술했듯이, 객체가 로드 될 때, aspectj weaver와 spring-instrument가 각각 객체에 대한 처리를 해줘야지 됩니다. 먼저 aspectj weaver는 실질적으로 일을 하는 객체들이 모여있고, spring-instrument의 경우에는 aspectj weaver에 class loader를 위임하는 일을 맡아서 하게 됩니다. jvm argument에 다음 option을 추가시켜줍니다.

-javaagent:/fullpath/aspectjweaver-1.8.1.jar
-javaagent:/fullpath/spring-instrument-4.0.6.RELEASE.jar

aspectjweaver와 spring-instrument의 뒤에 붙는 버젼은 aspectj와 spring의 버젼과 동일합니다.

intelliJ

intelliJ를 사용하고 있고, JUnit test를 돌리는 경우에는 Run/Debug Configuration에 다음 설정을 추가해줘야지 됩니다.

VM Option에 jvm argument를 default로 넣어주고, 실행시 언제나 사용하도록 구성되어야지 됩니다.

gradle

gradle의 test 시에 역시 다음 jvmargs가 필요하기 때문에 다음과 같은 설정이 필요합니다.

    test {
        jvmArgs '-javaagent:/weavers/spring-instrument-4.0.6.RELEASE.jar ' +
                '-javaagent:/weavers/aspectjweaver-1.8.1.jar'
    }
tomcat

tomcat에서 LTW를 사용하기 위해서는 2가지 방법이 있습니다. 특정 application context에서만 사용할 수 도 있고, 모든 application context에서 사용할 수 있습니다.

특정 application context에서만 LTW를 이용

먼저, spring-instrument.jar파일을 tomcat의 lib 폴더 안에 copy시켜줍니다. 그 후, application context의 context.xml안에 다음 내용을 추가합니다.

<Context path="/ltwdemo">
    <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>
모든 application context에서 LTW를 이용

tomcat 시작시, jvmargs에 -javaagent:/weavers/spring-instrument-4.0.6.RELEASE.jar를 추가해주면 됩니다.

CTW를 이용한 @Transaction의 처리

CTW를 이용하는 경우, 이는 compile시에 처리하는 것이기 때문에 code상의 변화는 거의 없습니다. @EnableLoadTimeWeaver만을 제거시켜주고, mode를 AspectJ로 설정해주면 됩니다.

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ, order = 0)

우리가 개발을 할때, compile을 하는 도구는 거의 2가지입니다. IDE와 build tool(maven, gradle, ant)입니다.

IntelliJ

setting의 compile option을 다음과 같이 변경합니다.

  • Use compiler : Ajc
  • Path to Ajc compiler : AspectJtools.jar 위치 지정
  • Command line parameters : -1.8 (JavaVersion 설정)
gradle

CTW에 대해서 open source로 plugin이 존재합니다. (https://github.com/eveoh/gradle-aspectj)
사용방법이 조금 까다롭습니다. 주의점은 다음과 같습니다.

  • ext.aspectjVersion property가 apply plugin:aspectj 보다 먼저 선언되어야지 됩니다. (파일 위치상에서 line이 더 위여야지 됩니다.)
  • spring-aspectj component가 아래와 같이 두번 선언되어야지 됩니다.
      aspectpath "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
      compile "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
    

다음은 CTW가 적용된 build.gradle의 전체 내용입니다.

apply plugin: 'java'

sourceCompatibility = 1.8
targetCompatibility = 1.8

version = '1.0'
buildscript {
    repositories {
        maven {
            url "https://maven.eveoh.nl/content/repositories/releases"
        }
    }
    dependencies {
        classpath "nl.eveoh:gradle-aspectj:1.4"
    }
}

repositories {
    mavenCentral()
}

ext {
    javaVersion = "1.8"
    springVersion = "4.0.6.RELEASE"
    springjpaVersion = "1.6.0.RELEASE"
    querydslVersion = "3.3.2"
    hibernateVersion = "4.3.4.Final"
    springsecurityVersion = "3.2.4.RELEASE"
    aspectjVersion = '1.8.1'
}

apply plugin: 'aspectj'
dependencies {
    compile 'org.slf4j:slf4j-api:1.7.6'
    compile 'org.slf4j:jcl-over-slf4j:1.7.6'
    compile 'ch.qos.logback:logback-classic:1.0.13'
    compile 'ch.qos.logback:logback-core:1.0.13'

    compile "org.springframework:spring-context:${rootProject.ext.springVersion}"
    aspectpath "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
    compile "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
    compile "org.springframework.data:spring-data-jpa:$rootProject.ext.springjpaVersion"
    compile group: 'org.apache.httpcomponents', name: 'httpclient', version:'4.2.5'
    compile 'org.apache.commons:commons-lang3:3.3.2'

    compile 'org.aspectj:aspectjrt:1.8.1'
    compile 'org.aspectj:aspectjtools:1.8.1'
    compile 'org.aspectj:aspectjweaver:1.8.1'

    testCompile "junit:junit:4.11"
    testCompile 'org.mockito:mockito-core:1.9.5'
    testCompile 'org.hamcrest:hamcrest-all:1.3'
    testCompile "org.springframework:spring-test:${rootProject.ext.springVersion}"

    compile 'mysql:mysql-connector-java:5.1.31'
}

Summary

LTW와 CTW를 이용한 @Transaction에 대해서 정리해봤습니다. 이상하게 인터넷에서 대부분의 코드가 @EnableTransactionManagement에서 mode를 바꾸면 된다. 식의 글만 있고, 명확히 어떤 일들을 해줘야지 되는지 적혀 있지 않아서 한번 정리해볼 필요성을 느껴서 작성하게 되었습니다. CTW가 모든 면에서 우월성을 가지고 있지만, 저는 lombok없는 개발은 어떻게 할지 잘 모르겠다는 생각까지 들 정도로 중독되어서… 걱정중입니다.; lombok과 CTW를 같이 사용할 방법에 대한 고민이 좀 더 필요할 것 같습니다.

Posted by Y2K
,

Spring @Transactional에 대하여.

DB에 대한 Transaction의 경계를 잡는데에 유용하게 쓰이는 Spring의 @Transaction은 Proxy mode와 AspectJ mode로 동작합니다. 일반적으로 많이 사용하는 것은 Proxy mode를 주로 사용하는데, Proxy mode를 사용하게 되는 경우, 다음과 같은 주의점이 필요합니다.

private method의 @Transaction 미적용 문제

이는 어찌보면 당연한 결과입니다. 우리는 Service 객체를 Proxy를 통해서 얻어오고, 얻어온 객체를 접근할 때 Proxy에 의하여 Transaction이 시작되기 때문에 당연히 이런 상황에서는 Transaction이 적용되지 못합니다.
이 문제는 큰 문제가 될 수 없습니다. 다만, 다음 문제는 조금 생각해봐야지 되는 문제가 발생하게 됩니다.

Transaction이 적용되지 않은 method에서 Transaction이 적용된 method를 호출할때.

소위 말하는 self call의 경우 입니다. 이 경우는 어떻게 될까요? 다음 code를 통해서 확인해보도록 하겠습니다.

먼저, 2개의 public method를 준비합니다.

public interface BookService {
    void doNotTransactionAction();
    void doTransactionAction();
}

그리고, 이에 대한 서비스 구현 코드를 다음과 같이 구성합니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

코드 내용은 매우 단순합니다. method가 호출되면 TransactionSynchronizationManager에서 현 Transaction의 이름을 확인하는 코드입니다. 첫번째 doNotTrancationAction에서는 당연히 Transaction이 없어야지 되고, 두번째 doTransactionAction에서는 당연히 Transaction이 있어야지 될 것으로 생각할 수 있습니다. 이제 이 두 코드를 호출하는 테스트 코드를 작성해서 시험해보면 다음과 같은 결과를 보입니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DomainConfiguration.class)
public class BookServiceImplTest {
    @Autowired
    private BookService bookService;

    @Before
    public void setUp() {
        assertThat(bookService, is(not(nullValue())));
    }

    @Test
    public void doNotTransactionAction() {
        bookService.doNotTransactionAction();
    }

    @Test
    public void doTransactionAction() {
        bookService.doTransactionAction();
    }
}

매우 당연한 결과이지만, 이 결과를 이제 수정해서 다음 코드로 바꾸어보도록 하겠습니다. Transaction이 적용되지 않은 code에서 Transaction이 적용된 method를 호출하는 경우에 어떻게 진행되는지 보도록 하겠습니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
        doTransactionAction();
    }

결과는 다음과 같이 Assert Error가 나타나게 됩니다.

java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.util.Assert.notNull(Assert.java:123)
    at me.xyzlast.bookstore.services.BookServiceImpl.doTransactionAction(BookServiceImpl.java:54)
    at me.xyzlast.bookstore.services.BookServiceImpl.doNotTransactionAction(BookServiceImpl.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

왜 이런 결과를 나타내게 될까요? 이는 Spring Transaction이 Proxy로 동작하기 때문입니다. Public으로 Service interface에 접근할 때, Transaction이 동작하기 때문에 발생하는 에러입니다. 이 문제는 보다 더 복잡한 문제를 야기할 수 있습니다. 다음과 같은 코드를 확인해보도록 하겠습니다.

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(!TransactionSynchronizationManager.isCurrentTransactionReadOnly());
    }

    @Transactional(readOnly = true)
    @Override
    public void doTransactionReadonlyAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
        doTransactionAction();
    }

2개의 Transaction action이 있습니다. 하나는 ReadOnly로 동작하고, 나머지 하나는 Not ReadOnly입니다. 그런데 ReadOnly method에서 Not ReadOnly method를 내부에서 호출하게 되면 어떻게 될까요. 결과는 Not ReadOnly method 내부에서도 Transaction은 ReadOnly로 동작하게 됩니다. 이는 매우 심각한 Transaction문제를 가지고 오게 됩니다.

Summary

Spring의 @Transactional annotation은 매우 훌륭합니다. 다만 이를 PROXY mode에서 사용할 때는 다음과 같은 원칙을 알고 사용해야지 됩니다.

  1. private method에서는 동작하지 않음
  2. transaction이 적용되지 않은 public method 내부에서 transaction이 적용된 public method를 호출하는 경우, transaction이 동작하지 않는다.
  3. readonly transaction이 적용된 public method 내부에서 not-readonly transaction이 적용된 public method를 호출하는 경우, 모든 method는 readonly transaction으로 동작하게 된다.
  4. (3)의 경우는 반대로도 적용된다.


Posted by Y2K
,