잊지 않겠습니다.

queryDSL이 4.x대로 변경이 되면서 package명에 큰 변화가 생겼습니다. 기존 com.mysema.querydsl 에서 com.querydsl로 package명이 바뀌어서 간략화 되었지요.

기존 querydsl 처리 부분에 대한 build.gradle을 변경한다면 다음과 같습니다.


dependencies {
compile("org.springframework.boot:spring-boot-starter-web") {
exclude module: "spring-boot-starter-tomcat"
}
compile 'com.graphql-java:graphql-java:2.1.0'
compile("org.springframework.boot:spring-boot-starter-jetty")
compile("org.springframework.boot:spring-boot-starter-actuator")
testCompile("junit:junit")
testCompile("org.springframework.boot:spring-boot-starter-test")
compile 'com.querydsl:querydsl-apt:4.1.4'
compile 'com.querydsl:querydsl-jpa:4.1.4'
compile 'org.springframework.data:spring-data-jpa:1.10.4.RELEASE'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.3.Final'
compile "org.hibernate:hibernate-entitymanager:5.2.3.Final"
compile 'com.h2database:h2:1.4.187'
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.8.9'
compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.8.9'
compileOnly "org.projectlombok:lombok:1.16.12"
}


sourceSets {
main {
java {
srcDirs 'src/main/java', 'src/main/generated'
}
}
}

task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
file(new File(projectDir, "/src/main/generated")).deleteDir()
file(new File(projectDir, "/src/main/generated")).mkdirs()
source = sourceSets.main.java
classpath = configurations.compile + configurations.compileOnly
options.compilerArgs = [
"-proc:only",
"-processor", "com.querydsl.apt.jpa.JPAAnnotationProcessor"
]
destinationDir = file('src/main/generated')
}

compileJava {
dependsOn generateQueryDSL
}

clean.doLast {
file(new File(projectDir, "/src/main/generated")).deleteDir()
}


기존의 generateQueryDSL task를 유지할 필요 없이, java compile option으로 QClass 생성을 넣어주는것으로 script를 간략화시키는것이 가능합니다. 또한 Q class가 존재할 때, 기존 Q Class를 지워주지 않으면 compile 시에 에러를 발생하게 되기 때문에 지워주는 작업역시 doFirst에 같이 실행하고 있습니다.

기존의 build.gradle보다 간략해보여서 전 개인적으로 맘에 드네요.

저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K
TAG gradle, java, JPA
지금 저는 Gradle을 이용한 web application 배포시에 FTP를 사용하고 있습니다.

그런데, 이 부분에 조금 문제가 있는 것이… 기존 gradle의 ant ftp module의 경우 가끔씩 hang이 걸려서 FTP upload를 실패하는 경우가 왕왕 보입니다. 그리고 FTP upload 시에 아무런 로그가 표시되지 않고, hang이 걸려버려서 정상적으로 upload가 되고 있는지에 대한 확인이 불가능했습니다.

그래서, FTPClient를 이용해서 File upload를 한번 만들어봤습니다.

기본적으로 commons-net의 FTPClient를 이용해서 처리합니다. 먼저 buildscript에 common-net을 등록시킵니다.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'commons-net:commons-net:3.3'
        classpath 'commons-io:commons-io:2.4'
    }
}

다음은 FTPClient를 이용한 upload code입니다. groovy 스러운 문법은 전 아직 잘 안되더군요.;;


import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import org.apache.commons.net.PrintCommandListener
import org.apache.commons.net.ftp.FTP
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.net.ftp.FTPReply
import org.apache.commons.io.IOUtils


void ftpUpload(ftpUrl, ftpUsername, ftpPassword, targetPath) {
    def ftp = new FTPClient()
    ftp.connect(ftpUrl)
    int reply = ftp.getReplyCode()
    if(!FTPReply.isPositiveCompletion(reply)) {
        ftp.disconnect()
        throw new Exception("Exception in connecting to FTP Server")
    }
    ftp.login(ftpUsername, ftpPassword)
    ftp.setFileType(FTP.BINARY_FILE_TYPE)
    ftp.changeWorkingDirectory(targetPath)
    for(File f : file(getDistPath()).listFiles()) {
        upload(f, ftp)
    }
    ftp.disconnect()
}

void upload(File src, FTPClient ftp) {
    if (src.isDirectory()) {
        ftp.makeDirectory(src.getName())
        ftp.changeWorkingDirectory(src.getName())
        for (File file : src.listFiles()) {
            upload(file, ftp);
        }
        ftp.changeToParentDirectory();
    }
    else {
        InputStream srcStream = null;
        try {
            def uploadCompleted = false
            while(!uploadCompleted) {
                srcStream = src.toURI().toURL().openStream()
                println 'upload : ' + src.getName()
                uploadCompleted = ftp.storeFile(src.getName(), srcStream)
                if(!uploadCompleted) {
                    println 'upload failed : retry this file ' + src.getName()
                    IOUtils.closeQuietly(uploadCompleted)
                }
            }
        } catch(Exception ex) {
            println ex.getMessage()
        }
        finally {
            IOUtils.closeQuietly(srcStream);
        }
    }
}

java 코드 같은 느낌입니다. groovy를 사용하고 있으면 좀 더 groovy 스러운 코드여야지 될 것 같은데요. ^^;;


저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K

Gradle + yo deploy script

gradle을 이용한 build script 만들기 재미가 붙어, 이번에는 yo로 만든 web application을 auto-deploy를 지원하는 script를 한번 작성해봤습니다.

지원하는 시나리오는 다음과 같습니다.

  1. 이번에 작성한 2개의 public web과 mobile web을 모두 사용 가능
  2. 다음과 같은 process를 진행 가능해야지 됨
  • grunt build
  • svn에 새로 deploy할 web application의 압축본을 add & commit
  • ftp를 이용한 web application 배포

먼저, 여러 web application을 지원하기 위한 조건은 외부 parameter를 받는 방법이 가장 좋아서 target을 지정하기로 했습니다.

작성된 web aplication들의 구조는 다음과 같습니다.

.
├── admin-web
├── mobile-web
├── public-web
└── sample-web

여기서 sample-web의 경우에는 팀원들에게 같이 공유할 sample web application이기 때문에 배포 대상이 되지 못하고, admin, mobile, public의 경우에는 배포 대상이 될 수 있습니다. 간단히 이름을 기준으로 admin, mobile, public을 기준으로 삼고, 이를 parameter로 받으면 될 수 있다는 판단이 내려졌습니다.

이제 다음 조건들이 필요합니다.

  • grunt,svn 과 같은 command의 실행
  • grunt deploy가 된 후에 zip compress
  • ftp를 이용한 data copy

gradle을 이용한 command의 실행

기본적으로 gradle을 이용한 command는 type을 Exec로 잡아주면 처리가 가능합니다. 이와 같은 sub directory 내에서 실행되어야지 될 command는 workingDir 값을 지정해줘서 처리가 가능합니다. 다음은 grunt build를 하는 code입니다.

task build(type: Exec) {
    workingDir getWebDir()
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'grunt', 'build'
    } else {
        commandLine 'grunt', 'build'
    }
    ext.output = {
        return standardOutput.toString()
    }
}

윈도우즈 계통과 linux/mac을 모두 지원하기 위해서 OS의 type을 설정해줘야지 됩니다. windows에서 cmd와 /c를 이용하는 것만을 주의하면 매우 간단한 코드입니다.

zip 파일 압축

zip 압축은 gradle에서 기본으로 제공하고 있는 Zip type을 이용하면 됩니다. zip type의 경우, from과 destinationDir만을 주의해서 처리하면 됩니다. 다음은 만들어진 distPath의 모든 내용을 yyyyMMddHHmm-hostname.zip 형식으로 압축을 하는 코드입니다.

task compressDist(type: Zip) {
    dependsOn 'build'

    from file(getDistPath())
    destinationDir file(deployPath)

    def now = new Date()
    def dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
    def hostname = InetAddress.getLocalHost().getHostName().toLowerCase()

    filename = String.format("%s-%s.zip", dateFormat.format(now), hostname)
    archiveName filename
}

FTP 파일 전송

gradle은 자체적으로 FTP를 지원하지 않습니다. 다만 ant 를 지원하고 있기 때문에, ant의 FTP를 이용하면 됩니다. 그런데 ant ftp의 경우에는 send시에 hang이 걸리는 버그를 가지고 있습니다. passive mode를 true로 설정하는 것으로 hang이 걸리는 경우를 조금 덜 하게 할 수 있긴 하지만, 그래도 send에서 hang이 걸리는 것을 완벽하게 막지는 못합니다. 이 부분에 대해서는 좀 더 논의가 필요할 것 같습니다. 다음은 ftp 전송 코드입니다.

task 'upload' {
    dependsOn 'commitSvn'
    doLast {
        def remoteDir = getFtpPath()
        ant {
            taskdef(name: 'ftp', classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP', classpath: configurations.ftpAntTask.asPath)
            ftp(action: 'mkdir', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword)
            ftp(action: 'delete', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset() { include(name: '**/*') }
            }
            ftp(action: 'send', remotedir: remoteDir, verbose: true,
                    depends: true, binary: true, passive: true,
                    server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset(dir: getDistPath()) {
                    include(name: '**/**/*')
                }
            }
        }
    }
}

각 build task에 대한 dependency를 추가하고, target argument를 받아서 처리하도록 코드 수정을 마져 완료한 최종 코드는 다음과 같습니다.

import org.apache.tools.ant.taskdefs.condition.Os

import java.text.SimpleDateFormat

configurations {
    ftpAntTask
}

repositories {
    mavenCentral()
}

dependencies {
    ftpAntTask("org.apache.ant:ant-commons-net:1.8.2") {
        module("commons-net:commons-net:1.4.1") {
            dependencies "oro:oro:2.0.8:jar"
        }
    }
}

def ftpUrl = '192.168.13.210'
def ftpUsername = 'ykyoon'
def ftpPassword = 'qwer12#$'
def filename = ''

def getDistPath() {
    return String.format('%s/dist', getWebDir())
}

def getWebDir() {
    return String.format('%s-web', project.target)
}

def getDeployPath() {
    return String.format('../deployed/%s-web/', project.target);
}

def getFtpPath() {
    return String.format('www-fms/%s', project.target);
}

task build(type: Exec) {
//    dependsOn 'upSvn'
    workingDir getWebDir()
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'grunt', 'build'
    } else {
        commandLine 'grunt', 'build'
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task compressDist(type: Zip) {
    dependsOn 'build'

    from file(getDistPath())
    destinationDir file(deployPath)

    def now = new Date()
    def dateFormat = new SimpleDateFormat("yyyyMMddHHmm");
    def hostname = InetAddress.getLocalHost().getHostName().toLowerCase()

    filename = String.format("%s-%s.zip", dateFormat.format(now), hostname)
    archiveName filename
}

task upSvn(type: Exec) {
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'svn', 'up'
    } else {
        commandLine 'svn', 'up'
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task addSvn(type: Exec) {
    dependsOn 'compressDist'

    String svnParam = getDeployPath() + filename
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'svn', 'add', svnParam
    } else {
        commandLine 'svn', 'add', svnParam
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task commitSvn(type: Exec) {
    dependsOn 'addSvn'
    String svnParam = getDeployPath() + filename
    String svnLog = '-mCommit file before deployed'

    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', '/c', 'svn', 'add', svnParam, svnLog
    } else {
        commandLine 'svn', 'commit', svnParam, svnLog
    }
    ext.output = {
        return standardOutput.toString()
    }
}

task 'upload' {
    dependsOn 'commitSvn'
    doLast {
        def remoteDir = getFtpPath()
        ant {
            taskdef(name: 'ftp', classname: 'org.apache.tools.ant.taskdefs.optional.net.FTP', classpath: configurations.ftpAntTask.asPath)
            ftp(action: 'mkdir', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword)
            ftp(action: 'delete', remotedir: remoteDir, server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset() { include(name: '**/*') }
            }
            ftp(action: 'send', remotedir: remoteDir, verbose: true,
                    depends: true, binary: true, passive: true,
                    server: ftpUrl, userid: ftpUsername, password: ftpPassword) {
                fileset(dir: getDistPath()) {
                    include(name: '**/**/*')
                }
            }
        }
    }
}

task 'deploy' {
    dependsOn 'build'
    dependsOn 'upload'
    doLast {
        println 'grunt build and upload'
    }
}

task help << {
    println '--------- gradle web command help ----------------------'
    println 'gradle deploy -Ptarget=public : public web build'
    println 'gradle deploy -Ptarget=mobile : mobile web build'
    println '--------- end line -----------------------------------------'
}

모두들 즐거운 코딩 되세요. ^^

저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K
TAG gradle, Yo

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 xyzlast 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 xyzlast Y2K

최신 Gradle 2.0으로 업데이트 한 내용이 존재합니다.


전에 정리한 적이 있는데, 조금 글을 다듬고 정리할 필요성이 있어서 다시 옮깁니다.
먼저, 개발되는 Project는 다음과 같은 구조를 갖습니다.

rootProject
-- domainSubProject (Spring Data JPA + queryDSL)
-- webAppSubProject (Spring Web MVC)

위와 같은 Project는 개발의 편의를 위해서 다음 두 조건을 만족해야지 됩니다.

  1. queryDSL을 사용하기 위한 Q-Entity를 생성이 compileJava task전에 수행되어야지 됩니다.
  2. web application 개발을 위해, tomcat을 실행시킬 수 있어야합니다.
  3. 개발된 web application을 test 환경 또는 product 환경에 배포가 가능해야지 됩니다.

root project

root project는 sub project들의 공통 설정이 필요합니다.

  1. build시에 필요한 plugin의 repository를 설정합니다.
  2. sub project들에서 사용될 공통 dependency들을 설정합니다. (sub project들의 build.gradle 이 너무 길어지는 것을 막을 수 있습니다.)
공통 plugin 추가
  • maven의 POM과 동일한 provided과 optional의 사용을 위해 spring prop plugin을 추가합니다.
  • subprojects들에 필요한 plugin들을 모두 추가합니다. (저는 java와 groovy, idea, eclipse 등을 추가했습니다.)
  • source java version과 target java version을 정해줍니다.
  • source code의 Encoding을 정해줍니다.
  • SubProject에서 기본적으로 사용될 dependency들을 모두 적어줍니다.

build.gradle (root project)

apply plugin: 'base'

// spring prop plugin 추가
buildscript {
    repositories {
        maven { url 'http://repo.springsource.org/plugins-release' }
    }
    dependencies {
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.5'
    }
}

subprojects {
    // Plugin 설정, 만약에 code에 대한 static analysis가 필요한 경우에 이곳에 설정.
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'
    apply plugin: 'groovy'
    apply plugin: 'propdeps'
    apply plugin: 'propdeps-maven'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'

    // 기본적으로 사용할 repository들을 정의
    repositories {
        mavenCentral()
        maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "http://192.168.13.209:8080/nexus/content/repositories/releases" }
    }

    dependencies {
        def springVersion = "4.0.1.RELEASE"

        compile 'org.slf4j:slf4j-api:1.7.5'
        compile "org.springframework:spring-context:${springVersion}"
        compile "org.springframework:spring-aspects:${springVersion}"
        compile "org.springframework:spring-jdbc:${springVersion}"
        compile "org.springframework:spring-context-support:${springVersion}"

        compile 'mysql:mysql-connector-java:5.1.27'
        compile 'com.jolbox:bonecp:0.8.0.RELEASE'
        compile 'com.google.guava:guava:15.0'
        compile 'org.aspectj:aspectjrt:1.7.4'
        compile 'org.aspectj:aspectjtools:1.7.4'
        compile 'org.aspectj:aspectjweaver:1.7.4'

        testCompile 'org.springframework:spring-test:4.0.0.RELEASE'
        testCompile "junit:junit:4.11"

        groovy "org.codehaus.groovy:groovy-all:2.1.6"
        testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT"
        testCompile "org.spockframework:spock-spring:1.0-groovy-2.0-SNAPSHOT"
    }

    // source, target compiler version 결정
    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    // source code encoding
    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
}
domain project
  • domain project는 queryDsl의 Q-Entity들의 생성이 필요합니다.
  • compileJava에 대한 Q-Entity 생성 Task의 Depend가 잡혀있으면 사용하기 편합니다.

build.gradle (domain project)

dependencies {
    def springVersion = "4.0.1.RELEASE"

    compile 'org.hibernate:hibernate-core:4.3.1.Final'
    compile 'org.hibernate:hibernate-entitymanager:4.3.1.Final'
    compile "org.springframework:spring-orm:${springVersion}"

    def queryDSL = '3.2.4'
    compile("com.mysema.querydsl:querydsl-core:$queryDSL")
    compile("com.mysema.querydsl:querydsl-jpa:$queryDSL")
    compile("com.mysema.querydsl:querydsl-sql:$queryDSL")
    provided("com.mysema.querydsl:querydsl-apt:$queryDSL") {
        exclude group: 'com.google.guava'
    }
    compile 'org.springframework.data:spring-data-jpa:1.5.0.RELEASE'
}

sourceSets {
    generated {
        java {
            srcDirs = ['src/main/generated']
        }
    }
}

// QEntity 생성 task
task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
    source = sourceSets.main.java
    classpath = configurations.compile + configurations.provided
    options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

// compileJava task에 dependency를 걸어줍니다.
compileJava {
    dependsOn generateQueryDSL
    // compile target에 generated된 QClass들의 위치를 추가.
    source sourceSets.generated.java.srcDirs.iterator().next()
}

compileGeneratedJava {
    dependsOn generateQueryDSL
    options.warnings = false
    classpath += sourceSets.main.runtimeClasspath
}

clean {
    delete sourceSets.generated.java.srcDirs
}

idea {
    module {
        sourceDirs += file('src/main/generated')
    }
}
webapplication project

마지막으로 web application project입니다. 이는 조건이 조금 더 많습니다.

  • tomcat을 실행시켜 local 개발 환경에서 사용할 수 있어야지 됩니다.
  • 외부 테스트 또는 운영환경의 tomcat에 배포가 가능해야지 됩니다.

위 조건을 만족하기 위해서 gradle의 tomcat plugin과 cargo plugin을 이용합니다.

plugin에 대한 자료들은 다음 url에서 보다 많은 자료를 볼 수 있습니다.

tomcat plugin > https://github.com/bmuschko/gradle-tomcat-plugin
cargo plugin > https://github.com/bmuschko/gradle-cargo-plugin

build.gradle (webapp project)

apply plugin: 'war'
apply plugin: 'tomcat'
apply plugin: 'cargo'

// tomcat과 cargo plugin에 대한 repository 설정입니다.
buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }

    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
        classpath 'org.gradle.api.plugins:gradle-cargo-plugin:1.4'
    }
}


dependencies {
    // tomcat plugin 설정입니다.
    String tomcatVersion = '7.0.47'
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
    tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
        exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    providedCompile 'javax.websocket:javax.websocket-api:1.0'
    providedCompile 'javax.servlet:jsp-api:2.0'
    providedCompile "org.apache.tomcat:tomcat-servlet-api:${tomcatVersion}"

    // cargo에 대한 설정입니다.
    def cargoVersion = '1.4.5'
    cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
            "org.codehaus.cargo:cargo-ant:$cargoVersion"


    def springVersion = "4.0.1.RELEASE"
    compile "org.springframework:spring-webmvc:${springVersion}"
    compile 'jstl:jstl:1.2'
    compile 'org.apache.tiles:tiles-jsp:3.0.3'

    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.apache.velocity:velocity:1.7'
    compile 'org.freemarker:freemarker:2.3.20'
    compile 'com.ctlok:spring-webmvc-rythm:1.4.4'
    compile project(':bookstoreHibernate')
}

// tomcarRun을 실행시키기 위해서 war에 대한 dependency를 주입합니다.
tomcatRun {
    contextPath = ""
    URIEncoding = 'UTF-8'
    dependsOn war
}

tomcatRunWar {
    dependsOn war
}

// cargo를 이용한 배포를 위해서 war에 대한 dependency를 주입합니다.
cargoRedeployRemote {
    dependsOn war
}

cargoDeployRemote {
    dependsOn war
}

cargo {
    containerId = 'tomcat7x'
    port = 8080

    deployable {
        context = "${project.name}"
    }

    // remoteDeploy 되는 target의 tomcat 정보
    remote {
        hostname = '192.168.13.209'
        username = 'ykyoon'
        password = 'qwer12#$'
    }
}

bower를 이용한 javascript dependency

web application에서의 외부 javascript dependency를 사용하는 방법입니다. bower를 이용하는 경우, 외부에서 javascript에 대한 source code를 모두 다운받고 compile된 javascript를 dist에 저장하게 됩니다.

그런데, 우리의 web application은 dist에 저장된 특정 파일만을 사용하게 됩니다. 그럼 이 dist에 있는 file을 최종적으로 배포할 webapp folder에 넣어줘야지 됩니다. 이를 위해서 개발된 것이 bower-installer 입니다. 그런데 bower-installer의 경우에는 윈도우즈에서 동작이 정상적이지 않습니다. 아니 실행이 되지 않습니다.; 그래서 bower-installer와 동일한 동작을 하는 task를 만들어봤습니다.

먼저, bower-installer는 bower.json의 install property에 설정합니다. jquery와 bootstrap에 대한 dependency를 설정한 bower.json 입니다.

bower.json

{
    "name" : "bookstore-web",
    "version" : "0.0.0.1",
    "dependencies" : {
        "jquery" : "1.11.0",
        "bootstrap" : "3.1.1"
    },
    "install" : {
        "path" : {
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "eot" : "src/main/webapp/lib/fonts",
            "svg" : "src/main/webapp/lib/fonts",
            "ttf" : "src/main/webapp/lib/fonts",
            "woff" : "src/main/webapp/lib/fonts",
            "map" : "src/main/webapp/lib/js"
        },
        "sources" : {
            "jquery" : [
                    "bower_components/jquery/dist/jquery.min.js",
                    "bower_components/jquery/dist/jquery.min.map"
                ],
            "bootstrap" : [
                    "bower_components/bootstrap/dist/css/bootstrap.min.css",
                    "bower_components/bootstrap/dist/css/bootstrap-theme.min.css",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff",
                    "bower_components/bootstrap/dist/js/bootstrap.min.js"
                ]
        }
    }
}

위의 install property에 지정된 js와 css들을 옮기는 task는 다음과 같이 설정할 수 있습니다. war task에 dependency를 주입해서 위의 tomcatRun이나 cargoRedeployRemote 등에서도 사용할 수 있습니다.

import org.apache.tools.ant.taskdefs.condition.Os
import groovy.json.JsonSlurper
task bowerInstall(type:Exec, description : 'copy js files dependencies that is defined in Bower.js') {

    if(Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', 'bower', 'install'
    } else {
        commandLine 'bower', 'install'
    }

    def jsonHandler = new JsonSlurper()
    def jsonFile = file("bower.json")
    def conf = jsonHandler.parseText(jsonFile.getText("UTF-8"))
    def pathMap = [:]

    conf.install.path.each {
        pathMap.put(it.key, it.value)
    }

    conf.install.sources.each {
        it.value.each { f ->
            def sourceFile = file(f)
            String sourceName = sourceFile.name
            int dotPos = sourceName.lastIndexOf(".")
            String ext = sourceName.substring(dotPos + 1, sourceName.length())
            if(pathMap.containsKey(ext)) {
                copy {
                    from f
                    into pathMap.get(ext)
                }
            }
        }
    }
}

war {
    dependsOn bowerInstall
}


위 정리된 내용을 github에 공유합니다. 아래 주소에서 git clone 하시면 됩니다. ^^

 

 https://github.com/xyzlast/study-spring-bookstore.git



저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

java8이 나오고 빠르게 한번 환경을 변경시켜보고 난 후, 문제점 상황을 정리해봤습니다.

jacoco 버젼 문제

jacoco를 최신 버젼으로 해줄 필요가 있습니다. jacoco는 최신 버젼이 0.7.0.201403182114 입니다. 버젼뒤의 날짜를 보시면 아시겠지만, java8 발표 일자와 완전히 동일합니다. 기존 jacoco에서는 byte code exception이 발생하기 때문에 반드시 업그레이드 할 필요가 있습니다.

tomcat 8

gradle tomcat plugin이 아직 tomcat8을 지원하지 못합니다. gradle을 이용한 build의 경우에는 java 8을 사용하시는 것을 고민해주시는 것이 좋습니다. 물론 java 8에서 tomcat7을 사용하는 것은 아무런 문제가 발생되지 않습니다.

SonarQube

sonarQube에서 아직 java 8을 지원하지 못하고 있습니다. 3월말에 최신 업데이트가 될 것이라는 이야기가 아래 링크에 있는데…… java8은 작년 여름에 release 될 예정이였지 않나요. 이걸 믿어야지 되는지 고민하고 있습니다.

http://sonarqube.15.x6.nabble.com/Sonar-Support-for-JDK-8-td5019488.html

무엇보다 위 링크에 이야기되고 있는 JIRA Issue는 이미 resolved된 상태입니다. 일단 stackoverflower에 질문을 올려두긴 했는데.. 메일링 리스트에서 물어보세요. 라는 답변을 받아서 상처받고 메일링 리스트에 가입해서 다시 물어봤습니다. ㅠ-ㅠ 답변을 기다려야지요.

FindBugs

sonarQube에서 지금 지원을 못하는 가장 결정적 이유가 바로 FindBug가 Java8을 지원하지 못하는 이슈가 있기 때문입니다. StringSequence class를 비롯해서 몇몇 Class에 대한 오류를 발생시키고 있습니다. Java8에서 가장 큰 이슈중 하나네요.

Java8

개인적으로는 이번 java8을 매우 기다리고 있습니다. PermGemSize에 대한 GC의 개선과 더불어 lamda expression으로 대표되는 java 언어의 확장성. 그리고 지겹고 지겨운 Date 객체 추가.

개인적으로는 이 3가지때문에 java8을 기다리고 있는데. 다른 분들은 어떠실지 모르겠네요.

저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

Gradle 2.0으로 업그레이드 된 버젼에 대해서 업데이트 된 글이 있습니다.


기존 gradle을 이용한 개발 환경 설정 업데이트입니다.


변경사항은 다음과 같습니다. 


1. jetty plugin의 제거 : 개발환경에서는 jetty / tomcat의 차이가 없을 것 같아, jetty를 없앴습니다.

2. spork를 이용한 테스트 환경 추가. : groovy base인 spork를 이용한 test 환경을 추가하였습니다.

3. tomcat plugin 사용시, tomcatRun 에러 수정 : tomcat plugin 사용시 tomcatRun 을 하면 servlet jar class not found 에러가 나오는 것을 수정했습니다. 

4. gradle 1.9 에서 동작하지 않던 버그를 수정했습니다.

5. build.gradle을 4개로 분리했습니다.

1) base.gradle : 기본 설정에 관련된 내용으로 구성됩니다.

2) domain.gradle : domain module에 관련된 내용으로 구성됩니다. (queryDsl QFile generate)

3) web.gradle : war에 관련된 내용으로 구성됩니다. (tomcatRun, tomcatRunWar)


build.gradle

apply plugin: 'base'
apply plugin: 'sonar-runner'
apply plugin: 'maven'
version = '1.0.0'

ext {
    javaVersion = 1.7
    springVersion = '4.0.0.RELEASE'
}


buildscript {
    repositories {
        mavenCentral()
        jcenter()
        maven { url 'http://repo.springsource.org/plugins-release' }
    }
    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
        classpath (group: 'org.gradle.api.plugins', name: 'gradle-cargo-plugin', version: '0.6.1')
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.1'
    }
}

allprojects {
    apply plugin: 'propdeps'
    apply plugin: 'propdeps-maven'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'

    repositories {
        mavenLocal()
        mavenCentral()
        jcenter()
        maven { url 'http://download.java.net/maven/2' }
        maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url 'https://maven.java.net/content/repositories/releases'}
        maven { url 'http://repo.springsource.org/plugins-release' }
    }

    dependencies {
        provided 'org.projectlombok:lombok:0.12.0'
    }
}


apply from: 'base.gradle'
apply from: 'domain.gradle'
apply from: 'web.gradle'


base.gradle

subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'
    apply plugin: 'jacoco'
    apply plugin: 'groovy'

    sonarRunner {
        sonarProperties {
            property "sonar.sourceEncoding", "UTF-8"
        }
    }

    jacoco {
        toolVersion = '0.6.3.201306030806'
    }

    test {
        jacoco {
            append = false
            destinationFile = file("target/jacoco.exec")
            classDumpFile = file("target/classpathdumps")
        }
    }

    if(project.hasProperty('target')) {
        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-${project.target}"]
        }
    } else {
        String hostname = InetAddress.getLocalHost().getHostName();
        if(hostname.endsWith('.local')) {   //맥의 경우, .local 이 모든 hostname에 추가됩니다.
            hostname.replace(".local", '')
        }

        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-" + hostname]
        }
    }

    dependencies {
        def slf4jVersion = "1.7.2"
        compile "org.slf4j:jcl-over-slf4j:$slf4jVersion"
        compile "org.slf4j:jul-to-slf4j:$slf4jVersion"
        compile "org.slf4j:slf4j-api:$slf4jVersion"
        compile 'ch.qos.logback:logback-classic:1.0.13'

        testCompile "junit:junit:4.11"

        groovy "org.codehaus.groovy:groovy-all:2.1.5"
        testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT"
        testCompile "org.spockframework:spock-spring:1.0-groovy-2.0-SNAPSHOT"
    }

    sourceCompatibility = rootProject.ext.javaVersion
    targetCompatibility = rootProject.ext.javaVersion

    tasks.withType(Compile) {
        options.encoding = 'UTF-8'
    }
}

sonarRunner {
    sonarProperties {
        property "sonar.host.url", "http://192.168.13.209:9000"
        property "sonar.jdbc.url", "jdbc:mysql://192.168.13.209:3306/sonar"
        property "sonar.jdbc.driverClassName", "com.mysql.jdbc.Driver"
        property "sonar.jdbc.username", "root"
        property "sonar.jdbc.password", 'qwer12#$'
    }
}


domain.gradle

configure(subprojects.findAll { it.name.endsWith('domain') }) {
    dependencies {
        compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.22'
        compile("com.jolbox:bonecp:0.8.0.RELEASE") {
            exclude group: 'com.google.guava'
        }
        compile group: 'com.google.guava', name: 'guava', version: '15.0'
        compile group: 'org.springframework', name: 'spring-core', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-orm', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-tx', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-context', version: "${rootProject.ext.springVersion}"
        compile group: 'org.springframework', name: 'spring-context-support', version: "${rootProject.ext.springVersion}"

        compile group: 'org.hibernate', name: 'hibernate-core', version: '4.1.10.Final'
        compile group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.0-api', version: '1.0.1. Final'
        compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '4.1.10.Final'
        compile group: 'org.hibernate', name: 'hibernate-validator', version: '4.3.1.Final'

        compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.4.2.RELEASE'

        def queryDSL = '3.2.4'
        compile("com.mysema.querydsl:querydsl-core:$queryDSL")
        compile("com.mysema.querydsl:querydsl-jpa:$queryDSL")
        compile("com.mysema.querydsl:querydsl-sql:$queryDSL")
        provided("com.mysema.querydsl:querydsl-apt:$queryDSL") {
            exclude group: 'com.google.guava'
        }

    }

    sourceSets {
        generated {
            java {
                srcDirs = ['src/main/generated']
            }
        }
    }

    task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
        source = sourceSets.main.java
        classpath = configurations.compile + configurations.provided
        options.compilerArgs = [
                "-proc:only",
                "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
        ]
        destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
    }

    compileJava {
        dependsOn generateQueryDSL
        source generateQueryDSL.destinationDir
    }

    compileGeneratedJava {
        dependsOn generateQueryDSL
        options.warnings = false
        classpath += sourceSets.main.runtimeClasspath
    }

    clean {
        delete sourceSets.generated.java.srcDirs
    }

    idea {
        module {
            sourceDirs += file('src/main/generated')
        }
    }
}



web.gradle

configure(subprojects.findAll { it.name.endsWith('Web') }) {
    apply plugin: 'war'
    apply plugin: 'tomcat'
    apply plugin: 'cargo'

    dependencies {
        String tomcatVersion = '7.0.47'
        tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
        tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
        tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
            exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
        }

        def cargoVersion = '1.3.3'
        cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
                "org.codehaus.cargo:cargo-ant:$cargoVersion"

        providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
        providedCompile 'javax.websocket:javax.websocket-api:1.0'
        providedCompile 'javax.servlet:jsp-api:2.0'
        providedCompile "org.apache.tomcat:tomcat-servlet-api:${tomcatVersion}"
    }

    tomcatRun {
        dependsOn war
    }

    tomcatRunWar {
        dependsOn war
    }

    cargoRedeployRemote {
        dependsOn war
    }

    cargoDeployRemote {
        dependsOn war
    }

    cargo {
        containerId = 'tomcat7x'
        port = 8080

        deployable {
            context = "${project.name}"
        }

        remote {
            hostname = '192.168.13.209'
            username = 'ykyoon'
            password = 'qwer12#$'
        }
    }
}





저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K
TAG build, gradle, java

gradle은 maven과 다르게 provided가 지원되지 않는다. 

지원되지 않기 때문에 다양한 방법으로 provided를 사용하고 있는데, spring.org에서 해결책을 내놓은 plugin으로 해결 가능하다. 


github 주소는 이곳에, 

https://github.com/spring-projects/gradle-plugins/tree/master/propdeps-plugin


사용법은 아래와 같다. 

buildscript {
    repositories {
        maven { url 'http://repo.springsource.org/plugins-release' }
    }
    dependencies {
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.1'
    }
}

// ...

configure(allprojects) {
    apply plugin: 'propdeps'
    apply plugin: 'propdeps-maven'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'
}


dependencies {
    compile("commons-logging:commons-logging:1.1.1")
    optional("log4j:log4j:1.2.17")
    provided("javax.servlet:javax.servlet-api:3.0.1")
    testCompile("junit:junit:4.11")
}
기존의 idea, eclipse task역시 반영을 받기 때문에 매우 유용하게 사용 가능하다.


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K
기존 maven을 사용할 때, 개인 개발 환경에서 개인적으로 가장 문제를 느낀 것은 개인 property파일의 문제였습니다. 
개인 property 파일을 local로 만들어주고, 그 local을 이용할 때 그 파일들은 따로 svn으로 관리하기가 매우 힘들었던 단점을 가지고 있습니다. 

예를 들어 logback의 설정파일안에 ykyoon은 c:\Logs안에 log 파일을 만들때, 다른 사람들은 d:\Logs에 만드는 등, property 파일들간의 충돌이 가장 큰 문제중 하나였습니다. 

따라서, 이러한 개인 property 파일들의 관리 및 svn이나 git를 이용한 버젼관리까지 같이 해주기 위해서는 사용자의 build system마다 다른 build 경로를 가질 수 있어야지 됩니다. 제가 gradle을 보게 된 가장 큰 이유중 하나입니다. 

그리고, Web Project의 경우에는 jetty 또는 tomcat을 이용해서 local run이 가능한 것이 개발의 속도를 높여줍니다. 
한가지 더 추가를 한다면, ~Web으로 시작되는 project들은 tomcat/jetty를 이용해서 running이 가능하다면 좋습니다. 

이와 같은 조건을 모두 만족하는 build.gradle을 만들어본다면 다음과 같습니다. 

1. 각 개발자 PC 상황에 맞는 resources path 설정

: 개발자들은 자신의 개인 PC를 이용해서 개발을 합니다. 네트워크의 충돌을 막기 위해서 일반적으로 hostname은 모두 다르게 구성됩니다. 따라서, hostname을 기반으로 하는 것이 좋은 것으로 판단됩니다.

hostname을 얻어내기 위해서는 다음과 같은 script를 구성하면 됩니다. 

        String hostname = InetAddress.getLocalHost().getHostName();
        if(hostname.endsWith('.local')) {   //맥의 경우, .local 이 모든 hostname에 추가됩니다.
            hostname.replace(".local", '')
        }

mac에서만 local host의 이름에 쓸데없는 값이 들어갈 수 있기 때문에 일단 제거를 시켜줍니다. 
그리고, 모든 subproject에서 이와 같은 속성을 사용하기 때문에 subproject를 다음과 같은 gradle script를 구성해줍니다. 
아래의 script결과, hostname이 ykyoon인 경우, 기존 src/main/resources, src/main/resources-ykyoon 이 모두 포함이 됩니다. 개발자들은 공통적인 resource의 경우에는 src/main/resources에 넣어주고, 추가적인 개인 환경에 따른 resources를 모두 자신의 호스트 이름이 들어있는 path에 넣어주고 svn이나 git를 통해서 파일을 관리해주면 됩니다. 

또한, 최종 build시에는 -P argument를 이용해서 target을 지정해주면, 배포환경에 따른 resources 역시 처리가 가능하게 됩니다. 

ext {
    javaVersion = 1.7
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'

    if(project.hasProperty('target')) {
        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-${project.target}"]
        }
    } else {
        String hostname = InetAddress.getLocalHost().getHostName();
        if(hostname.endsWith('.local')) {   //맥의 경우, .local 이 모든 hostname에 추가됩니다.
            hostname.replace(".local", '')
        }
        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-" + hostname]
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        compile 'org.slf4j:slf4j-api:1.7.5'
        testCompile "junit:junit:4.11"
    }
   
    sourceCompatibility = rootProject.ext.javaVersion
    targetCompatibility = rootProject.ext.javaVersion

    tasks.withType(Compile) {
        options.encoding = 'UTF-8'
    }
}


2. webProject의 local tomcat / jetty runner 설정

webProject를 구성할 때, 자신의 local server를 쉽게 구현할 수 있다는 것은 큰 장점입니다. javascript나 css, html의 수정을 바로바로 해볼 수 있다는 점에서 개발자의 편의를 증대시킬 수 있는 방법중 하나입니다. 

먼저, 기본적으로 webProject의 경우에는 모든 project가 'Web'으로 끝이 난다고 가정을 하면 좀 더 편하게 접근이 가능합니다. 모든 project들에 따로 설정을 해줄 필요도 없고 보다 직관적인 접근이 가능하기 때문에 개인적으로는 추천하는 방법입니다. 

project의 이름이 'Web'으로 끝나는 project 만을 선택하기 위해서 다음과 같은 configure를 설정합니다. 

configure(subprojects.findAll { it.name.endsWith('Web') }) {
     apply plugin: 'war'
}

이 subproject들은 이제 모두 'war' plugin을 갖습니다. 이제 tomcat과 jetty plug in을 이용해서 tomcat과 jetty를 실행시킬 수 있는 환경을 구축해야 됩니다. 

기본적으로 jetty의 경우, jetty plugin을 gradle에서 기본적으로 지원하고 있으나, jetty의 버젼이 6.0인 관계로 현 상황에서는 그다지 쓸만하지 못합니다. 3rd party plugin을 이용해서 최신의 jetty를 지원할 수 있도록 구성을 변경해야지 됩니다. 

외부 library를 끌어와서 gradle에서 사용해야지 되기 때문에 buildscripts 항목을 다음과 같이 추가합니다. 

    repositories {
        jcenter()
        maven { url = 'http://dl.bintray.com/khoulaiz/gradle-plugins' }
    }
    dependencies {
        classpath (group: 'com.sahlbach.gradle', name: 'gradle-jetty-eclipse-plugin', version: '1.9.+')
    }

이제 jetty를 사용할 수 있는 준비가 모두 마쳐졌습니다. configure에 이제 jetty plugin을 넣어주면, jettyEclipseRun/jettyEclipseWarRun task를 통해서 sub project들을 바로 web 으로  보여주는 것이 가능합니다. 

마지막으로 tomcat입니다. tomcat의 경우에는 gradle에서 제공되는 tomcat plugin이 제공되지 않았으나, 최근에 추가되어 활발히 사용되고 있습니다. 지금 버젼은 1.0이 나왔으며 아주 쉽게 사용이 가능합니다. buildscripts 항목에 다음을 추가합니다. 

classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
    
그리고 configure에 다음 항목을 추가합니다. 

    apply plugin: 'tomcat'

    dependencies {
        String tomcatVersion = '7.0.11'
        tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
        tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
        tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
            exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
        }
    }

위 항목의 추가로 tomcatRun을 실행시킬 수 있습니다. 

3. sonar analysis의 추가

code의 품질을 테스트 하고 code 품질에 대한 지속적인 monitoring을 통해서 project를 발전시키는 것이 가능합니다. 개발자들의 실력 향상에도 도움을 줄 수 있을 뿐 아니라 잠재적인 오류를 찾아내기 위해서라도 sonar analysis는 추가하는 것이 좋습니다. gradle에서는 sonar에 대해서 plugin을 지원하고 있으며, sonar version이 3.4 이하인 경우와 3.5 이상인 경우에 사용되는 plugin이 다름을 주의해야지 됩니다.  이 글은 sonar 3.5이상을 기준으로 구성되었습니다. 

sonar plugin은 언제나 multiple project의 root에 위치해야지 됩니다. 

apply plugin : 'sonar-runner'

그리고, sonar 서버에 대한 설정을 다음과 같이 root project에 넣어줍니다. 

sonarRunner {
    sonarProperties {
        property "sonar.host.url", "http://192.168.13.209:9000"
        property "sonar.jdbc.url", "jdbc:mysql://192.168.13.209:3306/sonar"
        property "sonar.jdbc.driverClassName", "com.mysql.jdbc.Driver"
        property "sonar.jdbc.username", "root"
        property "sonar.jdbc.password", 'qwer12#$'
    }
}

마지막으로 subprojects에 sonar가 해석할 code의 character set을 지정해주면 됩니다. 

    sonarRunner {
        sonarProperties {
            property "sonar.sourceEncoding", "UTF-8"
        }
    }

다만, 이렇게 한 경우 code coverage가 나오지 않습니다. 그 이유는 sonar-runner의 경우, jacoco를 가지고 있지 않습니다. 따라서, jacoco 설정을 추가해야지 됩니다. 

jacoco는 각 subproject들에 설정이 되어야지 되기 때문에 subprojects 안에 다음 plug in을 설정합니다. 

apply plugin : 'jacoco'

그리고, jacoco 설정을 넣습니다. jacoco결과를 sonar에서 해석하기 위한 exec 파일과 classdump 파일의 위치를 결정해야지 됩니다. sonar-runner는 기본적으로 ${projectDir}/target/jacoco.exec 에 있는 output 파일을 읽습니다. 현재 sonar-runner의 output 파일 load 경로를 바꾸는 방법은 없습니다. 따라서 jacoco plugin의 설정을 반드시 ${projectDir}/target/jacoco.exec로 바꿔줘야지 됩니다.  test를 행할때, 반드시 jacoco가 실행이 되도록 다음과 같이 설정합니다. 또한 output 파일 경로를 target/jacoco.exec로 변경시켜줍니다.

    test {
        jacoco {
            append = false
            destinationFile = file("target/jacoco.exec")
            classDumpFile = file("target/classpathdumps")
        }
    }

이제 gradle sonarRunner를 실행하면 sonar에 결과를 모두 표시하는 것을 볼 수 있습니다. 


4. jenkins 등록

gradle project의 경우 jenkins에서는 plugin 형태로 build process를 지원하고 있습니다. plugin 항목에서 gradle을 추가해야지 됩니다.

1) project type은 free-style software project로 등록합니다.

2) build는 invoke Gradle script로 수행합니다. 
* system에 gradle이 이미 설치가 되어 있어야지 됩니다.
* task는 sonarRunner로 등록

3) Test 결과를 jenkins에 등록합니다. sonar를 통해서도 보는 것이 가능하지만, jenkins에서 간단한 정보와 trend는 확인이 가능합니다. 더 자세한 정보를 원한다면 sonar를 통해 확인하면 더 좋습니다. 

a. findbugs analysis results : sonar에서는 ${rootProject}/build/sonar/${subProject}/findbugs-result.xml 로 findbugs result를 기록합니다. 파일 타잎을 **/findbugs-result.xml 로 지정해주면 됩니다. 


b. JUnit test 결과 등록 : sonar의 JUnit test 결과는 ${project}/build/test-reports/TEST-{testClassName}.xml 형태로 저장되어 있습니다. XMLs 항목을 **/TEST-*.xml 로 지정해주면 모든 test결과를 jenkins에 기록합니다.


c. JaCoCo coverage report 등록 : jacoco의 결과값은 위 설정에서 target/jacoco.exec로 이미 지정되어 있습니다. 각각의 project의 source code path만 조정하면 됩니다.
* Path to exec files : **/**.exec
* Path to class directories : **/classes/main
* Path to source directories : **/src/main/java
* Exclusions : **/classes/test


위와 같이 등록하면 이제 jenkins에서 모든 결과를 확인하고 볼 수 있습니다.




5. hot deploy의 지원

작성된 war를 개발자의 PC에서 테스트 환경 또는 배포환경으로 배포하는 과정을 지원하면 java로 만든 web project의 build과정 및 deploy과정이 모두 마쳐지게 됩니다. 
이 부분에 있어서 gradle은 따로 제공되고 있는 것이 없습니다. 대신 cargo plugin을 이용해서 hot deploy를 할 수 있습니다. cargo plugin은 지금 github에서 지원되고 있습니다.

먼저, 외부 library를 사용하기 때문에 buildscript에 다음을 추가해야지 됩니다. 

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'org.gradle.api.plugins:gradle-cargo-plugin:0.6.1'
    }
}

다음 war로 묶일 subproject의 configure에 plugin을 추가합니다. 

apply plugin: 'cargo'

dependencies {
    def cargoVersion = '1.3.3'
    cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
                "org.codehaus.cargo:cargo-ant:$cargoVersion"
}

여기서, dependencies에 cargo는 cargo에서만 임시로 사용될 dependency입니다. war의 providedCompile과 동일합니다. 

hot deploy될 remote server의 정보를 적어줍니다. 

cargo {
    containerId = 'tomcat7x'
    port = 8080

    deployable {
        context = "${project.name}"
    }

    remote {
        hostname = '192.168.13.209'
        username = 'ykyoon'
        password = 'qwer12#$'
    }
}

cargo는 tomcat, jetty, weblogic 등 유수의 web container를 지원합니다. 지원하는 container마다 Id를 부여하고 있으며, 아이디들은 https://github.com/bmuschko/gradle-cargo-plugin 에서 확인 가능합니다. 

이제 다음 명령어를 통해서 tomat에 deploy가 가능합니다. 

gradle cargoRemoteDeploy

기존에 Deploy가 되었다면 재배포가 이루어져야지 되기 때문에 
gradle cargoRemoteRedeploy
를 이용해서 배포가 가능합니다. 

추가로, cargo는 war로 만드는 작업을 하지 않습니다. war로 묶는 작업이 없기 때문에 war가 없으면 에러를 발생합니다. war task와 dependency를 잡아주기 위해서 다음 코드를 추가하면 더 편합니다. 

    cargoRedeployRemote {
        dependsOn war
    }

    cargoDeployRemote {
        dependsOn war
    }

이제 gradle을 이용한 java web project의 build의 전체 과정을 할 수 있는 script가 완성되었습니다. build를 관리하고, 자동화 하는 것은 개발자들이 보다 더 개발에 집중하고 나은 결과를 내기 위해서입니다. 또한 관리의 목적도 있습니다. 관리 및 공유의 목적을 가장 잘 달성하기 위한 방법으로 저는 개인적으로는 아래와 같이 생각합니다.

1. IDE 관련 파일은 SCM에 upload하지 않습니다. 지금 만들어진 script를 이용하면 eclipse, intelliJ 모두의 IDE load file을 만들어낼 수 있습니다. 각자의 개발환경이 겹치게 되면 개발에 어려움을 가지고 오게 됩니다. 개발에 필요하고 환경에 undependency한 file들만 올리는 것이 필요합니다.
2. jenkins와 같은 CI tool을 svn commit과 연동하지 않습니다. 연동 후, commit이 발생할 때마다 code coverage와 같은 report를 commit당으로 만들 필요는 없습니다. 시간을 정해두고 하는 CI가 더 효율적이라고 생각됩니다.

마지막으로 전체 script는 다음과 같습니다.

apply plugin: 'base'
apply plugin: 'sonar-runner'

import com.sahlbach.gradle.plugins.jettyEclipse.*

buildscript {
    repositories {
        jcenter()
        maven { url = 'http://dl.bintray.com/khoulaiz/gradle-plugins' }
    }
    dependencies {
        classpath (group: 'com.sahlbach.gradle', name: 'gradle-jetty-eclipse-plugin', version: '1.9.+')
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
        classpath (group: 'org.gradle.api.plugins', name: 'gradle-cargo-plugin', version: '0.6.+')
    }
}

ext {
    javaVersion = 1.7
}

sonarRunner {
    sonarProperties {
        property "sonar.host.url", "http://192.168.13.209:9000"
        property "sonar.jdbc.url", "jdbc:mysql://192.168.13.209:3306/sonar"
        property "sonar.jdbc.driverClassName", "com.mysql.jdbc.Driver"
        property "sonar.jdbc.username", "root"
        property "sonar.jdbc.password", 'qwer12#$'
    }
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'
    apply plugin: 'jacoco'

    sonarRunner {
        sonarProperties {
            property "sonar.sourceEncoding", "UTF-8"
        }
    }

    test {
        jacoco {
            append = false
            destinationFile = file("target/jacoco.exec")
            classDumpFile = file("target/classpathdumps")
        }
    }

    if(project.hasProperty('target')) {
        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-${project.target}"]
        }
    } else {
        String hostname = InetAddress.getLocalHost().getHostName();
        if(hostname.endsWith('.local')) {   //맥의 경우, .local 이 모든 hostname에 추가됩니다.
            hostname.replace(".local", '')
        }
        sourceSets {
            main.resources.srcDirs = ['src/main/resources', "src/main/resources-" + hostname]
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        compile 'org.slf4j:slf4j-api:1.7.5'
        testCompile "junit:junit:4.11"
    }
    
    sourceCompatibility = rootProject.ext.javaVersion
    targetCompatibility = rootProject.ext.javaVersion

    tasks.withType(Compile) {
        options.encoding = 'UTF-8'
    }
}

configure(subprojects.findAll { it.name.endsWith('Web') }) {
    apply plugin: 'war'
    apply plugin: 'jettyEclipse'
    apply plugin: 'tomcat'
    apply plugin: 'cargo'

    String configPath = project.rootProject.projectDir.absolutePath.toString() + "/config/jetty/webdefault.xml"

    dependencies {
        String tomcatVersion = '7.0.11'
        tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
        tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
        tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
            exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
        }

        def cargoVersion = '1.3.3'
        cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
                "org.codehaus.cargo:cargo-ant:$cargoVersion"
    }

    jettyEclipse {
        automaticReload = true
        webDefaultXml = file(configPath)
    }

    cargoRedeployRemote {
        dependsOn war
    }

    cargoDeployRemote {
        dependsOn war
    }

    cargo {
        containerId = 'tomcat7x'
        port = 8080

        deployable {
            context = "${project.name}"
        }

        remote {
            hostname = '192.168.13.209'
            username = 'ykyoon'
            password = 'qwer12#$'
        }
    }
}


모두 Happy codiing!!

모든 프로젝트는 GitHub에 올려뒀습니다. 참고하실 분들은 참고해주세요. ^^


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K


티스토리 툴바