2015. 11. 29.

Spring IO, Spring Boot, Spring Cloud 를 이용한 Configuration Server의 구축

서문

프로젝트를 진행하다보면, 배포 환경에 따른 설정 파일 관리에 여러가지로 어려움을 겪는 경우가 많다.
개발 환경설정파일에만 내용을 넣고 배포를 한다던가 배포시에 프로파일을 잘못 설정해서 배포가 된다던가 하는 실수들은 작지만 상당히 귀찮은 확인 작업을 동반하기도 한다.
그나마 자동화된 배포툴(Hudson 등등)을 사용하여 잘 짜여진 배포 프로세스를 만들어 놓은 훌륭한 회사에서는 그나마 이런 실수들을 줄일 수 있는 방법이 많지만, 아직도 쉘을 이용한 배포를 하는 우리 회사 같은 경우는 작업자의 실수를 가끔씩 겪을 수 밖에 없다. (이 경우 윗 사람으로 부터의 더 가혹한 소리들을 들어야만 한다. 부주의에 의한 실수이기에...)
특히, 실수가 일어난 경우 해당 내용을 확인하기 위해서는 배포되어있는 서버에 들어가 일일이 파일을 확인해야 하는데 이 작업 또한 만만찮은 귀찮은 작업 중 하나다.

모든 툴 들이 그렇듯 완벽하게 모두 해결해 주지는 않지만 Spring Cloud 의 Configuration 서버의 경우는 간단한 설정만으로도 꽤 괜찮은 솔루션을 제공해주고 있다.

이 글은 최대한 간단하게 만들어진 Configuration Server 에 대해서 정리하고자  작성된 글이다.
(거의 Spring Cloud 프로젝트 사이트의 Document 를 간단히 정리한 수준임을 밝혀둔다)

요구사항

- 버전 관리를 편리하게 하고 Gradle 을 간단히 유지하기 위해서 Spring IO 를 이용한다.
- Gradle 은 이전 포스트에서 작성했던 multi project 대응의 build.gradle 설정에서 시작한다.
  (이전글 보기)
- Configuration Server 는 Configuration Server의 local properties 를 이용한다. (github을 이용한 방법에 대한 설명은 인터넷에 굉장히 많고 spring cloud 프로젝트 사이트의 샘플도 이것으로 되어있으니 참고 바람)

Spring IO 설정

gradle 에서 사용할 다양한 버전 정보를 포함한 변수들을 저장하기 위한 gradle.properties 파일을 만들고 아래의 내용을 추가한다. 이 부분은 build.gradle 에 ext {} 를 이용해도 되나, 후에 이 블럭이 커져서 관리가 힘든 경우가 있어서 이번에는 gradle.properties 를 이용하는 것으로 했다.
dependencyManagementPluginVersion=0.5.4.RELEASE
springIoVersion=2.0.0.RELEASE


메인 프로젝트의 build.gradle의 buildscript 블럭에 아래의 내용을 추가한다.
buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementPluginVersion}"
    }
}

모든 프로젝트에 적용을 할 것이므로, allprojects 블럭을 아래와 같이 만든다.
allprojects {
    repositories {
        jcenter()
    }

    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        imports {
            mavenBom "io.spring.platform:platform-bom:${springIoVersion}"
        }
    }
}

여기까지 진행하면 일단 Spring IO 를 쓸 수 있는 환경은 완료!!

Configuration Server 의 build.gradle 작업

자, 이제 Configuaration Server의 build.gradle 파일을 만들고 Spring Cloud 설정을 해보자.
이전 글에서 만들었던 config-server(server-config-server)  모듈을 이용하도록 하겠다.

우선 아래처럼 spring-cloud 에서 사용할 dependencyManagement 를 위해 mavenBom 추가를 위해 아래와 같이 메인 프로젝트의 build.gradle 과 gradle.properties를 수정해보자.
dependencyManagementPluginVersion=0.5.2.RELEASE
springIoVersion=2.0.0.RELEASE
# Spring Cloud Dependency Management BOM Version
cloudStarterBomVersion=Angel.SR4
allprojects {
    repositories {
        jcenter()
    }

    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        imports {
            mavenBom "io.spring.platform:platform-bom:${springIoVersion}"

            // Spring Cloud 프로젝트를 위한 mavenBom 추가
            mavenBom "org.springframework.cloud:spring-cloud-starter-parent:$cloudStarterBomVersion"
        }
    }
}

자, 이제 config-server 모듈에 build.gradle 을 만들고 spring-cloud 의 dependency를 설정해보자.
dependencies {
    compile 'org.springframework.cloud:spring-cloud-starter-config'
    compile 'org.springframework.cloud:spring-cloud-config-server'
}

이제 gradle 설정을 다시 읽어보면 아래 프로젝트 구성과 config-server의 dependency 들은 아래와 같은 모습을 가지게 된다.




Configuration Server 의 프로그램 작업

config-server 모듈의 base package 에 spring boot 메인 클래스를 만들자.
편의상 ConfigServerApplication 이라는 클래스 파일을 생성하고 spring boot application 이므로 기본 annotation 과 메인 클래스를 작성하자.
(확인결과 default 패키지 즉 클래스패스에 package 없이 만들어진 경우 @SpringBootApplication 어노테이션은 사용할 수 없다.)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.context.annotation.Configuration;

/**
 * Created by road on 15. 11. 29.
 */
@Configuration
@EnableAutoConfiguration
@EnableConfigServer
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

굉장히 간단하다. 자 이제 configuration server 의 설정을 위해서 bootstrap.yml 을 생성해보자.
spring:
  application:
    name: configServer
  profiles:
    active: native
    
server:
  port: 8888

여기서 중요한 부분은 spring.profiles.active 를 native로 주는 정도가 되겠다.
이제 기본적인 서버 작업은 마무리 되었다.

이제 Configuration Server 에 설정 정보를 올리고 Client가 사용하는 샘플을 만들어 보자.

Configuration Server에 Sample Client의 설정파일 만들기

테스트용으로 만들 프로그램은 sample 이라는 이름을 가진 프로그램으로 작업하려고 한다.
지원하는 profile 은 dev, prod 이며, 그 설정을 클라이언트의 spring active profile 과 연결하려고 한다.

우선 configuration server 에 설정을 보관할 디렉터리와 파일을 생성한다.
github 이 아닌 file base 로 동작하기 위해서 앞에서 active profile 을 native로 만들었었다.
이 경우 spring 이 파일을 찾는 경로는 spring.cloud.config.server.native.searchLocations의 설정을 따르게 되어있다.
Default 로는 [classpath:/, classpath:/config, file:./, file:./config]의 설정을 따르게 된다. 여기서는 classpath:/config 를 사용하게 하기 위해서 resources에 config 디렉터리를 생성하고 그 안에 sample.yml, sample-dev.yml, sample-prod.yml 세개의 파일을 생성하고 아래의 내용을 입력하여 테스트 하도록 하겠다.
이 경우 sample.yml 은 profile에 상관없이 default로 읽혀지는 설정들이다.
즉, http://localhost:8888/sample/default  하면 나오는 기본값이다.
나머지 두개는 active profile에 따라서 읽혀지는 설정들이다.
http://localhost:8888/sample/dev 에 접속해보면 알 수 있지만, profile에 관계없이 기본 default 설정은 모두 읽고 각 profile 에 맞는 설정을 읽는 방식으로 되어있다.
다시 말해 공통 설정은 해당 {application name}.yml 에 넣고 profile에 따라 달라지는 설정은 {application name}-{profile}.yml 에 파일에 넣으면 된다.

여기까지의 프로젝트 구조와 설정파일 내용은 아래와 같다.

server:
  port: 8088

sample:
  foo: 'this is dev foo'

sample:
  foo: 'this is prod foo'
  bar: 'this is prod bar'

이제 이것을 사용할 client 프로그램을 만들어 보자.
이 부분은 별다른 설명 없이 진행하겠다. service 그룹에 sample 모듈을 만들고 시작하겠다.
이전 포스트에서 설명했듯이 service 디렉터리에 sample 이라는 디렉터리를 만들면 서브 프로젝트가 생성되고 그곳에 build.gradle 과 기타 프로그램 작업을 진행한다.

Sample Application 작업

service 디렉터리 하위에 sample 이라는 디렉터리를 생성한 후에 gradle을 refresh 하면 해당 모듈이 생성된 것을 볼 수 있다.

그 하위에 build.gradle 파일을 만들고 아래와 같이 dependency 를 추가한다.
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.cloud:spring-cloud-starter-config'
}

나중에 sample로 RestController 작업할 거라서 spring-boot-starter-web 을 함께 포함시켰다.

이제 SampleApplication.java 파일을 다음과 같이 만든다. Environment 설정을 프린트 하는 더 세련된 방법이 있으니 각자 찾아보시길... 난 일단 Configuration Server에 등록한 정보 중 sample-prod.yml 에만 있는 sample.bar 값에 대한 default 처리를 위해서 우선은 따로 처리하였다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by road on 15. 11. 29.
 */
@Configuration
@EnableAutoConfiguration
@RestController
public class SampleApplication {

    @Value("${sample.foo:this is foo by default}")
    private String fooValue = "";
    @Value("${sample.bar:this is bar by default}")
    private String barValue = "";

    @RequestMapping(value = "/configStatus")
    public Map getProperties() {
        Map configStatus = new HashMap<>();
        configStatus.put("foo", fooValue);
        configStatus.put("bar", barValue);
        return configStatus;
    };


    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
이제 마지막으로 bootstrap.xml 을 resources 에 등록하여 설정을 마무리 해보자.
spring:
  cloud:
    config:
      uri: http://localhost:8888
  profiles:
    active: dev
  application:
    name: sample

그리고 하는 김에 설정이 어떻게 동작하는지 볼 겸 application.yml 도 만들어 server.port=8080만을 추가해보았다.
server:
  port: 8080
최종적인 프로젝트의 모습은 아래와 같이 된다.
이제 Sample Application 을 실행해보자. (당연히 Configuration Server 를 먼저 실행해야 한다)
그럼 마지막 부분에 port가 8088로 실행된 것을 확인할 수 있다.
분명이 SampleApplication의 application.yml에서 server.port 를 8080으로 설정했음에도 불구하고 Configuration의 sample.yml에 등록한 8088이 실행되었다.

자 이제 http://localhost:8088/configStatus 에 접속해보자.
결과는 {"bar":"this is bar by default","foo":"this is dev foo"} 라고 나온다.
foo 는 configuration server의 값을 읽었고, bar는 SampleApplication의 @Value에 설정된 default 값이 설정되어있다.

이제 SampleApplication의 active profile 을 prod로 변경하고 다시 실행하고 다시 http://localhost:8088/configStatus 에 다시 접속해보면,
결과는 {"bar":"this is prod bar","foo":"this is prod foo"} 가 나오는 것을 확인할 수 있다.

결론

지금까지 간단하게 Configuration Server를 설정하는 방법에 대해서 알아봤다.
앞에서 봤지만 기본 서버 포트를 비롯하여 대부분의 Client Application 설정을 Configuration Server를 이용해서 처리할 수 있다.(대부분이지 전부는 아니다. 예를 들어 당연하겠지만 spring cloud 관련 설정들은 Configuration Server에 할 수 없다.)

여러 Application으로 나뉘어 개발되어지는 서비스가 있다면, 간단하게 Configuration Server를 구성하는 것으로 Property 때문에 배포대상에 따라 다시 빌드 한다거나 내용을 확인하기 위해 모든 서버에 접속을 한다거나 하는 부담을 많이 덜 수 있을 것이다.

데이터베이스 Connection 정보라던가 하는 것들의 정보의 경우는 일반 텍스트로 할 경우 불안감이 생길 수 있는데 이 부분을 위해서 Configuration Server의 경우 암호화 복호화 기능을 지원한다.
이 부분은 간단하지만 다음번에 정리를 해 볼까 한다.

궁금하다면 http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html#_encryption_and_decryption_2 에서 확인을 해 볼 수 있다.
또한, 이 외에도 Configuration이 변경되었을 때에 다시 적용할 수 있는 방법등에 대한 다양한 자료가 있으니 http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html 를 한번 쯤은 읽어보기를 것을 추천한다.



2015. 11. 28.

Gradle 을 이용해서 Multiproject 를 구성하는 방법 (중 하나)

개요

회사에서 Gradle 을 이용하게 된 이래로 계속 Multi-project 형태로 설정하여 진행을 해 오고 있다.
매번 멀티 프로젝트 형태로 만들어지고 있는 회사의 프로젝트들이다 보니 그때마다 다시 이전 빌드 스크립트를 보면서 만드는데 프로젝트들이 복잡하다보니 필요없는 설정들까지 복사해서 쓰고 있는 부분들이 있어서 한번 정리를 했으면 하고 있었다.
가장 기본적인 상태의 멀티프로젝트용 build.gradle 에 대한 여러가지 방법 중 한가지라 생각하고 참고가 된다면 좋겠다.

요구사항

기본 요구사항은 다음과 같다.
1) 멀티프로젝트는 디렉터리를 기반으로 아래와 같이 그룹으로 만들어 질 수 있어야 한다.
  - Shared : 다른 프로젝트에서 Dependency로 추가될 수 있는 공통 라이브러리를 포함하는 라이브러리 모듈 그룹
  - Web : Front 모듈 그룹
  - Server : 주로 어플리케이션 간의 설정 등을 관리하는 Server 모듈 그룹
  - Service : Web API 서버 모듈 그룹
2) 모든 Subproject 들은 Java project 이며, 프로젝트 명은 "모듈그룹명-모듈명" 으로 만든다. 즉, Server 모듈의 configuration-server 모듈이라면 server-configuration-server의 형태로 만들어지면 된다.

이후에 작업된 모든 내용은 Ubuntu 14.04 OS와 IntelliJ IDEA 15에서 작업되었다.
(윈도우즈와 이클립스에서도 동일한 내용을 동작이 될 것으로 보인다. 다만, 테스트되지 않았을 뿐이다)

Main Gradle Script 작업

1) 파일 구성
  - build.gradle : gradle script 파일
  - settings.gradle : sub project 관리를 위한 파일

2) build.gradle 파일 작성
  - 우선은  프로젝트 기본 정보로 group 정보와 version 을 설정한다. 본인의 입맛에 따라 설정하면 된다. 또한, 전체 프로젝트를 감싸는 프로젝트 인 만큼 plugin은 base 로 설정한다.
group 'com.road.sample'
version '1.0-SNAPSHOT'
apply plugin: 'base'
apply plugin: 'eclipse'
apply plugin: 'idea'

  - buildscript 와 allprojects 블럭을 설정한다. 여기서는 repository 정보로 jcenter(bintray) 를 설정했지만, 많이 사용하는 mavenCentral() 이나 커스텀으로 설정해도 된다.
buildscript {
    repositories {
        jcenter()
    }
}

// Wrapper를 이용해서 gradle version을 정해주는 것이 좋다.
task wrapper(type: Wrapper) {
    // version 정보는 후에 gradle.properties로 옮기는 게 좋을 듯 하다.
    gradleVersion = '2.9'
}

allprojects {
    repositories {
        jcenter()
    }
}

- subproject 들을 위한 설정을 진행한다. 아직은 별다른 내용이 없으므로,  모든 subproject는 자바 프로젝트라는 요구사항을 만족시키기 위해서 java plugin 으로 설정하고, 자바 버전과 인코딩 설정 그리고 모든 프로젝트에 junit dependency를 설정해주는 정도의 작업을 진행한다.
subprojects { subproject ->
    apply plugin: 'java'

    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

    repositories {
        jcenter()
    }

    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.11'
    }

}


여기까지 기본 설정은 마무리가 되었다.

여기까지 진행되면 아래와 비슷한 모습이 되어있을 것으로 기대된다.

(settings.gradle 파일은 아직 없을 것이지만...)

이제 subproject 를 설정하기 위한 작업들을 진행해야 한다.

요구사항에서 하위 그룹 디렉터리는 크게 Shared, Web, Server, Service 로 나누어진다.
해당 디렉터리의 하위에 모듈을 생성하고 그래들 빌드를 하면 해당 모듈이 서브 프로젝트로 등록되도록 처리하는 작업이 남았다.

우선은 settings.gradle 파일에 대한 설정을 먼저 진행하자.

3) settings.gradle 작성

- 우선은 root project의 이름을 지정한다. 안해도 되지만, root project가 rootproject 따위로 나오는 건 원하지 않으니까...
rootProject.name = 'gradle-multiproject'

- 요구사항의 그룹 디렉터리를 돌면서 하위 디렉터리가 있으면 서브프로젝트로 등록하는 스크립트를 등록하자.  만약 그룹 디렉터리가 없으면 해당 디렉터리를 생성하는 스크립트도 넣어보자.
['shared', 'web', 'server', 'service'].each {
    def projectDir = new File(rootDir, it)

    // 만약 그룹디렉터리가 없으면 생성한다.
    if( !projectDir.exists() ) {
        projectDir.mkdirs()
    }

    // 모듈 그룹 디렉터리 하위에 디렉터리가 있으면 서브프로젝트로 등록한다.
    projectDir.eachDir { dir ->
        include ":${it}-${dir.name}"
        project(":${it}-${dir.name}").projectDir = new File(projectDir.absolutePath, dir.name);
    }
}

여기서 build.gradle 설정을 다시 적용해보면 (각 IDEA에 맞춰서 또는 gradle 스크립트로) 아래 이미지와 같은 디렉터리 상태로 변경되어있을 것이다.

물론 gradle projects 명령을 이용해서 결과를 보면 아직 서브프로젝트가 없으므로 아래와 같이 나올 것이다.


이제 여기서 하위 모듈 하나 만들어서 넣어보자.

하위 모듈의 등록

하위 모듈은 server 그룹 디렉터리 안에 config-server를 하나 등록해 보고자 한다.

1) 디렉터리 생성

- server 디렉터리 하위에 config-server 라는 디렉터리를 하나 만들고 build.gradle 설정을 다시 적용해보자.



위 그림과 같이 정상적으로 서브 프로젝트가 만들어졌는데 뭔가 귀찮은 일이 벌어져버렸다.
자바 프로젝트의 기본 폴더인 src, src/java, src/main, src/test 등등을 다 수작업을 해야 하는 상황이 발생한거다. 이것까지 기본으로 하면 좋을 것 같다.

- 자 다시 메인 디렉터리의 build.gradle의 subprojects 처리 블럭에 아래의 내용을 추가하자.
    task initSourceFolders {
        subproject.sourceSets*.java.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }

        subproject.sourceSets*.resources.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }
    }

이제 build.gradle 을 다시 적용해보면 아래와 같은 형태로 기본 디렉터리 작업이 완료되고 해당 build path 등도 정상적으로 적용되어있는 것을 확인할 수 있다. 이제 끝.

하위 모듈들의 디렉터리에도 되도록이면 build.gradle 파일을 만들고 그 안에 dependency 를 작업하는 것이 메인의 dependency 관련 설정을 줄일 수 있어서 좋다.

이전에는 subprojets 처리 부분에서 각 프로젝트의 이름을 잡아서 설정을 넣어주는 방식을 했었는데, 처음에는 모듈 그룹에 적용하기 편리한 부분이 있었으나, 이후 프로젝트별로 나뉘어지는 dependency들까지 모두 그 안에 넣다보니 너무 복잡해지는 불편함이 있었다.
두가지 방법을 적당히 섞어서 쓴다면 매우 편리할 듯 하다.

그럼 그래들과 함께 개발을 편하게 진행할 수 있기를 바라며, 마지막으로 완성된 build.gradle과 settings.gradle 의 내용은 아래와 같다.

[build.gradle]

group 'com.road.sample'
version '1.0-SNAPSHOT'

apply plugin: 'base'
apply plugin: 'eclipse'
apply plugin: 'idea'

buildscript {
    repositories {
        jcenter()
    }
}


allprojects {
    repositories {
        jcenter()
    }
}

task wrapper(type: Wrapper) {
 gradleVersion = '2.9'
}

subprojects { subproject ->
    apply plugin: 'java'

    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

    repositories {
        jcenter()
    }

    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.11'
    }

    task initSourceFolders {
        subproject.sourceSets*.java.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }

        subproject.sourceSets*.resources.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }
    }
}

[settings.gradle]
rootProject.name = 'gradle-multiproject'
['shared', 'web', 'server', 'service'].each {
    def projectDir = new File(rootDir, it)

    // 만약 그룹디렉터리가 없으면 생성한다.
    if( !projectDir.exists() ) {
        projectDir.mkdirs()
    }

    // 모듈 그룹 디렉터리 하위에 디렉터리가 있으면 서브프로젝트로 등록한다.
    projectDir.eachDir { dir ->
        include ":${it}-${dir.name}"
        project(":${it}-${dir.name}").projectDir = new File(projectDir.absolutePath, dir.name);
    }
}