잊지 않겠습니다.

기존 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 Y2K
,