안정 과 고도화 사이에서 — *변화하지 않으면 죽고, 변화하면 깨진다* 의 균형점에 대한 고찰
어떤 시스템은 안정 하다. 5년째 같은 코드, 같은 알림, 같은 절차. 그리고 같이 늙는다. 어떤 시스템은 고도화 한다. 매주 새 기능, 새 의존성, 새 알림. 그리고 알 수 없게 깨진다. 좋은 시스템은 둘 다 한다. 어떻게 그게 가능한지 — 그게 이 글의 주제.
TL;DR — 한 줄 결론
안정성은 정적 균형 이 아니라 동적 평형 이다. 변화 없는 시스템은 안정한 게 아니라 *쇠퇴 한 것. *변화하는 시스템이 안정하려면 *변화 자체가 안전한 패턴 으로 일어나야 한다. 이 글은 그 *변화의 안전 패턴 9 가지 — 알림은 증상이 아니라 추적의 시작점, 점진 교체와 인터페이스 우선, 멱등성은 안정성의 형제, 부채는 *누적 이 아니라 드러내야 갚을 수 있는 것, 그리고 가장 중요한 “잘 돌고 있다” 의 함정.
1. 안정 은 변화 없음 이 아니다
대부분의 “안정적인” 시스템을 가까이서 들여다보면 충격적 이다. 겉으로는 변화 없음, 속으로는 끊임없는 보정. SRE 의 Error Budget 개념이 이걸 정확히 잡았다 — 허용된 실패율 안에서 끊임없이 실패하고 복구 하는 것이 안정 의 진짜 모습.
바깥에서 본 안정 시스템: 속에서 본 같은 시스템:
───────────────────────── ─────────────────────────
✓ 99.9% 가용성 ✓ 매일 *수십 번 의 에러*
✓ 매월 SLA 충족 ✓ 매일 *수십 번 의 자동 복구*
✓ 사고 0 건 ✓ *수십 번 의 알림 → 자동 해소*
✓ 사용자 만족 ✓ *끝없이 깎이는 에러 예산*
안정 이 변화 없음 이라는 환상 은 시스템이 안정하다고 믿게 하는 보고서 가 만든다. 실제로는 끊임없이 깨지고 끊임없이 보정 하는 동적 평형 이다. 그 평형이 무너지는 신호 — 알림 의 빈도가 갑자기 오르거나, 해소까지 시간이 갑자기 길어지거나, 옛 알림이 같은 모양으로 다시 오는 것.
안정 시스템 의 첫 번째 특징 : 변화를 *측정 가능한 형태 로 수용한다. SLO/SLA, 에러 예산, p99 latency, MTTR — 이 숫자들이 *변화 의 안전한 그릇 이다.
2. 고도화 는 기능 추가 가 아니다
“이 시스템 고도화 됐어요?” 라는 질문에 기능 N 개 추가했어요 라고 답하면 반쯤 틀린 답이다. 진짜 고도화 는 같은 일을 더 안전하게, 더 빠르게, 더 적은 비용으로 가능하게 만드는 것이다.
| 가짜 고도화 | 진짜 고도화 |
|---|---|
| 기능 5개 추가 | 기능 1개를 5명이 동시에 안전하게 변경 가능 |
| Redis 캐시 도입 | 캐시 일관성 깨지면 자동 감지·복구 |
| MSA 전환 | 서비스 간 경계 가 명확해 *모르고도 변경 가능* |
| AI 통합 | AI 호출 실패시 정상 응답 fallback |
| 새 알림 50개 | 기존 알림 *false positive 가 줄어 알림 전체 의 신뢰 가 오른다* |
가짜 고도화는 복잡도 만 증가 한다. 진짜 고도화는 복잡도 를 *흡수 하는 추상화* 를 같이 가져온다. 추상화 없는 기능 추가 가 기술 부채 의 정확한 정의.
3. *알림 은 증상이 아니라 *추적 의 시작점**
이게 가장 오해 가 많은 부분. 알림이 resolved 되면 원인 도 사라졌다 고 믿는 함정 — 불꽃이 한 번 튀었는데 비 와서 꺼진 상태 와 기름이 뿌려진 채로 비만 멈춘 상태 를 구분 못 한다.
3.1 KubeAPIErrorBudgetBurn (warning, auto-resolved) 의 진짜 의미
이 알림은 SLO 의 burn rate 가 임계를 잠시 넘었다 는 뜻. 원인은 그대로, 임계 만 다시 내려간 상태. 자동 해소 됐다고 치워두면 — 다음 critical 로 승격 될 때 그 사이 누적된 부채 가 한 번에 터진다.
좋은 패턴 :
- resolved 된 알림도 원인 추적 시도 (5분 만 투자해도 보통 답 나옴)
- 같은 알림이 반복적으로 짧게 fire 되면 근본 원인 추적 우선순위 ↑
- 추적 결과를 알림 자체에 메모 로 첨부 — 다음 사람 이 같은 추적을 반복 안 하게
3.2 증상 과 원인 의 거리
| 증상 (알림 의 표면) | 진짜 원인 (보통 수 layer 깊이) |
|---|---|
KubeAPIErrorBudgetBurn |
etcd p99 8.75s → fsync 디스크 → spinning HDD 위 stateful |
KubeJobFailed (etcd-leader-pin) |
cronjob endpoint status fail → 새 노드 가입 흔적 의 ghost member |
OOMKilled (node-agent x44) |
kopia 청크 처리 spike → cgroup limit 너무 작음 (노드 메모리 자체는 여유) |
CORS 403 (production) |
application-prod.yaml 의 옛 도메인 오타 — 코드 변경 없이 yaml 한 줄 |
원인은 항상 알림의 텍스트 밖 에 있다. 알림은 어디 부터 파야 할지의 출발점 이지 결론 이 아니다.
4. 부채 는 *누적 이 아니라 *드러내야 갚을 수 있는 것**
기술 부채는 모아두면 *복리 로 늘어난다* 는 비유 — 맞긴 한데 반쪽 만 맞다. 모아둔 부채는 *드러나야 갚을 수 있다. 드러나기 전에는 *존재 조차 모른다.
- 옛 도메인 오타 가 운영 yaml 에 수 주 동안 박혀있어도 — 운영이 옛 도메인 으로 들어오는 한 안 드러난다. 도메인 변경 시 즉시 폭발.
- cgroup limit 부족 이 4월 부터 있었어도 — 백업 워크로드 가 작을 때 는 안 드러난다. 데이터 늘어나면 OOM 시작.
- etcd ghost member 가 수 주 전 부터 있어도 — 그 IP 에 접속 시도 하는 코드 가 없으면 안 드러난다. CronJob 추가 시 폭발.
부채 의 표면화 패턴 — 알림 인프라, 주기적 진단, 카오스 엔지니어링, upgrade 시도 시 에러 메시지. 이 표면화 메커니즘 자체가 고도화 의 핵심 자산 이다.
부채 가 모이는 시스템 보다 부채 가 보이는 시스템 이 훨씬 안정 하다.
5. 점진 교체 패턴 — interface + Router + ConditionalOnProperty
레거시 부품을 새 부품으로 한 번에 바꾸지 않고 점진적으로 교체하는 패턴. 이게 고도화 의 안전한 모양 이다.
// 1. 인터페이스 정의 — 누구나 구현 가능한 추상화
public interface ExternalChannelClient {
Provider getProvider();
SyncResult sync(String externalId);
}
// 2. fake / default 구현 — 즉시 동작, 가짜 성공률
@Component
class FakeChannelClient implements ExternalChannelClient {
public Provider getProvider() { return null; } // fallback
public SyncResult sync(String id) { /* 80% 가짜 성공 */ }
}
// 3. 실 SDK 구현 — *조건부* 활성화
@Component
@ConditionalOnProperty(name = "channel.real-sdk.enabled", havingValue = "true")
class RealChannelClient implements ExternalChannelClient {
public Provider getProvider() { return Provider.REAL; }
public SyncResult sync(String id) { /* 진짜 API 호출 */ }
}
// 4. 라우터 — 자동 디스패치
@Component
class ChannelClientRouter {
public ChannelClient resolve(Provider p) {
return providerClients.getOrDefault(p, fallback);
}
}
이 4 단계 패턴 이 주는 것 :
- 지금 동작 (fake 가 즉시 응답)
- 점진 교체 (real 을 ConditionalOnProperty 로 하나씩 켬)
- 롤백 즉시 (real flag 만 끄면 fake 로 회귀)
- 기존 코드 변경 0 (Router 는 interface 만 보므로)
이게 진짜 고도화 다. 기능을 추가 하는 게 아니라, 추가 와 제거 가 똑같이 안전한 구조 를 만드는 것.
6. 멱등성 은 안정성 의 형제
같은 작업을 *몇 번 해도 결과가 같음. 이게 반복 안전성 의 정의 이고 분산 시스템 의 기본기.
6.1 멱등성 이 안정성 에 미치는 영향
- 알림 처리 자동화 가 한 번 더 돌아도 문제 없음
- 시드 마이그레이션이 재실행 돼도 중복 없음
- 외부 API 가 재시도 해도 중복 결제 없음
- CI/CD 가 같은 커밋 을 두 번 배포 해도 상태 안정
6.2 멱등 마이그레이션 의 예
INSERT INTO product_skus (product_id, sku_code, additional_price, stock_quantity, is_active)
SELECT 1, sku.code, sku.add_price, sku.stock, true
FROM (VALUES ('TSHIRT-RED-M', 0, 20), ...) AS sku(code, add_price, stock)
WHERE EXISTS (SELECT 1 FROM products WHERE id = 1)
AND NOT EXISTS (SELECT 1 FROM product_skus WHERE sku_code = sku.code);
EXISTS / NOT EXISTS 가드 두 개 — 상품이 있을 때 만, 그리고 같은 SKU 가 없을 때 만 INSERT. 반복 실행 해도 중복 0. 마이그레이션 자체가 재실행 가능 해야 재배포 와 롤백 이 안전해진다.
비-멱등 작업 을 멱등하게 바꾸는 것 — 모든 고도화 의 출발선.
7. “잘 돌고 있다” 의 함정
가장 위험한 말 : “지금 잘 돌고 있어요”. 잘 돌고 있는 그 밑 에 수 주 동안 :
- OOM 사이클 이 44 번 돌고 있고
- 옛 etcd ghost member 가 누군가 건드릴 때 까지 기다리고 있고
- fail2ban 이 자동 스크립트 의 SSH 시도 를 주기적으로 차단 하고 있고
- 옛 도메인 오타 가 운영 yaml 에 박혀있는 것이다.
잘 돌고 있다 는 증상 이 없다 와 원인 이 없다 를 혼동한 표현. 증상 이 없을 때 일수록 의도적으로 깊이 들여다봐야 한다 — 그게 카오스 엔지니어링 의 핵심 동기.
정기 점검 체크리스트 (직접 짠 것)
| 빈도 | 점검 |
|---|---|
| 매일 | resolved 된 알림 의 상위 패턴 (왜 떴나, 같은 게 또 떴나) |
| 주간 | CrashLoopBackOff / RestartCount 가 1 이상인 pod 의 원인 |
| 월간 | etcd member list, DHCP reservation, 외부 API 의 deprecated 라이브러리 |
| 분기 | 부하 테스트 재실행 — 6개월 전 보다 좋아졌나, 나빠졌나, 왜 |
| 연간 | 시스템 의 전체 구조 다이어그램 다시 그리기 — 그 사이 자라난 부분 발견 |
8. *협업 의 진화 — 시스템 의 *외부 표면**
흥미로운 패턴 — 시스템 의 안정성 이 그 시스템 을 만지는 사람들 의 협업 패턴 에도 의존한다.
4 개의 자동화 에이전트 가 같은 클러스터 에 동시 접근 하면서 같은 알림에 독립 반응 했고, 그 결과 내부 LAN 의 SSH 시도 가 자동 차단 시스템 에 잡혔다.
해결책 : 조정 hook — 작업 전에 같은 작업이 다른 에이전트 에서 진행 중 인지 확인 (TTL 기반 registry). 60초 안에 같은 작업 키 가 등록되어 있으면 블록 + 안내 메시지.
# 같은 (action, target) 키가 60s TTL 안에 *다른 세션*에서 시작됐으면 exit 2
if conflict := find_conflict(key, my_session, ttl=60):
sys.stderr.write(f"BLOCKED — '{conflict.session}' 이 {ago}s 전 시작")
sys.exit(2)
시스템 자체의 코드 도 운영 인프라의 일부 라는 인식 전환. 고도화 한 시스템 일수록 운영 에이전트 가 다양해 지고, 에이전트 끼리의 조정 메커니즘 도 시스템 의 본체 못지않게 중요해진다.
9. 변화 의 안전한 모양 6 가지
이 글의 시작에서 약속한 “변화 자체가 안전한 패턴”. 정리 :
▸ 1. 알림 인프라 우선 — 부채를 모이게 두지 말고 드러나게 만들 것. SLO / burn rate / RED 메트릭 / 분산 trace.
▸ 2. 인터페이스 우선 — 교체 가능한 자리 를 교체 전에 만들 것. ConditionalOnProperty / Strategy / Router.
▸ 3. 멱등 디폴트 — 모든 반복 가능한 작업 (마이그레이션, 시드, sync) 은 처음부터 멱등하게.
▸ 4. 점진 롤아웃 — feature flag, canary, 0 → 1% → 10% → 100%. 한 번에 100% 는 실패 의 시작점.
▸ 5. 옵저버빌리티 가 문서 보다 우선 — 문서 는 거짓말 하지만 메트릭 은 안 한다. 진짜 다이어그램 은 Grafana 대시보드.
▸ 6. 조정 메커니즘 의 명시적 설계 — 누가 / 언제 / 무엇 을 변경하는지 시스템 자체가 안다. 사람 의 기억 에 의존 금지.
10. *마무리 — 안정 과 고도화 는 서로의 *조건**
흔히 안정 vs 고도화 가 트레이드오프 라고 한다. 일부 맞다 — 지금 이 순간 의 변경은 지금 이 순간 의 위험을 늘린다.
하지만 시간 축 을 길게 보면 둘 은 서로 의 조건 이다 :
- 안정 한 시스템 만이 고도화 를 감당할 여유 가 있다 — 불나는 집 에서 리팩토링 못 한다.
- 고도화 한 시스템 만이 안정 을 유지 할 수단 을 갖는다 — 옛 패턴 의 시스템 은 외부 환경 변화 에 부서진다.
좋은 시스템 운영자 는 어느 쪽을 더 추구하느냐 가 아니라 둘 의 균형이 무너지는 신호 를 더 빨리 감지하느냐 로 결정된다. 그 신호는 알림 의 빈도, 해소까지 시간, 반복 패턴, 원인 추적 의 깊이 같은 측정 가능한 것들.
변화 없이는 죽고, 변화 만 으로는 깨진다. 그 사이 의 동적 평형 — 그게 시스템 의 진짜 모습 이자, 시스템 운영 이라는 직업 의 본질.
부록 — 오늘 의 한 줄 체크
업무 마치며 5 분 만 투자해서 :
- 오늘 자동 해소 된 알림 — 다 봤나? 그 중에 반복 패턴 있나?
- 오늘 한 변경 — 롤백 가능 한가? 문서 / 메트릭 으로 왜 했는지 다음 사람이 알 수 있는가?
- 오늘 추가한 의존성 — 교체 가능 한가? 인터페이스 뒤에 숨었는가?
- 오늘 의 기술 부채 — 어디 기록 됐는가? 알림 / 백로그 / 다이어그램 — 어디든.
이 5 분 이 다음 사고를 막는 가장 싼 비용. 그리고 그게 진짜 SRE 의 일 이다.
변화를 두려워하지 말고, 변화 의 안전한 패턴을 두려워하라.