Spring Boot 를 이용한 Spring Cloud Config 구성
Spring Cloud Config란?
Spring Cloud Config는 분산 시스템에서 외부화 된 구성(configuration) 에 대한 서버 및 클라이언트 측 지원을 제공한다.
이는 환경 설정등의 외부화를 통해 중앙에서 어플리케이션의 속성등을 관리하는 용도로 쓰인다.
시스템의 환경설정이나 Properties 설정등을 중앙으로 분리하여 관리하게 함으로써 설정정보를 배포하지 않아도 실시간으로 설정을 적용할 수 있다.
기존 어플리케이션에서 Profile 설정 -Dspring.profiles.active
을 이나 properties 의 Key통해 Production 환경과 Develop, Local 환경 값들을 관리하던것을 분리하고 쉽게 변경하기 위해 사용된다.
공식 사이트 Spring Cloud Config
Spring Cloud Config는 분산 시스템에서 외부화 된 구성(configuration) 에 대한 서버 및 클라이언트 측 지원을 제공한다.
이는 환경 설정등의 외부화를 통해 중앙에서 어플리케이션의 속성등을 관리하는 용도로 쓰인다.
시스템의 환경설정이나 Properties 설정등을 중앙으로 분리하여 관리하게 함으로써 설정정보를 배포하지 않아도 실시간으로 설정을 적용할 수 있다.
기존 어플리케이션에서 Profile 설정
시스템의 환경설정이나 Properties 설정등을 중앙으로 분리하여 관리하게 함으로써 설정정보를 배포하지 않아도 실시간으로 설정을 적용할 수 있다.
기존 어플리케이션에서 Profile 설정
-Dspring.profiles.active
을 이나 properties 의 Key통해 Production 환경과 Develop, Local 환경 값들을 관리하던것을 분리하고 쉽게 변경하기 위해 사용된다.공식 사이트 Spring Cloud Config
주요 특징
- 외부 구성 (이름 - 값 쌍 또는 이와 동등한 YAML 내용) 을위한 HTTP, 자원 기반 API
- 속성 값 암호화 및 해독 (대칭 또는 비대칭)
@EnableConfigServer
를 사용하여 Spring Boot 응용 프로그램에 쉽게 임베드 가능
- 원격으로 설정을 적용하고 수정할 수 있음
기본적으로 private 한 환경의 Git이나 SVN과 같은 버전 관리 시스템에 설정을 올려놓고 클라이언트가 설정 정보를 읽어오는 Config Server에 접근하여 설정값들을 읽어오는 구조라고 이해할 수 있다.
대형 분산 환경에서 서버의 테스트 환경도 다양해지고 Service 하는 도메인들도 MSA 구조로 바뀌면서 설정 정보 변경은 잦을수 밖에 없으며, 바뀔 때 마다 Production 환경에 빌드&배포를 한다는 것은 굉장히 비효율적이고 부담이 되는 작업일 수 밖에 없다. 특히나 클라우드 환경에서 머신들이 Auto Scaling 하는 환경이라면 더더욱 부담이 될 수 밖에 없을것이다.
이런 상황에서 중앙 제어를 통한 설정의 외부화는 장애를 사전에 방지하고 재빠르게 설정을 적용하는 등의 잇점이 있는 것은 분명한 일이다.
Spring Cloud Config Server는 git이나 file system 을 통해 설정을 외부화 하여 repository를 구성하고 각 MSA (Client Application) 도메인들은 Config Server로 부터 감지된 정보들을 적용하는 형태가 된다.
- 외부 구성 (이름 - 값 쌍 또는 이와 동등한 YAML 내용) 을위한 HTTP, 자원 기반 API
- 속성 값 암호화 및 해독 (대칭 또는 비대칭)
@EnableConfigServer
를 사용하여 Spring Boot 응용 프로그램에 쉽게 임베드 가능- 원격으로 설정을 적용하고 수정할 수 있음
기본적으로 private 한 환경의 Git이나 SVN과 같은 버전 관리 시스템에 설정을 올려놓고 클라이언트가 설정 정보를 읽어오는 Config Server에 접근하여 설정값들을 읽어오는 구조라고 이해할 수 있다.
대형 분산 환경에서 서버의 테스트 환경도 다양해지고 Service 하는 도메인들도 MSA 구조로 바뀌면서 설정 정보 변경은 잦을수 밖에 없으며, 바뀔 때 마다 Production 환경에 빌드&배포를 한다는 것은 굉장히 비효율적이고 부담이 되는 작업일 수 밖에 없다. 특히나 클라우드 환경에서 머신들이 Auto Scaling 하는 환경이라면 더더욱 부담이 될 수 밖에 없을것이다.
대형 분산 환경에서 서버의 테스트 환경도 다양해지고 Service 하는 도메인들도 MSA 구조로 바뀌면서 설정 정보 변경은 잦을수 밖에 없으며, 바뀔 때 마다 Production 환경에 빌드&배포를 한다는 것은 굉장히 비효율적이고 부담이 되는 작업일 수 밖에 없다. 특히나 클라우드 환경에서 머신들이 Auto Scaling 하는 환경이라면 더더욱 부담이 될 수 밖에 없을것이다.
이런 상황에서 중앙 제어를 통한 설정의 외부화는 장애를 사전에 방지하고 재빠르게 설정을 적용하는 등의 잇점이 있는 것은 분명한 일이다.
Spring Cloud Config Server는 git이나 file system 을 통해 설정을 외부화 하여 repository를 구성하고 각 MSA (Client Application) 도메인들은 Config Server로 부터 감지된 정보들을 적용하는 형태가 된다.
Spring Cloud Config Server는 git이나 file system 을 통해 설정을 외부화 하여 repository를 구성하고 각 MSA (Client Application) 도메인들은 Config Server로 부터 감지된 정보들을 적용하는 형태가 된다.
기본 정보 구성하기
기본적인 설정 정보를 Git 에 올려 놓고 Config 서버를 구성해 보자.
(설정 정보만 구성하는것은 아니고 실제 서비스의 domain 별 구성정보라던가 서버별 구동 profile 이라던가 다양한 구성을 할 수 있다.)
github 에서 환경 설정 값을 저장하기 위한 새 repository 를 생성한다.
여기서는 spring-cloud-config 라고 명명하였다.
리파지토리가 생성되면 에디터등을 통해 두개의 파일 config-dev.yml, config-production.yml
을 생성한다.
여기서는 IntelliJ의 Check out from Version Control 의 git 을 통해 파일을 생성한 후 Push 하였다.
Git url 을 지정하고 디렉토리에 Clone 한다. (github에서 생성해도 상관없다)

config-dev.yml, config-production.yml
파일에 각각 mode: "develop"
, mode: "production"
을 입력 한 후 Push 한다.

Github 에 적용된 파일이 올라갔다면 이제 이 파일을 읽어들이는 Config Server를 구성해보자.
IntelliJ의 Create New Project 를 통해서 Spring Initializr 를 선택한 후 항목들을 입력한다.
우리가 구성할 것은 Spring Cloud Config 이므로 아래와 같이 Boot 2.x 의 Cloud Config 항목에서 Config Server 번들을 선택한다.
실제 Config server 에 입력해야 할 항목은 @EnableConfigServer
어노테이션을 추가한 후 application.properties 에 아래와 같이 Git Repository 를 입력하는 것으로 설정은 끝이 난다.
server.port=8080
spring.cloud.config.server.git.uri=https://github.com/gliderwiki/spring-cloud-config.git
SpringCludeApplication의 전체 소스는 아래와 같다.
package com.example.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class SpringcloudApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudApplication.class, args);
}
}
boot 어플리케이션을 기동한 후 http://localhost:8080/config/dev 혹은 http://localhost:8080/config-dev/default 로 접속하면 Git 의 초기 설정 정보가 아래와 같이 JSON으로 내려오는 것을 확인할 수 있다.
현 화면에서는 8080 포트로 작성하였으나 추후 여러 클라이언트가 붙는 상황을 가정하여 8888 포트로 변경하였다.
위와 같은 구조로 클라리언트 어플리케이션 들이 붙는다고 가정하면 local, dev, stage, production 과 같은 구조의 설정 파일들을 분리 하여 적용할 수 도 있고, 각 도메인 별 설정파일 안에 다양한 환경 profile 들을 구성해서 적용할 수 있다는 것을 예상할 수 있을 것이다.
실제 config yml 파일들을 아래와 같이 변경한 후 push 하고 재 접속해보도록 하자.
config-dev.yml
service:
mode: "develop"
params:
args1 : Hello
args2 : World
config-production.yml
service:
mode: "production"
params:
args1 : Real
args2 : Service
간단한 Config server 구성만으로 적용된 내용이 갱신되는 것을 확인할 수 있다.
Git 에 저장한 설정파일은 yml이나 properties 둘 다 적용이 가능하다.
이제 Client 레벨에서 Config Server 에 접근하는 두개의 어플리케이션을 통해 하나는 개발용 Config를, 나머지 하나의 서비스는 리얼 Config의 설정된 내용들을 적용해보도록 한다.
초기 세팅은 http://start.spring.io/ 의 Spring Initializr를 사용하여 프로젝트를 제너레이션 해도 무방하다. 여기서는 Config Client 와 Web 를 선택한다.
IntelliJ에서는 File > New > Project 를 이용해 신규 클라이언트 프로젝트를 설정한다. 마찬가지로 Spring initializr 를 선택 한 후 다음을 클릭한다.
member-service 로 Artifact를 지정하고 다음을 클릭하고 마찬가지로 Config Client 와 Web 번들을 선택한다.
member-service 프로젝트는 Config Server 정보를 바라봐야 하기 때문에 아래와 같이 bootstrap.yml 에 Config Server 정보를 연결해준다.
server:
port: 8081
spring:
application:
name: member-service
cloud:
config:
url: http://localhost:8888
name: config
8888 포트의 Config 서버 정보를 url 로 기술하고 현재 프로젝트의 명, 접근 하는 서버의 명 정도를 기술한 후 8081 포트로 어플리케이션을 실행할 것이다.
속성값을 정의 할 때 application.properties 보다 bootstrap의 설정값을 먼저 읽어온다.
이는 Config Server의 정보와 이름 정도만 기술하면 어플리케이션의 구동시 cloud name 을 통해 HTTP 로 호출을 해서 구성을 검색하게 된다.
따라서 구동시에 application.properties 보다 먼저 읽어와야 할 속성이 있을 경우 bootstrap 에 설정하도록 한다.
마지막으로 Config 가 읽어오는 설정 파일중에 dev 설정을 읽어와야 하므로 VM Option으로 -Dspring.profiles.active={profile}
을 추가 해준다. 여기서는 config-dev.yml을 읽을 예정이므로 dev로 설정하였다.
필요에 따라 config 구성 파일 내에 local, dev, stage, production 을 구분 지어 놓아도 무방하다.
이제 어플리케이션을 실행해보자.
아래와 같이 Fetching config from server at: http://localhost:8888
으로 Config Server에 연결한 것을 확인할 수 있으며 profile 정보는 dev로 active 된것을 확인할 수 있다.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.2.RELEASE)
2018-06-01 10:08:22.687 INFO 54538 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
2018-06-01 10:08:23.424 INFO 54538 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=member-service, profiles=[dev], label=null, version=21a6b130c5b4f2f2546ee776656d7c394b60a653, state=null
2018-06-01 10:08:23.424 INFO 54538 --- [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}]}
2018-06-01 10:08:23.427 INFO 54538 --- [ main] c.e.s.m.MemberServiceApplication : The following profiles are active: dev
2018-06-01 10:08:23.435 INFO 54538 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2609b277: startup date [Fri Jun 01 10:08:23 KST 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@3754a4bf
2018-06-01 10:08:23.705 INFO 54538 --- [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=ddc33e78-033f-3c04-8011-781be6cad530
2018-06-01 10:08:23.737 INFO 54538 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$278e6290] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2018-06-01 10:08:23.876 INFO 54538 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2018-06-01 10:08:23.891 INFO 54538 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-01 10:08:23.891 INFO 54538 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.31
2018-06-01 10:08:23.895 INFO 54538 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/Users/kakao/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.]
2018-06-01 10:08:23.966 INFO 54538 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-01 10:08:23.966 INFO 54538 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 531 ms
2018-06-01 10:08:24.052 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-01 10:08:24.116 INFO 54538 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.244 INFO 54538 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2609b277: startup date [Fri Jun 01 10:08:23 KST 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@3754a4bf
2018-06-01 10:08:24.279 INFO 54538 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-01 10:08:24.279 INFO 54538 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-01 10:08:24.295 INFO 54538 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.295 INFO 54538 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.423 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-01 10:08:24.429 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'configurationPropertiesRebinder' has been autodetected for JMX exposure
2018-06-01 10:08:24.429 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'environmentManager' has been autodetected for JMX exposure
2018-06-01 10:08:24.430 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'refreshScope' has been autodetected for JMX exposure
2018-06-01 10:08:24.432 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located managed bean 'environmentManager': registering with JMX server as MBean [org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager]
2018-06-01 10:08:24.439 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located managed bean 'refreshScope': registering with JMX server as MBean [org.springframework.cloud.context.scope.refresh:name=refreshScope,type=RefreshScope]
2018-06-01 10:08:24.445 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located managed bean 'configurationPropertiesRebinder': registering with JMX server as MBean [org.springframework.cloud.context.properties:name=configurationPropertiesRebinder,context=2609b277,type=ConfigurationPropertiesRebinder]
2018-06-01 10:08:24.487 INFO 54538 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2018-06-01 10:08:24.490 INFO 54538 --- [ main] c.e.s.m.MemberServiceApplication : Started MemberServiceApplication in 2.589 seconds (JVM running for 3.514)
이제 member-service 프로젝트에서 설정 정보를 정상적으로 읽어오는지 Controller를 작성해서 확인해보자.
package com.example.springcloud.memberservice;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MemberController {
@Value("${service.params.args1}")
private String arguments1;
@Value("${service.params.args2}")
private String arguments2;
@GetMapping("/member")
public String member() {
return arguments1 + arguments2;
}
}
Git 에 올려놓은 설정파일중에 config-dev.yml 내에는 아래와 같은 정보들을 구성해놨기 때문에 이를 읽어와서 화면에 뿌려주는 간단한 어플리케이션이다.
service:
mode: "develop"
params:
args1 : Hello
args2 : World
어플리케이션을 재구동하고 localhost:8081/member 에 들어가면 원격지 git 의 설정중에 config-dev.yml을 ConfigServer가 읽어와서 클라이언트에 연결된 상태임을 알 수 있다.
하지만 여기서 중요한 것은 @Value 로 주입한 정보는 Static 한 값이므로 Git 설정 값을 변경해서 Push 한다고 해도 변경 사항이 바로 반영되지는 않는다. (왜??? 인지는 이후에 설명한다.)
이 부분은 다른 방식으로 처리해야 하는데 (profile 옵션을 여러개를 운용할 경우 profile parameter 값은 제대로 물고 오는 것을 확인할 수 있다.) 바로 @RefreshScope
어노테이션을 해당 클라이언트에 추가해주어야 한다.
그래도 바로 반영되는 것은 아니고, Spring boot 2.x 부터는 Spring boot actuator 를 이용해 POST 방식으로 end point 를 호출을 해주어야 적용이 된다. (여기서는 http://localhost:8081/actuator/refresh 를 curl이나 postman 등의 rest client 툴을 활용해 post 방식으로 호출해주어야 적용이 된다)
Spring boot 1.x 에서는 /refresh url 을 Post 로 호출해주었어야 했다.
아마도 한번 더 확인하고 반영을 할 수 있도록 하나의 단계를 더 마련한것이 아닌가 추측된다.
실제로 공식문서에는 아래와 같이 설명하고 있다.
The EnvironmentChangedEvent
covers a large class of refresh use cases, as long as you can actually make a change to the Environment
and publish the event (those APIs are public and part of core Spring). You can verify the changes are bound to @ConfigurationProperties
beans by visiting the /configprops
endpoint (normal Spring Boot Actuator feature). For instance a DataSource
can have its maxPoolSize
changed at runtime (the default DataSource
created by Spring Boot is an @ConfigurationProperties
bean) and grow capacity dynamically. Re-binding @ConfigurationProperties
does not cover another large class of use cases, where you need more control over the refresh, and where you need a change to be atomic over the whole ApplicationContext
. To address those concerns we have @RefreshScope
.
이제 @RefreshScope
어노테이션을 통해 동적으로 설정이 변경되는 작업을 진행해보자.
위의 과정과 마찬가지로 movie-service 라는 프로젝트를 생성한다. 이번에는 Spring Initializr 선택 시 하나 더 추가해야 하는데, Actuator 번들을 반드시 포함 해야 한다.
bootstrap.yml의 port는 8082로 설정하고 VM option 도 -Dspring.profiles.active=production
으로 변경 한다.
server:
port: 8082
spring:
application:
name: movie-service
cloud:
config:
url: http://localhost:8888
name: config
어플리케이션이 정상적으로 동작하는지 확인한 후 controller 를 아래와 같이 작성해보자.
@RestController
@RefreshScope
public class MovieController {
@Value("${service.params.args1}")
private String arguments1;
@Value("${service.params.args2}")
private String arguments2;
@GetMapping("/movie")
public String movie() {
return arguments1 + arguments2;
}
}
@RefreshScope
어노테이션을 추가해주었다.
http://localhost:8082/movie 로 브라우저에 접근해서 config-production.yml 에 있는 arg 값이 출력되는지 확인한다.
이제 spring-cloud-config 프로젝트에 있는 git yml 의 arg1, 2 값을 적당히 수정해서 푸시 해보자.
service:
mode: "production"
params:
args1 : Real
args2 : Service for KAKAO !!
위의 주소로 다시 접근해도 메세지는 변경 되지 않을 것이다.
터미널 혹은 rest client 를 사용하여 http://localhost:8082/actuator/refresh 를 POST 방식으로 호출한 후 다시 접근해본다.
$ curl -X POST http://localhost:8082/actuator/refresh
혹은 아래와 같이 입력해도 된다.
$ curl localhost:8082/actuator/refresh -d {} -H "Content-Type: application/json"
응답이 제대로 오는지 확인한 후 페이지를 호출하면 서버의 리로드 없이도 설정된 정보들이 출력되는 것을 확인할 수 있다.
이로써, 우리는 어플리케이션 내에서 속성값이 변경되거나 환경 설정값이 변경될때 마다 빌드 후 재 배포의 부담에서 보다 더 자유로워졌다.
관심사의 분리에서 시작된 어플리케이션 내의 성격이 다른 횡적 기능들에 대한 분리와 CoC와 더불어 향후 MSA 지향으로 대량의 트래픽을 처리하기 위해 환경이 비대해지고 중첩되는 것들을 별도로 분리한다거나, Profile 별 설정이나 도메인별 속성 설정등을 어플리케이션 빌드&배포와 상관없이 관리할 수 있다는 점만으로도 충분히 매력적이라고 생각한다.
Spring Cloud Config는 같은 번들의 Eureka나 Zuul 을 연결함으로써 좀 더 다이나믹한 서버 구조를 활용할 수 있다.
= FIN =
기본적인 설정 정보를 Git 에 올려 놓고 Config 서버를 구성해 보자.
(설정 정보만 구성하는것은 아니고 실제 서비스의 domain 별 구성정보라던가 서버별 구동 profile 이라던가 다양한 구성을 할 수 있다.)
(설정 정보만 구성하는것은 아니고 실제 서비스의 domain 별 구성정보라던가 서버별 구동 profile 이라던가 다양한 구성을 할 수 있다.)
github 에서 환경 설정 값을 저장하기 위한 새 repository 를 생성한다.
여기서는 spring-cloud-config 라고 명명하였다.
여기서는 spring-cloud-config 라고 명명하였다.
리파지토리가 생성되면 에디터등을 통해 두개의 파일
여기서는 IntelliJ의 Check out from Version Control 의 git 을 통해 파일을 생성한 후 Push 하였다.
config-dev.yml, config-production.yml
을 생성한다.여기서는 IntelliJ의 Check out from Version Control 의 git 을 통해 파일을 생성한 후 Push 하였다.
Git url 을 지정하고 디렉토리에 Clone 한다. (github에서 생성해도 상관없다)

config-dev.yml, config-production.yml
파일에 각각 mode: "develop"
, mode: "production"
을 입력 한 후 Push 한다.Github 에 적용된 파일이 올라갔다면 이제 이 파일을 읽어들이는 Config Server를 구성해보자.
IntelliJ의 Create New Project 를 통해서 Spring Initializr 를 선택한 후 항목들을 입력한다.
우리가 구성할 것은 Spring Cloud Config 이므로 아래와 같이 Boot 2.x 의 Cloud Config 항목에서 Config Server 번들을 선택한다.
실제 Config server 에 입력해야 할 항목은
@EnableConfigServer
어노테이션을 추가한 후 application.properties 에 아래와 같이 Git Repository 를 입력하는 것으로 설정은 끝이 난다.server.port=8080
spring.cloud.config.server.git.uri=https://github.com/gliderwiki/spring-cloud-config.git
SpringCludeApplication의 전체 소스는 아래와 같다.
package com.example.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class SpringcloudApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudApplication.class, args);
}
}
boot 어플리케이션을 기동한 후 http://localhost:8080/config/dev 혹은 http://localhost:8080/config-dev/default 로 접속하면 Git 의 초기 설정 정보가 아래와 같이 JSON으로 내려오는 것을 확인할 수 있다.
현 화면에서는 8080 포트로 작성하였으나 추후 여러 클라이언트가 붙는 상황을 가정하여 8888 포트로 변경하였다.
위와 같은 구조로 클라리언트 어플리케이션 들이 붙는다고 가정하면 local, dev, stage, production 과 같은 구조의 설정 파일들을 분리 하여 적용할 수 도 있고, 각 도메인 별 설정파일 안에 다양한 환경 profile 들을 구성해서 적용할 수 있다는 것을 예상할 수 있을 것이다.
실제 config yml 파일들을 아래와 같이 변경한 후 push 하고 재 접속해보도록 하자.
config-dev.yml
service:
mode: "develop"
params:
args1 : Hello
args2 : World
config-production.yml
service:
mode: "production"
params:
args1 : Real
args2 : Service
간단한 Config server 구성만으로 적용된 내용이 갱신되는 것을 확인할 수 있다.
Git 에 저장한 설정파일은 yml이나 properties 둘 다 적용이 가능하다.
Git 에 저장한 설정파일은 yml이나 properties 둘 다 적용이 가능하다.
이제 Client 레벨에서 Config Server 에 접근하는 두개의 어플리케이션을 통해 하나는 개발용 Config를, 나머지 하나의 서비스는 리얼 Config의 설정된 내용들을 적용해보도록 한다.
초기 세팅은 http://start.spring.io/ 의 Spring Initializr를 사용하여 프로젝트를 제너레이션 해도 무방하다. 여기서는 Config Client 와 Web 를 선택한다.
IntelliJ에서는 File > New > Project 를 이용해 신규 클라이언트 프로젝트를 설정한다. 마찬가지로 Spring initializr 를 선택 한 후 다음을 클릭한다.
member-service 로 Artifact를 지정하고 다음을 클릭하고 마찬가지로 Config Client 와 Web 번들을 선택한다.

member-service 프로젝트는 Config Server 정보를 바라봐야 하기 때문에 아래와 같이 bootstrap.yml 에 Config Server 정보를 연결해준다.
server:
port: 8081
spring:
application:
name: member-service
cloud:
config:
url: http://localhost:8888
name: config
8888 포트의 Config 서버 정보를 url 로 기술하고 현재 프로젝트의 명, 접근 하는 서버의 명 정도를 기술한 후 8081 포트로 어플리케이션을 실행할 것이다.
속성값을 정의 할 때 application.properties 보다 bootstrap의 설정값을 먼저 읽어온다.
이는 Config Server의 정보와 이름 정도만 기술하면 어플리케이션의 구동시 cloud name 을 통해 HTTP 로 호출을 해서 구성을 검색하게 된다.
따라서 구동시에 application.properties 보다 먼저 읽어와야 할 속성이 있을 경우 bootstrap 에 설정하도록 한다.

속성값을 정의 할 때 application.properties 보다 bootstrap의 설정값을 먼저 읽어온다.
이는 Config Server의 정보와 이름 정도만 기술하면 어플리케이션의 구동시 cloud name 을 통해 HTTP 로 호출을 해서 구성을 검색하게 된다.
따라서 구동시에 application.properties 보다 먼저 읽어와야 할 속성이 있을 경우 bootstrap 에 설정하도록 한다.
마지막으로 Config 가 읽어오는 설정 파일중에 dev 설정을 읽어와야 하므로 VM Option으로
-Dspring.profiles.active={profile}
을 추가 해준다. 여기서는 config-dev.yml을 읽을 예정이므로 dev로 설정하였다.필요에 따라 config 구성 파일 내에 local, dev, stage, production 을 구분 지어 놓아도 무방하다.
이제 어플리케이션을 실행해보자.
아래와 같이
아래와 같이
Fetching config from server at: http://localhost:8888
으로 Config Server에 연결한 것을 확인할 수 있으며 profile 정보는 dev로 active 된것을 확인할 수 있다.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.2.RELEASE)
2018-06-01 10:08:22.687 INFO 54538 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
2018-06-01 10:08:23.424 INFO 54538 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=member-service, profiles=[dev], label=null, version=21a6b130c5b4f2f2546ee776656d7c394b60a653, state=null
2018-06-01 10:08:23.424 INFO 54538 --- [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}]}
2018-06-01 10:08:23.427 INFO 54538 --- [ main] c.e.s.m.MemberServiceApplication : The following profiles are active: dev
2018-06-01 10:08:23.435 INFO 54538 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2609b277: startup date [Fri Jun 01 10:08:23 KST 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@3754a4bf
2018-06-01 10:08:23.705 INFO 54538 --- [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=ddc33e78-033f-3c04-8011-781be6cad530
2018-06-01 10:08:23.737 INFO 54538 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$278e6290] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2018-06-01 10:08:23.876 INFO 54538 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2018-06-01 10:08:23.891 INFO 54538 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-06-01 10:08:23.891 INFO 54538 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.31
2018-06-01 10:08:23.895 INFO 54538 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/Users/kakao/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.]
2018-06-01 10:08:23.966 INFO 54538 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-06-01 10:08:23.966 INFO 54538 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 531 ms
2018-06-01 10:08:24.052 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-01 10:08:24.054 INFO 54538 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-01 10:08:24.116 INFO 54538 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.244 INFO 54538 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@2609b277: startup date [Fri Jun 01 10:08:23 KST 2018]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@3754a4bf
2018-06-01 10:08:24.279 INFO 54538 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-01 10:08:24.279 INFO 54538 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-01 10:08:24.295 INFO 54538 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.295 INFO 54538 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-01 10:08:24.423 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-06-01 10:08:24.429 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'configurationPropertiesRebinder' has been autodetected for JMX exposure
2018-06-01 10:08:24.429 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'environmentManager' has been autodetected for JMX exposure
2018-06-01 10:08:24.430 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'refreshScope' has been autodetected for JMX exposure
2018-06-01 10:08:24.432 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located managed bean 'environmentManager': registering with JMX server as MBean [org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager]
2018-06-01 10:08:24.439 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located managed bean 'refreshScope': registering with JMX server as MBean [org.springframework.cloud.context.scope.refresh:name=refreshScope,type=RefreshScope]
2018-06-01 10:08:24.445 INFO 54538 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located managed bean 'configurationPropertiesRebinder': registering with JMX server as MBean [org.springframework.cloud.context.properties:name=configurationPropertiesRebinder,context=2609b277,type=ConfigurationPropertiesRebinder]
2018-06-01 10:08:24.487 INFO 54538 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2018-06-01 10:08:24.490 INFO 54538 --- [ main] c.e.s.m.MemberServiceApplication : Started MemberServiceApplication in 2.589 seconds (JVM running for 3.514)
이제 member-service 프로젝트에서 설정 정보를 정상적으로 읽어오는지 Controller를 작성해서 확인해보자.
package com.example.springcloud.memberservice;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MemberController {
@Value("${service.params.args1}")
private String arguments1;
@Value("${service.params.args2}")
private String arguments2;
@GetMapping("/member")
public String member() {
return arguments1 + arguments2;
}
}
Git 에 올려놓은 설정파일중에 config-dev.yml 내에는 아래와 같은 정보들을 구성해놨기 때문에 이를 읽어와서 화면에 뿌려주는 간단한 어플리케이션이다.
service:
mode: "develop"
params:
args1 : Hello
args2 : World
어플리케이션을 재구동하고 localhost:8081/member 에 들어가면 원격지 git 의 설정중에 config-dev.yml을 ConfigServer가 읽어와서 클라이언트에 연결된 상태임을 알 수 있다.
하지만 여기서 중요한 것은 @Value 로 주입한 정보는 Static 한 값이므로 Git 설정 값을 변경해서 Push 한다고 해도 변경 사항이 바로 반영되지는 않는다. (왜??? 인지는 이후에 설명한다.)
이 부분은 다른 방식으로 처리해야 하는데 (profile 옵션을 여러개를 운용할 경우 profile parameter 값은 제대로 물고 오는 것을 확인할 수 있다.) 바로
그래도 바로 반영되는 것은 아니고, Spring boot 2.x 부터는 Spring boot actuator 를 이용해 POST 방식으로 end point 를 호출을 해주어야 적용이 된다. (여기서는 http://localhost:8081/actuator/refresh 를 curl이나 postman 등의 rest client 툴을 활용해 post 방식으로 호출해주어야 적용이 된다)
@RefreshScope
어노테이션을 해당 클라이언트에 추가해주어야 한다.그래도 바로 반영되는 것은 아니고, Spring boot 2.x 부터는 Spring boot actuator 를 이용해 POST 방식으로 end point 를 호출을 해주어야 적용이 된다. (여기서는 http://localhost:8081/actuator/refresh 를 curl이나 postman 등의 rest client 툴을 활용해 post 방식으로 호출해주어야 적용이 된다)
Spring boot 1.x 에서는 /refresh url 을 Post 로 호출해주었어야 했다.
아마도 한번 더 확인하고 반영을 할 수 있도록 하나의 단계를 더 마련한것이 아닌가 추측된다.
실제로 공식문서에는 아래와 같이 설명하고 있다.
실제로 공식문서에는 아래와 같이 설명하고 있다.
TheEnvironmentChangedEvent
covers a large class of refresh use cases, as long as you can actually make a change to theEnvironment
and publish the event (those APIs are public and part of core Spring). You can verify the changes are bound to@ConfigurationProperties
beans by visiting the/configprops
endpoint (normal Spring Boot Actuator feature). For instance aDataSource
can have itsmaxPoolSize
changed at runtime (the defaultDataSource
created by Spring Boot is an@ConfigurationProperties
bean) and grow capacity dynamically. Re-binding@ConfigurationProperties
does not cover another large class of use cases, where you need more control over the refresh, and where you need a change to be atomic over the wholeApplicationContext
. To address those concerns we have@RefreshScope
.
이제
위의 과정과 마찬가지로 movie-service 라는 프로젝트를 생성한다. 이번에는 Spring Initializr 선택 시 하나 더 추가해야 하는데, Actuator 번들을 반드시 포함 해야 한다.
bootstrap.yml의 port는 8082로 설정하고 VM option 도
@RefreshScope
어노테이션을 통해 동적으로 설정이 변경되는 작업을 진행해보자.위의 과정과 마찬가지로 movie-service 라는 프로젝트를 생성한다. 이번에는 Spring Initializr 선택 시 하나 더 추가해야 하는데, Actuator 번들을 반드시 포함 해야 한다.
bootstrap.yml의 port는 8082로 설정하고 VM option 도
-Dspring.profiles.active=production
으로 변경 한다.server:
port: 8082
spring:
application:
name: movie-service
cloud:
config:
url: http://localhost:8888
name: config
어플리케이션이 정상적으로 동작하는지 확인한 후 controller 를 아래와 같이 작성해보자.
@RestController
@RefreshScope
public class MovieController {
@Value("${service.params.args1}")
private String arguments1;
@Value("${service.params.args2}")
private String arguments2;
@GetMapping("/movie")
public String movie() {
return arguments1 + arguments2;
}
}
@RefreshScope
어노테이션을 추가해주었다.
http://localhost:8082/movie 로 브라우저에 접근해서 config-production.yml 에 있는 arg 값이 출력되는지 확인한다.
이제 spring-cloud-config 프로젝트에 있는 git yml 의 arg1, 2 값을 적당히 수정해서 푸시 해보자.
service:
mode: "production"
params:
args1 : Real
args2 : Service for KAKAO !!
위의 주소로 다시 접근해도 메세지는 변경 되지 않을 것이다.
터미널 혹은 rest client 를 사용하여 http://localhost:8082/actuator/refresh 를 POST 방식으로 호출한 후 다시 접근해본다.
$ curl -X POST http://localhost:8082/actuator/refresh
혹은 아래와 같이 입력해도 된다.
$ curl localhost:8082/actuator/refresh -d {} -H "Content-Type: application/json"
응답이 제대로 오는지 확인한 후 페이지를 호출하면 서버의 리로드 없이도 설정된 정보들이 출력되는 것을 확인할 수 있다.
이로써, 우리는 어플리케이션 내에서 속성값이 변경되거나 환경 설정값이 변경될때 마다 빌드 후 재 배포의 부담에서 보다 더 자유로워졌다.
관심사의 분리에서 시작된 어플리케이션 내의 성격이 다른 횡적 기능들에 대한 분리와 CoC와 더불어 향후 MSA 지향으로 대량의 트래픽을 처리하기 위해 환경이 비대해지고 중첩되는 것들을 별도로 분리한다거나, Profile 별 설정이나 도메인별 속성 설정등을 어플리케이션 빌드&배포와 상관없이 관리할 수 있다는 점만으로도 충분히 매력적이라고 생각한다.
Spring Cloud Config는 같은 번들의 Eureka나 Zuul 을 연결함으로써 좀 더 다이나믹한 서버 구조를 활용할 수 있다.
= FIN =