bluetape4k-dependencies 1.3.0 활용기 Part 3: 운영에서 티 나는 변화

운영에서 가장 까다로운 문제는 바로 터지는 문제가 아닙니다. 바로 터지는 문제는 차라리 다루기 쉽습니다. 알람이 울리고, stack trace가 나오고, 담당자가 커피를 내려놓고 달려갑니다.
진짜 문제는 겉으로는 평화로운데 안쪽에서 데이터가 어긋나는 경우입니다. 요청은 성공한 것처럼 끝났고, 사용자도 당장은 모릅니다. 그런데 cache flush가 멈췄거나, metric이 엉뚱한 이름으로 나가거나, leader가 바뀌었는데 그 흔적을 찾기 어렵다면 나중에 훨씬 큰 비용으로 돌아옵니다.
dependencies 1.3.0에서 운영자가 바로 체감할 수 있는 변화는 이런 신호 쪽에 있습니다.
exposed 1.11.0: cache 상태를 health로 드러낸다.aws 0.4.0: Ktor/Spring에서 CloudWatch metric과 log 경계를 잡는다.leader 0.4.0: provider/storage 선택과 metric 확인 지점을 더 분명하게 만든다.
새 기능 목록만 다시 읽어서는 운영에서 어디를 봐야 할지 알기 어렵습니다. 여기서는 어떤 값을 보면 좋은지, 어떤 실패를 더 빨리 알아차릴 수 있는지에 초점을 맞춥니다.
Exposed cache: flush가 멈췄는지 health에서 본다
섹션 제목: “Exposed cache: flush가 멈췄는지 health에서 본다”write-behind cache는 성능을 위해 쓰지만, 실패했을 때 위험도도 같이 가져옵니다. DB에 바로 쓰지 않고 뒤에서 flush한다는 것은, 그 뒤쪽 작업이 멈췄을 때 애플리케이션이 한동안 정상처럼 보일 수 있다는 뜻입니다.
1.3.0 라인에서 exposed 쪽 변화 중 중요한 부분은 cache health입니다. 확인하고 싶은 값은 대략 이렇습니다.
{ "status": "OUT_OF_SERVICE", "components": { "exposedCache": { "status": "OUT_OF_SERVICE", "details": { "reports": [ { "name": "invoice-cache", "mode": "WRITE_BEHIND", "queueDepth": 384, "maxQueueDepth": 1000, "flushJobRunning": false, "lastFlushError": "RedisConnectionFailureException", "lastFlushErrorAt": "2026-06-28T03:20:14Z" } ] } } }}여기서 중요한 값은 status 하나가 아닙니다.
| 값 | 왜 봐야 하나 |
|---|---|
mode | write-through인지 write-behind인지에 따라 장애 의미가 달라진다. |
queueDepth | flush가 밀리고 있는지 확인한다. |
flushJobRunning | 뒤에서 실제 flush 작업이 돌고 있는지 확인한다. |
lastFlushError | cache 문제가 DB, Redis, serializer 중 어디에 가까운지 힌트를 준다. |
lastFlushErrorAt | 지금 문제인지, 이미 지나간 문제인지 구분한다. |
이 값이 없으면 운영자는 애플리케이션 로그를 뒤지며 추리를 해야 합니다.
사용자는 방금 저장했다고 말한다 -> API log는 200이다 -> DB에는 없다 -> cache queue는 봤나? -> flush job은 돌고 있나? -> Redis 연결은 살아 있나?health에 cache 상태가 나오면 추리 시간이 줄어듭니다. 장애가 사라지는 것은 아니지만, 적어도 어디서부터 볼지 정할 수 있습니다. 운영에서 이 차이는 큽니다. 삽을 들고 운동장을 전부 파는 일과, 누가 “여기부터 파세요”라고 표시해 둔 일의 차이입니다.
AWS CloudWatch/Logs: 이름을 정하지 않으면 나중에 못 찾는다
섹션 제목: “AWS CloudWatch/Logs: 이름을 정하지 않으면 나중에 못 찾는다”CloudWatch metric과 log는 “보내기만 하면 끝”이 아닙니다. namespace, log group, stream 이름이 엉키면 나중에 검색이 어려워집니다. 특히 여러 환경과 여러 서비스가 같은 AWS 계정에 올라가면 이름 규칙이 운영 문서가 됩니다.
aws 0.4.0에서는 Ktor/Spring에서 CloudWatch와 CloudWatch Logs를 붙이는 경로가 보강되었습니다. 원하는 방향은
애플리케이션 시작 지점에서 이름과 lifecycle을 모으는 것입니다.
의사코드로 보면 이런 형태입니다.
fun Application.module() { install(BluetapeCloudWatchMetrics) { namespace = "bluetape4k/billing" dimensions = mapOf( "service" to "billing-api", "environment" to deployment.environment, ) }
install(BluetapeCloudWatchLogs) { logGroupName = "/bluetape4k/billing-api" logStreamName = "${deployment.environment}/${deployment.instanceId}" }}Spring Boot라면 설정 파일에서 같은 결정을 드러낼 수 있습니다.
bluetape4k: aws: cloudwatch: namespace: bluetape4k/billing dimensions: service: billing-worker environment: prod cloudwatch-logs: log-group-name: /bluetape4k/billing-worker log-stream-name: prod/${HOSTNAME}예시 값은 단순하지만, 운영에서는 이런 이름이 중요합니다.
metric namespace: bluetape4k/billingdimension.service: billing-workerdimension.environment: prodlog group: /bluetape4k/billing-workerlog stream: prod/ip-10-0-12-34이름을 정하지 않으면 알람을 만들 때도, dashboard를 만들 때도, 장애 후 log를 찾을 때도 계속 비용을 냅니다. CloudWatch integration은 단순한 전송 코드가 아니라 “운영자가 무엇으로 검색할 것인가”를 정하는 코드입니다.
Leader: provider를 고르면 storage와 runtime도 같이 따라온다
섹션 제목: “Leader: provider를 고르면 storage와 runtime도 같이 따라온다”leader 0.4.0에서 다시 확인한 교훈은 단순합니다. leader provider 하나를 고르는 일은 실제로는 storage와 runtime을
같이 고르는 일입니다.
예를 들어 Redis backend를 고르면 Redis 장애 감지와 metric을 봐야 합니다. Kubernetes Lease를 고르면 Kubernetes client runtime이 따라옵니다. DynamoDB를 고르면 AWS 권한, table capacity, latency가 같이 따라옵니다.
leader election -> provider 선택 -> storage 선택 -> client/runtime 선택 -> metric/log/alert 선택Kubernetes K3s test runtime 충돌 사례도 이 흐름에서 나왔습니다. Fabric8 Kubernetes client가 기대하는 Vert.x
runtime line과 test runtime classpath가 맞지 않으면, leader election 로직을 검증하기도 전에 method mismatch가
터질 수 있습니다.
운영에서는 provider별로 봐야 할 값이 다릅니다.
| provider/storage | 먼저 볼 신호 | 장애 때 확인할 것 |
|---|---|---|
| Redis | lock 갱신 latency, Redis connection error | Redis failover, network partition, TTL 설정 |
| SQL/Exposed | lock row update latency, transaction error | DB lock wait, isolation, connection pool |
| Kubernetes Lease | lease renew 실패, API server latency | service account 권한, Fabric8/runtime line |
| DynamoDB | conditional write 실패, throttling | IAM, capacity, region, table key 설계 |
여기서 “분산 leader election을 쓴다”는 말은 너무 넓습니다. 실제 서비스에는 “Redis에 lock을 잡는 leader”, “Kubernetes Lease를 갱신하는 leader”, “DynamoDB conditional write로 대표를 정하는 leader”가 들어갑니다. 각 방식마다 장애 신호도 다릅니다.
metric 이름은 프로젝트마다 달라질 수 있지만, 운영자가 보고 싶은 질문은 거의 같습니다.
현재 leader는 누구인가?마지막 renew는 언제 성공했나?renew 실패가 몇 번 연속 발생했나?leader 전환이 너무 자주 일어나지는 않나?backend latency가 평소보다 늘었나?이 질문에 답할 수 없다면 leader election이 있어도 운영하기 어렵습니다. “대표는 뽑힌다”와 “대표가 왜 바뀌었는지 알 수 있다”는 다른 문제입니다.
production checklist
섹션 제목: “production checklist”dependencies 1.3.0으로 올린 뒤 운영 관점에서 확인할 항목을 간단히 정리하면 다음과 같습니다.
| 영역 | 확인할 질문 |
|---|---|
| Exposed cache | /actuator/health에서 cache mode, queue depth, flush 상태를 볼 수 있는가? |
| AWS metrics | namespace와 dimension 이름이 service/environment 기준으로 정리되어 있는가? |
| AWS logs | log group과 stream 이름만 보고 서비스와 인스턴스를 찾을 수 있는가? |
| Leader | provider/storage별 metric과 장애 신호를 구분해서 보고 있는가? |
| Runtime classpath | Kubernetes, Ktor, AWS SDK처럼 runtime line이 민감한 dependency를 dependencyInsight로 확인했는가? |
확인 명령은 상황에 맞게 좁혀서 쓰면 됩니다.
curl -s http://localhost:8080/actuator/health | jq '.components.exposedCache'
./gradlew dependencyInsight \ --dependency vertx-web-client \ --configuration k8sTestRuntimeClasspath
./gradlew dependencyInsight \ --dependency software.amazon.awssdk \ --configuration runtimeClasspath마무리
섹션 제목: “마무리”운영에서 좋은 변경은 꼭 화려하지 않습니다. queueDepth가 보이고, flushJobRunning이 보이고, metric namespace가
정리되고, provider별 runtime 충돌을 빨리 확인할 수 있으면 실제 장애 대응 시간이 줄어듭니다.
버그는 사라지지 않습니다. 대신 빨리 티 나게 만들 수는 있습니다. dependencies 1.3.0에서 운영 쪽으로 가져갈
핵심은 그 부분입니다. 문제가 터졌을 때 “어디부터 봐야 하지?”라는 시간을 줄이는 것. 그게 새 기능보다 더 고마운
날이 있습니다.
시리즈 글
섹션 제목: “시리즈 글”처음 bluetape4k-dependencies를 가져오는 방법부터 보고 싶다면
사용 가이드를 먼저 읽으면 됩니다.
- bluetape4k-dependencies 1.3.0 활용기 Part 1: 새 기능도 넣고 똥도 치우고
- bluetape4k-dependencies 1.3.0 활용기 Part 2: 서비스 조합하기
- bluetape4k-dependencies 1.3.0 활용기 Part 3: 운영에서 티 나는 변화
- bluetape4k-dependencies 1.3.0 활용기 Part 4: 입력 경계에서 치운 똥들
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.