[Spring Cloud] - 3. API 서버로 설정 값 불러오기

지난 포스트에서는 Configuration Server를 설정하는 두 가지 방법을 알아봤습니다. 첫 번째 방법은 로컬의 저장되어 있는 설정 파일을 가져오는 방법이었고, 두 번쨰 방법은 Github 등의 레포지터리에 있는 파일을 가져오는 방법이었습니다.

우리는 이러한 서버가 제대로 구축되어 있는지 확인하기 위해서 REST API를 직접 호출하여 해당 값을 반환하는 방법으로 테스트를 했었는데요. 하지만 이러한 결과를 보고, 어떻게 API 서버가 이러한 값을 받는지에 대해서는 아마 궁금해 하셨을 것이라 생각합니다.

그래서 이번 포스트에서는 직접 비즈니스 로직을 만들고, 해당 서버에 맞는 설정값을 만들어, 가져오는 방법을 알아보도록 하겠습니다.

먼저 API 서버를 만들어보도록 하죠.

 

새로운 Spring Boot 프로젝트 생성

우리는 지난 시간에 사용한 Member, Note 이렇게 두 가지 서비스를 그대로 사용하여 프로젝트를 만들어보도록 하겠습니다. Micro Service Architecture로 설계할 것이기 때문에 두 개의 프로젝트를 만들면 되겠습니다.

저는 의존성 매니저로 Gradle을 주로 사용합니다. 따라서 Type을 Gradle로 맞춰주었고, 이번 포스트에서는 Java가 아닌 Kotlin을 사용하여 API 서버를 만들어보도록 하겠습니다.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.2.2.RELEASE"
    id("io.spring.dependency-management") version "1.0.8.RELEASE"
    kotlin("jvm") version "1.3.61"
    kotlin("plugin.spring") version "1.3.61"
}

group = "xyz.neonkid"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {
    mavenCentral()
}

extra["springCloudVersion"] = "Hoxton.SR1"

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-rest")
    implementation("org.springframework.cloud:spring-cloud-starter-config")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}

Member API, Note API 모두 동일한 Gradle 파일로 만들어줍니다.

 

Member API

프로젝트를 만들었으니 이제 서버 코드를 작성해보도록 하죠. 포스트에서는 정확한 로직을 구현하기 보다는 Configuration Server의 정보를 가져오는지 파악하기 위해, Configuration Server의 정보를 가져오는 것을 로직을 구현해보도록 하겠습니다.

package xyz.neonkid.cloudmember

import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
 * Created by neonkid on 12/29/19
 */

@RestController
@RequestMapping(value = ["v1"])
@RefreshScope
class MemberController {
    @Value(value = "\${server.port}") lateinit var port: String
    @Value(value = "\${spring.message}") lateinit var message: String

    @GetMapping(value = ["/member/detail"])
    fun message() = "Member API Info - Port $port - $message"
}

간단히 현재 서버의 포트번호와 그리고, Configuration Server에서 각 설정별로 적혀있는 메시지를 출력하는 것으로 만들어봤습니다.

더보기

@RefreshScope

어노테이션은 Spring Cloud Config Server에서 설정값을 새로고침 하도록하는 어노테이션입니다. 보통 로컬 환경에서 설정값을 가지게 될 경우, 서버가 시작될 때 한 번 로드된 후, 메모리에 적재되어 동작하기 때문에 서버가 재시작되지 않고서는 변경값이 적용하지 않지만, 이 어노테이션은 서버에서 설정값이 바뀌면 중단없이 다시 로드할 수 있도록 하는 어노테이션이라고 보면 됩니다.

 

# bootstrap.yml
server:
  port: 8000

spring:
  profiles:
    active: dev
  application:
    name: member-service

management:
  endpoints:
    web:
      exposure:
        include: info, refresh
# bootstrap-dev.yml
spring:
  profiles: dev
  cloud:
    config:
      uri: http://localhost:9000
      fail-fast: true

Configuration Server와 마찬가지로 설정 파일의 이름을 bootstrap-*.yml로 합니다. bootstrap.yml에는 어떤 프로파일의 설정 값을 불러올 것인지 정하면 되고, bootstrap-*은 해당 프로파일의 이름과 함께 Configuration Server의 URI를 적어주면 완성입니다.

그리고, managment.endpoints.web.exposure.include 설정을 주었는데, 이 옵션은 refresh API를 활성화 시킴으로써 서버가 중단되지 않은 상태에서도 Configuration Server의 설정값을 변경할 수 있는 API를 활성화 시켜주는 옵션입니다.

 

Note API

Note API도 Member API와 동일하게 만들어주면 됩니다.

package xyz.neonkid.cloudnote

import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

/**
 * Created by neonkid on 12/29/19
 */

@RestController
@RequestMapping(value = ["v1"])
@RefreshScope
class NoteController {
    @Value(value = "\${server.port}") lateinit var port: String
    @Value(value = "\${spring.message}") lateinit var message: String

    @GetMapping(value = ["note/detail"])
    fun message() = "Note API Info - Port $port - $message"
}

Kotlin 코드를 작성하다 자꾸 세미콜론 누르는 습관이.... ;;

# bootstrap.yml
server:
  port: 8001

spring:
  profiles:
    active: prod
  application:
    name: note-service

management:
  endpoints:
    web:
      exposure:
        include: info, refresh
# bootstrap-prod.yml
spring:
  profiles: prod
  cloud:
    config:
      uri: http://localhost:9000
      fail-fast: true

각 서비스별로 포트 번호를 다르게 주어야겠죠? 그렇지 않으면.... 포트 충돌이 일어나니 주의.

 

Run Server

모든 서버가 만들어졌다면 이제 서버를 실행해보도록 하죠. 먼저 Configuration Server를 실행하고 다음 각 API 서버를 실행하면 되겠습니다.

2019-12-29 12:59:17.992  INFO 21594 --- [nio-9000-exec-1] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: file:///home/neonkid/server-configs/member-service-dev.yml
2019-12-29 12:59:20.609  INFO 21594 --- [nio-9000-exec-2] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: file:///home/neonkid/server-configs/member-service-dev.yml
2019-12-29 13:16:19.169  INFO 21594 --- [nio-9000-exec-4] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: file:///home/neonkid/server-configs/note-service-prod.yml
2019-12-29 13:16:21.786  INFO 21594 --- [nio-9000-exec-5] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: file:///home/neonkid/server-configs/note-service-prod.yml

순서대로 서버를 실행한다면, Configuration Server에서 위와 같은 메시지가 나타날 것입니다. 

$ curl http://localhost:8000/v1/member/detail
$ curl http://localhost:8001/v1/note/detail 

위 API를 각각 부르게 되면, Member API는 개발 서버의 설정 값을, Note API는 배포 서버의 설정 값을 가져오게 됩니다. 

Member API Info - Port 8000 - Loaded MemberService for Development Mode..
Note API Info - Port 8001 - Loaded NoteService for Production Mode..

 

Refresh Config

마지막으로 설정값을 서비스의 중단없이 동기화 하는 것에 대해 알아보겠습니다. 이 과정은 오직 설정 값에 대한 추가 혹은 변경에 대한 것이 적용됩니다. 만약, 개발 서버의 설정 프로파일을 배포 서버의 설정 프로파일로 바꾸는 등의 방법은 허용되지 않으니 이 부분을 읽기 전에 반드시 참고바랍니다.

우리는 여기에서 spring.message 라는 프로퍼티를 사용하였습니다. 따라서 이 값을 변경한 뒤, 각 API 서버별로 /actuator/refresh 메소드를 호출하면, 서비스의 중단없이 바로 설정값을 변경할 수 있습니다.

spring:
    profiles: dev
    message: This configuration is development for member API..

간단한 예시로 Member API의 개발 서버 설정 파일의 메시지를 위와 같이 변경해보겠습니다.

Refresh API를 호출하기 전까지는 변경 사항이 적용되지 않고, 메소드를 부른 뒤에는 변경된 Property 이름을 배열 형태로 출력해주며, 다음 호출시에는 변경된 부분을 반영해주는 모습입니다. 

 

마치며...

Configuration Server 구성 이후, API 서비스에 설정 파일을 동기화 하는 것까지 알아봤습니다. 내용 자체는 별 많은 내용이 있는 것은 아닙니다. 다만 여러가지 사용 방법이 존재하고, 그것을 자신의 서비스에 맞게 개발하는 것이 이 포스트의 목표입니다.

이 글을 작성하면서 마이크로 서비스 아키텍처 형태의 개발은 미리 한 가지 API를 만들어놓고 테스트 해 볼 수 있다는 점이 메리트라고 생각했습니다. 물론 운영 레벨까지 넘어간다면 로드 밸런싱 등 다양한 서버 사이드, 클라이언트 사이드의 깊은 백엔드를 고려해야 하는 머리 아픈 점은 당연히 존재하겠지만 로드 밸런싱이나 리버스 프록시와 같은 경우는 모놀리틱 아키텍처 또한 거쳐야하는 문제이기 때문에 이를 마이크로 서비스 아키텍처만이 가진 단점이라고 볼 수는 없을 것 같네요.

다음 파트에서는 Gateway에 대한 이야기로 만나뵙도록 하겠습니다.

comments powered by Disqus

Tistory Comments 0