안녕하세요
K-인사이트 입니다.
마이크로서비스를 구현하기 위해 여러 서비스들을 배치하다보면 자연히 수많은 설정 정보들을 어떻게 관리하고 적용하는지가 중요한 화두가 될것 입니다. 만약 서비스가 2~3개 정도라면 수작업이 문제가 되지 않겠지만 4~5개 이상이라면 설정을 관리하는데 있어 부하가 많이 발생하고 복잡하게 연결된 메쉬 구조일때 더욱 많은 어려움을 감내해야 할 것입니다. 만약, 여러분이 설정 정보를 변경한다고 하였을 때 중앙에서 설정들을 관리하고 이를 배포 없이 업데이트를 할 수 있다면 어떨까요? application.yaml 파일 혹은 application.properties 변경 정도로 배포 스케줄을 관리해야하는 부담이 줄어들 것입니다. 이 글에서는 Spring Cloud Config 를 통해서 분산 시스템을 위한 설정 서버와 클라이언트를 구성하고 설정 정보가 업데이트되면 이를 반영하는 방법을 자세히 알아보겠습니다.
Spring Cloud Config 란?
분산 시스템에서 외부화된 설정 정보를 서버 및 클라이언트에게 제공하는 시스템입니다. 설정 서버는 외부에서 모든 환경에 대한 정보들을 관리해주는 중앙 서버 역할을 합니다. 설정 정보 저장에 git 을 사용하도록 되어 있어 손쉽게 외부 도구들로 접근이 가능하며 버전관리가 가능합니다.
- Spring Cloud Config Server (설정 서버): 버전 관리 레포지토리로 백업된 중앙 집중식 구성 노출을 지원합니다.
- Spring Cloud Config Client (설정 클라이언트): 애플리케이션이 설정 서버에 연결하도록 지원합니다.
Spring Cloud Config 장점과 단점
중앙화된 설정 파일 관리는 장점도 있지만 단점도 있습니다. 중앙화된 관리 자체가 편리한 점이기도 하면서 장애가 발생할 때 단일 실패점(SPOF)로 작용할 수 있습니다. 장점과 단점을 나열해서 알아보겠습니다.
장점
- 여러 서비스의 설정 파일을 중앙 서버에서 관리가 가능
- 서버를 재배포 하지 않고 설정 파일의 변경사항을 반영할 수 있음.
단점
- Git 서버 또는 설정 서버에 의한 장애가 전파될 우려
- 우선 순위에 의해 설정 정보가 Overwrite 될 수 있음
서비스가 실행 중이라면 설정 서버에 장애가 발생하더라도 문제가 없으나 서비스가 시작될 때 GIT 또는 설정 서버에 문제가 있다면 서비스들까지 문제가 전파될 수 있습니다.
설정 파일 우선 순위의 이해
스프링 프로젝트에는 여러 계층에서 설정 파일이 관리될 수 있습니다. 프로젝트 내에 설정 파일이 위치하거나 설정 저장소에 위치하는 경우가 있으며 설정 파일 중에도 특정 프로파일에 적용되는 설정 파일로 나누어집니다. 따라서, 어떤 우선순위로 이 설정 파일들이 적용되고 중복된다면 어떻게 처리되는지 이해할 필요가 있습니다. 설정 파일은 크게 다음의 위치에 놓이고 위에서 아래의 순서로 로드됩니다. 만약 중복된 설정이 있다면 가장 마지막에 읽어지는 것이 우선 순위가 높습니다.
- 프로젝트, application.yaml
- 설정 저장소, application.yaml
- 프로젝트, application-{profile}.yaml
- 설정 저장소, {application name}/{application name}-{profile}
따라서, 동일한 값을 처리하는 설정이 있다면 덮어씌워지므로 주의가 필요합니다. 예를 들어 프로젝트 application.yaml 파일과 설정 저장소 application.yaml 파일에 동일한 설정이 작성되어 있다면 나중에 읽혀지는 설정 저장소의 application.yaml 파일의 내용으로 덮어씌워지게됩니다.
Spring Cloud Config 서비스 구현
설정 파일 저장소를 구축하고 설정 서버를 구현하겠습니다. 그리고 설정 서버를 바라보는 클라이언트 서버를 배치한 뒤에 이 설정 정보를 반영하고 갱신하는 전체 과정을 구현해보도록 하겠습니다. 관련된 예제는 Bitbucket 을 통해 제공하고 있으니 이 글의 맨 마지막에 있는 링크를 통해 전체 코드를 다운로드할 수 있습니다.
설정 파일 저장소 구축
git 레포지토리를 생성하고 설정 파일을 생성합니다. 이때, {app_name}-{profile}.yaml 구조로 작성합니다. 만약 앱이름이 kinsight 이고 프로파일이 tistory 이라면 설정 파일이름은 kinsight-tistory.yaml 이 됩니다. 폴더의 경로는 중요하지 않습니다. 파일을 체계적으로 관리하기 위함이고 Config Server 에서는 설정 파일의 이름을 중요하게 여깁니다.
아래와 같이 폴더 구조를 만듭니다. kin 은 앱이름이고 kr1-alpha 는 프로파일입니다.
파일의 컨텐츠에는 아래와 같이 설정 파일을 생성합니다.
# kin-kr1-alpha.yaml
com:
kin:
profile: alpha
region: kr1
Config Server 구축
새 프로젝트를 생성합니다. IDE는 IntelliJ를 기준으로 합니다. 아래와 같이 언어는 코틀린, 타입은 Gradle-Kotlin 을 선택합니다.
build.gradle.kts 파일은 아래와 같이 설정을 추가합니다. 핵심이 되는 내용은 extra 와 dependencyManagement 설정입니다. 의존성(dependencies)에 spring-cloud-config-server 가 추가되어 있어야 합니다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.4"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.23"
kotlin("plugin.spring") version "1.9.23"
}
group = "com.kin"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
extra["springCloudVersion"] = "2023.0.1"
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.cloud:spring-cloud-config-server")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
다음으로 src/main/resources 폴더의 application.properties 파일에 아래의 설정을 추가합니다.
spring.application.name=config-server
server.port=9999
spring.cloud.config.server.git.uri=https://k-in@bitbucket.org/kinstory/spring-cloud-config.git
spring.cloud.config.server.git.search-paths=config-sample/**
spring.cloud.config.server.git.default-label=main
위 설정의 내용은 아래와 같습니다. 여러분의 상황에 맞는 값으로 변경해도 무방합니다.
- spring.cloud.config.server.git.uri 설정은 config 들이 저장되는 git 레포지토리입니다.
- spring.cloud.config.server.git.search-paths 설정을 통해 config 파일이 있는 경로와 패턴을 입력합니다.
- spring.cloud.config.server.git.default-label 설정은 브랜치의 이름입니다.
다음으로 Config 서버를 구동하기 위해서 EnableConfigServer 어노테이션을 설정만 하면 완료됩니다. 아래의 코드를 참조합니다.
package com.kin.configserver
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cloud.config.server.EnableConfigServer
@SpringBootApplication
@EnableConfigServer
class ConfigServerApplication
fun main(args: Array<String>) {
runApplication<ConfigServerApplication>(*args)
}
Config Server 엔드포인트
앱을 실행하게되면 Config Server 는 GIT 으로부터 설정 정보를 불러옵니다. 그리고 설정 파일에서 라벨(label), 앱 이름(application), 프로파일(profile) 정보를 토대로 설정 정보를 출력하는 URL 경로가 생성됩니다. kin-kr1-alpha.yaml 설정 파일을 예로 URL 경로가 어떻게 생성되는지 보겠습니다.
- /{application}/{profile}[/{label}] → /kin/kr1-alpha[/main]
- /{application}-{profile}.yml → /kin-kr1-alpha.yml
- /{label}/{application}-{profile}.yml → /main/kin-kr1-alpha.yml
- /{application}-{profile}.properties → /kin-kr1-alpha.properties
- /{label}/{application}-{profile}.properties → /main/kin-kr1-alpha.properties
Bitbucket 레포지토리 예시를 기준으로 Config 서버가 제공하는 설정 URL 정보를 정리하면 아래와 같습니다. 글 맨 하단의 레포지토리를 따라가 다운로드 한 뒤에 실행하여 아래의 경로들로 접근하면 됩니다.
- http://localhost:9999/kin/kr1-alpha
- http://localhost:9999/kin/kr1-alpha/main
- http://localhost:9999/kin-kr1-alpha.yml
- http://localhost:9999/main/kin-kr1-alpha.yml
- http://localhost:9999/kin-kr1-alpha.properties
- http://localhost:9999/main/kin-kr1-alpha.properties
여기서 재밌는 점은 yaml, properties 확장자에 맞추어 포맷을 변경해준다는 점입니다. 즉, yaml 형식으로 설정(config)를 저장소 저장해두었다고 하더라도 properties 포맷으로 제공을 받을 수 있습니다.
Config Client 구축
Config 서버를 구축하였으므로 이제 Client 서버를 구축해보겠습니다. 아래 처럼 프로젝트를 생성합니다.
의존성 설정에는 Config Client 를 추가합니다. 최종적인 build.gralde.kts 파일은 아래와 같습니다. dependencyManagement 와 extra 설정이 필수입니다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.4"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.23"
kotlin("plugin.spring") version "1.9.23"
}
group = "com.kin"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
extra["springCloudVersion"] = "2023.0.1"
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-config")
implementation("org.springframework.boot:spring-boot-starter-actuator")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
ClientConfig 클래스를 생성하고 이를 설정 정보를 보여주기 위한 컨트롤러 클래스를 작성하였습니다.
% tree .
.
├── main
│ ├── kotlin
│ │ └── com
│ │ └── kin
│ │ └── configclient
│ │ ├── ClientConfig.kt
│ │ ├── ClientController.kt
│ │ └── ConfigClientApplication.kt
│ └── resources
│ └── application.properties
설정 정보를 바인딩할 클래스
yaml 파일을 읽어오기 위해 ClientConfig 클래스를 생성합니다. @RefreshScope 어노테이션은 설정 정보가 변경되면 다시 불러올 수 있도록 합니다. 단, Config Client 의 /actuator/refresh 엔드포인트를 호출하도록 하며 @ConfigurationProperties 어노테이션은 외부의 구성 속성 값을 매핑하는 데 사용됩니다. 즉, 설정값을 자바 메모리로 가져오는 역할을하게됩니다. 해당 어노테이션을 사용하게되면 @EnableConfigurationProperties를 설정해야 합니다. 이는 어플리케이션 클래스에 설정합니다. ClientConfig 클래스는 Bean에 등록하여 컨트롤러에서 사용하므로 @Configuration 어노테이션을 추가합니다.
package com.kin.configclient
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.cloud.context.config.annotation.RefreshScope
@Configuration
@ConfigurationProperties("com.kin")
@RefreshScope
class ClientConfig {
@Value("\\${com.kin.profile}")
private val profile: String? = null
@Value("\\${com.kin.region}")
private val region: String? = null
override fun toString(): String {
return "ClientConfig(profile=$profile, region=$region)"
}
}
다음으로 어플리케이션 속성(application.properties)에 Config Server의 주소를 설정합니다. 이때 미리 등록해놓은 kin-kr1-alpha.yaml 파일의 어플리케이션 이름과 프로파일 설정의 이름이 동일해야 합니다. 만약 그렇지 않다면 Config Client 서버는 앱 실행 시 에러가 발생합니다. optional 설정은 만약 Config Server 와 통신에 실패했을 때 앱이 종료되지 않고 실행되도록 합니다. 하지만 이 예시에서 ClientConfig 를 Autowired 로 설정하므로 앱은 에러가 발생하면서 종료됩니다.
spring.application.name=kin
spring.profiles.active=kr1-alpha
spring.config.import=optional:configserver:<http://localhost:9999>
management.endpoints.web.exposure.include=refresh
설정 정보 출력을 위한 컨트롤러
설정 정보가 제대로 반영되었는지 확인을 위한 컨트롤러를 생성합니다. RestController 로 구성하며 아래와 같이 간단한 코드를 작성합니다. 이를 통해 Config Client 서버가 실행되면 localhost:8080/config 주소를 통해서 설정 정보의 변경을 확인할 수 있습니다.
@RestController
class ConfigController {
@Qualifier("clientConfig")
@Autowired
private lateinit var myConfig: ClientConfig
@GetMapping("/config")
fun config(): ResponseEntity<String> {
println(myConfig)
return ResponseEntity.ok(myConfig.toString())
}
}
마지막으로 어플리케이션 클래스에 약간의 변경이 필요합니다. 앞서, @ConfigurationProperties 설정을 하였으므로 @EnableConfigurationProperties(ClientConfig::class) 어노테이션 설정이 추가되어야 합니다.
package com.kin.configclient
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling
@EnableConfigurationProperties(ClientConfig::class)
@EnableScheduling
@SpringBootApplication
class ConfigClientApplication
fun main(args: Array<String>) {
runApplication<ConfigClientApplication>(*args)
}
Config Client 동작확인
Config Client와 Server를 모두 실행합니다 아래와 같이 http://localhost:8080/config 경로로 접근하면 Config Server로 부터 받아온 정보를 출력합니다.
kin-kr1-alpha.yaml 파일에 debug 정보를 추가해줍니다.
ClientConfig 설정에도 debug 정보를 반영하도록 업데이트합니다.
@Configuration
@ConfigurationProperties("com.kin")
@RefreshScope
class ClientConfig {
@Value("\\${com.kin.profile}")
private val profile: String? = null
@Value("\\${com.kin.region}")
private val region: String? = null
@Value("\\${com.kin.debug}")
private val debug: Boolean? = null
override fun toString(): String {
return "ClientConfig(profile=$profile, region=$region, debug=$debug)"
}
}
이제, debug 설정을 업데이트하고 앱이 이를 중단없이 갱신하는지 확인해보겠습니다. git 을 통해서 커밋 후에 actuator/refresh 엔드포인트를 호출하고 Config Client 가 설정을 업데이트하는지 확인하는 방식으로 체크합니다.
debug 설정을 false 로 변경합니다. 변경된 내용을 Git 에 push 합니다.
다음으로 refresh 액츄에이터를 호출하여 갱신을 하도록 합니다. 아래의 요청을 Config Client 에게 호출하면 변경된 debug 값이 변경되었음을 알 수 있습니다.
> % curl -H "Content-Type: application/json" -d {} <http://localhost:8080/actuator/refresh>
["config.client.version","com.kin.debug"]%
또한, 호출 시 Config Client 서버의 로그에 Config Server 로부터 설정 정보를 가져온다는 로그가 확인됩니다.
정상적으로 가져오게되면 아래와 같이 debug 가 false로 업데이트 됩니다. 이는 앱을 재시작하지 않고 변경된 설정을 적용하였음을 말합니다.
샘플 프로젝트 다운로드
이 글에서 다룬 내용을 직접 다운로드하여 실습할 수 있도록 코드를 Bitbucket을 통해 공유한 상태입니다. 아래의 링크에 접속하여서 Clone 버튼을 누르면 실습 내용을 그대로 재현하여 테스트할 수 있습니다.
https://bitbucket.org/kinstory/spring-cloud-config/src/main/
맺음말
지금까지 MSA 아키텍처를 위한 필수 과정인 Spring Cloud Config Server/Client 프로젝트를 구성하는 방법에 대해 알아보았습니다. 최대한 여러분이 헤매지 않도록 필수적인 내용을 검증하여 설명드렸습니다. 관련해서 궁금한 내용이 있다면 댓글을 통해서 소통해주시면 감사하겠습니다.
이상입니다.
K-인사이트 올림.
참고
- 스프링 공식 사이트, Spring Cloud Config, https://docs.spring.io/spring-cloud-config/docs/current/reference/html/
이상입니다.
K-인사이트 올림.
'프로그래밍 > 코프링' 카테고리의 다른 글
코프링, 성공적인 서비스를 위한 멀티 모듈 프로젝트 구성 (Feat. 코틀린 Gradle) (55) | 2024.04.05 |
---|---|
코프링, 스프링 부트(Spring Boot) 코틀린으로 배워보자! (68) | 2024.03.20 |
코프링, 스프링 배치(Spring Batch) 코틀린으로 배워보자 (106) | 2024.03.07 |
코프링, 코틀린 데이터 클래스와 FlatFileParseException 해결 (75) | 2024.03.07 |
코프링, 스프링 @Bean 어노테이션(Annotations) (72) | 2024.03.07 |