Spring Cloud Config
100개의 마이크로서비스에 DB 비밀번호를 바꿔야 한다면? 각 서비스마다 설정 파일을 수정하고 재배포하면 수십 분이 걸린다. Spring Cloud Config는 모든 서비스의 설정을 한 곳에서 관리하고, 재배포 없이 런타임에 반영하는 중앙 집중 설정 관리 솔루션이다.
비유: 대기업 인사팀(Config Server)이 회사 규정집(설정)을 관리한다. 각 부서(마이크로서비스)는 자체 규정집을 갖지 않고 인사팀에 물어본다. 규정이 바뀌면 인사팀만 수정하면 되고, 각 부서에 공지(refresh)를 보내면 새 규정이 즉시 적용된다.
중앙 집중 설정 관리의 필요성
기존 방식 문제점:
서비스 A: application.yml → DB 비밀번호 변경 → 재빌드 → 재배포
서비스 B: application.yml → DB 비밀번호 변경 → 재빌드 → 재배포
서비스 C: application.yml → DB 비밀번호 변경 → 재빌드 → 재배포
...100개 서비스 반복
Spring Cloud Config:
Config Server (Git) → 비밀번호 변경 → 클라이언트에 refresh 신호 → 즉시 반영
→ 재빌드/재배포 없음
graph TD
subgraph "설정 저장소"
GIT[Git Repository\n/config-repo]
end
subgraph "Config Server"
CS[Spring Cloud Config Server\n:8888]
end
subgraph "마이크로서비스들"
OS[Order Service]
US[User Service]
PS[Product Service]
end
GIT -->|설정 파일 읽기| CS
CS -->|설정 제공| OS
CS -->|설정 제공| US
CS -->|설정 제공| PS
Config Server 구성
의존성
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
메인 클래스
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
application.yml (Git 기반)
server:
port: 8888
spring:
application:
name: config-server
cloud:
config:
server:
git:
# 설정 파일이 저장된 Git 저장소
uri: https://github.com/your-org/config-repo
# 기본 브랜치
default-label: main
# 로컬 클론 경로
basedir: /tmp/config-repo
# 검색 경로 (하위 디렉토리 사용 시)
search-paths: '{application}'
# private repo 인증
username: ${GIT_USERNAME}
password: ${GIT_TOKEN}
# 강제 pull (로컬 변경 무시)
force-pull: true
# 클론 실패 시 로컬 캐시 사용
clone-on-start: true
설정 파일 구조 (Git 저장소)
Config Server는 파일명 패턴으로 설정을 분류한다.
config-repo/
├── application.yml # 모든 서비스 공통 설정
├── application-prod.yml # 모든 서비스 prod 환경 공통
├── order-service.yml # order-service 전용 (모든 환경)
├── order-service-dev.yml # order-service dev 환경
├── order-service-prod.yml # order-service prod 환경
├── user-service.yml
└── user-service-prod.yml
URL 패턴
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
예시:
GET http://localhost:8888/order-service/prod
GET http://localhost:8888/order-service/prod/main
GET http://localhost:8888/order-service-prod.yml
설정 우선순위 (높음 → 낮음)
1. {application}-{profile}.yml (order-service-prod.yml)
2. {application}.yml (order-service.yml)
3. application-{profile}.yml (application-prod.yml)
4. application.yml (전체 공통)
Config Client 구성
의존성
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
bootstrap.yml (스프링 부트 2.x)
# bootstrap.yml: application.yml보다 먼저 로드됨
# Config Server에서 나머지 설정을 가져올 위치 지정
spring:
application:
name: order-service # Config Server에서 찾을 파일명
cloud:
config:
uri: http://localhost:8888
profile: prod # {application}-{profile}.yml 매핑
label: main # Git 브랜치/태그
fail-fast: true # Config Server 연결 실패 시 즉시 종료
retry:
max-attempts: 6
initial-interval: 1000
multiplier: 1.1
max-interval: 2000
application.yml (스프링 부트 3.x)
# 스프링 부트 3.x에서는 bootstrap.yml 대신 application.yml에 작성
spring:
application:
name: order-service
config:
import: "configserver:http://localhost:8888"
cloud:
config:
profile: prod
런타임 설정 갱신 (@RefreshScope)
@RefreshScope 사용
설정값을 런타임에 갱신하려면 해당 Bean에 @RefreshScope를 붙인다.
@RestController
@RefreshScope // POST /actuator/refresh 호출 시 이 Bean이 재생성됨
public class OrderController {
@Value("${order.max-items:10}")
private int maxItems;
@Value("${order.discount-rate:0.0}")
private double discountRate;
@GetMapping("/config")
public Map<String, Object> getConfig() {
return Map.of(
"maxItems", maxItems,
"discountRate", discountRate
);
}
}
// @ConfigurationProperties도 @RefreshScope 적용 가능
@Component
@RefreshScope
@ConfigurationProperties(prefix = "order")
public class OrderProperties {
private int maxItems = 10;
private double discountRate = 0.0;
// getter/setter
}
수동 refresh
# Git에 설정 변경 후 커밋
git commit -m "change order.max-items to 20"
git push
# 특정 서비스 인스턴스에 refresh 요청
curl -X POST http://order-service:8080/actuator/refresh
actuator 설정
management:
endpoints:
web:
exposure:
include: refresh, bus-refresh, health, info
Spring Cloud Bus (자동 전파)
개별 서비스 인스턴스마다 POST /actuator/refresh를 호출하면 인스턴스가 100개일 때 100번 호출해야 한다. Spring Cloud Bus는 메시지 브로커(RabbitMQ 또는 Kafka)를 통해 전체에 전파한다.
의존성
<!-- RabbitMQ 기반 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!-- Kafka 기반 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
sequenceDiagram
participant DEV as 개발자
participant GIT as Git
participant CS as Config Server
participant MQ as RabbitMQ/Kafka
participant OS1 as Order Service #1
participant OS2 as Order Service #2
participant US as User Service
DEV->>GIT: 설정 파일 변경 & push
DEV->>CS: POST /actuator/bus-refresh
CS->>MQ: RefreshRemoteApplicationEvent 발행
MQ->>OS1: 이벤트 수신
MQ->>OS2: 이벤트 수신
MQ->>US: 이벤트 수신
OS1->>CS: 새 설정 fetch
OS2->>CS: 새 설정 fetch
US->>CS: 새 설정 fetch
Note over OS1,US: 재배포 없이 새 설정 적용
Bus 설정
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
cloud:
bus:
enabled: true
refresh:
enabled: true
management:
endpoints:
web:
exposure:
include: bus-refresh
Webhook 자동화
GitHub Webhook 설정:
URL: http://config-server:8888/monitor
Content-Type: application/json
Events: Push events
→ Git push 시 자동으로 Config Server에 알림
→ Config Server가 Bus에 refresh 이벤트 발행
→ 모든 클라이언트에 자동 전파
설정 암호화
민감한 정보(DB 비밀번호, API 키)는 평문으로 Git에 저장하면 안 된다. Config Server는 대칭키/비대칭키 암호화를 지원한다.
대칭키 암호화
# Config Server application.yml
encrypt:
key: my-secret-encryption-key-32chars
# 값 암호화
curl -X POST http://localhost:8888/encrypt -d "my-db-password"
# 결과: AQBxxx...암호화된문자열
# 값 복호화
curl -X POST http://localhost:8888/decrypt -d "AQBxxx..."
설정 파일에서 암호화값 사용
# config-repo/order-service-prod.yml
spring:
datasource:
# {cipher} 접두사로 암호화된 값 표시
password: '{cipher}AQBxxx...암호화된문자열'
url: jdbc:mysql://prod-db:3306/orders
username: order_user
비대칭키 암호화 (RSA)
# 키쌍 생성
keytool -genkeypair -alias config-server-key \
-keyalg RSA -keysize 4096 \
-sigalg SHA256withRSA \
-dname "CN=Config Server,OU=IT" \
-keypass mypassword \
-keystore server.jks \
-storepass mypassword
# application.yml
encrypt:
keyStore:
location: classpath:server.jks
password: mypassword
alias: config-server-key
secret: mypassword
다양한 백엔드
Git 외에도 다양한 설정 저장소를 지원한다.
로컬 파일시스템 (개발 환경)
spring:
cloud:
config:
server:
native:
search-locations:
- file:///opt/config
- classpath:/config
profiles:
active: native
Vault (HashiCorp)
spring:
cloud:
config:
server:
vault:
host: localhost
port: 8200
scheme: http
backend: secret
default-key: application
AWS Parameter Store
spring:
cloud:
config:
server:
awsParameterStore:
region: ap-northeast-2
prefix: /config
극한 시나리오
시나리오 1: Config Server 장애
문제: Config Server 다운 → 클라이언트 재시작 불가
→ spring.cloud.config.fail-fast=true 설정 시 기동 중단
대응 전략:
1. Config Server 다중화 (로드밸런서 뒤에 2개 이상)
2. 클라이언트 retry 설정
3. 최후 수단: spring.cloud.config.fail-fast=false + 로컬 application.yml 폴백
retry 설정:
spring:
cloud:
config:
fail-fast: true
retry:
max-attempts: 10
initial-interval: 1000
multiplier: 1.5
max-interval: 5000
시나리오 2: 설정 변경 롤백
# Git 기반이라 롤백이 간단
git revert HEAD
git push
# 또는 특정 커밋으로 복구
git reset --hard {commit-hash}
git push --force
# Bus refresh로 전파
curl -X POST http://config-server:8888/actuator/bus-refresh
시나리오 3: 환경별 설정 오염 방지
# config-repo 구조 권장 패턴
config-repo/
├── shared/
│ └── application.yml # 진짜 공통 설정만
├── services/
│ ├── order-service/
│ │ ├── application.yml # dev 기본값
│ │ ├── application-staging.yml
│ │ └── application-prod.yml
│ └── user-service/
│ └── ...
└── secrets/
└── application-prod.yml # 암호화된 민감 정보만
# prod 설정에 실수로 dev DB 연결 방지:
# → 환경별 디렉토리 분리 + PR 리뷰 필수
시나리오 4: @RefreshScope 사용 불가 영역
// @RefreshScope는 Spring Bean에만 적용됨
// 다음은 refresh 불가:
// 1. DataSource (커넥션 풀 재생성 문제)
// 2. @Scheduled (초기화 시점에 값 고정)
// 3. static 필드
// 해결책: 런타임에 동적으로 값을 읽는 구조
@Service
public class OrderService {
// 나쁜 예: 시작 시점에 주입되어 refresh 안됨
// @Value("${order.fee-rate}")
// private double feeRate;
// 좋은 예: 매번 Environment에서 읽음
@Autowired
private Environment environment;
public double getFeeRate() {
return Double.parseDouble(
environment.getProperty("order.fee-rate", "0.03")
);
}
}
Config Server 보안
Config Server 자체는 민감한 정보를 제공하므로 반드시 보안을 적용해야 한다.
# Config Server에 Basic Auth 적용
spring:
security:
user:
name: config-admin
password: ${CONFIG_SERVER_PASSWORD}
# 클라이언트 설정
spring:
cloud:
config:
uri: http://localhost:8888
username: config-admin
password: ${CONFIG_SERVER_PASSWORD}
추가 보안 권장사항:
1. Config Server는 내부 네트워크에만 노출 (인터넷 직접 접근 차단)
2. mTLS로 클라이언트 인증
3. Git 토큰 최소 권한 (read-only)
4. 감사 로그: 누가 언제 어떤 설정을 변경했는지 Git history로 추적
5. 암호화되지 않은 민감 정보는 절대 Git에 커밋 금지